Wednesday, January 25, 2012

Qt 4.8 Address Book Tutorial - Part 7

This code is based on the Qt 4.8 Address Book Tutorial  Part 7, Additional Features.






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

'''
    PyQt4 conversion of Qt Tutorial 'Address Book' - Part 7
    
    This part covers some additional features that make the address book 
    more convenient for the frequent user.
    
    Although our address book is useful in isolation, it would be better if 
    we could exchange contact data with other applications. The vCard format 
    is a popular file format that can be used for this purpose. Here we extend 
    our address book client to allow contacts to be exported to vCard .vcf files.
    
    NOTES:
    =====
    Used QFile, QIODevice and QTextStream to create the vCard file.
    
last modified: 2012-01-25 jg
ref: 
    http://developer.qt.nokia.com/doc/qt-4.8/tutorials-addressbook.html
    http://developer.qt.nokia.com/doc/qt-4.8/tutorials-addressbook-part7.html
'''
import sys, pickle
from PyQt4.QtGui import (QApplication, QWidget, QLabel, QTextEdit, QLineEdit,
                         QGridLayout, QPushButton, QVBoxLayout, QMessageBox,
                         QHBoxLayout, QDialog, QFileDialog)
from PyQt4.QtCore import (Qt, pyqtSlot, QFile, QIODevice, QTextStream)

class FindDialog(QDialog):
    def __init__(self, parent=None):
        super(FindDialog, self).__init__(parent)

        findLabel = QLabel(self.tr("Enter the name of a contact:"))
        self.lineEdit = QLineEdit()

        findBtn = QPushButton(self.tr("&Find"))
        self.findText = ''

        layout = QHBoxLayout(self)
        layout.addWidget(findLabel)
        layout.addWidget(self.lineEdit)
        layout.addWidget(findBtn)

        self.setLayout(layout)
        self.setWindowTitle(self.tr("Find a Contact"))
        findBtn.clicked.connect(self.findClicked)
        findBtn.clicked.connect(self.accept)

    def findClicked(self):
        text = self.lineEdit.text()
        if text:
            self.findText = text
            self.lineEdit.clear()
            self.hide()
        else:
            QMessageBox.information(
                self,
                self.tr("Empty Field"),
                self.tr("Please enter a name."))

    def getFindText(self):
        return self.findText

class AddressBook(QWidget):

    # application operation 'modes'
    # assigns values such that NavMode = 0, AddMode = 1 and EditMode = 2
    NavMode, AddMode, EditMode = range(3)

    def __init__(self, parent=None):
        super(AddressBook, self).__init__(parent)

        # create a dictionary to hold contacts
        self._contacts = {}

        # create attributes to represent object state
        self._currentMode = ''      # user actions
        self._oldName = ''          # key (name) at mode change
        self._oldAddress = ''       # key value (address) at mode change

        # create input labels and fields
        nameLabel = QLabel(self.tr("Name:"))
        self._nameLine = QLineEdit(self)
        self._nameLine.setReadOnly(True)

        addrLabel = QLabel(self.tr("Address:"))
        self._addrText = QTextEdit(self)
        self._addrText.setReadOnly(True)

        # create and populate layout
        mainLayout = QGridLayout()
        mainLayout.addWidget(nameLabel, 0, 0)
        mainLayout.addWidget(self._nameLine, 0, 1)
        mainLayout.addWidget(addrLabel, 1, 0, Qt.AlignTop)
        mainLayout.addWidget(self._addrText, 1, 1)
        mainLayout.addLayout(self._initButtonPanel(), 1, 2)
        mainLayout.addLayout(self._initNavPanel(), 2, 1)

        # create find dialog
        self._dialog = FindDialog()

        # set this objects layout and window title
        self.setLayout(mainLayout)
        self.setWindowTitle(self.tr("Simple Address Book"))

    def _initNavPanel(self):
        self._nextBtn = QPushButton(self.tr("&Next"))
        self._prevBtn = QPushButton(self.tr("&Previous"))

        self._nextBtn.setEnabled(False)
        self._prevBtn.setEnabled(False)

        self._nextBtn.clicked.connect(self.next_)
        self._prevBtn.clicked.connect(self.previous)

        layout = QHBoxLayout()
        layout.addWidget(self._prevBtn)
        layout.addWidget(self._nextBtn)

        return layout

    def _initButtonPanel(self):
        # create buttons
        self._addBtn = QPushButton(self.tr("&Add"))
        self._submitBtn = QPushButton(self.tr("&Submit"))
        self._cancelBtn = QPushButton(self.tr("&Cancel"))
        self._editBtn = QPushButton(self.tr("&Edit"))
        self._removeBtn = QPushButton(self.tr("&Remove"))
        self._findBtn = QPushButton(self.tr("&Find"))
        self._loadBtn = QPushButton(self.tr("&Load..."))
        self._loadBtn.setToolTip(self.tr("Load contacts from a file."))
        self._saveBtn = QPushButton(self.tr("Sa&ve..."))
        self._saveBtn.setToolTip(self.tr("Save contacts to a file."))
        self._exportBtn = QPushButton(self.tr("E&xport"))

        # set button visibility
        self._addBtn.show()
        self._submitBtn.hide()
        self._cancelBtn.hide()
        self._editBtn.setEnabled(False)
        self._removeBtn.setEnabled(False)
        self._findBtn.setEnabled(False)
        self._saveBtn.setEnabled((False))
        self._exportBtn.setEnabled((False))

        # define signal/slot connections for event handling
        self._addBtn.clicked.connect(self._addContact)
        self._submitBtn.clicked.connect(self._submitContact)
        self._cancelBtn.clicked.connect(self._cancel)
        self._editBtn.clicked.connect(self._editContact)
        self._removeBtn.clicked.connect(self._removeContact)
        self._findBtn.clicked.connect(self.findContact)
        self._loadBtn.clicked.connect(self.loadFromFile)
        self._saveBtn.clicked.connect(self.saveToFile)
        self._exportBtn.clicked.connect(self.exportAsVCard)

        # layout buttons
        layout = QVBoxLayout()
        layout.addWidget(self._addBtn, Qt.AlignTop)
        layout.addWidget(self._submitBtn)
        layout.addWidget(self._cancelBtn)
        layout.addWidget(self._editBtn)
        layout.addWidget(self._removeBtn)
        layout.addWidget(self._findBtn)
        layout.addWidget(self._loadBtn)
        layout.addWidget(self._saveBtn)
        layout.addWidget(self._exportBtn)
        layout.addStretch()             # force additional space to bottom

        return layout

    # private slots -----------------------------------------------------------
    @pyqtSlot()
    def _addContact(self):
        self._oldName = self._nameLine.text()
        self._oldAddress = self._addrText.toPlainText()

        self._nameLine.clear()
        self._addrText.clear()

        self.updateInterface(self.AddMode)

    @pyqtSlot()
    def _submitContact(self):
        name = self._nameLine.text()
        address = self._addrText.toPlainText()

        if not (name or address):
            QMessageBox.information(self,
                                    self.tr("Empty Field"),
                                    self.tr("Please enter a name and address."))
            return
        if self._currentMode == self.AddMode:
            if name not in self._contacts:
                self._contacts[name] = address
                QMessageBox.information(self,
                                        self.tr("Add Successful"),
                 self.tr("{0} has been added to your address book.").format(name))
            else:
                QMessageBox.information(self,
                                         self.tr("Add Unsuccessful"),
                 self.tr("Sorry, {0} is already in your address book.").format(name))

        elif self._currentMode == self.EditMode:
            if self._oldName != name:
                if name not in self._contacts:
                    QMessageBox.information(self,
                                            self.tr("Edit Successful"),
                     self.tr("{0} has been edited in your address book.").format(name))
                    del self._contacts[self._oldName]
                    self._contacts[name] = address
                else:
                    QMessageBox.information(self,
                                             self.tr("Edit Unsuccessful"),
                     self.tr("Sorry, {0} is already in your address book.").format(name))
            elif self._oldAddress != address:
                    QMessageBox.information(self,
                                            self.tr("Edit Successful"),
                     self.tr("{0} has been edited in your address book.").format(name))
                    self._contacts[name] = address

        self.updateInterface(self.NavMode)

    @pyqtSlot()
    def _cancel(self):
        self._nameLine.setText(self._oldName)
        self._nameLine.setReadOnly(True)

        self._addrText.setText(self._oldAddress)
        self.updateInterface(self.NavMode)

    @pyqtSlot()
    def _editContact(self):
        self._oldName = self._nameLine.text()
        self._oldAddress = self._addrText.toPlainText()

        self.updateInterface(self.EditMode)

    @pyqtSlot()
    def _removeContact(self):
        name = self._nameLine.text()

        if name in self._contacts:
            ans = QMessageBox.question(
                        self,
                        self.tr("Confirm Remove"),
                        self.tr("Are you sure you want to remove {0}?").format(name),
                        QMessageBox.Yes | QMessageBox.No,
                        QMessageBox.No)
            if ans == QMessageBox.Yes:
                self.previous()
                del self._contacts[name]
                QMessageBox.information(
                        self,
                        self.tr("Remove successful"),
                        self.tr("{0} has been removed from your address book.").format(name),
                        QMessageBox.Ok)
        self.updateInterface(self.NavMode)

    # public slots ------------------------------------------------------------
    @pyqtSlot()
    def next_(self):
        name = self._nameLine.text()            # current contact's name

        sKeys = sorted(self._contacts.keys())   # sort names
        i = sKeys.index(name) + 1               # get index for next name in sorted list
        if i >= len(sKeys): i = 0               # if we're past the end, go to the beginning
        nextKey = sKeys[i]                      # name at new index is key we want

        self._nameLine.setText(nextKey)
        self._addrText.setText(self._contacts[nextKey])

    @pyqtSlot()
    def previous(self):
        name = self._nameLine.text()            # current contact's name

        sKeys = sorted(self._contacts.keys())   # sort names
        i = sKeys.index(name) - 1               # index for previous name in sorted list
        if i < 0: i = len(sKeys) - 1            # if below the beginning, go to the end
        prevKey = sKeys[i]                      # name at the new index is the key we want

        self._nameLine.setText(prevKey)
        self._addrText.setText(self._contacts[prevKey])

    @pyqtSlot()
    def findContact(self):
        self._dialog.show()

        if self._dialog.exec() == QDialog.Accepted:
            contactName = self._dialog.getFindText()

            if contactName in self._contacts:
                self._nameLine.setText(contactName)
                self._addrText.setText(self._contacts[contactName])
            else:
                QMessageBox.information(
                    self,
                    self.tr("Contact Not Found"),
                    self.tr("{0} is not in your Address Book").format(contactName))
                return

        self.updateInterface(self.NavMode)

    @pyqtSlot()
    def loadFromFile(self):
        fname = QFileDialog.getOpenFileName(
                    self,
                    self.tr("Open Address Book"), "",
                    self.tr("Address Book (*.abk);;All Files (*)"))

        if not fname:
            return

        with open(fname, 'rb') as f:
            self._contacts = pickle.load(f)

        # sort the contacts and display the first name
        sKeys = sorted(self._contacts.keys())
        name = sKeys[0]
        self._nameLine.setText(name)
        self._addrText.setText(self._contacts[name])

        self.updateInterface(self.NavMode)

    @pyqtSlot()
    def saveToFile(self):

        fname = QFileDialog.getSaveFileName(
                    self,
                    self.tr("Save Address Book"), "",
                    self.tr("Address Book (*.abk);;All Files (*)"))

        if not fname:
            return

        with open(fname, 'wb') as f:
            pickle.dump(self._contacts, f, pickle.HIGHEST_PROTOCOL)

        self.updateInterface(self.NavMode)

    @pyqtSlot()
    def exportAsVCard(self):
        name = self._nameLine.text()
        address = self._addrText.toPlainText()

        nameList = name.split()
        firstName = ''
        lastName = ''
        if len(nameList) > 0:
            firstName = nameList[0]
        if len(nameList) >= 2:
            lastName = nameList[-1]

        fileName = QFileDialog.getSaveFileName(
                        self,
                        self.tr("Export Contacts"),
                        '',
                        self.tr("vCard Files (*.vcf);;All Files(*)"))

        if not fileName:
            return

        file = QFile(fileName)
        if not file.open(QIODevice.WriteOnly):
            QMessageBox.information(
                self,
                self.tr("Unable to open file"),
                file.errorString())
            return

        # write the file
        outFile = QTextStream(file)
        outFile << "BEGIN:VCARD" << "\n"
        outFile << "VERSION:2.1" << "\n"
        outFile << "N:" << lastName << ";" << firstName << "\n"

        if lastName:
            outFile << "FN:" << " ".join(nameList) << "\n"
        else:
            outFile << "FN:" << firstName << "\n"

        address.replace(";", "\\")
        address.replace("\n", ";")
        address.replace(",", " ")

        outFile << "ADR;HOME:;" << address << "\n"
        outFile << "END:VCARD" << "\n"

        QMessageBox.information(
            self,
            self.tr("Export Successful"),
            self.tr("{0} has been exported to a vCard").format(name))

        self.updateInterface(self.NavMode)

    # public methods ---------------------------------------------------------
    def updateInterface(self, mode):
        '''
            Enable/disable buttons based on whether the user is adding,
            editing or navigating Address Book entries.
        '''
        self._currentMode = mode

        if mode != self.NavMode:
            self._nameLine.setReadOnly(False)
            self._nameLine.setFocus(Qt.OtherFocusReason)
            self._addrText.setReadOnly(False)

            self._addBtn.hide()
            self._editBtn.hide()
            self._removeBtn.hide()
            self._findBtn.hide()
            self._loadBtn.hide()
            self._saveBtn.hide()
            self._exportBtn.hide()

            self._submitBtn.show()
            self._cancelBtn.show()

            self._nextBtn.setEnabled(False)
            self._prevBtn.setEnabled(False)

        else:
            num = len(self._contacts)

            if num == 0:
                self._nameLine.clear()
                self._addrText.clear()

            self._nameLine.setReadOnly(True)
            self._addrText.setReadOnly(True)
            self._addBtn.setEnabled(True)

            self._editBtn.setEnabled(num >= 1)
            self._removeBtn.setEnabled(num >= 1)
            self._findBtn.setEnabled(num > 2)
            self._nextBtn.setEnabled(num > 1)
            self._prevBtn.setEnabled(num > 1)
            self._loadBtn.setEnabled(True)
            self._saveBtn.setEnabled(num >= 1)
            self._exportBtn.setEnabled(num >= 1)

            self._addBtn.show()
            self._editBtn.show()
            self._removeBtn.show()
            self._findBtn.show()
            self._loadBtn.show()
            self._saveBtn.show()
            self._exportBtn.show()

            self._submitBtn.hide()
            self._cancelBtn.hide()

def main():
    app = QApplication(sys.argv)    # required for all GUI applications

    ab = AddressBook()
    ab.show()                       # make me visible

    sys.exit(app.exec_())           # start main event thread

if __name__ == '__main__':
    main()