class Notification(RanaModule): """This module provides notification support.""" def __init__(self, *args, **kwargs): RanaModule.__init__(self, *args, **kwargs) self.notificationText = "" self.timeout = 5000 self.position = 'middle' self.expirationTimestamp = time.time() self.draw = False self.redrawn = None self.wipOverlayEnabled = Signal() self._showWorkInProgressOverlay = False self.workStartTimestamp = None self._wipOverlayText = "" # this indicates if the notification about # background processing should be shown self._tasks = {} self._tasksLock = threading.RLock() ## for WiP overlay testing #self._tasks = { # "foo" : ("foo", None), # "bar" : ("bar", None), # "baz" : ("baz", None) #} # key is an unique task name (unique for each instance of a task) # and value is a (status, progress) tuple self.tasksChanged = Signal() # connect thread manager signals to task status changes threadMgr.threadStatusChanged.connect(self.setTaskStatus) threadMgr.threadProgressChanged.connect(self.setTaskProgress) threadMgr.threadRemoved.connect(self.removeTask) # also with GTK GUI, assure screen is redrawn properly # when WiP overlay changes if gs.GUIString == "GTK": self.wipOverlayEnabled.connect(self._doRefresh) self.tasksChanged.connect(self._doRefresh) # we handle notification only with the GTK GUI and when the device module does not # support showing them if not self.modrana.dmod.hasNotificationSupport(): self.modrana.notificationTriggered.connect(self._startCustomNotificationCB) def _doRefresh(self, ignore): self.set('needRedraw', True) def handleMessage(self, message, messageType, args): """the first part is the message, that will be displayed, there can also by some parameters, delimited by # NEW: you can also use a message list for the notification first goes 'm', the message and then the timeout in seconds EXAMPLE: ml:notification:m:Hello world!;5 """ if messageType == 'ml' and message == 'm': # advanced message list based notification if args: timeout = self.timeout if len(args) >= 2: # convert to float, multiply by 1000 to convert to ms # and then convert to integer number of ms timeout = int(float(args[1])*1000) messageText = args[0] self.modrana.notify(messageText, timeout) elif messageType == 'ml' and message == 'workInProgressOverlay': if args: if args[0] == "enable": self._startWorkInProgressOverlay() if args[0] == "disable": self._stopWorkInProgressOverlay() elif messageType and message == "cancelTask": if args: self.cancelTask(args) else: parameterList = message.split('#') if len(parameterList) >= 2: messageText = parameterList[0] timeout = self.timeout self.modrana.notify(messageText, timeout) if len(parameterList) == 2: try: # override the default timeout and convert to ms timeout = int(parameterList[1])*1000 except Exception: self.log.exception("wrong timeout, using default 5 seconds") self.modrana.notify(messageText, timeout) else: self.log.error("wrong message: %s", message) def setTaskStatus(self, taskName, status): """Set textual status of the given WiP task :param taskName: task to modify :type taskName: str :param status: textual task status :type status: str """ with self._tasksLock: oldStatus, progress = self._tasks.get(taskName, (None, None)) # replace the task with updated one self._tasks[taskName] = (status, progress) self.tasksChanged(self._tasks) if status: self._startWorkInProgressOverlay() def setTaskProgress(self, taskName, progress): """Set numeric progress of the given WiP task :param taskName: task to modify :type taskName: str :param progress: numeric task progress in the range of 0.0 to 1.0 :type progress: float """ with self._tasksLock: status, oldProgress = self._tasks.get(taskName, (None, None)) # replace the task with updated one self._tasks[taskName] = (status, progress) self.tasksChanged(self._tasks) if progress is not None: self._startWorkInProgressOverlay() def removeTask(self, taskName): """Remove given task from WiP tracking :param taskName: name of the task to cancel :type taskName: str """ with self._tasksLock: try: del self._tasks[taskName] self.tasksChanged(self._tasks) except KeyError: self.log.error("error, can't remove unknown task: %s", taskName) if self._tasks == {}: self._stopWorkInProgressOverlay() def cancelTask(self, taskName): """Cancel a task - this both cancels the callback for the thread handling the task and removes the task from tracking :param taskName: task to cancel :type taskName: str """ threadMgr.cancel_thread(taskName) # and finally stop tracking the task self.removeTask(taskName) self.log.info("task %s has been cancelled", taskName) def _startWorkInProgressOverlay(self): """start background work notification""" if not self._showWorkInProgressOverlay: self._showWorkInProgressOverlay = True self.workStartTimestamp = time.time() self.wipOverlayEnabled(True) def _stopWorkInProgressOverlay(self): """stop background work notification""" self._showWorkInProgressOverlay = False self.wipOverlayEnabled(False) #def getWorkInProgressOverlayText(self): # elapsedSeconds = int(time.time() - self.workStartTimestamp) # if elapsedSeconds: # 0s doesnt look very good :) # return "%s %d s" % (self.wipOverlayText, elapsedSeconds) # else: # return self.wipOverlayText def drawWorkInProgressOverlay(self, cr): proj = self.m.get('projection', None) # we also need the projection module viewport = self.get('viewport', None) menus = self.m.get('menu', None) with self._tasksLock: if self._tasks and proj and viewport and menus: # we need to have both the viewport and projection modules available # also the menu module for the text taskCount = len(self._tasks) # background cr.set_source_rgba(0.5, 0.5, 1, 0.5) (sx, sy, w, h) = viewport itemHeight = h * 0.2 (bx, by, bw, bh) = (0, 0, w, itemHeight * taskCount) cr.rectangle(bx, by, bw, bh) cr.fill() taskIndex = 0 for taskName, taskState in six.iteritems(self._tasks): # cancel button coordinates cbdx = min(w, h) / 5.0 cbdy = cbdx cbx1 = (sx + w) - cbdx cby1 = sy + cbdy * taskIndex # cancel button self.drawCancelButton(cr, coords=(cbx1, cby1, cbdx, cbdy), taskName=taskName) status, progress = taskState # draw the text border = min(w / 20.0, h / 20.0) menus.showText(cr, status, bx + border, by + border + itemHeight * taskIndex, bw - 2 * border - cbdx, 30, "white") taskIndex += 1 def drawCancelButton(self, cr, coords=None, taskName=None): """draw the cancel button TODO: this and the other context buttons should be moved to a separate module, named contextMenu or something in the same style""" menus = self.m.get('menu', None) click = self.m.get('clickHandler', None) if menus and click: if coords: #use the provided coords (x1, y1, dx, dy) = coords else: # use the bottom right corner (x, y, w, h) = self.get('viewport') dx = min(w, h) / 5.0 dy = dx x1 = (x + w) - dx y1 = y # the cancel button sends a cancel message to onlineServices # to disable currently running operation menus.drawButton(cr, x1, y1, dx, dy, '#<span foreground="red">cancel</span>', "generic:;0.5;;0.5;;", '') if taskName: message = "ms:notification:cancelTask:%s" % taskName click.registerXYWH(x1, y1, dx, dy, message, layer=2) else: click.registerXYWH(x1, y1, dx, dy, "onlineServices:cancelOperation", layer=2) def _startCustomNotificationCB(self, message, ms_timeout, icon=""): self.position = 'middle' self.notificationText = message self.expirationTimestamp = time.time() + ms_timeout/1000.0 self.draw = True # enable drawing of notifications self.set('needRedraw', True) # make sure the notification is displayed def drawMasterOverlay(self, cr): """this function is called by the menu module, both in map mode and menu mode -> its bit of a hack, but we can """ self._drawNotification(cr) if self._showWorkInProgressOverlay: self.drawWorkInProgressOverlay(cr) def _drawNotification(self, cr): """Draw the notifications on the screen on top of everything.""" if time.time() <= self.expirationTimestamp: menus = self.m.get('menu', None) proj = self.m.get('projection', None) viewport = self.get('viewport', None) if not proj or not menus or not viewport: return x, y, w, h = viewport border = min(w / 20.0, h / 20.0) proj = self.m.get('projection', None) (x1, y1) = proj.screenPos(0.5, 0.5) # middle fo the screen cr.set_source_rgba(0, 0, 1, 0.45) # transparent blue font_size = 30 # compute rendered text size textw, texth = menus.measureWrappedText(cr, self.notificationText, w - border * 2, font_size) # the background rectangle is as large as the text + borders rw = textw + 2 * border rh = texth + 2 * border # center the rectangle & text to be in the middle of the screen rx = (w - rw) / 2.0 ry = (h - rh) / 2.0 tx = rx + border ty = ry + border # draw the background rectangle cr.set_line_width(2) cr.set_source_rgba(0, 0, 1, 0.45) # transparent blue cr.rectangle(rx, ry, rw, rh) # create the transparent background rectangle cr.fill() # draw the text if menus and viewport and proj: menus.showWrappedText(cr, self.notificationText, tx, ty, textw, font_size, "white") else: self.draw = False # we are finished, disable drawing notifications
class Notification(RanaModule): """This module provides notification support.""" def __init__(self, m, d, i): RanaModule.__init__(self, m, d, i) self.notificationText = "" self.timeout = 5 self.position = 'middle' self.expirationTimestamp = time.time() self.draw = False self.redrawn = None self.wipOverlayEnabled = Signal() self._showWorkInProgressOverlay = False self.workStartTimestamp = None self._wipOverlayText = "" # this indicates if the notification about # background processing should be shown self._tasks = {} self._tasksLock = threading.RLock() ## for WiP overlay testing #self._tasks = { # "foo" : ("foo", None), # "bar" : ("bar", None), # "baz" : ("baz", None) #} # key is an unique task name (unique for each instance of a task) # and value is a (status, progress) tuple self.tasksChanged = Signal() # connect thread manager signals to task status changes threadMgr.threadStatusChanged.connect(self.setTaskStatus) threadMgr.threadProgressChanged.connect(self.setTaskProgress) threadMgr.threadRemoved.connect(self.removeTask) # also with GTK GUI, assure screen is redrawn properly # when WiP overlay changes if gs.GUIString == "GTK": self.wipOverlayEnabled.connect(self._doRefresh) self.tasksChanged.connect(self._doRefresh) def _doRefresh(self, ignore): self.set('needRedraw', True) def handleMessage(self, message, messageType, args): """the first part is the message, that will be displayed, there can also by some parameters, delimited by # NEW: you can also use a message list for the notification first goes 'm', the message and then the timeout in seconds EXAMPLE: ml:notification:m:Hello world!;5 """ if messageType == 'ml' and message == 'm': # advanced message list based notification if args: timeout = self.timeout if len(args) >= 2: timeout = float(args[1]) messageText = args[0] self.handleNotification(messageText, timeout) elif messageType == 'ml' and message == 'workInProgressOverlay': if args: if args[0] == "enable": self._startWorkInProgressOverlay() if args[0] == "disable": self._stopWorkInProgressOverlay() elif messageType and message == "cancelTask": if args: self.cancelTask(args) else: parameterList = message.split('#') if len(parameterList) >= 2: messageText = parameterList[0] timeout = self.timeout self.handleNotification(messageText, timeout) if len(parameterList) == 2: try: timeout = int(parameterList[1]) # override the default timeout except Exception: import sys e = sys.exc_info()[1] print("notification: wrong timeout, using default 5 seconds") print(e) self.handleNotification(messageText, timeout) else: print("notification: wrong message: %s" % message) def setTaskStatus(self, taskName, status): """Set textual status of the given WiP task :param taskName: task to modify :type taskName: str :param status: textual task status :type status: str """ with self._tasksLock: oldStatus, progress = self._tasks.get(taskName, (None, None)) # replace the task with updated one self._tasks[taskName] = (status, progress) self.tasksChanged(self._tasks) if status: self._startWorkInProgressOverlay() def setTaskProgress(self, taskName, progress): """Set numeric progress of the given WiP task :param taskName: task to modify :type taskName: str :param progress: numeric task progress in the range of 0.0 to 1.0 :type progress: float """ with self._tasksLock: status, oldProgress = self._tasks.get(taskName, (None, None)) # replace the task with updated one self._tasks[taskName] = (status, progress) self.tasksChanged(self._tasks) if progress is not None: self._startWorkInProgressOverlay() def removeTask(self, taskName): """Remove given task from WiP tracking :param taskName: name of the task to cancel :type taskName: str """ with self._tasksLock: try: del self._tasks[taskName] self.tasksChanged(self._tasks) except KeyError: print("notification: error, can't remove unknown task: %s" % taskName) if self._tasks == {}: self._stopWorkInProgressOverlay() def cancelTask(self, taskName): """Cancel a task - this both cancels the callback for the thread handling the task and removes the task from tracking :param taskName: task to cancel :type taskName: str """ threadMgr.cancel_thread(taskName) # and finally stop tracking the task self.removeTask(taskName) print("notification: task %s has been cancelled" % taskName) def _startWorkInProgressOverlay(self): """start background work notification""" if not self._showWorkInProgressOverlay: self._showWorkInProgressOverlay = True self.workStartTimestamp = time.time() self.wipOverlayEnabled(True) def _stopWorkInProgressOverlay(self): """stop background work notification""" self._showWorkInProgressOverlay = False self.wipOverlayEnabled(False) #def getWorkInProgressOverlayText(self): # elapsedSeconds = int(time.time() - self.workStartTimestamp) # if elapsedSeconds: # 0s doesnt look very good :) # return "%s %d s" % (self.wipOverlayText, elapsedSeconds) # else: # return self.wipOverlayText def drawWorkInProgressOverlay(self, cr): proj = self.m.get('projection', None) # we also need the projection module viewport = self.get('viewport', None) menus = self.m.get('menu', None) with self._tasksLock: if self._tasks and proj and viewport and menus: # we need to have both the viewport and projection modules available # also the menu module for the text taskCount = len(self._tasks) # background cr.set_source_rgba(0.5, 0.5, 1, 0.5) (sx, sy, w, h) = viewport itemHeight = h * 0.2 (bx, by, bw, bh) = (0, 0, w, itemHeight * taskCount) cr.rectangle(bx, by, bw, bh) cr.fill() taskIndex = 0 for taskName, taskState in six.iteritems(self._tasks): # cancel button coordinates cbdx = min(w, h) / 5.0 cbdy = cbdx cbx1 = (sx + w) - cbdx cby1 = sy + cbdy * taskIndex # cancel button self.drawCancelButton(cr, coords=(cbx1, cby1, cbdx, cbdy), taskName=taskName) status, progress = taskState # draw the text border = min(w / 20.0, h / 20.0) menus.showText(cr, status, bx + border, by + border + itemHeight * taskIndex, bw - 2 * border - cbdx, 30, "white") taskIndex += 1 def drawCancelButton(self, cr, coords=None, taskName=None): """draw the cancel button TODO: this and the other context buttons should be moved to a separate module, named contextMenu or something in the same style""" menus = self.m.get('menu', None) click = self.m.get('clickHandler', None) if menus and click: if coords: #use the provided coords (x1, y1, dx, dy) = coords else: # use the bottom right corner (x, y, w, h) = self.get('viewport') dx = min(w, h) / 5.0 dy = dx x1 = (x + w) - dx y1 = y # the cancel button sends a cancel message to onlineServices # to disable currently running operation menus.drawButton(cr, x1, y1, dx, dy, '#<span foreground="red">cancel</span>', "generic:;0.5;;0.5;;", '') if taskName: message = "ms:notification:cancelTask:%s" % taskName click.registerXYWH(x1, y1, dx, dy, message, layer=2) else: click.registerXYWH(x1, y1, dx, dy, "onlineServices:cancelOperation", layer=2) def handleNotification(self, message, timeout=None, icon=""): """Handle a notification request As on some platforms, such as on Maemo 5 Fremantle, system notifications should only be triggered from the main thread, we pass the notification request to the main loop, to be dispatched once the main GUI thread becomes idle """ cron = self.m.get("cron", None) if cron: cron.addIdle(self._dispatchNotification, [message, timeout, icon]) def _dispatchNotification(self, message, timeout, icon): """Dispatch a notification using the most optimal method for the current device & platform combination NOTE: This method should be run from the main thread as there might be issues otherwise (such as the "Xlib: unexpected async reply" errors). """ # TODO: icon support if timeout is None: timeout = self.timeout print("notification: message: %s, timeout: %s" % (message, timeout)) # if some module sends a notification during init, the device module might not be loaded yet if self.modrana.dmod and self.modrana.gui: if self.dmod.hasNotificationSupport(): # use platform specific method print("notification@dmod: message: %s, timeout: %s" % (message, timeout)) self.dmod.notify(message, int(timeout) * 1000) elif self.modrana.gui.hasNotificationSupport(): self.modrana.gui.notify(message, timeout, icon) else: self._startCustomNotification(message, timeout, icon) else: self._startCustomNotification(message, timeout, icon) def _startCustomNotification(self, message, timeout, icon=""): print("notification: message: %s, timeout: %s" % (message, timeout)) self.position = 'middle' self.notificationText = message self.expirationTimestamp = time.time() + timeout self.draw = True # enable drawing of notifications self.set('needRedraw', True) # make sure the notification is displayed def drawMasterOverlay(self, cr): """this function is called by the menu module, both in map mode and menu mode -> its bit of a hack, but we can """ self.drawNotification(cr) if self._showWorkInProgressOverlay: self.drawWorkInProgressOverlay(cr) def drawNotification(self, cr): """Draw the notifications on the screen on top of everything.""" if time.time() <= self.expirationTimestamp: proj = self.m.get('projection', None) (x1, y1) = proj.screenPos(0.5, 0.5) # middle fo the screen cr.set_font_size(30) text = self.notificationText cr.set_source_rgba(0, 0, 1, 0.45) # transparent blue extents = cr.text_extents(text) (w, h) = (extents[2], extents[3]) (x, y) = (x1 - w / 2.0, y1 - h / 2.0) cr.set_line_width(2) cr.set_source_rgba(0, 0, 1, 0.45) # transparent blue (rx, ry, rw, rh) = (x - 0.25 * w, y - h * 1.5, w * 1.5, (h * 2)) cr.rectangle(rx, ry, rw, rh) # create the transparent background rectangle cr.fill() cr.set_source_rgba(1, 1, 1, 0.95) # slightly transparent white cr.move_to(x + 10, y) cr.show_text(text) # show the transparent notification text cr.stroke() cr.fill() else: self.draw = False # we are finished, disable drawing notifications
class WorkAreaScene(QtModule.QGraphicsScene): # # __init__ # def __init__(self, view): # QtModule.QGraphicsScene.__init__(self) # # Define signals for PyQt5 # if usePySide or usePyQt5: # self.startNodeConnector = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.traceNodeConnector = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.endNodeConnector = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.startNodeLink = Signal( ) #( QtModule.QGraphicsObject ) # QtModule.QGraphicsItem self.traceNodeLink = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.endNodeLink = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.onGfxNodeRemoved = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject ) self.onGfxLinkRemoved = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsObject ) self.nodeUpdated = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsItem ) self.gfxNodeParamChanged = Signal( ) #QtCore.pyqtSignal ( QtModule.QGraphicsItem, QtCore.QObject ) # self.view = view self.connectSignals() # # connectSignals # def connectSignals(self): if usePyQt4: QtCore.QObject.connect(self, QtCore.SIGNAL('selectionChanged()'), self.view.onSelectionChanged) QtCore.QObject.connect(self, QtCore.SIGNAL('startNodeLink'), self.view.onStartNodeLink) QtCore.QObject.connect(self, QtCore.SIGNAL('traceNodeLink'), self.view.onTraceNodeLink) QtCore.QObject.connect(self, QtCore.SIGNAL('endNodeLink'), self.view.onEndNodeLink) QtCore.QObject.connect(self, QtCore.SIGNAL('startNodeConnector'), self.view.onStartNodeConnector) QtCore.QObject.connect(self, QtCore.SIGNAL('traceNodeConnector'), self.view.onTraceNodeConnector) QtCore.QObject.connect(self, QtCore.SIGNAL('endNodeConnector'), self.view.onEndNodeConnector) QtCore.QObject.connect(self, QtCore.SIGNAL('onGfxNodeRemoved'), self.view.onRemoveNode) QtCore.QObject.connect(self, QtCore.SIGNAL('onGfxLinkRemoved'), self.view.onRemoveLink) else: self.selectionChanged.connect(self.view.onSelectionChanged) self.startNodeLink.connect(self.view.onStartNodeLink) self.traceNodeLink.connect(self.view.onTraceNodeLink) self.endNodeLink.connect(self.view.onEndNodeLink) self.startNodeConnector.connect(self.view.onStartNodeConnector) self.traceNodeConnector.connect(self.view.onTraceNodeConnector) self.endNodeConnector.connect(self.view.onEndNodeConnector) self.onGfxNodeRemoved.connect(self.view.onRemoveNode) self.onGfxLinkRemoved.connect(self.view.onRemoveLink)
class WorkAreaScene ( QtModule.QGraphicsScene ) : # # __init__ # def __init__ ( self, view ) : # QtModule.QGraphicsScene.__init__ ( self ) # # Define signals for PyQt5 # if usePySide or usePyQt5 : # self.startNodeConnector = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.traceNodeConnector = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.endNodeConnector = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.startNodeLink = Signal () #( QtModule.QGraphicsObject ) # QtModule.QGraphicsItem self.traceNodeLink = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.endNodeLink = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject, QtCore.QPointF ) self.onGfxNodeRemoved = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject ) self.onGfxLinkRemoved = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsObject ) self.nodeUpdated = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsItem ) self.gfxNodeParamChanged = Signal () #QtCore.pyqtSignal ( QtModule.QGraphicsItem, QtCore.QObject ) # self.view = view self.connectSignals () # # connectSignals # def connectSignals ( self ) : if usePyQt4 : QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'selectionChanged()' ), self.view.onSelectionChanged ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'startNodeLink' ), self.view.onStartNodeLink ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'traceNodeLink' ), self.view.onTraceNodeLink ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'endNodeLink' ), self.view.onEndNodeLink ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'startNodeConnector' ), self.view.onStartNodeConnector ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'traceNodeConnector' ), self.view.onTraceNodeConnector ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'endNodeConnector' ), self.view.onEndNodeConnector ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'onGfxNodeRemoved' ), self.view.onRemoveNode ) QtCore.QObject.connect ( self, QtCore.SIGNAL ( 'onGfxLinkRemoved' ), self.view.onRemoveLink ) else : self.selectionChanged.connect ( self.view.onSelectionChanged ) self.startNodeLink.connect ( self.view.onStartNodeLink ) self.traceNodeLink.connect ( self.view.onTraceNodeLink ) self.endNodeLink.connect ( self.view.onEndNodeLink ) self.startNodeConnector.connect ( self.view.onStartNodeConnector ) self.traceNodeConnector.connect ( self.view.onTraceNodeConnector ) self.endNodeConnector.connect ( self.view.onEndNodeConnector ) self.onGfxNodeRemoved.connect ( self.view.onRemoveNode ) self.onGfxLinkRemoved.connect ( self.view.onRemoveLink )