import sys
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 import utils as qt_utils
ENABLED_ROLE = Qt.UserRole - 1  # This is the standard Qt role enable state
WHITE_BRUSH = QtGui.QBrush(Qt.white)
[docs]class MacMultiComboBoxStyle(QtWidgets.QProxyStyle):
[docs]    def subControlRect(self, control, option, subControl, widget):
        """
        On Mac, add extra width to the popup for the checkbox.
        """
        rect = super().subControlRect(control, option, subControl, widget)
        if (control == QtWidgets.QStyle.CC_ComboBox and
                subControl == QtWidgets.QStyle.SC_ComboBoxListBoxPopup):
            rect.setWidth(rect.width() + 20)
        return rect  
[docs]class MultiComboBox(QtWidgets.QComboBox):
    """
    A combo box that allows multiple items to be selected.  Check marks are put
    next to each selected item.
    :ivar selectionChanged: A signal emitted whenever an item is selected or
        deselected.
    :vartype selectionChanged: `PyQt5.QtCore.pyqtSignal`
    """
    selectionChanged = QtCore.pyqtSignal()
    popupClosed = QtCore.pyqtSignal()
[docs]    def __init__(self, parent=None, include_all_and_none=False, delimiter=", "):
        """
        :param parent: The Qt parent widget
        :type parent: `PyQt5.QtWidgets.QWidget`
        :param include_all_and_none: Whether "All" and "None" options should be
            added to the top of the item list.
        :type include_all_and_none: bool
        :param delimiter: The delimiter to use when listing all selected items
        :type delimiter: str
        """
        super(MultiComboBox, self).__init__(parent)
        self.setModel(QtGui.QStandardItemModel())
        # Must use the styled delegate for the checkboxes to render on Mac.
        # Same Qt bug as http://stackoverflow.com/questions/28112433
        self.setItemDelegate(QtWidgets.QStyledItemDelegate(self))
        if sys.platform == 'darwin':
            # Set style with extra width in the popup to prevent cutoff text
            # See PANEL-15196
            self.setStyle(MacMultiComboBoxStyle())
        self._delimiter = delimiter
        self.currentIndexChanged[int].connect(self._itemSelected)
        self._first_select = True
        self._all_index = None
        self._none_index = None
        if include_all_and_none:
            self.addAllAndNone()
        self.view().viewport().installEventFilter(self)
        popup = self.findChild(QtWidgets.QFrame, None,
                               Qt.FindDirectChildrenOnly)
        # "popup" is a QComboBoxPrivateContainer instance. BIOLUM-3581
        # Over-riding of hidePopup() signal in can not be used for this
        # purpose with Qt5, as it's not called on Mac when user clicks on
        # an area outside the pop-up to close it. See QTBUG-50055.
        popup.resetButton.connect(self.popupClosed) 
[docs]    def eventFilter(self, viewport, event):
        # Event handler for keeping the pop-up open when items are selected.
        # See Qt documentation for method documentation
        if event.type() == event.MouseButtonRelease:
            if self.ignore_next_release:
                self.ignore_next_release = False
                return True
            index = self.view().indexAt(event.pos()).row()
            if self.isIndexEnabled(index):
                self._itemSelected(index)
            return True
        return False 
    def _addItem(self, text, checkable):
        item = QtGui.QStandardItem(text)
        if checkable:
            item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled |
                          Qt.ItemIsUserCheckable)
            item.setData(Qt.Unchecked, Qt.CheckStateRole)
        # Needed to overwrite the Maestro style, otherwise some rows will
        # have grey background after mouse hovers over them:
        # item.setData(WHITE_BRUSH, Qt.BackgroundRole)
        # NOTE: For now leaving this out as ideally we should fix Maestro's
        # style sheet. It's possible that different colors are desired on
        # different platforms.
        self.model().appendRow(item)
[docs]    def addItems(self, items):
        # See Qt documentation for method documentation
        for text in items:
            self._addItem(text, checkable=True) 
[docs]    def addAllAndNone(self):
        """
        Append "All" and "None" options to the item list
        """
        if self._all_index is None:
            self._all_index = self.count()
            self._addItem('All', checkable=False)
        if self._none_index is None:
            self._none_index = self.count()
            self._addItem('None', checkable=False) 
[docs]    def setDelimiter(self, delimiter):
        """
        Change the delimiter used when listing all selected items
        :param delimiter: The delimeter to use
        :type delimiter: str
        """
        self._delimiter = delimiter
        self.update() 
    def _itemSelected(self, index):
        """
        Respond to a new item being selected
        :param index: The index of the selected item
        :type index: int
        """
        if self._first_select:
            # The first item to be added will be auto-selected, which we don't
            # want.  To avoid this, we ignore the first selection event
            self._first_select = False
        elif index == self._all_index:
            self.selectAllItems()
        elif index == self._none_index:
            self.clearSelection()
        else:
            self._toggleSelection(index)
        # Clear the current index so the currentIndexChanged signal will always
        # be emitted even if the user clicks on the same item twice in a row
        with qt_utils.suppress_signals(self):
            self.setCurrentIndex(-1)
[docs]    def selectAllItems(self):
        """
        Select all listed items
        """
        self.setAllItemsSelected(True) 
[docs]    def clearSelection(self):
        """
        Deselect all listed items
        """
        self.setAllItemsSelected(False) 
[docs]    def setAllItemsSelected(self, selected=True):
        """
        Select or deselect all listed items
        :param selected: Whether to select or deselect
        :type selected: bool
        """
        for index in range(self.count()):
            if index not in (self._all_index, self._none_index):
                if selected and not self.isIndexEnabled(index):
                    continue
                self._setIndexChecked(index, selected)
        self.update()
        self.selectionChanged.emit() 
[docs]    def setItemSelected(self, item, selected=True):
        """
        Set the selection status of the specified item
        :param item: The item to modify
        :type item: str
        :param selected: Whether to select or deselect
        :type selected: bool
        :raise ValueError: If the specified item does not exist or if it's
            "All" or "None"
        """
        index = self.findText(item)
        if index == -1:
            raise ValueError("Specified item not found")
        self.setIndexSelected(index, selected) 
    def _setIndexChecked(self, index, checked):
        """
        Private method to set the state of the combo item.
        """
        item = self.model().item(index)
        check_state = Qt.Checked if checked else Qt.Unchecked
        item.setCheckState(check_state)
[docs]    def setIndexSelected(self, index, selected=True):
        """
        Set the selection status of the specified index
        :param index: The index of the item to modify
        :type index: int
        :param selected: Whether to select or deselect
        :type selected: bool
        :raise ValueError: IF the specified index corresponds to "All" or "None"
        """
        if index >= self.count():
            raise ValueError("Specified index not found")
        if index in (self._all_index, self._none_index):
            raise ValueError("Cannot select All or None")
        self._setIndexChecked(index, selected)
        self.update()
        self.selectionChanged.emit() 
[docs]    def isIndexSelected(self, index):
        """
        :param index: The index of the item to check.
        :type index: int
        :return: Whether the item is selected/checked.
        :rtype: bool
        """
        item = self.model().item(index)
        return (item.checkState() == Qt.Checked) 
[docs]    def setIndexEnabled(self, index, enable):
        """
        Set the enable state of the specified index.
        :param index: The index of the item to modify
        :type index: int
        :param enable: Whether to enable or not.
        :type enable: bool
        """
        value = None if enable else 0
        self.setItemData(index, value, ENABLED_ROLE) 
[docs]    def isIndexEnabled(self, index):
        """
        Return the enabled status for the given index.
        :param index: The index of the item to check.
        :type index: int
        :return: Whether the item is enabled.
        :rtype: bool
        """
        value = self.itemData(index, ENABLED_ROLE)
        return (value != 0) 
[docs]    def setSelectedItems(self, items):
        """
        Select the specified items.  All other items will be deselected.
        :param items: The list of items to select (as strings)
        :type items: list
        :raise ValueError: If any specified item does not exist
        """
        indexes = [self.findText(cur_item) for cur_item in items]
        if -1 in indexes:
            raise ValueError("Not all specified items found")
        self.setSelectedIndexes(indexes) 
[docs]    def setSelectedIndexes(self, indexes):
        """
        Select the specified indexes.  All other items will be deselected.
        :param indexes: The list of indexes to select (as ints)
        :type indexes: list
        """
        count = self.count()
        if any(i >= count for i in indexes):
            raise ValueError("Specified index not found")
        if self._all_index in indexes or self._none_index in indexes:
            raise ValueError("Cannot select All or None")
        self.clearSelection()
        for index in indexes:
            self._setIndexChecked(index, True)
        self.update()
        self.selectionChanged.emit() 
[docs]    def getSelectedItems(self):
        """
        Return a list of all selected items
        :return: All selected items (as strings)
        :rtype: list
        """
        return [
            self.itemText(i)
            for i in range(self.count())
            if self.isIndexSelected(i)
        ] 
[docs]    def items(self):
        """
        Return a list of all item texts except "All" and "None"
        :return: All items texts except "All" and "None"
        :rtype: list
        """
        return [
            self.itemText(i)
            for i in range(self.count())
            if i not in (self._all_index, self._none_index)
        ] 
[docs]    def getSelectedIndexes(self):
        """
        Return a list of all selected indexes
        :return: All selected indexes (as ints)
        :rtype: list
        """
        return [i for i in range(self.count()) if self.isIndexSelected(i)] 
    def _toggleSelection(self, index):
        """
        Toggle the selection status for the specified index
        :param index: The index to toggle
        :type index: int
        """
        check = not self.isIndexSelected(index)
        self._setIndexChecked(index, check)
        self.selectionChanged.emit()
[docs]    def currentText(self):
        # See Qt documentation for method documentation
        selected = self.getSelectedItems()
        text = self._delimiter.join(selected)
        return text 
[docs]    def paintEvent(self, event):
        """
        See Qt documentation for method documentation
        :note: The C++ paintEvent() and initStyleOption() methods won't call a
            Python implementation of currentText() or initStyleOption(), presumably
            due to a bug in PyQt.  To get around this, we reimplement the
            paintEvent() method in Python and force it to call the Python
            currentText() method
        """
        painter = QtWidgets.QStylePainter(self)
        palette = self.palette()
        painter.setPen(palette.color(palette.Text))
        opt = QtWidgets.QStyleOptionComboBox()
        self.initStyleOption(opt)
        opt.currentText = self.currentText()
        painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
        painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt) 
[docs]    def clear(self, keep_all_and_none=True):
        """
        Clear all items from the combo box
        :param keep_all_and_none: If True, the "All" and "None" items added via
            `addAllAndNone` will be kept
        :type keep_all_and_none: bool
        """
        if not keep_all_and_none or (self._all_index is None and
                                     self._none_index is None):
            super(MultiComboBox, self).clear()
            # If we've erased all items from the combo box, make sure to set
            # _first_select so we don't inadvertently auto-select the next item
            # added
            self._first_select = True
            self._all_index = None
            self._none_index = None
        else:
            for index in reversed(range(self.count())):
                if index not in (self._all_index, self._none_index):
                    super(MultiComboBox, self).removeItem(index)
        self.selectionChanged.emit() 
[docs]    def removeItem(self, index):
        # See Qt documentation for method documentation
        if self._all_index is not None:
            if index == self._all_index:
                self._all_index = None
            elif index < self._all_index:
                self._all_index -= 1
        if self._none_index is not None:
            if index == self._none_index:
                self._none_index = None
            elif index < self._none_index:
                self._none_index -= 1
        super(MultiComboBox, self).removeItem(index)
        if self.count() == 0:
            # If we've erased all items from the combo box, make sure to set
            # _first_select so we don't inadvertently auto-select the next item
            # added
            self._first_select = True
        self.update()
        self.selectionChanged.emit() 
[docs]    def addAndSelectItem(self, text):
        """
        Add a new item with the specified text and select it
        :param text: The text of the item to add
        :type text: str
        """
        index = self.count()
        self.addItem(text)
        self._setIndexChecked(index, True)
        self.update()
        self.selectionChanged.emit() 
[docs]    def af2SettingsGetValue(self):
        """
        This function adds support for the settings mixin. It allows to
        save checked item states in case this combo box is included in a
        settings panel.
        :return: List of selected rows.
        :rtype: list
        """
        return self.getSelectedIndexes() 
[docs]    def af2SettingsSetValue(self, indexes):
        """
        This function adds support for the settings mixin. It allows to
        set combo box check states when this table is included in a
        settings panel.
        :param indexes: List of rows to check.
        :type indexes: list
        """
        self.setSelectedIndexes(indexes)  
if __name__ == "__main__":
    # Demonstrate how to use this widget:
    app = QtWidgets.QApplication(sys.argv)
    combo = MultiComboBox()
    combo.addItems(['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'])
    combo.setSelectedIndexes([1, 2])
    combo.setIndexEnabled(3, False)
    combo.addAllAndNone()
    combo.show()
    sys.exit(app.exec_())