def insertStatusbarWidget(self, widget): w = QFrame() w.setLayout(QHBoxLayout()) w.layout().addWidget(widget) f = QFrame() f.setFrameStyle(QFrame.Plain | QFrame.VLine) w.layout().addWidget(f) self.ui.statusbar.insertPermanentWidget(0, w) return w
def addOverlayWidget(self, w, line, col=None, offset=0, minX=0): line -= 1 if line not in self.__overlayWidgets: cont = QFrame() cont.setStyleSheet("QFrame { background-color : transparent; }") cont.setLayout(QHBoxLayout()) cont.layout().setSpacing(20) cont.layout().setMargin(0) cont.layout().setSizeConstraint(QLayout.SetMinAndMaxSize) cont.setParent(self.viewport()) if col is None: col = self.lineLength(line) - 1 else: col = min(col, self.lineLength(line) - 1) self.__moveOverlayToLine(cont, line) pos = self.positionFromLineIndex(int(line), col) x = self.SendScintilla(Qsci.QsciScintilla.SCI_POINTXFROMPOSITION, 0, pos) cont.move(max(x + offset, minX), cont.y()) cont.resize(10, self.textHeight(line)) cont.show() self.__overlayWidgets[line] = cont w.setParent(self.__overlayWidgets[line]) w.resize(w.size().width(), self.textHeight(line)) self.__overlayWidgets[line].layout().addWidget(w)
class HtmlViewerWidget(QWidget): def __init__(self, parent): super(HtmlViewerWidget, self).__init__(parent) self.setLayout(QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.view = QWebView() self.view.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.frame = QFrame() self.frame.setLayout(QGridLayout()) self.frame.layout().setContentsMargins(0, 0, 0, 0) self.toolbar = QToolBar() self.spacer = QWidget() self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.copyAction = self.toolbar.addAction(QIcon(":/icons/clipboard"), "Copy Text") self.label = QLabel() self.closeAction = QPushButton(QIcon(":/icons/cancel"), "Close", self) self.spaceraction = self.toolbar.insertWidget(None, self.spacer) self.labelaction = self.toolbar.insertWidget(self.spaceraction, self.label) self.closeAction.pressed.connect(self.close) self.copyAction.triggered.connect(self.copy_text) self.layout().addWidget(self.frame) self.frame.layout().addWidget(self.toolbar) self.frame.layout().addWidget(self.view) self.frame.layout().addWidget(self.closeAction) self.effect = QGraphicsOpacityEffect() self.label.setGraphicsEffect(self.effect) self.anim = QPropertyAnimation(self.effect, "opacity") self.anim.setDuration(2000) self.anim.setStartValue(1.0) self.anim.setEndValue(0.0) self.anim.setEasingCurve(QEasingCurve.OutQuad) def copy_text(self): self.label.setText("Copied to clipboard") text = self.view.page().mainFrame().toPlainText() QApplication.clipboard().setText(text) self.anim.stop() self.anim.start() def showHTML(self, template, level, **data): html = templates.render_tample(template, **data) self.view.setHtml(html, templates.baseurl)
class HtmlViewerWidget(QWidget): def __init__(self, parent): super(HtmlViewerWidget, self).__init__(parent) self.setLayout(QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.view = QWebView() self.view.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.frame = QFrame() self.frame.setLayout(QGridLayout()) self.frame.layout().setContentsMargins(0, 0, 0, 0) self.toolbar = QToolBar() self.spacer = QWidget() self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.copyAction = self.toolbar.addAction(QIcon(":/icons/clipboard"), "Copy Text") self.label = QLabel() self.closeAction = self.toolbar.addAction(QIcon(":/icons/cancel"), "Close") self.spaceraction = self.toolbar.insertWidget(self.closeAction, self.spacer) self.labelaction = self.toolbar.insertWidget(self.spaceraction, self.label) self.closeAction.triggered.connect(self.close) self.copyAction.triggered.connect(self.copy_text) self.layout().addWidget(self.frame) self.frame.layout().addWidget(self.toolbar) self.frame.layout().addWidget(self.view) self.effect = QGraphicsOpacityEffect() self.label.setGraphicsEffect(self.effect) self.anim = QPropertyAnimation(self.effect, "opacity") self.anim.setDuration(2000) self.anim.setStartValue(1.0) self.anim.setEndValue(0.0) self.anim.setEasingCurve(QEasingCurve.OutQuad ) def copy_text(self): self.label.setText("Copied to clipboard") text = self.view.page().mainFrame().toPlainText() QApplication.clipboard().setText(text) self.anim.stop() self.anim.start() def showHTML(self, template, level, **data): html = templates.render_tample(template, **data) self.view.setHtml(html, templates.baseurl)
class QuickMenu(FramelessWindow): """ A quick menu popup for the widgets. The widgets are set using :func:`QuickMenu.setModel` which must be a model as returned by :func:`QtWidgetRegistry.model` """ #: An action has been triggered in the menu. triggered = Signal(QAction) #: An action has been hovered in the menu hovered = Signal(QAction) def __init__(self, parent=None, **kwargs): FramelessWindow.__init__(self, parent, **kwargs) self.setWindowFlags(Qt.Popup) self.__filterFunc = None self.__setupUi() self.__loop = None self.__model = QStandardItemModel() self.__triggeredAction = None def __setupUi(self): self.setLayout(QVBoxLayout(self)) self.layout().setContentsMargins(6, 6, 6, 6) self.__search = SearchWidget(self, objectName="search-line") self.__search.setPlaceholderText( self.tr("Search for widget or select from the list.") ) self.layout().addWidget(self.__search) self.__frame = QFrame(self, objectName="menu-frame") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) self.__frame.setLayout(layout) self.layout().addWidget(self.__frame) self.__pages = PagedMenu(self, objectName="paged-menu") self.__pages.currentChanged.connect(self.setCurrentIndex) self.__pages.triggered.connect(self.triggered) self.__pages.hovered.connect(self.hovered) self.__frame.layout().addWidget(self.__pages) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.__suggestPage = SuggestMenuPage(self, objectName="suggest-page") self.__suggestPage.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) self.__suggestPage.setIcon(icon_loader().get("icons/Search.svg")) if sys.platform == "darwin": view = self.__suggestPage.view() view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True) # Don't show the focus frame because it expands into the tab bar. view.setAttribute(Qt.WA_MacShowFocusRect, False) i = self.addPage(self.tr("Quick Search"), self.__suggestPage) button = self.__pages.tabButton(i) button.setObjectName("search-tab-button") button.setStyleSheet( "TabButton {\n" " qproperty-flat_: false;\n" " border: none;" "}\n") self.__search.textEdited.connect(self.__on_textEdited) self.__navigator = ItemViewKeyNavigator(self) self.__navigator.setView(self.__suggestPage.view()) self.__search.installEventFilter(self.__navigator) self.__grip = WindowSizeGrip(self) self.__grip.raise_() def setSizeGripEnabled(self, enabled): """ Enable the resizing of the menu with a size grip in a bottom right corner (enabled by default). """ if bool(enabled) != bool(self.__grip): if self.__grip: self.__grip.deleteLater() self.__grip = None else: self.__grip = WindowSizeGrip(self) self.__grip.raise_() def sizeGripEnabled(self): """ Is the size grip enabled. """ return bool(self.__grip) def addPage(self, name, page): """ Add the `page` (:class:`MenuPage`) with `name` and return it's index. The `page.icon()` will be used as the icon in the tab bar. """ icon = page.icon() tip = name if page.toolTip(): tip = page.toolTip() index = self.__pages.addPage(page, name, icon, tip) # Route the page's signals page.triggered.connect(self.__onTriggered) page.hovered.connect(self.hovered) # Install event filter to intercept key presses. page.view().installEventFilter(self) return index def createPage(self, index): """ Create a new page based on the contents of an index (:class:`QModeIndex`) item. """ page = MenuPage(self) page.setModel(index.model()) page.setRootIndex(index) view = page.view() if sys.platform == "darwin": view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True) # Don't show the focus frame because it expands into the tab # bar at the top. view.setAttribute(Qt.WA_MacShowFocusRect, False) name = unicode(index.data(Qt.DisplayRole)) page.setTitle(name) icon = index.data(Qt.DecorationRole).toPyObject() if isinstance(icon, QIcon): page.setIcon(icon) page.setToolTip(index.data(Qt.ToolTipRole).toPyObject()) return page def setModel(self, model): """ Set the model containing the actions. """ root = model.invisibleRootItem() for i in range(root.rowCount()): item = root.child(i) index = item.index() page = self.createPage(index) page.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) i = self.addPage(page.title(), page) brush = index.data(QtWidgetRegistry.BACKGROUND_ROLE) if brush.isValid(): brush = brush.toPyObject() base_color = brush.color() button = self.__pages.tabButton(i) button.setStyleSheet( TAB_BUTTON_STYLE_TEMPLATE % (create_css_gradient(base_color), create_css_gradient(base_color.darker(120))) ) self.__model = model self.__suggestPage.setModel(model) def setFilterFunc(self, func): """ Set a filter function. """ if func != self.__filterFunc: self.__filterFunc = func for i in range(0, self.__pages.count()): self.__pages.page(i).setFilterFunc(func) def popup(self, pos=None, searchText=""): """ Popup the menu at `pos` (in screen coordinates). 'Search' text field is initialized with `searchText` if provided. """ if pos is None: pos = QPoint() self.__clearCurrentItems() self.__search.setText(searchText) self.__suggestPage.setFilterFixedString(searchText) self.ensurePolished() if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled(): size = self.size() else: size = self.sizeHint() desktop = QApplication.desktop() screen_geom = desktop.availableGeometry(pos) # Adjust the size to fit inside the screen. if size.height() > screen_geom.height(): size.setHeight(screen_geom.height()) if size.width() > screen_geom.width(): size.setWidth(screen_geom.width()) geom = QRect(pos, size) if geom.top() < screen_geom.top(): geom.setTop(screen_geom.top()) if geom.left() < screen_geom.left(): geom.setLeft(screen_geom.left()) bottom_margin = screen_geom.bottom() - geom.bottom() right_margin = screen_geom.right() - geom.right() if bottom_margin < 0: # Falls over the bottom of the screen, move it up. geom.translate(0, bottom_margin) # TODO: right to left locale if right_margin < 0: # Falls over the right screen edge, move the menu to the # other side of pos. geom.translate(-size.width(), 0) self.setGeometry(geom) self.show() if searchText: self.setFocusProxy(self.__search) else: self.setFocusProxy(None) def exec_(self, pos=None, searchText=""): """ Execute the menu at position `pos` (in global screen coordinates). Return the triggered :class:`QAction` or `None` if no action was triggered. 'Search' text field is initialized with `searchText` if provided. """ self.popup(pos, searchText) self.setFocus(Qt.PopupFocusReason) self.__triggeredAction = None self.__loop = QEventLoop() self.__loop.exec_() self.__loop.deleteLater() self.__loop = None action = self.__triggeredAction self.__triggeredAction = None return action def hideEvent(self, event): """ Reimplemented from :class:`QWidget` """ FramelessWindow.hideEvent(self, event) if self.__loop: self.__loop.exit() def setCurrentPage(self, page): """ Set the current shown page to `page`. """ self.__pages.setCurrentPage(page) def setCurrentIndex(self, index): """ Set the current page index. """ self.__pages.setCurrentIndex(index) def __clearCurrentItems(self): """ Clear any selected (or current) items in all the menus. """ for i in range(self.__pages.count()): self.__pages.page(i).view().selectionModel().clear() def __onTriggered(self, action): """ Re-emit the action from the page. """ self.__triggeredAction = action # Hide and exit the event loop if necessary. self.hide() self.triggered.emit(action) def __on_textEdited(self, text): self.__suggestPage.setFilterFixedString(text) self.__pages.setCurrentPage(self.__suggestPage) def triggerSearch(self): """ Trigger action search. This changes to current page to the 'Suggest' page and sets the keyboard focus to the search line edit. """ self.__pages.setCurrentPage(self.__suggestPage) self.__search.setFocus(Qt.ShortcutFocusReason) # Make sure that the first enabled item is set current. self.__suggestPage.ensureCurrent() def keyPressEvent(self, event): if event.text(): # Ignore modifiers, ... self.__search.setFocus(Qt.ShortcutFocusReason) self.setCurrentIndex(0) self.__search.keyPressEvent(event) FramelessWindow.keyPressEvent(self, event) event.accept() def event(self, event): if event.type() == QEvent.ShortcutOverride: log.debug("Overriding shortcuts") event.accept() return True return FramelessWindow.event(self, event) def eventFilter(self, obj, event): if isinstance(obj, QTreeView): etype = event.type() if etype == QEvent.KeyPress: # ignore modifiers non printable characters, Enter, ... if event.text() and event.key() not in \ [Qt.Key_Enter, Qt.Key_Return]: self.__search.setFocus(Qt.ShortcutFocusReason) self.setCurrentIndex(0) self.__search.keyPressEvent(event) return True return FramelessWindow.eventFilter(self, obj, event)
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = True resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) asyncCallsStateChange = Signal() progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" savedWidgetGeometry = settings.Setting(None) def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self._guiElements = [] # used for automatic widget debugging self.__reportData = None # TODO: position used to be saved like this. Reimplement. #if save_position: # self.settingsList = getattr(self, "settingsList", []) + \ # ["widgetShown", "savedWidgetGeometry"] OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) # noinspection PyAttributeOutsideInit def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.warning_bar = gui.widgetBox(self, orientation="horizontal", margin=0, spacing=0) self.warning_icon = gui.widgetLabel(self.warning_bar, "") self.warning_label = gui.widgetLabel(self.warning_bar, "") self.warning_label.setStyleSheet("padding-top: 5px") self.warning_bar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) gui.rubber(self.warning_bar) self.warning_bar.setVisible(False) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox(self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-red.png")) # status bar handler functions def setState(self, stateType, id, text): stateChanged = super().setState(stateType, id, text) if not stateChanged or not hasattr(self, "widgetStatusArea"): return iconsShown = 0 warnings = [("Warning", self._warningWidget, self._owWarning), ("Error", self._errorWidget, self._owError)] for state, widget, use in warnings: if not widget: continue if use and self.widgetState[state]: widget.setToolTip("\n".join(self.widgetState[state].values())) widget.show() iconsShown = 1 else: widget.setToolTip("") widget.hide() if iconsShown: self.statusBarIconArea.show() else: self.statusBarIconArea.hide() if (stateType == "Warning" and self._owWarning) or \ (stateType == "Error" and self._owError): if text: self.setStatusBarText(stateType + ": " + text) else: self.setStatusBarText("") self.updateStatusBarState() def updateWidgetStateInfo(self, stateType, id, text): html = self.widgetStateToHtml(self._owInfo, self._owWarning, self._owError) if html: self.widgetStateInfoBox.show() self.widgetStateInfo.setText(html) self.widgetStateInfo.setToolTip(html) else: if not self.widgetStateInfoBox.isVisible(): dHeight = -self.widgetStateInfoBox.height() else: dHeight = 0 self.widgetStateInfoBox.hide() self.widgetStateInfo.setText("") self.widgetStateInfo.setToolTip("") width, height = self.width(), self.height() + dHeight self.resize(width, height) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass # ############################################## """ def isDataWithClass(self, data, wantedVarType=None, checkMissing=False): self.error([1234, 1235, 1236]) if not data: return 0 if not data.domain.classVar: self.error(1234, "A data set with a class attribute is required.") return 0 if wantedVarType and data.domain.classVar.varType != wantedVarType: self.error(1235, "Unable to handle %s class." % str(data.domain.class_var.var_type).lower()) return 0 if checkMissing and not orange.Preprocessor_dropMissingClasses(data): self.error(1236, "Unable to handle data set with no known classes") return 0 return 1 """ def restoreWidgetPosition(self): restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored: space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) return restored def __updateSavedGeometry(self): if self.__was_restored: # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). self.savedWidgetGeometry = self.saveGeometry() # when widget is resized, save the new width and height def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before showEvent and we must not overwrite the the savedGeometry # with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, ev): QDialog.moveEvent(self, ev) if self.save_position and self.isVisible(): self.__updateSavedGeometry() # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.__updateSavedGeometry() self.__was_restored = False QDialog.hideEvent(self, ev) def closeEvent(self, ev): if self.save_position and self.isVisible(): self.__updateSavedGeometry() self.__was_restored = False QDialog.closeEvent(self, ev) def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position: # Restore saved geometry on show self.restoreWidgetPosition() self.__was_restored = True def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if not any(s.name == signalName for s in self.outputs): raise ValueError( '{} is not a valid output signal for widget {}'.format( signalName, self.name)) if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) # this function is only intended for derived classes to send appropriate # signals when all settings are loaded def activate_loaded_settings(self): pass # reimplemented in other widgets def onDeleteWidget(self): pass def handleNewSignals(self): # this is called after all new signals have been handled # implement this in your widget if you want to process something only # after you received multiple signals pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self, processEvents=QEventLoop.AllEvents): """ Initialize the widget's progress (i.e show and set progress to 0%). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarSet(0, processEvents) def progressBarSet(self, value, processEvents=QEventLoop.AllEvents): """ Set the current progress bar to `value`. .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param float value: Progress value :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn( "progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle( self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") if old != value: self.progressBarValueChanged.emit(value) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value, processEvents=QEventLoop.AllEvents): self.progressBarSet(self.progressBarValue + value, processEvents) def progressBarFinished(self, processEvents=QEventLoop.AllEvents): """ Stop the widget's progress (i.e hide the progress bar). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, state_type, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[state_type]: self.widgetState[state_type].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[state_type]: self.widgetState[state_type].pop(id) changed = 1 else: self.widgetState[state_type][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(state_type, i, "") else: self.widgetStateChanged.emit(state_type, id, text or "") tooltip_lines = [] highest_type = None for a_type in ("Error", "Warning", "Info"): msgs_for_ids = self.widgetState.get(a_type) if not msgs_for_ids: continue msgs_for_ids = list(msgs_for_ids.values()) if not msgs_for_ids: continue tooltip_lines += msgs_for_ids if highest_type is None: highest_type = a_type if highest_type is None: self.set_warning_bar(None) elif len(tooltip_lines) == 1: msg = tooltip_lines[0] if "\n" in msg: self.set_warning_bar(highest_type, msg[:msg.index("\n")] + " (...)", msg) else: self.set_warning_bar(highest_type, tooltip_lines[0], tooltip_lines[0]) else: self.set_warning_bar( highest_type, "{} problems during execution".format(len(tooltip_lines)), "\n".join(tooltip_lines)) return changed def set_warning_bar(self, state_type, text=None, tooltip=None): colors = { "Error": ("#ffc6c6", "black", QStyle.SP_MessageBoxCritical), "Warning": ("#ffffc9", "black", QStyle.SP_MessageBoxWarning), "Info": ("#ceceff", "black", QStyle.SP_MessageBoxInformation) } current_height = self.height() if state_type is None: if not self.warning_bar.isHidden(): new_height = current_height - self.warning_bar.height() self.warning_bar.setVisible(False) self.resize(self.width(), new_height) return background, foreground, icon = colors[state_type] style = QApplication.instance().style() self.warning_icon.setPixmap(style.standardIcon(icon).pixmap(14, 14)) self.warning_bar.setStyleSheet( "background-color: {}; color: {};" "padding: 3px; padding-left: 6px; vertical-align: center".format( background, foreground)) self.warning_label.setText(text) self.warning_bar.setToolTip(tooltip) if self.warning_bar.isHidden(): self.warning_bar.setVisible(True) new_height = current_height + self.warning_bar.height() self.resize(self.width(), new_height) def widgetStateToHtml(self, info=True, warning=True, error=True): pixmaps = self.getWidgetStateIcons() items = [] iconPath = { "Info": "canvasIcons:information.png", "Warning": "canvasIcons:warning.png", "Error": "canvasIcons:error.png" } for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconPath[what], "\n".join( self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): iconsDir = os.path.join(environ.canvas_install_dir, "icons") QDir.addSearchPath( "canvasIcons", os.path.join(environ.canvas_install_dir, "icons/")) info = QPixmap("canvasIcons:information.png") warning = QPixmap("canvasIcons:warning.png") error = QPixmap("canvasIcons:error.png") cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible()) } def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the signal manager """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self)
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = False resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) asyncCallsStateChange = Signal() progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) # 'current_context' MUST be the first thing assigned to a widget self.current_context = settings.Context() if self.settingsHandler: stored_settings = kwargs.get("stored_settings", None) self.settingsHandler.initialize(self, stored_settings) # number of control signals that are currently being processed # needed by signalWrapper to know when everything was sent self.needProcessing = 0 # used by signalManager self.signalManager = kwargs.get("signal_manager", None) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self._guiElements = [] # used for automatic widget debugging self.__reportData = None # TODO: position used to be saved like this. Reimplement. # if save_position: # self.settingsList = getattr(self, "settingsList", []) + \ # ["widgetShown", "savedWidgetGeometry"] OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id # TODO: kill me self.__dict__.update(environ.directories) if self.name: self.setCaption(self.name.replace("&", "")) self.setFocusPolicy(Qt.StrongFocus) self.wrappers = [] # stored wrappers for widget events self.linksIn = {} # signalName : (dirty, widFrom, handler, signalData) self.linksOut = {} # signalName: (signalData, id) self.connections = {} # keys are (control, signal) and values are # wrapper instances. Used in connect/disconnect self.callbackDeposit = [] self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox( self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0, ) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(self.widgetDir, "icons/triangle-orange.png") ) self._errorWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(self.widgetDir + "icons/triangle-red.png") ) # status bar handler functions def setState(self, stateType, id, text): stateChanged = super().setState(stateType, id, text) if not stateChanged or not hasattr(self, "widgetStatusArea"): return iconsShown = 0 warnings = [("Warning", self._warningWidget, self._owWarning), ("Error", self._errorWidget, self._owError)] for state, widget, use in warnings: if not widget: continue if use and self.widgetState[state]: widget.setToolTip("\n".join(self.widgetState[state].values())) widget.show() iconsShown = 1 else: widget.setToolTip("") widget.hide() if iconsShown: self.statusBarIconArea.show() else: self.statusBarIconArea.hide() if (stateType == "Warning" and self._owWarning) or (stateType == "Error" and self._owError): if text: self.setStatusBarText(stateType + ": " + text) else: self.setStatusBarText("") self.updateStatusBarState() def updateWidgetStateInfo(self, stateType, id, text): html = self.widgetStateToHtml(self._owInfo, self._owWarning, self._owError) if html: self.widgetStateInfoBox.show() self.widgetStateInfo.setText(html) self.widgetStateInfo.setToolTip(html) else: if not self.widgetStateInfoBox.isVisible(): dHeight = -self.widgetStateInfoBox.height() else: dHeight = 0 self.widgetStateInfoBox.hide() self.widgetStateInfo.setText("") self.widgetStateInfo.setToolTip("") width, height = self.width(), self.height() + dHeight self.resize(width, height) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass def getIconNames(self, iconName): # if canvas sent us a prepared list of valid names, just return those if type(iconName) == list: return iconName names = [] name, ext = os.path.splitext(iconName) for num in [16, 32, 42, 60]: names.append("%s_%d%s" % (name, num, ext)) fullPaths = [] module_dir = os.path.dirname(sys.modules[self.__module__].__file__) for paths in [(self.widgetDir, name), (self.widgetDir, "icons", name), (module_dir, "icons", name)]: for name in names + [iconName]: fname = os.path.join(*paths) if os.path.exists(fname): fullPaths.append(fname) if fullPaths != []: break if len(fullPaths) > 1 and fullPaths[-1].endswith(iconName): # if we have the new icons we can remove the default icon fullPaths.pop() return fullPaths def setWidgetIcon(self, iconName): iconNames = self.getIconNames(iconName) icon = QIcon() for name in iconNames: pix = QPixmap(name) icon.addPixmap(pix) self.setWindowIcon(icon) # ############################################## def isDataWithClass(self, data, wantedVarType=None, checkMissing=False): self.error([1234, 1235, 1236]) if not data: return 0 if not data.domain.classVar: self.error(1234, "A data set with a class attribute is required.") return 0 if wantedVarType and data.domain.classVar.varType != wantedVarType: self.error(1235, "Unable to handle %s class." % str(data.domain.class_var.var_type).lower()) return 0 if checkMissing and not orange.Preprocessor_dropMissingClasses(data): self.error(1236, "Unable to handle data set with no known classes") return 0 return 1 # call processEvents(), but first remember position and size of widget in # case one of the events would be move or resize # call this function if needed in __init__ of the widget def safeProcessEvents(self): keys = ["widgetShown"] vals = [(key, getattr(self, key, None)) for key in keys] qApp.processEvents() for (key, val) in vals: if val != None: setattr(self, key, val) # this function is called at the end of the widget's __init__ when the # widgets is saving its position and size parameters def restoreWidgetPosition(self): if self.save_position: geometry = getattr(self, "savedWidgetGeometry", None) restored = False if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored: space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() # Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) # this is called in canvas when loading a schema. it opens the widgets # that were shown when saving the schema def restoreWidgetStatus(self): if self.save_position and getattr(self, "widgetShown", None): self.show() # when widget is resized, save new width and height into widgetWidth and # widgetHeight. some widgets can put this two variables into settings and # last widget shape is restored after restart def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives the resizeEvent before showEvent and we must not # overwrite the the savedGeometry before then) if self.save_position and self.isVisible(): self.savedWidgetGeometry = str(self.saveGeometry()) # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.widgetShown = 0 self.savedWidgetGeometry = str(self.saveGeometry()) QDialog.hideEvent(self, ev) # set widget state to shown def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position: self.widgetShown = 1 self.restoreWidgetPosition() def closeEvent(self, ev): if self.save_position: self.savedWidgetGeometry = str(self.saveGeometry()) QDialog.closeEvent(self, ev) def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): if self.parent != None and isinstance(self.parent, QTabWidget): self.parent.setTabText(self.parent.indexOf(self), caption) else: # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): if self.current_context is not None: self.settingsHandler.close_context(self) self.current_context = None def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) # this function is only intended for derived classes to send appropriate # signals when all settings are loaded def activateLoadedSettings(self): pass # reimplemented in other widgets def onDeleteWidget(self): pass def setOptions(self): pass def handleNewSignals(self): # this is called after all new signals have been handled # implement this in your widget if you want to process something only # after you received multiple signals pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self): self.progressBarValue = 0 self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") self.processingStateChanged.emit(1) def progressBarSet(self, value): if value > 0: self.__progressBarValue = value usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") self.progressBarValueChanged.emit(value) qApp.processEvents() def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) def progressBarAdvance(self, value): self.progressBarSet(self.progressBarValue + value) def progressBarFinished(self): self.setWindowTitle(self.captionTitle) self.processingStateChanged.emit(0) def openWidgetHelp(self): if "widgetInfo" in self.__dict__: # This widget is on a canvas. qApp.canvasDlg.helpWindow.showHelpFor(self.widgetInfo, True) def keyPressEvent(self, e): if e.key() in (Qt.Key_Help, Qt.Key_F1): self.openWidgetHelp() # e.ignore() elif (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) # self.setState("Warning", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, stateType, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[stateType]: self.widgetState[stateType].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[stateType]: self.widgetState[stateType].pop(id) changed = 1 else: self.widgetState[stateType][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(stateType, i, "") else: self.widgetStateChanged.emit(stateType, id, text or "") return changed def widgetStateToHtml(self, info=True, warning=True, error=True): pixmaps = self.getWidgetStateIcons() items = [] iconPath = { "Info": "canvasIcons:information.png", "Warning": "canvasIcons:warning.png", "Error": "canvasIcons:error.png", } for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append( '<img src="%s" style="float: left;"> %s' % (iconPath[what], "\n".join(self.widgetState[what].values())) ) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): iconsDir = os.path.join(environ.canvas_install_dir, "icons") QDir.addSearchPath("canvasIcons", os.path.join(environ.canvas_install_dir, "icons/")) info = QPixmap("canvasIcons:information.png") warning = QPixmap("canvasIcons:warning.png") error = QPixmap("canvasIcons:error.png") cls._cached__widget_state_icons = {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible()), } def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the signal manager """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self)
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = True resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) asyncCallsStateChange = Signal() progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" savedWidgetGeometry = settings.Setting(None) def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self._guiElements = [] # used for automatic widget debugging self.__reportData = None # TODO: position used to be saved like this. Reimplement. #if save_position: # self.settingsList = getattr(self, "settingsList", []) + \ # ["widgetShown", "savedWidgetGeometry"] OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) # noinspection PyAttributeOutsideInit def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.warning_bar = gui.widgetBox(self, orientation="horizontal", margin=0, spacing=0) self.warning_icon = gui.widgetLabel(self.warning_bar, "") self.warning_label = gui.widgetLabel(self.warning_bar, "") self.warning_label.setStyleSheet("padding-top: 5px") self.warning_bar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) gui.rubber(self.warning_bar) self.warning_bar.setVisible(False) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox(self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, os.path.join(environ.widget_install_dir, "icons/triangle-red.png")) # status bar handler functions def setState(self, stateType, id, text): stateChanged = super().setState(stateType, id, text) if not stateChanged or not hasattr(self, "widgetStatusArea"): return iconsShown = 0 warnings = [("Warning", self._warningWidget, self._owWarning), ("Error", self._errorWidget, self._owError)] for state, widget, use in warnings: if not widget: continue if use and self.widgetState[state]: widget.setToolTip("\n".join(self.widgetState[state].values())) widget.show() iconsShown = 1 else: widget.setToolTip("") widget.hide() if iconsShown: self.statusBarIconArea.show() else: self.statusBarIconArea.hide() if (stateType == "Warning" and self._owWarning) or \ (stateType == "Error" and self._owError): if text: self.setStatusBarText(stateType + ": " + text) else: self.setStatusBarText("") self.updateStatusBarState() def updateWidgetStateInfo(self, stateType, id, text): html = self.widgetStateToHtml(self._owInfo, self._owWarning, self._owError) if html: self.widgetStateInfoBox.show() self.widgetStateInfo.setText(html) self.widgetStateInfo.setToolTip(html) else: if not self.widgetStateInfoBox.isVisible(): dHeight = - self.widgetStateInfoBox.height() else: dHeight = 0 self.widgetStateInfoBox.hide() self.widgetStateInfo.setText("") self.widgetStateInfo.setToolTip("") width, height = self.width(), self.height() + dHeight self.resize(width, height) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass # ############################################## """ def isDataWithClass(self, data, wantedVarType=None, checkMissing=False): self.error([1234, 1235, 1236]) if not data: return 0 if not data.domain.classVar: self.error(1234, "A data set with a class attribute is required.") return 0 if wantedVarType and data.domain.classVar.varType != wantedVarType: self.error(1235, "Unable to handle %s class." % str(data.domain.class_var.var_type).lower()) return 0 if checkMissing and not orange.Preprocessor_dropMissingClasses(data): self.error(1236, "Unable to handle data set with no known classes") return 0 return 1 """ def restoreWidgetPosition(self): restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored: space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) return restored def __updateSavedGeometry(self): if self.__was_restored: # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). self.savedWidgetGeometry = self.saveGeometry() # when widget is resized, save the new width and height def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before showEvent and we must not overwrite the the savedGeometry # with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, ev): QDialog.moveEvent(self, ev) if self.save_position and self.isVisible(): self.__updateSavedGeometry() # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.__updateSavedGeometry() self.__was_restored = False QDialog.hideEvent(self, ev) def closeEvent(self, ev): if self.save_position and self.isVisible(): self.__updateSavedGeometry() self.__was_restored = False QDialog.closeEvent(self, ev) def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position: # Restore saved geometry on show self.restoreWidgetPosition() self.__was_restored = True def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) # this function is only intended for derived classes to send appropriate # signals when all settings are loaded def activate_loaded_settings(self): pass # reimplemented in other widgets def onDeleteWidget(self): pass def handleNewSignals(self): # this is called after all new signals have been handled # implement this in your widget if you want to process something only # after you received multiple signals pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self): self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarValue = 0 def progressBarSet(self, value): old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn("progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") self.progressBarValueChanged.emit(value) if old != value: self.progressBarValueChanged.emit(value) qApp.processEvents() def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value): self.progressBarSet(self.progressBarValue + value) def progressBarFinished(self): self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, state_type, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[state_type]: self.widgetState[state_type].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[state_type]: self.widgetState[state_type].pop(id) changed = 1 else: self.widgetState[state_type][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(state_type, i, "") else: self.widgetStateChanged.emit(state_type, id, text or "") tooltip_lines = [] highest_type = None for a_type in ("Error", "Warning", "Info"): msgs_for_ids = self.widgetState.get(a_type) if not msgs_for_ids: continue msgs_for_ids = list(msgs_for_ids.values()) if not msgs_for_ids: continue tooltip_lines += msgs_for_ids if highest_type is None: highest_type = a_type if highest_type is None: self.set_warning_bar(None) elif len(tooltip_lines) == 1: msg = tooltip_lines[0] if "\n" in msg: self.set_warning_bar( highest_type, msg[:msg.index("\n")] + " (...)", msg) else: self.set_warning_bar( highest_type, tooltip_lines[0], tooltip_lines[0]) else: self.set_warning_bar( highest_type, "{} problems during execution".format(len(tooltip_lines)), "\n".join(tooltip_lines)) return changed def set_warning_bar(self, state_type, text=None, tooltip=None): colors = {"Error": ("#ffc6c6", "black", QStyle.SP_MessageBoxCritical), "Warning": ("#ffffc9", "black", QStyle.SP_MessageBoxWarning), "Info": ("#ceceff", "black", QStyle.SP_MessageBoxInformation)} current_height = self.height() if state_type is None: if not self.warning_bar.isHidden(): new_height = current_height - self.warning_bar.height() self.warning_bar.setVisible(False) self.resize(self.width(), new_height) return background, foreground, icon = colors[state_type] style = QApplication.instance().style() self.warning_icon.setPixmap(style.standardIcon(icon).pixmap(14, 14)) self.warning_bar.setStyleSheet( "background-color: {}; color: {};" "padding: 3px; padding-left: 6px; vertical-align: center". format(background, foreground)) self.warning_label.setText(text) self.warning_label.setToolTip(tooltip) if self.warning_bar.isHidden(): self.warning_bar.setVisible(True) new_height = current_height + self.warning_bar.height() self.resize(self.width(), new_height) def widgetStateToHtml(self, info=True, warning=True, error=True): pixmaps = self.getWidgetStateIcons() items = [] iconPath = {"Info": "canvasIcons:information.png", "Warning": "canvasIcons:warning.png", "Error": "canvasIcons:error.png"} for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconPath[what], "\n".join(self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): iconsDir = os.path.join(environ.canvas_install_dir, "icons") QDir.addSearchPath("canvasIcons", os.path.join(environ.canvas_install_dir, "icons/")) info = QPixmap("canvasIcons:information.png") warning = QPixmap("canvasIcons:warning.png") error = QPixmap("canvasIcons:error.png") cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible())} def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the signal manager """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self)
class QuickMenu(FramelessWindow): """ A quick menu popup for the widgets. The widgets are set using :func:`QuickMenu.setModel` which must be a model as returned by :func:`QtWidgetRegistry.model` """ #: An action has been triggered in the menu. triggered = Signal(QAction) #: An action has been hovered in the menu hovered = Signal(QAction) def __init__(self, parent=None, **kwargs): FramelessWindow.__init__(self, parent, **kwargs) self.setWindowFlags(Qt.Popup) self.__filterFunc = None self.__setupUi() self.__loop = None self.__model = QStandardItemModel() self.__triggeredAction = None def __setupUi(self): self.setLayout(QVBoxLayout(self)) self.layout().setContentsMargins(6, 6, 6, 6) self.__search = SearchWidget(self, objectName="search-line") self.__search.setPlaceholderText( self.tr("Search for widget or select from the list.")) self.layout().addWidget(self.__search) self.__frame = QFrame(self, objectName="menu-frame") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) self.__frame.setLayout(layout) self.layout().addWidget(self.__frame) self.__pages = PagedMenu(self, objectName="paged-menu") self.__pages.currentChanged.connect(self.setCurrentIndex) self.__pages.triggered.connect(self.triggered) self.__pages.hovered.connect(self.hovered) self.__frame.layout().addWidget(self.__pages) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.__suggestPage = SuggestMenuPage(self, objectName="suggest-page") self.__suggestPage.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) self.__suggestPage.setIcon(icon_loader().get("icons/Search.svg")) if sys.platform == "darwin": view = self.__suggestPage.view() view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True) # Don't show the focus frame because it expands into the tab bar. view.setAttribute(Qt.WA_MacShowFocusRect, False) i = self.addPage(self.tr("Quick Search"), self.__suggestPage) button = self.__pages.tabButton(i) button.setObjectName("search-tab-button") button.setStyleSheet("TabButton {\n" " qproperty-flat_: false;\n" " border: none;" "}\n") self.__search.textEdited.connect(self.__on_textEdited) self.__navigator = ItemViewKeyNavigator(self) self.__navigator.setView(self.__suggestPage.view()) self.__search.installEventFilter(self.__navigator) self.__grip = WindowSizeGrip(self) self.__grip.raise_() def setSizeGripEnabled(self, enabled): """ Enable the resizing of the menu with a size grip in a bottom right corner (enabled by default). """ if bool(enabled) != bool(self.__grip): if self.__grip: self.__grip.deleteLater() self.__grip = None else: self.__grip = WindowSizeGrip(self) self.__grip.raise_() def sizeGripEnabled(self): """ Is the size grip enabled. """ return bool(self.__grip) def addPage(self, name, page): """ Add the `page` (:class:`MenuPage`) with `name` and return it's index. The `page.icon()` will be used as the icon in the tab bar. """ icon = page.icon() tip = name if page.toolTip(): tip = page.toolTip() index = self.__pages.addPage(page, name, icon, tip) # Route the page's signals page.triggered.connect(self.__onTriggered) page.hovered.connect(self.hovered) # Install event filter to intercept key presses. page.view().installEventFilter(self) return index def createPage(self, index): """ Create a new page based on the contents of an index (:class:`QModeIndex`) item. """ page = MenuPage(self) page.setModel(index.model()) page.setRootIndex(index) view = page.view() if sys.platform == "darwin": view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True) # Don't show the focus frame because it expands into the tab # bar at the top. view.setAttribute(Qt.WA_MacShowFocusRect, False) name = unicode(index.data(Qt.DisplayRole)) page.setTitle(name) icon = index.data(Qt.DecorationRole).toPyObject() if isinstance(icon, QIcon): page.setIcon(icon) page.setToolTip(index.data(Qt.ToolTipRole).toPyObject()) return page def setModel(self, model): """ Set the model containing the actions. """ root = model.invisibleRootItem() for i in range(root.rowCount()): item = root.child(i) index = item.index() page = self.createPage(index) page.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) i = self.addPage(page.title(), page) brush = index.data(QtWidgetRegistry.BACKGROUND_ROLE) if brush.isValid(): brush = brush.toPyObject() base_color = brush.color() button = self.__pages.tabButton(i) button.setStyleSheet( TAB_BUTTON_STYLE_TEMPLATE % (create_css_gradient(base_color), create_css_gradient(base_color.darker(120)))) self.__model = model self.__suggestPage.setModel(model) def setFilterFunc(self, func): """ Set a filter function. """ if func != self.__filterFunc: self.__filterFunc = func for i in range(0, self.__pages.count()): self.__pages.page(i).setFilterFunc(func) def popup(self, pos=None, searchText=""): """ Popup the menu at `pos` (in screen coordinates). 'Search' text field is initialized with `searchText` if provided. """ if pos is None: pos = QPoint() self.__clearCurrentItems() self.__search.setText(searchText) self.__suggestPage.setFilterFixedString(searchText) self.ensurePolished() if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled(): size = self.size() else: size = self.sizeHint() desktop = QApplication.desktop() screen_geom = desktop.availableGeometry(pos) # Adjust the size to fit inside the screen. if size.height() > screen_geom.height(): size.setHeight(screen_geom.height()) if size.width() > screen_geom.width(): size.setWidth(screen_geom.width()) geom = QRect(pos, size) if geom.top() < screen_geom.top(): geom.setTop(screen_geom.top()) if geom.left() < screen_geom.left(): geom.setLeft(screen_geom.left()) bottom_margin = screen_geom.bottom() - geom.bottom() right_margin = screen_geom.right() - geom.right() if bottom_margin < 0: # Falls over the bottom of the screen, move it up. geom.translate(0, bottom_margin) # TODO: right to left locale if right_margin < 0: # Falls over the right screen edge, move the menu to the # other side of pos. geom.translate(-size.width(), 0) self.setGeometry(geom) self.show() if searchText: self.setFocusProxy(self.__search) else: self.setFocusProxy(None) def exec_(self, pos=None, searchText=""): """ Execute the menu at position `pos` (in global screen coordinates). Return the triggered :class:`QAction` or `None` if no action was triggered. 'Search' text field is initialized with `searchText` if provided. """ self.popup(pos, searchText) self.setFocus(Qt.PopupFocusReason) self.__triggeredAction = None self.__loop = QEventLoop() self.__loop.exec_() self.__loop.deleteLater() self.__loop = None action = self.__triggeredAction self.__triggeredAction = None return action def hideEvent(self, event): """ Reimplemented from :class:`QWidget` """ FramelessWindow.hideEvent(self, event) if self.__loop: self.__loop.exit() def setCurrentPage(self, page): """ Set the current shown page to `page`. """ self.__pages.setCurrentPage(page) def setCurrentIndex(self, index): """ Set the current page index. """ self.__pages.setCurrentIndex(index) def __clearCurrentItems(self): """ Clear any selected (or current) items in all the menus. """ for i in range(self.__pages.count()): self.__pages.page(i).view().selectionModel().clear() def __onTriggered(self, action): """ Re-emit the action from the page. """ self.__triggeredAction = action # Hide and exit the event loop if necessary. self.hide() self.triggered.emit(action) def __on_textEdited(self, text): self.__suggestPage.setFilterFixedString(text) self.__pages.setCurrentPage(self.__suggestPage) def triggerSearch(self): """ Trigger action search. This changes to current page to the 'Suggest' page and sets the keyboard focus to the search line edit. """ self.__pages.setCurrentPage(self.__suggestPage) self.__search.setFocus(Qt.ShortcutFocusReason) # Make sure that the first enabled item is set current. self.__suggestPage.ensureCurrent() def keyPressEvent(self, event): if event.text(): # Ignore modifiers, ... self.__search.setFocus(Qt.ShortcutFocusReason) self.setCurrentIndex(0) self.__search.keyPressEvent(event) FramelessWindow.keyPressEvent(self, event) event.accept() def event(self, event): if event.type() == QEvent.ShortcutOverride: log.debug("Overriding shortcuts") event.accept() return True return FramelessWindow.event(self, event) def eventFilter(self, obj, event): if isinstance(obj, QTreeView): etype = event.type() if etype == QEvent.KeyPress: # ignore modifiers non printable characters, Enter, ... if event.text() and event.key() not in \ [Qt.Key_Enter, Qt.Key_Return]: self.__search.setFocus(Qt.ShortcutFocusReason) self.setCurrentIndex(0) self.__search.keyPressEvent(event) return True return FramelessWindow.eventFilter(self, obj, event)
class OWWidget(QDialog, Report, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget Meta Description # ----------------------- #: Widget name (:class:`str`) as presented in the Canvas name = None id = None category = None version = None #: Short widget description (:class:`str` optional), displayed in #: canvas help tooltips. description = None #: A longer widget description (:class:`str` optional) long_description = None #: Widget icon path relative to the defining module icon = "icons/Unknown.png" #: Widget priority used for sorting within a category #: (default ``sys.maxsize``). priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None #: A list of published input definitions inputs = [] #: A list of published output definitions outputs = [] # Default widget GUI layout settings # ---------------------------------- #: Should the widget have basic layout #: (If this flag is false then the `want_main_area` and #: `want_control_area` are ignored). want_basic_layout = True #: Should the widget construct a `mainArea` (this is a resizable #: area to the right of the `controlArea`). want_main_area = True #: Should the widget construct a `controlArea`. want_control_area = True #: Widget painted by `Save graph" button graph_name = None graph_writers = FileFormat.img_writers want_status_bar = False save_position = True #: If false the widget will receive fixed size constraint #: (derived from it's layout). Use for widgets which have simple #: static size contents. resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" savedWidgetGeometry = settings.Setting(None) #: A list of advice messages (:class:`Message`) to display to the user. #: When a widget is first shown a message from this list is selected #: for display. If a user accepts (clicks 'Ok. Got it') the choice is #: recorded and the message is never shown again (closing the message #: will not mark it as seen). Messages can be displayed again by pressing #: Shift + F1 #: #: :type: list of :class:`Message` UserAdviceMessages = [] def __new__(cls, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) self.__env = _asmappingproxy(kwargs.get("env", {})) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self.__reportData = None OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" self.__msgwidget = None self.__msgchoice = 0 if self.want_basic_layout: self.insertLayout() sc = QShortcut(QKeySequence(Qt.ShiftModifier | Qt.Key_F1), self) sc.activated.connect(self.__quicktip) return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" def inline_graph_report(self): box = gui.widgetBox(self.controlArea, orientation="horizontal") box.layout().addWidget(self.graphButton) box.layout().addWidget(self.report_button) # self.report_button_background.hide() # self.graphButtonBackground.hide() @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) class Splitter(QSplitter): def createHandle(self): return self.Handle(self.orientation(), self, cursor=Qt.PointingHandCursor) class Handle(QSplitterHandle): def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: splitter = self.splitter() splitter.setSizes([int(splitter.sizes()[0] == 0), 1000]) super().mouseReleaseEvent(event) def mouseMoveEvent(self, event): return # Prevent moving; just show/hide # noinspection PyAttributeOutsideInit def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.warning_bar = gui.widgetBox(self, orientation="horizontal", margin=0, spacing=0) self.warning_icon = gui.widgetLabel(self.warning_bar, "") self.warning_label = gui.widgetLabel(self.warning_bar, "") self.warning_label.setStyleSheet("padding-top: 5px") self.warning_bar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) gui.rubber(self.warning_bar) self.warning_bar.setVisible(False) self.want_main_area = self.graph_name is not None or self.want_main_area splitter = self.Splitter(Qt.Horizontal, self) self.layout().addWidget(splitter) if self.want_control_area: self.controlArea = gui.widgetBox(splitter, orientation="vertical", margin=0) splitter.setSizes([1]) # Results in smallest size allowed by policy if self.graph_name is not None or hasattr(self, "send_report"): leftSide = self.controlArea self.controlArea = gui.widgetBox(leftSide, margin=0) if self.graph_name is not None: self.graphButton = gui.button(leftSide, None, "&Save Graph") self.graphButton.clicked.connect(self.save_graph) self.graphButton.setAutoDefault(0) if hasattr(self, "send_report"): self.report_button = gui.button(leftSide, None, "&Report", callback=self.show_report) self.report_button.setAutoDefault(0) if self.want_main_area: self.controlArea.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self.controlArea.layout().setContentsMargins(4, 4, 0 if self.want_main_area else 4, 4) if self.want_main_area: self.mainArea = gui.widgetBox(splitter, orientation="vertical", margin=4, sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) splitter.setCollapsible(1, False) self.mainArea.layout().setContentsMargins(0 if self.want_control_area else 4, 4, 4, 4) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-red.png")) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) def save_graph(self): graph_obj = getdeepattr(self, self.graph_name, None) if graph_obj is None: return saveplot.save_plot(graph_obj, self.graph_writers) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) def __restoreWidgetGeometry(self): restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored and not self.windowState() & \ (Qt.WindowMaximized | Qt.WindowFullScreen): space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) return restored def __updateSavedGeometry(self): if self.__was_restored and self.isVisible(): # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). self.savedWidgetGeometry = self.saveGeometry() # when widget is resized, save the new width and height def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before first showEvent and we must not overwrite the the # savedGeometry with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, ev): QDialog.moveEvent(self, ev) if self.save_position and self.isVisible(): self.__updateSavedGeometry() # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.__updateSavedGeometry() QDialog.hideEvent(self, ev) def closeEvent(self, ev): if self.save_position and self.isVisible(): self.__updateSavedGeometry() QDialog.closeEvent(self, ev) def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position and not self.__was_restored: # Restore saved geometry on show self.__restoreWidgetGeometry() self.__was_restored = True self.__quicktipOnce() def wheelEvent(self, event): # Silently accept the wheel event. This is to ensure combo boxes # and other controls that have focus don't receive this event unless # the cursor is over them. event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): """ Send a `value` on the `signalName` widget output. An output with `signalName` must be defined in the class ``outputs`` list. """ if not any(s.name == signalName for s in self.outputs): raise ValueError('{} is not a valid output signal for widget {}'.format( signalName, self.name)) if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) def onDeleteWidget(self): """ Invoked by the canvas to notify the widget it has been deleted from the workflow. If possible, subclasses should gracefully cancel any currently executing tasks. """ pass def handleNewSignals(self): """ Invoked by the workflow signal propagation manager after all signals handlers have been called. Reimplement this method in order to coalesce updates from multiple updated inputs. """ pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self, processEvents=QEventLoop.AllEvents): """ Initialize the widget's progress (i.e show and set progress to 0%). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarSet(0, processEvents) def progressBarSet(self, value, processEvents=QEventLoop.AllEvents): """ Set the current progress bar to `value`. .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param float value: Progress value :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn("progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") if old != value: self.progressBarValueChanged.emit(value) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value, processEvents=QEventLoop.AllEvents): self.progressBarSet(self.progressBarValue + value, processEvents) def progressBarFinished(self, processEvents=QEventLoop.AllEvents): """ Stop the widget's progress (i.e hide the progress bar). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) @contextlib.contextmanager def progressBar(self, iterations=0): """ Context manager for progress bar. Using it ensures that the progress bar is removed at the end without needing the `finally` blocks. Usage: with self.progressBar(20) as progress: ... progress.advance() or with self.progressBar() as progress: ... progress.advance(0.15) or with self.progressBar(): ... self.progressBarSet(50) :param iterations: the number of iterations (optional) :type iterations: int """ progress_bar = gui.ProgressBar(self, iterations) yield progress_bar progress_bar.finish() # Let us not rely on garbage collector #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): """ Set widget's status message. This is a short status string to be displayed inline next to the instantiated widget icon in the canvas. """ if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): """ Return the widget's status message. """ return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): """ Set/clear a widget information message (for `id`). Args: id (int or list): The id of the message text (str): Text of the message. """ self.setState("Info", id, text) def warning(self, id=0, text=""): """ Set/clear a widget warning message (for `id`). Args: id (int or list): The id of the message text (str): Text of the message. """ self.setState("Warning", id, text) def error(self, id=0, text=""): """ Set/clear a widget error message (for `id`). Args: id (int or list): The id of the message text (str): Text of the message. """ self.setState("Error", id, text) def setState(self, state_type, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[state_type]: self.widgetState[state_type].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[state_type]: self.widgetState[state_type].pop(id) changed = 1 else: self.widgetState[state_type][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(state_type, i, "") else: self.widgetStateChanged.emit(state_type, id, text or "") tooltip_lines = [] highest_type = None for a_type in ("Error", "Warning", "Info"): msgs_for_ids = self.widgetState.get(a_type) if not msgs_for_ids: continue msgs_for_ids = list(msgs_for_ids.values()) if not msgs_for_ids: continue tooltip_lines += msgs_for_ids if highest_type is None: highest_type = a_type if highest_type is None: self.set_warning_bar(None) elif len(tooltip_lines) == 1: msg = tooltip_lines[0] if "\n" in msg: self.set_warning_bar( highest_type, msg[:msg.index("\n")] + " (...)", msg) else: self.set_warning_bar( highest_type, tooltip_lines[0], tooltip_lines[0]) else: self.set_warning_bar( highest_type, "{} problems during execution".format(len(tooltip_lines)), "\n".join(tooltip_lines)) return changed def set_warning_bar(self, state_type, text=None, tooltip=None): colors = {"Error": ("#ffc6c6", "black", QStyle.SP_MessageBoxCritical), "Warning": ("#ffffc9", "black", QStyle.SP_MessageBoxWarning), "Info": ("#ceceff", "black", QStyle.SP_MessageBoxInformation)} current_height = self.height() if state_type is None: if not self.warning_bar.isHidden(): new_height = current_height - self.warning_bar.height() self.warning_bar.setVisible(False) self.resize(self.width(), new_height) return background, foreground, icon = colors[state_type] style = QApplication.instance().style() self.warning_icon.setPixmap(style.standardIcon(icon).pixmap(14, 14)) self.warning_bar.setStyleSheet( "background-color: {}; color: {};" "padding: 3px; padding-left: 6px; vertical-align: center". format(background, foreground)) self.warning_label.setText(text) self.warning_bar.setToolTip(tooltip) if self.warning_bar.isHidden(): self.warning_bar.setVisible(True) new_height = current_height + self.warning_bar.height() self.resize(self.width(), new_height) def widgetStateToHtml(self, info=True, warning=True, error=True): iconpaths = { "Info": gui.resource_filename("icons/information.png"), "Warning": gui.resource_filename("icons/warning.png"), "Error": gui.resource_filename("icons/error.png") } items = [] for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconpaths[what], "\n".join(self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): info = QPixmap(gui.resource_filename("icons/information.png")) warning = QPixmap(gui.resource_filename("icons/warning.png")) error = QPixmap(gui.resource_filename("icons/error.png")) cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible())} def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the workflow signal manager. This is useful for instance if the widget does it's work in a separate thread or schedules processing from the event queue. In this case it can set the blocking flag in it's processNewSignals method schedule the task and return immediately. After the task has completed the widget can clear the flag and send the updated outputs. .. note:: Failure to clear this flag will block dependent nodes forever. """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self) def workflowEnv(self): """ Return (a view to) the workflow runtime environment. Returns ------- env : types.MappingProxyType """ return self.__env def workflowEnvChanged(self, key, value, oldvalue): """ A workflow environment variable `key` has changed to value. Called by the canvas framework to notify widget of a change in the workflow runtime environment. The default implementation does nothing. """ pass def __showMessage(self, message): if self.__msgwidget is not None: self.__msgwidget.hide() self.__msgwidget.deleteLater() self.__msgwidget = None if message is None: return buttons = MessageOverlayWidget.Ok | MessageOverlayWidget.Close if message.moreurl is not None: buttons |= MessageOverlayWidget.Help if message.icon is not None: icon = message.icon else: icon = Message.Information self.__msgwidget = MessageOverlayWidget( parent=self, text=message.text, icon=icon, wordWrap=True, standardButtons=buttons) b = self.__msgwidget.button(MessageOverlayWidget.Ok) b.setText("Ok, got it") self.__msgwidget.setStyleSheet(""" MessageOverlayWidget { background: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) } MessageOverlayWidget QLabel#text-label { color: white; }""" ) if message.moreurl is not None: helpbutton = self.__msgwidget.button(MessageOverlayWidget.Help) helpbutton.setText("Learn more\N{HORIZONTAL ELLIPSIS}") self.__msgwidget.helpRequested.connect( lambda: QDesktopServices.openUrl(QUrl(message.moreurl))) self.__msgwidget.setWidget(self) self.__msgwidget.show() def __quicktip(self): messages = list(self.UserAdviceMessages) if messages: message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def __quicktipOnce(self): filename = os.path.join(settings.widget_settings_dir(), "user-session-state.ini") namespace = ("user-message-history/{0.__module__}.{0.__qualname__}" .format(type(self))) session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) messages = self.UserAdviceMessages def ispending(msg): return not session_hist.value( "{}/confirmed".format(msg.persistent_id), defaultValue=False, type=bool) messages = list(filter(ispending, messages)) if not messages: return message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def userconfirmed(): session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) session_hist.setValue( "{}/confirmed".format(message.persistent_id), True) session_hist.sync() self.__msgwidget.accepted.connect(userconfirmed)
class OWWidget(QDialog, metaclass=WidgetMetaClass): # Global widget count widget_id = 0 # Widget description name = None id = None category = None version = None description = None long_description = None icon = "icons/Unknown.png" priority = sys.maxsize author = None author_email = None maintainer = None maintainer_email = None help = None help_ref = None url = None keywords = [] background = None replaces = None inputs = [] outputs = [] # Default widget layout settings want_basic_layout = True want_main_area = True want_control_area = True want_graph = False show_save_graph = True want_status_bar = False no_report = False save_position = True resizing_enabled = True widgetStateChanged = Signal(str, int, str) blockingStateChanged = Signal(bool) progressBarValueChanged = Signal(float) processingStateChanged = Signal(int) settingsHandler = None """:type: SettingsHandler""" savedWidgetGeometry = settings.Setting(None) def __new__(cls, parent=None, *args, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) self.__env = _asmappingproxy(kwargs.get("env", {})) setattr(self, gui.CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self)) self.__reportData = None OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id if self.name: self.setCaption(self.name) self.setFocusPolicy(Qt.StrongFocus) self.startTime = time.time() # used in progressbar self.widgetState = {"Info": {}, "Warning": {}, "Error": {}} self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__progressBarValue = -1 self.__progressState = 0 self.__statusMessage = "" if self.want_basic_layout: self.insertLayout() return self def __init__(self, *args, **kwargs): """QDialog __init__ was already called in __new__, please do not call it here.""" @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) # noinspection PyAttributeOutsideInit def insertLayout(self): def createPixmapWidget(self, parent, iconName): w = QLabel(parent) parent.layout().addWidget(w) w.setFixedSize(16, 16) w.hide() if os.path.exists(iconName): w.setPixmap(QPixmap(iconName)) return w self.setLayout(QVBoxLayout()) self.layout().setMargin(2) self.warning_bar = gui.widgetBox(self, orientation="horizontal", margin=0, spacing=0) self.warning_icon = gui.widgetLabel(self.warning_bar, "") self.warning_label = gui.widgetLabel(self.warning_bar, "") self.warning_label.setStyleSheet("padding-top: 5px") self.warning_bar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) gui.rubber(self.warning_bar) self.warning_bar.setVisible(False) self.topWidgetPart = gui.widgetBox(self, orientation="horizontal", margin=0) self.leftWidgetPart = gui.widgetBox(self.topWidgetPart, orientation="vertical", margin=0) if self.want_main_area: self.leftWidgetPart.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.leftWidgetPart.updateGeometry() self.mainArea = gui.widgetBox(self.topWidgetPart, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding), margin=0) self.mainArea.layout().setMargin(4) self.mainArea.updateGeometry() if self.want_control_area: self.controlArea = gui.widgetBox(self.leftWidgetPart, orientation="vertical", margin=4) if self.want_graph and self.show_save_graph: graphButtonBackground = gui.widgetBox(self.leftWidgetPart, orientation="horizontal", margin=4) self.graphButton = gui.button(graphButtonBackground, self, "&Save Graph") self.graphButton.setAutoDefault(0) if self.want_status_bar: self.widgetStatusArea = QFrame(self) self.statusBarIconArea = QFrame(self) self.widgetStatusBar = QStatusBar(self) self.layout().addWidget(self.widgetStatusArea) self.widgetStatusArea.setLayout(QHBoxLayout(self.widgetStatusArea)) self.widgetStatusArea.layout().addWidget(self.statusBarIconArea) self.widgetStatusArea.layout().addWidget(self.widgetStatusBar) self.widgetStatusArea.layout().setMargin(0) self.widgetStatusArea.setFrameShape(QFrame.StyledPanel) self.statusBarIconArea.setLayout(QHBoxLayout()) self.widgetStatusBar.setSizeGripEnabled(0) self.statusBarIconArea.hide() self._warningWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-orange.png")) self._errorWidget = createPixmapWidget( self.statusBarIconArea, gui.resource_filename("icons/triangle-red.png")) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) def updateStatusBarState(self): if not hasattr(self, "widgetStatusArea"): return if self.widgetState["Warning"] or self.widgetState["Error"]: self.widgetStatusArea.show() else: self.widgetStatusArea.hide() def setStatusBarText(self, text, timeout=5000): if hasattr(self, "widgetStatusBar"): self.widgetStatusBar.showMessage(" " + text, timeout) # TODO add! def prepareDataReport(self, data): pass def __restoreWidgetGeometry(self): restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: restored = self.restoreGeometry(QByteArray(geometry)) if restored and not self.windowState() & \ (Qt.WindowMaximized | Qt.WindowFullScreen): space = qApp.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) return restored def __updateSavedGeometry(self): if self.__was_restored and self.isVisible(): # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). self.savedWidgetGeometry = self.saveGeometry() # when widget is resized, save the new width and height def resizeEvent(self, ev): QDialog.resizeEvent(self, ev) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before first showEvent and we must not overwrite the the # savedGeometry with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, ev): QDialog.moveEvent(self, ev) if self.save_position and self.isVisible(): self.__updateSavedGeometry() # set widget state to hidden def hideEvent(self, ev): if self.save_position: self.__updateSavedGeometry() QDialog.hideEvent(self, ev) def closeEvent(self, ev): if self.save_position and self.isVisible(): self.__updateSavedGeometry() QDialog.closeEvent(self, ev) def showEvent(self, ev): QDialog.showEvent(self, ev) if self.save_position and not self.__was_restored: # Restore saved geometry on show self.__restoreWidgetGeometry() self.__was_restored = True def wheelEvent(self, event): """ Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # we have to save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) # put this widget on top of all windows def reshow(self): self.show() self.raise_() self.activateWindow() def send(self, signalName, value, id=None): if not any(s.name == signalName for s in self.outputs): raise ValueError('{} is not a valid output signal for widget {}'.format( signalName, self.name)) if self.signalManager is not None: self.signalManager.send(self, signalName, value, id) def __setattr__(self, name, value): """Set value to members of this instance or any of its members. If member is used in a gui control, notify the control about the change. name: name of the member, dot is used for nesting ("graph.point.size"). value: value to set to the member. """ names = name.rsplit(".") field_name = names.pop() obj = reduce(lambda o, n: getattr(o, n, None), names, self) if obj is None: raise AttributeError("Cannot set '{}' to {} ".format(name, value)) if obj is self: super().__setattr__(field_name, value) else: setattr(obj, field_name, value) notify_changed(obj, field_name, value) if self.settingsHandler: self.settingsHandler.fast_save(self, name, value) def openContext(self, *a): self.settingsHandler.open_context(self, *a) def closeContext(self): self.settingsHandler.close_context(self) def retrieveSpecificSettings(self): pass def storeSpecificSettings(self): pass def saveSettings(self): self.settingsHandler.update_defaults(self) def onDeleteWidget(self): """ Invoked by the canvas to notify the widget it has been deleted from the workflow. If possible, subclasses should gracefully cancel any currently executing tasks. """ pass def handleNewSignals(self): """ Invoked by the workflow signal propagation manager after all signals handlers have been called. Reimplement this method in order to coalesce updates from multiple updated inputs. """ pass # ############################################ # PROGRESS BAR FUNCTIONS def progressBarInit(self, processEvents=QEventLoop.AllEvents): """ Initialize the widget's progress (i.e show and set progress to 0%). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.startTime = time.time() self.setWindowTitle(self.captionTitle + " (0% complete)") if self.__progressState != 1: self.__progressState = 1 self.processingStateChanged.emit(1) self.progressBarSet(0, processEvents) def progressBarSet(self, value, processEvents=QEventLoop.AllEvents): """ Set the current progress bar to `value`. .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param float value: Progress value :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ old = self.__progressBarValue self.__progressBarValue = value if value > 0: if self.__progressState != 1: warnings.warn("progressBarSet() called without a " "preceding progressBarInit()", stacklevel=2) self.__progressState = 1 self.processingStateChanged.emit(1) usedTime = max(1, time.time() - self.startTime) totalTime = (100.0 * usedTime) / float(value) remainingTime = max(0, totalTime - usedTime) h = int(remainingTime / 3600) min = int((remainingTime - h * 3600) / 60) sec = int(remainingTime - h * 3600 - min * 60) if h > 0: text = "%(h)d:%(min)02d:%(sec)02d" % vars() else: text = "%(min)d:%(sec)02d" % vars() self.setWindowTitle(self.captionTitle + " (%(value).2f%% complete, remaining time: %(text)s)" % vars()) else: self.setWindowTitle(self.captionTitle + " (0% complete)") if old != value: self.progressBarValueChanged.emit(value) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) def progressBarValue(self): return self.__progressBarValue progressBarValue = pyqtProperty(float, fset=progressBarSet, fget=progressBarValue) processingState = pyqtProperty(int, fget=lambda self: self.__progressState) def progressBarAdvance(self, value, processEvents=QEventLoop.AllEvents): self.progressBarSet(self.progressBarValue + value, processEvents) def progressBarFinished(self, processEvents=QEventLoop.AllEvents): """ Stop the widget's progress (i.e hide the progress bar). .. note:: This method will by default call `QApplication.processEvents` with `processEvents`. To suppress this behavior pass ``processEvents=None``. :param processEvents: Process events flag :type processEvents: `QEventLoop.ProcessEventsFlags` or `None` """ self.setWindowTitle(self.captionTitle) if self.__progressState != 0: self.__progressState = 0 self.processingStateChanged.emit(0) if processEvents is not None and processEvents is not False: qApp.processEvents(processEvents) #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): return self.__statusMessage def keyPressEvent(self, e): if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) def information(self, id=0, text=None): self.setState("Info", id, text) def warning(self, id=0, text=""): self.setState("Warning", id, text) def error(self, id=0, text=""): self.setState("Error", id, text) def setState(self, state_type, id, text): changed = 0 if type(id) == list: for val in id: if val in self.widgetState[state_type]: self.widgetState[state_type].pop(val) changed = 1 else: if type(id) == str: text = id id = 0 if not text: if id in self.widgetState[state_type]: self.widgetState[state_type].pop(id) changed = 1 else: self.widgetState[state_type][id] = text changed = 1 if changed: if type(id) == list: for i in id: self.widgetStateChanged.emit(state_type, i, "") else: self.widgetStateChanged.emit(state_type, id, text or "") tooltip_lines = [] highest_type = None for a_type in ("Error", "Warning", "Info"): msgs_for_ids = self.widgetState.get(a_type) if not msgs_for_ids: continue msgs_for_ids = list(msgs_for_ids.values()) if not msgs_for_ids: continue tooltip_lines += msgs_for_ids if highest_type is None: highest_type = a_type if highest_type is None: self.set_warning_bar(None) elif len(tooltip_lines) == 1: msg = tooltip_lines[0] if "\n" in msg: self.set_warning_bar( highest_type, msg[:msg.index("\n")] + " (...)", msg) else: self.set_warning_bar( highest_type, tooltip_lines[0], tooltip_lines[0]) else: self.set_warning_bar( highest_type, "{} problems during execution".format(len(tooltip_lines)), "\n".join(tooltip_lines)) return changed def set_warning_bar(self, state_type, text=None, tooltip=None): colors = {"Error": ("#ffc6c6", "black", QStyle.SP_MessageBoxCritical), "Warning": ("#ffffc9", "black", QStyle.SP_MessageBoxWarning), "Info": ("#ceceff", "black", QStyle.SP_MessageBoxInformation)} current_height = self.height() if state_type is None: if not self.warning_bar.isHidden(): new_height = current_height - self.warning_bar.height() self.warning_bar.setVisible(False) self.resize(self.width(), new_height) return background, foreground, icon = colors[state_type] style = QApplication.instance().style() self.warning_icon.setPixmap(style.standardIcon(icon).pixmap(14, 14)) self.warning_bar.setStyleSheet( "background-color: {}; color: {};" "padding: 3px; padding-left: 6px; vertical-align: center". format(background, foreground)) self.warning_label.setText(text) self.warning_bar.setToolTip(tooltip) if self.warning_bar.isHidden(): self.warning_bar.setVisible(True) new_height = current_height + self.warning_bar.height() self.resize(self.width(), new_height) def widgetStateToHtml(self, info=True, warning=True, error=True): iconpaths = { "Info": gui.resource_filename("icons/information.png"), "Warning": gui.resource_filename("icons/warning.png"), "Error": gui.resource_filename("icons/error.png") } items = [] for show, what in [(info, "Info"), (warning, "Warning"), (error, "Error")]: if show and self.widgetState[what]: items.append('<img src="%s" style="float: left;"> %s' % (iconpaths[what], "\n".join(self.widgetState[what].values()))) return "<br>".join(items) @classmethod def getWidgetStateIcons(cls): if not hasattr(cls, "_cached__widget_state_icons"): info = QPixmap(gui.resource_filename("icons/information.png")) warning = QPixmap(gui.resource_filename("icons/warning.png")) error = QPixmap(gui.resource_filename("icons/error.png")) cls._cached__widget_state_icons = \ {"Info": info, "Warning": warning, "Error": error} return cls._cached__widget_state_icons defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible())} def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the workflow signal manager. This is useful for instance if the widget does it's work in a separate thread or schedules processing from the event queue. In this case it can set the blocking flag in it's processNewSignals method schedule the task and return immediately. After the task has completed the widget can clear the flag and send the updated outputs. .. note:: Failure to clear this flag will block dependent nodes forever. """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """ Is this widget blocking signal processing. """ return self.__blocking def resetSettings(self): self.settingsHandler.reset_settings(self) def workflowEnv(self): """ Return (a view to) the workflow runtime environment. Returns ------- env : types.MappingProxyType """ return self.__env def workflowEnvChanged(self, key, value, oldvalue): """ A workflow environment variable `key` has changed to value. Called by the canvas framework to notify widget of a change in the workflow runtime environment. The default implementation does nothing. """ pass
class RipperWidget(SmoothWidget, ripper.Ripper): """ One-button multi-track ripper. """ def __init__(self, parent=None): SmoothWidget.__init__(self, parent) ripper.Ripper.__init__(self) Layout = QVBoxLayout(self) self.discLabel = QLabel(self) Layout.addWidget(self.discLabel) # ONLY ONE BUTTON! self.button = QPushButton('Cancel', self) self.button.setEnabled(True) self.button.hide() QObject.connect(self.button, SIGNAL('clicked()'), self.cancel) Layout.addWidget(self.button) self.catFrame = QFrame(self) self.catFrame.setFrameStyle(QFrame.StyledPanel) self.catFrame.hide() QHBoxLayout(self.catFrame) self.categories = CategoryButtons(self.catFrame) QObject.connect(self.categories, SIGNAL('selected(QString &)'), self.start) self.catFrame.layout().addWidget(self.categories) Layout.addWidget(self.catFrame) self.progress = QProgressBar(self) self.progress.setRange(0, 100) self.progress.hide() Layout.addWidget(self.progress) def start(self, category): """ Should already have the track info. """ self.genre = str(category) self._progress_data = {} self.disc_length = 0.0 for i in range(self.count): sec = cd_logic.get_track_time_total(i) self._progress_data[i] = {'rip' : 0, 'enc' : 0, 'length' : sec} self.disc_length += sec self.rip_n_encode() text = '%s - %s' % (self.artist, self.album) self.discLabel.setText('Copying "'+text+'"...') self.button.show() self.progress.show() self.catFrame.hide() def cancel(self): self.stop_request = True path = os.path.join(ripper.LIBRARY, self.genre, self.artist+' - '+self.album) os.system('rm -rf \"%s\"' % path) self.discLabel.setText('Canceled') self.button.hide() self.progress.hide() self.progress.setValue(0) self.emit(SIGNAL('canceled()')) def read_disc_info(self): self.discLabel.setText('Getting disc info...') ripper.Ripper.read_disc_info(self) def ripper_event(self, e): """ """ event = QEvent(event_type(e.type)) event.data = e.__dict__ QApplication.instance().postEvent(self, event) def customEvent(self, e): if e.type() == event_type(ripper.CDDB_DONE): self.button.setEnabled(True) text = '%s - %s' % (self.artist, self.album) text += ' ( Select a category... )' self.discLabel.setText(text) self.catFrame.show() self.emit(SIGNAL('foundDiscInfo()')) elif e.type() == event_type(ripper.STATUS_UPDATE): self.updateProgress(e.data) self.emit(SIGNAL('status(QEvent *)'), e) def updateProgress(self, data): """ assume rip and enc are equal in time. """ state = data['state'] tracknum = data['tracknum'] percent = data['percent'] goal_percents = self.count * 200 item = self._progress_data[tracknum] if state == 'rip': item['rip'] = percent elif state == 'enc': item['enc'] = percent else: # err, etc... item['rip'] = 0 item['enc'] = 0 total = 0 for i, v in self._progress_data.items(): seconds = v['length'] rip_perc = v['rip'] enc_perc = v['enc'] worth = seconds / self.disc_length rip_perc *= worth enc_perc *= worth total += rip_perc + enc_perc #print i, worth, rip_perc, enc_perc, total percent = total / 2 self.progress.setValue(percent) if percent == 100: if self.is_ripping: print 'percent == 100 but still ripping?' if self.is_encoding: print 'percent == 100 but still encoding?' else: print 'percent == 100 and ripper finished' self.button.hide() self.progress.hide() text = 'Finished copying \"%s - %s\"' % (self.artist, self.album) self.discLabel.setText(text) self.emit(SIGNAL('doneRipping()'))