diff options
Diffstat (limited to 'examples/widgets/itemviews')
-rw-r--r-- | examples/widgets/itemviews/addressbook/adddialogwidget.py | 103 | ||||
-rw-r--r-- | examples/widgets/itemviews/addressbook/addressbook.py | 131 | ||||
-rw-r--r-- | examples/widgets/itemviews/addressbook/addresswidget.py | 249 | ||||
-rw-r--r-- | examples/widgets/itemviews/addressbook/newaddresstab.py | 94 | ||||
-rw-r--r-- | examples/widgets/itemviews/addressbook/tablemodel.py | 147 | ||||
-rwxr-xr-x | examples/widgets/itemviews/basicsortfiltermodel.py | 203 | ||||
-rwxr-xr-x | examples/widgets/itemviews/fetchmore.py | 148 | ||||
-rw-r--r-- | examples/widgets/itemviews/stardelegate/stardelegate.py | 174 | ||||
-rw-r--r-- | examples/widgets/itemviews/stardelegate/stareditor.py | 99 | ||||
-rw-r--r-- | examples/widgets/itemviews/stardelegate/starrating.py | 102 |
10 files changed, 1450 insertions, 0 deletions
diff --git a/examples/widgets/itemviews/addressbook/adddialogwidget.py b/examples/widgets/itemviews/addressbook/adddialogwidget.py new file mode 100644 index 000000000..c0dcaf6c0 --- /dev/null +++ b/examples/widgets/itemviews/addressbook/adddialogwidget.py @@ -0,0 +1,103 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtCore import Qt +from PySide2.QtWidgets import (QDialog, QLabel, QTextEdit, QLineEdit, + QDialogButtonBox, QGridLayout, QVBoxLayout) + +class AddDialogWidget(QDialog): + """ A dialog to add a new address to the addressbook. """ + + def __init__(self, parent=None): + super(AddDialogWidget, self).__init__(parent) + + nameLabel = QLabel("Name") + addressLabel = QLabel("Address") + buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | + QDialogButtonBox.Cancel) + + self.nameText = QLineEdit() + self.addressText = QTextEdit() + + grid = QGridLayout() + grid.setColumnStretch(1, 2) + grid.addWidget(nameLabel, 0, 0) + grid.addWidget(self.nameText, 0, 1) + grid.addWidget(addressLabel, 1, 0, Qt.AlignLeft | Qt.AlignTop) + grid.addWidget(self.addressText, 1, 1, Qt.AlignLeft) + + layout = QVBoxLayout() + layout.addLayout(grid) + layout.addWidget(buttonBox) + + self.setLayout(layout) + + self.setWindowTitle("Add a Contact") + + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + # These properties make using this dialog a little cleaner. It's much + # nicer to type "addDialog.address" to retrieve the address as compared + # to "addDialog.addressText.toPlainText()" + @property + def name(self): + return self.nameText.text() + + @property + def address(self): + return self.addressText.toPlainText() + + +if __name__ == "__main__": + import sys + from PySide2.QtWidgets import QApplication + + app = QApplication(sys.argv) + + dialog = AddDialogWidget() + if (dialog.exec_()): + name = dialog.name + address = dialog.address + print("Name:" + name) + print("Address:" + address) diff --git a/examples/widgets/itemviews/addressbook/addressbook.py b/examples/widgets/itemviews/addressbook/addressbook.py new file mode 100644 index 000000000..f8927be5e --- /dev/null +++ b/examples/widgets/itemviews/addressbook/addressbook.py @@ -0,0 +1,131 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtWidgets import (QMainWindow, QAction, QFileDialog, QApplication) + +from addresswidget import AddressWidget + + +class MainWindow(QMainWindow): + + def __init__(self, parent=None): + super(MainWindow, self).__init__(parent) + + self.addressWidget = AddressWidget() + self.setCentralWidget(self.addressWidget) + self.createMenus() + self.setWindowTitle("Address Book") + + def createMenus(self): + # Create the main menuBar menu items + fileMenu = self.menuBar().addMenu("&File") + toolMenu = self.menuBar().addMenu("&Tools") + + # Populate the File menu + openAction = self.createAction("&Open...", fileMenu, self.openFile) + saveAction = self.createAction("&Save As...", fileMenu, self.saveFile) + fileMenu.addSeparator() + exitAction = self.createAction("E&xit", fileMenu, self.close) + + # Populate the Tools menu + addAction = self.createAction("&Add Entry...", toolMenu, self.addressWidget.addEntry) + self.editAction = self.createAction("&Edit Entry...", toolMenu, self.addressWidget.editEntry) + toolMenu.addSeparator() + self.removeAction = self.createAction("&Remove Entry", toolMenu, self.addressWidget.removeEntry) + + # Disable the edit and remove menu items initially, as there are + # no items yet. + self.editAction.setEnabled(False) + self.removeAction.setEnabled(False) + + # Wire up the updateActions slot + self.addressWidget.selectionChanged.connect(self.updateActions) + + def createAction(self, text, menu, slot): + """ Helper function to save typing when populating menus + with action. + """ + action = QAction(text, self) + menu.addAction(action) + action.triggered.connect(slot) + return action + + # Quick gotcha: + # + # QFiledialog.getOpenFilename and QFileDialog.get.SaveFileName don't + # behave in PySide2 as they do in Qt, where they return a QString + # containing the filename. + # + # In PySide2, these functions return a tuple: (filename, filter) + + def openFile(self): + filename, _ = QFileDialog.getOpenFileName(self) + if filename: + self.addressWidget.readFromFile(filename) + + def saveFile(self): + filename, _ = QFileDialog.getSaveFileName(self) + if filename: + self.addressWidget.writeToFile(filename) + + def updateActions(self, selection): + """ Only allow the user to remove or edit an item if an item + is actually selected. + """ + indexes = selection.indexes() + + if len(indexes) > 0: + self.removeAction.setEnabled(True) + self.editAction.setEnabled(True) + else: + self.removeAction.setEnabled(False) + self.editAction.setEnabled(False) + + +if __name__ == "__main__": + """ Run the application. """ + import sys + app = QApplication(sys.argv) + mw = MainWindow() + mw.show() + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/addressbook/addresswidget.py b/examples/widgets/itemviews/addressbook/addresswidget.py new file mode 100644 index 000000000..7ecd42e7d --- /dev/null +++ b/examples/widgets/itemviews/addressbook/addresswidget.py @@ -0,0 +1,249 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +try: + import cpickle as pickle +except ImportError: + import pickle + +from PySide2.QtCore import (Qt, Signal, QRegExp, QModelIndex, + QItemSelection, QItemSelectionModel, QSortFilterProxyModel) +from PySide2.QtWidgets import (QWidget, QTabWidget, QMessageBox, QTableView, + QAbstractItemView) + +from tablemodel import TableModel +from newaddresstab import NewAddressTab +from adddialogwidget import AddDialogWidget + + +class AddressWidget(QTabWidget): + """ The central widget of the application. Most of the addressbook's + functionality is contained in this class. + """ + + selectionChanged = Signal(QItemSelection) + + def __init__(self, parent=None): + """ Initialize the AddressWidget. """ + super(AddressWidget, self).__init__(parent) + + self.tableModel = TableModel() + self.newAddressTab = NewAddressTab() + self.newAddressTab.sendDetails.connect(self.addEntry) + + self.addTab(self.newAddressTab, "Address Book") + + self.setupTabs() + + def addEntry(self, name=None, address=None): + """ Add an entry to the addressbook. """ + if name is None and address is None: + addDialog = AddDialogWidget() + + if addDialog.exec_(): + name = addDialog.name + address = addDialog.address + + address = {"name": name, "address": address} + addresses = self.tableModel.addresses[:] + + # The QT docs for this example state that what we're doing here + # is checking if the entered name already exists. What they + # (and we here) are actually doing is checking if the whole + # name/address pair exists already - ok for the purposes of this + # example, but obviously not how a real addressbook application + # should behave. + try: + addresses.remove(address) + QMessageBox.information(self, "Duplicate Name", + "The name \"%s\" already exists." % name) + except ValueError: + # The address didn't already exist, so let's add it to the model. + + # Step 1: create the row + self.tableModel.insertRows(0) + + # Step 2: get the index of the newly created row and use it. + # to set the name + ix = self.tableModel.index(0, 0, QModelIndex()) + self.tableModel.setData(ix, address["name"], Qt.EditRole) + + # Step 3: lather, rinse, repeat for the address. + ix = self.tableModel.index(0, 1, QModelIndex()) + self.tableModel.setData(ix, address["address"], Qt.EditRole) + + # Remove the newAddressTab, as we now have at least one + # address in the model. + self.removeTab(self.indexOf(self.newAddressTab)) + + # The screenshot for the QT example shows nicely formatted + # multiline cells, but the actual application doesn't behave + # quite so nicely, at least on Ubuntu. Here we resize the newly + # created row so that multiline addresses look reasonable. + tableView = self.currentWidget() + tableView.resizeRowToContents(ix.row()) + + def editEntry(self): + """ Edit an entry in the addressbook. """ + tableView = self.currentWidget() + proxyModel = tableView.model() + selectionModel = tableView.selectionModel() + + # Get the name and address of the currently selected row. + indexes = selectionModel.selectedRows() + + for index in indexes: + row = proxyModel.mapToSource(index).row() + ix = self.tableModel.index(row, 0, QModelIndex()) + name = self.tableModel.data(ix, Qt.DisplayRole) + ix = self.tableModel.index(row, 1, QModelIndex()) + address = self.tableModel.data(ix, Qt.DisplayRole) + + # Open an addDialogWidget, and only allow the user to edit the address. + addDialog = AddDialogWidget() + addDialog.setWindowTitle("Edit a Contact") + + addDialog.nameText.setReadOnly(True) + addDialog.nameText.setText(name) + addDialog.addressText.setText(address) + + # If the address is different, add it to the model. + if addDialog.exec_(): + newAddress = addDialog.address + if newAddress != address: + ix = self.tableModel.index(row, 1, QModelIndex()) + self.tableModel.setData(ix, newAddress, Qt.EditRole) + + def removeEntry(self): + """ Remove an entry from the addressbook. """ + tableView = self.currentWidget() + proxyModel = tableView.model() + selectionModel = tableView.selectionModel() + + # Just like editEntry, but this time remove the selected row. + indexes = selectionModel.selectedRows() + + for index in indexes: + row = proxyModel.mapToSource(index).row() + self.tableModel.removeRows(row) + + # If we've removed the last address in the model, display the + # newAddressTab + if self.tableModel.rowCount() == 0: + self.insertTab(0, self.newAddressTab, "Address Book") + + def setupTabs(self): + """ Setup the various tabs in the AddressWidget. """ + groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"] + + for group in groups: + proxyModel = QSortFilterProxyModel(self) + proxyModel.setSourceModel(self.tableModel) + proxyModel.setDynamicSortFilter(True) + + tableView = QTableView() + tableView.setModel(proxyModel) + tableView.setSortingEnabled(True) + tableView.setSelectionBehavior(QAbstractItemView.SelectRows) + tableView.horizontalHeader().setStretchLastSection(True) + tableView.verticalHeader().hide() + tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) + tableView.setSelectionMode(QAbstractItemView.SingleSelection) + + # This here be the magic: we use the group name (e.g. "ABC") to + # build the regex for the QSortFilterProxyModel for the group's + # tab. The regex will end up looking like "^[ABC].*", only + # allowing this tab to display items where the name starts with + # "A", "B", or "C". Notice that we set it to be case-insensitive. + reFilter = "^[%s].*" % group + + proxyModel.setFilterRegExp(QRegExp(reFilter, Qt.CaseInsensitive)) + proxyModel.setFilterKeyColumn(0) # Filter on the "name" column + proxyModel.sort(0, Qt.AscendingOrder) + + # This prevents an application crash (see: http://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash) + viewselectionmodel = tableView.selectionModel() + tableView.selectionModel().selectionChanged.connect(self.selectionChanged) + + self.addTab(tableView, group) + + # Note: the QT example uses a QDataStream for the saving and loading. + # Here we're using a python dictionary to store the addresses, which + # can't be streamed using QDataStream, so we just use cpickle for this + # example. + def readFromFile(self, filename): + """ Read contacts in from a file. """ + try: + f = open(filename, "rb") + addresses = pickle.load(f) + except IOError: + QMessageBox.information(self, "Unable to open file: %s" % filename) + finally: + f.close() + + if len(addresses) == 0: + QMessageBox.information(self, "No contacts in file: %s" % filename) + else: + for address in addresses: + self.addEntry(address["name"], address["address"]) + + def writeToFile(self, filename): + """ Save all contacts in the model to a file. """ + try: + f = open(filename, "wb") + pickle.dump(self.tableModel.addresses, f) + + except IOError: + QMessageBox.information(self, "Unable to open file: %s" % filename) + finally: + f.close() + + +if __name__ == "__main__": + import sys + from PySide2.QtWidgets import QApplication + + app = QApplication(sys.argv) + addressWidget = AddressWidget() + addressWidget.show() + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/addressbook/newaddresstab.py b/examples/widgets/itemviews/addressbook/newaddresstab.py new file mode 100644 index 000000000..82dcff441 --- /dev/null +++ b/examples/widgets/itemviews/addressbook/newaddresstab.py @@ -0,0 +1,94 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtCore import (Qt, Signal) +from PySide2.QtWidgets import (QWidget, QLabel, QPushButton, QVBoxLayout) + +from adddialogwidget import AddDialogWidget + +class NewAddressTab(QWidget): + """ An extra tab that prompts the user to add new contacts. + To be displayed only when there are no contacts in the model. + """ + + sendDetails = Signal(str, str) + + def __init__(self, parent=None): + super(NewAddressTab, self).__init__(parent) + + descriptionLabel = QLabel("There are no contacts in your address book." + "\nClick Add to add new contacts.") + + addButton = QPushButton("Add") + + layout = QVBoxLayout() + layout.addWidget(descriptionLabel) + layout.addWidget(addButton, 0, Qt.AlignCenter) + + self.setLayout(layout) + + addButton.clicked.connect(self.addEntry) + + def addEntry(self): + addDialog = AddDialogWidget() + + if addDialog.exec_(): + name = addDialog.name + address = addDialog.address + self.sendDetails.emit(name, address) + + +if __name__ == "__main__": + + def printAddress(name, address): + print("Name:" + name) + print("Address:" + address) + + import sys + from PySide2.QtWidgets import QApplication + + app = QApplication(sys.argv) + newAddressTab = NewAddressTab() + newAddressTab.sendDetails.connect(printAddress) + newAddressTab.show() + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/addressbook/tablemodel.py b/examples/widgets/itemviews/addressbook/tablemodel.py new file mode 100644 index 000000000..24316f981 --- /dev/null +++ b/examples/widgets/itemviews/addressbook/tablemodel.py @@ -0,0 +1,147 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtCore import (Qt, QAbstractTableModel, QModelIndex) + +class TableModel(QAbstractTableModel): + + def __init__(self, addresses=None, parent=None): + super(TableModel, self).__init__(parent) + + if addresses is None: + self.addresses = [] + else: + self.addresses = addresses + + def rowCount(self, index=QModelIndex()): + """ Returns the number of rows the model holds. """ + return len(self.addresses) + + def columnCount(self, index=QModelIndex()): + """ Returns the number of columns the model holds. """ + return 2 + + def data(self, index, role=Qt.DisplayRole): + """ Depending on the index and role given, return data. If not + returning data, return None (PySide equivalent of QT's + "invalid QVariant"). + """ + if not index.isValid(): + return None + + if not 0 <= index.row() < len(self.addresses): + return None + + if role == Qt.DisplayRole: + name = self.addresses[index.row()]["name"] + address = self.addresses[index.row()]["address"] + + if index.column() == 0: + return name + elif index.column() == 1: + return address + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ Set the headers to be displayed. """ + if role != Qt.DisplayRole: + return None + + if orientation == Qt.Horizontal: + if section == 0: + return "Name" + elif section == 1: + return "Address" + + return None + + def insertRows(self, position, rows=1, index=QModelIndex()): + """ Insert a row into the model. """ + self.beginInsertRows(QModelIndex(), position, position + rows - 1) + + for row in range(rows): + self.addresses.insert(position + row, {"name":"", "address":""}) + + self.endInsertRows() + return True + + def removeRows(self, position, rows=1, index=QModelIndex()): + """ Remove a row from the model. """ + self.beginRemoveRows(QModelIndex(), position, position + rows - 1) + + del self.addresses[position:position+rows] + + self.endRemoveRows() + return True + + def setData(self, index, value, role=Qt.EditRole): + """ Adjust the data (set it to <value>) depending on the given + index and role. + """ + if role != Qt.EditRole: + return False + + if index.isValid() and 0 <= index.row() < len(self.addresses): + address = self.addresses[index.row()] + if index.column() == 0: + address["name"] = value + elif index.column() == 1: + address["address"] = value + else: + return False + + self.dataChanged.emit(index, index, 0) + return True + + return False + + def flags(self, index): + """ Set the item flags at the given index. Seems like we're + implementing this function just to see how it's done, as we + manually adjust each tableView to have NoEditTriggers. + """ + if not index.isValid(): + return Qt.ItemIsEnabled + return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | + Qt.ItemIsEditable) diff --git a/examples/widgets/itemviews/basicsortfiltermodel.py b/examples/widgets/itemviews/basicsortfiltermodel.py new file mode 100755 index 000000000..3051c9fa0 --- /dev/null +++ b/examples/widgets/itemviews/basicsortfiltermodel.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +############################################################################# +## +## Copyright (C) 2013 Riverbank Computing Limited. +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2 import QtCore, QtGui, QtWidgets + + +class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + + self.proxyModel = QtCore.QSortFilterProxyModel() + self.proxyModel.setDynamicSortFilter(True) + + self.sourceGroupBox = QtWidgets.QGroupBox("Original Model") + self.proxyGroupBox = QtWidgets.QGroupBox("Sorted/Filtered Model") + + self.sourceView = QtWidgets.QTreeView() + self.sourceView.setRootIsDecorated(False) + self.sourceView.setAlternatingRowColors(True) + + self.proxyView = QtWidgets.QTreeView() + self.proxyView.setRootIsDecorated(False) + self.proxyView.setAlternatingRowColors(True) + self.proxyView.setModel(self.proxyModel) + self.proxyView.setSortingEnabled(True) + + self.sortCaseSensitivityCheckBox = QtWidgets.QCheckBox("Case sensitive sorting") + self.filterCaseSensitivityCheckBox = QtWidgets.QCheckBox("Case sensitive filter") + + self.filterPatternLineEdit = QtWidgets.QLineEdit() + self.filterPatternLabel = QtWidgets.QLabel("&Filter pattern:") + self.filterPatternLabel.setBuddy(self.filterPatternLineEdit) + + self.filterSyntaxComboBox = QtWidgets.QComboBox() + self.filterSyntaxComboBox.addItem("Regular expression", + QtCore.QRegExp.RegExp) + self.filterSyntaxComboBox.addItem("Wildcard", + QtCore.QRegExp.Wildcard) + self.filterSyntaxComboBox.addItem("Fixed string", + QtCore.QRegExp.FixedString) + self.filterSyntaxLabel = QtWidgets.QLabel("Filter &syntax:") + self.filterSyntaxLabel.setBuddy(self.filterSyntaxComboBox) + + self.filterColumnComboBox = QtWidgets.QComboBox() + self.filterColumnComboBox.addItem("Subject") + self.filterColumnComboBox.addItem("Sender") + self.filterColumnComboBox.addItem("Date") + self.filterColumnLabel = QtWidgets.QLabel("Filter &column:") + self.filterColumnLabel.setBuddy(self.filterColumnComboBox) + + self.filterPatternLineEdit.textChanged.connect(self.filterRegExpChanged) + self.filterSyntaxComboBox.currentIndexChanged.connect(self.filterRegExpChanged) + self.filterColumnComboBox.currentIndexChanged.connect(self.filterColumnChanged) + self.filterCaseSensitivityCheckBox.toggled.connect(self.filterRegExpChanged) + self.sortCaseSensitivityCheckBox.toggled.connect(self.sortChanged) + + sourceLayout = QtWidgets.QHBoxLayout() + sourceLayout.addWidget(self.sourceView) + self.sourceGroupBox.setLayout(sourceLayout) + + proxyLayout = QtWidgets.QGridLayout() + proxyLayout.addWidget(self.proxyView, 0, 0, 1, 3) + proxyLayout.addWidget(self.filterPatternLabel, 1, 0) + proxyLayout.addWidget(self.filterPatternLineEdit, 1, 1, 1, 2) + proxyLayout.addWidget(self.filterSyntaxLabel, 2, 0) + proxyLayout.addWidget(self.filterSyntaxComboBox, 2, 1, 1, 2) + proxyLayout.addWidget(self.filterColumnLabel, 3, 0) + proxyLayout.addWidget(self.filterColumnComboBox, 3, 1, 1, 2) + proxyLayout.addWidget(self.filterCaseSensitivityCheckBox, 4, 0, 1, 2) + proxyLayout.addWidget(self.sortCaseSensitivityCheckBox, 4, 2) + self.proxyGroupBox.setLayout(proxyLayout) + + mainLayout = QtWidgets.QVBoxLayout() + mainLayout.addWidget(self.sourceGroupBox) + mainLayout.addWidget(self.proxyGroupBox) + self.setLayout(mainLayout) + + self.setWindowTitle("Basic Sort/Filter Model") + self.resize(500, 450) + + self.proxyView.sortByColumn(1, QtCore.Qt.AscendingOrder) + self.filterColumnComboBox.setCurrentIndex(1) + + self.filterPatternLineEdit.setText("Andy|Grace") + self.filterCaseSensitivityCheckBox.setChecked(True) + self.sortCaseSensitivityCheckBox.setChecked(True) + + def setSourceModel(self, model): + self.proxyModel.setSourceModel(model) + self.sourceView.setModel(model) + + def filterRegExpChanged(self): + syntax_nr = self.filterSyntaxComboBox.itemData(self.filterSyntaxComboBox.currentIndex()) + syntax = QtCore.QRegExp.PatternSyntax(syntax_nr) + + if self.filterCaseSensitivityCheckBox.isChecked(): + caseSensitivity = QtCore.Qt.CaseSensitive + else: + caseSensitivity = QtCore.Qt.CaseInsensitive + + regExp = QtCore.QRegExp(self.filterPatternLineEdit.text(), + caseSensitivity, syntax) + self.proxyModel.setFilterRegExp(regExp) + + def filterColumnChanged(self): + self.proxyModel.setFilterKeyColumn(self.filterColumnComboBox.currentIndex()) + + def sortChanged(self): + if self.sortCaseSensitivityCheckBox.isChecked(): + caseSensitivity = QtCore.Qt.CaseSensitive + else: + caseSensitivity = QtCore.Qt.CaseInsensitive + + self.proxyModel.setSortCaseSensitivity(caseSensitivity) + + +def addMail(model, subject, sender, date): + model.insertRow(0) + model.setData(model.index(0, 0), subject) + model.setData(model.index(0, 1), sender) + model.setData(model.index(0, 2), date) + + +def createMailModel(parent): + model = QtGui.QStandardItemModel(0, 3, parent) + + model.setHeaderData(0, QtCore.Qt.Horizontal, "Subject") + model.setHeaderData(1, QtCore.Qt.Horizontal, "Sender") + model.setHeaderData(2, QtCore.Qt.Horizontal, "Date") + + addMail(model, "Happy New Year!", "Grace K. <grace@software-inc.com>", + QtCore.QDateTime(QtCore.QDate(2006, 12, 31), QtCore.QTime(17, 3))) + addMail(model, "Radically new concept", "Grace K. <grace@software-inc.com>", + QtCore.QDateTime(QtCore.QDate(2006, 12, 22), QtCore.QTime(9, 44))) + addMail(model, "Accounts", "pascale@nospam.com", + QtCore.QDateTime(QtCore.QDate(2006, 12, 31), QtCore.QTime(12, 50))) + addMail(model, "Expenses", "Joe Bloggs <joe@bloggs.com>", + QtCore.QDateTime(QtCore.QDate(2006, 12, 25), QtCore.QTime(11, 39))) + addMail(model, "Re: Expenses", "Andy <andy@nospam.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 2), QtCore.QTime(16, 5))) + addMail(model, "Re: Accounts", "Joe Bloggs <joe@bloggs.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 3), QtCore.QTime(14, 18))) + addMail(model, "Re: Accounts", "Andy <andy@nospam.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 3), QtCore.QTime(14, 26))) + addMail(model, "Sports", "Linda Smith <linda.smith@nospam.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 5), QtCore.QTime(11, 33))) + addMail(model, "AW: Sports", "Rolf Newschweinstein <rolfn@nospam.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 5), QtCore.QTime(12, 0))) + addMail(model, "RE: Sports", "Petra Schmidt <petras@nospam.com>", + QtCore.QDateTime(QtCore.QDate(2007, 1, 5), QtCore.QTime(12, 1))) + + return model + + +if __name__ == '__main__': + + import sys + + app = QtWidgets.QApplication(sys.argv) + window = Window() + window.setSourceModel(createMailModel(window)) + window.show() + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/fetchmore.py b/examples/widgets/itemviews/fetchmore.py new file mode 100755 index 000000000..08655db0d --- /dev/null +++ b/examples/widgets/itemviews/fetchmore.py @@ -0,0 +1,148 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2009 Darryl Wallace, 2009 <wallacdj@gmail.com> +## Copyright (C) 2013 Riverbank Computing Limited. +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2 import QtCore, QtGui, QtWidgets + + +class FileListModel(QtCore.QAbstractListModel): + numberPopulated = QtCore.Signal(int) + + def __init__(self, parent=None): + super(FileListModel, self).__init__(parent) + + self.fileCount = 0 + self.fileList = [] + + def rowCount(self, parent=QtCore.QModelIndex()): + return self.fileCount + + def data(self, index, role=QtCore.Qt.DisplayRole): + if not index.isValid(): + return None + + if index.row() >= len(self.fileList) or index.row() < 0: + return None + + if role == QtCore.Qt.DisplayRole: + return self.fileList[index.row()] + + if role == QtCore.Qt.BackgroundRole: + batch = (index.row() // 100) % 2 +# FIXME: QGuiApplication::palette() required + if batch == 0: + return QtWidgets.qApp.palette().base() + + return QtWidgets.qApp.palette().alternateBase() + + return None + + def canFetchMore(self, index): + return self.fileCount < len(self.fileList) + + def fetchMore(self, index): + remainder = len(self.fileList) - self.fileCount + itemsToFetch = min(100, remainder) + + self.beginInsertRows(QtCore.QModelIndex(), self.fileCount, + self.fileCount + itemsToFetch) + + self.fileCount += itemsToFetch + + self.endInsertRows() + + self.numberPopulated.emit(itemsToFetch) + + def setDirPath(self, path): + dir = QtCore.QDir(path) + + self.beginResetModel() + self.fileList = list(dir.entryList()) + self.fileCount = 0 + self.endResetModel() + + +class Window(QtWidgets.QWidget): + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + model = FileListModel(self) + model.setDirPath(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.PrefixPath)) + + label = QtWidgets.QLabel("Directory") + lineEdit = QtWidgets.QLineEdit() + label.setBuddy(lineEdit) + + view = QtWidgets.QListView() + view.setModel(model) + + self.logViewer = QtWidgets.QTextBrowser() + self.logViewer.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)) + + lineEdit.textChanged.connect(model.setDirPath) + lineEdit.textChanged.connect(self.logViewer.clear) + model.numberPopulated.connect(self.updateLog) + + layout = QtWidgets.QGridLayout() + layout.addWidget(label, 0, 0) + layout.addWidget(lineEdit, 0, 1) + layout.addWidget(view, 1, 0, 1, 2) + layout.addWidget(self.logViewer, 2, 0, 1, 2) + + self.setLayout(layout) + self.setWindowTitle("Fetch More Example") + + def updateLog(self, number): + self.logViewer.append("%d items added." % number) + + +if __name__ == '__main__': + + import sys + + app = QtWidgets.QApplication(sys.argv) + + window = Window() + window.show() + + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/stardelegate/stardelegate.py b/examples/widgets/itemviews/stardelegate/stardelegate.py new file mode 100644 index 000000000..44a2d6694 --- /dev/null +++ b/examples/widgets/itemviews/stardelegate/stardelegate.py @@ -0,0 +1,174 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtWidgets import (QItemDelegate, QStyledItemDelegate, QStyle) + +from starrating import StarRating +from stareditor import StarEditor + +class StarDelegate(QStyledItemDelegate): + """ A subclass of QStyledItemDelegate that allows us to render our + pretty star ratings. + """ + + def __init__(self, parent=None): + super(StarDelegate, self).__init__(parent) + + def paint(self, painter, option, index): + """ Paint the items in the table. + + If the item referred to by <index> is a StarRating, we handle the + painting ourselves. For the other items, we let the base class + handle the painting as usual. + + In a polished application, we'd use a better check than the + column number to find out if we needed to paint the stars, but + it works for the purposes of this example. + """ + if index.column() == 3: + starRating = StarRating(index.data()) + + # If the row is currently selected, we need to make sure we + # paint the background accordingly. + if option.state & QStyle.State_Selected: + # The original C++ example used option.palette.foreground() to + # get the brush for painting, but there are a couple of + # problems with that: + # - foreground() is obsolete now, use windowText() instead + # - more importantly, windowText() just returns a brush + # containing a flat color, where sometimes the style + # would have a nice subtle gradient or something. + # Here we just use the brush of the painter object that's + # passed in to us, which keeps the row highlighting nice + # and consistent. + painter.fillRect(option.rect, painter.brush()) + + # Now that we've painted the background, call starRating.paint() + # to paint the stars. + starRating.paint(painter, option.rect, option.palette) + else: + QStyledItemDelegate.paint(self, painter, option, index) + + def sizeHint(self, option, index): + """ Returns the size needed to display the item in a QSize object. """ + if index.column() == 3: + starRating = StarRating(index.data()) + return starRating.sizeHint() + else: + return QStyledItemDelegate.sizeHint(self, option, index) + + # The next 4 methods handle the custom editing that we need to do. + # If this were just a display delegate, paint() and sizeHint() would + # be all we needed. + + def createEditor(self, parent, option, index): + """ Creates and returns the custom StarEditor object we'll use to edit + the StarRating. + """ + if index.column() == 3: + editor = StarEditor(parent) + editor.editingFinished.connect(self.commitAndCloseEditor) + return editor + else: + return QStyledItemDelegate.createEditor(self, parent, option, index) + + def setEditorData(self, editor, index): + """ Sets the data to be displayed and edited by our custom editor. """ + if index.column() == 3: + editor.starRating = StarRating(index.data()) + else: + QStyledItemDelegate.setEditorData(self, editor, index) + + def setModelData(self, editor, model, index): + """ Get the data from our custom editor and stuffs it into the model. + """ + if index.column() == 3: + model.setData(index, editor.starRating.starCount) + else: + QStyledItemDelegate.setModelData(self, editor, model, index) + + def commitAndCloseEditor(self): + """ Erm... commits the data and closes the editor. :) """ + editor = self.sender() + + # The commitData signal must be emitted when we've finished editing + # and need to write our changed back to the model. + self.commitData.emit(editor) + self.closeEditor.emit(editor, QStyledItemDelegate.NoHint) + + +if __name__ == "__main__": + """ Run the application. """ + from PySide2.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem, + QAbstractItemView) + import sys + + app = QApplication(sys.argv) + + # Create and populate the tableWidget + tableWidget = QTableWidget(4, 4) + tableWidget.setItemDelegate(StarDelegate()) + tableWidget.setEditTriggers(QAbstractItemView.DoubleClicked | + QAbstractItemView.SelectedClicked) + tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + tableWidget.setHorizontalHeaderLabels(["Title", "Genre", "Artist", "Rating"]) + + data = [ ["Mass in B-Minor", "Baroque", "J.S. Bach", 5], + ["Three More Foxes", "Jazz", "Maynard Ferguson", 4], + ["Sex Bomb", "Pop", "Tom Jones", 3], + ["Barbie Girl", "Pop", "Aqua", 5] ] + + for r in range(len(data)): + tableWidget.setItem(r, 0, QTableWidgetItem(data[r][0])) + tableWidget.setItem(r, 1, QTableWidgetItem(data[r][1])) + tableWidget.setItem(r, 2, QTableWidgetItem(data[r][2])) + item = QTableWidgetItem() + item.setData(0, StarRating(data[r][3]).starCount) + tableWidget.setItem(r, 3, item) + + tableWidget.resizeColumnsToContents() + tableWidget.resize(500, 300) + tableWidget.show() + + sys.exit(app.exec_()) diff --git a/examples/widgets/itemviews/stardelegate/stareditor.py b/examples/widgets/itemviews/stardelegate/stareditor.py new file mode 100644 index 000000000..440659a4f --- /dev/null +++ b/examples/widgets/itemviews/stardelegate/stareditor.py @@ -0,0 +1,99 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtWidgets import (QWidget) +from PySide2.QtGui import (QPainter) +from PySide2.QtCore import Signal + + +class StarEditor(QWidget): + """ The custome editor for editing StarRatings. """ + + # A signal to tell the delegate when we've finished editing. + editingFinished = Signal() + + def __init__(self, parent=None): + """ Initialize the editor object, making sure we can watch mouse + events. + """ + super(StarEditor, self).__init__(parent) + + self.setMouseTracking(True) + self.setAutoFillBackground(True) + + def sizeHint(self): + """ Tell the caller how big we are. """ + return self.starRating.sizeHint() + + def paintEvent(self, event): + """ Paint the editor, offloading the work to the StarRating class. """ + painter = QPainter(self) + self.starRating.paint(painter, self.rect(), self.palette(), isEditable=True) + + def mouseMoveEvent(self, event): + """ As the mouse moves inside the editor, track the position and + update the editor to display as many stars as necessary. + """ + star = self.starAtPosition(event.x()) + + if (star != self.starRating.starCount) and (star != -1): + self.starRating.starCount = star + self.update() + + def mouseReleaseEvent(self, event): + """ Once the user has clicked his/her chosen star rating, tell the + delegate we're done editing. + """ + self.editingFinished.emit() + + def starAtPosition(self, x): + """ Calculate which star the user's mouse cursor is currently + hovering over. + """ + star = (x / (self.starRating.sizeHint().width() / + self.starRating.maxStarCount)) + 1 + if (star <= 0) or (star > self.starRating.maxStarCount): + return -1 + + return star diff --git a/examples/widgets/itemviews/stardelegate/starrating.py b/examples/widgets/itemviews/stardelegate/starrating.py new file mode 100644 index 000000000..b573db424 --- /dev/null +++ b/examples/widgets/itemviews/stardelegate/starrating.py @@ -0,0 +1,102 @@ +#!/usr/bin/python + +############################################################################# +## +## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +## Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +## Copyright (C) 2016 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the PySide examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from math import (cos, sin, pi) + +from PySide2.QtGui import (QPainter, QPolygonF) +from PySide2.QtCore import (QPointF, QSize, Qt) + +PAINTING_SCALE_FACTOR = 20 + + +class StarRating(object): + """ Handle the actual painting of the stars themselves. """ + + def __init__(self, starCount=1, maxStarCount=5): + self.starCount = starCount + self.maxStarCount = maxStarCount + + # Create the star shape we'll be drawing. + self.starPolygon = QPolygonF() + self.starPolygon.append(QPointF(1.0, 0.5)) + for i in range(1, 5): + self.starPolygon.append(QPointF(0.5 + 0.5 * cos(0.8 * i * pi), + 0.5 + 0.5 * sin(0.8 * i * pi))) + + # Create the diamond shape we'll show in the editor + self.diamondPolygon = QPolygonF() + diamondPoints = [QPointF(0.4, 0.5), QPointF(0.5, 0.4), + QPointF(0.6, 0.5), QPointF(0.5, 0.6), + QPointF(0.4, 0.5)] + for point in diamondPoints: + self.diamondPolygon.append(point) + + def sizeHint(self): + """ Tell the caller how big we are. """ + return PAINTING_SCALE_FACTOR * QSize(self.maxStarCount, 1) + + def paint(self, painter, rect, palette, isEditable=False): + """ Paint the stars (and/or diamonds if we're in editing mode). """ + painter.save() + + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setPen(Qt.NoPen) + + if isEditable: + painter.setBrush(palette.highlight()) + else: + painter.setBrush(palette.windowText()) + + yOffset = (rect.height() - PAINTING_SCALE_FACTOR) / 2 + painter.translate(rect.x(), rect.y() + yOffset) + painter.scale(PAINTING_SCALE_FACTOR, PAINTING_SCALE_FACTOR) + + for i in range(self.maxStarCount): + if i < self.starCount: + painter.drawPolygon(self.starPolygon, Qt.WindingFill) + elif isEditable: + painter.drawPolygon(self.diamondPolygon, Qt.WindingFill) + painter.translate(1.0, 0.0) + + painter.restore() |