aboutsummaryrefslogtreecommitdiffstats
path: root/examples/widgets/itemviews/address_book
diff options
context:
space:
mode:
Diffstat (limited to 'examples/widgets/itemviews/address_book')
-rw-r--r--examples/widgets/itemviews/address_book/adddialogwidget.py65
-rw-r--r--examples/widgets/itemviews/address_book/address_book.py99
-rw-r--r--examples/widgets/itemviews/address_book/address_book.pyproject4
-rw-r--r--examples/widgets/itemviews/address_book/addresswidget.py215
-rw-r--r--examples/widgets/itemviews/address_book/doc/address_book.pngbin0 -> 4922 bytes
-rw-r--r--examples/widgets/itemviews/address_book/doc/address_book.rst9
-rw-r--r--examples/widgets/itemviews/address_book/newaddresstab.py56
-rw-r--r--examples/widgets/itemviews/address_book/tablemodel.py109
8 files changed, 557 insertions, 0 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
new file mode 100644
index 000000000..40f2d9e63
--- /dev/null
+++ b/examples/widgets/itemviews/address_book/doc/address_book.png
Binary files differ
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/address_book/tablemodel.py b/examples/widgets/itemviews/address_book/tablemodel.py
new file mode 100644
index 000000000..3c1dbd4cc
--- /dev/null
+++ b/examples/widgets/itemviews/address_book/tablemodel.py
@@ -0,0 +1,109 @@
+# 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)
+
+
+class TableModel(QAbstractTableModel):
+
+ def __init__(self, addresses=None, parent=None):
+ super().__init__(parent)
+
+ if addresses is None:
+ self.addresses = []
+ else:
+ self.addresses = addresses
+
+ def rowCount(self, index=QModelIndex()):
+ """ Returns the number of rows the model holds. """
+ return len(self.addresses)
+
+ def columnCount(self, index=QModelIndex()):
+ """ Returns the number of columns the model holds. """
+ return 2
+
+ def data(self, index, role=Qt.DisplayRole):
+ """ Depending on the index and role given, return data. If not
+ returning data, return None (PySide equivalent of QT's
+ "invalid QVariant").
+ """
+ if not index.isValid():
+ return None
+
+ if not 0 <= index.row() < len(self.addresses):
+ return None
+
+ if role == Qt.DisplayRole:
+ name = self.addresses[index.row()]["name"]
+ address = self.addresses[index.row()]["address"]
+
+ if index.column() == 0:
+ return name
+ elif index.column() == 1:
+ return address
+
+ return None
+
+ def headerData(self, section, orientation, role=Qt.DisplayRole):
+ """ Set the headers to be displayed. """
+ if role != Qt.DisplayRole:
+ return None
+
+ if orientation == Qt.Horizontal:
+ if section == 0:
+ return "Name"
+ elif section == 1:
+ return "Address"
+
+ return None
+
+ def insertRows(self, position, rows=1, index=QModelIndex()):
+ """ Insert a row into the model. """
+ self.beginInsertRows(QModelIndex(), position, position + rows - 1)
+
+ for row in range(rows):
+ self.addresses.insert(position + row, {"name": "", "address": ""})
+
+ self.endInsertRows()
+ return True
+
+ def removeRows(self, position, rows=1, index=QModelIndex()):
+ """ Remove a row from the model. """
+ self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
+
+ del self.addresses[position:position + rows]
+
+ self.endRemoveRows()
+ return True
+
+ def setData(self, index, value, role=Qt.EditRole):
+ """ Adjust the data (set it to <value>) depending on the given
+ index and role.
+ """
+ if role != Qt.EditRole:
+ return False
+
+ if index.isValid() and 0 <= index.row() < len(self.addresses):
+ address = self.addresses[index.row()]
+ if index.column() == 0:
+ address["name"] = value
+ elif index.column() == 1:
+ address["address"] = value
+ else:
+ return False
+
+ self.dataChanged.emit(index, index, 0)
+ return True
+
+ return False
+
+ def flags(self, index):
+ """ Set the item flags at the given index. Seems like we're
+ implementing this function just to see how it's done, as we
+ manually adjust each tableView to have NoEditTriggers.
+ """
+ if not index.isValid():
+ return Qt.ItemIsEnabled
+ return Qt.ItemFlags(QAbstractTableModel.flags(self, index)
+ | Qt.ItemIsEditable)