Monday, January 30, 2012

Qt 4.8 Address Book Example - Part 2

Part 2 implements the methods and classes needed to initialize the AddressWidget; this includes the full  implementation of the TableModel and NewAddressTab classes.




#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
    PyQt4 conversion of Qt Address Book (Tabbed) Example
    
        The address book example shows how to use proxy models to display 
        different views onto data from a single model.
        
        This example provides an address book that allows contacts to be 
        grouped alphabetically into 9 groups: 
            ABC, DEF, GHI, ... , VW, ..., XYZ. 
        This is achieved by using multiple views on the same model, each of 
        which is filtered using an instance of the QSortFilterProxyModel class.
          
    BEHAVIOUR:
    =========
        Address tabs are visible and can be selected.
        
    NOTES:
    =====
        Implemented initialisation methods in AddressWidget
        which required the implementation of the TableModel
        and NewAddressTab classes.

        The Qt version of TableModel uses a list of QPair
        objects; QPair is not implemented in PyQt4; a 'list
        of lists' in the form:
            [ [str, str], [str,str], etc.]
         is used instead.
    
last modified: 2012-01-29 jg
ref:
    http://developer.qt.nokia.com/doc/qt-4.8/itemviews-addressbook.html
    
'''
from PyQt4.QtGui import (QApplication, QMainWindow, QWidget, QAction, QFileDialog,
                         QLabel, QPushButton, QVBoxLayout, QSortFilterProxyModel,
                         QTableView, QAbstractItemView, QTabWidget, QItemSelection)
from PyQt4.QtCore import (pyqtSlot, pyqtSignal, Qt, QAbstractTableModel, QModelIndex,
                          QRegExp)

class NewAddressTab(QWidget):
    def __init__(self, parent=None):    # initialise base class
        super(NewAddressTab, self).__init__(parent)

        descripLabel = QLabel(self.tr(
                        "There are currently no contacts in your Address Book"
                        "\nClick Add to add new contacts."))
        addBtn = QPushButton(self.tr("Add"))
        addBtn.clicked.connect(self._addEntry)

        layout = QVBoxLayout()
        layout.addWidget(descripLabel)
        layout.addWidget(addBtn)
        self.setLayout(layout)

    def _addEntry(self):
        pass


class TableModel(QAbstractTableModel):
    def __init__(self, parent=None, pairs=[['', '']]):    # initialise base class
        super(TableModel, self).__init__(parent)
        self._listOfPairs = pairs

    # overridden methods ------------------------------------------------------
    def rowCount(self, parent=QModelIndex()):
        return len(self._listOfPairs)

    def columnCount(self, parent=QModelIndex()):
        return 2

    def data(self, index, role):
        if not index.isValid():
            return None

        if (index.row() >= len(self._listOfPairs) or
            index.row() < 0):
            return None

        if role == Qt.DisplayRole:
            row = index.row()
            pair = self._listOfPairs[row]

            if index.column() == 0:
                return pair[0]
            elif index.column == 1:
                return pair[1]

        return None

    def headerData(self, section, orientation, role):
        if role != Qt.DisplayRole:
            return None

        if orientation == Qt.Horizontal:
            if section == 0: return self.tr("Name")
            elif section == 1: return self.tr("Address")
            else: return None

        return None

    def insertRows(self, position, rows, index=QModelIndex()):
        self.beginInsertRows(QModelIndex(), position, position + rows - 1)

        for r in range(rows):
            pair = [" ", " "]
            self._listOfPairs.insert(position, pair)

        self.endInsertRows()
        return True

    def removeRows(self, position, rows, index=QModelIndex()):
        self.beginRemoveRows(QModelIndex(), position, position + rows - 1)

        for r in range(rows):
            # use list slicing to remove rows
            self._listOfPairs = (self._listOfPairs[:position] +
                                 self._listOfPairs[position + rows:])

        self.endRemoveRows()
        return True

    def setData(self, index, value, role=Qt.EditRole):
        if index.isValid() and role == Qt.EditRole:
            row = index.row()
            pair = self._listOfPairs(row)

            if index.column() == 0:
                pair[0] = value
            elif index.column() == 1:
                pair[1] = value
            else:
                return False

            self._listOfPairs[row] = pair
            return True

        return False

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled
        return Qt.ItemFlags(QAbstractTableModel.flags(self, index) |
                            Qt.ItemIsEditable)

    # public methods ----------------------------------------------------------
    def getList(self):
        return self._listOfPairs

class AddressWidget(QTabWidget):
    def __init__(self, parent=None):    # initialise base class
        super(AddressWidget, self).__init__(parent)

        self._table = TableModel(self)
        newAddressTab = NewAddressTab(self)
        self.addTab(newAddressTab, "Address Book")

        self._setUpTabs()

    # signals --------------------------------------------------------------
    selectionChanged = pyqtSignal(QItemSelection, name="selectionChanged")

    # public slots ---------------------------------------------------------
    @pyqtSlot(str, str)
    def addEntry(self, name='', address=''):
        pass

    @pyqtSlot()
    def editEntry(self):
        pass

    @pyqtSlot()
    def removeEntry(self):
        pass

    # public methods ---------------------------------------------------------
    def readFromFile(self, fname):
        pass

    def writeToFile(self, fname):
        pass

    # private methods ---------------------------------------------------------
    def _setUpTabs(self):
        groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"]

        for i in range(len(groups)):
            tabName = groups[i]     # 'str' is a reserved word in Python

            proxyModel = QSortFilterProxyModel(self)
            proxyModel.setSourceModel(self._table)
            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)

            newStr = "^{0}.*".format(tabName)
            proxyModel.setFilterRegExp(QRegExp(newStr, Qt.CaseInsensitive))
            proxyModel.setFilterKeyColumn(0)
            proxyModel.sort(0, Qt.AscendingOrder)

            tableView.selectionModel().selectionChanged.connect(self.selectionChanged)

            self.addTab(tableView, tabName)


class MainWindow(QMainWindow):

    def __init__(self, parent=None):    # initialise base class
        super(MainWindow, self).__init__(parent)

        self._addrWidget = AddressWidget()
        self.setCentralWidget(self._addrWidget)
        self._createFileMenu()      # extracted from createMenus()
        self._createToolsMenu()     # extracted from createMenus()
        self._addrWidget.selectionChanged.connect(self._updateActions)
        self.setWindowTitle(self.tr("Address Book - Part 2"))

    # private methods ---------------------------------------------------------
    def _createFileMenu(self):
        # create actions
        openAct = QAction(self.tr("&Open..."), self)
        saveAct = QAction(self.tr("&Save..."), self)
        exitAct = QAction(self.tr("E&xit"), self)

        # connect signals/slots for event handling
        openAct.triggered.connect(self._openFile)
        saveAct.triggered.connect(self._saveFile)
        exitAct.triggered.connect(QApplication.instance().exit)

        # create menu
        fileMenu = self.menuBar().addMenu(self.tr("&File"))
        fileMenu.addAction(openAct)
        fileMenu.addAction(saveAct)
        fileMenu.addSeparator()
        fileMenu.addAction(exitAct)

    def _createToolsMenu(self):
        # create actions
        addAct = QAction(self.tr("&Add Entry..."), self)
        self._editAct = QAction(self.tr("&EditEntry..."), self)
        self._removeAct = QAction(self.tr("&Remove Entry"), self)

        # connect signals/slots for event handling
        addAct.triggered.connect(self._addrWidget.addEntry)
        self._editAct.triggered.connect(self._addrWidget.editEntry)
        self._removeAct.triggered.connect(self._addrWidget.removeEntry)

        # create menu
        toolMenu = self.menuBar().addMenu(self.tr("&Tools"))
        toolMenu.addAction(addAct)
        toolMenu.addAction(self._editAct)
        toolMenu.addSeparator()
        toolMenu.addAction(self._removeAct)

        # set menu item visibility
        self._editAct.setEnabled(False)
        self._removeAct.setEnabled(False)

    # private slots ---------------------------------------------------------
    @pyqtSlot()
    def _openFile(self):
        fname = QFileDialog.getOpenFileName(self)
        if fname:
            self._addrWidget.readFromFile(fname)

    @pyqtSlot()
    def _saveFile(self):
        fname = QFileDialog.getSaveFileName(self)
        if fname:
            self._addrWidget.writeToFile(fname)

    @pyqtSlot(QItemSelection)
    def _updateActions(self, sel):
        indexes = sel.indexes()

        if indexes:
            self._removeAct.setEnabled(True)
            self._editAct.setEnabled(True)
        else:
            self._removeAct.setEnabled(False)
            self._editAct.setEnabled(False)


# main ========================================================================
def main():
    import sys

    app = QApplication(sys.argv)
    mw = MainWindow()
    mw.show()

    sys.exit(app.exec_())

if __name__ == '__main__':
    main()