diff options
Diffstat (limited to 'examples/widgets/itemviews')
50 files changed, 2670 insertions, 1169 deletions
diff --git a/examples/widgets/itemviews/address_book/adddialogwidget.py b/examples/widgets/itemviews/address_book/adddialogwidget.py new file mode 100644 index 000000000..ecb853e80 --- /dev/null +++ b/examples/widgets/itemviews/address_book/adddialogwidget.py @@ -0,0 +1,65 @@ +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt +from PySide6.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().__init__(parent) + + name_label = QLabel("Name") + address_label = QLabel("Address") + button_box = QDialogButtonBox(QDialogButtonBox.Ok + | QDialogButtonBox.Cancel) + + self._name_text = QLineEdit() + self._address_text = QTextEdit() + + grid = QGridLayout() + grid.setColumnStretch(1, 2) + grid.addWidget(name_label, 0, 0) + grid.addWidget(self._name_text, 0, 1) + grid.addWidget(address_label, 1, 0, Qt.AlignLeft | Qt.AlignTop) + grid.addWidget(self._address_text, 1, 1, Qt.AlignLeft) + + layout = QVBoxLayout() + layout.addLayout(grid) + layout.addWidget(button_box) + + self.setLayout(layout) + + self.setWindowTitle("Add a Contact") + + button_box.accepted.connect(self.accept) + button_box.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._name_text.text() + + @property + def address(self): + return self._address_text.toPlainText() + + +if __name__ == "__main__": + import sys + from PySide6.QtWidgets import QApplication + + app = QApplication(sys.argv) + + dialog = AddDialogWidget() + if (dialog.exec()): + name = dialog.name + address = dialog.address + print(f"Name: {name}") + print(f"Address: {address}") diff --git a/examples/widgets/itemviews/address_book/address_book.py b/examples/widgets/itemviews/address_book/address_book.py new file mode 100644 index 000000000..af0cf3dee --- /dev/null +++ b/examples/widgets/itemviews/address_book/address_book.py @@ -0,0 +1,99 @@ +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Slot +from PySide6.QtGui import QAction +from PySide6.QtWidgets import (QMainWindow, QFileDialog, QApplication) + +from addresswidget import AddressWidget + + +class MainWindow(QMainWindow): + + def __init__(self, parent=None): + super().__init__(parent) + + self._address_widget = AddressWidget() + self.setCentralWidget(self._address_widget) + self.create_menus() + self.setWindowTitle("Address Book") + + def create_menus(self): + # Create the main menuBar menu items + file_menu = self.menuBar().addMenu("&File") + tool_menu = self.menuBar().addMenu("&Tools") + + # Populate the File menu + self.open_action = self.create_action("&Open...", file_menu, self.open_file) + self.save_action = self.create_action("&Save As...", file_menu, self.save_file) + file_menu.addSeparator() + self.exit_action = self.create_action("E&xit", file_menu, self.close) + + # Populate the Tools menu + self.add_action = self.create_action( + "&Add Entry...", tool_menu, self._address_widget.add_entry) + self._edit_action = self.create_action( + "&Edit Entry...", tool_menu, self._address_widget.edit_entry) + tool_menu.addSeparator() + self._remove_action = self.create_action( + "&Remove Entry", tool_menu, self._address_widget.remove_entry) + + # Disable the edit and remove menu items initially, as there are + # no items yet. + self._edit_action.setEnabled(False) + self._remove_action.setEnabled(False) + + # Wire up the updateActions slot + self._address_widget.selection_changed.connect(self.update_actions) + + def create_action(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 PySide6 as they do in Qt, where they return a QString + # containing the filename. + # + # In PySide6, these functions return a tuple: (filename, filter) + + @Slot() + def open_file(self): + filename, _ = QFileDialog.getOpenFileName(self) + if filename: + self._address_widget.read_from_file(filename) + + @Slot() + def save_file(self): + filename, _ = QFileDialog.getSaveFileName(self) + if filename: + self._address_widget.write_to_file(filename) + + def update_actions(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._remove_action.setEnabled(True) + self._edit_action.setEnabled(True) + else: + self._remove_action.setEnabled(False) + self._edit_action.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/address_book/address_book.pyproject b/examples/widgets/itemviews/address_book/address_book.pyproject new file mode 100644 index 000000000..1b5dd597c --- /dev/null +++ b/examples/widgets/itemviews/address_book/address_book.pyproject @@ -0,0 +1,4 @@ +{ + "files": ["tablemodel.py", "address_book.py", "adddialogwidget.py", + "addresswidget.py", "newaddresstab.py"] +} diff --git a/examples/widgets/itemviews/address_book/addresswidget.py b/examples/widgets/itemviews/address_book/addresswidget.py new file mode 100644 index 000000000..cb2f46ea1 --- /dev/null +++ b/examples/widgets/itemviews/address_book/addresswidget.py @@ -0,0 +1,215 @@ +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +try: + import cpickle as pickle +except ImportError: + import pickle + +from PySide6.QtCore import (Qt, Signal, Slot, QRegularExpression, QModelIndex, + QItemSelection, QSortFilterProxyModel) +from PySide6.QtWidgets import 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. + """ + + selection_changed = Signal(QItemSelection) + + def __init__(self, parent=None): + """ Initialize the AddressWidget. """ + super().__init__(parent) + + self._table_model = TableModel() + self._new_address_tab = NewAddressTab() + self._new_address_tab.send_details.connect(self.add_entry) + + self.addTab(self._new_address_tab, "Address Book") + + self.setup_tabs() + + @Slot() + def add_entry(self, name=None, address=None): + """ Add an entry to the addressbook. """ + if name is None and address is None: + add_dialog = AddDialogWidget() + + if add_dialog.exec(): + name = add_dialog.name + address = add_dialog.address + + address = {"name": name, "address": address} + addresses = self._table_model.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", + f'The name "{name}" already exists.') + except ValueError: + # The address didn't already exist, so let's add it to the model. + + # Step 1: create the row + self._table_model.insertRows(0) + + # Step 2: get the index of the newly created row and use it. + # to set the name + ix = self._table_model.index(0, 0, QModelIndex()) + self._table_model.setData(ix, address["name"], Qt.EditRole) + + # Step 3: lather, rinse, repeat for the address. + ix = self._table_model.index(0, 1, QModelIndex()) + self._table_model.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._new_address_tab)) + + # 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. + table_view = self.currentWidget() + table_view.resizeRowToContents(ix.row()) + + @Slot() + def edit_entry(self): + """ Edit an entry in the addressbook. """ + table_view = self.currentWidget() + proxy_model = table_view.model() + selection_model = table_view.selectionModel() + + # Get the name and address of the currently selected row. + indexes = selection_model.selectedRows() + if len(indexes) != 1: + return + + row = proxy_model.mapToSource(indexes[0]).row() + ix = self._table_model.index(row, 0, QModelIndex()) + name = self._table_model.data(ix, Qt.DisplayRole) + ix = self._table_model.index(row, 1, QModelIndex()) + address = self._table_model.data(ix, Qt.DisplayRole) + + # Open an addDialogWidget, and only allow the user to edit the address. + add_dialog = AddDialogWidget() + add_dialog.setWindowTitle("Edit a Contact") + + add_dialog._name_text.setReadOnly(True) + add_dialog._name_text.setText(name) + add_dialog._address_text.setText(address) + + # If the address is different, add it to the model. + if add_dialog.exec(): + new_address = add_dialog.address + if new_address != address: + ix = self._table_model.index(row, 1, QModelIndex()) + self._table_model.setData(ix, new_address, Qt.EditRole) + + @Slot() + def remove_entry(self): + """ Remove an entry from the addressbook. """ + table_view = self.currentWidget() + proxy_model = table_view.model() + selection_model = table_view.selectionModel() + + # Just like editEntry, but this time remove the selected row. + indexes = selection_model.selectedRows() + + for index in indexes: + row = proxy_model.mapToSource(index).row() + self._table_model.removeRows(row) + + # If we've removed the last address in the model, display the + # newAddressTab + if self._table_model.rowCount() == 0: + self.insertTab(0, self._new_address_tab, "Address Book") + + def setup_tabs(self): + """ Setup the various tabs in the AddressWidget. """ + groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"] + + for group in groups: + proxy_model = QSortFilterProxyModel(self) + proxy_model.setSourceModel(self._table_model) + proxy_model.setDynamicSortFilter(True) + + table_view = QTableView() + table_view.setModel(proxy_model) + table_view.setSortingEnabled(True) + table_view.setSelectionBehavior(QAbstractItemView.SelectRows) + table_view.horizontalHeader().setStretchLastSection(True) + table_view.verticalHeader().hide() + table_view.setEditTriggers(QAbstractItemView.NoEditTriggers) + table_view.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. + re = QRegularExpression(f"^[{group}].*") + assert re.isValid() + re.setPatternOptions(QRegularExpression.CaseInsensitiveOption) + proxy_model.setFilterRegularExpression(re) + proxy_model.setFilterKeyColumn(0) # Filter on the "name" column + proxy_model.sort(0, Qt.AscendingOrder) + + # This prevents an application crash (see: + # https://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash) # noqa: E501 + self.viewselectionmodel = table_view.selectionModel() + table_view.selectionModel().selectionChanged.connect(self.selection_changed) + + self.addTab(table_view, 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 read_from_file(self, filename): + """ Read contacts in from a file. """ + try: + f = open(filename, "rb") + addresses = pickle.load(f) + except IOError: + QMessageBox.information(self, f"Unable to open file: {filename}") + finally: + f.close() + + if len(addresses) == 0: + QMessageBox.information(self, f"No contacts in file: {filename}") + else: + for address in addresses: + self.add_entry(address["name"], address["address"]) + + def write_to_file(self, filename): + """ Save all contacts in the model to a file. """ + try: + f = open(filename, "wb") + pickle.dump(self._table_model.addresses, f) + + except IOError: + QMessageBox.information(self, f"Unable to open file: {filename}") + finally: + f.close() + + +if __name__ == "__main__": + import sys + from PySide6.QtWidgets import QApplication + + app = QApplication(sys.argv) + address_widget = AddressWidget() + address_widget.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/address_book/doc/address_book.png b/examples/widgets/itemviews/address_book/doc/address_book.png Binary files differnew file mode 100644 index 000000000..40f2d9e63 --- /dev/null +++ b/examples/widgets/itemviews/address_book/doc/address_book.png diff --git a/examples/widgets/itemviews/address_book/doc/address_book.rst b/examples/widgets/itemviews/address_book/doc/address_book.rst new file mode 100644 index 000000000..04b91054e --- /dev/null +++ b/examples/widgets/itemviews/address_book/doc/address_book.rst @@ -0,0 +1,9 @@ +Address Book Example +==================== + +The address book example shows how to use proxy models to display different +views onto data from a single model. + +.. image:: address_book.png + :width: 400 + :alt: Address Book Screenshot diff --git a/examples/widgets/itemviews/address_book/newaddresstab.py b/examples/widgets/itemviews/address_book/newaddresstab.py new file mode 100644 index 000000000..d3d037ad4 --- /dev/null +++ b/examples/widgets/itemviews/address_book/newaddresstab.py @@ -0,0 +1,56 @@ +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import (Qt, Signal) +from PySide6.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. + """ + + send_details = Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent) + + description_label = QLabel("There are no contacts in your address book." + "\nClick Add to add new contacts.") + + add_button = QPushButton("Add") + + layout = QVBoxLayout() + layout.addWidget(description_label) + layout.addWidget(add_button, 0, Qt.AlignCenter) + + self.setLayout(layout) + + add_button.clicked.connect(self.add_entry) + + def add_entry(self): + add_dialog = AddDialogWidget() + + if add_dialog.exec(): + name = add_dialog.name + address = add_dialog.address + self.send_details.emit(name, address) + + +if __name__ == "__main__": + + def print_address(name, address): + print(f"Name: {name}") + print(f"Address: {address}") + + import sys + from PySide6.QtWidgets import QApplication + + app = QApplication(sys.argv) + new_address_tab = NewAddressTab() + new_address_tab.send_details.connect(print_address) + new_address_tab.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/addressbook/tablemodel.py b/examples/widgets/itemviews/address_book/tablemodel.py index 155f09131..3c1dbd4cc 100644 --- a/examples/widgets/itemviews/addressbook/tablemodel.py +++ b/examples/widgets/itemviews/address_book/tablemodel.py @@ -1,51 +1,14 @@ +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import (Qt, QAbstractTableModel, QModelIndex) -############################################################################# -## -## 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 Qt for Python 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) + super().__init__(parent) if addresses is None: self.addresses = [] @@ -100,7 +63,7 @@ class TableModel(QAbstractTableModel): self.beginInsertRows(QModelIndex(), position, position + rows - 1) for row in range(rows): - self.addresses.insert(position + row, {"name":"", "address":""}) + self.addresses.insert(position + row, {"name": "", "address": ""}) self.endInsertRows() return True @@ -109,7 +72,7 @@ class TableModel(QAbstractTableModel): """ Remove a row from the model. """ self.beginRemoveRows(QModelIndex(), position, position + rows - 1) - del self.addresses[position:position+rows] + del self.addresses[position:position + rows] self.endRemoveRows() return True @@ -142,5 +105,5 @@ class TableModel(QAbstractTableModel): """ if not index.isValid(): return Qt.ItemIsEnabled - return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | - Qt.ItemIsEditable) + return Qt.ItemFlags(QAbstractTableModel.flags(self, index) + | Qt.ItemIsEditable) diff --git a/examples/widgets/itemviews/addressbook/adddialogwidget.py b/examples/widgets/itemviews/addressbook/adddialogwidget.py deleted file mode 100644 index 7991039ef..000000000 --- a/examples/widgets/itemviews/addressbook/adddialogwidget.py +++ /dev/null @@ -1,102 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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 deleted file mode 100644 index 262027a64..000000000 --- a/examples/widgets/itemviews/addressbook/addressbook.py +++ /dev/null @@ -1,130 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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/addressbook.pyproject b/examples/widgets/itemviews/addressbook/addressbook.pyproject deleted file mode 100644 index 2aa763753..000000000 --- a/examples/widgets/itemviews/addressbook/addressbook.pyproject +++ /dev/null @@ -1,4 +0,0 @@ -{ - "files": ["tablemodel.py", "addressbook.py", "adddialogwidget.py", - "addresswidget.py", "newaddresstab.py"] -} diff --git a/examples/widgets/itemviews/addressbook/addresswidget.py b/examples/widgets/itemviews/addressbook/addresswidget.py deleted file mode 100644 index b70b44b0a..000000000 --- a/examples/widgets/itemviews/addressbook/addresswidget.py +++ /dev/null @@ -1,247 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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, QSortFilterProxyModel) -from PySide2.QtWidgets import 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 deleted file mode 100644 index ab54fb8a7..000000000 --- a/examples/widgets/itemviews/addressbook/newaddresstab.py +++ /dev/null @@ -1,93 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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/basicfiltermodel/basicsortfiltermodel.py b/examples/widgets/itemviews/basicfiltermodel/basicsortfiltermodel.py new file mode 100644 index 000000000..a30b0abdf --- /dev/null +++ b/examples/widgets/itemviews/basicfiltermodel/basicsortfiltermodel.py @@ -0,0 +1,178 @@ +# Copyright (C) 2013 Riverbank Computing Limited. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import (QDate, QDateTime, QRegularExpression, + QSortFilterProxyModel, QTime, Qt, Slot) +from PySide6.QtGui import QStandardItemModel +from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QGridLayout, + QGroupBox, QHBoxLayout, QLabel, QLineEdit, + QTreeView, QVBoxLayout, QWidget) + + +REGULAR_EXPRESSION = 0 +WILDCARD = 1 +FIXED_STRING = 2 + + +class Window(QWidget): + def __init__(self): + super().__init__() + + self._proxy_model = QSortFilterProxyModel() + self._proxy_model.setDynamicSortFilter(True) + + self._source_group_box = QGroupBox("Original Model") + self._proxy_group_box = QGroupBox("Sorted/Filtered Model") + + self._source_view = QTreeView() + self._source_view.setRootIsDecorated(False) + self._source_view.setAlternatingRowColors(True) + + self._proxy_view = QTreeView() + self._proxy_view.setRootIsDecorated(False) + self._proxy_view.setAlternatingRowColors(True) + self._proxy_view.setModel(self._proxy_model) + self._proxy_view.setSortingEnabled(True) + + self._sort_case_sensitivity_check_box = QCheckBox("Case sensitive sorting") + self._filter_case_sensitivity_check_box = QCheckBox("Case sensitive filter") + + self._filter_pattern_line_edit = QLineEdit() + self._filter_pattern_line_edit.setClearButtonEnabled(True) + self._filter_pattern_label = QLabel("&Filter pattern:") + self._filter_pattern_label.setBuddy(self._filter_pattern_line_edit) + + self._filter_syntax_combo_box = QComboBox() + self._filter_syntax_combo_box.addItem("Regular expression", + REGULAR_EXPRESSION) + self._filter_syntax_combo_box.addItem("Wildcard", + WILDCARD) + self._filter_syntax_combo_box.addItem("Fixed string", + FIXED_STRING) + self._filter_syntax_label = QLabel("Filter &syntax:") + self._filter_syntax_label.setBuddy(self._filter_syntax_combo_box) + + self._filter_column_combo_box = QComboBox() + self._filter_column_combo_box.addItem("Subject") + self._filter_column_combo_box.addItem("Sender") + self._filter_column_combo_box.addItem("Date") + self._filter_column_label = QLabel("Filter &column:") + self._filter_column_label.setBuddy(self._filter_column_combo_box) + + self._filter_pattern_line_edit.textChanged.connect(self.filter_reg_exp_changed) + self._filter_syntax_combo_box.currentIndexChanged.connect(self.filter_reg_exp_changed) + self._filter_column_combo_box.currentIndexChanged.connect(self.filter_column_changed) + self._filter_case_sensitivity_check_box.toggled.connect(self.filter_reg_exp_changed) + self._sort_case_sensitivity_check_box.toggled.connect(self.sort_changed) + + source_layout = QHBoxLayout() + source_layout.addWidget(self._source_view) + self._source_group_box.setLayout(source_layout) + + proxy_layout = QGridLayout() + proxy_layout.addWidget(self._proxy_view, 0, 0, 1, 3) + proxy_layout.addWidget(self._filter_pattern_label, 1, 0) + proxy_layout.addWidget(self._filter_pattern_line_edit, 1, 1, 1, 2) + proxy_layout.addWidget(self._filter_syntax_label, 2, 0) + proxy_layout.addWidget(self._filter_syntax_combo_box, 2, 1, 1, 2) + proxy_layout.addWidget(self._filter_column_label, 3, 0) + proxy_layout.addWidget(self._filter_column_combo_box, 3, 1, 1, 2) + proxy_layout.addWidget(self._filter_case_sensitivity_check_box, 4, 0, 1, 2) + proxy_layout.addWidget(self._sort_case_sensitivity_check_box, 4, 2) + self._proxy_group_box.setLayout(proxy_layout) + + main_layout = QVBoxLayout() + main_layout.addWidget(self._source_group_box) + main_layout.addWidget(self._proxy_group_box) + self.setLayout(main_layout) + + self.setWindowTitle("Basic Sort/Filter Model") + self.resize(500, 450) + + self._proxy_view.sortByColumn(1, Qt.AscendingOrder) + self._filter_column_combo_box.setCurrentIndex(1) + + self._filter_pattern_line_edit.setText("Andy|Grace") + self._filter_case_sensitivity_check_box.setChecked(True) + self._sort_case_sensitivity_check_box.setChecked(True) + + def set_source_model(self, model): + self._proxy_model.setSourceModel(model) + self._source_view.setModel(model) + + @Slot() + def filter_reg_exp_changed(self): + syntax_nr = self._filter_syntax_combo_box.currentData() + pattern = self._filter_pattern_line_edit.text() + if syntax_nr == WILDCARD: + pattern = QRegularExpression.wildcardToRegularExpression(pattern) + elif syntax_nr == FIXED_STRING: + pattern = QRegularExpression.escape(pattern) + + reg_exp = QRegularExpression(pattern) + if not self._filter_case_sensitivity_check_box.isChecked(): + options = reg_exp.patternOptions() + options |= QRegularExpression.CaseInsensitiveOption + reg_exp.setPatternOptions(options) + self._proxy_model.setFilterRegularExpression(reg_exp) + + @Slot() + def filter_column_changed(self): + self._proxy_model.setFilterKeyColumn(self._filter_column_combo_box.currentIndex()) + + @Slot() + def sort_changed(self): + if self._sort_case_sensitivity_check_box.isChecked(): + case_sensitivity = Qt.CaseSensitive + else: + case_sensitivity = Qt.CaseInsensitive + + self._proxy_model.setSortCaseSensitivity(case_sensitivity) + + +def add_mail(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 create_mail_model(parent): + model = QStandardItemModel(0, 3, parent) + + model.setHeaderData(0, Qt.Horizontal, "Subject") + model.setHeaderData(1, Qt.Horizontal, "Sender") + model.setHeaderData(2, Qt.Horizontal, "Date") + + add_mail(model, "Happy New Year!", "Grace K. <grace@software-inc.com>", + QDateTime(QDate(2006, 12, 31), QTime(17, 3))) + add_mail(model, "Radically new concept", "Grace K. <grace@software-inc.com>", + QDateTime(QDate(2006, 12, 22), QTime(9, 44))) + add_mail(model, "Accounts", "pascale@nospam.com", + QDateTime(QDate(2006, 12, 31), QTime(12, 50))) + add_mail(model, "Expenses", "Joe Bloggs <joe@bloggs.com>", + QDateTime(QDate(2006, 12, 25), QTime(11, 39))) + add_mail(model, "Re: Expenses", "Andy <andy@nospam.com>", + QDateTime(QDate(2007, 1, 2), QTime(16, 5))) + add_mail(model, "Re: Accounts", "Joe Bloggs <joe@bloggs.com>", + QDateTime(QDate(2007, 1, 3), QTime(14, 18))) + add_mail(model, "Re: Accounts", "Andy <andy@nospam.com>", + QDateTime(QDate(2007, 1, 3), QTime(14, 26))) + add_mail(model, "Sports", "Linda Smith <linda.smith@nospam.com>", + QDateTime(QDate(2007, 1, 5), QTime(11, 33))) + add_mail(model, "AW: Sports", "Rolf Newschweinstein <rolfn@nospam.com>", + QDateTime(QDate(2007, 1, 5), QTime(12, 0))) + add_mail(model, "RE: Sports", "Petra Schmidt <petras@nospam.com>", + QDateTime(QDate(2007, 1, 5), QTime(12, 1))) + + return model + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = Window() + window.set_source_model(create_mail_model(window)) + window.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/basicfiltermodel/basicsortfiltermodel.pyproject b/examples/widgets/itemviews/basicfiltermodel/basicsortfiltermodel.pyproject new file mode 100644 index 000000000..3351bba88 --- /dev/null +++ b/examples/widgets/itemviews/basicfiltermodel/basicsortfiltermodel.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["basicsortfiltermodel.py"] +} diff --git a/examples/widgets/itemviews/basicsortfiltermodel.py b/examples/widgets/itemviews/basicsortfiltermodel.py deleted file mode 100644 index 4aea218d2..000000000 --- a/examples/widgets/itemviews/basicsortfiltermodel.py +++ /dev/null @@ -1,202 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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/dirview/dirview.py b/examples/widgets/itemviews/dirview/dirview.py new file mode 100644 index 000000000..d1be6958e --- /dev/null +++ b/examples/widgets/itemviews/dirview/dirview.py @@ -0,0 +1,59 @@ +# Copyright (C) 2020 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from argparse import ArgumentParser, RawTextHelpFormatter + +from PySide6.QtWidgets import (QApplication, QFileSystemModel, + QFileIconProvider, QScroller, QTreeView) +from PySide6.QtCore import QDir + +"""PySide6 port of the widgets/itemviews/dirview example from Qt v6.x""" + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + name = "Dir View" + argument_parser = ArgumentParser(description=name, + formatter_class=RawTextHelpFormatter) + argument_parser.add_argument("--no-custom", "-c", action="store_true", + help="Set QFileSystemModel.DontUseCustomDirectoryIcons") + argument_parser.add_argument("--no-watch", "-w", action="store_true", + help="Set QFileSystemModel.DontWatch") + argument_parser.add_argument("directory", + help="The directory to start in.", + nargs='?', type=str) + options = argument_parser.parse_args() + root_path = options.directory + + model = QFileSystemModel() + icon_provider = QFileIconProvider() + model.setIconProvider(icon_provider) + model.setRootPath("") + if options.no_custom: + model.setOption(QFileSystemModel.DontUseCustomDirectoryIcons) + if options.no_watch: + model.setOption(QFileSystemModel.DontWatchForChanges) + tree = QTreeView() + tree.setModel(model) + if root_path: + root_index = model.index(QDir.cleanPath(root_path)) + if root_index.isValid(): + tree.setRootIndex(root_index) + + # Demonstrating look and feel features + tree.setAnimated(False) + tree.setIndentation(20) + tree.setSortingEnabled(True) + availableSize = tree.screen().availableGeometry().size() + tree.resize(availableSize / 2) + tree.setColumnWidth(0, tree.width() / 3) + + # Make it flickable on touchscreens + QScroller.grabGesture(tree, QScroller.ScrollerGestureType.TouchGesture) + + tree.setWindowTitle(name) + tree.show() + + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/dirview/dirview.pyproject b/examples/widgets/itemviews/dirview/dirview.pyproject new file mode 100644 index 000000000..9470083c9 --- /dev/null +++ b/examples/widgets/itemviews/dirview/dirview.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["dirview.py"] +} diff --git a/examples/widgets/itemviews/dirview/doc/dirview.rst b/examples/widgets/itemviews/dirview/doc/dirview.rst new file mode 100644 index 000000000..7044fdf58 --- /dev/null +++ b/examples/widgets/itemviews/dirview/doc/dirview.rst @@ -0,0 +1,5 @@ +Dir View Example +================ + +The Dir View example shows a tree view of the local file system. It uses the +QFileSystemModel class to provide file and directory information. diff --git a/examples/widgets/itemviews/editabletreemodel/default.txt b/examples/widgets/itemviews/editabletreemodel/default.txt new file mode 100644 index 000000000..98746548b --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/default.txt @@ -0,0 +1,40 @@ +Getting Started How to familiarize yourself with Qt Designer + Launching Designer Running the Qt Designer application + The User Interface How to interact with Qt Designer + +Designing a Component Creating a GUI for your application + Creating a Dialog How to create a dialog + Composing the Dialog Putting widgets into the dialog example + Creating a Layout Arranging widgets on a form + Signal and Slot Connections Making widget communicate with each other + +Using a Component in Your Application Generating code from forms + The Direct Approach Using a form without any adjustments + The Single Inheritance Approach Subclassing a form's base class + The Multiple Inheritance Approach Subclassing the form itself + Automatic Connections Connecting widgets using a naming scheme + A Dialog Without Auto-Connect How to connect widgets without a naming scheme + A Dialog With Auto-Connect Using automatic connections + +Form Editing Mode How to edit a form in Qt Designer + Managing Forms Loading and saving forms + Editing a Form Basic editing techniques + The Property Editor Changing widget properties + The Object Inspector Examining the hierarchy of objects on a form + Layouts Objects that arrange widgets on a form + Applying and Breaking Layouts Managing widgets in layouts + Horizontal and Vertical Layouts Standard row and column layouts + The Grid Layout Arranging widgets in a matrix + Previewing Forms Checking that the design works + +Using Containers How to group widgets together + General Features Common container features + Frames QFrame + Group Boxes QGroupBox + Stacked Widgets QStackedWidget + Tab Widgets QTabWidget + Toolbox Widgets QToolBox + +Connection Editing Mode Connecting widgets together with signals and slots + Connecting Objects Making connections in Qt Designer + Editing Connections Changing existing connections diff --git a/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png Binary files differnew file mode 100644 index 000000000..b50c792aa --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png diff --git a/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst new file mode 100644 index 000000000..c936972a0 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst @@ -0,0 +1,10 @@ +Editable Tree Model Example +=========================== + +A Python application that demonstrates the analogous example in C++ +`Editable Tree Model Example <https://doc.qt.io/qt-6/qtwidgets-itemviews-editabletreemodel-example.html>`_ + +.. image:: editabletreemodel.png + :width: 611 + :alt: editabletreemodel screenshot + diff --git a/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject b/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject new file mode 100644 index 000000000..1e67c727b --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject @@ -0,0 +1,7 @@ +{ + "files": ["main.py", + "mainwindow.py", + "treeitem.py", + "treemodel.py", + "default.txt"] +} diff --git a/examples/widgets/itemviews/editabletreemodel/main.py b/examples/widgets/itemviews/editabletreemodel/main.py new file mode 100644 index 000000000..491baa68d --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/main.py @@ -0,0 +1,14 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + + +import sys +from PySide6.QtWidgets import QApplication +from mainwindow import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/editabletreemodel/mainwindow.py b/examples/widgets/itemviews/editabletreemodel/mainwindow.py new file mode 100644 index 000000000..1489bf28b --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/mainwindow.py @@ -0,0 +1,163 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from pathlib import Path + +from PySide6.QtCore import (QAbstractItemModel, QItemSelectionModel, + QModelIndex, Qt, Slot) +from PySide6.QtWidgets import (QAbstractItemView, QMainWindow, QTreeView, + QWidget) +from PySide6.QtTest import QAbstractItemModelTester + +from treemodel import TreeModel + + +class MainWindow(QMainWindow): + def __init__(self, parent: QWidget = None): + super().__init__(parent) + self.resize(573, 468) + + self.view = QTreeView() + self.view.setAlternatingRowColors(True) + self.view.setSelectionBehavior(QAbstractItemView.SelectItems) + self.view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + self.view.setAnimated(False) + self.view.setAllColumnsShowFocus(True) + self.setCentralWidget(self.view) + + menubar = self.menuBar() + file_menu = menubar.addMenu("&File") + self.exit_action = file_menu.addAction("E&xit") + self.exit_action.setShortcut("Ctrl+Q") + self.exit_action.triggered.connect(self.close) + + actions_menu = menubar.addMenu("&Actions") + actions_menu.triggered.connect(self.update_actions) + self.insert_row_action = actions_menu.addAction("Insert Row") + self.insert_row_action.setShortcut("Ctrl+I, R") + self.insert_row_action.triggered.connect(self.insert_row) + self.insert_column_action = actions_menu.addAction("Insert Column") + self.insert_column_action.setShortcut("Ctrl+I, C") + self.insert_column_action.triggered.connect(self.insert_column) + actions_menu.addSeparator() + self.remove_row_action = actions_menu.addAction("Remove Row") + self.remove_row_action.setShortcut("Ctrl+R, R") + self.remove_row_action.triggered.connect(self.remove_row) + self.remove_column_action = actions_menu.addAction("Remove Column") + self.remove_column_action.setShortcut("Ctrl+R, C") + self.remove_column_action.triggered.connect(self.remove_column) + actions_menu.addSeparator() + self.insert_child_action = actions_menu.addAction("Insert Child") + self.insert_child_action.setShortcut("Ctrl+N") + self.insert_child_action.triggered.connect(self.insert_child) + help_menu = menubar.addMenu("&Help") + about_qt_action = help_menu.addAction("About Qt", qApp.aboutQt) # noqa: F821 + about_qt_action.setShortcut("F1") + + self.setWindowTitle("Editable Tree Model") + + headers = ["Title", "Description"] + + file = Path(__file__).parent / "default.txt" + self.model = TreeModel(headers, file.read_text(), self) + + if "-t" in sys.argv: + QAbstractItemModelTester(self.model, self) + self.view.setModel(self.model) + self.view.expandAll() + + for column in range(self.model.columnCount()): + self.view.resizeColumnToContents(column) + + selection_model = self.view.selectionModel() + selection_model.selectionChanged.connect(self.update_actions) + + self.update_actions() + + @Slot() + def insert_child(self) -> None: + selection_model = self.view.selectionModel() + index: QModelIndex = selection_model.currentIndex() + model: QAbstractItemModel = self.view.model() + + if model.columnCount(index) == 0: + if not model.insertColumn(0, index): + return + + if not model.insertRow(0, index): + return + + for column in range(model.columnCount(index)): + child: QModelIndex = model.index(0, column, index) + model.setData(child, "[No data]", Qt.EditRole) + if not model.headerData(column, Qt.Horizontal): + model.setHeaderData(column, Qt.Horizontal, "[No header]", + Qt.EditRole) + + selection_model.setCurrentIndex( + model.index(0, 0, index), QItemSelectionModel.ClearAndSelect + ) + self.update_actions() + + @Slot() + def insert_column(self) -> None: + model: QAbstractItemModel = self.view.model() + column: int = self.view.selectionModel().currentIndex().column() + + changed: bool = model.insertColumn(column + 1) + if changed: + model.setHeaderData(column + 1, Qt.Horizontal, "[No header]", + Qt.EditRole) + + self.update_actions() + + @Slot() + def insert_row(self) -> None: + index: QModelIndex = self.view.selectionModel().currentIndex() + model: QAbstractItemModel = self.view.model() + parent: QModelIndex = index.parent() + + if not model.insertRow(index.row() + 1, parent): + return + + self.update_actions() + + for column in range(model.columnCount(parent)): + child: QModelIndex = model.index(index.row() + 1, column, parent) + model.setData(child, "[No data]", Qt.EditRole) + + @Slot() + def remove_column(self) -> None: + model: QAbstractItemModel = self.view.model() + column: int = self.view.selectionModel().currentIndex().column() + + if model.removeColumn(column): + self.update_actions() + + @Slot() + def remove_row(self) -> None: + index: QModelIndex = self.view.selectionModel().currentIndex() + model: QAbstractItemModel = self.view.model() + + if model.removeRow(index.row(), index.parent()): + self.update_actions() + + @Slot() + def update_actions(self) -> None: + selection_model = self.view.selectionModel() + has_selection: bool = not selection_model.selection().isEmpty() + self.remove_row_action.setEnabled(has_selection) + self.remove_column_action.setEnabled(has_selection) + + current_index = selection_model.currentIndex() + has_current: bool = current_index.isValid() + self.insert_row_action.setEnabled(has_current) + self.insert_column_action.setEnabled(has_current) + + if has_current: + self.view.closePersistentEditor(current_index) + msg = f"Position: ({current_index.row()},{current_index.column()})" + if not current_index.parent().isValid(): + msg += " in top level" + self.statusBar().showMessage(msg) diff --git a/examples/widgets/itemviews/editabletreemodel/treeitem.py b/examples/widgets/itemviews/editabletreemodel/treeitem.py new file mode 100644 index 000000000..1a25b0774 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/treeitem.py @@ -0,0 +1,94 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + + +class TreeItem: + def __init__(self, data: list, parent: 'TreeItem' = None): + self.item_data = data + self.parent_item = parent + self.child_items = [] + + def child(self, number: int) -> 'TreeItem': + if number < 0 or number >= len(self.child_items): + return None + return self.child_items[number] + + def last_child(self): + return self.child_items[-1] if self.child_items else None + + def child_count(self) -> int: + return len(self.child_items) + + def child_number(self) -> int: + if self.parent_item: + return self.parent_item.child_items.index(self) + return 0 + + def column_count(self) -> int: + return len(self.item_data) + + def data(self, column: int): + if column < 0 or column >= len(self.item_data): + return None + return self.item_data[column] + + def insert_children(self, position: int, count: int, columns: int) -> bool: + if position < 0 or position > len(self.child_items): + return False + + for row in range(count): + data = [None] * columns + item = TreeItem(data.copy(), self) + self.child_items.insert(position, item) + + return True + + def insert_columns(self, position: int, columns: int) -> bool: + if position < 0 or position > len(self.item_data): + return False + + for column in range(columns): + self.item_data.insert(position, None) + + for child in self.child_items: + child.insert_columns(position, columns) + + return True + + def parent(self): + return self.parent_item + + def remove_children(self, position: int, count: int) -> bool: + if position < 0 or position + count > len(self.child_items): + return False + + for row in range(count): + self.child_items.pop(position) + + return True + + def remove_columns(self, position: int, columns: int) -> bool: + if position < 0 or position + columns > len(self.item_data): + return False + + for column in range(columns): + self.item_data.pop(position) + + for child in self.child_items: + child.remove_columns(position, columns) + + return True + + def set_data(self, column: int, value): + if column < 0 or column >= len(self.item_data): + return False + + self.item_data[column] = value + return True + + def __repr__(self) -> str: + result = f"<treeitem.TreeItem at 0x{id(self):x}" + for d in self.item_data: + result += f' "{d}"' if d else " <None>" + result += f", {len(self.child_items)} children>" + return result diff --git a/examples/widgets/itemviews/editabletreemodel/treemodel.py b/examples/widgets/itemviews/editabletreemodel/treemodel.py new file mode 100644 index 000000000..a58572fca --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/treemodel.py @@ -0,0 +1,199 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + + +from PySide6.QtCore import QModelIndex, Qt, QAbstractItemModel +from treeitem import TreeItem + + +class TreeModel(QAbstractItemModel): + + def __init__(self, headers: list, data: str, parent=None): + super().__init__(parent) + + self.root_data = headers + self.root_item = TreeItem(self.root_data.copy()) + self.setup_model_data(data.split("\n"), self.root_item) + + def columnCount(self, parent: QModelIndex = None) -> int: + return self.root_item.column_count() + + def data(self, index: QModelIndex, role: int = None): + if not index.isValid(): + return None + + if role != Qt.DisplayRole and role != Qt.EditRole: + return None + + item: TreeItem = self.get_item(index) + + return item.data(index.column()) + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + if not index.isValid(): + return Qt.NoItemFlags + + return Qt.ItemIsEditable | QAbstractItemModel.flags(self, index) + + def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem: + if index.isValid(): + item: TreeItem = index.internalPointer() + if item: + return item + + return self.root_item + + def headerData(self, section: int, orientation: Qt.Orientation, + role: int = Qt.DisplayRole): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.root_item.data(section) + + return None + + def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: + if parent.isValid() and parent.column() != 0: + return QModelIndex() + + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return QModelIndex() + + child_item: TreeItem = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + return QModelIndex() + + def insertColumns(self, position: int, columns: int, + parent: QModelIndex = QModelIndex()) -> bool: + self.beginInsertColumns(parent, position, position + columns - 1) + success: bool = self.root_item.insert_columns(position, columns) + self.endInsertColumns() + + return success + + def insertRows(self, position: int, rows: int, + parent: QModelIndex = QModelIndex()) -> bool: + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return False + + self.beginInsertRows(parent, position, position + rows - 1) + column_count = self.root_item.column_count() + success: bool = parent_item.insert_children(position, rows, column_count) + self.endInsertRows() + + return success + + def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex: + if not index.isValid(): + return QModelIndex() + + child_item: TreeItem = self.get_item(index) + if child_item: + parent_item: TreeItem = child_item.parent() + else: + parent_item = None + + if parent_item == self.root_item or not parent_item: + return QModelIndex() + + return self.createIndex(parent_item.child_number(), 0, parent_item) + + def removeColumns(self, position: int, columns: int, + parent: QModelIndex = QModelIndex()) -> bool: + self.beginRemoveColumns(parent, position, position + columns - 1) + success: bool = self.root_item.remove_columns(position, columns) + self.endRemoveColumns() + + if self.root_item.column_count() == 0: + self.removeRows(0, self.rowCount()) + + return success + + def removeRows(self, position: int, rows: int, + parent: QModelIndex = QModelIndex()) -> bool: + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return False + + self.beginRemoveRows(parent, position, position + rows - 1) + success: bool = parent_item.remove_children(position, rows) + self.endRemoveRows() + + return success + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + if parent.isValid() and parent.column() > 0: + return 0 + + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return 0 + return parent_item.child_count() + + def setData(self, index: QModelIndex, value, role: int) -> bool: + if role != Qt.EditRole: + return False + + item: TreeItem = self.get_item(index) + result: bool = item.set_data(index.column(), value) + + if result: + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + + return result + + def setHeaderData(self, section: int, orientation: Qt.Orientation, value, + role: int = None) -> bool: + if role != Qt.EditRole or orientation != Qt.Horizontal: + return False + + result: bool = self.root_item.set_data(section, value) + + if result: + self.headerDataChanged.emit(orientation, section, section) + + return result + + def setup_model_data(self, lines: list, parent: TreeItem): + parents = [parent] + indentations = [0] + + for line in lines: + line = line.rstrip() + if line and "\t" in line: + + position = 0 + while position < len(line): + if line[position] != " ": + break + position += 1 + + column_data = line[position:].split("\t") + column_data = [string for string in column_data if string] + + if position > indentations[-1]: + if parents[-1].child_count() > 0: + parents.append(parents[-1].last_child()) + indentations.append(position) + else: + while position < indentations[-1] and parents: + parents.pop() + indentations.pop() + + parent: TreeItem = parents[-1] + col_count = self.root_item.column_count() + parent.insert_children(parent.child_count(), 1, col_count) + + for column in range(len(column_data)): + child = parent.last_child() + child.set_data(column, column_data[column]) + + def _repr_recursion(self, item: TreeItem, indent: int = 0) -> str: + result = " " * indent + repr(item) + "\n" + for child in item.child_items: + result += self._repr_recursion(child, indent + 2) + return result + + def __repr__(self) -> str: + return self._repr_recursion(self.root_item) diff --git a/examples/widgets/itemviews/fetchmore.py b/examples/widgets/itemviews/fetchmore.py deleted file mode 100644 index 2b0d8c104..000000000 --- a/examples/widgets/itemviews/fetchmore.py +++ /dev/null @@ -1,147 +0,0 @@ - -############################################################################# -## -## 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 Qt for Python 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, 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 qApp.palette().base() - - return 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/fetchmore/fetchmore.py b/examples/widgets/itemviews/fetchmore/fetchmore.py new file mode 100644 index 000000000..5150250e0 --- /dev/null +++ b/examples/widgets/itemviews/fetchmore/fetchmore.py @@ -0,0 +1,137 @@ +# Copyright (C) 2009 Darryl Wallace, 2009 <wallacdj@gmail.com> +# Copyright (C) 2013 Riverbank Computing Limited. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +"""PySide6 port of the itemviews/fetchmore/fetchmore example from Qt v6.x + +Navigate to a directory with many entries by doubleclicking and scroll +down the list to see the model being populated on demand. +""" + +import sys + +from PySide6.QtCore import (QAbstractListModel, QDir, + QModelIndex, Qt, Signal, Slot) +from PySide6.QtWidgets import (QApplication, QFileIconProvider, QListView, + QPlainTextEdit, QSizePolicy, QVBoxLayout, + QWidget) + + +BATCH_SIZE = 100 + + +class FileListModel(QAbstractListModel): + + number_populated = Signal(str, int, int, int) + + def __init__(self, parent=None): + super().__init__(parent) + + self._path = '' + self._file_count = 0 + self._file_list = [] + self._icon_provider = QFileIconProvider() + + def rowCount(self, parent=QModelIndex()): + return self._file_count + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + row = index.row() + if row >= len(self._file_list) or row < 0: + return None + + if role == Qt.DisplayRole: + return self._file_list[row].fileName() + + if role == Qt.BackgroundRole: + batch = row // BATCH_SIZE + palette = qApp.palette() # noqa: F821 + return palette.base() if batch % 2 == 0 else palette.alternateBase() + + if role == Qt.DecorationRole: + return self._icon_provider.icon(self._file_list[row]) + + return None + + def canFetchMore(self, index): + return self._file_count < len(self._file_list) + + def fetchMore(self, index): + start = self._file_count + total = len(self._file_list) + remainder = total - start + items_to_fetch = min(BATCH_SIZE, remainder) + + self.beginInsertRows(QModelIndex(), start, start + items_to_fetch) + + self._file_count += items_to_fetch + + self.endInsertRows() + + self.number_populated.emit(self._path, start, items_to_fetch, total) + + @Slot(str) + def set_dir_path(self, path): + self._path = path + directory = QDir(path) + + self.beginResetModel() + directory_filter = QDir.AllEntries | QDir.NoDot + self._file_list = directory.entryInfoList(directory_filter, QDir.Name) + self._file_count = 0 + self.endResetModel() + + def fileinfo_at(self, index): + return self._file_list[index.row()] + + +class Window(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + self._model = FileListModel(self) + self._model.set_dir_path(QDir.rootPath()) + + self._view = QListView() + self._view.setModel(self._model) + + self._log_viewer = QPlainTextEdit() + self._log_viewer.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, + QSizePolicy.Preferred)) + + self._model.number_populated.connect(self.update_log) + self._view.activated.connect(self.activated) + + layout = QVBoxLayout(self) + layout.addWidget(self._view) + layout.addWidget(self._log_viewer) + + self.setWindowTitle("Fetch More Example") + + @Slot(str, int, int, int) + def update_log(self, path, start, number, total): + native_path = QDir.toNativeSeparators(path) + last = start + number - 1 + entry = f'{start}..{last}/{total} items from "{native_path}" added.' + self._log_viewer.appendPlainText(entry) + + @Slot(QModelIndex) + def activated(self, index): + fileinfo = self._model.fileinfo_at(index) + if fileinfo.isDir(): + self._log_viewer.clear() + self._model.set_dir_path(fileinfo.absoluteFilePath()) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + window = Window() + window.resize(400, 500) + window.show() + + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/fetchmore/fetchmore.pyproject b/examples/widgets/itemviews/fetchmore/fetchmore.pyproject new file mode 100644 index 000000000..e5c369374 --- /dev/null +++ b/examples/widgets/itemviews/fetchmore/fetchmore.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["fetchmore.py"] +} diff --git a/examples/widgets/itemviews/itemviews.pyproject b/examples/widgets/itemviews/itemviews.pyproject deleted file mode 100644 index a582259cc..000000000 --- a/examples/widgets/itemviews/itemviews.pyproject +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files": ["basicsortfiltermodel.py", "fetchmore.py"] -} diff --git a/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png Binary files differnew file mode 100644 index 000000000..8b5c8d0c3 --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png diff --git a/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst new file mode 100644 index 000000000..d5e2831bf --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst @@ -0,0 +1,8 @@ +JSON Model Example +================== + +Simple example to visualize the values of a JSON file. + +.. image:: jsonmodel.png + :width: 400 + :alt: JSON Model Screenshot diff --git a/examples/widgets/itemviews/jsonmodel/example.json b/examples/widgets/itemviews/jsonmodel/example.json new file mode 100644 index 000000000..3c3ecfbfd --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/example.json @@ -0,0 +1,26 @@ +{ + "id": "0001", + "type": "donut", + "name": "Cake", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Blueberry" }, + { "id": "1004", "type": "Devil's Food" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5007", "type": "Powdered Sugar" }, + { "id": "5006", "type": "Chocolate with Sprinkles" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] +} diff --git a/examples/widgets/itemviews/jsonmodel/jsonmodel.py b/examples/widgets/itemviews/jsonmodel/jsonmodel.py new file mode 100644 index 000000000..6e614c77f --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/jsonmodel.py @@ -0,0 +1,320 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import json +import sys +from typing import Any, List, Dict, Union + +from PySide6.QtWidgets import QTreeView, QApplication, QHeaderView +from PySide6.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt, QFileInfo + + +class TreeItem: + """A Json item corresponding to a line in QTreeView""" + + def __init__(self, parent: "TreeItem" = None): + self._parent = parent + self._key = "" + self._value = "" + self._value_type = None + self._children = [] + + def appendChild(self, item: "TreeItem"): + """Add item as a child""" + self._children.append(item) + + def child(self, row: int) -> "TreeItem": + """Return the child of the current item from the given row""" + return self._children[row] + + def parent(self) -> "TreeItem": + """Return the parent of the current item""" + return self._parent + + def childCount(self) -> int: + """Return the number of children of the current item""" + return len(self._children) + + def row(self) -> int: + """Return the row where the current item occupies in the parent""" + return self._parent._children.index(self) if self._parent else 0 + + @property + def key(self) -> str: + """Return the key name""" + return self._key + + @key.setter + def key(self, key: str): + """Set key name of the current item""" + self._key = key + + @property + def value(self) -> str: + """Return the value name of the current item""" + return self._value + + @value.setter + def value(self, value: str): + """Set value name of the current item""" + self._value = value + + @property + def value_type(self): + """Return the python type of the item's value.""" + return self._value_type + + @value_type.setter + def value_type(self, value): + """Set the python type of the item's value.""" + self._value_type = value + + @classmethod + def load( + cls, value: Union[List, Dict], parent: "TreeItem" = None, sort=True + ) -> "TreeItem": + """Create a 'root' TreeItem from a nested list or a nested dictonary + + Examples: + with open("file.json") as file: + data = json.dump(file) + root = TreeItem.load(data) + + This method is a recursive function that calls itself. + + Returns: + TreeItem: TreeItem + """ + rootItem = TreeItem(parent) + rootItem.key = "root" + + if isinstance(value, dict): + items = sorted(value.items()) if sort else value.items() + + for key, value in items: + child = cls.load(value, rootItem) + child.key = key + child.value_type = type(value) + rootItem.appendChild(child) + + elif isinstance(value, list): + for index, value in enumerate(value): + child = cls.load(value, rootItem) + child.key = index + child.value_type = type(value) + rootItem.appendChild(child) + + else: + rootItem.value = value + rootItem.value_type = type(value) + + return rootItem + + +class JsonModel(QAbstractItemModel): + """ An editable model of Json data """ + + def __init__(self, parent: QObject = None): + super().__init__(parent) + + self._rootItem = TreeItem() + self._headers = ("key", "value") + + def clear(self): + """ Clear data from the model """ + self.load({}) + + def load(self, document: dict): + """Load model from a nested dictionary returned by json.loads() + + Arguments: + document (dict): JSON-compatible dictionary + """ + + assert isinstance( + document, (dict, list, tuple) + ), "`document` must be of dict, list or tuple, " f"not {type(document)}" + + self.beginResetModel() + + self._rootItem = TreeItem.load(document) + self._rootItem.value_type = type(document) + + self.endResetModel() + + return True + + def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any: + """Override from QAbstractItemModel + + Return data from a json item according index and role + + """ + if not index.isValid(): + return None + + item = index.internalPointer() + + if role == Qt.DisplayRole: + if index.column() == 0: + return item.key + + if index.column() == 1: + return item.value + + elif role == Qt.EditRole: + if index.column() == 1: + return item.value + + def setData(self, index: QModelIndex, value: Any, role: Qt.ItemDataRole): + """Override from QAbstractItemModel + + Set json item according index and role + + Args: + index (QModelIndex) + value (Any) + role (Qt.ItemDataRole) + + """ + if role == Qt.EditRole: + if index.column() == 1: + item = index.internalPointer() + item.value = str(value) + + self.dataChanged.emit(index, index, [Qt.EditRole]) + + return True + + return False + + def headerData( + self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole + ): + """Override from QAbstractItemModel + + For the JsonModel, it returns only data for columns (orientation = Horizontal) + + """ + if role != Qt.DisplayRole: + return None + + if orientation == Qt.Horizontal: + return self._headers[section] + + def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex: + """Override from QAbstractItemModel + + Return index according row, column and parent + + """ + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + parentItem = self._rootItem + else: + parentItem = parent.internalPointer() + + childItem = parentItem.child(row) + if childItem: + return self.createIndex(row, column, childItem) + else: + return QModelIndex() + + def parent(self, index: QModelIndex) -> QModelIndex: + """Override from QAbstractItemModel + + Return parent index of index + + """ + + if not index.isValid(): + return QModelIndex() + + childItem = index.internalPointer() + parentItem = childItem.parent() + + if parentItem == self._rootItem: + return QModelIndex() + + return self.createIndex(parentItem.row(), 0, parentItem) + + def rowCount(self, parent=QModelIndex()): + """Override from QAbstractItemModel + + Return row count from parent index + """ + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parentItem = self._rootItem + else: + parentItem = parent.internalPointer() + + return parentItem.childCount() + + def columnCount(self, parent=QModelIndex()): + """Override from QAbstractItemModel + + Return column number. For the model, it always return 2 columns + """ + return 2 + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + """Override from QAbstractItemModel + + Return flags of index + """ + flags = super(JsonModel, self).flags(index) + + if index.column() == 1: + return Qt.ItemIsEditable | flags + else: + return flags + + def to_json(self, item=None): + + if item is None: + item = self._rootItem + + nchild = item.childCount() + + if item.value_type is dict: + document = {} + for i in range(nchild): + ch = item.child(i) + document[ch.key] = self.to_json(ch) + return document + + elif item.value_type == list: + document = [] + for i in range(nchild): + ch = item.child(i) + document.append(self.to_json(ch)) + return document + + else: + return item.value + + +if __name__ == "__main__": + + app = QApplication(sys.argv) + view = QTreeView() + model = JsonModel() + + view.setModel(model) + + json_path = QFileInfo(__file__).absoluteDir().filePath("example.json") + + with open(json_path) as file: + document = json.load(file) + model.load(document) + + view.show() + view.header().setSectionResizeMode(0, QHeaderView.Stretch) + view.setAlternatingRowColors(True) + view.resize(500, 300) + app.exec() diff --git a/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject b/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject new file mode 100644 index 000000000..7d551b31c --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["jsonmodel.py", "example.json"] +} diff --git a/examples/widgets/itemviews/spinboxdelegate/doc/spinboxdelegate.rst b/examples/widgets/itemviews/spinboxdelegate/doc/spinboxdelegate.rst new file mode 100644 index 000000000..12e505207 --- /dev/null +++ b/examples/widgets/itemviews/spinboxdelegate/doc/spinboxdelegate.rst @@ -0,0 +1,5 @@ +SpinBox Delegate Example +========================= + +A simple example that shows how a view can use a custom delegate to edit +data obtained from a model. diff --git a/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.py b/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.py new file mode 100644 index 000000000..577f0faa5 --- /dev/null +++ b/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.py @@ -0,0 +1,79 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtWidgets import (QApplication, QStyledItemDelegate, QSpinBox, + QTableView) +from PySide6.QtGui import QStandardItemModel, Qt +from PySide6.QtCore import QModelIndex + +"""PySide6 port of the widgets/itemviews/spinboxdelegate from Qt v6.x""" + + +#! [0] +class SpinBoxDelegate(QStyledItemDelegate): + """A delegate that allows the user to change integer values from the model + using a spin box widget. """ + +#! [0] + def __init__(self, parent=None): + super().__init__(parent) +#! [0] + +#! [1] + def createEditor(self, parent, option, index): + editor = QSpinBox(parent) + editor.setFrame(False) + editor.setMinimum(0) + editor.setMaximum(100) + return editor +#! [1] + +#! [2] + def setEditorData(self, editor, index): + value = index.model().data(index, Qt.EditRole) + editor.setValue(value) +#! [2] + +#! [3] + def setModelData(self, editor, model, index): + editor.interpretText() + value = editor.value() + model.setData(index, value, Qt.EditRole) +#! [3] + +#! [4] + def updateEditorGeometry(self, editor, option, index): + editor.setGeometry(option.rect) +#! [4] + + +#! [main0] +if __name__ == '__main__': + app = QApplication(sys.argv) + + model = QStandardItemModel(4, 2) + tableView = QTableView() + tableView.setModel(model) + + delegate = SpinBoxDelegate() + tableView.setItemDelegate(delegate) +#! [main0] + + tableView.horizontalHeader().setStretchLastSection(True) + +#! [main1] + for row in range(4): + for column in range(2): + index = model.index(row, column, QModelIndex()) + value = (row + 1) * (column + 1) + model.setData(index, value) +#! [main1] //# [main2] +#! [main2] + +#! [main3] + tableView.setWindowTitle("Spin Box Delegate") + tableView.show() + sys.exit(app.exec()) +#! [main3] diff --git a/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.pyproject b/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.pyproject new file mode 100644 index 000000000..70616905c --- /dev/null +++ b/examples/widgets/itemviews/spinboxdelegate/spinboxdelegate.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["spinboxdelegate.py"] +} diff --git a/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png Binary files differnew file mode 100644 index 000000000..ae7dde24b --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png diff --git a/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst new file mode 100644 index 000000000..c0839b232 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst @@ -0,0 +1,10 @@ +Spreadsheet example +=================== + +The Spreadsheet example shows how a table view can be used to create a simple +spreadsheet application. Custom delegates are used to render different types of +data in distinctive colors. + +.. image:: spreadsheet.png + :width: 400 + :alt: Spreadsheet screenshot diff --git a/examples/widgets/itemviews/spreadsheet/main.py b/examples/widgets/itemviews/spreadsheet/main.py new file mode 100644 index 000000000..0ecc5ec23 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/main.py @@ -0,0 +1,19 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QApplication, QLayout + +from spreadsheet import SpreadSheet + +if __name__ == "__main__": + app = QApplication() + + sheet = SpreadSheet(10, 6) + sheet.setWindowIcon(QPixmap(":/images/interview.png")) + sheet.show() + sheet.layout().setSizeConstraint(QLayout.SetFixedSize) + + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/spreadsheet/spreadsheet.py b/examples/widgets/itemviews/spreadsheet/spreadsheet.py new file mode 100644 index 000000000..82ebe5ebb --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/spreadsheet.py @@ -0,0 +1,544 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QPoint, Qt, QCoreApplication, Slot +from PySide6.QtGui import QAction, QBrush, QPixmap, QColor, QPainter +from PySide6.QtWidgets import (QColorDialog, QComboBox, QDialog, QFontDialog, + QGroupBox, QHBoxLayout, QMainWindow, QLabel, + QLineEdit, QMessageBox, QPushButton, QToolBar, + QTableWidgetItem, QTableWidget, QVBoxLayout, QWidget) + +from spreadsheetdelegate import SpreadSheetDelegate +from spreadsheetitem import SpreadSheetItem + +from typing import Optional +from numbers import Number + + +class SpreadSheet(QMainWindow): + def __init__(self, rows: Number, cols: Number, parent: Optional[QWidget] = None) -> None: + super().__init__(parent) + + self._tool_bar = QToolBar(self) + self._color_action = QAction() + self._font_action = QAction() + self._first_separator = QAction() + self._cell_sum_action = QAction() + self._cell_add_action = QAction() + self._cell_sub_action = QAction() + self._cell_mul_action = QAction() + self._cell_div_action = QAction() + self._second_separator = QAction() + self._clear_action = QAction() + self._about_spreadsheet = QAction() + self._exit_action = QAction() + + # self._print_action = QAction() + + self._cell_label = QLabel(self._tool_bar) + self._table = QTableWidget(rows, cols, self) + self._formula_input = QLineEdit(self) + + self.addToolBar(self._tool_bar) + + self._cell_label.setMinimumSize(80, 0) + + self._tool_bar.addWidget(self._cell_label) + self._tool_bar.addWidget(self._formula_input) + + self._table.setSizeAdjustPolicy(QTableWidget.SizeAdjustPolicy.AdjustToContents) + for c in range(cols): + character = chr(ord('A') + c) + self._table.setHorizontalHeaderItem(c, QTableWidgetItem(character)) + + self._table.setItemPrototype(self._table.item(rows - 1, cols - 1)) + self._table.setItemDelegate(SpreadSheetDelegate()) + + self.create_actions() + self.update_color(None) + self.setup_menu_bar() + self.setup_contents() + self.setup_context_menu() + self.setCentralWidget(self._table) + + self.statusBar() + self._table.currentItemChanged.connect(self.update_status) + self._table.currentItemChanged.connect(self.update_color) + self._table.currentItemChanged.connect(self.update_line_edit) + self._table.itemChanged.connect(self.update_status) + self._formula_input.returnPressed.connect(self.return_pressed) + self._table.itemChanged.connect(self.update_line_edit) + + self.setWindowTitle("Spreadsheet") + + def create_actions(self) -> None: + self._cell_sum_action = QAction("Sum", self) + self._cell_sum_action.triggered.connect(self.action_sum) + + self._cell_add_action = QAction("&Add", self) + self._cell_add_action.setShortcut(Qt.CTRL | Qt.Key_Plus) + self._cell_add_action.triggered.connect(self.action_add) + + self._cell_sub_action = QAction("&Subtract", self) + self._cell_sub_action.setShortcut(Qt.CTRL | Qt.Key_Minus) + self._cell_sub_action.triggered.connect(self.action_subtract) + + self._cell_mul_action = QAction("&Multiply", self) + self._cell_mul_action.setShortcut(Qt.CTRL | Qt.Key_multiply) + self._cell_mul_action.triggered.connect(self.action_multiply) + + self._cell_div_action = QAction("&Divide", self) + self._cell_div_action.setShortcut(Qt.CTRL | Qt.Key_division) + self._cell_div_action.triggered.connect(self.action_divide) + + self._font_action = QAction("Font...", self) + self._font_action.setShortcut(Qt.CTRL | Qt.Key_F) + self._font_action.triggered.connect(self.select_font) + + self._color_action = QAction(QPixmap(16, 16), "Background &Color...", self) + self._color_action.triggered.connect(self.select_color) + + self._clear_action = QAction("Clear", self) + self._clear_action.setShortcut(Qt.Key_Delete) + self._clear_action.triggered.connect(self.clear) + + self._about_spreadsheet = QAction("About Spreadsheet", self) + self._about_spreadsheet.triggered.connect(self.show_about) + + self._exit_action = QAction("E&xit", self) + self._exit_action.triggered.connect(QCoreApplication.quit) + + self._first_separator = QAction(self) + self._first_separator.setSeparator(True) + + self._second_separator = QAction(self) + self._second_separator.setSeparator(True) + + def setup_menu_bar(self) -> None: + file_menu = self.menuBar().addMenu("&File") + # file_menu.addAction(self._print_action) + file_menu.addAction(self._exit_action) + + cell_menu = self.menuBar().addMenu("&Cell") + cell_menu.addAction(self._cell_add_action) + cell_menu.addAction(self._cell_sub_action) + cell_menu.addAction(self._cell_mul_action) + cell_menu.addAction(self._cell_div_action) + cell_menu.addAction(self._cell_sum_action) + cell_menu.addSeparator() + cell_menu.addAction(self._color_action) + cell_menu.addAction(self._font_action) + + self.menuBar().addSeparator() + + about_menu = self.menuBar().addMenu("&Help") + about_menu.addAction(self._about_spreadsheet) + + @Slot(QTableWidgetItem) + def update_status(self, item: QTableWidgetItem) -> None: + if item and item == self._table.currentItem(): + self.statusBar().showMessage(str(item.data(Qt.StatusTipRole)), 1000) + self._cell_label.setText( + "Cell: ({})".format( + SpreadSheetItem.encode_pos(self._table.row(item), self._table.column(item)) + ) + ) + + @Slot(QTableWidgetItem) + def update_color(self, item: QTableWidgetItem) -> None: + pix = QPixmap(16, 16) + col = QColor() + if item: + col = item.background().color() + if not col.isValid(): + col = self.palette().base().color() + + pt = QPainter(pix) + pt.fillRect(0, 0, 16, 16, col) + + lighter = col.lighter() + pt.setPen(lighter) + light_frame = [QPoint(0, 15), QPoint(0, 0), QPoint(15, 0)] + pt.drawPolyline(light_frame) + + pt.setPen(col.darker()) + darkFrame = [QPoint(1, 15), QPoint(15, 15), QPoint(15, 1)] + pt.drawPolyline(darkFrame) + + pt.end() + + self._color_action.setIcon(pix) + + @Slot(QTableWidgetItem) + def update_line_edit(self, item: QTableWidgetItem) -> None: + if item != self._table.currentItem(): + return + if item: + self._formula_input.setText(str(item.data(Qt.EditRole))) + else: + self._formula_input.clear() + + @Slot() + def return_pressed(self) -> None: + text = self._formula_input.text() + row = self._table.currentRow() + col = self._table.currentColumn() + item = self._table.item(row, col) + if not item: + self._table.setItem(row, col, SpreadSheetItem(text)) + else: + item.setData(Qt.EditRole, text) + self._table.viewport().update() + + @Slot() + def select_color(self) -> None: + item = self._table.currentItem() + col = item.background().color() if item else self._table.palette().base().color() + col = QColorDialog.getColor(col, self) + if not col.isValid(): + return + + selected = self._table.selectedItems() + if not selected: + return + + for i in selected: + if i: + i.setBackground(col) + + self.update_color(self._table.currentItem()) + + @Slot() + def select_font(self) -> None: + selected = self._table.selectedItems() + if not selected: + return + + ok = False + fnt = QFontDialog.getFont(ok, self.font(), self) + + if not ok: + return + for i in selected: + if i: + i.setFont(fnt) + + def run_input_dialog(self, title: str, c1Text: str, c2Text: str, opText: str, + outText: str, cell1: str, cell2: str, outCell: str) -> bool: + rows, cols = [], [] + for c in range(self._table.columnCount()): + cols.append(chr(ord('A') + c)) + for r in range(self._table.rowCount()): + rows.append(str(1 + r)) + + add_dialog = QDialog(self) + add_dialog.setWindowTitle(title) + + group = QGroupBox(title, add_dialog) + group.setMinimumSize(250, 100) + + cell1_label = QLabel(c1Text, group) + cell1_row_input = QComboBox(group) + c1_row, c1_col = SpreadSheetItem.decode_pos(cell1) + cell1_row_input.addItems(rows) + cell1_row_input.setCurrentIndex(c1_row) + + cell1_col_input = QComboBox(group) + cell1_col_input.addItems(cols) + cell1_col_input.setCurrentIndex(c1_col) + + operator_label = QLabel(opText, group) + operator_label.setAlignment(Qt.AlignHCenter) + + cell2_label = QLabel(c2Text, group) + cell2_row_input = QComboBox(group) + c2_row, c2_col = SpreadSheetItem.decode_pos(cell2) + cell2_row_input.addItems(rows) + cell2_row_input.setCurrentIndex(c2_row) + cell2_col_input = QComboBox(group) + cell2_col_input.addItems(cols) + cell2_col_input.setCurrentIndex(c2_col) + + equals_label = QLabel("=", group) + equals_label.setAlignment(Qt.AlignHCenter) + + out_label = QLabel(outText, group) + out_row_input = QComboBox(group) + out_row, out_col = SpreadSheetItem.decode_pos(outCell) + out_row_input.addItems(rows) + out_row_input.setCurrentIndex(out_row) + out_col_input = QComboBox(group) + out_col_input.addItems(cols) + out_col_input.setCurrentIndex(out_col) + + cancel_button = QPushButton("Cancel", add_dialog) + cancel_button.clicked.connect(add_dialog.reject) + + ok_button = QPushButton("OK", add_dialog) + ok_button.setDefault(True) + ok_button.clicked.connect(add_dialog.accept) + + buttons_layout = QHBoxLayout() + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_button) + buttons_layout.addSpacing(10) + buttons_layout.addWidget(cancel_button) + + dialog_layout = QVBoxLayout(add_dialog) + dialog_layout.addWidget(group) + dialog_layout.addStretch(1) + dialog_layout.addItem(buttons_layout) + + cell1_layout = QHBoxLayout() + cell1_layout.addWidget(cell1_label) + cell1_layout.addSpacing(10) + cell1_layout.addWidget(cell1_col_input) + cell1_layout.addSpacing(10) + cell1_layout.addWidget(cell1_row_input) + + cell2_layout = QHBoxLayout() + cell2_layout.addWidget(cell2_label) + cell2_layout.addSpacing(10) + cell2_layout.addWidget(cell2_col_input) + cell2_layout.addSpacing(10) + cell2_layout.addWidget(cell2_row_input) + + out_layout = QHBoxLayout() + out_layout.addWidget(out_label) + out_layout.addSpacing(10) + out_layout.addWidget(out_col_input) + out_layout.addSpacing(10) + out_layout.addWidget(out_row_input) + + v_layout = QVBoxLayout(group) + v_layout.addItem(cell1_layout) + v_layout.addWidget(operator_label) + v_layout.addItem(cell2_layout) + v_layout.addWidget(equals_label) + v_layout.addStretch(1) + v_layout.addItem(out_layout) + + if add_dialog.exec(): + cell1 = cell1_col_input.currentText() + cell1_row_input.currentText() + cell2 = cell2_col_input.currentText() + cell2_row_input.currentText() + outCell = out_col_input.currentText() + out_row_input.currentText() + return True + + return False + + @Slot() + def action_sum(self) -> None: + row_first = row_last = row_cur = 0 + col_first = col_last = col_cur = 0 + + selected = self._table.selectedItems() + + if selected is not None: + first = selected[0] + last = selected[-1] + row_first = self._table.row(first) + row_last = self._table.row(last) + col_first = self._table.column(first) + col_last = self._table.column(last) + + current = self._table.currentItem() + + if current: + row_cur = self._table.row(current) + col_cur = self._table.column(current) + + cell1 = SpreadSheetItem.encode_pos(row_first, col_first) + cell2 = SpreadSheetItem.encode_pos(row_last, col_last) + out = SpreadSheetItem.encode_pos(row_cur, col_cur) + + if self.run_input_dialog( + "Sum cells", "First cell:", "Last cell:", + f"{(chr(0x03a3))}", "Output to:", + cell1, cell2, out + ): + row, col = SpreadSheetItem.decode_pos(out) + self._table.item(row, col).setText(f"sum {cell1} {cell2}") + + def action_math_helper(self, title: str, op: str) -> None: + cell1 = "C1" + cell2 = "C2" + out = "C3" + + current = self._table.currentItem() + if current: + out = SpreadSheetItem.encode_pos(self._table.currentRow(), self._table.currentColumn()) + + if self.run_input_dialog(title, "Cell 1", "Cell 2", op, "Output to:", cell1, cell2, out): + row, col = SpreadSheetItem.decode_pos(out) + self._table.item(row, col).setText(f"{op} {cell1} {cell2}") + + @Slot() + def action_add(self) -> None: + self.action_math_helper("Addition", "+") + + @Slot() + def action_subtract(self) -> None: + self.action_math_helper("Subtraction", "-") + + @Slot() + def action_multiply(self) -> None: + self.action_math_helper("Multiplication", "*") + + @Slot() + def action_divide(self) -> None: + self.action_math_helper("Division", "/") + + @Slot() + def clear(self) -> None: + selected_items = self._table.selectedItems() + for item in selected_items: + item.setText("") + + def setup_context_menu(self) -> None: + self.addAction(self._cell_add_action) + self.addAction(self._cell_sub_action) + self.addAction(self._cell_mul_action) + self.addAction(self._cell_div_action) + self.addAction(self._cell_sum_action) + self.addAction(self._first_separator) + self.addAction(self._color_action) + self.addAction(self._font_action) + self.addAction(self._second_separator) + self.addAction(self._clear_action) + self.setContextMenuPolicy(Qt.ActionsContextMenu) + + def setup_contents(self) -> None: + title_background = QBrush(Qt.lightGray) + title_font = self._table.font() + title_font.setBold(True) + + # column 0 + self._table.setItem(0, 0, SpreadSheetItem("Item")) + self._table.item(0, 0).setBackground(title_background) + self._table.item(0, 0).setToolTip( + "This column shows the purchased item/service" + ) + self._table.item(0, 0).setFont(title_font) + + self._table.setItem(1, 0, SpreadSheetItem("AirportBus")) + self._table.setItem(2, 0, SpreadSheetItem("Flight (Munich)")) + self._table.setItem(3, 0, SpreadSheetItem("Lunch")) + self._table.setItem(4, 0, SpreadSheetItem("Flight (LA)")) + self._table.setItem(5, 0, SpreadSheetItem("Taxi")) + self._table.setItem(6, 0, SpreadSheetItem("Dinner")) + self._table.setItem(7, 0, SpreadSheetItem("Hotel")) + self._table.setItem(8, 0, SpreadSheetItem("Flight (Oslo)")) + self._table.setItem(9, 0, SpreadSheetItem("Total:")) + + self._table.item(9, 0).setFont(title_font) + self._table.item(9, 0).setBackground(title_background) + + # column 1 + self._table.setItem(0, 1, SpreadSheetItem("Date")) + self._table.item(0, 1).setBackground(title_background) + self._table.item(0, 1).setToolTip( + "This column shows the purchase date, double click to change" + ) + self._table.item(0, 1).setFont(title_font) + + self._table.setItem(1, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(2, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(3, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(4, 1, SpreadSheetItem("21/5/2006")) + self._table.setItem(5, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(6, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(7, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(8, 1, SpreadSheetItem("18/6/2006")) + + self._table.setItem(9, 1, SpreadSheetItem()) + self._table.item(9, 1).setBackground(title_background) + + # column 2 + self._table.setItem(0, 2, SpreadSheetItem("Price")) + self._table.item(0, 2).setBackground(title_background) + self._table.item(0, 2).setToolTip("This column shows the price of the purchase") + self._table.item(0, 2).setFont(title_font) + + self._table.setItem(1, 2, SpreadSheetItem("150")) + self._table.setItem(2, 2, SpreadSheetItem("2350")) + self._table.setItem(3, 2, SpreadSheetItem("-14")) + self._table.setItem(4, 2, SpreadSheetItem("980")) + self._table.setItem(5, 2, SpreadSheetItem("5")) + self._table.setItem(6, 2, SpreadSheetItem("120")) + self._table.setItem(7, 2, SpreadSheetItem("300")) + self._table.setItem(8, 2, SpreadSheetItem("1240")) + + self._table.setItem(9, 2, SpreadSheetItem()) + self._table.item(9, 2).setBackground(Qt.lightGray) + + # column 3 + self._table.setItem(0, 3, SpreadSheetItem("Currency")) + self._table.item(0, 3).setBackground(title_background) + self._table.item(0, 3).setToolTip("This column shows the currency") + self._table.item(0, 3).setFont(title_font) + + self._table.setItem(1, 3, SpreadSheetItem("NOK")) + self._table.setItem(2, 3, SpreadSheetItem("NOK")) + self._table.setItem(3, 3, SpreadSheetItem("EUR")) + self._table.setItem(4, 3, SpreadSheetItem("EUR")) + self._table.setItem(5, 3, SpreadSheetItem("USD")) + self._table.setItem(6, 3, SpreadSheetItem("USD")) + self._table.setItem(7, 3, SpreadSheetItem("USD")) + self._table.setItem(8, 3, SpreadSheetItem("USD")) + + self._table.setItem(9, 3, SpreadSheetItem()) + self._table.item(9, 3).setBackground(Qt.lightGray) + + # column 4 + self._table.setItem(0, 4, SpreadSheetItem("Ex. Rate")) + self._table.item(0, 4).setBackground(title_background) + self._table.item(0, 4).setToolTip("This column shows the exchange rate to NOK") + self._table.item(0, 4).setFont(title_font) + + self._table.setItem(1, 4, SpreadSheetItem("1")) + self._table.setItem(2, 4, SpreadSheetItem("1")) + self._table.setItem(3, 4, SpreadSheetItem("8")) + self._table.setItem(4, 4, SpreadSheetItem("8")) + self._table.setItem(5, 4, SpreadSheetItem("7")) + self._table.setItem(6, 4, SpreadSheetItem("7")) + self._table.setItem(7, 4, SpreadSheetItem("7")) + self._table.setItem(8, 4, SpreadSheetItem("7")) + + self._table.setItem(9, 4, SpreadSheetItem()) + self._table.item(9, 4).setBackground(title_background) + + # column 5 + self._table.setItem(0, 5, SpreadSheetItem("NOK")) + self._table.item(0, 5).setBackground(title_background) + self._table.item(0, 5).setToolTip("This column shows the expenses in NOK") + self._table.item(0, 5).setFont(title_font) + + self._table.setItem(1, 5, SpreadSheetItem("* C2 E2")) + self._table.setItem(2, 5, SpreadSheetItem("* C3 E3")) + self._table.setItem(3, 5, SpreadSheetItem("* C4 E4")) + self._table.setItem(4, 5, SpreadSheetItem("* C5 E5")) + self._table.setItem(5, 5, SpreadSheetItem("* C6 E6")) + self._table.setItem(6, 5, SpreadSheetItem("* C7 E7")) + self._table.setItem(7, 5, SpreadSheetItem("* C8 E8")) + self._table.setItem(8, 5, SpreadSheetItem("* C9 E9")) + + self._table.setItem(9, 5, SpreadSheetItem("sum F2 F9")) + self._table.item(9, 5).setBackground(title_background) + + @Slot() + def show_about(self) -> None: + html_text = ( + "<HTML>" + "<p><b>This demo shows use of <c>QTableWidget</c> with custom handling for" + " individual cells.</b></p>" + "<p>Using a customized table item we make it possible to have dynamic" + " output in different cells. The content that is implemented for this" + " particular demo is:" + "<ul>" + "<li>Adding two cells.</li>" + "<li>Subtracting one cell from another.</li>" + "<li>Multiplying two cells.</li>" + "<li>Dividing one cell with another.</li>" + "<li>Summing the contents of an arbitrary number of cells.</li>" + "</HTML>") + QMessageBox.about(self, "About Spreadsheet", html_text) diff --git a/examples/widgets/itemviews/spreadsheet/spreadsheetdelegate.py b/examples/widgets/itemviews/spreadsheet/spreadsheetdelegate.py new file mode 100644 index 000000000..57aba6f47 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/spreadsheetdelegate.py @@ -0,0 +1,67 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import (QAbstractItemModel, QDate, QModelIndex, QObject, + QStringListModel, Qt, Slot) +from PySide6.QtWidgets import (QCompleter, QDateTimeEdit, QLineEdit, + QStyleOptionViewItem, QStyledItemDelegate, QWidget) + +from typing import Optional + + +class SpreadSheetDelegate(QStyledItemDelegate): + def __init__(self, parent: Optional[QObject] = None) -> None: + super().__init__(parent) + + def create_editor(self, parent: QWidget, + option: QStyleOptionViewItem, + index: QModelIndex) -> QWidget: + if index.column() == 1: + editor = QDateTimeEdit(parent) + editor.setDisplayFormat("dd/M/yyyy") + editor.setCalendarPopup(True) + return editor + + editor = QLineEdit(parent) + + # create a completer with the strings in the column as model + allStrings = QStringListModel() + for i in range(1, index.model().rowCount()): + strItem = str(index.model().data(index.sibling(i, index.column()), Qt.EditRole)) + + if not allStrings.contains(strItem): + allStrings.append(strItem) + + autoComplete = QCompleter(allStrings) + editor.setCompleter(autoComplete) + editor.editingFinished.connect(SpreadSheetDelegate.commit_and_close_editor) + return editor + + @Slot() + def commit_and_close_editor(self) -> None: + editor = self.sender() + self.commitData.emit(editor) + self.closeEditor.emit(editor) + + def set_editor_data(self, editor: QWidget, index: QModelIndex) -> None: + edit = QLineEdit(editor) + if edit: + edit.setText(str(index.model().data(index, Qt.EditRole))) + return + + dateEditor = QDateTimeEdit(editor) + if dateEditor: + dateEditor.setDate( + QDate.fromString( + str(index.model().data(index, Qt.EditRole)), "d/M/yyyy")) + + def set_model_data(self, editor: QWidget, + model: QAbstractItemModel, index: QModelIndex) -> None: + edit = QLineEdit(editor) + if edit: + model.setData(index, edit.text()) + return + + dateEditor = QDateTimeEdit(editor) + if dateEditor: + model.setData(index, dateEditor.date().toString("dd/M/yyyy")) diff --git a/examples/widgets/itemviews/spreadsheet/spreadsheetitem.py b/examples/widgets/itemviews/spreadsheet/spreadsheetitem.py new file mode 100644 index 000000000..dc70da883 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/spreadsheetitem.py @@ -0,0 +1,122 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from typing import Any, Tuple +from PySide6.QtCore import QMetaType, Qt +from PySide6.QtWidgets import QTableWidget, QTableWidgetItem + + +class SpreadSheetItem(QTableWidgetItem): + is_resolving = False + + def __init_subclass__(cls) -> None: + return super().__init_subclass__() + + def data(self, role: int) -> Any: + if role == Qt.EditRole or role == Qt.StatusTipRole: + return self.formula() + + if role == Qt.DisplayRole: + return self.display() + + t = str(self.display()) + + if role == Qt.ForegroundRole: + try: + number = int(t) + color = Qt.red if number < 0 else Qt.blue + except ValueError: + color = Qt.black + return color + + if role == Qt.TextAlignmentRole: + if t and (t[0].isdigit() or t[0] == '-'): + return int(Qt.AlignRight | Qt.AlignVCenter) + + return super().data(role) + + def setData(self, role: int, value: Any) -> None: + super().setData(role, value) + if self.tableWidget(): + self.tableWidget().viewport().update() + + def display(self) -> QMetaType.Type.QVariant: + # avoid circular dependencies + if self.is_resolving: + return QMetaType.Type.QVariant + + self.is_resolving = True + result = self.compute_formula(self.formula(), self.tableWidget(), self) + self.is_resolving = False + return result + + def formula(self) -> None: + return str(super().data(Qt.DisplayRole)) + + def compute_formula(self, formula: str, widget: QTableWidget, this) -> QMetaType.Type.QVariant: + # check if the string is actually a formula or not + list_ = formula.split(' ') + if not list_ or not widget: + return formula # it is a normal string + + op = list_[0].lower() if list_[0] else "" + + first_row = -1 + first_col = -1 + second_row = -1 + second_col = -1 + + if len(list_) > 1: + SpreadSheetItem.decode_pos(list_[1]) + + if len(list_) > 2: + SpreadSheetItem.decode_pos(list_[2]) + + start = widget.item(first_row, first_col) + end = widget.item(second_row, second_col) + + first_val = int(start.text()) if start else 0 + second_val = int(end.text()) if start else 0 + + if op == "sum": + sum = 0 + for r in range(first_row, second_row + 1): + for c in range(first_col, second_col + 1): + table_item = widget.item(r, c) + if table_item and table_item != this: + sum += int(table_item.text()) + + result = sum + elif op == "+": + result = first_val + second_val + elif op == "-": + result = first_val - second_val + elif op == "*": + result = first_val * second_val + elif op == "/": + if second_val == 0: + result = "nan" + else: + result = first_val / second_val + elif op == "=": + if start: + result = start.text() + else: + result = formula + + return result + + def decode_pos(pos: str) -> Tuple[int, int]: + if (not pos): + col = -1 + row = -1 + else: + col = ord(pos[0].encode("latin1")) - ord('A') + try: + row = int(pos[1:]) - 1 + except ValueError: + row = -1 + return row, col + + def encode_pos(row: int, col: int) -> str: + return str(chr(col + ord('A'))) + str(row + 1) diff --git a/examples/widgets/itemviews/stardelegate/doc/stardelegate.png b/examples/widgets/itemviews/stardelegate/doc/stardelegate.png Binary files differnew file mode 100644 index 000000000..343416397 --- /dev/null +++ b/examples/widgets/itemviews/stardelegate/doc/stardelegate.png diff --git a/examples/widgets/itemviews/stardelegate/doc/stardelegate.rst b/examples/widgets/itemviews/stardelegate/doc/stardelegate.rst new file mode 100644 index 000000000..fe8a2732a --- /dev/null +++ b/examples/widgets/itemviews/stardelegate/doc/stardelegate.rst @@ -0,0 +1,10 @@ +Star Delegate Example +===================== + +Demonstrates Qt's itemview architecture + +This example demonstrates the Qt model view architecture. + +.. image:: stardelegate.png + :width: 400 + :alt: Star Delegate Screenshot diff --git a/examples/widgets/itemviews/stardelegate/stardelegate.py b/examples/widgets/itemviews/stardelegate/stardelegate.py index 86fd99ce6..973eb14f6 100644 --- a/examples/widgets/itemviews/stardelegate/stardelegate.py +++ b/examples/widgets/itemviews/stardelegate/stardelegate.py @@ -1,58 +1,21 @@ +# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -############################################################################# -## -## 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 Qt for Python 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 QStyledItemDelegate, QStyle +from PySide6.QtWidgets import 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) + super().__init__(parent) def paint(self, painter, option, index): """ Paint the items in the table. @@ -66,7 +29,7 @@ class StarDelegate(QStyledItemDelegate): it works for the purposes of this example. """ if index.column() == 3: - starRating = StarRating(index.data()) + star_rating = StarRating(index.data()) # If the row is currently selected, we need to make sure we # paint the background accordingly. @@ -85,15 +48,15 @@ class StarDelegate(QStyledItemDelegate): # Now that we've painted the background, call starRating.paint() # to paint the stars. - starRating.paint(painter, option.rect, option.palette) + star_rating.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() + star_rating = StarRating(index.data()) + return star_rating.sizeHint() else: return QStyledItemDelegate.sizeHint(self, option, index) @@ -107,7 +70,7 @@ class StarDelegate(QStyledItemDelegate): """ if index.column() == 3: editor = StarEditor(parent) - editor.editingFinished.connect(self.commitAndCloseEditor) + editor.editing_finished.connect(self.commit_and_close_editor) return editor else: return QStyledItemDelegate.createEditor(self, parent, option, index) @@ -115,7 +78,7 @@ class StarDelegate(QStyledItemDelegate): 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()) + editor.star_rating = StarRating(index.data()) else: QStyledItemDelegate.setEditorData(self, editor, index) @@ -123,11 +86,11 @@ class StarDelegate(QStyledItemDelegate): """ Get the data from our custom editor and stuffs it into the model. """ if index.column() == 3: - model.setData(index, editor.starRating.starCount) + model.setData(index, editor.star_rating.star_count) else: QStyledItemDelegate.setModelData(self, editor, model, index) - def commitAndCloseEditor(self): + def commit_and_close_editor(self): """ Erm... commits the data and closes the editor. :) """ editor = self.sender() @@ -139,35 +102,35 @@ class StarDelegate(QStyledItemDelegate): if __name__ == "__main__": """ Run the application. """ - from PySide2.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem, + from PySide6.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] ] + table_widget = QTableWidget(4, 4) + table_widget.setItemDelegate(StarDelegate()) + table_widget.setEditTriggers(QAbstractItemView.DoubleClicked + | QAbstractItemView.SelectedClicked) + table_widget.setSelectionBehavior(QAbstractItemView.SelectRows) + table_widget.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])) + table_widget.setItem(r, 0, QTableWidgetItem(data[r][0])) + table_widget.setItem(r, 1, QTableWidgetItem(data[r][1])) + table_widget.setItem(r, 2, QTableWidgetItem(data[r][2])) item = QTableWidgetItem() - item.setData(0, StarRating(data[r][3]).starCount) - tableWidget.setItem(r, 3, item) + item.setData(0, StarRating(data[r][3]).star_count) + table_widget.setItem(r, 3, item) - tableWidget.resizeColumnsToContents() - tableWidget.resize(500, 300) - tableWidget.show() + table_widget.resizeColumnsToContents() + table_widget.resize(500, 300) + table_widget.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/stardelegate/stareditor.py b/examples/widgets/itemviews/stardelegate/stareditor.py index 820aba8bf..296afa950 100644 --- a/examples/widgets/itemviews/stardelegate/stareditor.py +++ b/examples/widgets/itemviews/stardelegate/stareditor.py @@ -1,100 +1,62 @@ +# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -############################################################################# -## -## 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 Qt for Python 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 +from PySide6.QtWidgets import (QWidget) +from PySide6.QtGui import (QPainter) +from PySide6.QtCore import Signal from starrating import StarRating + class StarEditor(QWidget): """ The custom editor for editing StarRatings. """ # A signal to tell the delegate when we've finished editing. - editingFinished = Signal() + editing_finished = Signal() def __init__(self, parent=None): """ Initialize the editor object, making sure we can watch mouse events. """ - super(StarEditor, self).__init__(parent) + super().__init__(parent) self.setMouseTracking(True) self.setAutoFillBackground(True) - self.starRating = StarRating() + self.star_rating = StarRating() def sizeHint(self): """ Tell the caller how big we are. """ - return self.starRating.sizeHint() + return self.star_rating.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) + with QPainter(self) as painter: + self.star_rating.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()) + star = self.star_at_position(event.x()) - if (star != self.starRating.starCount) and (star != -1): - self.starRating.starCount = star + if (star != self.star_rating.star_count) and (star != -1): + self.star_rating.star_count = 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() + self.editing_finished.emit() - def starAtPosition(self, x): + def star_at_position(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): + star = (x / (self.star_rating.sizeHint().width() / self.star_rating.MAX_STAR_COUNT)) + 1 + if (star <= 0) or (star > self.star_rating.MAX_STAR_COUNT): return -1 return star diff --git a/examples/widgets/itemviews/stardelegate/starrating.py b/examples/widgets/itemviews/stardelegate/starrating.py index d40b382f4..28dbacd6f 100644 --- a/examples/widgets/itemviews/stardelegate/starrating.py +++ b/examples/widgets/itemviews/stardelegate/starrating.py @@ -1,50 +1,12 @@ - -############################################################################# -## -## 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 Qt for Python 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$ -## -############################################################################# +# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net> +# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from math import (cos, sin, pi) -from PySide2.QtGui import (QPainter, QPolygonF) -from PySide2.QtCore import (QPointF, QSize, Qt) +from PySide6.QtGui import (QPainter, QPolygonF) +from PySide6.QtCore import (QPointF, QSize, Qt) PAINTING_SCALE_FACTOR = 20 @@ -53,26 +15,26 @@ class StarRating(object): """ Handle the actual painting of the stars themselves. """ def __init__(self, starCount=1, maxStarCount=5): - self.starCount = starCount - self.maxStarCount = maxStarCount + self.star_count = starCount + self.MAX_STAR_COUNT = maxStarCount # Create the star shape we'll be drawing. - self.starPolygon = QPolygonF() - self.starPolygon.append(QPointF(1.0, 0.5)) + self._star_polygon = QPolygonF() + self._star_polygon.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))) + self._star_polygon.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)] - self.diamondPolygon.append(diamondPoints) + self._diamond_polygon = QPolygonF() + diamond_points = [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)] + self._diamond_polygon.append(diamond_points) def sizeHint(self): """ Tell the caller how big we are. """ - return PAINTING_SCALE_FACTOR * QSize(self.maxStarCount, 1) + return PAINTING_SCALE_FACTOR * QSize(self.MAX_STAR_COUNT, 1) def paint(self, painter, rect, palette, isEditable=False): """ Paint the stars (and/or diamonds if we're in editing mode). """ @@ -86,15 +48,15 @@ class StarRating(object): else: painter.setBrush(palette.windowText()) - yOffset = (rect.height() - PAINTING_SCALE_FACTOR) / 2 - painter.translate(rect.x(), rect.y() + yOffset) + y_offset = (rect.height() - PAINTING_SCALE_FACTOR) / 2 + painter.translate(rect.x(), rect.y() + y_offset) painter.scale(PAINTING_SCALE_FACTOR, PAINTING_SCALE_FACTOR) - for i in range(self.maxStarCount): - if i < self.starCount: - painter.drawPolygon(self.starPolygon, Qt.WindingFill) + for i in range(self.MAX_STAR_COUNT): + if i < self.star_count: + painter.drawPolygon(self._star_polygon, Qt.WindingFill) elif isEditable: - painter.drawPolygon(self.diamondPolygon, Qt.WindingFill) + painter.drawPolygon(self._diamond_polygon, Qt.WindingFill) painter.translate(1.0, 0.0) painter.restore() |