1#!/usr/bin/env python
2
3
4#############################################################################
5##
6## Copyright (C) 2013 Riverbank Computing Limited.
7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8## All rights reserved.
9##
10## This file is part of the examples of PyQt.
11##
12## $QT_BEGIN_LICENSE:BSD$
13## You may use this file under the terms of the BSD license as follows:
14##
15## "Redistribution and use in source and binary forms, with or without
16## modification, are permitted provided that the following conditions are
17## met:
18##   * Redistributions of source code must retain the above copyright
19##     notice, this list of conditions and the following disclaimer.
20##   * Redistributions in binary form must reproduce the above copyright
21##     notice, this list of conditions and the following disclaimer in
22##     the documentation and/or other materials provided with the
23##     distribution.
24##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
25##     the names of its contributors may be used to endorse or promote
26##     products derived from this software without specific prior written
27##     permission.
28##
29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
40## $QT_END_LICENSE$
41##
42#############################################################################
43
44
45import pickle
46
47from PyQt5.QtCore import QFile, QIODevice, Qt, QTextStream
48from PyQt5.QtWidgets import (QDialog, QFileDialog, QGridLayout, QHBoxLayout,
49        QLabel, QLineEdit, QMessageBox, QPushButton, QTextEdit, QVBoxLayout,
50        QWidget)
51
52
53class SortedDict(dict):
54    class Iterator(object):
55        def __init__(self, sorted_dict):
56            self._dict = sorted_dict
57            self._keys = sorted(self._dict.keys())
58            self._nr_items = len(self._keys)
59            self._idx = 0
60
61        def __iter__(self):
62            return self
63
64        def next(self):
65            if self._idx >= self._nr_items:
66                raise StopIteration
67
68            key = self._keys[self._idx]
69            value = self._dict[key]
70            self._idx += 1
71
72            return key, value
73
74        __next__ = next
75
76    def __iter__(self):
77        return SortedDict.Iterator(self)
78
79    iterkeys = __iter__
80
81
82class AddressBook(QWidget):
83    NavigationMode, AddingMode, EditingMode = range(3)
84
85    def __init__(self, parent=None):
86        super(AddressBook, self).__init__(parent)
87
88        self.contacts = SortedDict()
89        self.oldName = ''
90        self.oldAddress = ''
91        self.currentMode = self.NavigationMode
92
93        nameLabel = QLabel("Name:")
94        self.nameLine = QLineEdit()
95        self.nameLine.setReadOnly(True)
96
97        addressLabel = QLabel("Address:")
98        self.addressText = QTextEdit()
99        self.addressText.setReadOnly(True)
100
101        self.addButton = QPushButton("&Add")
102        self.addButton.show()
103        self.editButton = QPushButton("&Edit")
104        self.editButton.setEnabled(False)
105        self.removeButton = QPushButton("&Remove")
106        self.removeButton.setEnabled(False)
107        self.findButton = QPushButton("&Find")
108        self.findButton.setEnabled(False)
109        self.submitButton = QPushButton("&Submit")
110        self.submitButton.hide()
111        self.cancelButton = QPushButton("&Cancel")
112        self.cancelButton.hide()
113
114        self.nextButton = QPushButton("&Next")
115        self.nextButton.setEnabled(False)
116        self.previousButton = QPushButton("&Previous")
117        self.previousButton.setEnabled(False)
118
119        self.loadButton = QPushButton("&Load...")
120        self.loadButton.setToolTip("Load contacts from a file")
121        self.saveButton = QPushButton("Sa&ve...")
122        self.saveButton.setToolTip("Save contacts to a file")
123        self.saveButton.setEnabled(False)
124
125        self.exportButton = QPushButton("Ex&port")
126        self.exportButton.setToolTip("Export as vCard")
127        self.exportButton.setEnabled(False)
128
129        self.dialog = FindDialog()
130
131        self.addButton.clicked.connect(self.addContact)
132        self.submitButton.clicked.connect(self.submitContact)
133        self.editButton.clicked.connect(self.editContact)
134        self.removeButton.clicked.connect(self.removeContact)
135        self.findButton.clicked.connect(self.findContact)
136        self.cancelButton.clicked.connect(self.cancel)
137        self.nextButton.clicked.connect(self.next)
138        self.previousButton.clicked.connect(self.previous)
139        self.loadButton.clicked.connect(self.loadFromFile)
140        self.saveButton.clicked.connect(self.saveToFile)
141        self.exportButton.clicked.connect(self.exportAsVCard)
142
143        buttonLayout1 = QVBoxLayout()
144        buttonLayout1.addWidget(self.addButton)
145        buttonLayout1.addWidget(self.editButton)
146        buttonLayout1.addWidget(self.removeButton)
147        buttonLayout1.addWidget(self.findButton)
148        buttonLayout1.addWidget(self.submitButton)
149        buttonLayout1.addWidget(self.cancelButton)
150        buttonLayout1.addWidget(self.loadButton)
151        buttonLayout1.addWidget(self.saveButton)
152        buttonLayout1.addWidget(self.exportButton)
153        buttonLayout1.addStretch()
154
155        buttonLayout2 = QHBoxLayout()
156        buttonLayout2.addWidget(self.previousButton)
157        buttonLayout2.addWidget(self.nextButton)
158
159        mainLayout = QGridLayout()
160        mainLayout.addWidget(nameLabel, 0, 0)
161        mainLayout.addWidget(self.nameLine, 0, 1)
162        mainLayout.addWidget(addressLabel, 1, 0, Qt.AlignTop)
163        mainLayout.addWidget(self.addressText, 1, 1)
164        mainLayout.addLayout(buttonLayout1, 1, 2)
165        mainLayout.addLayout(buttonLayout2, 2, 1)
166
167        self.setLayout(mainLayout)
168        self.setWindowTitle("Simple Address Book")
169
170    def addContact(self):
171        self.oldName = self.nameLine.text()
172        self.oldAddress = self.addressText.toPlainText()
173
174        self.nameLine.clear()
175        self.addressText.clear()
176
177        self.updateInterface(self.AddingMode)
178
179    def editContact(self):
180        self.oldName = self.nameLine.text()
181        self.oldAddress = self.addressText.toPlainText()
182
183        self.updateInterface(self.EditingMode)
184
185    def submitContact(self):
186        name = self.nameLine.text()
187        address = self.addressText.toPlainText()
188
189        if name == "" or address == "":
190            QMessageBox.information(self, "Empty Field",
191                    "Please enter a name and address.")
192            return
193
194        if self.currentMode == self.AddingMode:
195            if name not in self.contacts:
196                self.contacts[name] = address
197                QMessageBox.information(self, "Add Successful",
198                        "\"%s\" has been added to your address book." % name)
199            else:
200                QMessageBox.information(self, "Add Unsuccessful",
201                        "Sorry, \"%s\" is already in your address book." % name)
202                return
203
204        elif self.currentMode == self.EditingMode:
205            if self.oldName != name:
206                if name not in self.contacts:
207                    QMessageBox.information(self, "Edit Successful",
208                            "\"%s\" has been edited in your address book." % self.oldName)
209                    del self.contacts[self.oldName]
210                    self.contacts[name] = address
211                else:
212                    QMessageBox.information(self, "Edit Unsuccessful",
213                            "Sorry, \"%s\" is already in your address book." % name)
214                    return
215            elif self.oldAddress != address:
216                QMessageBox.information(self, "Edit Successful",
217                        "\"%s\" has been edited in your address book." % name)
218                self.contacts[name] = address
219
220        self.updateInterface(self.NavigationMode)
221
222    def cancel(self):
223        self.nameLine.setText(self.oldName)
224        self.addressText.setText(self.oldAddress)
225        self.updateInterface(self.NavigationMode)
226
227    def removeContact(self):
228        name = self.nameLine.text()
229        address = self.addressText.toPlainText()
230
231        if name in self.contacts:
232            button = QMessageBox.question(self, "Confirm Remove",
233                    "Are you sure you want to remove \"%s\"?" % name,
234                    QMessageBox.Yes | QMessageBox.No)
235
236            if button == QMessageBox.Yes:
237                self.previous()
238                del self.contacts[name]
239
240                QMessageBox.information(self, "Remove Successful",
241                        "\"%s\" has been removed from your address book." % name)
242
243        self.updateInterface(self.NavigationMode)
244
245    def next(self):
246        name = self.nameLine.text()
247        it = iter(self.contacts)
248
249        try:
250            while True:
251                this_name, _ = it.next()
252
253                if this_name == name:
254                    next_name, next_address = it.next()
255                    break
256        except StopIteration:
257            next_name, next_address = iter(self.contacts).next()
258
259        self.nameLine.setText(next_name)
260        self.addressText.setText(next_address)
261
262    def previous(self):
263        name = self.nameLine.text()
264
265        prev_name = prev_address = None
266        for this_name, this_address in self.contacts:
267            if this_name == name:
268                break
269
270            prev_name = this_name
271            prev_address = this_address
272        else:
273            self.nameLine.clear()
274            self.addressText.clear()
275            return
276
277        if prev_name is None:
278            for prev_name, prev_address in self.contacts:
279                pass
280
281        self.nameLine.setText(prev_name)
282        self.addressText.setText(prev_address)
283
284    def findContact(self):
285        self.dialog.show()
286
287        if self.dialog.exec_() == QDialog.Accepted:
288            contactName = self.dialog.getFindText()
289
290            if contactName in self.contacts:
291                self.nameLine.setText(contactName)
292                self.addressText.setText(self.contacts[contactName])
293            else:
294                QMessageBox.information(self, "Contact Not Found",
295                        "Sorry, \"%s\" is not in your address book." % contactName)
296                return
297
298        self.updateInterface(self.NavigationMode)
299
300    def updateInterface(self, mode):
301        self.currentMode = mode
302
303        if self.currentMode in (self.AddingMode, self.EditingMode):
304            self.nameLine.setReadOnly(False)
305            self.nameLine.setFocus(Qt.OtherFocusReason)
306            self.addressText.setReadOnly(False)
307
308            self.addButton.setEnabled(False)
309            self.editButton.setEnabled(False)
310            self.removeButton.setEnabled(False)
311
312            self.nextButton.setEnabled(False)
313            self.previousButton.setEnabled(False)
314
315            self.submitButton.show()
316            self.cancelButton.show()
317
318            self.loadButton.setEnabled(False)
319            self.saveButton.setEnabled(False)
320            self.exportButton.setEnabled(False)
321
322        elif self.currentMode == self.NavigationMode:
323            if not self.contacts:
324                self.nameLine.clear()
325                self.addressText.clear()
326
327            self.nameLine.setReadOnly(True)
328            self.addressText.setReadOnly(True)
329            self.addButton.setEnabled(True)
330
331            number = len(self.contacts)
332            self.editButton.setEnabled(number >= 1)
333            self.removeButton.setEnabled(number >= 1)
334            self.findButton.setEnabled(number > 2)
335            self.nextButton.setEnabled(number > 1)
336            self.previousButton.setEnabled(number >1 )
337
338            self.submitButton.hide()
339            self.cancelButton.hide()
340
341            self.exportButton.setEnabled(number >= 1)
342
343            self.loadButton.setEnabled(True)
344            self.saveButton.setEnabled(number >= 1)
345
346    def saveToFile(self):
347        fileName, _ = QFileDialog.getSaveFileName(self, "Save Address Book",
348                '', "Address Book (*.abk);;All Files (*)")
349
350        if not fileName:
351            return
352
353        try:
354            out_file = open(str(fileName), 'wb')
355        except IOError:
356            QMessageBox.information(self, "Unable to open file",
357                    "There was an error opening \"%s\"" % fileName)
358            return
359
360        pickle.dump(self.contacts, out_file)
361        out_file.close()
362
363    def loadFromFile(self):
364        fileName, _ = QFileDialog.getOpenFileName(self, "Open Address Book",
365                '', "Address Book (*.abk);;All Files (*)")
366
367        if not fileName:
368            return
369
370        try:
371            in_file = open(str(fileName), 'rb')
372        except IOError:
373            QMessageBox.information(self, "Unable to open file",
374                    "There was an error opening \"%s\"" % fileName)
375            return
376
377        self.contacts = pickle.load(in_file)
378        in_file.close()
379
380        if len(self.contacts) == 0:
381            QMessageBox.information(self, "No contacts in file",
382                    "The file you are attempting to open contains no "
383                    "contacts.")
384        else:
385            for name, address in self.contacts:
386                self.nameLine.setText(name)
387                self.addressText.setText(address)
388
389        self.updateInterface(self.NavigationMode)
390
391    def exportAsVCard(self):
392        name = str(self.nameLine.text())
393        address = self.addressText.toPlainText()
394
395        nameList = name.split()
396
397        if len(nameList) > 1:
398            firstName = nameList[0]
399            lastName = nameList[-1]
400        else:
401            firstName = name
402            lastName = ''
403
404        fileName, _ = QFileDialog.getSaveFileName(self, "Export Contact", '',
405                "vCard Files (*.vcf);;All Files (*)")
406
407        if not fileName:
408            return
409
410        out_file = QFile(fileName)
411
412        if not out_file.open(QIODevice.WriteOnly):
413            QMessageBox.information(self, "Unable to open file",
414                    out_file.errorString())
415            return
416
417        out_s = QTextStream(out_file)
418
419        out_s << 'BEGIN:VCARD' << '\n'
420        out_s << 'VERSION:2.1' << '\n'
421        out_s << 'N:' << lastName << ';' << firstName << '\n'
422        out_s << 'FN:' << ' '.join(nameList) << '\n'
423
424        address.replace(';', '\\;')
425        address.replace('\n', ';')
426        address.replace(',', ' ')
427
428        out_s << 'ADR;HOME:;' << address << '\n'
429        out_s << 'END:VCARD' << '\n'
430
431        QMessageBox.information(self, "Export Successful",
432                "\"%s\" has been exported as a vCard." % name)
433
434
435class FindDialog(QDialog):
436    def __init__(self, parent=None):
437        super(FindDialog, self).__init__(parent)
438
439        findLabel = QLabel("Enter the name of a contact:")
440        self.lineEdit = QLineEdit()
441
442        self.findButton = QPushButton("&Find")
443        self.findText = ''
444
445        layout = QHBoxLayout()
446        layout.addWidget(findLabel)
447        layout.addWidget(self.lineEdit)
448        layout.addWidget(self.findButton)
449
450        self.setLayout(layout)
451        self.setWindowTitle("Find a Contact")
452
453        self.findButton.clicked.connect(self.findClicked)
454        self.findButton.clicked.connect(self.accept)
455
456    def findClicked(self):
457        text = self.lineEdit.text()
458
459        if not text:
460            QMessageBox.information(self, "Empty Field",
461                    "Please enter a name.")
462            return
463
464        self.findText = text
465        self.lineEdit.clear()
466        self.hide()
467
468    def getFindText(self):
469        return self.findText
470
471
472if __name__ == '__main__':
473    import sys
474
475    from PyQt5.QtWidgets import QApplication
476
477    app = QApplication(sys.argv)
478
479    addressBook = AddressBook()
480    addressBook.show()
481
482    sys.exit(app.exec_())
483