Source code for schrodinger.ui.qt.pop_up_widgets

"""
Widgets for creating a line edit with a pop up editor.  To create a new pop up
editor:

- Create a subclass of `PopUp`.  In `setup()`, define and layout the desired
  widgets.
- Instantiate `LineEditWithPopUp` for a stand-alone line edit or
  `PopUpDelegate` for a table delegate. `__init__` takes the `PopUp` subclass
  as an argument.
"""

import enum
import sys
import weakref

import schrodinger.ui.qt.utils as qt_utils
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt.standard.icons import icons
from schrodinger.ui.qt.standard_widgets import hyperlink

PopUpAlignment = enum.Enum("PopUpAlignment", ["Center", "Right"])
(REJECT, ACCEPT, ACCEPT_MULTI, UNKNOWN) = list(range(4))
"""
Constants representing what event triggered the closing of a popup. These enums
are emitted by the popUpClosing signal.
- REJECT if the user closed the pop up by pressing Esc
- ACCEPT if the user closed the pop up by hitting Enter or by shifting
  focus
- ACCEPT_MULTI if the user closed the pop up by hitting Ctrl+Enter
- UNKNOWN if the event that closed the widget is not known (this happens
    when we use the Qt.PopUp window flag to do the popup closing for us)
"""





class _AbstractPopUpEventFilter(QtCore.QObject):
    """
    An event filter that will hide or resize the `PopUp` when appropriate.
    """

    def __init__(self, parent):
        """
        :param parent: The widget with a pop up
        :type parent: `_WidgetWithPopUpMixin`
        """

        super(_AbstractPopUpEventFilter, self).__init__(parent)
        # use a weakref to avoid a circular reference, since the widget will
        # hold a reference to this object
        self._widget = weakref.proxy(parent)
        self._pop_up = weakref.proxy(parent._pop_up)


class _LostFocusEventFilter(_AbstractPopUpEventFilter):

    def eventFilter(self, obj, event):
        """
        Hide the pop up if it and the widget have lost focus or if the user
        hits enter or escape.  If the pop up is closed, the popUpClosed signal
        is emitted with a constant representing how the popup was closed.
        See the docstrings at the top of pop_up_widgets for more information.

        - REJECT if the user closed the pop up by pressing Esc
        - ACCEPT if the user closed the pop up by hitting Enter or by shifting
          focus
        - ACCEPT_MULTI if the user closed the pop up by hitting Ctrl+Enter
        - UNKNOWN if the event that closed the widget is not known (this happens
            when we use the Qt.PopUp window flag to do the popup closing for us)
        """

        lost_focus = (
            event.type() == event.FocusOut and
            not (self._widget.hasFocus() or self._pop_up.subWidgetHasFocus()))
        hide = event.type() == event.Hide and obj is self._widget
        if hide or lost_focus:
            self._pop_up.hide()
            self._widget.popUpClosing.emit(ACCEPT)
            # Don't return True, since other objects may also want to handle
            # this event

        elif (event.type() == event.KeyPress and
              event.key() in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape) and
              not isinstance(obj, QtWidgets.QListView)):
            # If the user hit enter or escape.  Note that we ignore key
            # presses on the combo box list views, since that are used
            # to select an entry from the list

            if event.key() == Qt.Key_Escape:
                emit_with = REJECT
            elif event.modifiers() & Qt.ControlModifier:
                emit_with = ACCEPT_MULTI
            else:
                emit_with = ACCEPT

            self._widget.setFocus()
            self._pop_up.hide()
            self._widget.popUpClosing.emit(emit_with)
            return True

        return False


class _WindowEventFilter(_AbstractPopUpEventFilter):

    def eventFilter(self, obj, event):
        """
        Hide the pop up if the user clicks away from it.  When the pop up is
        closed, the pop_up_closed signal is emitted with ACCEPT.  Also
        recalculate the pop up's position if the window is resized.

        Note: MousePressEvents will be swallowed by widgets with viewports.
        The _LostFocusEventFilter should catch these occurences and close
        the popup anyways.

        See Qt documentation for an explanation of arguments and return value.
        """

        if event.type() == event.MouseButtonPress and self._pop_up.isVisible():
            global_pos = event.globalPos()
            pop_up_pos = self._pop_up.mapFromGlobal(global_pos)
            pop_up_rect = self._pop_up.contentsRect()
            line_edit_pos = self._widget.mapFromGlobal(global_pos)
            line_edit_rect = self._widget.contentsRect()
            if not (pop_up_rect.contains(pop_up_pos) or
                    line_edit_rect.contains(line_edit_pos)):
                self._widget.setFocus()
                self._pop_up.hide()
                self._widget.popUpClosing.emit(ACCEPT)
        elif event.type() == event.Resize:
            # If the window is resized, it's possible that there's no longer
            # enough room for the pop up in its current position.
            self._widget._setPopUpGeometry()
        return False


class _MoveEventFilter(_AbstractPopUpEventFilter):

    def eventFilter(self, obj, event):
        """
        Recalculate the pop up's position if a parent widget moves.
        """

        if event.type() == event.Move:
            self._widget._setPopUpGeometry()
        return False


class _AbstractWidgetWithPopUpMixin:
    """
    Mixin for a widget class that should produce a popup. Includes a framework
    for setting the size and position of the popup frame. Subclasses must
    implement a `_setPopUpGeometry()` method.
    """

    ALIGN_TOP, ALIGN_BOTTOM, ALIGN_RIGHT, ALIGN_LEFT, ALIGN_AUTO = list(
        range(5))

    def __init__(self, parent, pop_up_class=None):
        super().__init__(parent)
        self._popup_halign = self.ALIGN_AUTO
        self._popup_valign = self.ALIGN_AUTO
        if pop_up_class:
            self.setPopUpClass(pop_up_class)
        else:
            self._pop_up = None

    def setPopUpClass(self, pop_up_class):
        """
        If a pop up class was not specified via the constructor, use this
        method to set it after the fact. Useful for placing widgets into
        `*.ui` files.
        """
        pop_up_widget = pop_up_class(self.parent().window())
        self.setPopUp(pop_up_widget)

    def setPopUp(self, pop_up):
        """
        Set the pop up widget to the specified pop up widget instance.

        :type pop_up: Instance to set as the pop up widget.
        :type pop_up: PopUp
        """
        self._pop_up = pop_up
        self._pop_up.dataChanged.connect(self.popUpUpdated)
        self._pop_up.popUpResized.connect(self._setPopUpGeometry)
        self._pop_up.hide()

    def setPopupHalign(self, popup_halign):
        """
        Specify whether the pop up should have its right edge aligned with the
        right edge of the widget (ALIGN_RIGHT), have its left edge aligned
        with the left edge of the widget (ALIGN_LEFT), or have it's
        horizontal alignment determined automatically (ALIGN_AUTO).  Note that
        this setting is moot if the widget is wider than the pop up's size
        hint, as the pop up will be extended to the same width as the widget.

        :param popup_halign: The desired horizontal alignment of the pop up.
            Must be one of ALIGN_RIGHT, ALIGN_LEFT, or ALIGN_AUTO.
        :type popup_halign: int
        """

        if popup_halign not in (self.ALIGN_LEFT, self.ALIGN_RIGHT,
                                self.ALIGN_AUTO):
            err = "Unrecognized value for popup_halign: %s" % self._popup_halign
            raise ValueError(err)
        self._popup_halign = popup_halign
        self._setPopUpGeometry()

    def setPopupValign(self, popup_valign):
        """
        Specify whether the pop up should appear above (ALIGN_TOP), below
        (ALIGN_BOTTOM) the widget, or have it's vertical alignment determined
        automatically (ALIGN_AUTO).

        :param popup_valign: The desired vertical alignment of the pop up.
            Must be either ALIGN_TOP, ALIGN_BOTTOM, or ALIGN_AUTO.
        :type popup_valign: int
        """

        if popup_valign not in (self.ALIGN_TOP, self.ALIGN_BOTTOM,
                                self.ALIGN_AUTO):
            err = "Unrecognized value for popup_valign: %s" % self._popup_halign
            raise ValueError(err)
        self._popup_valign = popup_valign
        self._setPopUpGeometry()

    def moveEvent(self, event):
        """
        Update the pop up position and size when the widget is moved
        """

        self._setPopUpGeometry()
        return super().moveEvent(event)

    def resizeEvent(self, event):
        """
        Update the pop up position and size when the widget is resized
        """

        self._setPopUpGeometry()
        return super().resizeEvent(event)

    def _setPopUpGeometry(self):
        """
        Determine the appropriate position and size for the pop up.  Note that
        the pop up will never be narrower than the widget.
        """
        raise NotImplementedError

    def popUpUpdated(self, text):
        """
        Whenever the pop up emits the dataChanged signal, update the widget.
        This function should be implemented in subclasses if required.

        :param text: The text emitted with the dataChanged signal
        :type text: str
        """


class _WidgetWithPopUpMixin(_AbstractWidgetWithPopUpMixin):
    """
    Container for methods shared between ComboBoxWithPopUp and LineEditWithPopUp
    classes.
    """

    def __init__(self, parent, pop_up_class=None):
        super().__init__(parent, pop_up_class)
        self._first_show = True

    def setPopUp(self, pop_up):
        # See super class for method documentation.
        super().setPopUp(pop_up)
        parent = self.parent()
        window = parent.window()
        self._focus_filter = _LostFocusEventFilter(self)
        self.installEventFilter(self._focus_filter)
        self._pop_up.installPopUpEventFilter(self._focus_filter)
        self._window_filter = _WindowEventFilter(self)
        window.installEventFilter(self._window_filter)

        # Install an event filter on every widget in the hierarchy up to the
        # panel so that if any of them are moved, the the pop up will be moved
        # as well.
        self._move_event_filter = _MoveEventFilter(self)
        cur_widget = parent
        while cur_widget is not None and not cur_widget.isWindow():
            cur_widget.installEventFilter(self._move_event_filter)
            cur_widget = cur_widget.parent()

    def _setPopUpGeometry(self):
        """
        Determine the appropriate position and size for the pop up.  Note that
        the pop up will never be narrower than the widget.
        """

        halign, valign = self._getPopupAlignment()
        rect = self.geometry()
        le_height = rect.height()
        le_width = rect.width()
        le_right = rect.right()

        pop_up_size = self._pop_up.sizeHint()
        pop_up_height = pop_up_size.height()
        pop_up_width = pop_up_size.width()

        rect.setHeight(pop_up_height)
        if pop_up_width > le_width:
            rect.setWidth(pop_up_width)

        if halign == self.ALIGN_RIGHT:
            new_right = rect.right()
            x_trans = le_right - new_right
        elif halign == self.ALIGN_LEFT:
            x_trans = 0

        if valign == self.ALIGN_BOTTOM:
            y_trans = le_height
        elif valign == self.ALIGN_TOP:
            y_trans = -rect.height()
        rect.translate(x_trans, y_trans)

        # translate rect so it's in the coordinate system of the pop up's parent
        # (which is the window)
        new_topleft = self.parent().mapTo(self._pop_up.parent(), rect.topLeft())
        rect.moveTopLeft(new_topleft)
        self._pop_up.setGeometry(rect)

    def _getPopupAlignment(self):
        """
        Get the horizontal and vertical alignment of the pop up.  If either
        alignment is set to ALIGN_AUTO, alignment will be determined based on
        the current widget placement in the window.

        :return: A tuple of:
              - the horizontal alignment (either ALIGN_LEFT or ALIGN_RIGHT)
              - the vertical alignement (either ALIGN_TOP or ALIGN_BOTTOM)
        :rtype: tuple
        """

        parent = self.parent()
        window = self.window()
        rect = self.geometry()
        topleft = rect.topLeft()
        bottomright = rect.bottomRight()
        if parent is not window:
            # Make sure that values are in the coordinate system of the window
            topleft = parent.mapTo(window, topleft)
            bottomright = parent.mapTo(window, bottomright)
        if self._popup_halign == self.ALIGN_AUTO:
            le_left = topleft.x()
            le_right = bottomright.x()
            pop_up_width = self._pop_up.estimateMaxWidth()
            window_width = self.window().width()
            halign = self._autoPopupAlignment(le_left, self.ALIGN_LEFT,
                                              le_right, self.ALIGN_RIGHT,
                                              pop_up_width, window_width)
        else:
            halign = self._popup_halign

        if self._popup_valign == self.ALIGN_AUTO:
            le_top = topleft.y()
            le_bottom = bottomright.y()
            pop_up_height = self._pop_up.estimateMaxHeight()
            window_height = self.window().height()
            valign = self._autoPopupAlignment(le_bottom, self.ALIGN_BOTTOM,
                                              le_top, self.ALIGN_TOP,
                                              pop_up_height, window_height)
        else:
            valign = self._popup_valign

        return halign, valign

    def _autoPopupAlignment(self, le_bottom, align_bottom, le_top, align_top,
                            max_pop_up_height, window_height):
        """
        Determine the appropriate pop up placement based on the window geometry.
        Note that variable names here refer to vertical alignment, but this
        function is also used to determine horizontal alignment (bottom -> left,
        top -> right).

        :param le_bottom: The bottom (or left) coordinate of the widget
        :type le_bottom: int

        :param align_bottom: The flag value for bottom (or left) alignment
        :type align_bottom: int

        :param le_top: The top (or right) coordinate of the widget
        :type le_top: int

        :param align_top: The flag value for top (or right) alignment
        :type align_top: int

        :param max_pop_up_height: The maximum height (or width) of the pop up
        :type max_pop_up_height: int

        :param window_height: The height (or width) of the window containing
            this widget
        :type window_height: int

        :return: The flag value for the appropriate pop up alignment
        :rtype: int
        """

        top_if_up = le_top - max_pop_up_height
        bottom_if_down = le_bottom + max_pop_up_height
        past_bottom_by = bottom_if_down - window_height
        past_top_by = -top_if_up
        if past_bottom_by < 0 or past_bottom_by < past_top_by:
            return align_bottom
        else:
            return align_top

    def showEvent(self, event):
        """
        Update the pop up position and size when the widget is shown
        """

        if self._first_show:
            # If this is the first time the widget is being shown, then it's
            # location hasn't been initialized yet.  It will think it's at
            # location (0, 0) until after the first draw.  As such we use a
            # single shot timer to wait until after the draw to position the pop
            # up.
            QtCore.QTimer.singleShot(0, self._setPopUpGeometry)
            self._first_show = False
        else:
            self._setPopUpGeometry()
        super(_WidgetWithPopUpMixin, self).showEvent(event)


[docs]class LineEditWithPopUp(_WidgetWithPopUpMixin, QtWidgets.QLineEdit): """ A line edit with a pop up that appears whenever the line edit has focus. :ivar popUpClosing: A signal emitted when the pop up is closed. :vartype popUpClosing: `PyQt5.QtCore.pyqtSignal` The signal is emitted with: - REJECT if the user closed the pop up by pressing Esc - ACCEPT if the user closed the pop up by hitting Enter or by shifting focus - ACCEPT_MULTI if the user closed the pop up by hitting Ctrl+Enter :ivar _pop_up: The pop up widget :vartype _pop_up: `PopUp` """ popUpClosing = QtCore.pyqtSignal(int)
[docs] def __init__(self, parent, pop_up_class): """ :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param pop_up_class: The class of the pop up widget. Should be a subclass of `PopUp`. :type pop_up_class: type """ super().__init__(parent, pop_up_class) self.textChanged.connect(self.textUpdated) self.textUpdated("") self._pop_up.hide()
[docs] def focusInEvent(self, event): """ When the line edit receives focus, show the pop up """ self._pop_up.show() super(LineEditWithPopUp, self).focusInEvent(event)
[docs] def mousePressEvent(self, event): """ If the user clicks on the line edit and it already has focus, show the pop up again (in case the user hid it with a key press) """ if self.hasFocus(): self._pop_up.show() super(LineEditWithPopUp, self).mousePressEvent(event)
[docs] def textUpdated(self, text): """ Whenever the text in the line edit is changed, show the pop up and call `PopUp.lineEditUpdated`. The default implementation prevents the `PopUp` from sending signals during the execution of `PopUp.lineEditUpdated`. This prevents an infinite loop of `PopUp.lineEditUpdated` and `LineEditWithPopUp.popUpUpdated`. :param text: The current text in the line edit :type text: str """ self._pop_up.show() with qt_utils.suppress_signals(self._pop_up): self._pop_up.lineEditUpdated(text)
[docs]class ComboBoxWithPopUp(_WidgetWithPopUpMixin, QtWidgets.QComboBox): """ A combo box with a pop up that appears whenever the menu is pressed. :ivar popUpClosing: A signal emitted when the pop up is closed. :vartype popUpClosing: `PyQt5.QtCore.pyqtSignal` The signal is emitted with: - REJECT if the user closed the pop up by pressing Esc - ACCEPT if the user closed the pop up by hitting Enter or by shifting focus - ACCEPT_MULTI if the user closed the pop up by hitting Ctrl+Enter :ivar _pop_up: The pop up widget :vartype _pop_up: `PopUp` """ popUpClosing = QtCore.pyqtSignal(int)
[docs] def showPopup(self): if self._pop_up is not None: self._pop_up.show()
[docs] def hidePopup(self): if self._pop_up is not None: self._pop_up.hide() super().hidePopup()
[docs]class PopUpDelegate(QtWidgets.QStyledItemDelegate): """ A table delegate that uses a `LineEditWithPopUp` as an editor. :ivar commitDataToSelected: Commit the data from the current editor to all selected cells. Only emitted if the class is instantiated with `enable_accept_multi=True`. This signal (and behavior) is not a standard Qt behavior, so the table view must manually connect this signal and respond to it appropriately. This signal is emitted with the editor, the current index, and the delegate. :vartype commitDataToSelected: `PyQt5.QtCore.pyqtSignal` """ commitDataToSelected = QtCore.pyqtSignal(QtWidgets.QWidget, QtCore.QModelIndex, QtWidgets.QAbstractItemDelegate)
[docs] def __init__(self, parent, pop_up_class, enable_accept_multi=False): """ :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param pop_up_class: The class of the pop up widget. Should be a subclass of `PopUp`. :type pop_up_class: type :param enable_accept_multi: Whether committing data to all selected cells at once is enabled. If True, `commitDataToSelected` will be emitted when the `LineEditWithPopUp` emits `popUpClosing` with `ACCEPT_MULTI`. If False, `commitData` will be emitted instead. :type enable_accept_multi: bool """ super(PopUpDelegate, self).__init__(parent) self._pop_up_class = pop_up_class self._enable_accept_multi = enable_accept_multi
[docs] def createEditor(self, parent, option, index): """ Create the editor and connect the `popUpClosing` signal. If a subclass needs to modify editor instantiation, `_createEditor` should be reimplemented instead of this function to ensure that the `popUpClosing` signal is connected properly. See Qt documentation for an explanation of the arguments and return value. """ editor = self._createEditor(parent, option, index) editor.index = index editor.popUpClosing.connect(lambda x: self.popUpClosed(editor, x)) return editor
def _createEditor(self, parent, option, index): """ Create and return the `LineEditWithPopUp` editor. If a subclass needs to modify editor instantiation, this function should be reimplemented instead of `createEditor` to ensure that the `popUpClosing` signal is connected properly. See Qt createEditor documentation for an explanation of the arguments and return value. """ return LineEditWithPopUp(parent, self._pop_up_class)
[docs] def setEditorData(self, editor, index): # See Qt documentation value = index.data() editor.setText(value)
[docs] def setModelData(self, editor, model, index): # See Qt documentation if editor.hasAcceptableInput(): value = editor.text() model.setData(index, value)
[docs] def eventFilter(self, editor, event): """ Ignore the editor losing focus, since focus may be switching to one of the pop up widgets. If the editor including the popup loses focus, popUpClosed will be called. See Qt documentation for an explanation of the arguments and return value. """ if isinstance(event, QtGui.QFocusEvent) and event.lostFocus(): return False else: return super(PopUpDelegate, self).eventFilter(editor, event)
[docs] def popUpClosed(self, editor, accept): """ Respond to the editor closing by either rejecting or accepting the data. If `enable_accept_multi` is True, the data may also be committed to all selected rows. :param editor: The editor that was just closed :type editor: `LineEditWithPopUp` :param accept: The signal that was emitted by the editor when it closed :type accept: int """ if accept == ACCEPT: self.commitData.emit(editor) elif accept == ACCEPT_MULTI: if self._enable_accept_multi: self.commitDataToSelected.emit(editor, editor.index, self) else: self.commitData.emit(editor) self.closeEditor.emit(editor, self.NoHint)
class _AbstractButtonWithPopUp(_AbstractWidgetWithPopUpMixin): """ A mixin to allow for checkable buttons with a pop up that appears whenever the button is pressed. Note that when the pop up is visible, the button is set to be "checked", which is supposed to change the appearance of push buttons or tool buttons on certain platforms. Note: This mixin should be used with subclasses of `QtWidgets.QAbstractButton`. :ivar popUpClosing: A signal emitted when the pop up is closed. :vartype popUpClosing: `PyQt5.QtCore.pyqtSignal` The signal is emitted with: - REJECT if the user closed the pop up by pressing Esc - ACCEPT if the user closed the pop up by hitting Enter or by shifting focus - ACCEPT_MULTI if the user closed the pop up by hitting Ctrl+Enter - UNKNOWN if the event that closed the widget is not known (this happens when we use the Qt.PopUp window flag to do the popup closing for us) :ivar _pop_up: The pop up widget :vartype _pop_up: `PopUp` """ popUpClosing = QtCore.pyqtSignal(int) def __init__(self, parent, pop_up_class=None): """ :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param pop_up_class: The class of the pop up widget. Should be a subclass of `PopUp`. :type pop_up_class: type """ super().__init__(parent, pop_up_class) if not hasattr(self, 'clicked'): msg = ('This mixin should be used with classes that implement a' ' "clicked" signal.') raise TypeError(msg) self.clicked.connect(self.onClicked) self.setCheckable(True) self.setChecked(False) def setPopUp(self, pop_up): # See super class for method documentation. super().setPopUp(pop_up) self._pop_up.visibilityChanged.connect(self._onPopUpVisibilityChanged) self._pop_up.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup) def onClicked(self): """ If the button is clicked, toggle the check state of the button, which should toggle the visibility of the pop up. """ self.pop_up_visible = not self.pop_up_visible if self.pop_up_visible: # Qt toggles the UnderMouse attribute whenever there's an # enter or leave event. When the popup is visible, the button # won't receive any leave events leaving it in a "hovered" # state even after closing the popup. To prevent this, # we just manually set the attribute to False when we open # the popup. self.setAttribute(Qt.WA_UnderMouse, False) @property def pop_up_visible(self): """ :return: whether the pop up frame is visible to its parent :rtype: bool """ if self._pop_up is None: return False pop_up_parent = self._pop_up.parent() return self._pop_up.isVisibleTo(pop_up_parent) @pop_up_visible.setter def pop_up_visible(self, visible): """ :param visible: whether the pop up should be visible :type visible: bool """ self._pop_up.setVisible(visible) # Although the check state should be changed when the pop up # visibility changes, this is sometimes unreliable (such as when # pop up parent class is not visible, and therefore showEvent() is # never called and the visibilityChanged signal is not emitted) self._onPopUpVisibilityChanged(visible) def _onPopUpVisibilityChanged(self, visible): """ When the pop up visibility changes, respond by changing the check state of the button and positioning the pop up appropriately. The button should be checked if and only if the pop up is visible, for appearance purposes, even if this is subclassed with a button type that is not generally considered "checkable", such as a `QPushButton` or a `QToolButton`. :param visible: whether the pop up is now visible :type visible: bool """ if self.isChecked() != visible: self.setChecked(visible) if visible: self._setPopUpGeometry() if not visible: self.popUpClosing.emit(UNKNOWN) def _setPopUpGeometry(self): """ Position the popup frame according to the specified alignment settings. """ pop_up = self._pop_up if pop_up is None: return pop_up_height, pop_up_width = pop_up.height(), pop_up.width() btn_height, btn_width = self.height(), self.width() btn_pos = self.mapToGlobal(QtCore.QPoint(0, 0)) btn_x, btn_y = btn_pos.x(), btn_pos.y() if self._popup_valign in [self.ALIGN_TOP, self.ALIGN_AUTO]: # Place pop up directly above button popup_new_y = btn_y - pop_up_height elif self._popup_valign == self.ALIGN_BOTTOM: # Place pop up directly below button popup_new_y = btn_y + btn_height if self._popup_halign == self.ALIGN_LEFT: # Align right edge of pop up with right edge of button popup_new_x = btn_x + btn_width - pop_up_width elif self._popup_halign == self.ALIGN_AUTO: # Align middle of pop up with middle of button popup_new_x = btn_x + 0.5 * (btn_width - pop_up_width) elif self._popup_halign == self.ALIGN_RIGHT: # Align left edge of pop up with left edge of button popup_new_x = btn_x pop_up.move(popup_new_x, popup_new_y) def setChecked(self, checked): """ Set the button check state, and also alter the pop up visibility. Note that the button should be checked if and only if the pop up is visible, and that changing the visibility of the pop up (e.g. by clicking the button) will also change the check state. :param checked: whether the button should be checked :type checked: bool """ super().setChecked(checked) if self.pop_up_visible != checked: self.pop_up_visible = checked
[docs]class PushButtonWithPopUp(_AbstractButtonWithPopUp, QtWidgets.QPushButton): """ A checkable push button with a pop up that appears whenever the button is pressed. """
[docs] def __init__(self, parent, pop_up_class=None): super().__init__(parent, pop_up_class)
[docs]class PushButtonWithIndicatorAndPopUp(_AbstractButtonWithPopUp, hyperlink.ButtonWithArrowMixin, QtWidgets.QPushButton): """ A push button with a menu indicator arrow which shows a pop up when the button is pressed. """ pass
[docs]class LinkButtonWithPopUp(_AbstractButtonWithPopUp, hyperlink.ButtonWithArrowMixin, hyperlink.MenuLink): """ A push button that is rendered as a blue link, with a pop up that appears whenever the button is pressed. See schrodinger.ui.qt.standard_widgets.hyperlink.MenuLink """ pass
[docs]class ToolButtonWithPopUp(_AbstractButtonWithPopUp, QtWidgets.QToolButton): """ A checkable tool button with a pop up that appears whenever the button is pressed. """
[docs] def __init__(self, parent, pop_up_class=None, arrow_type=Qt.UpArrow, text=None): """ :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param pop_up_class: The class of the pop up widget. Should be a subclass of `PopUp`. :type pop_up_class: type :param arrow_type: Type of arrow to display in the button :type arrow_type: `Qt.ArrowType` :param text: Text to set for this button. If not specified, only an icon will be shown. :type text: str """ super().__init__(parent, pop_up_class) self.setArrowType(arrow_type) if text is not None: self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.setText(text)
[docs]class AddButtonWithIndicatorAndPopUp(PushButtonWithIndicatorAndPopUp): """ PushButton with a "+" icon and "Add" text. Button also has a menu indicator which shows a pop up when clicked. """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, *kwargs) self.setIcon(QtGui.QIcon(icons.ADD_LB)) self.setIconSize(QtCore.QSize(20, 20)) self.setText("Add") if sys.platform.startswith("darwin"): self.setFixedWidth(90) else: self.setFixedWidth(75) self.setStyleSheet("QPushButton {text-align:left;}")