3 from __future__ import with_statement
4 from __future__ import division
11 import util.qt_compat as qt_compat
12 QtCore = qt_compat.QtCore
13 QtGui = qt_compat.import_module("QtGui")
15 from util import qtpie
16 from util import qui_utils
17 from util import misc as misc_utils
19 import backends.null_backend as null_backend
20 import backends.file_backend as file_backend
23 _moduleLogger = logging.getLogger(__name__)
26 _SENTINEL_ICON = QtGui.QIcon()
29 class Dialpad(object):
31 def __init__(self, app, session, errorLog):
33 self._session = session
34 self._errorLog = errorLog
36 self._plus = QtGui.QPushButton("+")
37 self._plus.clicked.connect(lambda: self._on_keypress("+"))
38 self._entry = QtGui.QLineEdit()
40 backAction = QtGui.QAction(None)
41 backAction.setText("Back")
42 backAction.triggered.connect(self._on_backspace)
43 backPieItem = qtpie.QActionPieItem(backAction)
44 clearAction = QtGui.QAction(None)
45 clearAction.setText("Clear")
46 clearAction.triggered.connect(self._on_clear_text)
47 clearPieItem = qtpie.QActionPieItem(clearAction)
49 qtpie.PieFiling.NULL_CENTER,
51 qtpie.PieFiling.NULL_CENTER,
52 qtpie.PieFiling.NULL_CENTER,
54 self._back = qtpie.QPieButton(backPieItem)
55 self._back.set_center(backPieItem)
56 for slice in backSlices:
57 self._back.insertItem(slice)
59 self._entryLayout = QtGui.QHBoxLayout()
60 self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
61 self._entryLayout.addWidget(self._entry, 1000)
62 self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
64 smsIcon = self._app.get_icon("messages.png")
65 self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
66 self._smsButton.clicked.connect(self._on_sms_clicked)
67 self._smsButton.setSizePolicy(QtGui.QSizePolicy(
68 QtGui.QSizePolicy.MinimumExpanding,
69 QtGui.QSizePolicy.MinimumExpanding,
70 QtGui.QSizePolicy.PushButton,
72 callIcon = self._app.get_icon("dialpad.png")
73 self._callButton = QtGui.QPushButton(callIcon, "Call")
74 self._callButton.clicked.connect(self._on_call_clicked)
75 self._callButton.setSizePolicy(QtGui.QSizePolicy(
76 QtGui.QSizePolicy.MinimumExpanding,
77 QtGui.QSizePolicy.MinimumExpanding,
78 QtGui.QSizePolicy.PushButton,
81 self._padLayout = QtGui.QGridLayout()
82 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
83 columns = [0, 1, 2] * 3
95 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
96 self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
97 self._zerothButton = QtGui.QPushButton("0")
98 self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
99 self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
100 QtGui.QSizePolicy.MinimumExpanding,
101 QtGui.QSizePolicy.MinimumExpanding,
102 QtGui.QSizePolicy.PushButton,
104 self._padLayout.addWidget(self._smsButton, 3, 0)
105 self._padLayout.addWidget(self._zerothButton)
106 self._padLayout.addWidget(self._callButton, 3, 2)
108 self._layout = QtGui.QVBoxLayout()
109 self._layout.addLayout(self._entryLayout, 0)
110 self._layout.addLayout(self._padLayout, 1000000)
111 self._widget = QtGui.QWidget()
112 self._widget.setLayout(self._layout)
119 self._smsButton.setEnabled(True)
120 self._callButton.setEnabled(True)
123 self._smsButton.setEnabled(False)
124 self._callButton.setEnabled(False)
126 def get_settings(self):
129 def set_settings(self, settings):
135 def refresh(self, force = True):
138 def _generate_key_button(self, center, letters):
139 button = QtGui.QPushButton("%s\n%s" % (center, letters))
140 button.setSizePolicy(QtGui.QSizePolicy(
141 QtGui.QSizePolicy.MinimumExpanding,
142 QtGui.QSizePolicy.MinimumExpanding,
143 QtGui.QSizePolicy.PushButton,
145 button.clicked.connect(lambda: self._on_keypress(center))
148 @misc_utils.log_exception(_moduleLogger)
149 def _on_keypress(self, key):
150 with qui_utils.notify_error(self._errorLog):
151 self._entry.insert(key)
153 @misc_utils.log_exception(_moduleLogger)
154 def _on_backspace(self, toggled = False):
155 with qui_utils.notify_error(self._errorLog):
156 self._entry.backspace()
158 @misc_utils.log_exception(_moduleLogger)
159 def _on_clear_text(self, toggled = False):
160 with qui_utils.notify_error(self._errorLog):
164 @qt_compat.Slot(bool)
165 @misc_utils.log_exception(_moduleLogger)
166 def _on_sms_clicked(self, checked = False):
167 with qui_utils.notify_error(self._errorLog):
168 number = misc_utils.make_ugly(str(self._entry.text()))
172 title = misc_utils.make_pretty(number)
173 description = misc_utils.make_pretty(number)
174 numbersWithDescriptions = [(number, "")]
175 self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
178 @qt_compat.Slot(bool)
179 @misc_utils.log_exception(_moduleLogger)
180 def _on_call_clicked(self, checked = False):
181 with qui_utils.notify_error(self._errorLog):
182 number = misc_utils.make_ugly(str(self._entry.text()))
186 title = misc_utils.make_pretty(number)
187 description = misc_utils.make_pretty(number)
188 numbersWithDescriptions = [(number, "")]
189 self._session.draft.clear()
190 self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
191 self._session.draft.call()
194 class TimeCategories(object):
203 _NO_ELAPSED = datetime.timedelta(hours=1)
204 _WEEK_ELAPSED = datetime.timedelta(weeks=1)
205 _MONTH_ELAPSED = datetime.timedelta(days=30)
207 def __init__(self, parentItem):
209 QtGui.QStandardItem(description)
210 for (i, description) in zip(
211 xrange(self._MAX_SECTIONS),
212 ["Now", "Today", "Week", "Month", "Past"],
215 for item in self._timeItems:
216 item.setEditable(False)
217 item.setCheckable(False)
219 parentItem.appendRow(row)
221 self._today = datetime.datetime(1900, 1, 1)
223 self.prepare_for_update(self._today)
225 def prepare_for_update(self, newToday):
226 self._today = newToday
227 for item in self._timeItems:
228 item.removeRows(0, item.rowCount())
230 hour = self._today.strftime("%X")
231 day = self._today.strftime("%x")
233 _moduleLogger.exception("Can't format times")
236 self._timeItems[self._NOW_SECTION].setText(hour)
237 self._timeItems[self._TODAY_SECTION].setText(day)
239 def add_row(self, rowDate, row):
240 elapsedTime = self._today - rowDate
241 todayTuple = self._today.timetuple()
242 rowTuple = rowDate.timetuple()
243 if elapsedTime < self._NO_ELAPSED:
244 section = self._NOW_SECTION
245 elif todayTuple[0:3] == rowTuple[0:3]:
246 section = self._TODAY_SECTION
247 elif elapsedTime < self._WEEK_ELAPSED:
248 section = self._WEEK_SECTION
249 elif elapsedTime < self._MONTH_ELAPSED:
250 section = self._MONTH_SECTION
252 section = self._REST_SECTION
253 self._timeItems[section].appendRow(row)
255 def get_item(self, timeIndex, rowIndex, column):
256 timeItem = self._timeItems[timeIndex]
257 item = timeItem.child(rowIndex, column)
261 class History(object):
267 HISTORY_RECEIVED = "Received"
268 HISTORY_MISSED = "Missed"
269 HISTORY_PLACED = "Placed"
272 HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
273 HISTORY_COLUMNS = ["Details", "From"]
274 assert len(HISTORY_COLUMNS) == MAX_IDX
276 def __init__(self, app, session, errorLog):
277 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
279 self._session = session
280 self._session.historyUpdated.connect(self._on_history_updated)
281 self._errorLog = errorLog
283 self._typeSelection = QtGui.QComboBox()
284 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
285 self._typeSelection.setCurrentIndex(
286 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
288 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
289 refreshIcon = qui_utils.get_theme_icon(
290 ("view-refresh", "general_refresh", "gtk-refresh", ),
293 if refreshIcon is not _SENTINEL_ICON:
294 self._refreshButton = QtGui.QPushButton(refreshIcon, "")
296 self._refreshButton = QtGui.QPushButton("Refresh")
297 self._refreshButton.clicked.connect(self._on_refresh_clicked)
298 self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
299 QtGui.QSizePolicy.Minimum,
300 QtGui.QSizePolicy.Minimum,
301 QtGui.QSizePolicy.PushButton,
303 self._managerLayout = QtGui.QHBoxLayout()
304 self._managerLayout.addWidget(self._typeSelection, 1000)
305 self._managerLayout.addWidget(self._refreshButton, 0)
307 self._itemStore = QtGui.QStandardItemModel()
308 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
309 self._categoryManager = TimeCategories(self._itemStore)
311 self._itemView = QtGui.QTreeView()
312 self._itemView.setModel(self._itemStore)
313 self._itemView.setUniformRowHeights(True)
314 self._itemView.setRootIsDecorated(False)
315 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
316 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
317 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
318 self._itemView.setHeaderHidden(True)
319 self._itemView.setItemsExpandable(False)
320 self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
321 self._itemView.activated.connect(self._on_row_activated)
323 self._layout = QtGui.QVBoxLayout()
324 self._layout.addLayout(self._managerLayout)
325 self._layout.addWidget(self._itemView)
326 self._widget = QtGui.QWidget()
327 self._widget.setLayout(self._layout)
329 self._populate_items()
336 self._itemView.setEnabled(True)
339 self._itemView.setEnabled(False)
341 def get_settings(self):
343 "filter": self._selectedFilter,
346 def set_settings(self, settings):
347 selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
348 if selectedFilter in self.HISTORY_ITEM_TYPES:
349 self._selectedFilter = selectedFilter
350 self._typeSelection.setCurrentIndex(
351 self.HISTORY_ITEM_TYPES.index(selectedFilter)
355 self._itemView.clear()
357 def refresh(self, force=True):
358 self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
360 if self._selectedFilter == self.HISTORY_RECEIVED:
361 self._session.update_history(self._session.HISTORY_RECEIVED, force)
362 elif self._selectedFilter == self.HISTORY_MISSED:
363 self._session.update_history(self._session.HISTORY_MISSED, force)
364 elif self._selectedFilter == self.HISTORY_PLACED:
365 self._session.update_history(self._session.HISTORY_PLACED, force)
366 elif self._selectedFilter == self.HISTORY_ALL:
367 self._session.update_history(self._session.HISTORY_ALL, force)
369 assert False, "How did we get here?"
371 if self._app.notifyOnMissed and self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_BACKGROUND:
372 self._app.ledHandler.off()
374 def _populate_items(self):
375 self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
377 history = self._session.get_history()
378 history.sort(key=lambda item: item["time"], reverse=True)
379 for event in history:
380 if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
383 relTime = misc_utils.abbrev_relative_date(event["relTime"])
384 action = event["action"]
385 number = event["number"]
386 prettyNumber = misc_utils.make_pretty(number)
388 if not name or name == number:
389 name = event["location"]
393 detailsItem = QtGui.QStandardItem("%s - %s\n%s" % (relTime, action, prettyNumber))
394 detailsFont = detailsItem.font()
395 detailsFont.setPointSize(max(detailsFont.pointSize() - 7, 5))
396 detailsItem.setFont(detailsFont)
397 nameItem = QtGui.QStandardItem(name)
398 nameFont = nameItem.font()
399 nameFont.setPointSize(nameFont.pointSize() + 4)
400 nameItem.setFont(nameFont)
401 row = detailsItem, nameItem
403 item.setEditable(False)
404 item.setCheckable(False)
405 row[0].setData(event)
406 self._categoryManager.add_row(event["time"], row)
407 self._itemView.expandAll()
410 @misc_utils.log_exception(_moduleLogger)
411 def _on_filter_changed(self, newItem):
412 with qui_utils.notify_error(self._errorLog):
413 self._selectedFilter = str(newItem)
414 self._populate_items()
417 @misc_utils.log_exception(_moduleLogger)
418 def _on_history_updated(self):
419 with qui_utils.notify_error(self._errorLog):
420 self._populate_items()
423 @misc_utils.log_exception(_moduleLogger)
424 def _on_refresh_clicked(self, arg = None):
425 with qui_utils.notify_error(self._errorLog):
426 self.refresh(force=True)
428 @qt_compat.Slot(QtCore.QModelIndex)
429 @misc_utils.log_exception(_moduleLogger)
430 def _on_row_activated(self, index):
431 with qui_utils.notify_error(self._errorLog):
432 timeIndex = index.parent()
433 if not timeIndex.isValid():
435 timeRow = timeIndex.row()
437 detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
438 fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
439 contactDetails = detailsItem.data()
441 title = unicode(fromItem.text())
442 number = str(contactDetails["number"])
443 contactId = number # ids don't seem too unique so using numbers
446 for t in xrange(self._itemStore.rowCount()):
447 randomTimeItem = self._itemStore.item(t, 0)
448 for i in xrange(randomTimeItem.rowCount()):
449 iItem = randomTimeItem.child(i, 0)
450 iContactDetails = iItem.data()
451 iNumber = str(iContactDetails["number"])
452 if number != iNumber:
454 relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
455 action = str(iContactDetails["action"])
456 number = str(iContactDetails["number"])
457 prettyNumber = misc_utils.make_pretty(number)
458 rowItems = relTime, action, prettyNumber
459 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
460 description = "<table>%s</table>" % "".join(descriptionRows)
461 numbersWithDescriptions = [(str(contactDetails["number"]), "")]
462 self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
465 class Messages(object):
468 VOICEMAIL_MESSAGES = "Voicemail"
469 TEXT_MESSAGES = "SMS"
470 ALL_TYPES = "All Messages"
471 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
473 UNREAD_STATUS = "Unread"
474 UNARCHIVED_STATUS = "Inbox"
476 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
478 _MIN_MESSAGES_SHOWN = 1
480 def __init__(self, app, session, errorLog):
481 self._selectedTypeFilter = self.ALL_TYPES
482 self._selectedStatusFilter = self.ALL_STATUS
484 self._session = session
485 self._session.messagesUpdated.connect(self._on_messages_updated)
486 self._errorLog = errorLog
488 self._typeSelection = QtGui.QComboBox()
489 self._typeSelection.addItems(self.MESSAGE_TYPES)
490 self._typeSelection.setCurrentIndex(
491 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
493 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
495 self._statusSelection = QtGui.QComboBox()
496 self._statusSelection.addItems(self.MESSAGE_STATUSES)
497 self._statusSelection.setCurrentIndex(
498 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
500 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
502 refreshIcon = qui_utils.get_theme_icon(
503 ("view-refresh", "general_refresh", "gtk-refresh", ),
506 if refreshIcon is not _SENTINEL_ICON:
507 self._refreshButton = QtGui.QPushButton(refreshIcon, "")
509 self._refreshButton = QtGui.QPushButton("Refresh")
510 self._refreshButton.clicked.connect(self._on_refresh_clicked)
511 self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
512 QtGui.QSizePolicy.Minimum,
513 QtGui.QSizePolicy.Minimum,
514 QtGui.QSizePolicy.PushButton,
517 self._selectionLayout = QtGui.QHBoxLayout()
518 self._selectionLayout.addWidget(self._typeSelection, 1000)
519 self._selectionLayout.addWidget(self._statusSelection, 1000)
520 self._selectionLayout.addWidget(self._refreshButton, 0)
522 self._itemStore = QtGui.QStandardItemModel()
523 self._itemStore.setHorizontalHeaderLabels(["Messages"])
524 self._categoryManager = TimeCategories(self._itemStore)
526 self._htmlDelegate = qui_utils.QHtmlDelegate()
527 self._itemView = QtGui.QTreeView()
528 self._itemView.setModel(self._itemStore)
529 self._itemView.setUniformRowHeights(False)
530 self._itemView.setRootIsDecorated(False)
531 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
532 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
533 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
534 self._itemView.setHeaderHidden(True)
535 self._itemView.setItemsExpandable(False)
536 self._itemView.setItemDelegate(self._htmlDelegate)
537 self._itemView.activated.connect(self._on_row_activated)
538 self._itemView.header().sectionResized.connect(self._on_column_resized)
540 self._layout = QtGui.QVBoxLayout()
541 self._layout.addLayout(self._selectionLayout)
542 self._layout.addWidget(self._itemView)
543 self._widget = QtGui.QWidget()
544 self._widget.setLayout(self._layout)
546 self._populate_items()
553 self._itemView.setEnabled(True)
556 self._itemView.setEnabled(False)
558 def get_settings(self):
560 "type": self._selectedTypeFilter,
561 "status": self._selectedStatusFilter,
564 def set_settings(self, settings):
565 selectedType = settings.get("type", self.ALL_TYPES)
566 if selectedType in self.MESSAGE_TYPES:
567 self._selectedTypeFilter = selectedType
568 self._typeSelection.setCurrentIndex(
569 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
572 selectedStatus = settings.get("status", self.ALL_STATUS)
573 if selectedStatus in self.MESSAGE_STATUSES:
574 self._selectedStatusFilter = selectedStatus
575 self._statusSelection.setCurrentIndex(
576 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
580 self._itemView.clear()
582 def refresh(self, force=True):
583 self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
585 if self._selectedTypeFilter == self.NO_MESSAGES:
587 elif self._selectedTypeFilter == self.TEXT_MESSAGES:
588 self._session.update_messages(self._session.MESSAGE_TEXTS, force)
589 elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES:
590 self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force)
591 elif self._selectedTypeFilter == self.ALL_TYPES:
592 self._session.update_messages(self._session.MESSAGE_ALL, force)
594 assert False, "How did we get here?"
596 if self._app.notifyOnSms or self._app.notifyOnVoicemail and self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_BACKGROUND:
597 self._app.ledHandler.off()
599 def _populate_items(self):
600 self._categoryManager.prepare_for_update(self._session.get_when_messages_updated())
602 rawMessages = self._session.get_messages()
603 rawMessages.sort(key=lambda item: item["time"], reverse=True)
604 for item in rawMessages:
605 isUnarchived = not item["isArchived"]
606 isUnread = not item["isRead"]
608 self.UNREAD_STATUS: isUnarchived and isUnread,
609 self.UNARCHIVED_STATUS: isUnarchived,
610 self.ALL_STATUS: True,
611 }[self._selectedStatusFilter]
612 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
613 if not (visibleType and visibleStatus):
616 relTime = misc_utils.abbrev_relative_date(item["relTime"])
617 number = item["number"]
618 prettyNumber = misc_utils.make_pretty(number)
620 if not name or name == number:
621 name = item["location"]
625 messageParts = list(item["messageParts"])
626 if len(messageParts) == 0:
627 messages = ("No Transcription", )
628 elif len(messageParts) == 1:
629 if messageParts[0][1]:
630 messages = (messageParts[0][1], )
632 messages = ("No Transcription", )
635 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
636 for messagePart in messageParts
639 firstMessage = "<b>%s<br/>%s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
641 expandedMessages = [firstMessage]
642 expandedMessages.extend(messages)
643 if self._MIN_MESSAGES_SHOWN < len(messages):
644 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
645 collapsedMessages = [firstMessage, secondMessage]
646 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
648 collapsedMessages = expandedMessages
650 item = dict(item.iteritems())
651 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
652 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
654 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
655 messageItem.setData(item)
656 messageItem.setEditable(False)
657 messageItem.setCheckable(False)
658 row = (messageItem, )
659 self._categoryManager.add_row(item["time"], row)
660 self._itemView.expandAll()
663 @misc_utils.log_exception(_moduleLogger)
664 def _on_type_filter_changed(self, newItem):
665 with qui_utils.notify_error(self._errorLog):
666 self._selectedTypeFilter = str(newItem)
667 self._populate_items()
670 @misc_utils.log_exception(_moduleLogger)
671 def _on_status_filter_changed(self, newItem):
672 with qui_utils.notify_error(self._errorLog):
673 self._selectedStatusFilter = str(newItem)
674 self._populate_items()
677 @misc_utils.log_exception(_moduleLogger)
678 def _on_refresh_clicked(self, arg = None):
679 with qui_utils.notify_error(self._errorLog):
680 self.refresh(force=True)
683 @misc_utils.log_exception(_moduleLogger)
684 def _on_messages_updated(self):
685 with qui_utils.notify_error(self._errorLog):
686 self._populate_items()
688 @qt_compat.Slot(QtCore.QModelIndex)
689 @misc_utils.log_exception(_moduleLogger)
690 def _on_row_activated(self, index):
691 with qui_utils.notify_error(self._errorLog):
692 timeIndex = index.parent()
693 if not timeIndex.isValid():
695 timeRow = timeIndex.row()
697 item = self._categoryManager.get_item(timeRow, row, 0)
698 contactDetails = item.data()
700 name = unicode(contactDetails["name"])
701 number = str(contactDetails["number"])
702 if not name or name == number:
703 name = unicode(contactDetails["location"])
707 if str(contactDetails["type"]) == "Voicemail":
708 messageId = str(contactDetails["id"])
711 contactId = str(contactDetails["contactId"])
713 description = unicode(contactDetails["expandedMessages"])
714 numbersWithDescriptions = [(number, "")]
715 self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions)
717 @qt_compat.Slot(QtCore.QModelIndex)
718 @misc_utils.log_exception(_moduleLogger)
719 def _on_column_resized(self, index, oldSize, newSize):
720 self._htmlDelegate.setWidth(newSize, self._itemStore)
723 class Contacts(object):
725 # @todo Provide some sort of letter jump
727 def __init__(self, app, session, errorLog):
729 self._session = session
730 self._session.accountUpdated.connect(self._on_contacts_updated)
731 self._errorLog = errorLog
732 self._addressBookFactories = [
733 null_backend.NullAddressBookFactory(),
734 file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
736 self._addressBooks = []
738 self._listSelection = QtGui.QComboBox()
739 self._listSelection.addItems([])
740 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
741 self._activeList = "None"
742 refreshIcon = qui_utils.get_theme_icon(
743 ("view-refresh", "general_refresh", "gtk-refresh", ),
746 if refreshIcon is not _SENTINEL_ICON:
747 self._refreshButton = QtGui.QPushButton(refreshIcon, "")
749 self._refreshButton = QtGui.QPushButton("Refresh")
750 self._refreshButton.clicked.connect(self._on_refresh_clicked)
751 self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
752 QtGui.QSizePolicy.Minimum,
753 QtGui.QSizePolicy.Minimum,
754 QtGui.QSizePolicy.PushButton,
756 self._managerLayout = QtGui.QHBoxLayout()
757 self._managerLayout.addWidget(self._listSelection, 1000)
758 self._managerLayout.addWidget(self._refreshButton, 0)
760 self._itemStore = QtGui.QStandardItemModel()
761 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
764 self._itemView = QtGui.QTreeView()
765 self._itemView.setModel(self._itemStore)
766 self._itemView.setUniformRowHeights(True)
767 self._itemView.setRootIsDecorated(False)
768 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
769 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
770 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
771 self._itemView.setHeaderHidden(True)
772 self._itemView.setItemsExpandable(False)
773 self._itemView.activated.connect(self._on_row_activated)
775 self._layout = QtGui.QVBoxLayout()
776 self._layout.addLayout(self._managerLayout)
777 self._layout.addWidget(self._itemView)
778 self._widget = QtGui.QWidget()
779 self._widget.setLayout(self._layout)
781 self.update_addressbooks()
782 self._populate_items()
789 self._itemView.setEnabled(True)
792 self._itemView.setEnabled(False)
794 def get_settings(self):
796 "selectedAddressbook": self._activeList,
799 def set_settings(self, settings):
800 currentItem = settings.get("selectedAddressbook", "None")
801 bookNames = [book["name"] for book in self._addressBooks]
803 newIndex = bookNames.index(currentItem)
805 # Switch over to None for the user
807 self._listSelection.setCurrentIndex(newIndex)
808 self._activeList = currentItem
811 self._itemView.clear()
813 def refresh(self, force=True):
814 self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
815 self._backend.update_account(force)
819 return self._addressBooks[self._listSelection.currentIndex()]["book"]
821 def update_addressbooks(self):
822 self._addressBooks = [
823 {"book": book, "name": book.name}
824 for factory in self._addressBookFactories
825 for book in factory.get_addressbooks()
827 self._addressBooks.append(
829 "book": self._session,
830 "name": "Google Voice",
834 currentItem = str(self._listSelection.currentText())
835 self._activeList = currentItem
836 if currentItem == "":
839 self._listSelection.clear()
840 bookNames = [book["name"] for book in self._addressBooks]
842 newIndex = bookNames.index(currentItem)
844 # Switch over to None for the user
846 self._itemStore.clear()
847 _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
848 self._listSelection.addItems(bookNames)
849 self._listSelection.setCurrentIndex(newIndex)
851 def _populate_items(self):
852 self._itemStore.clear()
853 self._alphaItem = dict(
854 (letter, QtGui.QStandardItem(letter))
855 for letter in self._prefixes()
857 for letter in self._prefixes():
858 item = self._alphaItem[letter]
859 item.setEditable(False)
860 item.setCheckable(False)
862 self._itemStore.appendRow(row)
864 for item in self._get_contacts():
868 numbers = item["numbers"]
870 nameItem = QtGui.QStandardItem(name)
871 nameItem.setEditable(False)
872 nameItem.setCheckable(False)
873 nameItem.setData(item)
874 nameItemFont = nameItem.font()
875 nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5))
876 nameItem.setFont(nameItemFont)
879 rowKey = name[0].upper()
880 rowKey = rowKey if rowKey in self._alphaItem else "#"
881 self._alphaItem[rowKey].appendRow(row)
882 self._itemView.expandAll()
885 return itertools.chain(string.ascii_uppercase, ("#", ))
887 def _jump_to_prefix(self, letter):
888 i = list(self._prefixes()).index(letter)
889 rootIndex = self._itemView.rootIndex()
890 currentIndex = self._itemView.model().index(i, 0, rootIndex)
891 self._itemView.scrollTo(currentIndex)
892 self._itemView.setItemSelected(self._itemView.topLevelItem(i), True)
894 def _get_contacts(self):
895 contacts = list(self._backend.get_contacts().itervalues())
896 contacts.sort(key=lambda contact: contact["name"].lower())
900 @misc_utils.log_exception(_moduleLogger)
901 def _on_filter_changed(self, newItem):
902 with qui_utils.notify_error(self._errorLog):
903 self._activeList = str(newItem)
904 self.refresh(force=False)
905 self._populate_items()
908 @misc_utils.log_exception(_moduleLogger)
909 def _on_refresh_clicked(self, arg = None):
910 with qui_utils.notify_error(self._errorLog):
911 self.refresh(force=True)
914 @misc_utils.log_exception(_moduleLogger)
915 def _on_contacts_updated(self):
916 with qui_utils.notify_error(self._errorLog):
917 self._populate_items()
919 @qt_compat.Slot(QtCore.QModelIndex)
920 @misc_utils.log_exception(_moduleLogger)
921 def _on_row_activated(self, index):
922 with qui_utils.notify_error(self._errorLog):
923 letterIndex = index.parent()
924 if not letterIndex.isValid():
926 letterRow = letterIndex.row()
927 letter = list(self._prefixes())[letterRow]
928 letterItem = self._alphaItem[letter]
929 rowIndex = index.row()
930 item = letterItem.child(rowIndex, 0)
931 contactDetails = item.data()
933 name = unicode(contactDetails["name"])
935 name = unicode(contactDetails["location"])
939 contactId = str(contactDetails["contactId"])
940 numbers = contactDetails["numbers"]
944 for (k, v) in number.iteritems()
946 for number in numbers
948 numbersWithDescriptions = [
950 number["phoneNumber"],
951 self._choose_phonetype(number),
953 for number in numbers
957 self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
960 def _choose_phonetype(numberDetails):
961 if "phoneTypeName" in numberDetails:
962 return numberDetails["phoneTypeName"]
963 elif "phoneType" in numberDetails:
964 return numberDetails["phoneType"]