X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgv_views.py;h=ab6eb6173d9d2417b1d188d98dbc4192b5fa6980;hb=35398cbc6f8edecb35115faf0f496617f963676e;hp=42268daf4664fbb70a27395ef0fed71812522d8c;hpb=d01834355cda6b6b610804dc4f39ff0dcaf2d8a2;p=gc-dialer diff --git a/src/gv_views.py b/src/gv_views.py index 42268da..08de9b7 100644 --- a/src/gv_views.py +++ b/src/gv_views.py @@ -1,1288 +1,930 @@ -#!/usr/bin/python2.5 - -""" -DialCentral - Front end for Google's Grand Central service. -Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -""" +#!/usr/bin/env python from __future__ import with_statement +from __future__ import division -import ConfigParser -import warnings - -import gobject -import pango -import gtk - -import gtk_toolbox -import hildonize -import null_backend - - -def make_ugly(prettynumber): - """ - function to take a phone number and strip out all non-numeric - characters - - >>> make_ugly("+012-(345)-678-90") - '01234567890' - """ - import re - uglynumber = re.sub('\D', '', prettynumber) - return uglynumber - - -def make_pretty(phonenumber): - """ - Function to take a phone number and return the pretty version - pretty numbers: - if phonenumber begins with 0: - ...-(...)-...-.... - if phonenumber begins with 1: ( for gizmo callback numbers ) - 1 (...)-...-.... - if phonenumber is 13 digits: - (...)-...-.... - if phonenumber is 10 digits: - ...-.... - >>> make_pretty("12") - '12' - >>> make_pretty("1234567") - '123-4567' - >>> make_pretty("2345678901") - '(234)-567-8901' - >>> make_pretty("12345678901") - '1 (234)-567-8901' - >>> make_pretty("01234567890") - '+012-(345)-678-90' - """ - if phonenumber is None or phonenumber is "": - return "" - - phonenumber = make_ugly(phonenumber) - - if len(phonenumber) < 3: - return phonenumber - - if phonenumber[0] == "0": - prettynumber = "" - prettynumber += "+%s" % phonenumber[0:3] - if 3 < len(phonenumber): - prettynumber += "-(%s)" % phonenumber[3:6] - if 6 < len(phonenumber): - prettynumber += "-%s" % phonenumber[6:9] - if 9 < len(phonenumber): - prettynumber += "-%s" % phonenumber[9:] - return prettynumber - elif len(phonenumber) <= 7: - prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:]) - elif len(phonenumber) > 8 and phonenumber[0] == "1": - prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) - elif len(phonenumber) > 7: - prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:]) - return prettynumber - - -class MergedAddressBook(object): - """ - Merger of all addressbooks - """ - - def __init__(self, addressbookFactories, sorter = None): - self.__addressbookFactories = addressbookFactories - self.__addressbooks = None - self.__sort_contacts = sorter if sorter is not None else self.null_sorter - - def clear_caches(self): - self.__addressbooks = None - for factory in self.__addressbookFactories: - factory.clear_caches() - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "" - - def open_addressbook(self, bookId): - return self - - def contact_source_short_name(self, contactId): - if self.__addressbooks is None: - return "" - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId) +import datetime +import string +import itertools +import logging - @staticmethod - def factory_name(): - return "All Contacts" - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - if self.__addressbooks is None: - self.__addressbooks = list( - factory.open_addressbook(id) - for factory in self.__addressbookFactories - for (f, id, name) in factory.get_addressbooks() - ) - contacts = ( - ("-".join([str(bookIndex), contactId]), contactName) - for (bookIndex, addressbook) in enumerate(self.__addressbooks) - for (contactId, contactName) in addressbook.get_contacts() - ) - sortedContacts = self.__sort_contacts(contacts) - return sortedContacts - - def get_contact_details(self, contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - if self.__addressbooks is None: - return [] - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].get_contact_details(originalId) - - @staticmethod - def null_sorter(contacts): - """ - Good for speed/low memory - """ - return contacts +from PyQt4 import QtGui +from PyQt4 import QtCore - @staticmethod - def basic_firtname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) +from util import qtpie +from util import qui_utils +from util import misc as misc_utils - @staticmethod - def basic_lastname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) +import backends.null_backend as null_backend +import backends.file_backend as file_backend - @staticmethod - def reversed_firtname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - @staticmethod - def reversed_lastname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) +_moduleLogger = logging.getLogger(__name__) - @staticmethod - def guess_firstname(name): - if ", " in name: - return name.split(", ", 1)[-1] - else: - return name.rsplit(" ", 1)[0] - @staticmethod - def guess_lastname(name): - if ", " in name: - return name.split(", ", 1)[0] - else: - return name.rsplit(" ", 1)[-1] +class Dialpad(object): - @classmethod - def advanced_firstname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_firstname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts + def __init__(self, app, session, errorLog): + self._app = app + self._session = session + self._errorLog = errorLog + + self._plus = QtGui.QPushButton("+") + self._plus.clicked.connect(lambda: self._on_keypress("+")) + self._entry = QtGui.QLineEdit() + + backAction = QtGui.QAction(None) + backAction.setText("Back") + backAction.triggered.connect(self._on_backspace) + backPieItem = qtpie.QActionPieItem(backAction) + clearAction = QtGui.QAction(None) + clearAction.setText("Clear") + clearAction.triggered.connect(self._on_clear_text) + clearPieItem = qtpie.QActionPieItem(clearAction) + backSlices = [ + qtpie.PieFiling.NULL_CENTER, + clearPieItem, + qtpie.PieFiling.NULL_CENTER, + qtpie.PieFiling.NULL_CENTER, ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @classmethod - def advanced_lastname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_lastname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts + self._back = qtpie.QPieButton(backPieItem) + self._back.set_center(backPieItem) + for slice in backSlices: + self._back.insertItem(slice) + + self._entryLayout = QtGui.QHBoxLayout() + self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter) + self._entryLayout.addWidget(self._entry, 1000) + self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter) + + smsIcon = self._app.get_icon("messages.png") + self._smsButton = QtGui.QPushButton(smsIcon, "SMS") + self._smsButton.clicked.connect(self._on_sms_clicked) + self._smsButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.PushButton, + )) + callIcon = self._app.get_icon("dialpad.png") + self._callButton = QtGui.QPushButton(callIcon, "Call") + self._callButton.clicked.connect(self._on_call_clicked) + self._callButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.PushButton, + )) + + self._padLayout = QtGui.QGridLayout() + rows = [0, 0, 0, 1, 1, 1, 2, 2, 2] + columns = [0, 1, 2] * 3 + keys = [ + ("1", ""), + ("2", "ABC"), + ("3", "DEF"), + ("4", "GHI"), + ("5", "JKL"), + ("6", "MNO"), + ("7", "PQRS"), + ("8", "TUV"), + ("9", "WXYZ"), ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - -class PhoneTypeSelector(object): - - ACTION_CANCEL = "cancel" - ACTION_SELECT = "select" - ACTION_DIAL = "dial" - ACTION_SEND_SMS = "sms" - - def __init__(self, widgetTree, gcBackend): - self._gcBackend = gcBackend - self._widgetTree = widgetTree - - self._dialog = self._widgetTree.get_widget("phonetype_dialog") - self._smsDialog = SmsEntryDialog(self._widgetTree) - - self._smsButton = self._widgetTree.get_widget("sms_button") - self._smsButton.connect("clicked", self._on_phonetype_send_sms) - - self._dialButton = self._widgetTree.get_widget("dial_button") - self._dialButton.connect("clicked", self._on_phonetype_dial) - - self._selectButton = self._widgetTree.get_widget("select_button") - self._selectButton.connect("clicked", self._on_phonetype_select) - - self._cancelButton = self._widgetTree.get_widget("cancel_button") - self._cancelButton.connect("clicked", self._on_phonetype_cancel) - - self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self._typeviewselection = None - - self._message = self._widgetTree.get_widget("phoneSelectionMessage") - self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport") - self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow") - self._typeview = self._widgetTree.get_widget("phonetypes") - self._typeview.connect("row-activated", self._on_phonetype_select) - - self._action = self.ACTION_CANCEL - - def run(self, contactDetails, message = "", parent = None): - self._action = self.ACTION_CANCEL - self._typemodel.clear() - self._typeview.set_model(self._typemodel) - - # Add the column to the treeview - textrenderer = gtk.CellRendererText() - numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0) - self._typeview.append_column(numberColumn) - - textrenderer = gtk.CellRendererText() - typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1) - self._typeview.append_column(typeColumn) - - self._typeviewselection = self._typeview.get_selection() - self._typeviewselection.set_mode(gtk.SELECTION_SINGLE) - - for phoneType, phoneNumber in contactDetails: - display = " - ".join((phoneNumber, phoneType)) - display = phoneType - row = (phoneNumber, display) - self._typemodel.append(row) - - self._typeviewselection.select_iter(self._typemodel.get_iter_first()) - if message: - self._message.set_markup(message) - self._message.show() - else: - self._message.set_markup("") - self._message.hide() - - if parent is not None: - self._dialog.set_transient_for(parent) - - try: - self._dialog.show() - adjustment = self._scrollWindow.get_vadjustment() - dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height - dx = max(dx, 0) - adjustment.value = dx - - userResponse = self._dialog.run() - finally: - self._dialog.hide() - - if userResponse == gtk.RESPONSE_OK: - phoneNumber = self._get_number() - phoneNumber = make_ugly(phoneNumber) - else: - phoneNumber = "" - if not phoneNumber: - self._action = self.ACTION_CANCEL - - if self._action == self.ACTION_SEND_SMS: - smsMessage = self._smsDialog.run(phoneNumber, message, parent) - if not smsMessage: - phoneNumber = "" - self._action = self.ACTION_CANCEL - else: - smsMessage = "" - - self._typeviewselection.unselect_all() - self._typeview.remove_column(numberColumn) - self._typeview.remove_column(typeColumn) - self._typeview.set_model(None) - - return self._action, phoneNumber, smsMessage - - def _get_number(self): - model, itr = self._typeviewselection.get_selected() - if not itr: - return "" - - phoneNumber = self._typemodel.get_value(itr, 0) - return phoneNumber - - def _on_phonetype_dial(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_DIAL - - def _on_phonetype_send_sms(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_SEND_SMS - - def _on_phonetype_select(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_SELECT - - def _on_phonetype_cancel(self, *args): - self._dialog.response(gtk.RESPONSE_CANCEL) - self._action = self.ACTION_CANCEL - - -class SmsEntryDialog(object): - - """ - @todo Add multi-SMS messages like GoogleVoice - """ - - MAX_CHAR = 160 - - def __init__(self, widgetTree): - self._widgetTree = widgetTree - self._dialog = self._widgetTree.get_widget("smsDialog") - - self._smsButton = self._widgetTree.get_widget("sendSmsButton") - self._smsButton.connect("clicked", self._on_send) - - self._cancelButton = self._widgetTree.get_widget("cancelSmsButton") - self._cancelButton.connect("clicked", self._on_cancel) - - self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount") - self._message = self._widgetTree.get_widget("smsMessage") - self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport") - self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow") - self._smsEntry = self._widgetTree.get_widget("smsEntry") - self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) - - def run(self, number, message = "", parent = None): - if message: - self._message.set_markup(message) - self._message.show() - else: - self._message.set_markup("") - self._message.hide() - self._smsEntry.get_buffer().set_text("") - self._update_letter_count() - - if parent is not None: - self._dialog.set_transient_for(parent) - - try: - self._dialog.show() - adjustment = self._scrollWindow.get_vadjustment() - dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height - dx = max(dx, 0) - adjustment.value = dx - - userResponse = self._dialog.run() - finally: - self._dialog.hide() - - if userResponse == gtk.RESPONSE_OK: - entryBuffer = self._smsEntry.get_buffer() - enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) - enteredMessage = enteredMessage[0:self.MAX_CHAR] - else: - enteredMessage = "" - - return enteredMessage.strip() - - def _update_letter_count(self, *args): - entryLength = self._smsEntry.get_buffer().get_char_count() - charsLeft = self.MAX_CHAR - entryLength - self._letterCountLabel.set_text(str(charsLeft)) - if charsLeft < 0: - self._smsButton.set_sensitive(False) - else: - self._smsButton.set_sensitive(True) - - def _on_entry_changed(self, *args): - self._update_letter_count() - - def _on_send(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - - def _on_cancel(self, *args): - self._dialog.response(gtk.RESPONSE_CANCEL) - - -class Dialpad(object): - - def __init__(self, widgetTree, errorDisplay): - self._errorDisplay = errorDisplay - self._smsDialog = SmsEntryDialog(widgetTree) - - self._numberdisplay = widgetTree.get_widget("numberdisplay") - self._dialButton = widgetTree.get_widget("dial") - self._backButton = widgetTree.get_widget("back") - self._phonenumber = "" - self._prettynumber = "" - - callbackMapping = { - "on_dial_clicked": self._on_dial_clicked, - "on_sms_clicked": self._on_sms_clicked, - "on_digit_clicked": self._on_digit_clicked, - "on_clear_number": self._on_clear_number, - } - widgetTree.signal_autoconnect(callbackMapping) - - self._originalLabel = self._backButton.get_label() - self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton) - self._backTapHandler.on_tap = self._on_backspace - self._backTapHandler.on_hold = self._on_clearall - self._backTapHandler.on_holding = self._set_clear_button - self._backTapHandler.on_cancel = self._reset_back_button - - self._window = gtk_toolbox.find_parent_window(self._numberdisplay) + for (num, letters), (row, column) in zip(keys, zip(rows, columns)): + self._padLayout.addWidget(self._generate_key_button(num, letters), row, column) + self._zerothButton = QtGui.QPushButton("0") + self._zerothButton.clicked.connect(lambda: self._on_keypress("0")) + self._zerothButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.PushButton, + )) + self._padLayout.addWidget(self._smsButton, 3, 0) + self._padLayout.addWidget(self._zerothButton) + self._padLayout.addWidget(self._callButton, 3, 2) + + self._layout = QtGui.QVBoxLayout() + self._layout.addLayout(self._entryLayout, 0) + self._layout.addLayout(self._padLayout, 1000000) + self._widget = QtGui.QWidget() + self._widget.setLayout(self._layout) + + @property + def toplevel(self): + return self._widget def enable(self): - self._dialButton.grab_focus() - self._backTapHandler.enable() + self._smsButton.setEnabled(True) + self._callButton.setEnabled(True) def disable(self): - self._reset_back_button() - self._backTapHandler.disable() - - def number_selected(self, action, number, message): - """ - @note Actual dial function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def get_number(self): - return self._phonenumber - - def set_number(self, number): - """ - Set the number to dial - """ - try: - self._phonenumber = make_ugly(number) - self._prettynumber = make_pretty(self._phonenumber) - self._numberdisplay.set_label("%s" % (self._prettynumber)) - except TypeError, e: - self._errorDisplay.push_exception() + self._smsButton.setEnabled(False) + self._callButton.setEnabled(False) - def clear(self): - self.set_number("") - - @staticmethod - def name(): - return "Dialpad" + def get_settings(self): + return {} - def load_settings(self, config, section): + def set_settings(self, settings): pass - def save_settings(self, config, section): - """ - @note Thread Agnostic - """ + def clear(self): pass - def _on_sms_clicked(self, widget): - action = PhoneTypeSelector.ACTION_SEND_SMS - phoneNumber = self.get_number() - - message = self._smsDialog.run(phoneNumber, "", self._window) - if not message: - phoneNumber = "" - action = PhoneTypeSelector.ACTION_CANCEL - - if action == PhoneTypeSelector.ACTION_CANCEL: - return - self.number_selected(action, phoneNumber, message) - - def _on_dial_clicked(self, widget): - action = PhoneTypeSelector.ACTION_DIAL - phoneNumber = self.get_number() - message = "" - self.number_selected(action, phoneNumber, message) - - def _on_clear_number(self, *args): - self.clear() - - def _on_digit_clicked(self, widget): - self.set_number(self._phonenumber + widget.get_name()[-1]) - - def _on_backspace(self, taps): - self.set_number(self._phonenumber[:-taps]) - self._reset_back_button() - - def _on_clearall(self, taps): - self.clear() - self._reset_back_button() - return False - - def _set_clear_button(self): - self._backButton.set_label("gtk-clear") - - def _reset_back_button(self): - self._backButton.set_label(self._originalLabel) - - -class AccountInfo(object): - - def __init__(self, widgetTree, backend, alarmHandler, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - self._isPopulated = False - self._alarmHandler = alarmHandler - self._notifyOnMissed = False - self._notifyOnVoicemail = False - self._notifyOnSms = False - - self._callbackList = gtk.ListStore(gobject.TYPE_STRING) - self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") - self._callbackCombo = widgetTree.get_widget("callbackcombo") - self._onCallbackentryChangedId = 0 - - self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox") - self._minutesEntry = widgetTree.get_widget("minutesEntry") - self._missedCheckbox = widgetTree.get_widget("missedCheckbox") - self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox") - self._smsCheckbox = widgetTree.get_widget("smsCheckbox") - self._onNotifyToggled = 0 - self._onMinutesChanged = 0 - self._onMissedToggled = 0 - self._onVoicemailToggled = 0 - self._onSmsToggled = 0 - self._applyAlarmTimeoutId = None - - self._defaultCallback = "" - - def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - - self._accountViewNumberDisplay.set_use_markup(True) - self.set_account_number("") + def refresh(self, force = True): + pass - self._callbackList.clear() - self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed) + def _generate_key_button(self, center, letters): + button = QtGui.QPushButton("%s\n%s" % (center, letters)) + button.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.PushButton, + )) + button.clicked.connect(lambda: self._on_keypress(center)) + return button + + @misc_utils.log_exception(_moduleLogger) + def _on_keypress(self, key): + with qui_utils.notify_error(self._errorLog): + self._entry.insert(key) + + @misc_utils.log_exception(_moduleLogger) + def _on_backspace(self, toggled = False): + with qui_utils.notify_error(self._errorLog): + self._entry.backspace() + + @misc_utils.log_exception(_moduleLogger) + def _on_clear_text(self, toggled = False): + with qui_utils.notify_error(self._errorLog): + self._entry.clear() + + @QtCore.pyqtSlot() + @QtCore.pyqtSlot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_sms_clicked(self, checked = False): + with qui_utils.notify_error(self._errorLog): + number = misc_utils.make_ugly(str(self._entry.text())) + self._entry.clear() + + contactId = number + title = misc_utils.make_pretty(number) + description = misc_utils.make_pretty(number) + numbersWithDescriptions = [(number, "")] + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + + @QtCore.pyqtSlot() + @QtCore.pyqtSlot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_call_clicked(self, checked = False): + with qui_utils.notify_error(self._errorLog): + number = misc_utils.make_ugly(str(self._entry.text())) + self._entry.clear() + + contactId = number + title = misc_utils.make_pretty(number) + description = misc_utils.make_pretty(number) + numbersWithDescriptions = [(number, "")] + self._session.draft.clear() + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + self._session.draft.call() + + +class TimeCategories(object): + + _NOW_SECTION = 0 + _TODAY_SECTION = 1 + _WEEK_SECTION = 2 + _MONTH_SECTION = 3 + _REST_SECTION = 4 + _MAX_SECTIONS = 5 + + _NO_ELAPSED = datetime.timedelta(hours=1) + _WEEK_ELAPSED = datetime.timedelta(weeks=1) + _MONTH_ELAPSED = datetime.timedelta(days=30) + + def __init__(self, parentItem): + self._timeItems = [ + QtGui.QStandardItem(description) + for (i, description) in zip( + xrange(self._MAX_SECTIONS), + ["Now", "Today", "Week", "Month", "Past"], + ) + ] + for item in self._timeItems: + item.setEditable(False) + item.setCheckable(False) + row = (item, ) + parentItem.appendRow(row) - if self._alarmHandler is not None: - self._minutesEntry.set_range(1, 60) + self._today = datetime.datetime(1900, 1, 1) - self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) - self._minutesEntry.set_value(self._alarmHandler.recurrence) - self._missedCheckbox.set_active(self._notifyOnMissed) - self._voicemailCheckbox.set_active(self._notifyOnVoicemail) - self._smsCheckbox.set_active(self._notifyOnSms) + self.prepare_for_update(self._today) - self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled) - self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed) - self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled) - self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled) - self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled) + def prepare_for_update(self, newToday): + self._today = newToday + for item in self._timeItems: + item.removeRows(0, item.rowCount()) + try: + hour = self._today.strftime("%X") + day = self._today.strftime("%x") + except ValueError: + _moduleLogger.exception("Can't format times") + hour = "Now" + day = "Today" + self._timeItems[self._NOW_SECTION].setText(hour) + self._timeItems[self._TODAY_SECTION].setText(day) + + def add_row(self, rowDate, row): + elapsedTime = self._today - rowDate + todayTuple = self._today.timetuple() + rowTuple = rowDate.timetuple() + if elapsedTime < self._NO_ELAPSED: + section = self._NOW_SECTION + elif todayTuple[0:3] == rowTuple[0:3]: + section = self._TODAY_SECTION + elif elapsedTime < self._WEEK_ELAPSED: + section = self._WEEK_SECTION + elif elapsedTime < self._MONTH_ELAPSED: + section = self._MONTH_SECTION else: - self._notifyCheckbox.set_sensitive(False) - self._minutesEntry.set_sensitive(False) - self._missedCheckbox.set_sensitive(False) - self._voicemailCheckbox.set_sensitive(False) - self._smsCheckbox.set_sensitive(False) + section = self._REST_SECTION + self._timeItems[section].appendRow(row) - self.update(force=True) + def get_item(self, timeIndex, rowIndex, column): + timeItem = self._timeItems[timeIndex] + item = timeItem.child(rowIndex, column) + return item - def disable(self): - self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId) - self._onCallbackentryChangedId = 0 - - if self._alarmHandler is not None: - self._notifyCheckbox.disconnect(self._onNotifyToggled) - self._minutesEntry.disconnect(self._onMinutesChanged) - self._missedCheckbox.disconnect(self._onNotifyToggled) - self._voicemailCheckbox.disconnect(self._onNotifyToggled) - self._smsCheckbox.disconnect(self._onNotifyToggled) - self._onNotifyToggled = 0 - self._onMinutesChanged = 0 - self._onMissedToggled = 0 - self._onVoicemailToggled = 0 - self._onSmsToggled = 0 - else: - self._notifyCheckbox.set_sensitive(True) - self._minutesEntry.set_sensitive(True) - self._missedCheckbox.set_sensitive(True) - self._voicemailCheckbox.set_sensitive(True) - self._smsCheckbox.set_sensitive(True) - - self.clear() - self._callbackList.clear() - - def get_selected_callback_number(self): - return make_ugly(self._callbackCombo.get_child().get_text()) - - def set_account_number(self, number): - """ - Displays current account number - """ - self._accountViewNumberDisplay.set_label("%s" % (number)) - - def update(self, force = False): - if not force and self._isPopulated: - return False - self._populate_callback_combo() - self.set_account_number(self._backend.get_account_number()) - return True - def clear(self): - self._callbackCombo.get_child().set_text("") - self.set_account_number("") - self._isPopulated = False +class History(object): - def save_everything(self): - raise NotImplementedError + DETAILS_IDX = 0 + FROM_IDX = 1 + MAX_IDX = 2 - @staticmethod - def name(): - return "Account Info" - - def load_settings(self, config, section): - self._defaultCallback = config.get(section, "callback") - self._notifyOnMissed = config.getboolean(section, "notifyOnMissed") - self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail") - self._notifyOnSms = config.getboolean(section, "notifyOnSms") - - def save_settings(self, config, section): - """ - @note Thread Agnostic - """ - callback = self.get_selected_callback_number() - config.set(section, "callback", callback) - config.set(section, "notifyOnMissed", repr(self._notifyOnMissed)) - config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail)) - config.set(section, "notifyOnSms", repr(self._notifyOnSms)) - - def _populate_callback_combo(self): - self._isPopulated = True - self._callbackList.clear() - try: - callbackNumbers = self._backend.get_callback_numbers() - except StandardError, e: - self._errorDisplay.push_exception() - self._isPopulated = False - return - - for number, description in callbackNumbers.iteritems(): - self._callbackList.append((make_pretty(number),)) - - self._callbackCombo.set_model(self._callbackList) - self._callbackCombo.set_text_column(0) - #callbackNumber = self._backend.get_callback_number() - callbackNumber = self._defaultCallback - self._callbackCombo.get_child().set_text(make_pretty(callbackNumber)) - - def _set_callback_number(self, number): - try: - if not self._backend.is_valid_syntax(number): - self._errorDisplay.push_message("%s is not a valid callback number" % number) - elif number == self._backend.get_callback_number(): - warnings.warn( - "Callback number already is %s" % ( - self._backend.get_callback_number(), - ), - UserWarning, - 2 - ) - else: - self._backend.set_callback_number(number) - assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % ( - make_pretty(number), make_pretty(self._backend.get_callback_number()) - ) - warnings.warn( - "Callback number set to %s" % ( - self._backend.get_callback_number(), - ), - UserWarning, 2 - ) - except StandardError, e: - self._errorDisplay.push_exception() + HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"] + HISTORY_COLUMNS = ["Details", "From"] + assert len(HISTORY_COLUMNS) == MAX_IDX - def _update_alarm_settings(self): - try: - isEnabled = self._notifyCheckbox.get_active() - recurrence = self._minutesEntry.get_value_as_int() - if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence: - self._alarmHandler.apply_settings(isEnabled, recurrence) - finally: - self.save_everything() - self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) - self._minutesEntry.set_value(self._alarmHandler.recurrence) - - def _on_callbackentry_changed(self, *args): - text = self.get_selected_callback_number() - number = make_ugly(text) - self._set_callback_number(number) - - def _on_notify_toggled(self, *args): - if self._applyAlarmTimeoutId is not None: - gobject.source_remove(self._applyAlarmTimeoutId) - self._applyAlarmTimeoutId = None - self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout) - - def _on_minutes_changed(self, *args): - if self._applyAlarmTimeoutId is not None: - gobject.source_remove(self._applyAlarmTimeoutId) - self._applyAlarmTimeoutId = None - self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout) - - def _on_apply_timeout(self, *args): - self._applyAlarmTimeoutId = None - - self._update_alarm_settings() - return False - - def _on_missed_toggled(self, *args): - self._notifyOnMissed = self._missedCheckbox.get_active() - self.save_everything() - - def _on_voicemail_toggled(self, *args): - self._notifyOnVoicemail = self._voicemailCheckbox.get_active() - self.save_everything() - - def _on_sms_toggled(self, *args): - self._notifyOnSms = self._smsCheckbox.get_active() - self.save_everything() - - -class RecentCallsView(object): - - NUMBER_IDX = 0 - DATE_IDX = 1 - ACTION_IDX = 2 - FROM_IDX = 3 - - def __init__(self, widgetTree, backend, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - - self._isPopulated = False - self._recentmodel = gtk.ListStore( - gobject.TYPE_STRING, # number - gobject.TYPE_STRING, # date - gobject.TYPE_STRING, # action - gobject.TYPE_STRING, # from + def __init__(self, app, session, errorLog): + self._selectedFilter = self.HISTORY_ITEM_TYPES[-1] + self._app = app + self._session = session + self._session.historyUpdated.connect(self._on_history_updated) + self._errorLog = errorLog + + self._typeSelection = QtGui.QComboBox() + self._typeSelection.addItems(self.HISTORY_ITEM_TYPES) + self._typeSelection.setCurrentIndex( + self.HISTORY_ITEM_TYPES.index(self._selectedFilter) ) - self._recentview = widgetTree.get_widget("recentview") - self._recentviewselection = None - self._onRecentviewRowActivatedId = 0 - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - hildonize.set_cell_thumb_selectable(textrenderer) - self._dateColumn = gtk.TreeViewColumn("Date") - self._dateColumn.pack_start(textrenderer, expand=True) - self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - hildonize.set_cell_thumb_selectable(textrenderer) - self._actionColumn = gtk.TreeViewColumn("Action") - self._actionColumn.pack_start(textrenderer, expand=True) - self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - hildonize.set_cell_thumb_selectable(textrenderer) - self._nameColumn = gtk.TreeViewColumn("From") - self._nameColumn.pack_start(textrenderer, expand=True) - self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX) - self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - hildonize.set_cell_thumb_selectable(textrenderer) - self._numberColumn = gtk.TreeViewColumn("Number") - self._numberColumn.pack_start(textrenderer, expand=True) - self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX) - - self._window = gtk_toolbox.find_parent_window(self._recentview) - self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) - - self._updateSink = gtk_toolbox.threaded_stage( - gtk_toolbox.comap( - self._idly_populate_recentview, - gtk_toolbox.null_sink(), - ) + self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed) + refreshIcon = qui_utils.get_theme_icon( + ("view-refresh", "general_refresh", "gtk-refresh", ) ) + self._refreshButton = QtGui.QPushButton(refreshIcon, "") + self._refreshButton.clicked.connect(self._on_refresh_clicked) + self._refreshButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.PushButton, + )) + self._managerLayout = QtGui.QHBoxLayout() + self._managerLayout.addWidget(self._typeSelection, 1000) + self._managerLayout.addWidget(self._refreshButton, 0) + + self._itemStore = QtGui.QStandardItemModel() + self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS) + self._categoryManager = TimeCategories(self._itemStore) + + self._itemView = QtGui.QTreeView() + self._itemView.setModel(self._itemStore) + self._itemView.setUniformRowHeights(True) + self._itemView.setRootIsDecorated(False) + self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self._itemView.setHeaderHidden(True) + self._itemView.setItemsExpandable(False) + self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) + self._itemView.activated.connect(self._on_row_activated) + + self._layout = QtGui.QVBoxLayout() + self._layout.addLayout(self._managerLayout) + self._layout.addWidget(self._itemView) + self._widget = QtGui.QWidget() + self._widget.setLayout(self._layout) + + self._populate_items() + + @property + def toplevel(self): + return self._widget def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - self._recentview.set_model(self._recentmodel) - - self._recentview.append_column(self._dateColumn) - self._recentview.append_column(self._actionColumn) - self._recentview.append_column(self._numberColumn) - self._recentview.append_column(self._nameColumn) - self._recentviewselection = self._recentview.get_selection() - self._recentviewselection.set_mode(gtk.SELECTION_SINGLE) - - self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated) + self._itemView.setEnabled(True) def disable(self): - self._recentview.disconnect(self._onRecentviewRowActivatedId) - - self.clear() - - self._recentview.remove_column(self._dateColumn) - self._recentview.remove_column(self._actionColumn) - self._recentview.remove_column(self._nameColumn) - self._recentview.remove_column(self._numberColumn) - self._recentview.set_model(None) + self._itemView.setEnabled(False) - def number_selected(self, action, number, message): - """ - @note Actual dial function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") + def get_settings(self): + return { + "filter": self._selectedFilter, + } - def update(self, force = False): - if not force and self._isPopulated: - return False - self._updateSink.send(()) - return True + def set_settings(self, settings): + selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1]) + if selectedFilter in self.HISTORY_ITEM_TYPES: + self._selectedFilter = selectedFilter + self._typeSelection.setCurrentIndex( + self.HISTORY_ITEM_TYPES.index(selectedFilter) + ) def clear(self): - self._isPopulated = False - self._recentmodel.clear() - - @staticmethod - def name(): - return "Recent Calls" - - def load_settings(self, config, section): - pass - - def save_settings(self, config, section): - """ - @note Thread Agnostic - """ - pass - - def _idly_populate_recentview(self): - self._recentmodel.clear() - self._isPopulated = True - - try: - recentItems = self._backend.get_recent() - except StandardError, e: - self._errorDisplay.push_exception_with_lock() - self._isPopulated = False - recentItems = [] - - for personName, phoneNumber, date, action in recentItems: - if not personName: - personName = "Unknown" - prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber - prettyNumber = make_pretty(prettyNumber) - item = (prettyNumber, date, action.capitalize(), personName) - with gtk_toolbox.gtk_lock(): - self._recentmodel.append(item) - - return False - - def _on_recentview_row_activated(self, treeview, path, view_column): - model, itr = self._recentviewselection.get_selected() - if not itr: - return - - number = self._recentmodel.get_value(itr, self.NUMBER_IDX) - number = make_ugly(number) - contactPhoneNumbers = [("Phone", number)] - description = self._recentmodel.get_value(itr, self.FROM_IDX) - - action, phoneNumber, message = self._phoneTypeSelector.run( - contactPhoneNumbers, - message = description, - parent = self._window, + self._itemView.clear() + + def refresh(self, force=True): + self._itemView.setFocus(QtCore.Qt.OtherFocusReason) + self._session.update_history(force) + if self._app.notifyOnMissed and self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_BACKGROUND: + self._app.ledHandler.off() + + def _populate_items(self): + self._categoryManager.prepare_for_update(self._session.get_when_history_updated()) + + history = self._session.get_history() + history.sort(key=lambda item: item["time"], reverse=True) + for event in history: + if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]: + continue + + relTime = misc_utils.abbrev_relative_date(event["relTime"]) + action = event["action"] + number = event["number"] + prettyNumber = misc_utils.make_pretty(number) + name = event["name"] + if not name or name == number: + name = event["location"] + if not name: + name = "Unknown" + + detailsItem = QtGui.QStandardItem("%s - %s\n%s" % (relTime, action, prettyNumber)) + detailsFont = detailsItem.font() + detailsFont.setPointSize(max(detailsFont.pointSize() - 7, 5)) + detailsItem.setFont(detailsFont) + nameItem = QtGui.QStandardItem(name) + nameFont = nameItem.font() + nameFont.setPointSize(nameFont.pointSize() + 4) + nameItem.setFont(nameFont) + row = detailsItem, nameItem + for item in row: + item.setEditable(False) + item.setCheckable(False) + row[0].setData(event) + self._categoryManager.add_row(event["time"], row) + self._itemView.expandAll() + + @QtCore.pyqtSlot(str) + @misc_utils.log_exception(_moduleLogger) + def _on_filter_changed(self, newItem): + with qui_utils.notify_error(self._errorLog): + self._selectedFilter = str(newItem) + self._populate_items() + + @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) + def _on_history_updated(self): + with qui_utils.notify_error(self._errorLog): + self._populate_items() + + @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_clicked(self, arg = None): + with qui_utils.notify_error(self._errorLog): + self.refresh(force=True) + + @QtCore.pyqtSlot(QtCore.QModelIndex) + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, index): + with qui_utils.notify_error(self._errorLog): + timeIndex = index.parent() + if not timeIndex.isValid(): + return + timeRow = timeIndex.row() + row = index.row() + detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX) + fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX) + contactDetails = detailsItem.data().toPyObject() + + title = unicode(fromItem.text()) + number = str(contactDetails[QtCore.QString("number")]) + contactId = number # ids don't seem too unique so using numbers + + descriptionRows = [] + for t in xrange(self._itemStore.rowCount()): + randomTimeItem = self._itemStore.item(t, 0) + for i in xrange(randomTimeItem.rowCount()): + iItem = randomTimeItem.child(i, 0) + iContactDetails = iItem.data().toPyObject() + iNumber = str(iContactDetails[QtCore.QString("number")]) + if number != iNumber: + continue + relTime = misc_utils.abbrev_relative_date(iContactDetails[QtCore.QString("relTime")]) + action = str(iContactDetails[QtCore.QString("action")]) + number = str(iContactDetails[QtCore.QString("number")]) + prettyNumber = misc_utils.make_pretty(number) + rowItems = relTime, action, prettyNumber + descriptionRows.append("