Search code examples
pythonpython-2.7mayapyside2

Rebuilding a window


I am using PySide2 to rebuild a window like seen in the screenshot(what you see in the screenshot is an example, Ueli App launcher, available on Github), it's an app launcher, but since I am very new to PySide2 I do not know a lot of functions and asking here for help, where I should look into.

What I need: I type something into the textfield(on the screenshot where it says 'test') and options that fulfill my conditions get appended to UI so that I can select them with the arrow keys and press enter to open/run the program/file. I have trouble with all the UI related stuff, the searching for the commands is something I already have.

Ueli App launcher, available on Github

Here is some code I already have(it is for Maya, the 3D Software):

from PySide2 import QtWidgets, QtCore
from PySide2.QtGui import *
from PySide2.QtWidgets import QDesktopWidget, QScrollArea
from maya import OpenMayaUI
from functools import partial
import sys
try:
    from shiboken import wrapInstance
    import shiboken
except:
    from shiboken2 import wrapInstance
    import shiboken2 as shiboken

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        window = OpenMayaUI.MQtUtil.mainWindow()
        mayaWindow = shiboken.wrapInstance(long(window), QtWidgets.QMainWindow)
        super(MainWindow, self).__init__(mayaWindow)

        self.setWindowTitle('Test Window')
        self.resize(630, 50);
        self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.WindowType.NoDropShadowWindowHint)
        self.setStyleSheet("background-color: rgb(65, 65, 65);")

        # main widget
        main_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(main_widget)

        # layout initialize
        self.boxLayout = QtWidgets.QVBoxLayout()
        main_widget.setLayout(self.boxLayout)
        self.formLayout = QtWidgets.QFormLayout()

        # Add Widgets
        self.textField = QtWidgets.QLineEdit()
        self.textField.setFont(QFont('Helvetica', 16))
        self.textField.setStyleSheet("margin: 10px; padding: 10px; \
                                    background-color: \
                                    rgb(40,40,40);\
                                    color: rgb(245,245,245); \
                                    border-style: solid; \
                                    border-radius: 3px; \
                                    border-width: 0.5px; \
                                    border-color: \
                                    rgb(35,35,35);")
        self.textField.textChanged.connect(self.searchForCommands)
        self.formLayout.addRow(self.textField)

        # global layout setting
        self.boxLayout.addLayout(self.formLayout)
        self.centerWindow()

        self.textField.setFocus()

    def mousePressEvent(self, QMouseEvent):
        xPosition = QMouseEvent.pos().x()
        yPosition = QMouseEvent.pos().y()
        width = self.width()
        height = self.height()
        if xPosition > self.width() or xPosition < 0:
            self.destroy()
        if yPosition > self.height() or yPosition < 0:
            self.destroy()

    def centerWindow(self):
        qRect = self.frameGeometry()
        centerPoint = QDesktopWidget().availableGeometry().center()
        qRect.moveCenter(centerPoint)
        self.move(qRect.topLeft())

    def searchForCommands(self):
        test = ['Monkey', 'Giraffe', 'Dragon', 'Bull']

        #scrollArea = QScrollArea()
        #scrollArea.setWidget()
        #formLayout.addRow(self.textField)

        if self.textField.text().isspace() or self.textField.text() != ' ':
            if self.textField.text() == '':
                pass
            else:
                for x in test:
                    if self.textField.text() in x:
                        self.formLayout.addRow(QtWidgets.QPushButton(self.textField.text()))
                        print 'Input: "' + self.textField.text() + '" is in "' + x + '".'
                    if self.textField.text() == x:
                        print 'Input: "' + self.textField.text() + '" equals "' + x + '".'


if __name__ == '__main__':
    app = QtWidgets.QApplication.instance()
    if app is None: 
        app = QtWidgets.QApplication(sys.argv)

    w = MainWindow()
    w.show()
    #sys.exit(app.exec_())

Solution

  • A possible solution is to create a QListView with a delegate that implements custom painting, for the filter you could use a QSortFilterProxyModel as I show below:

    import os
    import sys
    
    from PySide2 import QtCore, QtGui, QtWidgets
    
    TitleRole = QtCore.Qt.UserRole + 1000
    DescriptionRole = QtCore.Qt.UserRole + 1001
    IconRole = QtCore.Qt.UserRole + 1002
    
    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
    
    
    def get_icon_path(name):
        return os.path.join(CURRENT_DIR, "img", name)
    
    
    class FilterProxyModel(QtCore.QSortFilterProxyModel):
        def __init__(self, parent=None):
            super(FilterProxyModel, self).__init__(parent)
            self._filterText = ""
    
        @property
        def filterText(self):
            return self._filterText
    
        @filterText.setter
        def filterText(self, text):
            self._filterText = text
            self.invalidateFilter()
    
        def filterAcceptsRow(self, sourceRow, sourceParent):
            if not self.filterText:
                return True
            index = self.sourceModel().index(
                sourceRow, self.filterKeyColumn(), sourceParent
            )
            # Returns True if the row must be displayed, otherwise False must be returned.
            title = index.data(TitleRole)
            description = index.data(DescriptionRole)
    
            return self.filterText.lower() in title.lower()
    
    
    class StandardItem(QtGui.QStandardItem):
        def __init__(self, title="", description="", icon=QtGui.QIcon()):
            super(StandardItem, self).__init__()
            self.title = title
            self.description = description
            self.icon = icon
    
        @property
        def title(self):
            return self.data(TitleRole)
    
        @title.setter
        def title(self, title):
            self.setData(title, TitleRole)
    
        @property
        def description(self):
            return self.data(DescriptionRole)
    
        @description.setter
        def description(self, description):
            self.setData(description, DescriptionRole)
    
        @property
        def icon(self):
            return self.data(IconRole)
    
        @icon.setter
        def icon(self, icon):
            self.setData(icon, IconRole)
    
    
    class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
        def sizeHint(self, option, index):
            return QtCore.QSize(50, 50)
    
        def paint(self, painter, option, index):
            super(StyledItemDelegate, self).paint(painter, option, index)
            title = index.data(TitleRole)
            description = index.data(DescriptionRole)
            icon = index.data(IconRole)
    
            mode = QtGui.QIcon.Normal
            if not (option.state & QtWidgets.QStyle.State_Enabled):
                mode = QtGui.QIcon.Disabled
            elif option.state & QtWidgets.QStyle.State_Selected:
                mode = QtGui.QIcon.Selected
    
            state = (
                QtGui.QIcon.On
                if option.state & QtWidgets.QStyle.State_Open
                else QtGui.QIcon.Off
            )
            iconRect = QtCore.QRect(option.rect)
            iconRect.setSize(QtCore.QSize(40, 40))
            icon.paint(
                painter, iconRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, mode, state
            )
    
            titleFont = QtGui.QFont(option.font)
            titleFont.setPixelSize(20)
            fm = QtGui.QFontMetrics(titleFont)
            titleRect = QtCore.QRect(option.rect)
            titleRect.setLeft(iconRect.right())
            titleRect.setHeight(fm.height())
    
            color = (
                option.palette.color(QtGui.QPalette.BrightText)
                if option.state & QtWidgets.QStyle.State_Selected
                else option.palette.color(QtGui.QPalette.WindowText)
            )
            painter.save()
            painter.setFont(titleFont)
            pen = painter.pen()
            pen.setColor(color)
            painter.setPen(pen)
            painter.drawText(titleRect, title)
            painter.restore()
    
            descriptionFont = QtGui.QFont(option.font)
            descriptionFont.setPixelSize(15)
            fm = QtGui.QFontMetrics(descriptionFont)
            descriptionRect = QtCore.QRect(option.rect)
            descriptionRect.setTopLeft(titleRect.bottomLeft())
            descriptionRect.setHeight(fm.height())
            painter.save()
            painter.setFont(descriptionFont)
            pen = painter.pen()
            pen.setColor(color)
            painter.setPen(pen)
            painter.drawText(
                descriptionRect,
                fm.elidedText(description, QtCore.Qt.ElideRight, descriptionRect.width()),
            )
            painter.restore()
    
    
    class LauncherWidget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(LauncherWidget, self).__init__(parent)
            self.setWindowTitle("Test Window")
            self.setStyleSheet("background-color: rgb(65, 65, 65);")
            self.releaseKeyboard()
    
            self.textField = QtWidgets.QLineEdit()
            self.textField.textChanged.connect(self.onTextChanged)
            self.textField.setFont(QtGui.QFont("Helvetica", 16))
            self.textField.setStyleSheet(
                "margin: 10px; padding: 10px; \
                                        background-color: \
                                        rgb(40,40,40);\
                                        color: rgb(245,245,245); \
                                        border-style: solid; \
                                        border-radius: 3px; \
                                        border-width: 0.5px; \
                                        border-color: \
                                        rgb(35,35,35);"
            )
    
            self.model = QtGui.QStandardItemModel(self)
            self.proxymodel = FilterProxyModel(self)
            self.proxymodel.setSourceModel(self.model)
            self.listview = QtWidgets.QListView(
                editTriggers=QtWidgets.QAbstractItemView.NoEditTriggers
            )
            self.listview.setItemDelegate(StyledItemDelegate(self.listview))
            self.listview.setModel(self.proxymodel)
            self.listview.clicked.connect(self.onClicked)
            self.listview.selectionModel().currentChanged.connect(self.resetCurrentIndex)
    
            QtWidgets.QShortcut(
                QtGui.QKeySequence(QtCore.Qt.Key_Up), self, activated=self.moveUp
            )
            QtWidgets.QShortcut(
                QtGui.QKeySequence(QtCore.Qt.Key_Down), self, activated=self.moveDown
            )
    
            lay = QtWidgets.QVBoxLayout(self)
            lay.setContentsMargins(0, 0, 0, 0)
            lay.addWidget(self.textField, strecth=1)
            lay.addWidget(self.listview, strecth=0)
    
            self.centerWindow()
            self.fill_model()
    
            self.resetCurrentIndex()
    
        def fill_model(self):
            # https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json
            for title, description, path_icon in (
                (
                    "Hydrogen",
                    "Hydrogen is a chemical element with chemical symbol H and atomic number 1. With an atomic weight of 1.00794 u, hydrogen is the lightest element on the periodic table. Its monatomic form (H) is the most abundant chemical substance in the Universe, constituting roughly 75% of all baryonic mass.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Helium",
                    "Helium is a chemical element with symbol He and atomic number 2. It is a colorless, odorless, tasteless, non-toxic, inert, monatomic gas that heads the noble gas group in the periodic table. Its boiling and melting points are the lowest among all the elements.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Lithium",
                    'Lithium (from Greek:\u03bb\u03af\u03b8\u03bf\u03c2 lithos, "stone") is a chemical element with the symbol Li and atomic number 3. It is a soft, silver-white metal belonging to the alkali metal group of chemical elements. Under standard conditions it is the lightest metal and the least dense solid element.',
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Beryllium",
                    "Beryllium is a chemical element with symbol Be and atomic number 4. It is created through stellar nucleosynthesis and is a relatively rare element in the universe. It is a divalent element which occurs naturally only in combination with other elements in minerals.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Boron",
                    "Boron is a metalloid chemical element with symbol B and atomic number 5. Produced entirely by cosmic ray spallation and supernovae and not by stellar nucleosynthesis, it is a low-abundance element in both the Solar system and the Earth's crust. Boron is concentrated on Earth by the water-solubility of its more common naturally occurring compounds, the borate minerals.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Carbon",
                    'Carbon (from Latin:carbo "coal") is a chemical element with symbol C and atomic number 6. On the periodic table, it is the first (row 2) of six elements in column (group) 14, which have in common the composition of their outer electron shell. It is nonmetallic and tetravalent\u2014making four electrons available to form covalent chemical bonds.',
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Nitrogen",
                    "Nitrogen is a chemical element with symbol N and atomic number 7. It is the lightest pnictogen and at room temperature, it is a transparent, odorless diatomic gas. Nitrogen is a common element in the universe, estimated at about seventh in total abundance in the Milky Way and the Solar System.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Oxygen",
                    "Oxygen is a chemical element with symbol O and atomic number 8. It is a member of the chalcogen group on the periodic table and is a highly reactive nonmetal and oxidizing agent that readily forms compounds (notably oxides) with most elements. By mass, oxygen is the third-most abundant element in the universe, after hydrogen and helium.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Fluorine",
                    "Fluorine is a chemical element with symbol F and atomic number 9. It is the lightest halogen and exists as a highly toxic pale yellow diatomic gas at standard conditions. As the most electronegative element, it is extremely reactive:almost all other elements, including some noble gases, form compounds with fluorine.",
                    get_icon_path("so-icon.png"),
                ),
                (
                    "Neon",
                    "Neon is a chemical element with symbol Ne and atomic number 10. It is in group 18 (noble gases) of the periodic table. Neon is a colorless, odorless, inert monatomic gas under standard conditions, with about two-thirds the density of air.",
                    get_icon_path("so-icon.png"),
                ),
            ):
    
                it = StandardItem(
                    title=title, description=description, icon=QtGui.QIcon(path_icon),
                )
                self.model.appendRow(it)
    
        @QtCore.Slot()
        def resetCurrentIndex(self):
            if not self.listview.currentIndex().isValid():
                self.listview.setCurrentIndex(self.listview.model().index(0, 0))
    
        @QtCore.Slot()
        def moveUp(self):
            ix = self.listview.currentIndex()
            if ix.row() > 0:
                self.listview.setCurrentIndex(ix.sibling(ix.row() - 1, ix.column()))
    
        def moveDown(self):
            ix = self.listview.currentIndex()
            if ix.row() < (self.listview.model().rowCount() - 1):
                self.listview.setCurrentIndex(ix.sibling(ix.row() + 1, ix.column()))
    
        @QtCore.Slot(str)
        def onTextChanged(self, text):
            self.proxymodel.filterText = text
            self.resetCurrentIndex()
    
        @QtCore.Slot(QtCore.QModelIndex)
        def onClicked(self, index):
            ix = self.proxymodel.mapToSource(index)
            it = self.model.itemFromIndex(ix)
            if it is None:
                return
            print("clicked:", it.title, it.description)
    
        def centerWindow(self):
            self.setGeometry(
                QtWidgets.QStyle.alignedRect(
                    QtCore.Qt.LeftToRight,
                    QtCore.Qt.AlignCenter,
                    self.size(),
                    QtWidgets.qApp.desktop().availableGeometry(),
                )
            )
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        w = LauncherWidget()
        w.show()
        sys.exit(app.exec_())
    

    enter image description here