def _QTest_qWaitForWindowActive(widget, timeout=1000): # A Qt5 compatible (probably) QTest.qWaitForWindowActive(QWidget, int) # (mostly copied from qtestsystem.h in qt5/qtbase) from AnyQt.QtCore import \ Qt, QCoreApplication, QEventLoop, QElapsedTimer, QEvent window = widget.window() timer = QElapsedTimer() timer.start() while not window.isActiveWindow(): remaining = timeout - timer.elapsed() if remaining <= 0: break QCoreApplication.processEvents(QEventLoop.AllEvents, remaining) QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete) QTest.qSleep(10) # See the explanation in qtestsystem.h if window.isActiveWindow(): wait_no = 0 while window.pos().isNull(): if wait_no > timeout // 10: break wait_no += 1 QTest.qWait(10) return window.isActiveWindow()
def get_output(self, widget, signal_name, timeout=DEFAULT_TIMEOUT): if not isinstance(signal_name, str): signal_name = signal_name.name elapsed = QElapsedTimer() if widget.isInvalidated(): elapsed.start() spy = QSignalSpy(widget.invalidatedStateChanged) assert spy.wait(timeout) timeout = timeout - elapsed.elapsed() value = self.outputs.get((widget, signal_name)) if isinstance(value, _Invalidated) and timeout >= 0: spy = QSignalSpy(value.completed) assert spy.wait(timeout), "Failed to get output in the specified timeout" assert len(spy) == 1 value = spy[0][0] return value
def _QTest_qWaitForWindowExposed(widget, timeout=1000): # A Qt5 compatible (probably) QTest.qWaitForWindowExposed(QWidget, int) # (mostly copied from qtestsystem.h in qt5/qtbase) from AnyQt.QtCore import \ Qt, QCoreApplication, QEventLoop, QElapsedTimer, QEvent window = widget.window() timer = QElapsedTimer() timer.start() # Is widget.testAttribute(Qt.WA_Mapped) a suitable replacement for # QWindow.isExposed in Qt5?? # Not exactly. In Qt5 # window().testAttribute(Qt.WA_Mapped) == window().windowHandle.isExposed() # but both are False if a window is fully obscured by other windows, # in Qt4 there is no difference if a window is obscured. while not window.testAttribute(Qt.WA_Mapped): remaining = timeout - timer.elapsed() if remaining <= 0: break QCoreApplication.processEvents(QEventLoop.AllEvents, remaining) QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete) QTest.qSleep(10) return window.testAttribute(Qt.WA_Mapped)
class ComboBoxSearch(QComboBox): """ A drop down list combo box with filter/search. The popup list view is filtered by text entered in the filter field. Note ---- `popup`, `lineEdit` and `completer` from the base QComboBox class are unused. Setting/modifying them will have no effect. """ # NOTE: Setting editable + QComboBox.NoInsert policy + ... did not achieve # the same results. def __init__(self, parent=None, **kwargs): self.__maximumContentsLength = MAXIMUM_CONTENTS_LENGTH self.__searchline = QLineEdit(visible=False, frame=False) self.__searchline.setAttribute(Qt.WA_MacShowFocusRect, False) self.__popup = None # type: Optional[QAbstractItemModel] self.__proxy = None # type: Optional[QSortFilterProxyModel] self.__popupTimer = QElapsedTimer() super().__init__(parent, **kwargs) self.__searchline.setParent(self) self.__searchline.setFocusProxy(self) self.setFocusPolicy(Qt.ClickFocus | Qt.TabFocus) def setMaximumContentsLength(self, length): # type: (int) -> None """ Set the maximum contents length hint. The hint specifies the upper bound on the `sizeHint` and `minimumSizeHint` width specified in character length. Set to 0 or negative value to disable. Note ---- This property does not affect the widget's `maximumSize`. The widget can still grow depending on its `sizePolicy`. Parameters ---------- length : int Maximum contents length hint. """ if self.__maximumContentsLength != length: self.__maximumContentsLength = length self.updateGeometry() def _get_size_hint(self): sh = super().sizeHint() if self.__maximumContentsLength > 0: width = ( self.fontMetrics().width("X") * self.__maximumContentsLength + self.iconSize().width() + 4 ) sh = sh.boundedTo(QSize(width, sh.height())) return sh def sizeHint(self): # type: () -> QSize # reimplemented return self._get_size_hint() def minimumSizeHint(self): # type: () -> QSize # reimplemented return self._get_size_hint() def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart() def hidePopup(self): """Reimplemented""" if self.__popup is not None: popup = self.__popup self.__popup = self.__proxy = None popup.setFocusProxy(None) popup.hide() popup.deleteLater() popup.removeEventFilter(self) popup.viewport().removeEventFilter(self) # need to call base hidePopup even though the base showPopup was not # called (update internal state wrt. 'pressed' arrow, ...) super().hidePopup() self.__searchline.hide() self.update() def initStyleOption(self, option): # type: (QStyleOptionComboBox) -> None super().initStyleOption(option) option.editable = True def __updateGeometries(self): opt = QStyleOptionComboBox() self.initStyleOption(opt) editarea = self.style().subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self) self.__searchline.setGeometry(editarea) def resizeEvent(self, event): """Reimplemented.""" super().resizeEvent(event) self.__updateGeometries() def paintEvent(self, event): """Reimplemented.""" opt = QStyleOptionComboBox() self.initStyleOption(opt) painter = QStylePainter(self) painter.drawComplexControl(QStyle.CC_ComboBox, opt) if not self.__searchline.isVisibleTo(self): opt.editable = False painter.drawControl(QStyle.CE_ComboBoxLabel, opt) def eventFilter(self, obj, event): # pylint: disable=too-many-branches # type: (QObject, QEvent) -> bool """Reimplemented.""" etype = event.type() if etype == QEvent.FocusOut and self.__popup is not None: self.hidePopup() return True if etype == QEvent.Hide and self.__popup is not None: self.hidePopup() return False if etype == QEvent.KeyPress or etype == QEvent.KeyRelease or \ etype == QEvent.ShortcutOverride and obj is self.__popup: event = event # type: QKeyEvent key, modifiers = event.key(), event.modifiers() if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select): current = self.__popup.currentIndex() if current.isValid(): self.__activateProxyIndex(current) elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return False # elif key in (Qt.Key_Tab, Qt.Key_Backtab): pass elif key == Qt.Key_Escape or \ (key == Qt.Key_F4 and modifiers & Qt.AltModifier): self.__popup.hide() return True else: # pass the input events to the filter edit line (no propagation # up the parent chain). self.__searchline.event(event) if event.isAccepted(): return True if etype == QEvent.MouseButtonRelease and self.__popup is not None \ and obj is self.__popup.viewport() \ and self.__popupTimer.elapsed() >= \ QApplication.doubleClickInterval(): event = event # type: QMouseEvent index = self.__popup.indexAt(event.pos()) if index.isValid(): self.__activateProxyIndex(index) if etype == QEvent.MouseMove and self.__popup is not None \ and obj is self.__popup.viewport(): event = event # type: QMouseEvent opt = QStyleOptionComboBox() self.initStyleOption(opt) style = self.style() # type: QStyle if style.styleHint(QStyle.SH_ComboBox_ListMouseTracking, opt, self): index = self.__popup.indexAt(event.pos()) if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.__popup.setCurrentIndex(index) if etype == QEvent.MouseButtonPress and self.__popup is obj: # Popup border or out of window mouse button press/release. # At least on windows this needs to be handled. style = self.style() opt = QStyleOptionComboBox() self.initStyleOption(opt) opt.subControls = QStyle.SC_All opt.activeSubControls = QStyle.SC_ComboBoxArrow pos = self.mapFromGlobal(event.globalPos()) sc = style.hitTestComplexControl(QStyle.CC_ComboBox, opt, pos, self) if sc != QStyle.SC_None: self.__popup.setAttribute(Qt.WA_NoMouseReplay) self.hidePopup() return super().eventFilter(obj, event) def __activateProxyIndex(self, index): # type: (QModelIndex) -> None # Set current and activate the source index corresponding to the proxy # index in the popup's model. if self.__popup is not None and index.isValid(): proxy = self.__popup.model() assert index.model() is proxy index = proxy.mapToSource(index) assert index.model() is self.model() if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.hidePopup() text = self.itemText(index.row()) self.setCurrentIndex(index.row()) self.activated[int].emit(index.row()) self.activated[str].emit(text)
class ComboBoxSearch(QComboBox): """ A drop down list combo box with filter/search. The popup list view is filtered by text entered in the filter field. Note ---- `popup`, `lineEdit` and `completer` from the base QComboBox class are unused. Setting/modifying them will have no effect. """ # NOTE: Setting editable + QComboBox.NoInsert policy + ... did not achieve # the same results. def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__searchline = QLineEdit(self, visible=False, frame=False) self.__searchline.setAttribute(Qt.WA_MacShowFocusRect, False) self.__searchline.setFocusProxy(self) self.__popup = None # type: Optional[QAbstractItemModel] self.__proxy = None # type: Optional[QSortFilterProxyModel] self.__popupTimer = QElapsedTimer() self.setFocusPolicy(Qt.ClickFocus | Qt.TabFocus) def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart() def hidePopup(self): """Reimplemented""" if self.__popup is not None: popup = self.__popup self.__popup = self.__proxy = None popup.setFocusProxy(None) popup.hide() popup.deleteLater() popup.removeEventFilter(self) popup.viewport().removeEventFilter(self) # need to call base hidePopup even though the base showPopup was not # called (update internal state wrt. 'pressed' arrow, ...) super().hidePopup() self.__searchline.hide() self.update() def initStyleOption(self, option): # type: (QStyleOptionComboBox) -> None super().initStyleOption(option) option.editable = True def __updateGeometries(self): opt = QStyleOptionComboBox() self.initStyleOption(opt) editarea = self.style().subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self) self.__searchline.setGeometry(editarea) def resizeEvent(self, event): """Reimplemented.""" super().resizeEvent(event) self.__updateGeometries() def paintEvent(self, event): """Reimplemented.""" opt = QStyleOptionComboBox() self.initStyleOption(opt) painter = QStylePainter(self) painter.drawComplexControl(QStyle.CC_ComboBox, opt) if not self.__searchline.isVisibleTo(self): opt.editable = False painter.drawControl(QStyle.CE_ComboBoxLabel, opt) def eventFilter(self, obj, event): # pylint: disable=too-many-branches # type: (QObject, QEvent) -> bool """Reimplemented.""" etype = event.type() if etype == QEvent.FocusOut and self.__popup is not None: self.hidePopup() return True if etype == QEvent.Hide and self.__popup is not None: self.hidePopup() return False if etype == QEvent.KeyPress or etype == QEvent.KeyRelease or \ etype == QEvent.ShortcutOverride and obj is self.__popup: event = event # type: QKeyEvent key, modifiers = event.key(), event.modifiers() if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select): current = self.__popup.currentIndex() if current.isValid(): self.__activateProxyIndex(current) elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return False # elif key in (Qt.Key_Tab, Qt.Key_Backtab): pass elif key == Qt.Key_Escape or \ (key == Qt.Key_F4 and modifiers & Qt.AltModifier): self.__popup.hide() return True else: # pass the input events to the filter edit line (no propagation # up the parent chain). self.__searchline.event(event) if event.isAccepted(): return True if etype == QEvent.MouseButtonRelease and self.__popup is not None \ and obj is self.__popup.viewport() \ and self.__popupTimer.elapsed() >= \ QApplication.doubleClickInterval(): event = event # type: QMouseEvent index = self.__popup.indexAt(event.pos()) if index.isValid(): self.__activateProxyIndex(index) if etype == QEvent.MouseMove and self.__popup is not None \ and obj is self.__popup.viewport(): event = event # type: QMouseEvent opt = QStyleOptionComboBox() self.initStyleOption(opt) style = self.style() # type: QStyle if style.styleHint(QStyle.SH_ComboBox_ListMouseTracking, opt, self): index = self.__popup.indexAt(event.pos()) if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.__popup.setCurrentIndex(index) if etype == QEvent.MouseButtonPress and self.__popup is obj: # Popup border or out of window mouse button press/release. # At least on windows this needs to be handled. style = self.style() opt = QStyleOptionComboBox() self.initStyleOption(opt) opt.subControls = QStyle.SC_All opt.activeSubControls = QStyle.SC_ComboBoxArrow pos = self.mapFromGlobal(event.globalPos()) sc = style.hitTestComplexControl(QStyle.CC_ComboBox, opt, pos, self) if sc != QStyle.SC_None: self.__popup.setAttribute(Qt.WA_NoMouseReplay) self.hidePopup() return super().eventFilter(obj, event) def __activateProxyIndex(self, index): # type: (QModelIndex) -> None # Set current and activate the source index corresponding to the proxy # index in the popup's model. if self.__popup is not None and index.isValid(): proxy = self.__popup.model() assert index.model() is proxy index = proxy.mapToSource(index) assert index.model() is self.model() if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.hidePopup() text = self.itemText(index.row()) self.setCurrentIndex(index.row()) self.activated[int].emit(index.row()) self.activated[str].emit(text)