def test_run_query(self): w = AddonManagerDialog() query_res = [ addons._QueryResult("uber-pkg", None), addons._QueryResult( "unter-pkg", Installable("unter-pkg", "0.0.0", "", "", "", [])) ] def query(names): return query_res with patch.object(QMessageBox, "exec_", return_value=QMessageBox.Cancel), \ patch.object(addons, "query_pypi", query): f = w.runQueryAndAddResults(["uber-pkg", "unter-pkg"], ) loop = QEventLoop() f.add_done_callback(qinvoke(lambda f: loop.quit(), loop)) loop.exec() items = w.items() self.assertEqual(items, [Available(query_res[1].installable)])
def test_py_owned(self): class Obj(QObject, PyOwned): pass executor = ThreadPoolExecutor() ref = SimpleNamespace(obj=Obj()) wref = weakref.ref(ref.obj) event = threading.Event() event.clear() def clear_ref(): del ref.obj event.set() executor.submit(clear_ref) event.wait() self.assertIsNotNone(wref()) self.assertIn(wref(), PyOwned._PyOwned__delete_later_set) loop = QEventLoop() QTimer.singleShot(0, loop.quit) loop.exec() self.assertIsNone(wref())
class _TaskRunnable(QRunnable): """ A QRunnable for running a :class:`Task` by a :class:`ThreadExecutor`. """ def __init__(self, future, task, args, kwargs): QRunnable.__init__(self) self.future = future self.task = task self.args = args self.kwargs = kwargs self.eventLoop = None def run(self): """ Reimplemented from `QRunnable.run` """ self.eventLoop = QEventLoop() self.eventLoop.processEvents() # Move the task to the current thread so it's events, signals, slots # are triggered from this thread. assert self.task.thread() is _TaskDepotThread.instance() QMetaObject.invokeMethod(self.task.thread(), "transfer", Qt.BlockingQueuedConnection, Q_ARG(object, self.task), Q_ARG(object, QThread.currentThread())) self.eventLoop.processEvents() # Schedule task.run from the event loop. self.task.start() # Quit the loop and exit when task finishes or is cancelled. self.task.finished.connect(self.eventLoop.quit) self.task.cancelled.connect(self.eventLoop.quit) self.eventLoop.exec()
class EventSpy(QObject): """ A testing utility class (similar to QSignalSpy) to record events delivered to a QObject instance. Note ---- Only event types can be recorded (as QEvent instances are deleted on delivery). Note ---- Can only be used with a QCoreApplication running. Parameters ---------- object : QObject An object whose events need to be recorded. etype : Union[QEvent.Type, Sequence[QEvent.Type] A event type (or types) that should be recorded """ def __init__(self, object: QObject, etype, **kwargs): super().__init__(**kwargs) if not isinstance(object, QObject): raise TypeError self.__object = object try: len(etype) except TypeError: etypes = {etype} else: etypes = set(etype) self.__etypes = etypes self.__record = [] self.__loop = QEventLoop() self.__timer = QTimer(self, singleShot=True) self.__timer.timeout.connect(self.__loop.quit) self.__object.installEventFilter(self) def wait(self, timeout=5000): """ Start an event loop that runs until a spied event or a timeout occurred. Parameters ---------- timeout : int Timeout in milliseconds. Returns ------- res : bool True if the event occurred and False otherwise. Example ------- >>> app = QCoreApplication.instance() or QCoreApplication([]) >>> obj = QObject() >>> spy = EventSpy(obj, QEvent.User) >>> app.postEvent(obj, QEvent(QEvent.User)) >>> spy.wait() True >>> print(spy.events()) [1000] """ count = len(self.__record) self.__timer.stop() self.__timer.setInterval(timeout) self.__timer.start() self.__loop.exec() self.__timer.stop() return len(self.__record) != count def eventFilter(self, reciever: QObject, event: QEvent) -> bool: if reciever is self.__object and event.type() in self.__etypes: self.__record.append(event.type()) if self.__loop.isRunning(): self.__loop.quit() return super().eventFilter(reciever, event) def events(self) -> List[QEvent.Type]: """ Return a list of all (listened to) event types that occurred. Returns ------- events : List[QEvent.Type] """ return list(self.__record)
class UserSettingsDialog(QMainWindow): """ A User Settings/Defaults dialog. """ MAC_UNIFIED = True def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.setWindowFlags(Qt.Dialog) self.setWindowModality(Qt.ApplicationModal) self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) self.__macUnified = sys.platform == "darwin" and self.MAC_UNIFIED self._manager = BindingManager(self, submitPolicy=BindingManager.AutoSubmit) self.__loop = None self.__settings = config.settings() self.__setupUi() def __setupUi(self): """Set up the UI. """ if self.__macUnified: self.tab = QToolBar( floatable=False, movable=False, allowedAreas=Qt.TopToolBarArea, ) self.addToolBar(Qt.TopToolBarArea, self.tab) self.setUnifiedTitleAndToolBarOnMac(True) # This does not seem to work self.setWindowFlags(self.windowFlags() & \ ~Qt.MacWindowToolBarButtonHint) self.tab.actionTriggered[QAction].connect( self.__macOnToolBarAction ) central = QStackedWidget() central.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) else: self.tab = central = QTabWidget(self) # Add a close button to the bottom of the dialog # (to satisfy GNOME 3 which shows the dialog without a title bar). container = container_widget_helper() container.layout().addWidget(central) buttonbox = QDialogButtonBox(QDialogButtonBox.Close) buttonbox.rejected.connect(self.close) container.layout().addWidget(buttonbox) self.setCentralWidget(container) self.stack = central # General Tab tab = QWidget() self.addTab(tab, self.tr("General"), toolTip=self.tr("General Options")) form = FormLayout() tab.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) nodes = QWidget(self, objectName="nodes") nodes.setLayout(QVBoxLayout()) nodes.layout().setContentsMargins(0, 0, 0, 0) cb_anim = QCheckBox( self.tr("Enable node animations"), objectName="enable-node-animations", toolTip=self.tr("Enable shadow and ping animations for nodes " "in the workflow.") ) self.bind(cb_anim, "checked", "schemeedit/enable-node-animations") nodes.layout().addWidget(cb_anim) form.addRow(self.tr("Nodes"), nodes) links = QWidget(self, objectName="links") links.setLayout(QVBoxLayout()) links.layout().setContentsMargins(0, 0, 0, 0) cb_show = QCheckBox( self.tr("Show channel names between widgets"), objectName="show-channel-names", toolTip=self.tr("Show source and sink channel names " "over the links.") ) self.bind(cb_show, "checked", "schemeedit/show-channel-names") links.layout().addWidget(cb_show) form.addRow(self.tr("Links"), links) quickmenu = QWidget(self, objectName="quickmenu-options") quickmenu.setLayout(QVBoxLayout()) quickmenu.layout().setContentsMargins(0, 0, 0, 0) cb1 = QCheckBox(self.tr("Open on double click"), toolTip=self.tr("Open quick menu on a double click " "on an empty spot in the canvas")) cb2 = QCheckBox(self.tr("Open on right click"), toolTip=self.tr("Open quick menu on a right click " "on an empty spot in the canvas")) cb3 = QCheckBox(self.tr("Open on space key press"), toolTip=self.tr("Open quick menu on Space key press " "while the mouse is hovering over the canvas.")) cb4 = QCheckBox(self.tr("Open on any key press"), toolTip=self.tr("Open quick menu on any key press " "while the mouse is hovering over the canvas.")) cb5 = QCheckBox(self.tr("Show categories"), toolTip=self.tr("In addition to searching, allow filtering " "by categories.")) self.bind(cb1, "checked", "quickmenu/trigger-on-double-click") self.bind(cb2, "checked", "quickmenu/trigger-on-right-click") self.bind(cb3, "checked", "quickmenu/trigger-on-space-key") self.bind(cb4, "checked", "quickmenu/trigger-on-any-key") self.bind(cb5, "checked", "quickmenu/show-categories") quickmenu.layout().addWidget(cb1) quickmenu.layout().addWidget(cb2) quickmenu.layout().addWidget(cb3) quickmenu.layout().addWidget(cb4) quickmenu.layout().addWidget(cb5) form.addRow(self.tr("Quick menu"), quickmenu) startup = QWidget(self, objectName="startup-group") startup.setLayout(QVBoxLayout()) startup.layout().setContentsMargins(0, 0, 0, 0) cb_splash = QCheckBox(self.tr("Show splash screen"), self, objectName="show-splash-screen") cb_welcome = QCheckBox(self.tr("Show welcome screen"), self, objectName="show-welcome-screen") cb_crash = QCheckBox(self.tr("Load crashed scratch workflows"), self, objectName="load-crashed-workflows") self.bind(cb_splash, "checked", "startup/show-splash-screen") self.bind(cb_welcome, "checked", "startup/show-welcome-screen") self.bind(cb_crash, "checked", "startup/load-crashed-workflows") startup.layout().addWidget(cb_splash) startup.layout().addWidget(cb_welcome) startup.layout().addWidget(cb_crash) form.addRow(self.tr("On startup"), startup) toolbox = QWidget(self, objectName="toolbox-group") toolbox.setLayout(QVBoxLayout()) toolbox.layout().setContentsMargins(0, 0, 0, 0) exclusive = QCheckBox(self.tr("Only one tab can be open at a time")) self.bind(exclusive, "checked", "mainwindow/toolbox-dock-exclusive") toolbox.layout().addWidget(exclusive) form.addRow(self.tr("Tool box"), toolbox) tab.setLayout(form) # Style tab tab = StyleConfigWidget() self.addTab(tab, self.tr("&Style"), toolTip="Application style") self.bind(tab, "selectedStyle_", "application-style/style-name") self.bind(tab, "selectedPalette_", "application-style/palette") # Output Tab tab = QWidget() self.addTab(tab, self.tr("Output"), toolTip="Output Redirection") form = FormLayout() combo = QComboBox() combo.addItems([self.tr("Critical"), self.tr("Error"), self.tr("Warn"), self.tr("Info"), self.tr("Debug")]) self.bind(combo, "currentIndex", "logging/level") form.addRow(self.tr("Logging"), combo) box = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) cb1 = QCheckBox(self.tr("Open in external browser"), objectName="open-in-external-browser") self.bind(cb1, "checked", "help/open-in-external-browser") layout.addWidget(cb1) box.setLayout(layout) form.addRow(self.tr("Help window"), box) tab.setLayout(form) # Categories Tab tab = QWidget() layout = QVBoxLayout() view = QListView( editTriggers=QListView.NoEditTriggers ) from .. import registry reg = registry.global_registry() model = QStandardItemModel() settings = QSettings() for cat in reg.categories(): item = QStandardItem() item.setText(cat.name) item.setCheckable(True) visible, _ = category_state(cat, settings) item.setCheckState(Qt.Checked if visible else Qt.Unchecked) model.appendRow([item]) view.setModel(model) layout.addWidget(view) tab.setLayout(layout) model.itemChanged.connect( lambda item: save_category_state( reg.category(str(item.text())), _State(item.checkState() == Qt.Checked, -1), settings ) ) self.addTab(tab, "Categories") # Add-ons Tab tab = QWidget() self.addTab(tab, self.tr("Add-ons"), toolTip="Settings related to add-on installation") form = FormLayout() conda = QWidget(self, objectName="conda-group") conda.setLayout(QVBoxLayout()) conda.layout().setContentsMargins(0, 0, 0, 0) cb_conda_install = QCheckBox(self.tr("Install add-ons with conda"), self, objectName="allow-conda") self.bind(cb_conda_install, "checked", "add-ons/allow-conda") conda.layout().addWidget(cb_conda_install) form.addRow(self.tr("Conda"), conda) form.addRow(self.tr("Pip"), QLabel("Pip install arguments:")) line_edit_pip = QLineEdit() self.bind(line_edit_pip, "text", "add-ons/pip-install-arguments") form.addRow("", line_edit_pip) tab.setLayout(form) # Network Tab tab = QWidget() self.addTab(tab, self.tr("Network"), toolTip="Settings related to networking") form = FormLayout() line_edit_http_proxy = QLineEdit() self.bind(line_edit_http_proxy, "text", "network/http-proxy") form.addRow("HTTP proxy:", line_edit_http_proxy) line_edit_https_proxy = QLineEdit() self.bind(line_edit_https_proxy, "text", "network/https-proxy") form.addRow("HTTPS proxy:", line_edit_https_proxy) tab.setLayout(form) if self.__macUnified: # Need some sensible size otherwise mac unified toolbar 'takes' # the space that should be used for layout of the contents self.adjustSize() def addTab(self, widget, text, toolTip=None, icon=None): if self.__macUnified: action = QAction(text, self) if toolTip: action.setToolTip(toolTip) if icon: action.setIcon(toolTip) action.setData(len(self.tab.actions())) self.tab.addAction(action) self.stack.addWidget(widget) else: i = self.tab.addTab(widget, text) if toolTip: self.tab.setTabToolTip(i, toolTip) if icon: self.tab.setTabIcon(i, icon) def setCurrentIndex(self, index: int): if self.__macUnified: self.stack.setCurrentIndex(index) else: self.tab.setCurrentIndex(index) def widget(self, index): if self.__macUnified: return self.stack.widget(index) else: return self.tab.widget(index) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.hide() self.deleteLater() def bind(self, source, source_property, key, transformer=None): target = UserDefaultsPropertyBinding(self.__settings, key) source = PropertyBinding(source, source_property) source.set(target.get()) self._manager.bind(target, source) def commit(self): self._manager.commit() def revert(self): self._manager.revert() def reset(self): for target, source in self._manager.bindings(): try: source.reset() except NotImplementedError: # Cannot reset. pass except Exception: log.error("Error reseting %r", source.propertyName, exc_info=True) def exec(self): self.__loop = QEventLoop() self.show() status = self.__loop.exec() self.__loop = None refresh_proxies() return status def exec_(self, *args, **kwargs): warnings.warn( "exec_ is deprecated, use exec", DeprecationWarning, stacklevel=2 ) return self.exec(*args, **kwargs) def hideEvent(self, event): super().hideEvent(event) if self.__loop is not None: self.__loop.exit(0) self.__loop = None def __macOnToolBarAction(self, action): index = action.data() self.stack.setCurrentIndex(index)
class CategoryPopupMenu(FramelessWindow): """ A menu popup from which nodes can be dragged or clicked/activated. """ triggered = Signal(QAction) hovered = Signal(QAction) def __init__(self, parent=None, **kwargs): # type: (Optional[QWidget], Any) -> None super().__init__(parent, **kwargs) self.setWindowFlags(self.windowFlags() | Qt.Popup) layout = QVBoxLayout() layout.setContentsMargins(6, 6, 6, 6) self.__menu = MenuPage() self.__menu.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE) if sys.platform == "darwin": self.__menu.view().setAttribute(Qt.WA_MacShowFocusRect, False) self.__menu.triggered.connect(self.__onTriggered) self.__menu.hovered.connect(self.hovered) self.__dragListener = ItemViewDragStartEventListener(self) self.__dragListener.dragStarted.connect(self.__onDragStarted) self.__menu.view().viewport().installEventFilter(self.__dragListener) self.__menu.view().installEventFilter(self) layout.addWidget(self.__menu) self.setLayout(layout) self.__action = None # type: Optional[QAction] self.__loop = None # type: Optional[QEventLoop] def setCategoryItem(self, item): """ Set the category root item (:class:`QStandardItem`). """ warnings.warn( "setCategoryItem is deprecated. Use the more general 'setModel'" "and setRootIndex", DeprecationWarning, stacklevel=2) model = item.model() self.__menu.setModel(model) self.__menu.setRootIndex(item.index()) def setModel(self, model): # type: (QAbstractItemModel) -> None """ Set the model. Parameters ---------- model : QAbstractItemModel """ self.__menu.setModel(model) def setRootIndex(self, index): # type: (QModelIndex) -> None """ Set the root index in `model`. Parameters ---------- index : QModelIndex """ self.__menu.setRootIndex(index) def setActionRole(self, role): # type: (Qt.ItemDataRole) -> None """ Set the action role in model. This is an item role in `model` that returns a QAction for the item. Parameters ---------- role : Qt.ItemDataRole """ self.__menu.setActionRole(role) def popup(self, pos=None): # type: (Optional[QPoint]) -> None """ Show the popup at `pos`. Parameters ---------- pos : Optional[QPoint] The position in global screen coordinates """ if pos is None: pos = self.pos() self.adjustSize() geom = widget_popup_geometry(pos, self) self.setGeometry(geom) self.show() self.__menu.view().setFocus() def exec(self, pos=None): # type: (Optional[QPoint]) -> Optional[QAction] self.popup(pos) self.__loop = QEventLoop() self.__action = None self.__loop.exec() self.__loop = None if self.__action is not None: action = self.__action else: action = None return action def exec_(self, *args, **kwargs): warnings.warn("exec_ is deprecated, use exec", DeprecationWarning, stacklevel=2) return self.exec(*args, **kwargs) def hideEvent(self, event): # type: (QHideEvent) -> None if self.__loop is not None: self.__loop.exit(0) super().hideEvent(event) def __onTriggered(self, action): # type: (QAction) -> None self.__action = action self.triggered.emit(action) self.hide() if self.__loop: self.__loop.exit(0) def __onDragStarted(self, index): # type: (QModelIndex) -> None desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) icon = index.data(Qt.DecorationRole) drag_data = QMimeData() drag_data.setData( "application/vnd.orange-canvas.registry.qualified-name", desc.qualified_name.encode('utf-8')) drag = QDrag(self) drag.setPixmap(icon.pixmap(38)) drag.setMimeData(drag_data) # TODO: Should animate (accept) hide. self.hide() # When a drag is started and the menu hidden the item's tool tip # can still show for a short time UNDER the cursor preventing a # drop. viewport = self.__menu.view().viewport() filter = ToolTipEventFilter() viewport.installEventFilter(filter) drag.exec(Qt.CopyAction) viewport.removeEventFilter(filter) def eventFilter(self, obj, event): if isinstance(obj, QTreeView) and event.type() == QEvent.KeyPress: key = event.key() if key in [Qt.Key_Return, Qt.Key_Enter]: curr = obj.currentIndex() if curr.isValid(): obj.activated.emit(curr) return True return super().eventFilter(obj, event)