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