From: Ed Page Date: Wed, 10 Aug 2011 00:10:18 +0000 (-0500) Subject: BROKEN: Moved everything X-Git-Url: http://git.maemo.org/git/?p=gc-dialer;a=commitdiff_plain;h=0f8dd9d965abc692b4624da75d5c65b3fe6feca4 BROKEN: Moved everything --- diff --git a/DialCentral b/DialCentral new file mode 100755 index 0000000..a20d4fe --- /dev/null +++ b/DialCentral @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import sys + + +sys.path.append("/opt/dialcentral/lib") + + +import dialcentral_qt + + +if __name__ == "__main__": + dialcentral_qt.run() diff --git a/README b/README deleted file mode 100644 index b87ce9c..0000000 --- a/README +++ /dev/null @@ -1,37 +0,0 @@ -Building a package -=================== -Run - make PLATFORM=... package -which will create a "./pkg-.../..." heirarchy. Move this structure to somewhere on the tablet, then run pypackager. - -Supported PLATFORMs include - desktop - os2007 - os2008 - -SDK Enviroment -=================== - -Native - -Follow install instructions - Ubuntu: http://www.linuxuk.org/node/38 -Install Nokia stuff (for each target) - fakeroot apt-get install maemo-explicit - -Userful commands -Login - /scratchbox/login -Change targets - sb-conf select DIABLO_ARMEL - sb-conf select DIABLO_X86 -Fixing it - fakeroot apt-get -f install - -Starting scratchbox - Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite - scratchbox - export DISPLAY=:2 - af-sb-init.sh start -Then running a command in the "Maemo" terminal will launch it in the Xephyr session - Tip: run with "run-standalone.sh" for niceness? diff --git a/data/LICENSE b/data/LICENSE deleted file mode 100644 index fb44a62..0000000 --- a/data/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -http://www.gentleface.com/free_icon_set.html -The Creative Commons Attribution-NonCommercial -- FREE -http://creativecommons.org/licenses/by-nc-nd/3.0/ - -Sound: -http://www.freesound.org/samplesViewSingle.php?id=2166 -http://creativecommons.org/licenses/sampling+/1.0/ - -placed.png, received.png, placed.png -Free for commercial use -http://www.iconeden.com/icon/free/get/bright-free-stock-iconset diff --git a/data/app/LICENSE b/data/app/LICENSE new file mode 100644 index 0000000..fb44a62 --- /dev/null +++ b/data/app/LICENSE @@ -0,0 +1,11 @@ +http://www.gentleface.com/free_icon_set.html +The Creative Commons Attribution-NonCommercial -- FREE +http://creativecommons.org/licenses/by-nc-nd/3.0/ + +Sound: +http://www.freesound.org/samplesViewSingle.php?id=2166 +http://creativecommons.org/licenses/sampling+/1.0/ + +placed.png, received.png, placed.png +Free for commercial use +http://www.iconeden.com/icon/free/get/bright-free-stock-iconset diff --git a/data/app/bell.flac b/data/app/bell.flac new file mode 100644 index 0000000..419420e Binary files /dev/null and b/data/app/bell.flac differ diff --git a/data/app/bell.wav b/data/app/bell.wav new file mode 100644 index 0000000..6b7fc1b Binary files /dev/null and b/data/app/bell.wav differ diff --git a/data/app/contacts.png b/data/app/contacts.png new file mode 100644 index 0000000..aa1a7ce Binary files /dev/null and b/data/app/contacts.png differ diff --git a/data/app/dialpad.png b/data/app/dialpad.png new file mode 100644 index 0000000..b54013b Binary files /dev/null and b/data/app/dialpad.png differ diff --git a/data/app/history.png b/data/app/history.png new file mode 100644 index 0000000..887989a Binary files /dev/null and b/data/app/history.png differ diff --git a/data/app/messages.png b/data/app/messages.png new file mode 100644 index 0000000..e117918 Binary files /dev/null and b/data/app/messages.png differ diff --git a/data/app/missed.png b/data/app/missed.png new file mode 100644 index 0000000..34f71c4 Binary files /dev/null and b/data/app/missed.png differ diff --git a/data/app/placed.png b/data/app/placed.png new file mode 100644 index 0000000..329771d Binary files /dev/null and b/data/app/placed.png differ diff --git a/data/app/received.png b/data/app/received.png new file mode 100644 index 0000000..2b45263 Binary files /dev/null and b/data/app/received.png differ diff --git a/data/bell.flac b/data/bell.flac deleted file mode 100644 index 419420e..0000000 Binary files a/data/bell.flac and /dev/null differ diff --git a/data/bell.wav b/data/bell.wav deleted file mode 100644 index 6b7fc1b..0000000 Binary files a/data/bell.wav and /dev/null differ diff --git a/data/contacts.png b/data/contacts.png deleted file mode 100644 index aa1a7ce..0000000 Binary files a/data/contacts.png and /dev/null differ diff --git a/data/dialcentral-base.svg b/data/dialcentral-base.svg new file mode 100644 index 0000000..aa35390 --- /dev/null +++ b/data/dialcentral-base.svg @@ -0,0 +1,148 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/dialcentral.colors b/data/dialcentral.colors new file mode 100644 index 0000000..c6b9fa9 --- /dev/null +++ b/data/dialcentral.colors @@ -0,0 +1,3 @@ +Your icon's dominant color is #72ab40 +A suggested disabled color is #d1ffa9 +A suggested pressed color is #57743e diff --git a/data/dialcentral.png b/data/dialcentral.png new file mode 100644 index 0000000..5f8b811 Binary files /dev/null and b/data/dialcentral.png differ diff --git a/data/dialcentral.svg b/data/dialcentral.svg new file mode 100644 index 0000000..e0d45a8 --- /dev/null +++ b/data/dialcentral.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/dialpad.png b/data/dialpad.png deleted file mode 100644 index b54013b..0000000 Binary files a/data/dialpad.png and /dev/null differ diff --git a/data/history.png b/data/history.png deleted file mode 100644 index 887989a..0000000 Binary files a/data/history.png and /dev/null differ diff --git a/data/messages.png b/data/messages.png deleted file mode 100644 index e117918..0000000 Binary files a/data/messages.png and /dev/null differ diff --git a/data/missed.png b/data/missed.png deleted file mode 100644 index 34f71c4..0000000 Binary files a/data/missed.png and /dev/null differ diff --git a/data/placed.png b/data/placed.png deleted file mode 100644 index 329771d..0000000 Binary files a/data/placed.png and /dev/null differ diff --git a/data/received.png b/data/received.png deleted file mode 100644 index 2b45263..0000000 Binary files a/data/received.png and /dev/null differ diff --git a/data/template.desktop b/data/template.desktop new file mode 100644 index 0000000..3b446d7 --- /dev/null +++ b/data/template.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Encoding=UTF-8 +Version=1.0 +Type=Application +Name=DialCentral +Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py +Icon=dialcentral +Categories=Network;InstantMessaging;Qt; diff --git a/dialcentral/__init__.py b/dialcentral/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/dialcentral/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/dialcentral/alarm_handler.py b/dialcentral/alarm_handler.py new file mode 100644 index 0000000..a79f992 --- /dev/null +++ b/dialcentral/alarm_handler.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python + +import os +import time +import datetime +import ConfigParser +import logging + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore +import dbus + + +_FREMANTLE_ALARM = "Fremantle" +_DIABLO_ALARM = "Diablo" +_NO_ALARM = "None" + + +try: + import alarm + ALARM_TYPE = _FREMANTLE_ALARM +except (ImportError, OSError): + try: + import osso.alarmd as alarmd + ALARM_TYPE = _DIABLO_ALARM + except (ImportError, OSError): + ALARM_TYPE = _NO_ALARM + + +_moduleLogger = logging.getLogger(__name__) + + +def _get_start_time(recurrence): + now = datetime.datetime.now() + startTimeMinute = now.minute + max(recurrence, 5) # being safe + startTimeHour = now.hour + int(startTimeMinute / 60) + startTimeMinute = startTimeMinute % 59 + now.replace(minute=startTimeMinute) + timestamp = int(time.mktime(now.timetuple())) + return timestamp + + +def _create_recurrence_mask(recurrence, base): + """ + >>> bin(_create_recurrence_mask(60, 60)) + '0b1' + >>> bin(_create_recurrence_mask(30, 60)) + '0b1000000000000000000000000000001' + >>> bin(_create_recurrence_mask(2, 60)) + '0b10101010101010101010101010101010101010101010101010101010101' + >>> bin(_create_recurrence_mask(1, 60)) + '0b111111111111111111111111111111111111111111111111111111111111' + """ + mask = 0 + for i in xrange(base / recurrence): + mask |= 1 << (recurrence * i) + return mask + + +def _unpack_minutes(recurrence): + """ + >>> _unpack_minutes(0) + (0, 0, 0) + >>> _unpack_minutes(1) + (0, 0, 1) + >>> _unpack_minutes(59) + (0, 0, 59) + >>> _unpack_minutes(60) + (0, 1, 0) + >>> _unpack_minutes(129) + (0, 2, 9) + >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2) + (5, 3, 2) + >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2) + (5, 3, 2) + """ + minutesInAnHour = 60 + minutesInDay = 24 * minutesInAnHour + minutesInAWeek = minutesInDay * 7 + + days = recurrence / minutesInDay + daysOfWeek = days % 7 + recurrence -= days * minutesInDay + hours = recurrence / minutesInAnHour + recurrence -= hours * minutesInAnHour + mins = recurrence % minutesInAnHour + recurrence -= mins + assert recurrence == 0, "Recurrence %d" % recurrence + return daysOfWeek, hours, mins + + +class _FremantleAlarmHandler(object): + + _INVALID_COOKIE = -1 + _REPEAT_FOREVER = -1 + _TITLE = "Dialcentral Notifications" + _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py")) + + def __init__(self): + self._recurrence = 5 + + self._alarmCookie = self._INVALID_COOKIE + self._launcher = self._LAUNCHER + + def load_settings(self, config, sectionName): + try: + self._recurrence = config.getint(sectionName, "recurrence") + self._alarmCookie = config.getint(sectionName, "alarmCookie") + launcher = config.get(sectionName, "notifier") + if launcher: + self._launcher = launcher + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + pass + + def save_settings(self, config, sectionName): + try: + config.set(sectionName, "recurrence", str(self._recurrence)) + config.set(sectionName, "alarmCookie", str(self._alarmCookie)) + launcher = self._launcher if self._launcher != self._LAUNCHER else "" + config.set(sectionName, "notifier", launcher) + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + pass + + def apply_settings(self, enabled, recurrence): + if recurrence != self._recurrence or enabled != self.isEnabled: + if self.isEnabled: + self._clear_alarm() + if enabled: + self._set_alarm(recurrence) + self._recurrence = int(recurrence) + + @property + def recurrence(self): + return self._recurrence + + @property + def isEnabled(self): + return self._alarmCookie != self._INVALID_COOKIE + + def _set_alarm(self, recurrenceMins): + assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins + alarmTime = _get_start_time(recurrenceMins) + + event = alarm.Event() + event.appid = self._TITLE + event.alarm_time = alarmTime + event.recurrences_left = self._REPEAT_FOREVER + + action = event.add_actions(1)[0] + action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED + action.command = self._launcher + + recurrence = event.add_recurrences(1)[0] + recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60) + recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE + recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE + recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE + recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE + recurrence.special |= alarm.RECUR_SPECIAL_NONE + + assert event.is_sane() + self._alarmCookie = alarm.add_event(event) + + def _clear_alarm(self): + if self._alarmCookie == self._INVALID_COOKIE: + return + alarm.delete_event(self._alarmCookie) + self._alarmCookie = self._INVALID_COOKIE + + +class _DiabloAlarmHandler(object): + + _INVALID_COOKIE = -1 + _TITLE = "Dialcentral Notifications" + _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py")) + _REPEAT_FOREVER = -1 + + def __init__(self): + self._recurrence = 5 + + bus = dbus.SystemBus() + self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd"); + self._alarmCookie = self._INVALID_COOKIE + self._launcher = self._LAUNCHER + + def load_settings(self, config, sectionName): + try: + self._recurrence = config.getint(sectionName, "recurrence") + self._alarmCookie = config.getint(sectionName, "alarmCookie") + launcher = config.get(sectionName, "notifier") + if launcher: + self._launcher = launcher + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + pass + + def save_settings(self, config, sectionName): + config.set(sectionName, "recurrence", str(self._recurrence)) + config.set(sectionName, "alarmCookie", str(self._alarmCookie)) + launcher = self._launcher if self._launcher != self._LAUNCHER else "" + config.set(sectionName, "notifier", launcher) + + def apply_settings(self, enabled, recurrence): + if recurrence != self._recurrence or enabled != self.isEnabled: + if self.isEnabled: + self._clear_alarm() + if enabled: + self._set_alarm(recurrence) + self._recurrence = int(recurrence) + + @property + def recurrence(self): + return self._recurrence + + @property + def isEnabled(self): + return self._alarmCookie != self._INVALID_COOKIE + + def _set_alarm(self, recurrence): + assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence + alarmTime = _get_start_time(recurrence) + + #Setup the alarm arguments so that they can be passed to the D-Bus add_event method + _DEFAULT_FLAGS = ( + alarmd.ALARM_EVENT_NO_DIALOG | + alarmd.ALARM_EVENT_NO_SNOOZE | + alarmd.ALARM_EVENT_CONNECTED + ) + action = [] + action.extend(['flags', _DEFAULT_FLAGS]) + action.extend(['title', self._TITLE]) + action.extend(['path', self._launcher]) + action.extend([ + 'arguments', + dbus.Array( + [alarmTime, int(27)], + signature=dbus.Signature('v') + ) + ]) #int(27) used in place of alarm_index + + event = [] + event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)]) + event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus + event.append(dbus.UInt32(len(action) / 2)) + event.extend(action) + event.extend(['time', dbus.Int64(alarmTime)]) + event.extend(['recurr_interval', dbus.UInt32(recurrence)]) + event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)]) + + self._alarmCookie = self._alarmdDBus.add_event(*event); + + def _clear_alarm(self): + if self._alarmCookie == self._INVALID_COOKIE: + return + deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie)) + self._alarmCookie = self._INVALID_COOKIE + assert deleteResult != -1, "Deleting of alarm event failed" + + +class _ApplicationAlarmHandler(object): + + _REPEAT_FOREVER = -1 + _MIN_TO_MS_FACTORY = 1000 * 60 + + def __init__(self): + self._timer = QtCore.QTimer() + self._timer.setSingleShot(False) + self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY) + + def load_settings(self, config, sectionName): + try: + self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY) + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + pass + self._timer.start() + + def save_settings(self, config, sectionName): + config.set(sectionName, "recurrence", str(self.recurrence)) + + def apply_settings(self, enabled, recurrence): + self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY) + if enabled: + self._timer.start() + else: + self._timer.stop() + + @property + def notifySignal(self): + return self._timer.timeout + + @property + def recurrence(self): + return int(self._timer.interval() / self._MIN_TO_MS_FACTORY) + + @property + def isEnabled(self): + return self._timer.isActive() + + +class _NoneAlarmHandler(object): + + def __init__(self): + self._enabled = False + self._recurrence = 5 + + def load_settings(self, config, sectionName): + try: + self._recurrence = config.getint(sectionName, "recurrence") + self._enabled = True + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + pass + + def save_settings(self, config, sectionName): + config.set(sectionName, "recurrence", str(self.recurrence)) + + def apply_settings(self, enabled, recurrence): + self._enabled = enabled + + @property + def recurrence(self): + return self._recurrence + + @property + def isEnabled(self): + return self._enabled + + +_BACKGROUND_ALARM_FACTORY = { + _FREMANTLE_ALARM: _FremantleAlarmHandler, + _DIABLO_ALARM: _DiabloAlarmHandler, + _NO_ALARM: None, +}[ALARM_TYPE] + + +class AlarmHandler(object): + + ALARM_NONE = "No Alert" + ALARM_BACKGROUND = "Background Alert" + ALARM_APPLICATION = "Application Alert" + ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION] + + ALARM_FACTORY = { + ALARM_NONE: _NoneAlarmHandler, + ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY, + ALARM_APPLICATION: _ApplicationAlarmHandler, + } + + def __init__(self): + self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()} + self._currentAlarmType = self.ALARM_NONE + + def load_settings(self, config, sectionName): + try: + self._currentAlarmType = config.get(sectionName, "alarm") + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + _moduleLogger.exception("Falling back to old style") + self._currentAlarmType = self.ALARM_BACKGROUND + if self._currentAlarmType not in self.ALARM_TYPES: + self._currentAlarmType = self.ALARM_NONE + + self._init_alarm(self._currentAlarmType) + if self._currentAlarmType in self._alarms: + self._alarms[self._currentAlarmType].load_settings(config, sectionName) + if not self._alarms[self._currentAlarmType].isEnabled: + _moduleLogger.info("Config file lied, not actually enabled") + self._currentAlarmType = self.ALARM_NONE + else: + _moduleLogger.info("Background alerts not supported") + self._currentAlarmType = self.ALARM_NONE + + def save_settings(self, config, sectionName): + config.set(sectionName, "alarm", self._currentAlarmType) + self._alarms[self._currentAlarmType].save_settings(config, sectionName) + + def apply_settings(self, t, recurrence): + self._init_alarm(t) + newHandler = self._alarms[t] + oldHandler = self._alarms[self._currentAlarmType] + if newHandler != oldHandler: + oldHandler.apply_settings(False, 0) + newHandler.apply_settings(True, recurrence) + self._currentAlarmType = t + + @property + def alarmType(self): + return self._currentAlarmType + + @property + def backgroundNotificationsSupported(self): + return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None + + @property + def applicationNotifySignal(self): + self._init_alarm(self.ALARM_APPLICATION) + return self._alarms[self.ALARM_APPLICATION].notifySignal + + @property + def recurrence(self): + return self._alarms[self._currentAlarmType].recurrence + + @property + def isEnabled(self): + return self._currentAlarmType != self.ALARM_NONE + + def _init_alarm(self, t): + if t not in self._alarms and self.ALARM_FACTORY[t] is not None: + self._alarms[t] = self.ALARM_FACTORY[t]() + + +def main(): + logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' + logging.basicConfig(level=logging.DEBUG, format=logFormat) + import constants + try: + import optparse + except ImportError: + return + + parser = optparse.OptionParser() + parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data") + parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False) + parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False) + parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5) + (commandOptions, commandArgs) = parser.parse_args() + + alarmHandler = AlarmHandler() + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + alarmHandler.load_settings(config, "alarm") + + if commandOptions.display: + print "Alarm (%s) is %s for every %d minutes" % ( + alarmHandler._alarmCookie, + "enabled" if alarmHandler.isEnabled else "disabled", + alarmHandler.recurrence, + ) + else: + isEnabled = commandOptions.enabled + recurrence = commandOptions.recurrence + alarmHandler.apply_settings(isEnabled, recurrence) + + alarmHandler.save_settings(config, "alarm") + configFile = open(constants._user_settings_, "wb") + try: + config.write(configFile) + finally: + configFile.close() + + +if __name__ == "__main__": + main() diff --git a/dialcentral/alarm_notify.py b/dialcentral/alarm_notify.py new file mode 100755 index 0000000..bc6240e --- /dev/null +++ b/dialcentral/alarm_notify.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python + +import os +import filecmp +import ConfigParser +import pprint +import logging +import logging.handlers + +import constants +from backends.gvoice import gvoice + + +def get_missed(backend): + missedPage = backend._browser.download(backend._XML_MISSED_URL) + missedJson = backend._grab_json(missedPage) + return missedJson + + +def get_voicemail(backend): + voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL) + voicemailJson = backend._grab_json(voicemailPage) + return voicemailJson + + +def get_sms(backend): + smsPage = backend._browser.download(backend._XML_SMS_URL) + smsJson = backend._grab_json(smsPage) + return smsJson + + +def remove_reltime(data): + for messageData in data["messages"].itervalues(): + for badPart in [ + "relTime", + "relativeStartTime", + "time", + "star", + "isArchived", + "isRead", + "isSpam", + "isTrash", + "labels", + ]: + if badPart in messageData: + del messageData[badPart] + for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]: + if globalBad in data: + del data[globalBad] + + +def is_type_changed(backend, type, get_material): + jsonMaterial = get_material(backend) + unreadCount = jsonMaterial["unreadCounts"][type] + + previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type) + currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type) + + try: + os.remove(previousSnapshotPath) + except OSError, e: + # check if failed purely because the old file didn't exist, which is fine + if e.errno != 2: + raise + try: + os.rename(currentSnapshotPath, previousSnapshotPath) + previousExists = True + except OSError, e: + # check if failed purely because the new old file didn't exist, which is fine + if e.errno != 2: + raise + previousExists = False + + remove_reltime(jsonMaterial) + textMaterial = pprint.pformat(jsonMaterial) + currentSnapshot = file(currentSnapshotPath, "w") + try: + currentSnapshot.write(textMaterial) + finally: + currentSnapshot.close() + + if unreadCount == 0 or not previousExists: + return False + + seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath) + return not seemEqual + + +def create_backend(config): + gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt") + backend = gvoice.GVoiceBackend(gvCookiePath) + + loggedIn = False + + if not loggedIn: + loggedIn = backend.refresh_account_info() is not None + + if not loggedIn: + import base64 + try: + blobs = ( + config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) + for i in xrange(2) + ) + creds = ( + base64.b64decode(blob) + for blob in blobs + ) + username, password = tuple(creds) + loggedIn = backend.login(username, password) is not None + except ConfigParser.NoOptionError, e: + pass + except ConfigParser.NoSectionError, e: + pass + + assert loggedIn + return backend + + +def is_changed(config, backend): + try: + notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed") + notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail") + notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms") + except ConfigParser.NoOptionError, e: + notifyOnMissed = False + notifyOnVoicemail = False + notifyOnSms = False + except ConfigParser.NoSectionError, e: + notifyOnMissed = False + notifyOnVoicemail = False + notifyOnSms = False + logging.debug( + "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms) + ) + + notifySources = [] + if notifyOnMissed: + notifySources.append(("missed", get_missed)) + if notifyOnVoicemail: + notifySources.append(("voicemail", get_voicemail)) + if notifyOnSms: + notifySources.append(("sms", get_sms)) + + notifyUser = False + for type, get_material in notifySources: + if is_type_changed(backend, type, get_material): + notifyUser = True + return notifyUser + + +def notify_on_change(): + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + backend = create_backend(config) + notifyUser = is_changed(config, backend) + + if notifyUser: + logging.info("Changed") + import led_handler + led = led_handler.LedHandler() + led.on() + else: + logging.info("No Change") + + +if __name__ == "__main__": + logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' + logging.basicConfig(level=logging.DEBUG, format=logFormat) + rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1) + rotating.setFormatter(logging.Formatter(logFormat)) + root = logging.getLogger() + root.addHandler(rotating) + logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__)) + logging.info("OS: %s" % (os.uname()[0], )) + logging.info("Kernel: %s (%s) for %s" % os.uname()[2:]) + logging.info("Hostname: %s" % os.uname()[1]) + try: + notify_on_change() + except: + logging.exception("Error") + raise diff --git a/dialcentral/backends/__init__.py b/dialcentral/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dialcentral/backends/file_backend.py b/dialcentral/backends/file_backend.py new file mode 100644 index 0000000..9f8927a --- /dev/null +++ b/dialcentral/backends/file_backend.py @@ -0,0 +1,176 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Eric Warnke ericew AT gmail 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 + +Filesystem backend for contact support +""" + +from __future__ import with_statement + +import os +import csv + + +def try_unicode(s): + try: + return s.decode("UTF-8") + except UnicodeDecodeError: + return s + + +class CsvAddressBook(object): + """ + Currently supported file format + @li Has the first line as a header + @li Escapes with quotes + @li Comma as delimiter + @li Column 0 is name, column 1 is number + """ + + def __init__(self, name, csvPath): + self._name = name + self._csvPath = csvPath + self._contacts = {} + + @property + def name(self): + return self._name + + def update_account(self, force = True): + if not force or not self._contacts: + return + self._contacts = dict( + self._read_csv(self._csvPath) + ) + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + if not self._contacts: + self._contacts = dict( + self._read_csv(self._csvPath) + ) + return self._contacts + + def _read_csv(self, csvPath): + try: + f = open(csvPath, "rU") + csvReader = iter(csv.reader(f)) + except IOError, e: + if e.errno == 2: + return + raise + + header = csvReader.next() + nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header) + + yieldCount = 0 + for row in csvReader: + contactDetails = [] + for (phoneType, phoneColumn) in phoneColumns: + try: + if len(row[phoneColumn]) == 0: + continue + contactDetails.append({ + "phoneType": try_unicode(phoneType), + "phoneNumber": row[phoneColumn], + }) + except IndexError: + pass + if 0 < len(contactDetails): + nameParts = (row[i].strip() for i in nameColumns) + nameParts = (part for part in nameParts if part) + fullName = " ".join(nameParts).strip() + if not fullName: + for fallbackColumn in nameFallbacks: + if row[fallbackColumn].strip(): + fullName = row[fallbackColumn].strip() + break + else: + fullName = "Unknown" + fullName = try_unicode(fullName) + yield str(yieldCount), { + "contactId": "%s-%d" % (self._name, yieldCount), + "name": fullName, + "numbers": contactDetails, + } + yieldCount += 1 + + @classmethod + def _guess_columns(cls, row): + firstMiddleLast = [-1, -1, -1] + names = [] + nameFallbacks = [] + phones = [] + for i, item in enumerate(row): + lowerItem = item.lower() + if 0 <= lowerItem.find("name"): + names.append((item, i)) + + if 0 <= lowerItem.find("couple"): + names.insert(0, (item, i)) + + if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"): + firstMiddleLast[0] = i + elif 0 <= lowerItem.find("middle"): + firstMiddleLast[1] = i + elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"): + firstMiddleLast[2] = i + elif 0 <= lowerItem.find("phone"): + phones.append((item, i)) + elif 0 <= lowerItem.find("mobile"): + phones.append((item, i)) + elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"): + nameFallbacks.append(i) + if len(names) == 0: + names.append(("Name", 0)) + if len(phones) == 0: + phones.append(("Phone", 1)) + + nameColumns = [i for i in firstMiddleLast if 0 <= i] + if len(nameColumns) < 2: + del nameColumns[:] + nameColumns.append(names[0][1]) + + return nameColumns, nameFallbacks, phones + + +class FilesystemAddressBookFactory(object): + + FILETYPE_SUPPORT = { + "csv": CsvAddressBook, + } + + def __init__(self, path): + self._path = path + + def get_addressbooks(self): + for root, dirs, filenames in os.walk(self._path): + for filename in filenames: + try: + name, ext = filename.rsplit(".", 1) + except ValueError: + continue + + try: + cls = self.FILETYPE_SUPPORT[ext] + except KeyError: + continue + yield cls(name, os.path.join(root, filename)) diff --git a/dialcentral/backends/gv_backend.py b/dialcentral/backends/gv_backend.py new file mode 100644 index 0000000..17bbc90 --- /dev/null +++ b/dialcentral/backends/gv_backend.py @@ -0,0 +1,321 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Eric Warnke ericew AT gmail 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 + +Google Voice backend code + +Resources + http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ + http://posttopic.com/topic/google-voice-add-on-development +""" + +from __future__ import with_statement + +import itertools +import logging + +from gvoice import gvoice + +from util import io as io_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class GVDialer(object): + + MESSAGE_TEXTS = "Text" + MESSAGE_VOICEMAILS = "Voicemail" + MESSAGE_ALL = "All" + + HISTORY_RECEIVED = "Received" + HISTORY_MISSED = "Missed" + HISTORY_PLACED = "Placed" + HISTORY_ALL = "All" + + def __init__(self, cookieFile = None): + self._gvoice = gvoice.GVoiceBackend(cookieFile) + self._texts = [] + self._voicemails = [] + self._received = [] + self._missed = [] + self._placed = [] + + def is_quick_login_possible(self): + """ + @returns True then refresh_account_info might be enough to login, else full login is required + """ + return self._gvoice.is_quick_login_possible() + + def refresh_account_info(self): + return self._gvoice.refresh_account_info() + + def login(self, username, password): + """ + Attempt to login to GoogleVoice + @returns Whether login was successful or not + """ + return self._gvoice.login(username, password) + + def logout(self): + self._texts = [] + self._voicemails = [] + self._received = [] + self._missed = [] + self._placed = [] + return self._gvoice.logout() + + def persist(self): + return self._gvoice.persist() + + def is_dnd(self): + return self._gvoice.is_dnd() + + def set_dnd(self, doNotDisturb): + return self._gvoice.set_dnd(doNotDisturb) + + def call(self, outgoingNumber): + """ + This is the main function responsible for initating the callback + """ + return self._gvoice.call(outgoingNumber) + + def cancel(self, outgoingNumber=None): + """ + Cancels a call matching outgoing and forwarding numbers (if given). + Will raise an error if no matching call is being placed + """ + return self._gvoice.cancel(outgoingNumber) + + def send_sms(self, phoneNumbers, message): + self._gvoice.send_sms(phoneNumbers, message) + + def search(self, query): + """ + Search your Google Voice Account history for calls, voicemails, and sms + Returns ``Folder`` instance containting matching messages + """ + return self._gvoice.search(query) + + def get_feed(self, feed): + return self._gvoice.get_feed(feed) + + def download(self, messageId, targetPath): + """ + Download a voicemail or recorded call MP3 matching the given ``msg`` + which can either be a ``Message`` instance, or a SHA1 identifier. + Message hashes can be found in ``self.voicemail().messages`` for example. + Returns location of saved file. + """ + self._gvoice.download(messageId, targetPath) + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return self._gvoice.is_valid_syntax(number) + + def get_account_number(self): + """ + @returns The GoogleVoice phone number + """ + return self._gvoice.get_account_number() + + def get_callback_numbers(self): + """ + @returns a dictionary mapping call back numbers to descriptions + @note These results are cached for 30 minutes. + """ + return self._gvoice.get_callback_numbers() + + def set_callback_number(self, callbacknumber): + """ + Set the number that GoogleVoice calls + @param callbacknumber should be a proper 10 digit number + """ + return self._gvoice.set_callback_number(callbacknumber) + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + return self._gvoice.get_callback_number() + + def get_call_history(self, historyType): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + """ + history = list(self._get_call_history(historyType)) + history.sort(key=lambda item: item["time"]) + return history + + def _get_call_history(self, historyType): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + """ + if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received: + self._received = list(self._gvoice.get_received_calls()) + for item in self._received: + item["action"] = self.HISTORY_RECEIVED + if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed: + self._missed = list(self._gvoice.get_missed_calls()) + for item in self._missed: + item["action"] = self.HISTORY_MISSED + if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed: + self._placed = list(self._gvoice.get_placed_calls()) + for item in self._placed: + item["action"] = self.HISTORY_PLACED + received = self._received + missed = self._missed + placed = self._placed + for item in received: + yield item + for item in missed: + yield item + for item in placed: + yield item + + def get_messages(self, messageType): + messages = list(self._get_messages(messageType)) + messages.sort(key=lambda message: message["time"]) + return messages + + def _get_messages(self, messageType): + if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails: + self._voicemails = list(self._gvoice.get_voicemails()) + if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts: + self._texts = list(self._gvoice.get_texts()) + voicemails = self._voicemails + smss = self._texts + + conversations = itertools.chain(voicemails, smss) + for conversation in conversations: + messages = conversation.messages + messageParts = [ + (message.whoFrom, self._format_message(message), message.when) + for message in messages + ] + + messageDetails = { + "id": conversation.id, + "contactId": conversation.contactId, + "name": conversation.name, + "time": conversation.time, + "relTime": conversation.relTime, + "prettyNumber": conversation.prettyNumber, + "number": conversation.number, + "location": conversation.location, + "messageParts": messageParts, + "type": conversation.type, + "isRead": conversation.isRead, + "isTrash": conversation.isTrash, + "isSpam": conversation.isSpam, + "isArchived": conversation.isArchived, + } + yield messageDetails + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "GV" + + @staticmethod + def factory_name(): + return "Google Voice" + + def _format_message(self, message): + messagePartFormat = { + "med1": "%s", + "med2": "%s", + "high": "%s", + } + return " ".join( + messagePartFormat[text.accuracy] % io_utils.escape(text.text) + for text in message.body + ) + + +def sort_messages(allMessages): + sortableAllMessages = [ + (message["time"], message) + for message in allMessages + ] + sortableAllMessages.sort(reverse=True) + return ( + message + for (exactTime, message) in sortableAllMessages + ) + + +def decorate_recent(recentCallData): + """ + @returns (personsName, phoneNumber, date, action) + """ + contactId = recentCallData["contactId"] + if recentCallData["name"]: + header = recentCallData["name"] + elif recentCallData["prettyNumber"]: + header = recentCallData["prettyNumber"] + elif recentCallData["location"]: + header = recentCallData["location"] + else: + header = "Unknown" + + number = recentCallData["number"] + relTime = recentCallData["relTime"] + action = recentCallData["action"] + return contactId, header, number, relTime, action + + +def decorate_message(messageData): + contactId = messageData["contactId"] + exactTime = messageData["time"] + if messageData["name"]: + header = messageData["name"] + elif messageData["prettyNumber"]: + header = messageData["prettyNumber"] + else: + header = "Unknown" + number = messageData["number"] + relativeTime = messageData["relTime"] + + messageParts = list(messageData["messageParts"]) + if len(messageParts) == 0: + messages = ("No Transcription", ) + elif len(messageParts) == 1: + messages = (messageParts[0][1], ) + else: + messages = [ + "%s: %s" % (messagePart[0], messagePart[1]) + for messagePart in messageParts + ] + + decoratedResults = contactId, header, number, relativeTime, messages + return decoratedResults diff --git a/dialcentral/backends/gvoice/__init__.py b/dialcentral/backends/gvoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dialcentral/backends/gvoice/browser_emu.py b/dialcentral/backends/gvoice/browser_emu.py new file mode 100644 index 0000000..4fef6e8 --- /dev/null +++ b/dialcentral/backends/gvoice/browser_emu.py @@ -0,0 +1,210 @@ +""" +@author: Laszlo Nagy +@copyright: (c) 2005 by Szoftver Messias Bt. +@licence: BSD style + +Objects of the MozillaEmulator class can emulate a browser that is capable of: + + - cookie management + - configurable user agent string + - GET and POST + - multipart POST (send files) + - receive content into file + +I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: + + 1. Use firefox + 2. Install and open the livehttpheaders plugin + 3. Use the website manually with firefox + 4. Check the GET and POST requests in the livehttpheaders capture window + 5. Create an instance of the above class and send the same GET and POST requests to the server. + +Optional steps: + + - You can change user agent string in the build_opened method + - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files +""" + +import urllib2 +import cookielib +import logging + +import socket + + +_moduleLogger = logging.getLogger(__name__) +socket.setdefaulttimeout(25) + + +def add_proxy(protocol, url, port): + proxyInfo = "%s:%s" % (url, port) + proxy = urllib2.ProxyHandler( + {protocol: proxyInfo} + ) + opener = urllib2.build_opener(proxy) + urllib2.install_opener(opener) + + +class MozillaEmulator(object): + + USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)' + #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16" + + def __init__(self, trycount = 1): + """Create a new MozillaEmulator object. + + @param trycount: The download() method will retry the operation if it + fails. You can specify -1 for infinite retrying. A value of 0 means no + retrying. A value of 1 means one retry. etc.""" + self.debug = False + self.trycount = trycount + self._cookies = cookielib.LWPCookieJar() + self._loadedFromCookies = False + self._storeCookies = False + + def load_cookies(self, path): + assert not self._loadedFromCookies, "Load cookies only once" + if path is None: + return + + self._cookies.filename = path + try: + self._cookies.load() + except cookielib.LoadError: + _moduleLogger.exception("Bad cookie file") + except IOError: + _moduleLogger.exception("No cookie file") + except Exception, e: + _moduleLogger.exception("Unknown error with cookies") + else: + self._loadedFromCookies = True + self._storeCookies = True + + return self._loadedFromCookies + + def save_cookies(self): + if self._storeCookies: + self._cookies.save() + + def clear_cookies(self): + if self._storeCookies: + self._cookies.clear() + + def download(self, url, + postdata = None, extraheaders = None, forbidRedirect = False, + trycount = None, only_head = False, + ): + """Download an URL with GET or POST methods. + + @param postdata: It can be a string that will be POST-ed to the URL. + When None is given, the method will be GET instead. + @param extraheaders: You can add/modify HTTP headers with a dict here. + @param forbidRedirect: Set this flag if you do not want to handle + HTTP 301 and 302 redirects. + @param trycount: Specify the maximum number of retries here. + 0 means no retry on error. Using -1 means infinite retring. + None means the default value (that is self.trycount). + @param only_head: Create the openerdirector and return it. In other + words, this will not retrieve any content except HTTP headers. + + @return: The raw HTML page data + """ + _moduleLogger.debug("Performing download of %s" % url) + + if extraheaders is None: + extraheaders = {} + if trycount is None: + trycount = self.trycount + cnt = 0 + + while True: + try: + req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) + openerdirector = u.open(req) + if self.debug: + _moduleLogger.info("%r - %r" % (req.get_method(), url)) + _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) + _moduleLogger.info("%r" % (openerdirector.headers)) + self._cookies.extract_cookies(openerdirector, req) + if only_head: + return openerdirector + + return self._read(openerdirector, trycount) + except urllib2.URLError, e: + _moduleLogger.debug("%s: %s" % (e, url)) + cnt += 1 + if (-1 < trycount) and (trycount < cnt): + raise + + # Retry :-) + _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt) + + def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): + if extraheaders is None: + extraheaders = {} + + txheaders = { + 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', + 'Accept-Language': 'en,en-us;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'User-Agent': self.USER_AGENT, + } + for key, value in extraheaders.iteritems(): + txheaders[key] = value + req = urllib2.Request(url, postdata, txheaders) + self._cookies.add_cookie_header(req) + if forbidRedirect: + redirector = HTTPNoRedirector() + #_moduleLogger.info("Redirection disabled") + else: + redirector = urllib2.HTTPRedirectHandler() + #_moduleLogger.info("Redirection enabled") + + http_handler = urllib2.HTTPHandler(debuglevel=self.debug) + https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) + + u = urllib2.build_opener( + http_handler, + https_handler, + urllib2.HTTPCookieProcessor(self._cookies), + redirector + ) + if not postdata is None: + req.add_data(postdata) + return (req, u) + + def _read(self, openerdirector, trycount): + chunks = [] + + chunk = openerdirector.read() + chunks.append(chunk) + #while chunk and cnt < trycount: + # time.sleep(1) + # cnt += 1 + # chunk = openerdirector.read() + # chunks.append(chunk) + + data = "".join(chunks) + + if "Content-Length" in openerdirector.info(): + assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( + openerdirector.info()["Content-Length"], + len(data), + ) + + return data + + +class HTTPNoRedirector(urllib2.HTTPRedirectHandler): + """This is a custom http redirect handler that FORBIDS redirection.""" + + def http_error_302(self, req, fp, code, msg, headers): + e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + if e.code in (301, 302): + if 'location' in headers: + newurl = headers.getheaders('location')[0] + elif 'uri' in headers: + newurl = headers.getheaders('uri')[0] + e.newurl = newurl + _moduleLogger.info("New url: %s" % e.newurl) + raise e diff --git a/dialcentral/backends/gvoice/gvoice.py b/dialcentral/backends/gvoice/gvoice.py new file mode 100755 index 0000000..b0825ef --- /dev/null +++ b/dialcentral/backends/gvoice/gvoice.py @@ -0,0 +1,1050 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Eric Warnke ericew AT gmail 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 + +Google Voice backend code + +Resources + http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ + http://posttopic.com/topic/google-voice-add-on-development +""" + +from __future__ import with_statement + +import os +import re +import urllib +import urllib2 +import time +import datetime +import itertools +import logging +import inspect + +from xml.sax import saxutils +from xml.etree import ElementTree + +try: + import simplejson as _simplejson + simplejson = _simplejson +except ImportError: + simplejson = None + +import browser_emu + + +_moduleLogger = logging.getLogger(__name__) + + +class NetworkError(RuntimeError): + pass + + +class MessageText(object): + + ACCURACY_LOW = "med1" + ACCURACY_MEDIUM = "med2" + ACCURACY_HIGH = "high" + + def __init__(self): + self.accuracy = None + self.text = None + + def __str__(self): + return self.text + + def to_dict(self): + return to_dict(self) + + def __eq__(self, other): + return self.accuracy == other.accuracy and self.text == other.text + + +class Message(object): + + def __init__(self): + self.whoFrom = None + self.body = None + self.when = None + + def __str__(self): + return "%s (%s): %s" % ( + self.whoFrom, + self.when, + "".join(unicode(part) for part in self.body) + ) + + def to_dict(self): + selfDict = to_dict(self) + selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None + return selfDict + + def __eq__(self, other): + return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body + + +class Conversation(object): + + TYPE_VOICEMAIL = "Voicemail" + TYPE_SMS = "SMS" + + def __init__(self): + self.type = None + self.id = None + self.contactId = None + self.name = None + self.location = None + self.prettyNumber = None + self.number = None + + self.time = None + self.relTime = None + self.messages = None + self.isRead = None + self.isSpam = None + self.isTrash = None + self.isArchived = None + + def __cmp__(self, other): + cmpValue = cmp(self.contactId, other.contactId) + if cmpValue != 0: + return cmpValue + + cmpValue = cmp(self.time, other.time) + if cmpValue != 0: + return cmpValue + + cmpValue = cmp(self.id, other.id) + if cmpValue != 0: + return cmpValue + + def to_dict(self): + selfDict = to_dict(self) + selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None + return selfDict + + +class GVoiceBackend(object): + """ + This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers + the functions include login, setting up a callback number, and initalting a callback + """ + + PHONE_TYPE_HOME = 1 + PHONE_TYPE_MOBILE = 2 + PHONE_TYPE_WORK = 3 + PHONE_TYPE_GIZMO = 7 + + def __init__(self, cookieFile = None): + # Important items in this function are the setup of the browser emulation and cookie file + self._browser = browser_emu.MozillaEmulator(1) + self._loadedFromCookies = self._browser.load_cookies(cookieFile) + + self._token = "" + self._accountNum = "" + self._lastAuthed = 0.0 + self._callbackNumber = "" + self._callbackNumbers = {} + + # Suprisingly, moving all of these from class to self sped up startup time + + self._validateRe = re.compile("^\+?[0-9]{10,}$") + + self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" + + SECURE_URL_BASE = "https://www.google.com/voice/" + SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/" + self._tokenURL = SECURE_URL_BASE + "m" + self._callUrl = SECURE_URL_BASE + "call/connect" + self._callCancelURL = SECURE_URL_BASE + "call/cancel" + self._sendSmsURL = SECURE_URL_BASE + "sms/send" + + self._isDndURL = "https://www.google.com/voice/m/donotdisturb" + self._isDndRe = re.compile(r"""""") + self._setDndURL = "https://www.google.com/voice/m/savednd" + + self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/" + self._markAsReadURL = SECURE_URL_BASE + "m/mark" + self._archiveMessageURL = SECURE_URL_BASE + "m/archive" + + self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/" + self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/" + # HACK really this redirects to the main pge and we are grabbing some javascript + self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact" + self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export" + self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user" + self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/" + + self.XML_FEEDS = ( + 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms', + 'recorded', 'placed', 'received', 'missed' + ) + self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox" + self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred" + self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all" + self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam" + self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash" + self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/" + self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/" + self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/" + self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread" + self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/" + self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/" + self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/" + self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/" + + self._galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) + + self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) + self._messagesContactIDRegex = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) + self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) + self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + + def is_quick_login_possible(self): + """ + @returns True then refresh_account_info might be enough to login, else full login is required + """ + return self._loadedFromCookies or 0.0 < self._lastAuthed + + def refresh_account_info(self): + try: + page = self._get_page(self._JSON_CONTACTS_URL) + accountData = self._grab_account_info(page) + except Exception, e: + _moduleLogger.exception(str(e)) + return None + + self._browser.save_cookies() + self._lastAuthed = time.time() + return accountData + + def _get_token(self): + tokenPage = self._get_page(self._tokenURL) + + galxTokens = self._galxRe.search(tokenPage) + if galxTokens is not None: + galxToken = galxTokens.group(1) + else: + galxToken = "" + _moduleLogger.debug("Could not grab GALX token") + return galxToken + + def _login(self, username, password, token): + loginData = { + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", + "GALX": token, + "continue": self._JSON_CONTACTS_URL, + } + + loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData) + return loginSuccessOrFailurePage + + def login(self, username, password): + """ + Attempt to login to GoogleVoice + @returns Whether login was successful or not + @blocks + """ + self.logout() + galxToken = self._get_token() + loginSuccessOrFailurePage = self._login(username, password, galxToken) + + try: + accountData = self._grab_account_info(loginSuccessOrFailurePage) + except Exception, e: + # Retry in case the redirect failed + # luckily refresh_account_info does everything we need for a retry + accountData = self.refresh_account_info() + if accountData is None: + _moduleLogger.exception(str(e)) + return None + _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this") + + self._browser.save_cookies() + self._lastAuthed = time.time() + return accountData + + def persist(self): + self._browser.save_cookies() + + def shutdown(self): + self._browser.save_cookies() + self._token = None + self._lastAuthed = 0.0 + + def logout(self): + self._browser.clear_cookies() + self._browser.save_cookies() + self._token = None + self._lastAuthed = 0.0 + self._callbackNumbers = {} + + def is_dnd(self): + """ + @blocks + """ + isDndPage = self._get_page(self._isDndURL) + + dndGroup = self._isDndRe.search(isDndPage) + if dndGroup is None: + return False + dndStatus = dndGroup.group(1) + isDnd = True if dndStatus.strip().lower() == "true" else False + return isDnd + + def set_dnd(self, doNotDisturb): + """ + @blocks + """ + dndPostData = { + "doNotDisturb": 1 if doNotDisturb else 0, + } + + dndPage = self._get_page_with_token(self._setDndURL, dndPostData) + + def call(self, outgoingNumber): + """ + This is the main function responsible for initating the callback + @blocks + """ + outgoingNumber = self._send_validation(outgoingNumber) + subscriberNumber = None + phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack + + callData = { + 'outgoingNumber': outgoingNumber, + 'forwardingNumber': self._callbackNumber, + 'subscriberNumber': subscriberNumber or 'undefined', + 'phoneType': str(phoneType), + 'remember': '1', + } + _moduleLogger.info("%r" % callData) + + page = self._get_page_with_token( + self._callUrl, + callData, + ) + self._parse_with_validation(page) + return True + + def cancel(self, outgoingNumber=None): + """ + Cancels a call matching outgoing and forwarding numbers (if given). + Will raise an error if no matching call is being placed + @blocks + """ + page = self._get_page_with_token( + self._callCancelURL, + { + 'outgoingNumber': outgoingNumber or 'undefined', + 'forwardingNumber': self._callbackNumber or 'undefined', + 'cancelType': 'C2C', + }, + ) + self._parse_with_validation(page) + + def send_sms(self, phoneNumbers, message): + """ + @blocks + """ + validatedPhoneNumbers = [ + self._send_validation(phoneNumber) + for phoneNumber in phoneNumbers + ] + flattenedPhoneNumbers = ",".join(validatedPhoneNumbers) + page = self._get_page_with_token( + self._sendSmsURL, + { + 'phoneNumber': flattenedPhoneNumbers, + 'text': unicode(message).encode("utf-8"), + }, + ) + self._parse_with_validation(page) + + def search(self, query): + """ + Search your Google Voice Account history for calls, voicemails, and sms + Returns ``Folder`` instance containting matching messages + @blocks + """ + page = self._get_page( + self._XML_SEARCH_URL, + {"q": query}, + ) + json, html = extract_payload(page) + return json + + def get_feed(self, feed): + """ + @blocks + """ + actualFeed = "_XML_%s_URL" % feed.upper() + feedUrl = getattr(self, actualFeed) + + page = self._get_page(feedUrl) + json, html = extract_payload(page) + + return json + + def recording_url(self, messageId): + url = self._downloadVoicemailURL+messageId + return url + + def download(self, messageId, targetPath): + """ + Download a voicemail or recorded call MP3 matching the given ``msg`` + which can either be a ``Message`` instance, or a SHA1 identifier. + Message hashes can be found in ``self.voicemail().messages`` for example. + @returns location of saved file. + @blocks + """ + page = self._get_page(self.recording_url(messageId)) + with open(targetPath, 'wb') as fo: + fo.write(page) + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return self._validateRe.match(number) is not None + + def get_account_number(self): + """ + @returns The GoogleVoice phone number + """ + return self._accountNum + + def get_callback_numbers(self): + """ + @returns a dictionary mapping call back numbers to descriptions + @note These results are cached for 30 minutes. + """ + return self._callbackNumbers + + def set_callback_number(self, callbacknumber): + """ + Set the number that GoogleVoice calls + @param callbacknumber should be a proper 10 digit number + """ + self._callbackNumber = callbacknumber + _moduleLogger.info("Callback number changed: %r" % self._callbackNumber) + return True + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + return self._callbackNumber + + def get_received_calls(self): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + @blocks + """ + return self._parse_recent(self._get_page(self._XML_RECEIVED_URL)) + + def get_missed_calls(self): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + @blocks + """ + return self._parse_recent(self._get_page(self._XML_MISSED_URL)) + + def get_placed_calls(self): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + @blocks + """ + return self._parse_recent(self._get_page(self._XML_PLACED_URL)) + + def get_csv_contacts(self): + data = { + "groupToExport": "mine", + "exportType": "ALL", + "out": "OUTLOOK_CSV", + } + encodedData = urllib.urlencode(data) + contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData) + return contacts + + def get_voicemails(self): + """ + @blocks + """ + voicemailPage = self._get_page(self._XML_VOICEMAIL_URL) + voicemailHtml = self._grab_html(voicemailPage) + voicemailJson = self._grab_json(voicemailPage) + if voicemailJson is None: + return () + parsedVoicemail = self._parse_voicemail(voicemailHtml) + voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson) + return voicemails + + def get_texts(self): + """ + @blocks + """ + smsPage = self._get_page(self._XML_SMS_URL) + smsHtml = self._grab_html(smsPage) + smsJson = self._grab_json(smsPage) + if smsJson is None: + return () + parsedSms = self._parse_sms(smsHtml) + smss = self._merge_conversation_sources(parsedSms, smsJson) + return smss + + def get_unread_counts(self): + countPage = self._get_page(self._JSON_SMS_COUNT_URL) + counts = parse_json(countPage) + counts = counts["unreadCounts"] + return counts + + def mark_message(self, messageId, asRead): + """ + @blocks + """ + postData = { + "read": 1 if asRead else 0, + "id": messageId, + } + + markPage = self._get_page(self._markAsReadURL, postData) + + def archive_message(self, messageId): + """ + @blocks + """ + postData = { + "id": messageId, + } + + markPage = self._get_page(self._archiveMessageURL, postData) + + def _grab_json(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + jsonElement = xmlTree.getchildren()[0] + flatJson = jsonElement.text + jsonTree = parse_json(flatJson) + return jsonTree + + def _grab_html(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + htmlElement = xmlTree.getchildren()[1] + flatHtml = htmlElement.text + return flatHtml + + def _grab_account_info(self, page): + accountData = parse_json(page) + self._token = accountData["r"] + self._accountNum = accountData["number"]["raw"] + for callback in accountData["phones"].itervalues(): + self._callbackNumbers[callback["phoneNumber"]] = callback["name"] + if len(self._callbackNumbers) == 0: + _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page) + return accountData + + def _send_validation(self, number): + if not self.is_valid_syntax(number): + raise ValueError('Number is not valid: "%s"' % number) + return number + + def _parse_recent(self, recentPage): + allRecentHtml = self._grab_html(recentPage) + allRecentData = self._parse_history(allRecentHtml) + for recentCallData in allRecentData: + yield recentCallData + + def _parse_history(self, historyHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = google_strptime(exactTime) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + location = locationGroup.group(1).strip() if locationGroup else "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup else "" + numberGroup = self._voicemailNumberRegex.search(messageHtml) + number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + yield { + "id": messageId.strip(), + "contactId": contactId, + "name": unescape(name), + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": unescape(location), + } + + @staticmethod + def _interpret_voicemail_regex(group): + quality, content, number = group.group(2), group.group(3), group.group(4) + text = MessageText() + if quality is not None and content is not None: + text.accuracy = quality + text.text = unescape(content) + return text + elif number is not None: + text.accuracy = MessageText.ACCURACY_HIGH + text.text = number + return text + + def _parse_voicemail(self, voicemailHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + conv = Conversation() + conv.type = Conversation.TYPE_VOICEMAIL + conv.id = messageId.strip() + + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + conv.time = google_strptime(exactTimeText) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "") + + nameGroup = self._voicemailNameRegex.search(messageHtml) + conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") + numberGroup = self._voicemailNumberRegex.search(messageHtml) + conv.number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + messageGroups = self._voicemailMessageRegex.finditer(messageHtml) + messageParts = [ + self._interpret_voicemail_regex(group) + for group in messageGroups + ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), ) + message = Message() + message.body = messageParts + message.whoFrom = conv.name + try: + message.when = conv.time.strftime("%I:%M %p") + except ValueError: + _moduleLogger.exception("Confusing time provided: %r" % conv.time) + message.when = "Unknown" + conv.messages = (message, ) + + yield conv + + @staticmethod + def _interpret_sms_message_parts(fromPart, textPart, timePart): + text = MessageText() + text.accuracy = MessageText.ACCURACY_MEDIUM + text.text = unescape(textPart) + + message = Message() + message.body = (text, ) + message.whoFrom = fromPart + message.when = timePart + + return message + + def _parse_sms(self, smsHtml): + splitSms = self._seperateVoicemailsRegex.split(smsHtml) + for messageId, messageHtml in itergroup(splitSms[1:], 2): + conv = Conversation() + conv.type = Conversation.TYPE_SMS + conv.id = messageId.strip() + + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + conv.time = google_strptime(exactTimeText) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + conv.location = "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") + numberGroup = self._voicemailNumberRegex.search(messageHtml) + conv.number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + fromGroups = self._smsFromRegex.finditer(messageHtml) + fromParts = (group.group(1).strip() for group in fromGroups) + textGroups = self._smsTextRegex.finditer(messageHtml) + textParts = (group.group(1).strip() for group in textGroups) + timeGroups = self._smsTimeRegex.finditer(messageHtml) + timeParts = (group.group(1).strip() for group in timeGroups) + + messageParts = itertools.izip(fromParts, textParts, timeParts) + messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts] + conv.messages = messages + + yield conv + + @staticmethod + def _merge_conversation_sources(parsedMessages, json): + for message in parsedMessages: + jsonItem = json["messages"][message.id] + message.isRead = jsonItem["isRead"] + message.isSpam = jsonItem["isSpam"] + message.isTrash = jsonItem["isTrash"] + message.isArchived = "inbox" not in jsonItem["labels"] + yield message + + def _get_page(self, url, data = None, refererUrl = None): + headers = {} + if refererUrl is not None: + headers["Referer"] = refererUrl + + encodedData = urllib.urlencode(data) if data is not None else None + + try: + page = self._browser.download(url, encodedData, None, headers) + except urllib2.URLError, e: + _moduleLogger.error("Translating error: %s" % str(e)) + raise NetworkError("%s is not accesible" % url) + + return page + + def _get_page_with_token(self, url, data = None, refererUrl = None): + if data is None: + data = {} + data['_rnr_se'] = self._token + + page = self._get_page(url, data, refererUrl) + + return page + + def _parse_with_validation(self, page): + json = parse_json(page) + self._validate_response(json) + return json + + def _validate_response(self, response): + """ + Validates that the JSON response is A-OK + """ + try: + assert response is not None, "Response not provided" + assert 'ok' in response, "Response lacks status" + assert response['ok'], "Response not good" + except AssertionError: + try: + if response["data"]["code"] == 20: + raise RuntimeError( +"""Ambiguous error 20 returned by Google Voice. +Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber) + except KeyError: + pass + raise RuntimeError('There was a problem with GV: %s' % response) + + +_UNESCAPE_ENTITIES = { + """: '"', + " ": " ", + "'": "'", +} + + +def unescape(text): + plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) + return plain + + +def google_strptime(time): + """ + Hack: Google always returns the time in the same locale. Sadly if the + local system's locale is different, there isn't a way to perfectly handle + the time. So instead we handle implement some time formatting + """ + abbrevTime = time[:-3] + parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M") + if time.endswith("PM"): + parsedTime += datetime.timedelta(hours=12) + return parsedTime + + +def itergroup(iterator, count, padValue = None): + """ + Iterate in groups of 'count' values. If there + aren't enough values, the last result is padded with + None. + + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print list(val) + [1, 2, 3] + [4, 5, 6] + >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + (7, None, None) + >>> for val in itergroup("123456", 3): + ... print tuple(val) + ('1', '2', '3') + ('4', '5', '6') + >>> for val in itergroup("123456", 3): + ... print repr("".join(val)) + '123' + '456' + """ + paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) + nIterators = (paddedIterator, ) * count + return itertools.izip(*nIterators) + + +def safe_eval(s): + _TRUE_REGEX = re.compile("true") + _FALSE_REGEX = re.compile("false") + _COMMENT_REGEX = re.compile("^\s+//.*$", re.M) + s = _TRUE_REGEX.sub("True", s) + s = _FALSE_REGEX.sub("False", s) + s = _COMMENT_REGEX.sub("#", s) + try: + results = eval(s, {}, {}) + except SyntaxError: + _moduleLogger.exception("Oops") + results = None + return results + + +def _fake_parse_json(flattened): + return safe_eval(flattened) + + +def _actual_parse_json(flattened): + return simplejson.loads(flattened) + + +if simplejson is None: + parse_json = _fake_parse_json +else: + parse_json = _actual_parse_json + + +def extract_payload(flatXml): + xmlTree = ElementTree.fromstring(flatXml) + + jsonElement = xmlTree.getchildren()[0] + flatJson = jsonElement.text + jsonTree = parse_json(flatJson) + + htmlElement = xmlTree.getchildren()[1] + flatHtml = htmlElement.text + + return jsonTree, flatHtml + + +def guess_phone_type(number): + if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): + return GVoiceBackend.PHONE_TYPE_GIZMO + else: + return GVoiceBackend.PHONE_TYPE_MOBILE + + +def get_sane_callback(backend): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + numbers = backend.get_callback_numbers() + + priorityOrderedCriteria = [ + ("\+1747", None), + ("1747", None), + ("747", None), + (None, "gizmo"), + (None, "computer"), + (None, "sip"), + (None, None), + ] + + for numberCriteria, descriptionCriteria in priorityOrderedCriteria: + numberMatcher = None + descriptionMatcher = None + if numberCriteria is not None: + numberMatcher = re.compile(numberCriteria) + elif descriptionCriteria is not None: + descriptionMatcher = re.compile(descriptionCriteria, re.I) + + for number, description in numbers.iteritems(): + if numberMatcher is not None and numberMatcher.match(number) is None: + continue + if descriptionMatcher is not None and descriptionMatcher.match(description) is None: + continue + return number + + +def set_sane_callback(backend): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + number = get_sane_callback(backend) + backend.set_callback_number(number) + + +def _is_not_special(name): + return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name + + +def to_dict(obj): + members = inspect.getmembers(obj) + return dict((name, value) for (name, value) in members if _is_not_special(name)) + + +def grab_debug_info(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVoiceBackend(cookieFile) + browser = backend._browser + + _TEST_WEBPAGES = [ + ("token", backend._tokenURL), + ("login", backend._loginURL), + ("isdnd", backend._isDndURL), + ("account", backend._XML_ACCOUNT_URL), + ("html_contacts", backend._XML_CONTACTS_URL), + ("contacts", backend._JSON_CONTACTS_URL), + ("csv", backend._CSV_CONTACTS_URL), + + ("voicemail", backend._XML_VOICEMAIL_URL), + ("html_sms", backend._XML_SMS_URL), + ("sms", backend._JSON_SMS_URL), + ("count", backend._JSON_SMS_COUNT_URL), + + ("recent", backend._XML_RECENT_URL), + ("placed", backend._XML_PLACED_URL), + ("recieved", backend._XML_RECEIVED_URL), + ("missed", backend._XML_MISSED_URL), + ] + + # Get Pages + print "Grabbing pre-login pages" + for name, url in _TEST_WEBPAGES: + try: + page = browser.download(url) + except StandardError, e: + print e.message + continue + print "\tWriting to file" + with open("not_loggedin_%s.txt" % name, "w") as f: + f.write(page) + + # Login + print "Attempting login" + galxToken = backend._get_token() + loginSuccessOrFailurePage = backend._login(username, password, galxToken) + with open("loggingin.txt", "w") as f: + print "\tWriting to file" + f.write(loginSuccessOrFailurePage) + try: + backend._grab_account_info(loginSuccessOrFailurePage) + except Exception: + # Retry in case the redirect failed + # luckily refresh_account_info does everything we need for a retry + loggedIn = backend.refresh_account_info() is not None + if not loggedIn: + raise + + # Get Pages + print "Grabbing post-login pages" + for name, url in _TEST_WEBPAGES: + try: + page = browser.download(url) + except StandardError, e: + print str(e) + continue + print "\tWriting to file" + with open("loggedin_%s.txt" % name, "w") as f: + f.write(page) + + # Cookies + browser.save_cookies() + print "\tWriting cookies to file" + with open("cookies.txt", "w") as f: + f.writelines( + "%s: %s\n" % (c.name, c.value) + for c in browser._cookies + ) + + +def grab_voicemails(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVoiceBackend(cookieFile) + backend.login(username, password) + voicemails = list(backend.get_voicemails()) + for voicemail in voicemails: + print voicemail.id + backend.download(voicemail.id, ".") + + +def main(): + import sys + logging.basicConfig(level=logging.DEBUG) + args = sys.argv + if 3 <= len(args): + username = args[1] + password = args[2] + + grab_debug_info(username, password) + grab_voicemails(username, password) + + +if __name__ == "__main__": + main() diff --git a/dialcentral/backends/null_backend.py b/dialcentral/backends/null_backend.py new file mode 100644 index 0000000..ebaa932 --- /dev/null +++ b/dialcentral/backends/null_backend.py @@ -0,0 +1,39 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Eric Warnke ericew AT gmail 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 +""" + + +class NullAddressBook(object): + + @property + def name(self): + return "None" + + def update_account(self, force = True): + pass + + def get_contacts(self): + return {} + + +class NullAddressBookFactory(object): + + def get_addressbooks(self): + yield NullAddressBook() diff --git a/dialcentral/backends/qt_backend.py b/dialcentral/backends/qt_backend.py new file mode 100644 index 0000000..88e52fa --- /dev/null +++ b/dialcentral/backends/qt_backend.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import logging + +import util.qt_compat as qt_compat +if qt_compat.USES_PYSIDE: + try: + import QtMobility.Contacts as _QtContacts + QtContacts = _QtContacts + except ImportError: + QtContacts = None +else: + QtContacts = None + +import null_backend + + +_moduleLogger = logging.getLogger(__name__) + + +class QtContactsAddressBook(object): + + def __init__(self, name, uri): + self._name = name + self._uri = uri + self._manager = QtContacts.QContactManager.fromUri(uri) + self._contacts = None + + @property + def name(self): + return self._name + + @property + def error(self): + return self._manager.error() + + def update_account(self, force = True): + if not force and self._contacts is not None: + return + self._contacts = dict(self._get_contacts()) + + def get_contacts(self): + if self._contacts is None: + self._contacts = dict(self._get_contacts()) + return self._contacts + + def _get_contacts(self): + contacts = self._manager.contacts() + for contact in contacts: + contactId = contact.localId() + contactName = contact.displayLabel() + phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName) + phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails] + contactDetails = phones + if 0 < len(contactDetails): + yield str(contactId), { + "contactId": str(contactId), + "name": contactName, + "numbers": contactDetails, + } + + +class _QtContactsAddressBookFactory(object): + + def __init__(self): + self._availableManagers = {} + + availableMgrs = QtContacts.QContactManager.availableManagers() + availableMgrs.remove("invalid") + for managerName in availableMgrs: + params = {} + managerUri = QtContacts.QContactManager.buildUri(managerName, params) + self._availableManagers[managerName] = managerUri + + def get_addressbooks(self): + for name, uri in self._availableManagers.iteritems(): + book = QtContactsAddressBook(name, uri) + if book.error: + _moduleLogger.info("Could not load %r due to %r" % (name, book.error)) + else: + yield book + + +class _EmptyAddressBookFactory(object): + + def get_addressbooks(self): + if False: + yield None + + +if QtContacts is not None: + QtContactsAddressBookFactory = _QtContactsAddressBookFactory +else: + QtContactsAddressBookFactory = _EmptyAddressBookFactory + _moduleLogger.info("QtContacts support not available") + + +if __name__ == "__main__": + factory = QtContactsAddressBookFactory() + books = factory.get_addressbooks() + for book in books: + print book.name + print book.get_contacts() diff --git a/dialcentral/call_handler.py b/dialcentral/call_handler.py new file mode 100644 index 0000000..9b9c47d --- /dev/null +++ b/dialcentral/call_handler.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import logging + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore +import dbus +try: + import telepathy as _telepathy + import util.tp_utils as telepathy_utils + telepathy = _telepathy +except ImportError: + telepathy = None + +import util.misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class _FakeSignaller(object): + + def start(self): + pass + + def stop(self): + pass + + +class _MissedCallWatcher(QtCore.QObject): + + callMissed = qt_compat.Signal() + + def __init__(self): + QtCore.QObject.__init__(self) + self._isStarted = False + self._isSupported = True + + self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel) + self._outstandingRequests = [] + + @property + def isSupported(self): + return self._isSupported + + @property + def isStarted(self): + return self._isStarted + + def start(self): + if self._isStarted: + _moduleLogger.info("voicemail monitor already started") + return + try: + self._newChannelSignaller.start() + except RuntimeError: + _moduleLogger.exception("Missed call detection not supported") + self._newChannelSignaller = _FakeSignaller() + self._isSupported = False + self._isStarted = True + + def stop(self): + if not self._isStarted: + _moduleLogger.info("voicemail monitor stopped without starting") + return + _moduleLogger.info("Stopping voicemail refresh") + self._newChannelSignaller.stop() + + # I don't want to trust whether the cancel happens within the current + # callback or not which could be the deciding factor between invalid + # iterators or infinite loops + localRequests = [r for r in self._outstandingRequests] + for request in localRequests: + localRequests.cancel() + + self._isStarted = False + + @misc_utils.log_exception(_moduleLogger) + def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType): + if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA: + return + + conn = telepathy.client.Connection(serviceName, connObjectPath) + try: + chan = telepathy.client.Channel(serviceName, channelObjectPath) + except dbus.exceptions.UnknownMethodException: + _moduleLogger.exception("Client might not have implemented a deprecated method") + return + missDetection = telepathy_utils.WasMissedCall( + bus, conn, chan, self._on_missed_call, self._on_error_for_missed + ) + self._outstandingRequests.append(missDetection) + + @misc_utils.log_exception(_moduleLogger) + def _on_missed_call(self, missDetection): + _moduleLogger.info("Missed a call") + self.callMissed.emit() + self._outstandingRequests.remove(missDetection) + + @misc_utils.log_exception(_moduleLogger) + def _on_error_for_missed(self, missDetection, reason): + _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason)) + self._outstandingRequests.remove(missDetection) + + +class _DummyMissedCallWatcher(QtCore.QObject): + + callMissed = qt_compat.Signal() + + def __init__(self): + QtCore.QObject.__init__(self) + self._isStarted = False + + @property + def isSupported(self): + return False + + @property + def isStarted(self): + return self._isStarted + + def start(self): + self._isStarted = True + + def stop(self): + if not self._isStarted: + _moduleLogger.info("voicemail monitor stopped without starting") + return + _moduleLogger.info("Stopping voicemail refresh") + self._isStarted = False + + +if telepathy is not None: + MissedCallWatcher = _MissedCallWatcher +else: + MissedCallWatcher = _DummyMissedCallWatcher + + +if __name__ == "__main__": + pass + diff --git a/dialcentral/constants.py b/dialcentral/constants.py new file mode 100644 index 0000000..b9d3c79 --- /dev/null +++ b/dialcentral/constants.py @@ -0,0 +1,13 @@ +import os + +__pretty_app_name__ = "DialCentral" +__app_name__ = "dialcentral" +__version__ = "1.3.6" +__build__ = 0 +__app_magic__ = 0xdeadbeef +_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__) +_user_settings_ = "%s/settings.ini" % _data_path_ +_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_ +_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__) +_notifier_logpath_ = "%s/notifier.log" % _data_path_ +IS_MAEMO = True diff --git a/dialcentral/dialcentral_qt.py b/dialcentral/dialcentral_qt.py new file mode 100755 index 0000000..a464ad6 --- /dev/null +++ b/dialcentral/dialcentral_qt.py @@ -0,0 +1,812 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +from __future__ import with_statement + +import os +import base64 +import ConfigParser +import functools +import logging +import logging.handlers + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +import constants +import alarm_handler +from util import qtpie +from util import qwrappers +from util import qui_utils +from util import misc as misc_utils + +import session + + +_moduleLogger = logging.getLogger(__name__) + + +class Dialcentral(qwrappers.ApplicationWrapper): + + _DATA_PATHS = [ + os.path.join(os.path.dirname(__file__), "../share"), + os.path.join(os.path.dirname(__file__), "../data"), + ] + + def __init__(self, app): + self._dataPath = None + self._aboutDialog = None + self.notifyOnMissed = False + self.notifyOnVoicemail = False + self.notifyOnSms = False + + self._streamHandler = None + self._ledHandler = None + self._alarmHandler = alarm_handler.AlarmHandler() + + qwrappers.ApplicationWrapper.__init__(self, app, constants) + + def load_settings(self): + try: + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + except IOError, e: + _moduleLogger.info("No settings") + return + except ValueError: + _moduleLogger.info("Settings were corrupt") + return + except ConfigParser.MissingSectionHeaderError: + _moduleLogger.info("Settings were corrupt") + return + except Exception: + _moduleLogger.exception("Unknown loading error") + + self._mainWindow.load_settings(config) + + def save_settings(self): + _moduleLogger.info("Saving settings") + config = ConfigParser.SafeConfigParser() + + self._mainWindow.save_settings(config) + + with open(constants._user_settings_, "wb") as configFile: + config.write(configFile) + + def get_icon(self, name): + if self._dataPath is None: + for path in self._DATA_PATHS: + if os.path.exists(os.path.join(path, name)): + self._dataPath = path + break + if self._dataPath is not None: + icon = QtGui.QIcon(os.path.join(self._dataPath, name)) + return icon + else: + return None + + def get_resource(self, name): + if self._dataPath is None: + for path in self._DATA_PATHS: + if os.path.exists(os.path.join(path, name)): + self._dataPath = path + break + if self._dataPath is not None: + return os.path.join(self._dataPath, name) + else: + return None + + def _close_windows(self): + qwrappers.ApplicationWrapper._close_windows(self) + if self._aboutDialog is not None: + self._aboutDialog.close() + + @property + def fsContactsPath(self): + return os.path.join(constants._data_path_, "contacts") + + @property + def streamHandler(self): + if self._streamHandler is None: + import stream_handler + self._streamHandler = stream_handler.StreamHandler() + return self._streamHandler + + @property + def alarmHandler(self): + return self._alarmHandler + + @property + def ledHandler(self): + if self._ledHandler is None: + import led_handler + self._ledHandler = led_handler.LedHandler() + return self._ledHandler + + def _new_main_window(self): + return MainWindow(None, self) + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_about(self, checked = True): + with qui_utils.notify_error(self._errorLog): + if self._aboutDialog is None: + import dialogs + self._aboutDialog = dialogs.AboutDialog(self) + response = self._aboutDialog.run(self._mainWindow.window) + + +class DelayedWidget(object): + + def __init__(self, app, settingsNames): + self._layout = QtGui.QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self._widget = QtGui.QWidget() + self._widget.setContentsMargins(0, 0, 0, 0) + self._widget.setLayout(self._layout) + self._settings = dict((name, "") for name in settingsNames) + + self._child = None + self._isEnabled = True + + @property + def toplevel(self): + return self._widget + + def has_child(self): + return self._child is not None + + def set_child(self, child): + if self._child is not None: + self._layout.removeWidget(self._child.toplevel) + self._child = child + if self._child is not None: + self._layout.addWidget(self._child.toplevel) + + self._child.set_settings(self._settings) + + if self._isEnabled: + self._child.enable() + else: + self._child.disable() + + @property + def child(self): + return self._child + + def enable(self): + self._isEnabled = True + if self._child is not None: + self._child.enable() + + def disable(self): + self._isEnabled = False + if self._child is not None: + self._child.disable() + + def clear(self): + if self._child is not None: + self._child.clear() + + def refresh(self, force=True): + if self._child is not None: + self._child.refresh(force) + + def get_settings(self): + if self._child is not None: + return self._child.get_settings() + else: + return self._settings + + def set_settings(self, settings): + if self._child is not None: + self._child.set_settings(settings) + else: + self._settings = settings + + +def _tab_factory(tab, app, session, errorLog): + import gv_views + return gv_views.__dict__[tab](app, session, errorLog) + + +class MainWindow(qwrappers.WindowWrapper): + + KEYPAD_TAB = 0 + RECENT_TAB = 1 + MESSAGES_TAB = 2 + CONTACTS_TAB = 3 + MAX_TABS = 4 + + _TAB_TITLES = [ + "Dialpad", + "History", + "Messages", + "Contacts", + ] + assert len(_TAB_TITLES) == MAX_TABS + + _TAB_ICONS = [ + "dialpad.png", + "history.png", + "messages.png", + "contacts.png", + ] + assert len(_TAB_ICONS) == MAX_TABS + + _TAB_CLASS = [ + functools.partial(_tab_factory, "Dialpad"), + functools.partial(_tab_factory, "History"), + functools.partial(_tab_factory, "Messages"), + functools.partial(_tab_factory, "Contacts"), + ] + assert len(_TAB_CLASS) == MAX_TABS + + # Hack to allow delay importing/loading of tabs + _TAB_SETTINGS_NAMES = [ + (), + ("filter", ), + ("status", "type"), + ("selectedAddressbook", ), + ] + assert len(_TAB_SETTINGS_NAMES) == MAX_TABS + + def __init__(self, parent, app): + qwrappers.WindowWrapper.__init__(self, parent, app) + self._window.setWindowTitle("%s" % constants.__pretty_app_name__) + self._window.resized.connect(self._on_window_resized) + self._errorLog = self._app.errorLog + + self._session = session.Session(self._errorLog, constants._data_path_) + self._session.error.connect(self._on_session_error) + self._session.loggedIn.connect(self._on_login) + self._session.loggedOut.connect(self._on_logout) + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) + self._session.newMessages.connect(self._on_new_message_alert) + self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert) + self._voicemailRefreshDelay = QtCore.QTimer() + self._voicemailRefreshDelay.setInterval(30 * 1000) + self._voicemailRefreshDelay.timeout.connect(self._on_call_missed) + self._voicemailRefreshDelay.setSingleShot(True) + self._callHandler = None + self._updateVoicemailOnMissedCall = False + + self._defaultCredentials = "", "" + self._curentCredentials = "", "" + self._currentTab = 0 + + self._credentialsDialog = None + self._smsEntryDialog = None + self._accountDialog = None + + self._tabsContents = [ + DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i]) + for i in xrange(self.MAX_TABS) + ] + for tab in self._tabsContents: + tab.disable() + + self._tabWidget = QtGui.QTabWidget() + if qui_utils.screen_orientation() == QtCore.Qt.Vertical: + self._tabWidget.setTabPosition(QtGui.QTabWidget.South) + else: + self._tabWidget.setTabPosition(QtGui.QTabWidget.West) + defaultTabIconSize = self._tabWidget.iconSize() + defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height() + for tabIndex, (tabTitle, tabIcon) in enumerate( + zip(self._TAB_TITLES, self._TAB_ICONS) + ): + icon = self._app.get_icon(tabIcon) + if constants.IS_MAEMO and icon is not None: + tabTitle = "" + + if icon is None: + self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle) + else: + iconSize = icon.availableSizes()[0] + defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width()) + defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height()) + self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle) + defaultTabIconWidth = max(defaultTabIconWidth, 32) + defaultTabIconHeight = max(defaultTabIconHeight, 32) + self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight)) + self._tabWidget.currentChanged.connect(self._on_tab_changed) + self._tabWidget.setContentsMargins(0, 0, 0, 0) + + self._layout.addWidget(self._tabWidget) + + self._loginAction = QtGui.QAction(None) + self._loginAction.setText("Login") + self._loginAction.triggered.connect(self._on_login_requested) + + self._importAction = QtGui.QAction(None) + self._importAction.setText("Import") + self._importAction.triggered.connect(self._on_import) + + self._accountAction = QtGui.QAction(None) + self._accountAction.setText("Account") + self._accountAction.triggered.connect(self._on_account) + + self._refreshConnectionAction = QtGui.QAction(None) + self._refreshConnectionAction.setText("Refresh Connection") + self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a")) + self._refreshConnectionAction.triggered.connect(self._on_refresh_connection) + + self._refreshTabAction = QtGui.QAction(None) + self._refreshTabAction.setText("Refresh Tab") + self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r")) + self._refreshTabAction.triggered.connect(self._on_refresh) + + fileMenu = self._window.menuBar().addMenu("&File") + fileMenu.addAction(self._loginAction) + fileMenu.addAction(self._refreshTabAction) + fileMenu.addAction(self._refreshConnectionAction) + + toolsMenu = self._window.menuBar().addMenu("&Tools") + toolsMenu.addAction(self._accountAction) + toolsMenu.addAction(self._importAction) + toolsMenu.addAction(self._app.aboutAction) + + self._initialize_tab(self._tabWidget.currentIndex()) + self.set_fullscreen(self._app.fullscreenAction.isChecked()) + self.update_orientation(self._app.orientation) + + def _init_call_handler(self): + if self._callHandler is not None: + return + import call_handler + self._callHandler = call_handler.MissedCallWatcher() + self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start) + + def set_default_credentials(self, username, password): + self._defaultCredentials = username, password + + def get_default_credentials(self): + return self._defaultCredentials + + def walk_children(self): + if self._smsEntryDialog is not None: + return (self._smsEntryDialog, ) + else: + return () + + def start(self): + qwrappers.WindowWrapper.start(self) + assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up" + if self._defaultCredentials != ("", ""): + username, password = self._defaultCredentials[0], self._defaultCredentials[1] + self._curentCredentials = username, password + self._session.login(username, password) + else: + self._prompt_for_login() + + def close(self): + for diag in ( + self._credentialsDialog, + self._accountDialog, + ): + if diag is not None: + diag.close() + for child in self.walk_children(): + child.window.destroyed.disconnect(self._on_child_close) + child.window.closed.disconnect(self._on_child_close) + child.close() + self._window.close() + + def destroy(self): + qwrappers.WindowWrapper.destroy(self) + if self._session.state != self._session.LOGGEDOUT_STATE: + self._session.logout() + + def get_current_tab(self): + return self._currentTab + + def set_current_tab(self, tabIndex): + self._tabWidget.setCurrentIndex(tabIndex) + + def load_settings(self, config): + blobs = "", "" + isFullscreen = False + orientation = self._app.orientation + tabIndex = 0 + try: + blobs = [ + config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) + for i in xrange(len(self.get_default_credentials())) + ] + isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen") + tabIndex = config.getint(constants.__pretty_app_name__, "tab") + orientation = config.get(constants.__pretty_app_name__, "orientation") + except ConfigParser.NoOptionError, e: + _moduleLogger.info( + "Settings file %s is missing option %s" % ( + constants._user_settings_, + e.option, + ), + ) + except ConfigParser.NoSectionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + except Exception: + _moduleLogger.exception("Unknown loading error") + + try: + self._app.alarmHandler.load_settings(config, "alarm") + self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed") + self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail") + self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms") + self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall") + except ConfigParser.NoOptionError, e: + _moduleLogger.info( + "Settings file %s is missing option %s" % ( + constants._user_settings_, + e.option, + ), + ) + except ConfigParser.NoSectionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + except Exception: + _moduleLogger.exception("Unknown loading error") + + creds = ( + base64.b64decode(blob) + for blob in blobs + ) + self.set_default_credentials(*creds) + self._app.fullscreenAction.setChecked(isFullscreen) + self._app.set_orientation(orientation) + self.set_current_tab(tabIndex) + + backendId = 2 # For backwards compatibility + for tabIndex, tabTitle in enumerate(self._TAB_TITLES): + sectionName = "%s - %s" % (backendId, tabTitle) + settings = self._tabsContents[tabIndex].get_settings() + for settingName in settings.iterkeys(): + try: + settingValue = config.get(sectionName, settingName) + except ConfigParser.NoOptionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + return + except ConfigParser.NoSectionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + return + except Exception: + _moduleLogger.exception("Unknown loading error") + return + settings[settingName] = settingValue + self._tabsContents[tabIndex].set_settings(settings) + + def save_settings(self, config): + config.add_section(constants.__pretty_app_name__) + config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab())) + config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked())) + config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation)) + for i, value in enumerate(self.get_default_credentials()): + blob = base64.b64encode(value) + config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob) + + config.add_section("alarm") + self._app.alarmHandler.save_settings(config, "alarm") + config.add_section("2 - Account Info") + config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed)) + config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail)) + config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms)) + config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall)) + + backendId = 2 # For backwards compatibility + for tabIndex, tabTitle in enumerate(self._TAB_TITLES): + sectionName = "%s - %s" % (backendId, tabTitle) + config.add_section(sectionName) + tabSettings = self._tabsContents[tabIndex].get_settings() + for settingName, settingValue in tabSettings.iteritems(): + config.set(sectionName, settingName, settingValue) + + def update_orientation(self, orientation): + qwrappers.WindowWrapper.update_orientation(self, orientation) + windowOrientation = self.idealWindowOrientation + if windowOrientation == QtCore.Qt.Horizontal: + self._tabWidget.setTabPosition(QtGui.QTabWidget.West) + else: + self._tabWidget.setTabPosition(QtGui.QTabWidget.South) + + def _initialize_tab(self, index): + assert index < self.MAX_TABS, "Invalid tab" + if not self._tabsContents[index].has_child(): + tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog) + self._tabsContents[index].set_child(tab) + self._tabsContents[index].refresh(force=False) + + def _prompt_for_login(self): + if self._credentialsDialog is None: + import dialogs + self._credentialsDialog = dialogs.CredentialsDialog(self._app) + credentials = self._credentialsDialog.run( + self._defaultCredentials[0], self._defaultCredentials[1], self.window + ) + if credentials is None: + return + username, password = credentials + self._curentCredentials = username, password + self._session.login(username, password) + + def _show_account_dialog(self): + if self._accountDialog is None: + import dialogs + self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog) + self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported) + self._accountDialog.settingsApproved.connect(self._on_settings_approved) + + if self._callHandler is not None and not self._callHandler.isSupported: + self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED + elif self._updateVoicemailOnMissedCall: + self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED + else: + self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED + self._accountDialog.notifications = self._app.alarmHandler.alarmType + self._accountDialog.notificationTime = self._app.alarmHandler.recurrence + self._accountDialog.notifyOnMissed = self._app.notifyOnMissed + self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail + self._accountDialog.notifyOnSms = self._app.notifyOnSms + self._accountDialog.set_callbacks( + self._session.get_callback_numbers(), self._session.get_callback_number() + ) + accountNumberToDisplay = self._session.get_account_number() + if not accountNumberToDisplay: + accountNumberToDisplay = "Not Available (%s)" % self._session.state + self._accountDialog.set_account_number(accountNumberToDisplay) + self._accountDialog.orientation = self._app.orientation + + self._accountDialog.run() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_settings_approved(self): + if self._accountDialog.doClear: + self._session.logout_and_clear() + self._defaultCredentials = "", "" + self._curentCredentials = "", "" + for tab in self._tabsContents: + tab.disable() + else: + callbackNumber = self._accountDialog.selectedCallback + self._session.set_callback_number(callbackNumber) + + if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED: + pass + elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED: + self._updateVoicemailOnMissedCall = True + self._init_call_handler() + self._callHandler.start() + else: + self._updateVoicemailOnMissedCall = False + if self._callHandler is not None: + self._callHandler.stop() + if ( + self._accountDialog.notifyOnMissed or + self._accountDialog.notifyOnVoicemail or + self._accountDialog.notifyOnSms + ): + notifications = self._accountDialog.notifications + else: + notifications = self._accountDialog.ALARM_NONE + self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime) + + self._app.notifyOnMissed = self._accountDialog.notifyOnMissed + self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail + self._app.notifyOnSms = self._accountDialog.notifyOnSms + self._app.set_orientation(self._accountDialog.orientation) + self._app.save_settings() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_window_resized(self): + with qui_utils.notify_error(self._app.errorLog): + windowOrientation = self.idealWindowOrientation + if windowOrientation == QtCore.Qt.Horizontal: + self._tabWidget.setTabPosition(QtGui.QTabWidget.West) + else: + self._tabWidget.setTabPosition(QtGui.QTabWidget.South) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_new_message_alert(self): + with qui_utils.notify_error(self._errorLog): + if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION: + if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal: + self._errorLog.push_message("New messages") + else: + self._app.ledHandler.on() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_call_missed(self): + with qui_utils.notify_error(self._errorLog): + self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True) + + @qt_compat.Slot(str) + @misc_utils.log_exception(_moduleLogger) + def _on_session_error(self, message): + with qui_utils.notify_error(self._errorLog): + self._errorLog.push_error(message) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_login(self): + with qui_utils.notify_error(self._errorLog): + changedAccounts = self._defaultCredentials != self._curentCredentials + noCallback = not self._session.get_callback_number() + if changedAccounts or noCallback: + self._show_account_dialog() + + self._defaultCredentials = self._curentCredentials + + for tab in self._tabsContents: + tab.enable() + self._initialize_tab(self._currentTab) + if self._updateVoicemailOnMissedCall: + self._init_call_handler() + self._callHandler.start() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_logout(self): + with qui_utils.notify_error(self._errorLog): + for tab in self._tabsContents: + tab.disable() + if self._callHandler is not None: + self._callHandler.stop() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_app_alert(self): + with qui_utils.notify_error(self._errorLog): + if self._session.state == self._session.LOGGEDIN_STATE: + messageType = { + (True, True): self._session.MESSAGE_ALL, + (True, False): self._session.MESSAGE_TEXTS, + (False, True): self._session.MESSAGE_VOICEMAILS, + }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)] + self._session.update_messages(messageType, force=True) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + with qui_utils.notify_error(self._errorLog): + if self._session.draft.get_num_contacts() == 0: + return + + if self._smsEntryDialog is None: + import dialogs + self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog) + self._smsEntryDialog.window.destroyed.connect(self._on_child_close) + self._smsEntryDialog.window.closed.connect(self._on_child_close) + self._smsEntryDialog.window.show() + + @misc_utils.log_exception(_moduleLogger) + def _on_child_close(self, obj = None): + self._smsEntryDialog = None + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_login_requested(self, checked = True): + with qui_utils.notify_error(self._errorLog): + self._prompt_for_login() + + @qt_compat.Slot(int) + @misc_utils.log_exception(_moduleLogger) + def _on_tab_changed(self, index): + with qui_utils.notify_error(self._errorLog): + self._currentTab = index + self._initialize_tab(index) + if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION: + self._app.ledHandler.off() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_refresh(self, checked = True): + with qui_utils.notify_error(self._errorLog): + self._tabsContents[self._currentTab].refresh(force=True) + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_connection(self, checked = True): + with qui_utils.notify_error(self._errorLog): + self._session.refresh_connection() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_import(self, checked = True): + with qui_utils.notify_error(self._errorLog): + csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)") + csvName = unicode(csvName) + if not csvName: + return + import shutil + shutil.copy2(csvName, self._app.fsContactsPath) + if self._tabsContents[self.CONTACTS_TAB].has_child: + self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_account(self, checked = True): + with qui_utils.notify_error(self._errorLog): + assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings" + self._show_account_dialog() + + +def run(): + try: + os.makedirs(constants._data_path_) + except OSError, e: + if e.errno != 17: + raise + + logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' + logging.basicConfig(level=logging.DEBUG, format=logFormat) + rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1) + rotating.setFormatter(logging.Formatter(logFormat)) + root = logging.getLogger() + root.addHandler(rotating) + _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__)) + _moduleLogger.info("OS: %s" % (os.uname()[0], )) + _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:]) + _moduleLogger.info("Hostname: %s" % os.uname()[1]) + + try: + import gobject + gobject.threads_init() + except ImportError: + _moduleLogger.info("GObject support not available") + try: + import dbus + try: + from dbus.mainloop.qt import DBusQtMainLoop + DBusQtMainLoop(set_as_default=True) + _moduleLogger.info("Using Qt mainloop") + except ImportError: + try: + from dbus.mainloop.glib import DBusGMainLoop + DBusGMainLoop(set_as_default=True) + _moduleLogger.info("Using GObject mainloop") + except ImportError: + _moduleLogger.info("Mainloop not available") + except ImportError: + _moduleLogger.info("DBus support not available") + + app = QtGui.QApplication([]) + handle = Dialcentral(app) + qtpie.init_pies() + return app.exec_() + + +if __name__ == "__main__": + import sys + + val = run() + sys.exit(val) diff --git a/dialcentral/dialogs.py b/dialcentral/dialogs.py new file mode 100644 index 0000000..8fbf328 --- /dev/null +++ b/dialcentral/dialogs.py @@ -0,0 +1,1192 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import functools +import copy +import logging + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +import constants +from util import qwrappers +from util import qui_utils +from util import misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class CredentialsDialog(object): + + def __init__(self, app): + self._app = app + self._usernameField = QtGui.QLineEdit() + self._passwordField = QtGui.QLineEdit() + self._passwordField.setEchoMode(QtGui.QLineEdit.Password) + + self._credLayout = QtGui.QGridLayout() + self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0) + self._credLayout.addWidget(self._usernameField, 0, 1) + self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0) + self._credLayout.addWidget(self._passwordField, 1, 1) + + self._loginButton = QtGui.QPushButton("&Login") + self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel) + self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole) + + self._layout = QtGui.QVBoxLayout() + self._layout.addLayout(self._credLayout) + self._layout.addWidget(self._buttonLayout) + + self._dialog = QtGui.QDialog() + self._dialog.setWindowTitle("Login") + self._dialog.setLayout(self._layout) + self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + self._buttonLayout.accepted.connect(self._dialog.accept) + self._buttonLayout.rejected.connect(self._dialog.reject) + + self._closeWindowAction = QtGui.QAction(None) + self._closeWindowAction.setText("Close") + self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) + self._closeWindowAction.triggered.connect(self._on_close_window) + + self._dialog.addAction(self._closeWindowAction) + self._dialog.addAction(app.quitAction) + self._dialog.addAction(app.fullscreenAction) + + def run(self, defaultUsername, defaultPassword, parent=None): + self._dialog.setParent(parent, QtCore.Qt.Dialog) + try: + self._usernameField.setText(defaultUsername) + self._passwordField.setText(defaultPassword) + + response = self._dialog.exec_() + if response == QtGui.QDialog.Accepted: + return str(self._usernameField.text()), str(self._passwordField.text()) + elif response == QtGui.QDialog.Rejected: + return None + else: + _moduleLogger.error("Unknown response") + return None + finally: + self._dialog.setParent(None, QtCore.Qt.Dialog) + + def close(self): + try: + self._dialog.reject() + except RuntimeError: + _moduleLogger.exception("Oh well") + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_close_window(self, checked = True): + with qui_utils.notify_error(self._app.errorLog): + self._dialog.reject() + + +class AboutDialog(object): + + def __init__(self, app): + self._app = app + self._title = QtGui.QLabel( + "

%s

Version: %s

" % ( + constants.__pretty_app_name__, constants.__version__ + ) + ) + self._title.setTextFormat(QtCore.Qt.RichText) + self._title.setAlignment(QtCore.Qt.AlignCenter) + self._copyright = QtGui.QLabel("
Developed by Ed Page
Icons: See website
") + self._copyright.setTextFormat(QtCore.Qt.RichText) + self._copyright.setAlignment(QtCore.Qt.AlignCenter) + self._link = QtGui.QLabel('DialCentral Website') + self._link.setTextFormat(QtCore.Qt.RichText) + self._link.setAlignment(QtCore.Qt.AlignCenter) + self._link.setOpenExternalLinks(True) + + self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel) + + self._layout = QtGui.QVBoxLayout() + self._layout.addWidget(self._title) + self._layout.addWidget(self._copyright) + self._layout.addWidget(self._link) + self._layout.addWidget(self._buttonLayout) + + self._dialog = QtGui.QDialog() + self._dialog.setWindowTitle("About") + self._dialog.setLayout(self._layout) + self._buttonLayout.rejected.connect(self._dialog.reject) + + self._closeWindowAction = QtGui.QAction(None) + self._closeWindowAction.setText("Close") + self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) + self._closeWindowAction.triggered.connect(self._on_close_window) + + self._dialog.addAction(self._closeWindowAction) + self._dialog.addAction(app.quitAction) + self._dialog.addAction(app.fullscreenAction) + + def run(self, parent=None): + self._dialog.setParent(parent, QtCore.Qt.Dialog) + + response = self._dialog.exec_() + return response + + def close(self): + try: + self._dialog.reject() + except RuntimeError: + _moduleLogger.exception("Oh well") + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_close_window(self, checked = True): + with qui_utils.notify_error(self._app.errorLog): + self._dialog.reject() + + +class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper): + + # @bug Can't enter custom callback numbers + + _RECURRENCE_CHOICES = [ + (1, "1 minute"), + (2, "2 minutes"), + (3, "3 minutes"), + (5, "5 minutes"), + (8, "8 minutes"), + (10, "10 minutes"), + (15, "15 minutes"), + (30, "30 minutes"), + (45, "45 minutes"), + (60, "1 hour"), + (3*60, "3 hours"), + (6*60, "6 hours"), + (12*60, "12 hours"), + ] + + ALARM_NONE = "No Alert" + ALARM_BACKGROUND = "Background Alert" + ALARM_APPLICATION = "Application Alert" + + VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported" + VOICEMAIL_CHECK_DISABLED = "Disabled" + VOICEMAIL_CHECK_ENABLED = "Enabled" + + settingsApproved = qt_compat.Signal() + + def __init__(self, parent, app, errorLog): + QtCore.QObject.__init__(self) + qwrappers.WindowWrapper.__init__(self, parent, app) + self._app = app + self._doClear = False + + self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET") + self._notificationSelecter = QtGui.QComboBox() + self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change) + self._notificationTimeSelector = QtGui.QComboBox() + #self._notificationTimeSelector.setEditable(True) + self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop) + for _, label in self._RECURRENCE_CHOICES: + self._notificationTimeSelector.addItem(label) + self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls") + self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail") + self._smsNotificationButton = QtGui.QCheckBox("SMS") + self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls") + self._clearButton = QtGui.QPushButton("Clear Account") + self._clearButton.clicked.connect(self._on_clear) + self._callbackSelector = QtGui.QComboBox() + #self._callbackSelector.setEditable(True) + self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop) + self._orientationSelector = QtGui.QComboBox() + for orientationMode in [ + self._app.DEFAULT_ORIENTATION, + self._app.AUTO_ORIENTATION, + self._app.LANDSCAPE_ORIENTATION, + self._app.PORTRAIT_ORIENTATION, + ]: + self._orientationSelector.addItem(orientationMode) + + self._update_notification_state() + + self._credLayout = QtGui.QGridLayout() + self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0) + self._credLayout.addWidget(self._accountNumberLabel, 0, 1) + self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0) + self._credLayout.addWidget(self._callbackSelector, 1, 1) + self._credLayout.addWidget(self._notificationSelecter, 2, 0) + self._credLayout.addWidget(self._notificationTimeSelector, 2, 1) + self._credLayout.addWidget(QtGui.QLabel(""), 3, 0) + self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1) + self._credLayout.addWidget(QtGui.QLabel(""), 4, 0) + self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1) + self._credLayout.addWidget(QtGui.QLabel(""), 5, 0) + self._credLayout.addWidget(self._smsNotificationButton, 5, 1) + self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0) + self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1) + self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0) + self._credLayout.addWidget(self._orientationSelector, 7, 1) + self._credLayout.addWidget(QtGui.QLabel(""), 8, 0) + self._credLayout.addWidget(QtGui.QLabel(""), 9, 0) + self._credLayout.addWidget(self._clearButton, 9, 1) + + self._credWidget = QtGui.QWidget() + self._credWidget.setLayout(self._credLayout) + self._credWidget.setContentsMargins(0, 0, 0, 0) + self._scrollSettings = QtGui.QScrollArea() + self._scrollSettings.setWidget(self._credWidget) + self._scrollSettings.setWidgetResizable(True) + self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + self._applyButton = QtGui.QPushButton("&Apply") + self._applyButton.clicked.connect(self._on_settings_apply) + self._cancelButton = QtGui.QPushButton("&Cancel") + self._cancelButton.clicked.connect(self._on_settings_cancel) + self._buttonLayout = QtGui.QHBoxLayout() + self._buttonLayout.addStretch() + self._buttonLayout.addWidget(self._cancelButton) + self._buttonLayout.addStretch() + self._buttonLayout.addWidget(self._applyButton) + self._buttonLayout.addStretch() + + self._layout.addWidget(self._scrollSettings) + self._layout.addLayout(self._buttonLayout) + self._layout.setDirection(QtGui.QBoxLayout.TopToBottom) + + self._window.setWindowTitle("Account") + self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + + @property + def doClear(self): + return self._doClear + + def setIfNotificationsSupported(self, isSupported): + if isSupported: + self._notificationSelecter.clear() + self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND]) + self._notificationTimeSelector.setEnabled(False) + self._missedCallsNotificationButton.setEnabled(False) + self._voicemailNotificationButton.setEnabled(False) + self._smsNotificationButton.setEnabled(False) + else: + self._notificationSelecter.clear() + self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION]) + self._notificationTimeSelector.setEnabled(False) + self._missedCallsNotificationButton.setEnabled(False) + self._voicemailNotificationButton.setEnabled(False) + self._smsNotificationButton.setEnabled(False) + + def set_account_number(self, num): + self._accountNumberLabel.setText(num) + + orientation = property( + lambda self: str(self._orientationSelector.currentText()), + lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode), + ) + + def _set_voicemail_on_missed(self, status): + if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED: + self._voicemailOnMissedButton.setChecked(False) + self._voicemailOnMissedButton.hide() + elif status == self.VOICEMAIL_CHECK_DISABLED: + self._voicemailOnMissedButton.setChecked(False) + self._voicemailOnMissedButton.show() + elif status == self.VOICEMAIL_CHECK_ENABLED: + self._voicemailOnMissedButton.setChecked(True) + self._voicemailOnMissedButton.show() + else: + raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status) + + def _get_voicemail_on_missed(self): + if not self._voicemailOnMissedButton.isVisible(): + return self.VOICEMAIL_CHECK_NOT_SUPPORTED + elif self._voicemailOnMissedButton.isChecked(): + return self.VOICEMAIL_CHECK_ENABLED + else: + return self.VOICEMAIL_CHECK_DISABLED + + updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed) + + notifications = property( + lambda self: str(self._notificationSelecter.currentText()), + lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled), + ) + + notifyOnMissed = property( + lambda self: self._missedCallsNotificationButton.isChecked(), + lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled), + ) + + notifyOnVoicemail = property( + lambda self: self._voicemailNotificationButton.isChecked(), + lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled), + ) + + notifyOnSms = property( + lambda self: self._smsNotificationButton.isChecked(), + lambda self, enabled: self._smsNotificationButton.setChecked(enabled), + ) + + def _get_notification_time(self): + index = self._notificationTimeSelector.currentIndex() + minutes = self._RECURRENCE_CHOICES[index][0] + return minutes + + def _set_notification_time(self, minutes): + for i, (time, _) in enumerate(self._RECURRENCE_CHOICES): + if time == minutes: + self._notificationTimeSelector.setCurrentIndex(i) + break + else: + self._notificationTimeSelector.setCurrentIndex(0) + + notificationTime = property(_get_notification_time, _set_notification_time) + + @property + def selectedCallback(self): + index = self._callbackSelector.currentIndex() + data = str(self._callbackSelector.itemData(index)) + return data + + def set_callbacks(self, choices, default): + self._callbackSelector.clear() + + self._callbackSelector.addItem("Not Set", "") + + uglyDefault = misc_utils.make_ugly(default) + if not uglyDefault: + uglyDefault = default + for number, description in choices.iteritems(): + prettyNumber = misc_utils.make_pretty(number) + uglyNumber = misc_utils.make_ugly(number) + if not uglyNumber: + prettyNumber = number + uglyNumber = number + + self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber) + if uglyNumber == uglyDefault: + self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1) + + def run(self): + self._doClear = False + self._window.show() + + def close(self): + try: + self._window.hide() + except RuntimeError: + _moduleLogger.exception("Oh well") + + def _update_notification_state(self): + currentText = str(self._notificationSelecter.currentText()) + if currentText == self.ALARM_BACKGROUND: + self._notificationTimeSelector.setEnabled(True) + + self._missedCallsNotificationButton.setEnabled(True) + self._voicemailNotificationButton.setEnabled(True) + self._smsNotificationButton.setEnabled(True) + elif currentText == self.ALARM_APPLICATION: + self._notificationTimeSelector.setEnabled(True) + + self._missedCallsNotificationButton.setEnabled(False) + self._voicemailNotificationButton.setEnabled(True) + self._smsNotificationButton.setEnabled(True) + + self._missedCallsNotificationButton.setChecked(False) + else: + self._notificationTimeSelector.setEnabled(False) + + self._missedCallsNotificationButton.setEnabled(False) + self._voicemailNotificationButton.setEnabled(False) + self._smsNotificationButton.setEnabled(False) + + self._missedCallsNotificationButton.setChecked(False) + self._voicemailNotificationButton.setChecked(False) + self._smsNotificationButton.setChecked(False) + + @qt_compat.Slot(int) + @misc_utils.log_exception(_moduleLogger) + def _on_notification_change(self, index): + with qui_utils.notify_error(self._app.errorLog): + self._update_notification_state() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_settings_cancel(self, checked = False): + with qui_utils.notify_error(self._app.errorLog): + self.hide() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + def _on_settings_apply(self, checked = False): + self.__on_settings_apply(checked) + + @misc_utils.log_exception(_moduleLogger) + def __on_settings_apply(self, checked = False): + with qui_utils.notify_error(self._app.errorLog): + self.settingsApproved.emit() + self.hide() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_clear(self, checked = False): + with qui_utils.notify_error(self._app.errorLog): + self._doClear = True + self.settingsApproved.emit() + self.hide() + + +class ContactList(object): + + _SENTINEL_ICON = QtGui.QIcon() + + def __init__(self, app, session): + self._app = app + self._session = session + self._targetLayout = QtGui.QVBoxLayout() + self._targetList = QtGui.QWidget() + self._targetList.setLayout(self._targetLayout) + self._uiItems = [] + self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON) + + @property + def toplevel(self): + return self._targetList + + def setVisible(self, isVisible): + self._targetList.setVisible(isVisible) + + def update(self): + cids = list(self._session.draft.get_contacts()) + amountCommon = min(len(cids), len(self._uiItems)) + + # Run through everything in common + for i in xrange(0, amountCommon): + cid = cids[i] + uiItem = self._uiItems[i] + title = self._session.draft.get_title(cid) + description = self._session.draft.get_description(cid) + numbers = self._session.draft.get_numbers(cid) + uiItem["cid"] = cid + uiItem["title"] = title + uiItem["description"] = description + uiItem["numbers"] = numbers + uiItem["label"].setText(title) + self._populate_number_selector(uiItem["selector"], cid, i, numbers) + uiItem["rowWidget"].setVisible(True) + + # More contacts than ui items + for i in xrange(amountCommon, len(cids)): + cid = cids[i] + title = self._session.draft.get_title(cid) + description = self._session.draft.get_description(cid) + numbers = self._session.draft.get_numbers(cid) + + titleLabel = QtGui.QLabel(title) + titleLabel.setWordWrap(True) + numberSelector = QtGui.QComboBox() + self._populate_number_selector(numberSelector, cid, i, numbers) + + callback = functools.partial( + self._on_change_number, + i + ) + callback.__name__ = "thanks partials for not having names and pyqt for requiring them" + numberSelector.activated.connect( + qt_compat.Slot(int)(callback) + ) + + if self._closeIcon is self._SENTINEL_ICON: + deleteButton = QtGui.QPushButton("Delete") + else: + deleteButton = QtGui.QPushButton(self._closeIcon, "") + deleteButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.PushButton, + )) + callback = functools.partial( + self._on_remove_contact, + i + ) + callback.__name__ = "thanks partials for not having names and pyqt for requiring them" + deleteButton.clicked.connect(callback) + + rowLayout = QtGui.QHBoxLayout() + rowLayout.addWidget(titleLabel, 1000) + rowLayout.addWidget(numberSelector, 0) + rowLayout.addWidget(deleteButton, 0) + rowWidget = QtGui.QWidget() + rowWidget.setLayout(rowLayout) + self._targetLayout.addWidget(rowWidget) + + uiItem = {} + uiItem["cid"] = cid + uiItem["title"] = title + uiItem["description"] = description + uiItem["numbers"] = numbers + uiItem["label"] = titleLabel + uiItem["selector"] = numberSelector + uiItem["rowWidget"] = rowWidget + self._uiItems.append(uiItem) + amountCommon = i+1 + + # More UI items than contacts + for i in xrange(amountCommon, len(self._uiItems)): + uiItem = self._uiItems[i] + uiItem["rowWidget"].setVisible(False) + amountCommon = i+1 + + def _populate_number_selector(self, selector, cid, cidIndex, numbers): + selector.clear() + + selectedNumber = self._session.draft.get_selected_number(cid) + if len(numbers) == 1: + # If no alt numbers available, check the address book + numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1]) + else: + defaultIndex = _index_number(numbers, selectedNumber) + + for number, description in numbers: + if description: + label = "%s - %s" % (number, description) + else: + label = number + selector.addItem(label) + selector.setVisible(True) + if 1 < len(numbers): + selector.setEnabled(True) + selector.setCurrentIndex(defaultIndex) + else: + selector.setEnabled(False) + + @misc_utils.log_exception(_moduleLogger) + def _on_change_number(self, cidIndex, index): + with qui_utils.notify_error(self._app.errorLog): + # Exception thrown when the first item is removed + try: + cid = self._uiItems[cidIndex]["cid"] + numbers = self._session.draft.get_numbers(cid) + except IndexError: + _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) + return + except KeyError: + _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) + return + number = numbers[index][0] + self._session.draft.set_selected_number(cid, number) + + @misc_utils.log_exception(_moduleLogger) + def _on_remove_contact(self, index, toggled): + with qui_utils.notify_error(self._app.errorLog): + self._session.draft.remove_contact(self._uiItems[index]["cid"]) + + +class VoicemailPlayer(object): + + def __init__(self, app, session, errorLog): + self._app = app + self._session = session + self._errorLog = errorLog + self._token = None + self._session.voicemailAvailable.connect(self._on_voicemail_downloaded) + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) + + self._playButton = QtGui.QPushButton("Play") + self._playButton.clicked.connect(self._on_voicemail_play) + self._pauseButton = QtGui.QPushButton("Pause") + self._pauseButton.clicked.connect(self._on_voicemail_pause) + self._pauseButton.hide() + self._resumeButton = QtGui.QPushButton("Resume") + self._resumeButton.clicked.connect(self._on_voicemail_resume) + self._resumeButton.hide() + self._stopButton = QtGui.QPushButton("Stop") + self._stopButton.clicked.connect(self._on_voicemail_stop) + self._stopButton.hide() + + self._downloadButton = QtGui.QPushButton("Download Voicemail") + self._downloadButton.clicked.connect(self._on_voicemail_download) + self._downloadLayout = QtGui.QHBoxLayout() + self._downloadLayout.addWidget(self._downloadButton) + self._downloadWidget = QtGui.QWidget() + self._downloadWidget.setLayout(self._downloadLayout) + + self._playLabel = QtGui.QLabel("Voicemail") + self._saveButton = QtGui.QPushButton("Save") + self._saveButton.clicked.connect(self._on_voicemail_save) + self._playerLayout = QtGui.QHBoxLayout() + self._playerLayout.addWidget(self._playLabel) + self._playerLayout.addWidget(self._playButton) + self._playerLayout.addWidget(self._pauseButton) + self._playerLayout.addWidget(self._resumeButton) + self._playerLayout.addWidget(self._stopButton) + self._playerLayout.addWidget(self._saveButton) + self._playerWidget = QtGui.QWidget() + self._playerWidget.setLayout(self._playerLayout) + + self._visibleWidget = None + self._layout = QtGui.QHBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self._widget = QtGui.QWidget() + self._widget.setLayout(self._layout) + self._update_state() + + @property + def toplevel(self): + return self._widget + + def destroy(self): + self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded) + self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed) + self._invalidate_token() + + def _invalidate_token(self): + if self._token is not None: + self._token.invalidate() + self._token.error.disconnect(self._on_play_error) + self._token.stateChange.connect(self._on_play_state) + self._token.invalidated.connect(self._on_play_invalidated) + + def _show_download(self, messageId): + if self._visibleWidget is self._downloadWidget: + return + self._hide() + self._layout.addWidget(self._downloadWidget) + self._visibleWidget = self._downloadWidget + self._visibleWidget.show() + + def _show_player(self, messageId): + if self._visibleWidget is self._playerWidget: + return + self._hide() + self._layout.addWidget(self._playerWidget) + self._visibleWidget = self._playerWidget + self._visibleWidget.show() + + def _hide(self): + if self._visibleWidget is None: + return + self._visibleWidget.hide() + self._layout.removeWidget(self._visibleWidget) + self._visibleWidget = None + + def _update_play_state(self): + if self._token is not None and self._token.isValid: + self._playButton.setText("Stop") + else: + self._playButton.setText("Play") + + def _update_state(self): + if self._session.draft.get_num_contacts() != 1: + self._hide() + return + + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + if messageId is None: + self._hide() + return + + if self._session.is_available(messageId): + self._show_player(messageId) + else: + self._show_download(messageId) + if self._token is not None: + self._token.invalidate() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_save(self, arg): + with qui_utils.notify_error(self._app.errorLog): + targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)") + targetPath = unicode(targetPath) + if not targetPath: + return + + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + sourcePath = self._session.voicemail_path(messageId) + import shutil + shutil.copy2(sourcePath, targetPath) + + @misc_utils.log_exception(_moduleLogger) + def _on_play_error(self, error): + with qui_utils.notify_error(self._app.errorLog): + self._app.errorLog.push_error(error) + + @misc_utils.log_exception(_moduleLogger) + def _on_play_invalidated(self): + with qui_utils.notify_error(self._app.errorLog): + self._playButton.show() + self._pauseButton.hide() + self._resumeButton.hide() + self._stopButton.hide() + self._invalidate_token() + + @misc_utils.log_exception(_moduleLogger) + def _on_play_state(self, state): + with qui_utils.notify_error(self._app.errorLog): + if state == self._token.STATE_PLAY: + self._playButton.hide() + self._pauseButton.show() + self._resumeButton.hide() + self._stopButton.show() + elif state == self._token.STATE_PAUSE: + self._playButton.hide() + self._pauseButton.hide() + self._resumeButton.show() + self._stopButton.show() + elif state == self._token.STATE_STOP: + self._playButton.show() + self._pauseButton.hide() + self._resumeButton.hide() + self._stopButton.hide() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_play(self, arg): + with qui_utils.notify_error(self._app.errorLog): + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + sourcePath = self._session.voicemail_path(messageId) + + self._invalidate_token() + uri = "file://%s" % sourcePath + self._token = self._app.streamHandler.set_file(uri) + self._token.stateChange.connect(self._on_play_state) + self._token.invalidated.connect(self._on_play_invalidated) + self._token.error.connect(self._on_play_error) + self._token.play() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_pause(self, arg): + with qui_utils.notify_error(self._app.errorLog): + self._token.pause() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_resume(self, arg): + with qui_utils.notify_error(self._app.errorLog): + self._token.play() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_stop(self, arg): + with qui_utils.notify_error(self._app.errorLog): + self._token.stop() + + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_download(self, arg): + with qui_utils.notify_error(self._app.errorLog): + (cid, ) = self._session.draft.get_contacts() + messageId = self._session.draft.get_message_id(cid) + self._session.download_voicemail(messageId) + self._hide() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + with qui_utils.notify_error(self._app.errorLog): + self._update_state() + + @qt_compat.Slot(str, str) + @misc_utils.log_exception(_moduleLogger) + def _on_voicemail_downloaded(self, messageId, filepath): + with qui_utils.notify_error(self._app.errorLog): + self._update_state() + + +class SMSEntryWindow(qwrappers.WindowWrapper): + + MAX_CHAR = 160 + # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5 + + def __init__(self, parent, app, session, errorLog): + qwrappers.WindowWrapper.__init__(self, parent, app) + self._session = session + self._session.messagesUpdated.connect(self._on_refresh_history) + self._session.historyUpdated.connect(self._on_refresh_history) + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) + + self._session.draft.sendingMessage.connect(self._on_op_started) + self._session.draft.calling.connect(self._on_op_started) + self._session.draft.calling.connect(self._on_calling_started) + self._session.draft.cancelling.connect(self._on_op_started) + + self._session.draft.sentMessage.connect(self._on_op_finished) + self._session.draft.called.connect(self._on_op_finished) + self._session.draft.cancelled.connect(self._on_op_finished) + self._session.draft.error.connect(self._on_op_error) + + self._errorLog = errorLog + + self._targetList = ContactList(self._app, self._session) + self._history = QtGui.QLabel() + self._history.setTextFormat(QtCore.Qt.RichText) + self._history.setWordWrap(True) + self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog) + self._smsEntry = QtGui.QTextEdit() + self._smsEntry.textChanged.connect(self._on_letter_count_changed) + + self._entryLayout = QtGui.QVBoxLayout() + self._entryLayout.addWidget(self._targetList.toplevel) + self._entryLayout.addWidget(self._history) + self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0) + self._entryLayout.addWidget(self._smsEntry) + self._entryLayout.setContentsMargins(0, 0, 0, 0) + self._entryWidget = QtGui.QWidget() + self._entryWidget.setLayout(self._entryLayout) + self._entryWidget.setContentsMargins(0, 0, 0, 0) + self._scrollEntry = QtGui.QScrollArea() + self._scrollEntry.setWidget(self._entryWidget) + self._scrollEntry.setWidgetResizable(True) + self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) + self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + self._characterCountLabel = QtGui.QLabel("") + self._singleNumberSelector = QtGui.QComboBox() + self._cids = [] + self._singleNumberSelector.activated.connect(self._on_single_change_number) + self._smsButton = QtGui.QPushButton("SMS") + self._smsButton.clicked.connect(self._on_sms_clicked) + self._smsButton.setEnabled(False) + self._dialButton = QtGui.QPushButton("Dial") + self._dialButton.clicked.connect(self._on_call_clicked) + self._cancelButton = QtGui.QPushButton("Cancel Call") + self._cancelButton.clicked.connect(self._on_cancel_clicked) + self._cancelButton.setVisible(False) + + self._buttonLayout = QtGui.QHBoxLayout() + self._buttonLayout.addWidget(self._characterCountLabel) + self._buttonLayout.addStretch() + self._buttonLayout.addWidget(self._singleNumberSelector) + self._buttonLayout.addStretch() + self._buttonLayout.addWidget(self._smsButton) + self._buttonLayout.addWidget(self._dialButton) + self._buttonLayout.addWidget(self._cancelButton) + + self._layout.addWidget(self._errorDisplay.toplevel) + self._layout.addWidget(self._scrollEntry) + self._layout.addLayout(self._buttonLayout) + self._layout.setDirection(QtGui.QBoxLayout.TopToBottom) + + self._window.setWindowTitle("Contact") + self._window.closed.connect(self._on_close_window) + self._window.hidden.connect(self._on_close_window) + self._window.resized.connect(self._on_window_resized) + + self._scrollTimer = QtCore.QTimer() + self._scrollTimer.setInterval(100) + self._scrollTimer.setSingleShot(True) + self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom) + + self._smsEntry.setPlainText(self._session.draft.message) + self._update_letter_count() + self._update_target_fields() + self.set_fullscreen(self._app.fullscreenAction.isChecked()) + self.update_orientation(self._app.orientation) + + def close(self): + if self._window is None: + # Already closed + return + window = self._window + try: + message = unicode(self._smsEntry.toPlainText()) + self._session.draft.message = message + self.hide() + except AttributeError: + _moduleLogger.exception("Oh well") + except RuntimeError: + _moduleLogger.exception("Oh well") + + def destroy(self): + self._session.messagesUpdated.disconnect(self._on_refresh_history) + self._session.historyUpdated.disconnect(self._on_refresh_history) + self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed) + self._session.draft.sendingMessage.disconnect(self._on_op_started) + self._session.draft.calling.disconnect(self._on_op_started) + self._session.draft.calling.disconnect(self._on_calling_started) + self._session.draft.cancelling.disconnect(self._on_op_started) + self._session.draft.sentMessage.disconnect(self._on_op_finished) + self._session.draft.called.disconnect(self._on_op_finished) + self._session.draft.cancelled.disconnect(self._on_op_finished) + self._session.draft.error.disconnect(self._on_op_error) + self._voicemailPlayer.destroy() + window = self._window + self._window = None + try: + window.close() + window.destroy() + except AttributeError: + _moduleLogger.exception("Oh well") + except RuntimeError: + _moduleLogger.exception("Oh well") + + def update_orientation(self, orientation): + qwrappers.WindowWrapper.update_orientation(self, orientation) + self._scroll_to_bottom() + + def _update_letter_count(self): + count = len(self._smsEntry.toPlainText()) + numTexts, numCharInText = divmod(count, self.MAX_CHAR) + numTexts += 1 + numCharsLeftInText = self.MAX_CHAR - numCharInText + self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts)) + + def _update_button_state(self): + self._cancelButton.setEnabled(True) + if self._session.draft.get_num_contacts() == 0: + self._dialButton.setEnabled(False) + self._smsButton.setEnabled(False) + elif self._session.draft.get_num_contacts() == 1: + count = len(self._smsEntry.toPlainText()) + if count == 0: + self._dialButton.setEnabled(True) + self._smsButton.setEnabled(False) + else: + self._dialButton.setEnabled(False) + self._smsButton.setEnabled(True) + else: + self._dialButton.setEnabled(False) + count = len(self._smsEntry.toPlainText()) + if count == 0: + self._smsButton.setEnabled(False) + else: + self._smsButton.setEnabled(True) + + def _update_history(self, cid): + draftContactsCount = self._session.draft.get_num_contacts() + if draftContactsCount != 1: + self._history.setVisible(False) + else: + description = self._session.draft.get_description(cid) + + self._targetList.setVisible(False) + if description: + self._history.setText(description) + self._history.setVisible(True) + else: + self._history.setText("") + self._history.setVisible(False) + + def _update_target_fields(self): + draftContactsCount = self._session.draft.get_num_contacts() + if draftContactsCount == 0: + self.hide() + del self._cids[:] + elif draftContactsCount == 1: + (cid, ) = self._session.draft.get_contacts() + title = self._session.draft.get_title(cid) + numbers = self._session.draft.get_numbers(cid) + + self._targetList.setVisible(False) + self._update_history(cid) + self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers) + self._cids = [cid] + + self._scroll_to_bottom() + self._window.setWindowTitle(title) + self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason) + self.show() + self._window.raise_() + else: + self._targetList.setVisible(True) + self._targetList.update() + self._history.setText("") + self._history.setVisible(False) + self._singleNumberSelector.setVisible(False) + + self._scroll_to_bottom() + self._window.setWindowTitle("Contacts") + self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason) + self.show() + self._window.raise_() + + def _populate_number_selector(self, selector, cid, cidIndex, numbers): + selector.clear() + + selectedNumber = self._session.draft.get_selected_number(cid) + if len(numbers) == 1: + # If no alt numbers available, check the address book + numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1]) + else: + defaultIndex = _index_number(numbers, selectedNumber) + + for number, description in numbers: + if description: + label = "%s - %s" % (number, description) + else: + label = number + selector.addItem(label) + selector.setVisible(True) + if 1 < len(numbers): + selector.setEnabled(True) + selector.setCurrentIndex(defaultIndex) + else: + selector.setEnabled(False) + + def _scroll_to_bottom(self): + self._scrollTimer.start() + + @misc_utils.log_exception(_moduleLogger) + def _on_delayed_scroll_to_bottom(self): + with qui_utils.notify_error(self._app.errorLog): + self._scrollEntry.ensureWidgetVisible(self._smsEntry) + + @misc_utils.log_exception(_moduleLogger) + def _on_sms_clicked(self, arg): + with qui_utils.notify_error(self._app.errorLog): + message = unicode(self._smsEntry.toPlainText()) + self._session.draft.message = message + self._session.draft.send() + + @misc_utils.log_exception(_moduleLogger) + def _on_call_clicked(self, arg): + with qui_utils.notify_error(self._app.errorLog): + message = unicode(self._smsEntry.toPlainText()) + self._session.draft.message = message + self._session.draft.call() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_cancel_clicked(self, message): + with qui_utils.notify_error(self._app.errorLog): + self._session.draft.cancel() + + @misc_utils.log_exception(_moduleLogger) + def _on_single_change_number(self, index): + with qui_utils.notify_error(self._app.errorLog): + # Exception thrown when the first item is removed + cid = self._cids[0] + try: + numbers = self._session.draft.get_numbers(cid) + except KeyError: + _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) + return + number = numbers[index][0] + self._session.draft.set_selected_number(cid, number) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_history(self): + with qui_utils.notify_error(self._app.errorLog): + draftContactsCount = self._session.draft.get_num_contacts() + if draftContactsCount != 1: + # Changing contact count will automatically refresh it + return + (cid, ) = self._session.draft.get_contacts() + self._update_history(cid) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + with qui_utils.notify_error(self._app.errorLog): + self._update_target_fields() + self._update_button_state() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_op_started(self): + with qui_utils.notify_error(self._app.errorLog): + self._smsEntry.setReadOnly(True) + self._smsButton.setVisible(False) + self._dialButton.setVisible(False) + self.show() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_calling_started(self): + with qui_utils.notify_error(self._app.errorLog): + self._cancelButton.setVisible(True) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_op_finished(self): + with qui_utils.notify_error(self._app.errorLog): + self._smsEntry.setPlainText("") + self._smsEntry.setReadOnly(False) + self._cancelButton.setVisible(False) + self._smsButton.setVisible(True) + self._dialButton.setVisible(True) + self.close() + self.destroy() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_op_error(self, message): + with qui_utils.notify_error(self._app.errorLog): + self._smsEntry.setReadOnly(False) + self._cancelButton.setVisible(False) + self._smsButton.setVisible(True) + self._dialButton.setVisible(True) + + self._errorLog.push_error(message) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_letter_count_changed(self): + with qui_utils.notify_error(self._app.errorLog): + self._update_letter_count() + self._update_button_state() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_window_resized(self): + with qui_utils.notify_error(self._app.errorLog): + self._scroll_to_bottom() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc_utils.log_exception(_moduleLogger) + def _on_close_window(self, checked = True): + with qui_utils.notify_error(self._app.errorLog): + self.close() + + +def _index_number(numbers, default): + uglyDefault = misc_utils.make_ugly(default) + uglyContactNumbers = list( + misc_utils.make_ugly(contactNumber) + for (contactNumber, _) in numbers + ) + defaultMatches = [ + misc_utils.similar_ugly_numbers(uglyDefault, contactNumber) + for contactNumber in uglyContactNumbers + ] + try: + defaultIndex = defaultMatches.index(True) + except ValueError: + defaultIndex = -1 + _moduleLogger.warn( + "Could not find contact number %s among %r" % ( + default, numbers + ) + ) + return defaultIndex + + +def _get_contact_numbers(session, contactId, number, description): + contactPhoneNumbers = [] + if contactId and contactId != "0": + try: + contactDetails = copy.deepcopy(session.get_contacts()[contactId]) + contactPhoneNumbers = contactDetails["numbers"] + except KeyError: + contactPhoneNumbers = [] + contactPhoneNumbers = [ + (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown")) + for contactPhoneNumber in contactPhoneNumbers + ] + defaultIndex = _index_number(contactPhoneNumbers, number) + + if not contactPhoneNumbers or defaultIndex == -1: + contactPhoneNumbers += [(number, description)] + defaultIndex = 0 + + return contactPhoneNumbers, defaultIndex diff --git a/dialcentral/examples/log_notifier.py b/dialcentral/examples/log_notifier.py new file mode 100644 index 0000000..541ac18 --- /dev/null +++ b/dialcentral/examples/log_notifier.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +from __future__ import with_statement + +import sys +import datetime +import ConfigParser + + +sys.path.insert(0,"/usr/lib/dialcentral/") + + +import constants +import alarm_notify + + +def notify_on_change(): + with open(constants._notifier_logpath_, "a") as file: + file.write("Notification: %r\n" % (datetime.datetime.now(), )) + + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + backend = alarm_notify.create_backend(config) + notifyUser = alarm_notify.is_changed(config, backend) + + if notifyUser: + file.write("\tChange occurred\n") + + +if __name__ == "__main__": + notify_on_change() diff --git a/dialcentral/examples/sound_notifier.py b/dialcentral/examples/sound_notifier.py new file mode 100644 index 0000000..c31e413 --- /dev/null +++ b/dialcentral/examples/sound_notifier.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import os +import sys +import ConfigParser +import logging + + +sys.path.insert(0,"/usr/lib/dialcentral/") + + +import constants +import alarm_notify + + +def notify_on_change(): + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + backend = alarm_notify.create_backend(config) + notifyUser = alarm_notify.is_changed(config, backend) + + config = ConfigParser.SafeConfigParser() + config.read(constants._custom_notifier_settings_) + soundFile = config.get("Sound Notifier", "soundfile") + soundFile = "/usr/lib/gv-notifier/alert.mp3" + + if notifyUser: + import subprocess + import led_handler + logging.info("Changed, playing %s" % soundFile) + led = led_handler.LedHandler() + led.on() + soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True) + else: + logging.info("No Change") + + +if __name__ == "__main__": + logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_) + logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__)) + logging.info("OS: %s" % (os.uname()[0], )) + logging.info("Kernel: %s (%s) for %s" % os.uname()[2:]) + logging.info("Hostname: %s" % os.uname()[1]) + try: + notify_on_change() + except: + logging.exception("Error") + raise diff --git a/dialcentral/gv_views.py b/dialcentral/gv_views.py new file mode 100644 index 0000000..2bd0663 --- /dev/null +++ b/dialcentral/gv_views.py @@ -0,0 +1,977 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import datetime +import string +import itertools +import logging + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +from util import qtpie +from util import qui_utils +from util import misc as misc_utils + +import backends.null_backend as null_backend +import backends.file_backend as file_backend +import backends.qt_backend as qt_backend + + +_moduleLogger = logging.getLogger(__name__) + + +_SENTINEL_ICON = QtGui.QIcon() + + +class Dialpad(object): + + 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, + ] + 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"), + ] + 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._smsButton.setEnabled(True) + self._callButton.setEnabled(True) + + def disable(self): + self._smsButton.setEnabled(False) + self._callButton.setEnabled(False) + + def get_settings(self): + return {} + + def set_settings(self, settings): + pass + + def clear(self): + pass + + def refresh(self, force = True): + pass + + 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() + + @qt_compat.Slot() + @qt_compat.Slot(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, None, title, description, numbersWithDescriptions) + + @qt_compat.Slot() + @qt_compat.Slot(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, None, 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) + + self._today = datetime.datetime(1900, 1, 1) + + self.prepare_for_update(self._today) + + 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: + section = self._REST_SECTION + self._timeItems[section].appendRow(row) + + def get_item(self, timeIndex, rowIndex, column): + timeItem = self._timeItems[timeIndex] + item = timeItem.child(rowIndex, column) + return item + + +class History(object): + + DETAILS_IDX = 0 + FROM_IDX = 1 + MAX_IDX = 2 + + HISTORY_RECEIVED = "Received" + HISTORY_MISSED = "Missed" + HISTORY_PLACED = "Placed" + HISTORY_ALL = "All" + + HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL] + HISTORY_COLUMNS = ["", "From"] + assert len(HISTORY_COLUMNS) == MAX_IDX + + 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._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed) + refreshIcon = qui_utils.get_theme_icon( + ("view-refresh", "general_refresh", "gtk-refresh", ), + _SENTINEL_ICON + ) + if refreshIcon is not _SENTINEL_ICON: + self._refreshButton = QtGui.QPushButton(refreshIcon, "") + else: + self._refreshButton = QtGui.QPushButton("Refresh") + 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.setIndentation(0) + 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._actionIcon = { + "Placed": self._app.get_icon("placed.png"), + "Missed": self._app.get_icon("missed.png"), + "Received": self._app.get_icon("received.png"), + } + + self._populate_items() + + @property + def toplevel(self): + return self._widget + + def enable(self): + self._itemView.setEnabled(True) + + def disable(self): + self._itemView.setEnabled(False) + + def get_settings(self): + return { + "filter": self._selectedFilter, + } + + 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._itemView.clear() + + def refresh(self, force=True): + self._itemView.setFocus(QtCore.Qt.OtherFocusReason) + + if self._selectedFilter == self.HISTORY_RECEIVED: + self._session.update_history(self._session.HISTORY_RECEIVED, force) + elif self._selectedFilter == self.HISTORY_MISSED: + self._session.update_history(self._session.HISTORY_MISSED, force) + elif self._selectedFilter == self.HISTORY_PLACED: + self._session.update_history(self._session.HISTORY_PLACED, force) + elif self._selectedFilter == self.HISTORY_ALL: + self._session.update_history(self._session.HISTORY_ALL, force) + else: + assert False, "How did we get here?" + + if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE: + 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 = event["relTime"] + action = event["action"] + number = event["number"] + prettyNumber = misc_utils.make_pretty(number) + if prettyNumber.startswith("+1 "): + prettyNumber = prettyNumber[len("+1 "):] + name = event["name"] + if not name or name == number: + name = event["location"] + if not name: + name = "Unknown" + + detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime)) + detailsFont = detailsItem.font() + detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 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[self.DETAILS_IDX].setData(event) + self._categoryManager.add_row(event["time"], row) + self._itemView.expandAll() + + @qt_compat.Slot(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() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_history_updated(self): + with qui_utils.notify_error(self._errorLog): + self._populate_items() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_clicked(self, arg = None): + with qui_utils.notify_error(self._errorLog): + self.refresh(force=True) + + @qt_compat.Slot(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() + + title = unicode(fromItem.text()) + number = str(contactDetails["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() + iNumber = str(iContactDetails["number"]) + if number != iNumber: + continue + relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"]) + action = str(iContactDetails["action"]) + number = str(iContactDetails["number"]) + prettyNumber = misc_utils.make_pretty(number) + rowItems = relTime, action, prettyNumber + descriptionRows.append("%s" % "".join(rowItems)) + description = "%s
" % "".join(descriptionRows) + numbersWithDescriptions = [(str(contactDetails["number"]), "")] + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) + + +class Messages(object): + + NO_MESSAGES = "None" + VOICEMAIL_MESSAGES = "Voicemail" + TEXT_MESSAGES = "SMS" + ALL_TYPES = "All Messages" + MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES] + + UNREAD_STATUS = "Unread" + UNARCHIVED_STATUS = "Inbox" + ALL_STATUS = "Any" + MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS] + + _MIN_MESSAGES_SHOWN = 1 + + def __init__(self, app, session, errorLog): + self._selectedTypeFilter = self.ALL_TYPES + self._selectedStatusFilter = self.ALL_STATUS + self._app = app + self._session = session + self._session.messagesUpdated.connect(self._on_messages_updated) + self._errorLog = errorLog + + self._typeSelection = QtGui.QComboBox() + self._typeSelection.addItems(self.MESSAGE_TYPES) + self._typeSelection.setCurrentIndex( + self.MESSAGE_TYPES.index(self._selectedTypeFilter) + ) + self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed) + + self._statusSelection = QtGui.QComboBox() + self._statusSelection.addItems(self.MESSAGE_STATUSES) + self._statusSelection.setCurrentIndex( + self.MESSAGE_STATUSES.index(self._selectedStatusFilter) + ) + self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed) + + refreshIcon = qui_utils.get_theme_icon( + ("view-refresh", "general_refresh", "gtk-refresh", ), + _SENTINEL_ICON + ) + if refreshIcon is not _SENTINEL_ICON: + self._refreshButton = QtGui.QPushButton(refreshIcon, "") + else: + self._refreshButton = QtGui.QPushButton("Refresh") + self._refreshButton.clicked.connect(self._on_refresh_clicked) + self._refreshButton.setSizePolicy(QtGui.QSizePolicy( + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.Minimum, + QtGui.QSizePolicy.PushButton, + )) + + self._selectionLayout = QtGui.QHBoxLayout() + self._selectionLayout.addWidget(self._typeSelection, 1000) + self._selectionLayout.addWidget(self._statusSelection, 1000) + self._selectionLayout.addWidget(self._refreshButton, 0) + + self._itemStore = QtGui.QStandardItemModel() + self._itemStore.setHorizontalHeaderLabels(["Messages"]) + self._categoryManager = TimeCategories(self._itemStore) + + self._htmlDelegate = qui_utils.QHtmlDelegate() + self._itemView = QtGui.QTreeView() + self._itemView.setModel(self._itemStore) + self._itemView.setUniformRowHeights(False) + 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.setItemDelegate(self._htmlDelegate) + self._itemView.activated.connect(self._on_row_activated) + self._itemView.header().sectionResized.connect(self._on_column_resized) + + self._layout = QtGui.QVBoxLayout() + self._layout.addLayout(self._selectionLayout) + 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): + self._itemView.setEnabled(True) + + def disable(self): + self._itemView.setEnabled(False) + + def get_settings(self): + return { + "type": self._selectedTypeFilter, + "status": self._selectedStatusFilter, + } + + def set_settings(self, settings): + selectedType = settings.get("type", self.ALL_TYPES) + if selectedType in self.MESSAGE_TYPES: + self._selectedTypeFilter = selectedType + self._typeSelection.setCurrentIndex( + self.MESSAGE_TYPES.index(self._selectedTypeFilter) + ) + + selectedStatus = settings.get("status", self.ALL_STATUS) + if selectedStatus in self.MESSAGE_STATUSES: + self._selectedStatusFilter = selectedStatus + self._statusSelection.setCurrentIndex( + self.MESSAGE_STATUSES.index(self._selectedStatusFilter) + ) + + def clear(self): + self._itemView.clear() + + def refresh(self, force=True): + self._itemView.setFocus(QtCore.Qt.OtherFocusReason) + + if self._selectedTypeFilter == self.NO_MESSAGES: + pass + elif self._selectedTypeFilter == self.TEXT_MESSAGES: + self._session.update_messages(self._session.MESSAGE_TEXTS, force) + elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES: + self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force) + elif self._selectedTypeFilter == self.ALL_TYPES: + self._session.update_messages(self._session.MESSAGE_ALL, force) + else: + assert False, "How did we get here?" + + if (self._app.notifyOnSms or self._app.notifyOnVoicemail) and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE: + self._app.ledHandler.off() + + def _populate_items(self): + self._categoryManager.prepare_for_update(self._session.get_when_messages_updated()) + + rawMessages = self._session.get_messages() + rawMessages.sort(key=lambda item: item["time"], reverse=True) + for item in rawMessages: + isUnarchived = not item["isArchived"] + isUnread = not item["isRead"] + visibleStatus = { + self.UNREAD_STATUS: isUnarchived and isUnread, + self.UNARCHIVED_STATUS: isUnarchived, + self.ALL_STATUS: True, + }[self._selectedStatusFilter] + visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES] + if not (visibleType and visibleStatus): + continue + + relTime = misc_utils.abbrev_relative_date(item["relTime"]) + number = item["number"] + prettyNumber = misc_utils.make_pretty(number) + name = item["name"] + if not name or name == number: + name = item["location"] + if not name: + name = "Unknown" + + messageParts = list(item["messageParts"]) + if len(messageParts) == 0: + messages = ("No Transcription", ) + elif len(messageParts) == 1: + if messageParts[0][1]: + messages = (messageParts[0][1], ) + else: + messages = ("No Transcription", ) + else: + messages = [ + "%s: %s" % (messagePart[0], messagePart[1]) + for messagePart in messageParts + ] + + firstMessage = "%s
%s
(%s)" % (name, prettyNumber, relTime) + + expandedMessages = [firstMessage] + expandedMessages.extend(messages) + if self._MIN_MESSAGES_SHOWN < len(messages): + secondMessage = "%d Messages Hidden..." % (len(messages) - self._MIN_MESSAGES_SHOWN, ) + collapsedMessages = [firstMessage, secondMessage] + collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):]) + else: + collapsedMessages = expandedMessages + + item = dict(item.iteritems()) + item["collapsedMessages"] = "
\n".join(collapsedMessages) + item["expandedMessages"] = "
\n".join(expandedMessages) + + messageItem = QtGui.QStandardItem(item["collapsedMessages"]) + messageItem.setData(item) + messageItem.setEditable(False) + messageItem.setCheckable(False) + row = (messageItem, ) + self._categoryManager.add_row(item["time"], row) + self._itemView.expandAll() + + @qt_compat.Slot(str) + @misc_utils.log_exception(_moduleLogger) + def _on_type_filter_changed(self, newItem): + with qui_utils.notify_error(self._errorLog): + self._selectedTypeFilter = str(newItem) + self._populate_items() + + @qt_compat.Slot(str) + @misc_utils.log_exception(_moduleLogger) + def _on_status_filter_changed(self, newItem): + with qui_utils.notify_error(self._errorLog): + self._selectedStatusFilter = str(newItem) + self._populate_items() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_clicked(self, arg = None): + with qui_utils.notify_error(self._errorLog): + self.refresh(force=True) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_messages_updated(self): + with qui_utils.notify_error(self._errorLog): + self._populate_items() + + @qt_compat.Slot(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() + item = self._categoryManager.get_item(timeRow, row, 0) + contactDetails = item.data() + + name = unicode(contactDetails["name"]) + number = str(contactDetails["number"]) + if not name or name == number: + name = unicode(contactDetails["location"]) + if not name: + name = "Unknown" + + if str(contactDetails["type"]) == "Voicemail": + messageId = str(contactDetails["id"]) + else: + messageId = None + contactId = str(contactDetails["contactId"]) + title = name + description = unicode(contactDetails["expandedMessages"]) + numbersWithDescriptions = [(number, "")] + self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions) + + @qt_compat.Slot(QtCore.QModelIndex) + @misc_utils.log_exception(_moduleLogger) + def _on_column_resized(self, index, oldSize, newSize): + self._htmlDelegate.setWidth(newSize, self._itemStore) + + +class Contacts(object): + + # @todo Provide some sort of letter jump + + def __init__(self, app, session, errorLog): + self._app = app + self._session = session + self._session.accountUpdated.connect(self._on_contacts_updated) + self._errorLog = errorLog + self._addressBookFactories = [ + null_backend.NullAddressBookFactory(), + file_backend.FilesystemAddressBookFactory(app.fsContactsPath), + qt_backend.QtContactsAddressBookFactory(), + ] + self._addressBooks = [] + + self._listSelection = QtGui.QComboBox() + self._listSelection.addItems([]) + self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed) + self._activeList = "None" + refreshIcon = qui_utils.get_theme_icon( + ("view-refresh", "general_refresh", "gtk-refresh", ), + _SENTINEL_ICON + ) + if refreshIcon is not _SENTINEL_ICON: + self._refreshButton = QtGui.QPushButton(refreshIcon, "") + else: + self._refreshButton = QtGui.QPushButton("Refresh") + 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._listSelection, 1000) + self._managerLayout.addWidget(self._refreshButton, 0) + + self._itemStore = QtGui.QStandardItemModel() + self._itemStore.setHorizontalHeaderLabels(["Contacts"]) + self._alphaItem = {} + + 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.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.update_addressbooks() + self._populate_items() + + @property + def toplevel(self): + return self._widget + + def enable(self): + self._itemView.setEnabled(True) + + def disable(self): + self._itemView.setEnabled(False) + + def get_settings(self): + return { + "selectedAddressbook": self._activeList, + } + + def set_settings(self, settings): + currentItem = settings.get("selectedAddressbook", "None") + bookNames = [book["name"] for book in self._addressBooks] + try: + newIndex = bookNames.index(currentItem) + except ValueError: + # Switch over to None for the user + newIndex = 0 + self._listSelection.setCurrentIndex(newIndex) + self._activeList = currentItem + + def clear(self): + self._itemView.clear() + + def refresh(self, force=True): + self._itemView.setFocus(QtCore.Qt.OtherFocusReason) + self._backend.update_account(force) + + @property + def _backend(self): + return self._addressBooks[self._listSelection.currentIndex()]["book"] + + def update_addressbooks(self): + self._addressBooks = [ + {"book": book, "name": book.name} + for factory in self._addressBookFactories + for book in factory.get_addressbooks() + ] + self._addressBooks.append( + { + "book": self._session, + "name": "Google Voice", + } + ) + + currentItem = str(self._listSelection.currentText()) + self._activeList = currentItem + if currentItem == "": + # Not loaded yet + currentItem = "None" + self._listSelection.clear() + bookNames = [book["name"] for book in self._addressBooks] + try: + newIndex = bookNames.index(currentItem) + except ValueError: + # Switch over to None for the user + newIndex = 0 + self._itemStore.clear() + _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem) + self._listSelection.addItems(bookNames) + self._listSelection.setCurrentIndex(newIndex) + + def _populate_items(self): + self._itemStore.clear() + self._alphaItem = dict( + (letter, QtGui.QStandardItem(letter)) + for letter in self._prefixes() + ) + for letter in self._prefixes(): + item = self._alphaItem[letter] + item.setEditable(False) + item.setCheckable(False) + row = (item, ) + self._itemStore.appendRow(row) + + for item in self._get_contacts(): + name = item["name"] + if not name: + name = "Unknown" + numbers = item["numbers"] + + nameItem = QtGui.QStandardItem(name) + nameItem.setEditable(False) + nameItem.setCheckable(False) + nameItem.setData(item) + nameItemFont = nameItem.font() + nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5)) + nameItem.setFont(nameItemFont) + + row = (nameItem, ) + rowKey = name[0].upper() + rowKey = rowKey if rowKey in self._alphaItem else "#" + self._alphaItem[rowKey].appendRow(row) + self._itemView.expandAll() + + def _prefixes(self): + return itertools.chain(string.ascii_uppercase, ("#", )) + + def _jump_to_prefix(self, letter): + i = list(self._prefixes()).index(letter) + rootIndex = self._itemView.rootIndex() + currentIndex = self._itemView.model().index(i, 0, rootIndex) + self._itemView.scrollTo(currentIndex) + self._itemView.setItemSelected(self._itemView.topLevelItem(i), True) + + def _get_contacts(self): + contacts = list(self._backend.get_contacts().itervalues()) + contacts.sort(key=lambda contact: contact["name"].lower()) + return contacts + + @qt_compat.Slot(str) + @misc_utils.log_exception(_moduleLogger) + def _on_filter_changed(self, newItem): + with qui_utils.notify_error(self._errorLog): + self._activeList = str(newItem) + self.refresh(force=False) + self._populate_items() + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_refresh_clicked(self, arg = None): + with qui_utils.notify_error(self._errorLog): + self.refresh(force=True) + + @qt_compat.Slot() + @misc_utils.log_exception(_moduleLogger) + def _on_contacts_updated(self): + with qui_utils.notify_error(self._errorLog): + self._populate_items() + + @qt_compat.Slot(QtCore.QModelIndex) + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, index): + with qui_utils.notify_error(self._errorLog): + letterIndex = index.parent() + if not letterIndex.isValid(): + return + letterRow = letterIndex.row() + letter = list(self._prefixes())[letterRow] + letterItem = self._alphaItem[letter] + rowIndex = index.row() + item = letterItem.child(rowIndex, 0) + contactDetails = item.data() + + name = unicode(contactDetails["name"]) + if not name: + name = unicode(contactDetails["location"]) + if not name: + name = "Unknown" + + contactId = str(contactDetails["contactId"]) + numbers = contactDetails["numbers"] + numbers = [ + dict( + (str(k), str(v)) + for (k, v) in number.iteritems() + ) + for number in numbers + ] + numbersWithDescriptions = [ + ( + number["phoneNumber"], + self._choose_phonetype(number), + ) + for number in numbers + ] + title = name + description = name + self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) + + @staticmethod + def _choose_phonetype(numberDetails): + if "phoneTypeName" in numberDetails: + return numberDetails["phoneTypeName"] + elif "phoneType" in numberDetails: + return numberDetails["phoneType"] + else: + return "" diff --git a/dialcentral/led_handler.py b/dialcentral/led_handler.py new file mode 100755 index 0000000..0914105 --- /dev/null +++ b/dialcentral/led_handler.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +import dbus + + +class _NokiaLedHandler(object): + + def __init__(self): + self._bus = dbus.SystemBus() + self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request") + self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request") + + self._ledPattern = "PatternCommunicationChat" + + def on(self): + self._mceRequest.req_led_pattern_activate(self._ledPattern) + + def off(self): + self._mceRequest.req_led_pattern_deactivate(self._ledPattern) + + +class _NoLedHandler(object): + + def __init__(self): + pass + + def on(self): + pass + + def off(self): + pass + + +class LedHandler(object): + + def __init__(self): + self._actual = None + self._isReal = False + + def on(self): + self._lazy_init() + self._actual.on() + + def off(self): + self._lazy_init() + self._actual.off() + + @property + def isReal(self): + self._lazy_init() + self._isReal + + def _lazy_init(self): + if self._actual is not None: + return + try: + self._actual = _NokiaLedHandler() + self._isReal = True + except dbus.DBusException: + self._actual = _NoLedHandler() + self._isReal = False + + +if __name__ == "__main__": + leds = _NokiaLedHandler() + leds.off() diff --git a/dialcentral/session.py b/dialcentral/session.py new file mode 100644 index 0000000..dbdc3e4 --- /dev/null +++ b/dialcentral/session.py @@ -0,0 +1,830 @@ +from __future__ import with_statement + +import os +import time +import datetime +import contextlib +import logging + +try: + import cPickle + pickle = cPickle +except ImportError: + import pickle + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore + +from util import qore_utils +from util import qui_utils +from util import concurrent +from util import misc as misc_utils + +import constants + + +_moduleLogger = logging.getLogger(__name__) + + +class _DraftContact(object): + + def __init__(self, messageId, title, description, numbersWithDescriptions): + self.messageId = messageId + self.title = title + self.description = description + self.numbers = numbersWithDescriptions + self.selectedNumber = numbersWithDescriptions[0][0] + + +class Draft(QtCore.QObject): + + sendingMessage = qt_compat.Signal() + sentMessage = qt_compat.Signal() + calling = qt_compat.Signal() + called = qt_compat.Signal() + cancelling = qt_compat.Signal() + cancelled = qt_compat.Signal() + error = qt_compat.Signal(str) + + recipientsChanged = qt_compat.Signal() + + def __init__(self, asyncQueue, backend, errorLog): + QtCore.QObject.__init__(self) + self._errorLog = errorLog + self._contacts = {} + self._asyncQueue = asyncQueue + self._backend = backend + self._busyReason = None + self._message = "" + + def send(self): + assert 0 < len(self._contacts), "No contacts selected" + assert 0 < len(self._message), "No message to send" + numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()] + le = self._asyncQueue.add_async(self._send) + le.start(numbers, self._message) + + def call(self): + assert len(self._contacts) == 1, "Must select 1 and only 1 contact" + assert len(self._message) == 0, "Cannot send message with call" + (contact, ) = self._contacts.itervalues() + number = misc_utils.make_ugly(contact.selectedNumber) + le = self._asyncQueue.add_async(self._call) + le.start(number) + + def cancel(self): + le = self._asyncQueue.add_async(self._cancel) + le.start() + + def _get_message(self): + return self._message + + def _set_message(self, message): + self._message = message + + message = property(_get_message, _set_message) + + def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions): + if self._busyReason is not None: + raise RuntimeError("Please wait for %r" % self._busyReason) + # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up + contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions) + self._contacts[contactId] = contactDetails + self.recipientsChanged.emit() + + def remove_contact(self, contactId): + if self._busyReason is not None: + raise RuntimeError("Please wait for %r" % self._busyReason) + assert contactId in self._contacts, "Contact missing" + del self._contacts[contactId] + self.recipientsChanged.emit() + + def get_contacts(self): + return self._contacts.iterkeys() + + def get_num_contacts(self): + return len(self._contacts) + + def get_message_id(self, cid): + return self._contacts[cid].messageId + + def get_title(self, cid): + return self._contacts[cid].title + + def get_description(self, cid): + return self._contacts[cid].description + + def get_numbers(self, cid): + return self._contacts[cid].numbers + + def get_selected_number(self, cid): + return self._contacts[cid].selectedNumber + + def set_selected_number(self, cid, number): + # @note I'm lazy, this isn't firing any kind of signal since only one + # controller right now and that is the viewer + assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable" + self._contacts[cid].selectedNumber = number + + def clear(self): + if self._busyReason is not None: + raise RuntimeError("Please wait for %r" % self._busyReason) + self._clear() + + def _clear(self): + oldContacts = self._contacts + self._contacts = {} + self._message = "" + if oldContacts: + self.recipientsChanged.emit() + + @contextlib.contextmanager + def _busy(self, message): + if self._busyReason is not None: + raise RuntimeError("Already busy doing %r" % self._busyReason) + try: + self._busyReason = message + yield + finally: + self._busyReason = None + + def _send(self, numbers, text): + self.sendingMessage.emit() + try: + with self._busy("Sending Text"): + with qui_utils.notify_busy(self._errorLog, "Sending Text"): + yield ( + self._backend[0].send_sms, + (numbers, text), + {}, + ) + self.sentMessage.emit() + self._clear() + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + + def _call(self, number): + self.calling.emit() + try: + with self._busy("Calling"): + with qui_utils.notify_busy(self._errorLog, "Calling"): + yield ( + self._backend[0].call, + (number, ), + {}, + ) + self.called.emit() + self._clear() + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + + def _cancel(self): + self.cancelling.emit() + try: + with qui_utils.notify_busy(self._errorLog, "Cancelling"): + yield ( + self._backend[0].cancel, + (), + {}, + ) + self.cancelled.emit() + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + + +class Session(QtCore.QObject): + + # @todo Somehow add support for csv contacts + # @BUG When loading without caches, downloads messages twice + + stateChange = qt_compat.Signal(str) + loggedOut = qt_compat.Signal() + loggedIn = qt_compat.Signal() + callbackNumberChanged = qt_compat.Signal(str) + + accountUpdated = qt_compat.Signal() + messagesUpdated = qt_compat.Signal() + newMessages = qt_compat.Signal() + historyUpdated = qt_compat.Signal() + dndStateChange = qt_compat.Signal(bool) + voicemailAvailable = qt_compat.Signal(str, str) + + error = qt_compat.Signal(str) + + LOGGEDOUT_STATE = "logged out" + LOGGINGIN_STATE = "logging in" + LOGGEDIN_STATE = "logged in" + + MESSAGE_TEXTS = "Text" + MESSAGE_VOICEMAILS = "Voicemail" + MESSAGE_ALL = "All" + + HISTORY_RECEIVED = "Received" + HISTORY_MISSED = "Missed" + HISTORY_PLACED = "Placed" + HISTORY_ALL = "All" + + _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0") + + _LOGGEDOUT_TIME = -1 + _LOGGINGIN_TIME = 0 + + def __init__(self, errorLog, cachePath): + QtCore.QObject.__init__(self) + self._errorLog = errorLog + self._pool = qore_utils.FutureThread() + self._asyncQueue = concurrent.AsyncTaskQueue(self._pool) + self._backend = [] + self._loggedInTime = self._LOGGEDOUT_TIME + self._loginOps = [] + self._cachePath = cachePath + self._voicemailCachePath = None + self._username = None + self._password = None + self._draft = Draft(self._asyncQueue, self._backend, self._errorLog) + self._delayedRelogin = QtCore.QTimer() + self._delayedRelogin.setInterval(0) + self._delayedRelogin.setSingleShot(True) + self._delayedRelogin.timeout.connect(self._on_delayed_relogin) + + self._contacts = {} + self._accountUpdateTime = datetime.datetime(1971, 1, 1) + self._messages = [] + self._cleanMessages = [] + self._messageUpdateTime = datetime.datetime(1971, 1, 1) + self._history = [] + self._historyUpdateTime = datetime.datetime(1971, 1, 1) + self._dnd = False + self._callback = "" + + @property + def state(self): + return { + self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE, + self._LOGGINGIN_TIME: self.LOGGINGIN_STATE, + }.get(self._loggedInTime, self.LOGGEDIN_STATE) + + @property + def draft(self): + return self._draft + + def login(self, username, password): + assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state + assert username != "", "No username specified" + if self._cachePath is not None: + cookiePath = os.path.join(self._cachePath, "%s.cookies" % username) + else: + cookiePath = None + + if self._username != username or not self._backend: + from backends import gv_backend + del self._backend[:] + self._backend[0:0] = [gv_backend.GVDialer(cookiePath)] + + self._pool.start() + le = self._asyncQueue.add_async(self._login) + le.start(username, password) + + def logout(self): + assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state + _moduleLogger.info("Logging out") + self._pool.stop() + self._loggedInTime = self._LOGGEDOUT_TIME + self._backend[0].persist() + self._save_to_cache() + self._clear_voicemail_cache() + self.stateChange.emit(self.LOGGEDOUT_STATE) + self.loggedOut.emit() + + def clear(self): + assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state + self._backend[0].logout() + del self._backend[0] + self._clear_cache() + self._draft.clear() + + def logout_and_clear(self): + assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state + _moduleLogger.info("Logging out and clearing the account") + self._pool.stop() + self._loggedInTime = self._LOGGEDOUT_TIME + self.clear() + self.stateChange.emit(self.LOGGEDOUT_STATE) + self.loggedOut.emit() + + def update_account(self, force = True): + if not force and self._contacts: + return + le = self._asyncQueue.add_async(self._update_account), (), {} + self._perform_op_while_loggedin(le) + + def refresh_connection(self): + le = self._asyncQueue.add_async(self._refresh_authentication) + le.start() + + def get_contacts(self): + return self._contacts + + def get_when_contacts_updated(self): + return self._accountUpdateTime + + def update_messages(self, messageType, force = True): + if not force and self._messages: + return + le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {} + self._perform_op_while_loggedin(le) + + def get_messages(self): + return self._messages + + def get_when_messages_updated(self): + return self._messageUpdateTime + + def update_history(self, historyType, force = True): + if not force and self._history: + return + le = self._asyncQueue.add_async(self._update_history), (historyType, ), {} + self._perform_op_while_loggedin(le) + + def get_history(self): + return self._history + + def get_when_history_updated(self): + return self._historyUpdateTime + + def update_dnd(self): + le = self._asyncQueue.add_async(self._update_dnd), (), {} + self._perform_op_while_loggedin(le) + + def set_dnd(self, dnd): + le = self._asyncQueue.add_async(self._set_dnd) + le.start(dnd) + + def is_available(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + return os.path.exists(actualPath) + + def voicemail_path(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + if not os.path.exists(actualPath): + raise RuntimeError("Voicemail not available") + return actualPath + + def download_voicemail(self, messageId): + le = self._asyncQueue.add_async(self._download_voicemail) + le.start(messageId) + + def _set_dnd(self, dnd): + oldDnd = self._dnd + try: + assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state + with qui_utils.notify_busy(self._errorLog, "Setting DND Status"): + yield ( + self._backend[0].set_dnd, + (dnd, ), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._dnd = dnd + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) + + def get_dnd(self): + return self._dnd + + def get_account_number(self): + if self.state != self.LOGGEDIN_STATE: + return "" + return self._backend[0].get_account_number() + + def get_callback_numbers(self): + if self.state != self.LOGGEDIN_STATE: + return {} + return self._backend[0].get_callback_numbers() + + def get_callback_number(self): + return self._callback + + def set_callback_number(self, callback): + le = self._asyncQueue.add_async(self._set_callback_number) + le.start(callback) + + def _set_callback_number(self, callback): + oldCallback = self._callback + try: + assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state + yield ( + self._backend[0].set_callback_number, + (callback, ), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._callback = callback + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) + + def _login(self, username, password): + with qui_utils.notify_busy(self._errorLog, "Logging In"): + self._loggedInTime = self._LOGGINGIN_TIME + self.stateChange.emit(self.LOGGINGIN_STATE) + finalState = self.LOGGEDOUT_STATE + accountData = None + try: + if accountData is None and self._backend[0].is_quick_login_possible(): + accountData = yield ( + self._backend[0].refresh_account_info, + (), + {}, + ) + if accountData is not None: + _moduleLogger.info("Logged in through cookies") + else: + # Force a clearing of the cookies + yield ( + self._backend[0].logout, + (), + {}, + ) + + if accountData is None: + accountData = yield ( + self._backend[0].login, + (username, password), + {}, + ) + if accountData is not None: + _moduleLogger.info("Logged in through credentials") + + if accountData is not None: + self._loggedInTime = int(time.time()) + oldUsername = self._username + self._username = username + self._password = password + finalState = self.LOGGEDIN_STATE + if oldUsername != self._username: + needOps = not self._load() + else: + needOps = True + + self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username) + try: + os.makedirs(self._voicemailCachePath) + except OSError, e: + if e.errno != 17: + raise + + self.loggedIn.emit() + self.stateChange.emit(finalState) + finalState = None # Mark it as already set + self._process_account_data(accountData) + + if needOps: + loginOps = self._loginOps[:] + else: + loginOps = [] + del self._loginOps[:] + for asyncOp, args, kwds in loginOps: + asyncOp.start(*args, **kwds) + else: + self._loggedInTime = self._LOGGEDOUT_TIME + self.error.emit("Error logging in") + except Exception, e: + _moduleLogger.exception("Booh") + self._loggedInTime = self._LOGGEDOUT_TIME + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + finally: + if finalState is not None: + self.stateChange.emit(finalState) + if accountData is not None and self._callback: + self.set_callback_number(self._callback) + + def _update_account(self): + try: + with qui_utils.notify_busy(self._errorLog, "Updating Account"): + accountData = yield ( + self._backend[0].refresh_account_info, + (), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._loggedInTime = int(time.time()) + self._process_account_data(accountData) + + def _refresh_authentication(self): + try: + with qui_utils.notify_busy(self._errorLog, "Updating Account"): + accountData = yield ( + self._backend[0].refresh_account_info, + (), + {}, + ) + accountData = None + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + # refresh_account_info does not normally throw, so it is fine if we + # just quit early because something seriously wrong is going on + return + + if accountData is not None: + self._loggedInTime = int(time.time()) + self._process_account_data(accountData) + else: + self._delayedRelogin.start() + + def _load(self): + updateMessages = len(self._messages) != 0 + updateHistory = len(self._history) != 0 + oldDnd = self._dnd + oldCallback = self._callback + + self._messages = [] + self._cleanMessages = [] + self._history = [] + self._dnd = False + self._callback = "" + + loadedFromCache = self._load_from_cache() + if loadedFromCache: + updateMessages = True + updateHistory = True + + if updateMessages: + self.messagesUpdated.emit() + if updateHistory: + self.historyUpdated.emit() + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) + + return loadedFromCache + + def _load_from_cache(self): + if self._cachePath is None: + return False + cachePath = os.path.join(self._cachePath, "%s.cache" % self._username) + + try: + with open(cachePath, "rb") as f: + dumpedData = pickle.load(f) + except (pickle.PickleError, IOError, EOFError, ValueError, ImportError): + _moduleLogger.exception("Pickle fun loading") + return False + except: + _moduleLogger.exception("Weirdness loading") + return False + + try: + version, build = dumpedData[0:2] + except ValueError: + _moduleLogger.exception("Upgrade/downgrade fun") + return False + except: + _moduleLogger.exception("Weirdlings") + return False + + if misc_utils.compare_versions( + self._OLDEST_COMPATIBLE_FORMAT_VERSION, + misc_utils.parse_version(version), + ) <= 0: + try: + ( + version, build, + messages, messageUpdateTime, + history, historyUpdateTime, + dnd, callback + ) = dumpedData + except ValueError: + _moduleLogger.exception("Upgrade/downgrade fun") + return False + except: + _moduleLogger.exception("Weirdlings") + return False + + _moduleLogger.info("Loaded cache") + self._messages = messages + self._alert_on_messages(self._messages) + self._messageUpdateTime = messageUpdateTime + self._history = history + self._historyUpdateTime = historyUpdateTime + self._dnd = dnd + self._callback = callback + return True + else: + _moduleLogger.debug( + "Skipping cache due to version mismatch (%s-%s)" % ( + version, build + ) + ) + return False + + def _save_to_cache(self): + _moduleLogger.info("Saving cache") + if self._cachePath is None: + return + cachePath = os.path.join(self._cachePath, "%s.cache" % self._username) + + try: + dataToDump = ( + constants.__version__, constants.__build__, + self._messages, self._messageUpdateTime, + self._history, self._historyUpdateTime, + self._dnd, self._callback + ) + with open(cachePath, "wb") as f: + pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL) + _moduleLogger.info("Cache saved") + except (pickle.PickleError, IOError): + _moduleLogger.exception("While saving") + + def _clear_cache(self): + updateMessages = len(self._messages) != 0 + updateHistory = len(self._history) != 0 + oldDnd = self._dnd + oldCallback = self._callback + + self._messages = [] + self._messageUpdateTime = datetime.datetime(1971, 1, 1) + self._history = [] + self._historyUpdateTime = datetime.datetime(1971, 1, 1) + self._dnd = False + self._callback = "" + + if updateMessages: + self.messagesUpdated.emit() + if updateHistory: + self.historyUpdated.emit() + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) + + self._save_to_cache() + self._clear_voicemail_cache() + + def _clear_voicemail_cache(self): + import shutil + shutil.rmtree(self._voicemailCachePath, True) + + def _update_messages(self, messageType): + try: + assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state + with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType): + self._messages = yield ( + self._backend[0].get_messages, + (messageType, ), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._messageUpdateTime = datetime.datetime.now() + self.messagesUpdated.emit() + self._alert_on_messages(self._messages) + + def _update_history(self, historyType): + try: + assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state + with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType): + self._history = yield ( + self._backend[0].get_call_history, + (historyType, ), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._historyUpdateTime = datetime.datetime.now() + self.historyUpdated.emit() + + def _update_dnd(self): + with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"): + oldDnd = self._dnd + try: + assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state + self._dnd = yield ( + self._backend[0].is_dnd, + (), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + if oldDnd != self._dnd: + self.dndStateChange(self._dnd) + + def _download_voicemail(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + targetPath = "%s.%s.part" % (actualPath, time.time()) + if os.path.exists(actualPath): + self.voicemailAvailable.emit(messageId, actualPath) + return + with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"): + try: + yield ( + self._backend[0].download, + (messageId, targetPath), + {}, + ) + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + return + + if os.path.exists(actualPath): + try: + os.remove(targetPath) + except: + _moduleLogger.exception("Ignoring file problems with cache") + self.voicemailAvailable.emit(messageId, actualPath) + return + else: + os.rename(targetPath, actualPath) + self.voicemailAvailable.emit(messageId, actualPath) + + def _perform_op_while_loggedin(self, op): + if self.state == self.LOGGEDIN_STATE: + op, args, kwds = op + op.start(*args, **kwds) + else: + self._push_login_op(op) + + def _push_login_op(self, asyncOp): + assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out" + if asyncOp in self._loginOps: + _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp) + return + self._loginOps.append(asyncOp) + + def _process_account_data(self, accountData): + self._contacts = dict( + (contactId, contactDetails) + for contactId, contactDetails in accountData["contacts"].iteritems() + # A zero contact id is the catch all for unknown contacts + if contactId != "0" + ) + + self._accountUpdateTime = datetime.datetime.now() + self.accountUpdated.emit() + + def _alert_on_messages(self, messages): + cleanNewMessages = list(self._clean_messages(messages)) + cleanNewMessages.sort(key=lambda m: m["contactId"]) + if self._cleanMessages: + if self._cleanMessages != cleanNewMessages: + self.newMessages.emit() + self._cleanMessages = cleanNewMessages + + def _clean_messages(self, messages): + for message in messages: + cleaned = dict( + kv + for kv in message.iteritems() + if kv[0] not in + [ + "relTime", + "time", + "isArchived", + "isRead", + "isSpam", + "isTrash", + ] + ) + + # Don't let outbound messages cause alerts, especially if the package has only outbound + cleaned["messageParts"] = [ + tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:" + ] + if not cleaned["messageParts"]: + continue + + yield cleaned + + @misc_utils.log_exception(_moduleLogger) + def _on_delayed_relogin(self): + try: + username = self._username + password = self._password + self.logout() + self.login(username, password) + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + return diff --git a/dialcentral/stream_gst.py b/dialcentral/stream_gst.py new file mode 100644 index 0000000..ce97fb6 --- /dev/null +++ b/dialcentral/stream_gst.py @@ -0,0 +1,145 @@ +import logging + +import gobject +import gst + +import util.misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class Stream(gobject.GObject): + + # @bug Advertising state changes a bit early, should watch for GStreamer state change + + STATE_PLAY = "play" + STATE_PAUSE = "pause" + STATE_STOP = "stop" + + __gsignals__ = { + 'state-change' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'eof' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'error' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), + ), + } + + def __init__(self): + gobject.GObject.__init__(self) + #Fields + self._uri = "" + self._elapsed = 0 + self._duration = 0 + + #Set up GStreamer + self._player = gst.element_factory_make("playbin2", "player") + bus = self._player.get_bus() + bus.add_signal_watch() + bus.connect("message", self._on_message) + + #Constants + self._timeFormat = gst.Format(gst.FORMAT_TIME) + self._seekFlag = gst.SEEK_FLAG_FLUSH + + @property + def playing(self): + return self.state == self.STATE_PLAY + + @property + def has_file(self): + return 0 < len(self._uri) + + @property + def state(self): + state = self._player.get_state()[1] + return self._translate_state(state) + + def set_file(self, uri): + if self._uri != uri: + self._invalidate_cache() + if self.state != self.STATE_STOP: + self.stop() + + self._uri = uri + self._player.set_property("uri", uri) + + def play(self): + if self.state == self.STATE_PLAY: + _moduleLogger.info("Already play") + return + _moduleLogger.info("Play") + self._player.set_state(gst.STATE_PLAYING) + self.emit("state-change", self.STATE_PLAY) + + def pause(self): + if self.state == self.STATE_PAUSE: + _moduleLogger.info("Already pause") + return + _moduleLogger.info("Pause") + self._player.set_state(gst.STATE_PAUSED) + self.emit("state-change", self.STATE_PAUSE) + + def stop(self): + if self.state == self.STATE_STOP: + _moduleLogger.info("Already stop") + return + self._player.set_state(gst.STATE_NULL) + _moduleLogger.info("Stopped") + self.emit("state-change", self.STATE_STOP) + + @property + def elapsed(self): + try: + self._elapsed = self._player.query_position(self._timeFormat, None)[0] + except: + pass + return self._elapsed + + @property + def duration(self): + try: + self._duration = self._player.query_duration(self._timeFormat, None)[0] + except: + _moduleLogger.exception("Query failed") + return self._duration + + def seek_time(self, ns): + self._elapsed = ns + self._player.seek_simple(self._timeFormat, self._seekFlag, ns) + + def _invalidate_cache(self): + self._elapsed = 0 + self._duration = 0 + + def _translate_state(self, gstState): + return { + gst.STATE_NULL: self.STATE_STOP, + gst.STATE_PAUSED: self.STATE_PAUSE, + gst.STATE_PLAYING: self.STATE_PLAY, + }.get(gstState, self.STATE_STOP) + + @misc_utils.log_exception(_moduleLogger) + def _on_message(self, bus, message): + t = message.type + if t == gst.MESSAGE_EOS: + self._player.set_state(gst.STATE_NULL) + self.emit("eof", self._uri) + elif t == gst.MESSAGE_ERROR: + self._player.set_state(gst.STATE_NULL) + err, debug = message.parse_error() + _moduleLogger.error("Error: %s, (%s)" % (err, debug)) + self.emit("error", err, debug) + + +gobject.type_register(Stream) diff --git a/dialcentral/stream_handler.py b/dialcentral/stream_handler.py new file mode 100644 index 0000000..3c0c9e3 --- /dev/null +++ b/dialcentral/stream_handler.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import logging + +import util.qt_compat as qt_compat +QtCore = qt_compat.QtCore + +import util.misc as misc_utils +try: + import stream_gst + stream = stream_gst +except ImportError: + try: + import stream_osso + stream = stream_osso + except ImportError: + import stream_null + stream = stream_null + + +_moduleLogger = logging.getLogger(__name__) + + +class StreamToken(QtCore.QObject): + + stateChange = qt_compat.Signal(str) + invalidated = qt_compat.Signal() + error = qt_compat.Signal(str) + + STATE_PLAY = stream.Stream.STATE_PLAY + STATE_PAUSE = stream.Stream.STATE_PAUSE + STATE_STOP = stream.Stream.STATE_STOP + + def __init__(self, stream): + QtCore.QObject.__init__(self) + self._stream = stream + self._stream.connect("state-change", self._on_stream_state) + self._stream.connect("eof", self._on_stream_eof) + self._stream.connect("error", self._on_stream_error) + + @property + def state(self): + if self.isValid: + return self._stream.state + else: + return self.STATE_STOP + + @property + def isValid(self): + return self._stream is not None + + def play(self): + self._stream.play() + + def pause(self): + self._stream.pause() + + def stop(self): + self._stream.stop() + + def invalidate(self): + if self._stream is None: + return + _moduleLogger.info("Playback token invalidated") + self._stream = None + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_state(self, s, state): + if not self.isValid: + return + if state == self.STATE_STOP: + self.invalidate() + self.stateChange.emit(state) + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_eof(self, s, uri): + if not self.isValid: + return + self.invalidate() + self.stateChange.emit(self.STATE_STOP) + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_error(self, s, error, debug): + if not self.isValid: + return + _moduleLogger.info("Error %s %s" % (error, debug)) + self.error.emit(str(error)) + + +class StreamHandler(QtCore.QObject): + + def __init__(self): + QtCore.QObject.__init__(self) + self._stream = stream.Stream() + self._token = StreamToken(self._stream) + + def set_file(self, path): + self._token.invalidate() + self._token = StreamToken(self._stream) + self._stream.set_file(path) + return self._token + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_state(self, s, state): + _moduleLogger.info("State change %r" % state) + + +if __name__ == "__main__": + pass + diff --git a/dialcentral/stream_null.py b/dialcentral/stream_null.py new file mode 100644 index 0000000..44fbbed --- /dev/null +++ b/dialcentral/stream_null.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import logging + + +_moduleLogger = logging.getLogger(__name__) + + +class Stream(object): + + STATE_PLAY = "play" + STATE_PAUSE = "pause" + STATE_STOP = "stop" + + def __init__(self): + pass + + def connect(self, signalName, slot): + pass + + @property + def playing(self): + return False + + @property + def has_file(self): + return False + + @property + def state(self): + return self.STATE_STOP + + def set_file(self, uri): + pass + + def play(self): + pass + + def pause(self): + pass + + def stop(self): + pass + + @property + def elapsed(self): + return 0 + + @property + def duration(self): + return 0 + + def seek_time(self, ns): + pass + + +if __name__ == "__main__": + pass + diff --git a/dialcentral/stream_osso.py b/dialcentral/stream_osso.py new file mode 100644 index 0000000..abc453f --- /dev/null +++ b/dialcentral/stream_osso.py @@ -0,0 +1,181 @@ +import logging + +import gobject +import dbus + +import util.misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class Stream(gobject.GObject): + + STATE_PLAY = "play" + STATE_PAUSE = "pause" + STATE_STOP = "stop" + + __gsignals__ = { + 'state-change' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'eof' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'error' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), + ), + } + + _SERVICE_NAME = "com.nokia.osso_media_server" + _OBJECT_PATH = "/com/nokia/osso_media_server" + _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music" + + def __init__(self): + gobject.GObject.__init__(self) + #Fields + self._state = self.STATE_STOP + self._nextState = self.STATE_STOP + self._uri = "" + self._elapsed = 0 + self._duration = 0 + + session_bus = dbus.SessionBus() + + # Get the osso-media-player proxy object + oms_object = session_bus.get_object( + self._SERVICE_NAME, + self._OBJECT_PATH, + introspect=False, + follow_name_owner_changes=True, + ) + # Use the audio interface + oms_audio_interface = dbus.Interface( + oms_object, + self._AUDIO_INTERFACE_NAME, + ) + self._audioProxy = oms_audio_interface + + self._audioProxy.connect_to_signal("state_changed", self._on_state_changed) + self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream) + + error_signals = [ + "no_media_selected", + "file_not_found", + "type_not_found", + "unsupported_type", + "gstreamer", + "dsp", + "device_unavailable", + "corrupted_file", + "out_of_memory", + "audio_codec_not_supported", + ] + for error in error_signals: + self._audioProxy.connect_to_signal(error, self._on_error) + + @property + def playing(self): + return self.state == self.STATE_PLAY + + @property + def has_file(self): + return 0 < len(self._uri) + + @property + def state(self): + return self._state + + def set_file(self, uri): + if self._uri != uri: + self._invalidate_cache() + if self.state != self.STATE_STOP: + self.stop() + + self._uri = uri + self._audioProxy.set_media_location(self._uri) + + def play(self): + if self._nextState == self.STATE_PLAY: + _moduleLogger.info("Already play") + return + _moduleLogger.info("Play") + self._audioProxy.play() + self._nextState = self.STATE_PLAY + #self.emit("state-change", self.STATE_PLAY) + + def pause(self): + if self._nextState == self.STATE_PAUSE: + _moduleLogger.info("Already pause") + return + _moduleLogger.info("Pause") + self._audioProxy.pause() + self._nextState = self.STATE_PAUSE + #self.emit("state-change", self.STATE_PLAY) + + def stop(self): + if self._nextState == self.STATE_STOP: + _moduleLogger.info("Already stop") + return + self._audioProxy.stop() + _moduleLogger.info("Stopped") + self._nextState = self.STATE_STOP + #self.emit("state-change", self.STATE_STOP) + + @property + def elapsed(self): + pos_info = self._audioProxy.get_position() + if isinstance(pos_info, tuple): + self._elapsed, self._duration = pos_info + return self._elapsed + + @property + def duration(self): + pos_info = self._audioProxy.get_position() + if isinstance(pos_info, tuple): + self._elapsed, self._duration = pos_info + return self._duration + + def seek_time(self, ns): + _moduleLogger.debug("Seeking to: %s", ns) + self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) ) + + def _invalidate_cache(self): + self._elapsed = 0 + self._duration = 0 + + @misc_utils.log_exception(_moduleLogger) + def _on_error(self, *args): + err, debug = "", repr(args) + _moduleLogger.error("Error: %s, (%s)" % (err, debug)) + self.emit("error", err, debug) + + @misc_utils.log_exception(_moduleLogger) + def _on_end_of_stream(self, *args): + self._state = self.STATE_STOP + self._nextState = self.STATE_STOP + self.emit("eof", self._uri) + + @misc_utils.log_exception(_moduleLogger) + def _on_state_changed(self, state): + _moduleLogger.info("State: %s", state) + state = { + "playing": self.STATE_PLAY, + "paused": self.STATE_PAUSE, + "stopped": self.STATE_STOP, + }[state] + if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP: + # They seem to want to advertise stop right as the stream is starting, breaking the owner of this + return + self._state = state + self._nextState = state + self.emit("state-change", state) + + +gobject.type_register(Stream) diff --git a/dialcentral/util/__init__.py b/dialcentral/util/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/dialcentral/util/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/dialcentral/util/algorithms.py b/dialcentral/util/algorithms.py new file mode 100644 index 0000000..e94fb61 --- /dev/null +++ b/dialcentral/util/algorithms.py @@ -0,0 +1,664 @@ +#!/usr/bin/env python + +""" +@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448 +""" + +import itertools +import functools +import datetime +import types +import array +import random + + +def ordered_itr(collection): + """ + >>> [v for v in ordered_itr({"a": 1, "b": 2})] + [('a', 1), ('b', 2)] + >>> [v for v in ordered_itr([3, 1, 10, -20])] + [-20, 1, 3, 10] + """ + if isinstance(collection, types.DictType): + keys = list(collection.iterkeys()) + keys.sort() + for key in keys: + yield key, collection[key] + else: + values = list(collection) + values.sort() + for value in values: + yield value + + +def itercat(*iterators): + """ + Concatenate several iterators into one. + + >>> [v for v in itercat([1, 2, 3], [4, 1, 3])] + [1, 2, 3, 4, 1, 3] + """ + for i in iterators: + for x in i: + yield x + + +def product(*args, **kwds): + # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy + # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 + pools = map(tuple, args) * kwds.get('repeat', 1) + result = [[]] + for pool in pools: + result = [x+[y] for x in result for y in pool] + for prod in result: + yield tuple(prod) + + +def iterwhile(func, iterator): + """ + Iterate for as long as func(value) returns true. + >>> through = lambda b: b + >>> [v for v in iterwhile(through, [True, True, False])] + [True, True] + """ + iterator = iter(iterator) + while 1: + next = iterator.next() + if not func(next): + raise StopIteration + yield next + + +def iterfirst(iterator, count=1): + """ + Iterate through 'count' first values. + + >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)] + [1, 2, 3] + """ + iterator = iter(iterator) + for i in xrange(count): + yield iterator.next() + + +def iterstep(iterator, n): + """ + Iterate every nth value. + + >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)] + [1, 2, 3, 4, 5] + >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)] + [1, 3, 5] + >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)] + [1, 4] + """ + iterator = iter(iterator) + while True: + yield iterator.next() + # skip n-1 values + for dummy in xrange(n-1): + iterator.next() + + +def itergroup(iterator, count, padValue = None): + """ + Iterate in groups of 'count' values. If there + aren't enough values, the last result is padded with + None. + + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print list(val) + [1, 2, 3] + [4, 5, 6] + >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + (7, None, None) + >>> for val in itergroup("123456", 3): + ... print tuple(val) + ('1', '2', '3') + ('4', '5', '6') + >>> for val in itergroup("123456", 3): + ... print repr("".join(val)) + '123' + '456' + """ + paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) + nIterators = (paddedIterator, ) * count + return itertools.izip(*nIterators) + + +def xzip(*iterators): + """Iterative version of builtin 'zip'.""" + iterators = itertools.imap(iter, iterators) + while 1: + yield tuple([x.next() for x in iterators]) + + +def xmap(func, *iterators): + """Iterative version of builtin 'map'.""" + iterators = itertools.imap(iter, iterators) + values_left = [1] + + def values(): + # Emulate map behaviour, i.e. shorter + # sequences are padded with None when + # they run out of values. + values_left[0] = 0 + for i in range(len(iterators)): + iterator = iterators[i] + if iterator is None: + yield None + else: + try: + yield iterator.next() + values_left[0] = 1 + except StopIteration: + iterators[i] = None + yield None + while 1: + args = tuple(values()) + if not values_left[0]: + raise StopIteration + yield func(*args) + + +def xfilter(func, iterator): + """Iterative version of builtin 'filter'.""" + iterator = iter(iterator) + while 1: + next = iterator.next() + if func(next): + yield next + + +def xreduce(func, iterator, default=None): + """Iterative version of builtin 'reduce'.""" + iterator = iter(iterator) + try: + prev = iterator.next() + except StopIteration: + return default + single = 1 + for next in iterator: + single = 0 + prev = func(prev, next) + if single: + return func(prev, default) + return prev + + +def daterange(begin, end, delta = datetime.timedelta(1)): + """ + Form a range of dates and iterate over them. + + Arguments: + begin -- a date (or datetime) object; the beginning of the range. + end -- a date (or datetime) object; the end of the range. + delta -- (optional) a datetime.timedelta object; how much to step each iteration. + Default step is 1 day. + + Usage: + """ + if not isinstance(delta, datetime.timedelta): + delta = datetime.timedelta(delta) + + ZERO = datetime.timedelta(0) + + if begin < end: + if delta <= ZERO: + raise StopIteration + test = end.__gt__ + else: + if delta >= ZERO: + raise StopIteration + test = end.__lt__ + + while test(begin): + yield begin + begin += delta + + +class LazyList(object): + """ + A Sequence whose values are computed lazily by an iterator. + + Module for the creation and use of iterator-based lazy lists. + this module defines a class LazyList which can be used to represent sequences + of values generated lazily. One can also create recursively defined lazy lists + that generate their values based on ones previously generated. + + Backport to python 2.5 by Michael Pust + """ + + __author__ = 'Dan Spitz' + + def __init__(self, iterable): + self._exhausted = False + self._iterator = iter(iterable) + self._data = [] + + def __len__(self): + """Get the length of a LazyList's computed data.""" + return len(self._data) + + def __getitem__(self, i): + """Get an item from a LazyList. + i should be a positive integer or a slice object.""" + if isinstance(i, int): + #index has not yet been yielded by iterator (or iterator exhausted + #before reaching that index) + if i >= len(self): + self.exhaust(i) + elif i < 0: + raise ValueError('cannot index LazyList with negative number') + return self._data[i] + + #LazyList slices are iterators over a portion of the list. + elif isinstance(i, slice): + start, stop, step = i.start, i.stop, i.step + if any(x is not None and x < 0 for x in (start, stop, step)): + raise ValueError('cannot index or step through a LazyList with' + 'a negative number') + #set start and step to their integer defaults if they are None. + if start is None: + start = 0 + if step is None: + step = 1 + + def LazyListIterator(): + count = start + predicate = ( + (lambda: True) + if stop is None + else (lambda: count < stop) + ) + while predicate(): + try: + yield self[count] + #slices can go out of actual index range without raising an + #error + except IndexError: + break + count += step + return LazyListIterator() + + raise TypeError('i must be an integer or slice') + + def __iter__(self): + """return an iterator over each value in the sequence, + whether it has been computed yet or not.""" + return self[:] + + def computed(self): + """Return an iterator over the values in a LazyList that have + already been computed.""" + return self[:len(self)] + + def exhaust(self, index = None): + """Exhaust the iterator generating this LazyList's values. + if index is None, this will exhaust the iterator completely. + Otherwise, it will iterate over the iterator until either the list + has a value for index or the iterator is exhausted. + """ + if self._exhausted: + return + if index is None: + ind_range = itertools.count(len(self)) + else: + ind_range = range(len(self), index + 1) + + for ind in ind_range: + try: + self._data.append(self._iterator.next()) + except StopIteration: #iterator is fully exhausted + self._exhausted = True + break + + +class RecursiveLazyList(LazyList): + + def __init__(self, prod, *args, **kwds): + super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds)) + + +class RecursiveLazyListFactory: + + def __init__(self, producer): + self._gen = producer + + def __call__(self, *a, **kw): + return RecursiveLazyList(self._gen, *a, **kw) + + +def lazylist(gen): + """ + Decorator for creating a RecursiveLazyList subclass. + This should decorate a generator function taking the LazyList object as its + first argument which yields the contents of the list in order. + + >>> #fibonnacci sequence in a lazy list. + >>> @lazylist + ... def fibgen(lst): + ... yield 0 + ... yield 1 + ... for a, b in itertools.izip(lst, lst[1:]): + ... yield a + b + ... + >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence + >>> fibs = fibgen() + >>> + >>> #prime numbers in a lazy list. + >>> @lazylist + ... def primegen(lst): + ... yield 2 + ... for candidate in itertools.count(3): #start at next number after 2 + ... #if candidate is not divisible by any smaller prime numbers, + ... #it is a prime. + ... if all(candidate % p for p in lst.computed()): + ... yield candidate + ... + >>> #same for primes- treat it like an infinitely long list containing all prime numbers. + >>> primes = primegen() + >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2] + 0 1 1 2 3 5 + >>> print list(fibs[:10]), list(primes[:10]) + [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + """ + return RecursiveLazyListFactory(gen) + + +def map_func(f): + """ + >>> import misc + >>> misc.validate_decorator(map_func) + """ + + @functools.wraps(f) + def wrapper(*args): + result = itertools.imap(f, args) + return result + return wrapper + + +def reduce_func(function): + """ + >>> import misc + >>> misc.validate_decorator(reduce_func(lambda x: x)) + """ + + def decorator(f): + + @functools.wraps(f) + def wrapper(*args): + result = reduce(function, f(args)) + return result + return wrapper + return decorator + + +def any_(iterable): + """ + @note Python Version <2.5 + + >>> any_([True, True]) + True + >>> any_([True, False]) + True + >>> any_([False, False]) + False + """ + + for element in iterable: + if element: + return True + return False + + +def all_(iterable): + """ + @note Python Version <2.5 + + >>> all_([True, True]) + True + >>> all_([True, False]) + False + >>> all_([False, False]) + False + """ + + for element in iterable: + if not element: + return False + return True + + +def for_every(pred, seq): + """ + for_every takes a one argument predicate function and a sequence. + @param pred The predicate function should return true or false. + @returns true if every element in seq returns true for predicate, else returns false. + + >>> for_every (lambda c: c > 5,(6,7,8,9)) + True + + @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907 + """ + + for i in seq: + if not pred(i): + return False + return True + + +def there_exists(pred, seq): + """ + there_exists takes a one argument predicate function and a sequence. + @param pred The predicate function should return true or false. + @returns true if any element in seq returns true for predicate, else returns false. + + >>> there_exists (lambda c: c > 5,(6,7,8,9)) + True + + @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907 + """ + + for i in seq: + if pred(i): + return True + return False + + +def func_repeat(quantity, func, *args, **kwd): + """ + Meant to be in connection with "reduce" + """ + for i in xrange(quantity): + yield func(*args, **kwd) + + +def function_map(preds, item): + """ + Meant to be in connection with "reduce" + """ + results = (pred(item) for pred in preds) + + return results + + +def functional_if(combiner, preds, item): + """ + Combines the result of a list of predicates applied to item according to combiner + + @see any, every for example combiners + """ + pass_bool = lambda b: b + + bool_results = function_map(preds, item) + return combiner(pass_bool, bool_results) + + +def pushback_itr(itr): + """ + >>> list(pushback_itr(xrange(5))) + [0, 1, 2, 3, 4] + >>> + >>> first = True + >>> itr = pushback_itr(xrange(5)) + >>> for i in itr: + ... print i + ... if first and i == 2: + ... first = False + ... print itr.send(i) + 0 + 1 + 2 + None + 2 + 3 + 4 + >>> + >>> first = True + >>> itr = pushback_itr(xrange(5)) + >>> for i in itr: + ... print i + ... if first and i == 2: + ... first = False + ... print itr.send(i) + ... print itr.send(i) + 0 + 1 + 2 + None + None + 2 + 2 + 3 + 4 + >>> + >>> itr = pushback_itr(xrange(5)) + >>> print itr.next() + 0 + >>> print itr.next() + 1 + >>> print itr.send(10) + None + >>> print itr.next() + 10 + >>> print itr.next() + 2 + >>> print itr.send(20) + None + >>> print itr.send(30) + None + >>> print itr.send(40) + None + >>> print itr.next() + 40 + >>> print itr.next() + 30 + >>> print itr.send(50) + None + >>> print itr.next() + 50 + >>> print itr.next() + 20 + >>> print itr.next() + 3 + >>> print itr.next() + 4 + """ + for item in itr: + maybePushedBack = yield item + queue = [] + while queue or maybePushedBack is not None: + if maybePushedBack is not None: + queue.append(maybePushedBack) + maybePushedBack = yield None + else: + item = queue.pop() + maybePushedBack = yield item + + +def itr_available(queue, initiallyBlock = False): + if initiallyBlock: + yield queue.get() + while not queue.empty(): + yield queue.get_nowait() + + +class BloomFilter(object): + """ + http://en.wikipedia.org/wiki/Bloom_filter + Sources: + http://code.activestate.com/recipes/577684-bloom-filter/ + http://code.activestate.com/recipes/577686-bloom-filter/ + + >>> from random import sample + >>> from string import ascii_letters + >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut + ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas + ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota + ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey + ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon + ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah + ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split() + >>> bf = BloomFilter(num_bits=1000, num_probes=14) + >>> for state in states: + ... bf.add(state) + >>> numStatesFound = sum(state in bf for state in states) + >>> numStatesFound, len(states) + (50, 50) + >>> trials = 100 + >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials)) + >>> numGarbageFound, trials + (0, 100) + """ + + def __init__(self, num_bits, num_probes): + num_words = (num_bits + 31) // 32 + self._arr = array.array('B', [0]) * num_words + self._num_probes = num_probes + + def add(self, key): + for i, mask in self._get_probes(key): + self._arr[i] |= mask + + def union(self, bfilter): + if self._match_template(bfilter): + for i, b in enumerate(bfilter._arr): + self._arr[i] |= b + else: + # Union b/w two unrelated bloom filter raises this + raise ValueError("Mismatched bloom filters") + + def intersection(self, bfilter): + if self._match_template(bfilter): + for i, b in enumerate(bfilter._arr): + self._arr[i] &= b + else: + # Intersection b/w two unrelated bloom filter raises this + raise ValueError("Mismatched bloom filters") + + def __contains__(self, key): + return all(self._arr[i] & mask for i, mask in self._get_probes(key)) + + def _match_template(self, bfilter): + return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes + + def _get_probes(self, key): + hasher = random.Random(key).randrange + for _ in range(self._num_probes): + array_index = hasher(len(self._arr)) + bit_index = hasher(32) + yield array_index, 1 << bit_index + + +if __name__ == "__main__": + import doctest + print doctest.testmod() diff --git a/dialcentral/util/concurrent.py b/dialcentral/util/concurrent.py new file mode 100644 index 0000000..f5f6e1d --- /dev/null +++ b/dialcentral/util/concurrent.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +from __future__ import with_statement + +import os +import errno +import time +import functools +import contextlib +import logging + +import misc + + +_moduleLogger = logging.getLogger(__name__) + + +class AsyncTaskQueue(object): + + def __init__(self, taskPool): + self._asyncs = [] + self._taskPool = taskPool + + def add_async(self, func): + self.flush() + a = AsyncGeneratorTask(self._taskPool, func) + self._asyncs.append(a) + return a + + def flush(self): + self._asyncs = [a for a in self._asyncs if not a.isDone] + + +class AsyncGeneratorTask(object): + + def __init__(self, pool, func): + self._pool = pool + self._func = func + self._run = None + self._isDone = False + + @property + def isDone(self): + return self._isDone + + def start(self, *args, **kwds): + assert self._run is None, "Task already started" + self._run = self._func(*args, **kwds) + trampoline, args, kwds = self._run.send(None) # priming the function + self._pool.add_task( + trampoline, + args, + kwds, + self.on_success, + self.on_error, + ) + + @misc.log_exception(_moduleLogger) + def on_success(self, result): + _moduleLogger.debug("Processing success for: %r", self._func) + try: + trampoline, args, kwds = self._run.send(result) + except StopIteration, e: + self._isDone = True + else: + self._pool.add_task( + trampoline, + args, + kwds, + self.on_success, + self.on_error, + ) + + @misc.log_exception(_moduleLogger) + def on_error(self, error): + _moduleLogger.debug("Processing error for: %r", self._func) + try: + trampoline, args, kwds = self._run.throw(error) + except StopIteration, e: + self._isDone = True + else: + self._pool.add_task( + trampoline, + args, + kwds, + self.on_success, + self.on_error, + ) + + def __repr__(self): + return "" % (self._func.__name__, id(self)) + + def __hash__(self): + return hash(self._func) + + def __eq__(self, other): + return self._func == other._func + + def __ne__(self, other): + return self._func != other._func + + +def synchronized(lock): + """ + Synchronization decorator. + + >>> import misc + >>> misc.validate_decorator(synchronized(object())) + """ + + def wrap(f): + + @functools.wraps(f) + def newFunction(*args, **kw): + lock.acquire() + try: + return f(*args, **kw) + finally: + lock.release() + return newFunction + return wrap + + +@contextlib.contextmanager +def qlock(queue, gblock = True, gtimeout = None, pblock = True, ptimeout = None): + """ + Locking with a queue, good for when you want to lock an item passed around + + >>> import Queue + >>> item = 5 + >>> lock = Queue.Queue() + >>> lock.put(item) + >>> with qlock(lock) as i: + ... print i + 5 + """ + item = queue.get(gblock, gtimeout) + try: + yield item + finally: + queue.put(item, pblock, ptimeout) + + +@contextlib.contextmanager +def flock(path, timeout=-1): + WAIT_FOREVER = -1 + DELAY = 0.1 + timeSpent = 0 + + acquired = False + + while timeSpent <= timeout or timeout == WAIT_FOREVER: + try: + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) + acquired = True + break + except OSError, e: + if e.errno != errno.EEXIST: + raise + time.sleep(DELAY) + timeSpent += DELAY + + assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout) + + try: + yield fd + finally: + os.unlink(path) diff --git a/dialcentral/util/coroutines.py b/dialcentral/util/coroutines.py new file mode 100755 index 0000000..b1e539e --- /dev/null +++ b/dialcentral/util/coroutines.py @@ -0,0 +1,623 @@ +#!/usr/bin/env python + +""" +Uses for generators +* Pull pipelining (iterators) +* Push pipelining (coroutines) +* State machines (coroutines) +* "Cooperative multitasking" (coroutines) +* Algorithm -> Object transform for cohesiveness (for example context managers) (coroutines) + +Design considerations +* When should a stage pass on exceptions or have it thrown within it? +* When should a stage pass on GeneratorExits? +* Is there a way to either turn a push generator into a iterator or to use + comprehensions syntax for push generators (I doubt it) +* When should the stage try and send data in both directions +* Since pull generators (generators), push generators (coroutines), subroutines, and coroutines are all coroutines, maybe we should rename the push generators to not confuse them, like signals/slots? and then refer to two-way generators as coroutines +** If so, make s* and co* implementation of functions +""" + +import threading +import Queue +import pickle +import functools +import itertools +import xml.sax +import xml.parsers.expat + + +def autostart(func): + """ + >>> @autostart + ... def grep_sink(pattern): + ... print "Looking for %s" % pattern + ... while True: + ... line = yield + ... if pattern in line: + ... print line, + >>> g = grep_sink("python") + Looking for python + >>> g.send("Yeah but no but yeah but no") + >>> g.send("A series of tubes") + >>> g.send("python generators rock!") + python generators rock! + >>> g.close() + """ + + @functools.wraps(func) + def start(*args, **kwargs): + cr = func(*args, **kwargs) + cr.next() + return cr + + return start + + +@autostart +def printer_sink(format = "%s"): + """ + >>> pr = printer_sink("%r") + >>> pr.send("Hello") + 'Hello' + >>> pr.send("5") + '5' + >>> pr.send(5) + 5 + >>> p = printer_sink() + >>> p.send("Hello") + Hello + >>> p.send("World") + World + >>> # p.throw(RuntimeError, "Goodbye") + >>> # p.send("Meh") + >>> # p.close() + """ + while True: + item = yield + print format % (item, ) + + +@autostart +def null_sink(): + """ + Good for uses like with cochain to pick up any slack + """ + while True: + item = yield + + +def itr_source(itr, target): + """ + >>> itr_source(xrange(2), printer_sink()) + 0 + 1 + """ + for item in itr: + target.send(item) + + +@autostart +def cofilter(predicate, target): + """ + >>> p = printer_sink() + >>> cf = cofilter(None, p) + >>> cf.send("") + >>> cf.send("Hello") + Hello + >>> cf.send([]) + >>> cf.send([1, 2]) + [1, 2] + >>> cf.send(False) + >>> cf.send(True) + True + >>> cf.send(0) + >>> cf.send(1) + 1 + >>> # cf.throw(RuntimeError, "Goodbye") + >>> # cf.send(False) + >>> # cf.send(True) + >>> # cf.close() + """ + if predicate is None: + predicate = bool + + while True: + try: + item = yield + if predicate(item): + target.send(item) + except StandardError, e: + target.throw(e.__class__, e.message) + + +@autostart +def comap(function, target): + """ + >>> p = printer_sink() + >>> cm = comap(lambda x: x+1, p) + >>> cm.send(0) + 1 + >>> cm.send(1.0) + 2.0 + >>> cm.send(-2) + -1 + >>> # cm.throw(RuntimeError, "Goodbye") + >>> # cm.send(0) + >>> # cm.send(1.0) + >>> # cm.close() + """ + while True: + try: + item = yield + mappedItem = function(item) + target.send(mappedItem) + except StandardError, e: + target.throw(e.__class__, e.message) + + +def func_sink(function): + return comap(function, null_sink()) + + +def expand_positional(function): + + @functools.wraps(function) + def expander(item): + return function(*item) + + return expander + + +@autostart +def append_sink(l): + """ + >>> l = [] + >>> apps = append_sink(l) + >>> apps.send(1) + >>> apps.send(2) + >>> apps.send(3) + >>> print l + [1, 2, 3] + """ + while True: + item = yield + l.append(item) + + +@autostart +def last_n_sink(l, n = 1): + """ + >>> l = [] + >>> lns = last_n_sink(l) + >>> lns.send(1) + >>> lns.send(2) + >>> lns.send(3) + >>> print l + [3] + """ + del l[:] + while True: + item = yield + extraCount = len(l) - n + 1 + if 0 < extraCount: + del l[0:extraCount] + l.append(item) + + +@autostart +def coreduce(target, function, initializer = None): + """ + >>> reduceResult = [] + >>> lns = last_n_sink(reduceResult) + >>> cr = coreduce(lns, lambda x, y: x + y, 0) + >>> cr.send(1) + >>> cr.send(2) + >>> cr.send(3) + >>> print reduceResult + [6] + >>> cr = coreduce(lns, lambda x, y: x + y) + >>> cr.send(1) + >>> cr.send(2) + >>> cr.send(3) + >>> print reduceResult + [6] + """ + isFirst = True + cumulativeRef = initializer + while True: + item = yield + if isFirst and initializer is None: + cumulativeRef = item + else: + cumulativeRef = function(cumulativeRef, item) + target.send(cumulativeRef) + isFirst = False + + +@autostart +def cotee(targets): + """ + Takes a sequence of coroutines and sends the received items to all of them + + >>> ct = cotee((printer_sink("1 %s"), printer_sink("2 %s"))) + >>> ct.send("Hello") + 1 Hello + 2 Hello + >>> ct.send("World") + 1 World + 2 World + >>> # ct.throw(RuntimeError, "Goodbye") + >>> # ct.send("Meh") + >>> # ct.close() + """ + while True: + try: + item = yield + for target in targets: + target.send(item) + except StandardError, e: + for target in targets: + target.throw(e.__class__, e.message) + + +class CoTee(object): + """ + >>> ct = CoTee() + >>> ct.register_sink(printer_sink("1 %s")) + >>> ct.register_sink(printer_sink("2 %s")) + >>> ct.stage.send("Hello") + 1 Hello + 2 Hello + >>> ct.stage.send("World") + 1 World + 2 World + >>> ct.register_sink(printer_sink("3 %s")) + >>> ct.stage.send("Foo") + 1 Foo + 2 Foo + 3 Foo + >>> # ct.stage.throw(RuntimeError, "Goodbye") + >>> # ct.stage.send("Meh") + >>> # ct.stage.close() + """ + + def __init__(self): + self.stage = self._stage() + self._targets = [] + + def register_sink(self, sink): + self._targets.append(sink) + + def unregister_sink(self, sink): + self._targets.remove(sink) + + def restart(self): + self.stage = self._stage() + + @autostart + def _stage(self): + while True: + try: + item = yield + for target in self._targets: + target.send(item) + except StandardError, e: + for target in self._targets: + target.throw(e.__class__, e.message) + + +def _flush_queue(queue): + while not queue.empty(): + yield queue.get() + + +@autostart +def cocount(target, start = 0): + """ + >>> cc = cocount(printer_sink("%s")) + >>> cc.send("a") + 0 + >>> cc.send(None) + 1 + >>> cc.send([]) + 2 + >>> cc.send(0) + 3 + """ + for i in itertools.count(start): + item = yield + target.send(i) + + +@autostart +def coenumerate(target, start = 0): + """ + >>> ce = coenumerate(printer_sink("%r")) + >>> ce.send("a") + (0, 'a') + >>> ce.send(None) + (1, None) + >>> ce.send([]) + (2, []) + >>> ce.send(0) + (3, 0) + """ + for i in itertools.count(start): + item = yield + decoratedItem = i, item + target.send(decoratedItem) + + +@autostart +def corepeat(target, elem): + """ + >>> cr = corepeat(printer_sink("%s"), "Hello World") + >>> cr.send("a") + Hello World + >>> cr.send(None) + Hello World + >>> cr.send([]) + Hello World + >>> cr.send(0) + Hello World + """ + while True: + item = yield + target.send(elem) + + +@autostart +def cointercept(target, elems): + """ + >>> cr = cointercept(printer_sink("%s"), [1, 2, 3, 4]) + >>> cr.send("a") + 1 + >>> cr.send(None) + 2 + >>> cr.send([]) + 3 + >>> cr.send(0) + 4 + >>> cr.send("Bye") + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "", line 1, in + cr.send("Bye") + StopIteration + """ + item = yield + for elem in elems: + target.send(elem) + item = yield + + +@autostart +def codropwhile(target, pred): + """ + >>> cdw = codropwhile(printer_sink("%s"), lambda x: x) + >>> cdw.send([0, 1, 2]) + >>> cdw.send(1) + >>> cdw.send(True) + >>> cdw.send(False) + >>> cdw.send([0, 1, 2]) + [0, 1, 2] + >>> cdw.send(1) + 1 + >>> cdw.send(True) + True + """ + while True: + item = yield + if not pred(item): + break + + while True: + item = yield + target.send(item) + + +@autostart +def cotakewhile(target, pred): + """ + >>> ctw = cotakewhile(printer_sink("%s"), lambda x: x) + >>> ctw.send([0, 1, 2]) + [0, 1, 2] + >>> ctw.send(1) + 1 + >>> ctw.send(True) + True + >>> ctw.send(False) + >>> ctw.send([0, 1, 2]) + >>> ctw.send(1) + >>> ctw.send(True) + """ + while True: + item = yield + if not pred(item): + break + target.send(item) + + while True: + item = yield + + +@autostart +def coslice(target, lower, upper): + """ + >>> cs = coslice(printer_sink("%r"), 3, 5) + >>> cs.send("0") + >>> cs.send("1") + >>> cs.send("2") + >>> cs.send("3") + '3' + >>> cs.send("4") + '4' + >>> cs.send("5") + >>> cs.send("6") + """ + for i in xrange(lower): + item = yield + for i in xrange(upper - lower): + item = yield + target.send(item) + while True: + item = yield + + +@autostart +def cochain(targets): + """ + >>> cr = cointercept(printer_sink("good %s"), [1, 2, 3, 4]) + >>> cc = cochain([cr, printer_sink("end %s")]) + >>> cc.send("a") + good 1 + >>> cc.send(None) + good 2 + >>> cc.send([]) + good 3 + >>> cc.send(0) + good 4 + >>> cc.send("Bye") + end Bye + """ + behind = [] + for target in targets: + try: + while behind: + item = behind.pop() + target.send(item) + while True: + item = yield + target.send(item) + except StopIteration: + behind.append(item) + + +@autostart +def queue_sink(queue): + """ + >>> q = Queue.Queue() + >>> qs = queue_sink(q) + >>> qs.send("Hello") + >>> qs.send("World") + >>> qs.throw(RuntimeError, "Goodbye") + >>> qs.send("Meh") + >>> qs.close() + >>> print [i for i in _flush_queue(q)] + [(None, 'Hello'), (None, 'World'), (, 'Goodbye'), (None, 'Meh'), (, None)] + """ + while True: + try: + item = yield + queue.put((None, item)) + except StandardError, e: + queue.put((e.__class__, e.message)) + except GeneratorExit: + queue.put((GeneratorExit, None)) + raise + + +def decode_item(item, target): + if item[0] is None: + target.send(item[1]) + return False + elif item[0] is GeneratorExit: + target.close() + return True + else: + target.throw(item[0], item[1]) + return False + + +def queue_source(queue, target): + """ + >>> q = Queue.Queue() + >>> for i in [ + ... (None, 'Hello'), + ... (None, 'World'), + ... (GeneratorExit, None), + ... ]: + ... q.put(i) + >>> qs = queue_source(q, printer_sink()) + Hello + World + """ + isDone = False + while not isDone: + item = queue.get() + isDone = decode_item(item, target) + + +def threaded_stage(target, thread_factory = threading.Thread): + messages = Queue.Queue() + + run_source = functools.partial(queue_source, messages, target) + thread_factory(target=run_source).start() + + # Sink running in current thread + return functools.partial(queue_sink, messages) + + +@autostart +def pickle_sink(f): + while True: + try: + item = yield + pickle.dump((None, item), f) + except StandardError, e: + pickle.dump((e.__class__, e.message), f) + except GeneratorExit: + pickle.dump((GeneratorExit, ), f) + raise + except StopIteration: + f.close() + return + + +def pickle_source(f, target): + try: + isDone = False + while not isDone: + item = pickle.load(f) + isDone = decode_item(item, target) + except EOFError: + target.close() + + +class EventHandler(object, xml.sax.ContentHandler): + + START = "start" + TEXT = "text" + END = "end" + + def __init__(self, target): + object.__init__(self) + xml.sax.ContentHandler.__init__(self) + self._target = target + + def startElement(self, name, attrs): + self._target.send((self.START, (name, attrs._attrs))) + + def characters(self, text): + self._target.send((self.TEXT, text)) + + def endElement(self, name): + self._target.send((self.END, name)) + + +def expat_parse(f, target): + parser = xml.parsers.expat.ParserCreate() + parser.buffer_size = 65536 + parser.buffer_text = True + parser.returns_unicode = False + parser.StartElementHandler = lambda name, attrs: target.send(('start', (name, attrs))) + parser.EndElementHandler = lambda name: target.send(('end', name)) + parser.CharacterDataHandler = lambda data: target.send(('text', data)) + parser.ParseFile(f) + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/dialcentral/util/go_utils.py b/dialcentral/util/go_utils.py new file mode 100644 index 0000000..61e731d --- /dev/null +++ b/dialcentral/util/go_utils.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python + +from __future__ import with_statement + +import time +import functools +import threading +import Queue +import logging + +import gobject + +import algorithms +import misc + + +_moduleLogger = logging.getLogger(__name__) + + +def make_idler(func): + """ + Decorator that makes a generator-function into a function that will continue execution on next call + """ + a = [] + + @functools.wraps(func) + def decorated_func(*args, **kwds): + if not a: + a.append(func(*args, **kwds)) + try: + a[0].next() + return True + except StopIteration: + del a[:] + return False + + return decorated_func + + +def async(func): + """ + Make a function mainloop friendly. the function will be called at the + next mainloop idle state. + + >>> import misc + >>> misc.validate_decorator(async) + """ + + @functools.wraps(func) + def new_function(*args, **kwargs): + + def async_function(): + func(*args, **kwargs) + return False + + gobject.idle_add(async_function) + + return new_function + + +class Async(object): + + def __init__(self, func, once = True): + self.__func = func + self.__idleId = None + self.__once = once + + def start(self): + assert self.__idleId is None + if self.__once: + self.__idleId = gobject.idle_add(self._on_once) + else: + self.__idleId = gobject.idle_add(self.__func) + + def is_running(self): + return self.__idleId is not None + + def cancel(self): + if self.__idleId is not None: + gobject.source_remove(self.__idleId) + self.__idleId = None + + def __call__(self): + return self.start() + + @misc.log_exception(_moduleLogger) + def _on_once(self): + self.cancel() + try: + self.__func() + except Exception: + pass + return False + + +class Timeout(object): + + def __init__(self, func, once = True): + self.__func = func + self.__timeoutId = None + self.__once = once + + def start(self, **kwds): + assert self.__timeoutId is None + + callback = self._on_once if self.__once else self.__func + + assert len(kwds) == 1 + timeoutInSeconds = kwds["seconds"] + assert 0 <= timeoutInSeconds + + if timeoutInSeconds == 0: + self.__timeoutId = gobject.idle_add(callback) + else: + self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback) + + def is_running(self): + return self.__timeoutId is not None + + def cancel(self): + if self.__timeoutId is not None: + gobject.source_remove(self.__timeoutId) + self.__timeoutId = None + + def __call__(self, **kwds): + return self.start(**kwds) + + @misc.log_exception(_moduleLogger) + def _on_once(self): + self.cancel() + try: + self.__func() + except Exception: + pass + return False + + +_QUEUE_EMPTY = object() + + +class FutureThread(object): + + def __init__(self): + self.__workQueue = Queue.Queue() + self.__thread = threading.Thread( + name = type(self).__name__, + target = self.__consume_queue, + ) + self.__isRunning = True + + def start(self): + self.__thread.start() + + def stop(self): + self.__isRunning = False + for _ in algorithms.itr_available(self.__workQueue): + pass # eat up queue to cut down dumb work + self.__workQueue.put(_QUEUE_EMPTY) + + def clear_tasks(self): + for _ in algorithms.itr_available(self.__workQueue): + pass # eat up queue to cut down dumb work + + def add_task(self, func, args, kwds, on_success, on_error): + task = func, args, kwds, on_success, on_error + self.__workQueue.put(task) + + @misc.log_exception(_moduleLogger) + def __trampoline_callback(self, on_success, on_error, isError, result): + if not self.__isRunning: + if isError: + _moduleLogger.error("Masking: %s" % (result, )) + isError = True + result = StopIteration("Cancelling all callbacks") + callback = on_success if not isError else on_error + try: + callback(result) + except Exception: + _moduleLogger.exception("Callback errored") + return False + + @misc.log_exception(_moduleLogger) + def __consume_queue(self): + while True: + task = self.__workQueue.get() + if task is _QUEUE_EMPTY: + break + func, args, kwds, on_success, on_error = task + + try: + result = func(*args, **kwds) + isError = False + except Exception, e: + _moduleLogger.error("Error, passing it back to the main thread") + result = e + isError = True + self.__workQueue.task_done() + + gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result) + _moduleLogger.debug("Shutting down worker thread") + + +class AutoSignal(object): + + def __init__(self, toplevel): + self.__disconnectPool = [] + toplevel.connect("destroy", self.__on_destroy) + + def connect_auto(self, widget, *args): + id = widget.connect(*args) + self.__disconnectPool.append((widget, id)) + + @misc.log_exception(_moduleLogger) + def __on_destroy(self, widget): + _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool))) + for widget, id in self.__disconnectPool: + widget.disconnect(id) + del self.__disconnectPool[:] + + +def throttled(minDelay, queue): + """ + Throttle the calls to a function by queueing all the calls that happen + before the minimum delay + + >>> import misc + >>> import Queue + >>> misc.validate_decorator(throttled(0, Queue.Queue())) + """ + + def actual_decorator(func): + + lastCallTime = [None] + + def process_queue(): + if 0 < len(queue): + func, args, kwargs = queue.pop(0) + lastCallTime[0] = time.time() * 1000 + func(*args, **kwargs) + return False + + @functools.wraps(func) + def new_function(*args, **kwargs): + now = time.time() * 1000 + if ( + lastCallTime[0] is None or + (now - lastCallTime >= minDelay) + ): + lastCallTime[0] = now + func(*args, **kwargs) + else: + queue.append((func, args, kwargs)) + lastCallDelta = now - lastCallTime[0] + processQueueTimeout = int(minDelay * len(queue) - lastCallDelta) + gobject.timeout_add(processQueueTimeout, process_queue) + + return new_function + + return actual_decorator + + +def _old_timeout_add_seconds(timeout, callback): + return gobject.timeout_add(timeout * 1000, callback) + + +def _timeout_add_seconds(timeout, callback): + return gobject.timeout_add_seconds(timeout, callback) + + +try: + gobject.timeout_add_seconds + timeout_add_seconds = _timeout_add_seconds +except AttributeError: + timeout_add_seconds = _old_timeout_add_seconds diff --git a/dialcentral/util/io.py b/dialcentral/util/io.py new file mode 100644 index 0000000..4198f4b --- /dev/null +++ b/dialcentral/util/io.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python + + +from __future__ import with_statement + +import os +import pickle +import contextlib +import itertools +import codecs +from xml.sax import saxutils +import csv +try: + import cStringIO as StringIO +except ImportError: + import StringIO + + +@contextlib.contextmanager +def change_directory(directory): + previousDirectory = os.getcwd() + os.chdir(directory) + currentDirectory = os.getcwd() + + try: + yield previousDirectory, currentDirectory + finally: + os.chdir(previousDirectory) + + +@contextlib.contextmanager +def pickled(filename): + """ + Here is an example usage: + with pickled("foo.db") as p: + p("users", list).append(["srid", "passwd", 23]) + """ + + if os.path.isfile(filename): + data = pickle.load(open(filename)) + else: + data = {} + + def getter(item, factory): + if item in data: + return data[item] + else: + data[item] = factory() + return data[item] + + yield getter + + pickle.dump(data, open(filename, "w")) + + +@contextlib.contextmanager +def redirect(object_, attr, value): + """ + >>> import sys + ... with redirect(sys, 'stdout', open('stdout', 'w')): + ... print "hello" + ... + >>> print "we're back" + we're back + """ + orig = getattr(object_, attr) + setattr(object_, attr, value) + try: + yield + finally: + setattr(object_, attr, orig) + + +def pathsplit(path): + """ + >>> pathsplit("/a/b/c") + ['', 'a', 'b', 'c'] + >>> pathsplit("./plugins/builtins.ini") + ['.', 'plugins', 'builtins.ini'] + """ + pathParts = path.split(os.path.sep) + return pathParts + + +def commonpath(l1, l2, common=None): + """ + >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1')) + (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1']) + >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini")) + (['.', 'plugins'], [''], ['builtins.ini']) + >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins")) + (['.', 'plugins'], ['builtins'], []) + """ + if common is None: + common = [] + + if l1 == l2: + return l1, [], [] + + for i, (leftDir, rightDir) in enumerate(zip(l1, l2)): + if leftDir != rightDir: + return l1[0:i], l1[i:], l2[i:] + else: + if leftDir == rightDir: + i += 1 + return l1[0:i], l1[i:], l2[i:] + + +def relpath(p1, p2): + """ + >>> relpath('/', '/') + './' + >>> relpath('/a/b/c/d', '/') + '../../../../' + >>> relpath('/a/b/c/d', '/a/b/c1/d1') + '../../c1/d1' + >>> relpath('/a/b/c/d', '/a/b/c1/d1/') + '../../c1/d1' + >>> relpath("./plugins/builtins", "./plugins") + '../' + >>> relpath("./plugins/", "./plugins/builtins.ini") + 'builtins.ini' + """ + sourcePath = os.path.normpath(p1) + destPath = os.path.normpath(p2) + + (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath)) + if len(sourceOnly) or len(destOnly): + relParts = itertools.chain( + (('..' + os.sep) * len(sourceOnly), ), + destOnly, + ) + return os.path.join(*relParts) + else: + return "."+os.sep + + +class UTF8Recoder(object): + """ + Iterator that reads an encoded stream and reencodes the input to UTF-8 + """ + def __init__(self, f, encoding): + self.reader = codecs.getreader(encoding)(f) + + def __iter__(self): + return self + + def next(self): + return self.reader.next().encode("utf-8") + + +class UnicodeReader(object): + """ + A CSV reader which will iterate over lines in the CSV file "f", + which is encoded in the given encoding. + """ + + def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): + f = UTF8Recoder(f, encoding) + self.reader = csv.reader(f, dialect=dialect, **kwds) + + def next(self): + row = self.reader.next() + return [unicode(s, "utf-8") for s in row] + + def __iter__(self): + return self + +class UnicodeWriter(object): + """ + A CSV writer which will write rows to CSV file "f", + which is encoded in the given encoding. + """ + + def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): + # Redirect output to a queue + self.queue = StringIO.StringIO() + self.writer = csv.writer(self.queue, dialect=dialect, **kwds) + self.stream = f + self.encoder = codecs.getincrementalencoder(encoding)() + + def writerow(self, row): + self.writer.writerow([s.encode("utf-8") for s in row]) + # Fetch UTF-8 output from the queue ... + data = self.queue.getvalue() + data = data.decode("utf-8") + # ... and reencode it into the target encoding + data = self.encoder.encode(data) + # write to the target stream + self.stream.write(data) + # empty queue + self.queue.truncate(0) + + def writerows(self, rows): + for row in rows: + self.writerow(row) + + +def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs): + # csv.py doesn't do Unicode; encode temporarily as UTF-8: + csv_reader = csv.reader(utf_8_encoder(unicode_csv_data), + dialect=dialect, **kwargs) + for row in csv_reader: + # decode UTF-8 back to Unicode, cell by cell: + yield [unicode(cell, 'utf-8') for cell in row] + + +def utf_8_encoder(unicode_csv_data): + for line in unicode_csv_data: + yield line.encode('utf-8') + + +_UNESCAPE_ENTITIES = { + """: '"', + " ": " ", + "'": "'", +} + + +_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys())) +del _ESCAPE_ENTITIES[" "] + + +def unescape(text): + plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) + return plain + + +def escape(text): + fancy = saxutils.escape(text, _ESCAPE_ENTITIES) + return fancy diff --git a/dialcentral/util/linux.py b/dialcentral/util/linux.py new file mode 100644 index 0000000..4e77445 --- /dev/null +++ b/dialcentral/util/linux.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + + +import os +import logging + +try: + from xdg import BaseDirectory as _BaseDirectory + BaseDirectory = _BaseDirectory +except ImportError: + BaseDirectory = None + + +_moduleLogger = logging.getLogger(__name__) + + +_libc = None + + +def set_process_name(name): + try: # change process name for killall + global _libc + if _libc is None: + import ctypes + _libc = ctypes.CDLL('libc.so.6') + _libc.prctl(15, name, 0, 0, 0) + except Exception, e: + _moduleLogger.warning('Unable to set processName: %s" % e') + + +def get_new_resource(resourceType, resource, name): + if BaseDirectory is not None: + if resourceType == "data": + base = BaseDirectory.xdg_data_home + if base == "/usr/share/mime": + # Ugly hack because somehow Maemo 4.1 seems to be set to this + base = os.path.join(os.path.expanduser("~"), ".%s" % resource) + elif resourceType == "config": + base = BaseDirectory.xdg_config_home + elif resourceType == "cache": + base = BaseDirectory.xdg_cache_home + else: + raise RuntimeError("Unknown type: "+resourceType) + else: + base = os.path.join(os.path.expanduser("~"), ".%s" % resource) + + filePath = os.path.join(base, resource, name) + dirPath = os.path.dirname(filePath) + if not os.path.exists(dirPath): + # Looking before I leap to not mask errors + os.makedirs(dirPath) + + return filePath + + +def get_existing_resource(resourceType, resource, name): + if BaseDirectory is not None: + if resourceType == "data": + base = BaseDirectory.xdg_data_home + elif resourceType == "config": + base = BaseDirectory.xdg_config_home + elif resourceType == "cache": + base = BaseDirectory.xdg_cache_home + else: + raise RuntimeError("Unknown type: "+resourceType) + else: + base = None + + if base is not None: + finalPath = os.path.join(base, name) + if os.path.exists(finalPath): + return finalPath + + altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource) + finalPath = os.path.join(altBase, name) + if os.path.exists(finalPath): + return finalPath + else: + raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), )) diff --git a/dialcentral/util/misc.py b/dialcentral/util/misc.py new file mode 100644 index 0000000..9b8d88c --- /dev/null +++ b/dialcentral/util/misc.py @@ -0,0 +1,900 @@ +#!/usr/bin/env python + +from __future__ import with_statement + +import sys +import re +import cPickle + +import functools +import contextlib +import inspect + +import optparse +import traceback +import warnings +import string + + +class AnyData(object): + + pass + + +_indentationLevel = [0] + + +def log_call(logger): + + def log_call_decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwds): + logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, )) + _indentationLevel[0] += 1 + try: + return func(*args, **kwds) + finally: + _indentationLevel[0] -= 1 + logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, )) + + return wrapper + + return log_call_decorator + + +def log_exception(logger): + + def log_exception_decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwds): + try: + return func(*args, **kwds) + except Exception: + logger.exception(func.__name__) + raise + + return wrapper + + return log_exception_decorator + + +def printfmt(template): + """ + This hides having to create the Template object and call substitute/safe_substitute on it. For example: + + >>> num = 10 + >>> word = "spam" + >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP + I would like to order 10 units of spam, please + """ + frame = inspect.stack()[-1][0] + try: + print string.Template(template).safe_substitute(frame.f_locals) + finally: + del frame + + +def is_special(name): + return name.startswith("__") and name.endswith("__") + + +def is_private(name): + return name.startswith("_") and not is_special(name) + + +def privatize(clsName, attributeName): + """ + At runtime, make an attributeName private + + Example: + >>> class Test(object): + ... pass + ... + >>> try: + ... dir(Test).index("_Test__me") + ... print dir(Test) + ... except: + ... print "Not Found" + Not Found + >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World") + >>> try: + ... dir(Test).index("_Test__me") + ... print "Found" + ... except: + ... print dir(Test) + 0 + Found + >>> print getattr(Test, obfuscate(Test.__name__, "__me")) + Hello World + >>> + >>> is_private(privatize(Test.__name__, "me")) + True + >>> is_special(privatize(Test.__name__, "me")) + False + """ + return "".join(["_", clsName, "__", attributeName]) + + +def obfuscate(clsName, attributeName): + """ + At runtime, turn a private name into the obfuscated form + + Example: + >>> class Test(object): + ... __me = "Hello World" + ... + >>> try: + ... dir(Test).index("_Test__me") + ... print "Found" + ... except: + ... print dir(Test) + 0 + Found + >>> print getattr(Test, obfuscate(Test.__name__, "__me")) + Hello World + >>> is_private(obfuscate(Test.__name__, "__me")) + True + >>> is_special(obfuscate(Test.__name__, "__me")) + False + """ + return "".join(["_", clsName, attributeName]) + + +class PAOptionParser(optparse.OptionParser, object): + """ + >>> if __name__ == '__main__': + ... #parser = PAOptionParser("My usage str") + ... parser = PAOptionParser() + ... parser.add_posarg("Foo", help="Foo usage") + ... parser.add_posarg("Bar", dest="bar_dest") + ... parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other")) + ... parser.add_option('--stocksym', dest='symbol') + ... values, args = parser.parse_args() + ... print values, args + ... + + python mycp.py -h + python mycp.py + python mycp.py foo + python mycp.py foo bar + + python mycp.py foo bar lava + Usage: pa.py [options] + + Positional Arguments: + Foo: Foo usage + Bar: + Language: + + pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other' + """ + + def __init__(self, *args, **kw): + self.posargs = [] + super(PAOptionParser, self).__init__(*args, **kw) + + def add_posarg(self, *args, **kw): + pa_help = kw.get("help", "") + kw["help"] = optparse.SUPPRESS_HELP + o = self.add_option("--%s" % args[0], *args[1:], **kw) + self.posargs.append((args[0], pa_help)) + + def get_usage(self, *args, **kwargs): + params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs])) + self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params + return super(PAOptionParser, self).get_usage(*args, **kwargs) + + def parse_args(self, *args, **kwargs): + args = sys.argv[1:] + args0 = [] + for p, v in zip(self.posargs, args): + args0.append("--%s" % p[0]) + args0.append(v) + args = args0 + args + options, args = super(PAOptionParser, self).parse_args(args, **kwargs) + if len(args) < len(self.posargs): + msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):]) + self.error(msg) + return options, args + + +def explicitly(name, stackadd=0): + """ + This is an alias for adding to '__all__'. Less error-prone than using + __all__ itself, since setting __all__ directly is prone to stomping on + things implicitly exported via L{alias}. + + @note Taken from PyExport (which could turn out pretty cool): + @li @a http://codebrowse.launchpad.net/~glyph/ + @li @a http://glyf.livejournal.com/74356.html + """ + packageVars = sys._getframe(1+stackadd).f_locals + globalAll = packageVars.setdefault('__all__', []) + globalAll.append(name) + + +def public(thunk): + """ + This is a decorator, for convenience. Rather than typing the name of your + function twice, you can decorate a function with this. + + To be real, @public would need to work on methods as well, which gets into + supporting types... + + @note Taken from PyExport (which could turn out pretty cool): + @li @a http://codebrowse.launchpad.net/~glyph/ + @li @a http://glyf.livejournal.com/74356.html + """ + explicitly(thunk.__name__, 1) + return thunk + + +def _append_docstring(obj, message): + if obj.__doc__ is None: + obj.__doc__ = message + else: + obj.__doc__ += message + + +def validate_decorator(decorator): + + def simple(x): + return x + + f = simple + f.__name__ = "name" + f.__doc__ = "doc" + f.__dict__["member"] = True + + g = decorator(f) + + if f.__name__ != g.__name__: + print f.__name__, "!=", g.__name__ + + if g.__doc__ is None: + print decorator.__name__, "has no doc string" + elif not g.__doc__.startswith(f.__doc__): + print g.__doc__, "didn't start with", f.__doc__ + + if not ("member" in g.__dict__ and g.__dict__["member"]): + print "'member' not in ", g.__dict__ + + +def deprecated_api(func): + """ + This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + + >>> validate_decorator(deprecated_api) + """ + + @functools.wraps(func) + def newFunc(*args, **kwargs): + warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning) + return func(*args, **kwargs) + + _append_docstring(newFunc, "\n@deprecated") + return newFunc + + +def unstable_api(func): + """ + This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + + >>> validate_decorator(unstable_api) + """ + + @functools.wraps(func) + def newFunc(*args, **kwargs): + warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning) + return func(*args, **kwargs) + _append_docstring(newFunc, "\n@unstable") + return newFunc + + +def enabled(func): + """ + This decorator doesn't add any behavior + + >>> validate_decorator(enabled) + """ + return func + + +def disabled(func): + """ + This decorator disables the provided function, and does nothing + + >>> validate_decorator(disabled) + """ + + @functools.wraps(func) + def emptyFunc(*args, **kargs): + pass + _append_docstring(emptyFunc, "\n@note Temporarily Disabled") + return emptyFunc + + +def metadata(document=True, **kwds): + """ + >>> validate_decorator(metadata(author="Ed")) + """ + + def decorate(func): + for k, v in kwds.iteritems(): + setattr(func, k, v) + if document: + _append_docstring(func, "\n@"+k+" "+v) + return func + return decorate + + +def prop(func): + """Function decorator for defining property attributes + + The decorated function is expected to return a dictionary + containing one or more of the following pairs: + fget - function for getting attribute value + fset - function for setting attribute value + fdel - function for deleting attribute + This can be conveniently constructed by the locals() builtin + function; see: + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183 + @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html + + Example: + >>> #Due to transformation from function to property, does not need to be validated + >>> #validate_decorator(prop) + >>> class MyExampleClass(object): + ... @prop + ... def foo(): + ... "The foo property attribute's doc-string" + ... def fget(self): + ... print "GET" + ... return self._foo + ... def fset(self, value): + ... print "SET" + ... self._foo = value + ... return locals() + ... + >>> me = MyExampleClass() + >>> me.foo = 10 + SET + >>> print me.foo + GET + 10 + """ + return property(doc=func.__doc__, **func()) + + +def print_handler(e): + """ + @see ExpHandler + """ + print "%s: %s" % (type(e).__name__, e) + + +def print_ignore(e): + """ + @see ExpHandler + """ + print 'Ignoring %s exception: %s' % (type(e).__name__, e) + + +def print_traceback(e): + """ + @see ExpHandler + """ + #print sys.exc_info() + traceback.print_exc(file=sys.stdout) + + +def ExpHandler(handler = print_handler, *exceptions): + """ + An exception handling idiom using decorators + Examples + Specify exceptions in order, first one is handled first + last one last. + + >>> validate_decorator(ExpHandler()) + >>> @ExpHandler(print_ignore, ZeroDivisionError) + ... @ExpHandler(None, AttributeError, ValueError) + ... def f1(): + ... 1/0 + >>> @ExpHandler(print_traceback, ZeroDivisionError) + ... def f2(): + ... 1/0 + >>> @ExpHandler() + ... def f3(*pargs): + ... l = pargs + ... return l[10] + >>> @ExpHandler(print_traceback, ZeroDivisionError) + ... def f4(): + ... return 1 + >>> + >>> + >>> f1() + Ignoring ZeroDivisionError exception: integer division or modulo by zero + >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Traceback (most recent call last): + ... + ZeroDivisionError: integer division or modulo by zero + >>> f3() + IndexError: tuple index out of range + >>> f4() + 1 + """ + + def wrapper(f): + localExceptions = exceptions + if not localExceptions: + localExceptions = [Exception] + t = [(ex, handler) for ex in localExceptions] + t.reverse() + + def newfunc(t, *args, **kwargs): + ex, handler = t[0] + try: + if len(t) == 1: + return f(*args, **kwargs) + else: + #Recurse for embedded try/excepts + dec_func = functools.partial(newfunc, t[1:]) + dec_func = functools.update_wrapper(dec_func, f) + return dec_func(*args, **kwargs) + except ex, e: + return handler(e) + + dec_func = functools.partial(newfunc, t) + dec_func = functools.update_wrapper(dec_func, f) + return dec_func + return wrapper + + +def into_debugger(func): + """ + >>> validate_decorator(into_debugger) + """ + + @functools.wraps(func) + def newFunc(*args, **kwargs): + try: + return func(*args, **kwargs) + except: + import pdb + pdb.post_mortem() + + return newFunc + + +class bindclass(object): + """ + >>> validate_decorator(bindclass) + >>> class Foo(BoundObject): + ... @bindclass + ... def foo(this_class, self): + ... return this_class, self + ... + >>> class Bar(Foo): + ... @bindclass + ... def bar(this_class, self): + ... return this_class, self + ... + >>> f = Foo() + >>> b = Bar() + >>> + >>> f.foo() # doctest: +ELLIPSIS + (, <...Foo object at ...>) + >>> b.foo() # doctest: +ELLIPSIS + (, <...Bar object at ...>) + >>> b.bar() # doctest: +ELLIPSIS + (, <...Bar object at ...>) + """ + + def __init__(self, f): + self.f = f + self.__name__ = f.__name__ + self.__doc__ = f.__doc__ + self.__dict__.update(f.__dict__) + self.m = None + + def bind(self, cls, attr): + + def bound_m(*args, **kwargs): + return self.f(cls, *args, **kwargs) + bound_m.__name__ = attr + self.m = bound_m + + def __get__(self, obj, objtype=None): + return self.m.__get__(obj, objtype) + + +class ClassBindingSupport(type): + "@see bindclass" + + def __init__(mcs, name, bases, attrs): + type.__init__(mcs, name, bases, attrs) + for attr, val in attrs.iteritems(): + if isinstance(val, bindclass): + val.bind(mcs, attr) + + +class BoundObject(object): + "@see bindclass" + __metaclass__ = ClassBindingSupport + + +def bindfunction(f): + """ + >>> validate_decorator(bindfunction) + >>> @bindfunction + ... def factorial(thisfunction, n): + ... # Within this function the name 'thisfunction' refers to the factorial + ... # function(with only one argument), even after 'factorial' is bound + ... # to another object + ... if n > 0: + ... return n * thisfunction(n - 1) + ... else: + ... return 1 + ... + >>> factorial(3) + 6 + """ + + @functools.wraps(f) + def bound_f(*args, **kwargs): + return f(bound_f, *args, **kwargs) + return bound_f + + +class Memoize(object): + """ + Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will only work on functions with non-mutable arguments + @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201 + + >>> validate_decorator(Memoize) + """ + + def __init__(self, fn): + self.fn = fn + self.__name__ = fn.__name__ + self.__doc__ = fn.__doc__ + self.__dict__.update(fn.__dict__) + self.memo = {} + + def __call__(self, *args): + if args not in self.memo: + self.memo[args] = self.fn(*args) + return self.memo[args] + + +class MemoizeMutable(object): + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will work on functions with mutable arguments(slower than Memoize) + @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201 + + >>> validate_decorator(MemoizeMutable) + """ + + def __init__(self, fn): + self.fn = fn + self.__name__ = fn.__name__ + self.__doc__ = fn.__doc__ + self.__dict__.update(fn.__dict__) + self.memo = {} + + def __call__(self, *args, **kw): + text = cPickle.dumps((args, kw)) + if text not in self.memo: + self.memo[text] = self.fn(*args, **kw) + return self.memo[text] + + +callTraceIndentationLevel = 0 + + +def call_trace(f): + """ + Synchronization decorator. + + >>> validate_decorator(call_trace) + >>> @call_trace + ... def a(a, b, c): + ... pass + >>> a(1, 2, c=3) + Entering a((1, 2), {'c': 3}) + Exiting a((1, 2), {'c': 3}) + """ + + @functools.wraps(f) + def verboseTrace(*args, **kw): + global callTraceIndentationLevel + + print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) + callTraceIndentationLevel += 1 + try: + result = f(*args, **kw) + except: + callTraceIndentationLevel -= 1 + print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) + raise + callTraceIndentationLevel -= 1 + print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) + return result + + @functools.wraps(f) + def smallTrace(*args, **kw): + global callTraceIndentationLevel + + print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__) + callTraceIndentationLevel += 1 + try: + result = f(*args, **kw) + except: + callTraceIndentationLevel -= 1 + print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__) + raise + callTraceIndentationLevel -= 1 + print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__) + return result + + #return smallTrace + return verboseTrace + + +@contextlib.contextmanager +def nested_break(): + """ + >>> with nested_break() as mylabel: + ... for i in xrange(3): + ... print "Outer", i + ... for j in xrange(3): + ... if i == 2: raise mylabel + ... if j == 2: break + ... print "Inner", j + ... print "more processing" + Outer 0 + Inner 0 + Inner 1 + Outer 1 + Inner 0 + Inner 1 + Outer 2 + """ + + class NestedBreakException(Exception): + pass + + try: + yield NestedBreakException + except NestedBreakException: + pass + + +@contextlib.contextmanager +def lexical_scope(*args): + """ + @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586 + Example: + >>> b = 0 + >>> with lexical_scope(1) as (a): + ... print a + ... + 1 + >>> with lexical_scope(1,2,3) as (a,b,c): + ... print a,b,c + ... + 1 2 3 + >>> with lexical_scope(): + ... d = 10 + ... def foo(): + ... pass + ... + >>> print b + 2 + """ + + frame = inspect.currentframe().f_back.f_back + saved = frame.f_locals.keys() + try: + if not args: + yield + elif len(args) == 1: + yield args[0] + else: + yield args + finally: + f_locals = frame.f_locals + for key in (x for x in f_locals.keys() if x not in saved): + del f_locals[key] + del frame + + +def normalize_number(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> normalize_number("+012-(345)-678-90") + '+01234567890' + >>> normalize_number("1-(345)-678-9000") + '+13456789000' + >>> normalize_number("+1-(345)-678-9000") + '+13456789000' + """ + uglynumber = re.sub('[^0-9+]', '', prettynumber) + if uglynumber.startswith("+"): + pass + elif uglynumber.startswith("1"): + uglynumber = "+"+uglynumber + elif 10 <= len(uglynumber): + assert uglynumber[0] not in ("+", "1"), "Number format confusing" + uglynumber = "+1"+uglynumber + else: + pass + + return uglynumber + + +_VALIDATE_RE = re.compile("^\+?[0-9]{10,}$") + + +def is_valid_number(number): + """ + @returns If This number be called ( syntax validation only ) + """ + return _VALIDATE_RE.match(number) is not None + + +def make_ugly(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> make_ugly("+012-(345)-678-90") + '+01234567890' + """ + return normalize_number(prettynumber) + + +def _make_pretty_with_areacode(phonenumber): + prettynumber = "(%s)" % (phonenumber[0:3], ) + if 3 < len(phonenumber): + prettynumber += " %s" % (phonenumber[3:6], ) + if 6 < len(phonenumber): + prettynumber += "-%s" % (phonenumber[6:], ) + return prettynumber + + +def _make_pretty_local(phonenumber): + prettynumber = "%s" % (phonenumber[0:3], ) + if 3 < len(phonenumber): + prettynumber += "-%s" % (phonenumber[3:], ) + return prettynumber + + +def _make_pretty_international(phonenumber): + prettynumber = phonenumber + if phonenumber.startswith("1"): + prettynumber = "1 " + prettynumber += _make_pretty_with_areacode(phonenumber[1:]) + return prettynumber + + +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") + '+1 (234) 567-8901' + >>> make_pretty("12345678901") + '+1 (234) 567-8901' + >>> make_pretty("01234567890") + '+012 (345) 678-90' + >>> make_pretty("+01234567890") + '+012 (345) 678-90' + >>> make_pretty("+12") + '+1 (2)' + >>> make_pretty("+123") + '+1 (23)' + >>> make_pretty("+1234") + '+1 (234)' + """ + if phonenumber is None or phonenumber == "": + return "" + + phonenumber = normalize_number(phonenumber) + + if phonenumber == "": + return "" + elif phonenumber[0] == "+": + prettynumber = _make_pretty_international(phonenumber[1:]) + if not prettynumber.startswith("+"): + prettynumber = "+"+prettynumber + elif 8 < len(phonenumber) and phonenumber[0] in ("1", ): + prettynumber = _make_pretty_international(phonenumber) + elif 7 < len(phonenumber): + prettynumber = _make_pretty_with_areacode(phonenumber) + elif 3 < len(phonenumber): + prettynumber = _make_pretty_local(phonenumber) + else: + prettynumber = phonenumber + return prettynumber.strip() + + +def similar_ugly_numbers(lhs, rhs): + return ( + lhs == rhs or + lhs[1:] == rhs and lhs.startswith("1") or + lhs[2:] == rhs and lhs.startswith("+1") or + lhs == rhs[1:] and rhs.startswith("1") or + lhs == rhs[2:] and rhs.startswith("+1") + ) + + +def abbrev_relative_date(date): + """ + >>> abbrev_relative_date("42 hours ago") + '42 h' + >>> abbrev_relative_date("2 days ago") + '2 d' + >>> abbrev_relative_date("4 weeks ago") + '4 w' + """ + parts = date.split(" ") + return "%s %s" % (parts[0], parts[1][0]) + + +def parse_version(versionText): + """ + >>> parse_version("0.5.2") + [0, 5, 2] + """ + return [ + int(number) + for number in versionText.split(".") + ] + + +def compare_versions(leftParsedVersion, rightParsedVersion): + """ + >>> compare_versions([0, 1, 2], [0, 1, 2]) + 0 + >>> compare_versions([0, 1, 2], [0, 1, 3]) + -1 + >>> compare_versions([0, 1, 2], [0, 2, 2]) + -1 + >>> compare_versions([0, 1, 2], [1, 1, 2]) + -1 + >>> compare_versions([0, 1, 3], [0, 1, 2]) + 1 + >>> compare_versions([0, 2, 2], [0, 1, 2]) + 1 + >>> compare_versions([1, 1, 2], [0, 1, 2]) + 1 + """ + for left, right in zip(leftParsedVersion, rightParsedVersion): + if left < right: + return -1 + elif right < left: + return 1 + else: + return 0 diff --git a/dialcentral/util/overloading.py b/dialcentral/util/overloading.py new file mode 100644 index 0000000..89cb738 --- /dev/null +++ b/dialcentral/util/overloading.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python +import new + +# Make the environment more like Python 3.0 +__metaclass__ = type +from itertools import izip as zip +import textwrap +import inspect + + +__all__ = [ + "AnyType", + "overloaded" +] + + +AnyType = object + + +class overloaded: + """ + Dynamically overloaded functions. + + This is an implementation of (dynamically, or run-time) overloaded + functions; also known as generic functions or multi-methods. + + The dispatch algorithm uses the types of all argument for dispatch, + similar to (compile-time) overloaded functions or methods in C++ and + Java. + + Most of the complexity in the algorithm comes from the need to support + subclasses in call signatures. For example, if an function is + registered for a signature (T1, T2), then a call with a signature (S1, + S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass + of T2, and there are no other more specific matches (see below). + + If there are multiple matches and one of those doesn't *dominate* all + others, the match is deemed ambiguous and an exception is raised. A + subtlety here: if, after removing the dominated matches, there are + still multiple matches left, but they all map to the same function, + then the match is not deemed ambiguous and that function is used. + Read the method find_func() below for details. + + @note Python 2.5 is required due to the use of predicates any() and all(). + @note only supports positional arguments + + @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514 + + >>> import misc + >>> misc.validate_decorator (overloaded) + >>> + >>> + >>> + >>> + >>> ################# + >>> #Basics, with reusing names and without + >>> @overloaded + ... def foo(x): + ... "prints x" + ... print x + ... + >>> @foo.register(int) + ... def foo(x): + ... "prints the hex representation of x" + ... print hex(x) + ... + >>> from types import DictType + >>> @foo.register(DictType) + ... def foo_dict(x): + ... "prints the keys of x" + ... print [k for k in x.iterkeys()] + ... + >>> #combines all of the doc strings to help keep track of the specializations + >>> foo.__doc__ # doctest: +ELLIPSIS + "prints x\\n\\n...overloading.foo ():\\n\\tprints the hex representation of x\\n\\n...overloading.foo_dict ():\\n\\tprints the keys of x" + >>> foo ("text") + text + >>> foo (10) #calling the specialized foo + 0xa + >>> foo ({3:5, 6:7}) #calling the specialization foo_dict + [3, 6] + >>> foo_dict ({3:5, 6:7}) #with using a unique name, you still have the option of calling the function directly + [3, 6] + >>> + >>> + >>> + >>> + >>> ################# + >>> #Multiple arguments, accessing the default, and function finding + >>> @overloaded + ... def two_arg (x, y): + ... print x,y + ... + >>> @two_arg.register(int, int) + ... def two_arg_int_int (x, y): + ... print hex(x), hex(y) + ... + >>> @two_arg.register(float, int) + ... def two_arg_float_int (x, y): + ... print x, hex(y) + ... + >>> @two_arg.register(int, float) + ... def two_arg_int_float (x, y): + ... print hex(x), y + ... + >>> two_arg.__doc__ # doctest: +ELLIPSIS + "...overloading.two_arg_int_int (, ):\\n\\n...overloading.two_arg_float_int (, ):\\n\\n...overloading.two_arg_int_float (, ):" + >>> two_arg(9, 10) + 0x9 0xa + >>> two_arg(9.0, 10) + 9.0 0xa + >>> two_arg(15, 16.0) + 0xf 16.0 + >>> two_arg.default_func(9, 10) + 9 10 + >>> two_arg.find_func ((int, float)) == two_arg_int_float + True + >>> (int, float) in two_arg + True + >>> (str, int) in two_arg + False + >>> + >>> + >>> + >>> ################# + >>> #wildcard + >>> @two_arg.register(AnyType, str) + ... def two_arg_any_str (x, y): + ... print x, y.lower() + ... + >>> two_arg("Hello", "World") + Hello world + >>> two_arg(500, "World") + 500 world + """ + + def __init__(self, default_func): + # Decorator to declare new overloaded function. + self.registry = {} + self.cache = {} + self.default_func = default_func + self.__name__ = self.default_func.__name__ + self.__doc__ = self.default_func.__doc__ + self.__dict__.update (self.default_func.__dict__) + + def __get__(self, obj, type=None): + if obj is None: + return self + return new.instancemethod(self, obj) + + def register(self, *types): + """ + Decorator to register an implementation for a specific set of types. + + .register(t1, t2)(f) is equivalent to .register_func((t1, t2), f). + """ + + def helper(func): + self.register_func(types, func) + + originalDoc = self.__doc__ if self.__doc__ is not None else "" + typeNames = ", ".join ([str(type) for type in types]) + typeNames = "".join ([func.__module__+".", func.__name__, " (", typeNames, "):"]) + overloadedDoc = "" + if func.__doc__ is not None: + overloadedDoc = textwrap.fill (func.__doc__, width=60, initial_indent="\t", subsequent_indent="\t") + self.__doc__ = "\n".join ([originalDoc, "", typeNames, overloadedDoc]).strip() + + new_func = func + + #Masking the function, so we want to take on its traits + if func.__name__ == self.__name__: + self.__dict__.update (func.__dict__) + new_func = self + return new_func + + return helper + + def register_func(self, types, func): + """Helper to register an implementation.""" + self.registry[tuple(types)] = func + self.cache = {} # Clear the cache (later we can optimize this). + + def __call__(self, *args): + """Call the overloaded function.""" + types = tuple(map(type, args)) + func = self.cache.get(types) + if func is None: + self.cache[types] = func = self.find_func(types) + return func(*args) + + def __contains__ (self, types): + return self.find_func(types) is not self.default_func + + def find_func(self, types): + """Find the appropriate overloaded function; don't call it. + + @note This won't work for old-style classes or classes without __mro__ + """ + func = self.registry.get(types) + if func is not None: + # Easy case -- direct hit in registry. + return func + + # Phillip Eby suggests to use issubclass() instead of __mro__. + # There are advantages and disadvantages. + + # I can't help myself -- this is going to be intense functional code. + # Find all possible candidate signatures. + mros = tuple(inspect.getmro(t) for t in types) + n = len(mros) + candidates = [sig for sig in self.registry + if len(sig) == n and + all(t in mro for t, mro in zip(sig, mros))] + + if not candidates: + # No match at all -- use the default function. + return self.default_func + elif len(candidates) == 1: + # Unique match -- that's an easy case. + return self.registry[candidates[0]] + + # More than one match -- weed out the subordinate ones. + + def dominates(dom, sub, + orders=tuple(dict((t, i) for i, t in enumerate(mro)) + for mro in mros)): + # Predicate to decide whether dom strictly dominates sub. + # Strict domination is defined as domination without equality. + # The arguments dom and sub are type tuples of equal length. + # The orders argument is a precomputed auxiliary data structure + # giving dicts of ordering information corresponding to the + # positions in the type tuples. + # A type d dominates a type s iff order[d] <= order[s]. + # A type tuple (d1, d2, ...) dominates a type tuple of equal length + # (s1, s2, ...) iff d1 dominates s1, d2 dominates s2, etc. + if dom is sub: + return False + return all(order[d] <= order[s] for d, s, order in zip(dom, sub, orders)) + + # I suppose I could inline dominates() but it wouldn't get any clearer. + candidates = [cand + for cand in candidates + if not any(dominates(dom, cand) for dom in candidates)] + if len(candidates) == 1: + # There's exactly one candidate left. + return self.registry[candidates[0]] + + # Perhaps these multiple candidates all have the same implementation? + funcs = set(self.registry[cand] for cand in candidates) + if len(funcs) == 1: + return funcs.pop() + + # No, the situation is irreducibly ambiguous. + raise TypeError("ambigous call; types=%r; candidates=%r" % + (types, candidates)) diff --git a/dialcentral/util/qore_utils.py b/dialcentral/util/qore_utils.py new file mode 100644 index 0000000..153558d --- /dev/null +++ b/dialcentral/util/qore_utils.py @@ -0,0 +1,99 @@ +import logging + +import qt_compat +QtCore = qt_compat.QtCore + +import misc + + +_moduleLogger = logging.getLogger(__name__) + + +class QThread44(QtCore.QThread): + """ + This is to imitate QThread in Qt 4.4+ for when running on older version + See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong + (On Lucid I have Qt 4.7 and this is still an issue) + """ + + def __init__(self, parent = None): + QtCore.QThread.__init__(self, parent) + + def run(self): + self.exec_() + + +class _WorkerThread(QtCore.QObject): + + _taskComplete = qt_compat.Signal(object) + + def __init__(self, futureThread): + QtCore.QObject.__init__(self) + self._futureThread = futureThread + self._futureThread._addTask.connect(self._on_task_added) + self._taskComplete.connect(self._futureThread._on_task_complete) + + @qt_compat.Slot(object) + def _on_task_added(self, task): + self.__on_task_added(task) + + @misc.log_exception(_moduleLogger) + def __on_task_added(self, task): + if not self._futureThread._isRunning: + _moduleLogger.error("Dropping task") + + func, args, kwds, on_success, on_error = task + + try: + result = func(*args, **kwds) + isError = False + except Exception, e: + _moduleLogger.error("Error, passing it back to the main thread") + result = e + isError = True + + taskResult = on_success, on_error, isError, result + self._taskComplete.emit(taskResult) + + +class FutureThread(QtCore.QObject): + + _addTask = qt_compat.Signal(object) + + def __init__(self): + QtCore.QObject.__init__(self) + self._thread = QThread44() + self._isRunning = False + self._worker = _WorkerThread(self) + self._worker.moveToThread(self._thread) + + def start(self): + self._thread.start() + self._isRunning = True + + def stop(self): + self._isRunning = False + self._thread.quit() + + def add_task(self, func, args, kwds, on_success, on_error): + assert self._isRunning, "Task queue not started" + task = func, args, kwds, on_success, on_error + self._addTask.emit(task) + + @qt_compat.Slot(object) + def _on_task_complete(self, taskResult): + self.__on_task_complete(taskResult) + + @misc.log_exception(_moduleLogger) + def __on_task_complete(self, taskResult): + on_success, on_error, isError, result = taskResult + if not self._isRunning: + if isError: + _moduleLogger.error("Masking: %s" % (result, )) + isError = True + result = StopIteration("Cancelling all callbacks") + callback = on_success if not isError else on_error + try: + callback(result) + except Exception: + _moduleLogger.exception("Callback errored") diff --git a/dialcentral/util/qt_compat.py b/dialcentral/util/qt_compat.py new file mode 100644 index 0000000..2ab7fa4 --- /dev/null +++ b/dialcentral/util/qt_compat.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +#try: +# import PySide.QtCore as _QtCore +# QtCore = _QtCore +# USES_PYSIDE = True +#except ImportError: +if True: + import sip + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) + import PyQt4.QtCore as _QtCore + QtCore = _QtCore + USES_PYSIDE = False + + +def _pyside_import_module(moduleName): + pyside = __import__('PySide', globals(), locals(), [moduleName], -1) + return getattr(pyside, moduleName) + + +def _pyqt4_import_module(moduleName): + pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1) + return getattr(pyside, moduleName) + + +if USES_PYSIDE: + import_module = _pyside_import_module + + Signal = QtCore.Signal + Slot = QtCore.Slot + Property = QtCore.Property +else: + import_module = _pyqt4_import_module + + Signal = QtCore.pyqtSignal + Slot = QtCore.pyqtSlot + Property = QtCore.pyqtProperty + + +if __name__ == "__main__": + pass + diff --git a/dialcentral/util/qtpie.py b/dialcentral/util/qtpie.py new file mode 100755 index 0000000..6b77d5d --- /dev/null +++ b/dialcentral/util/qtpie.py @@ -0,0 +1,1094 @@ +#!/usr/bin/env python + +import math +import logging + +import qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +import misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +_TWOPI = 2 * math.pi + + +def _radius_at(center, pos): + delta = pos - center + xDelta = delta.x() + yDelta = delta.y() + + radius = math.sqrt(xDelta ** 2 + yDelta ** 2) + return radius + + +def _angle_at(center, pos): + delta = pos - center + xDelta = delta.x() + yDelta = delta.y() + + radius = math.sqrt(xDelta ** 2 + yDelta ** 2) + angle = math.acos(xDelta / radius) + if 0 <= yDelta: + angle = _TWOPI - angle + + return angle + + +class QActionPieItem(object): + + def __init__(self, action, weight = 1): + self._action = action + self._weight = weight + + def action(self): + return self._action + + def setWeight(self, weight): + self._weight = weight + + def weight(self): + return self._weight + + def setEnabled(self, enabled = True): + self._action.setEnabled(enabled) + + def isEnabled(self): + return self._action.isEnabled() + + +class PieFiling(object): + + INNER_RADIUS_DEFAULT = 64 + OUTER_RADIUS_DEFAULT = 192 + + SELECTION_CENTER = -1 + SELECTION_NONE = -2 + + NULL_CENTER = QActionPieItem(QtGui.QAction(None)) + + def __init__(self): + self._innerRadius = self.INNER_RADIUS_DEFAULT + self._outerRadius = self.OUTER_RADIUS_DEFAULT + self._children = [] + self._center = self.NULL_CENTER + + self._cacheIndexToAngle = {} + self._cacheTotalWeight = 0 + + def insertItem(self, item, index = -1): + self._children.insert(index, item) + self._invalidate_cache() + + def removeItemAt(self, index): + item = self._children.pop(index) + self._invalidate_cache() + + def set_center(self, item): + if item is None: + item = self.NULL_CENTER + self._center = item + + def center(self): + return self._center + + def clear(self): + del self._children[:] + self._center = self.NULL_CENTER + self._invalidate_cache() + + def itemAt(self, index): + return self._children[index] + + def indexAt(self, center, point): + return self._angle_to_index(_angle_at(center, point)) + + def innerRadius(self): + return self._innerRadius + + def setInnerRadius(self, radius): + self._innerRadius = radius + + def outerRadius(self): + return self._outerRadius + + def setOuterRadius(self, radius): + self._outerRadius = radius + + def __iter__(self): + return iter(self._children) + + def __len__(self): + return len(self._children) + + def __getitem__(self, index): + return self._children[index] + + def _invalidate_cache(self): + self._cacheIndexToAngle.clear() + self._cacheTotalWeight = sum(child.weight() for child in self._children) + if self._cacheTotalWeight == 0: + self._cacheTotalWeight = 1 + + def _index_to_angle(self, index, isShifted): + key = index, isShifted + if key in self._cacheIndexToAngle: + return self._cacheIndexToAngle[key] + index = index % len(self._children) + + baseAngle = _TWOPI / self._cacheTotalWeight + + angle = math.pi / 2 + if isShifted: + if self._children: + angle -= (self._children[0].weight() * baseAngle) / 2 + else: + angle -= baseAngle / 2 + while angle < 0: + angle += _TWOPI + + for i, child in enumerate(self._children): + if index < i: + break + angle += child.weight() * baseAngle + while _TWOPI < angle: + angle -= _TWOPI + + self._cacheIndexToAngle[key] = angle + return angle + + def _angle_to_index(self, angle): + numChildren = len(self._children) + if numChildren == 0: + return self.SELECTION_CENTER + + baseAngle = _TWOPI / self._cacheTotalWeight + + iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2 + while iterAngle < 0: + iterAngle += _TWOPI + + oldIterAngle = iterAngle + for index, child in enumerate(self._children): + iterAngle += child.weight() * baseAngle + if oldIterAngle < angle and angle <= iterAngle: + return index - 1 if index != 0 else numChildren - 1 + elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle): + return index - 1 if index != 0 else numChildren - 1 + oldIterAngle = iterAngle + + +class PieArtist(object): + + ICON_SIZE_DEFAULT = 48 + + SHAPE_CIRCLE = "circle" + SHAPE_SQUARE = "square" + DEFAULT_SHAPE = SHAPE_SQUARE + + BACKGROUND_FILL = "fill" + BACKGROUND_NOFILL = "no fill" + + def __init__(self, filing, background = BACKGROUND_FILL): + self._filing = filing + + self._cachedOuterRadius = self._filing.outerRadius() + self._cachedInnerRadius = self._filing.innerRadius() + canvasSize = self._cachedOuterRadius * 2 + 1 + self._canvas = QtGui.QPixmap(canvasSize, canvasSize) + self._mask = None + self._backgroundState = background + self.palette = None + + def pieSize(self): + diameter = self._filing.outerRadius() * 2 + 1 + return QtCore.QSize(diameter, diameter) + + def centerSize(self): + painter = QtGui.QPainter(self._canvas) + text = self._filing.center().action().text() + fontMetrics = painter.fontMetrics() + if text: + textBoundingRect = fontMetrics.boundingRect(text) + else: + textBoundingRect = QtCore.QRect() + textWidth = textBoundingRect.width() + textHeight = textBoundingRect.height() + + return QtCore.QSize( + textWidth + self.ICON_SIZE_DEFAULT, + max(textHeight, self.ICON_SIZE_DEFAULT), + ) + + def show(self, palette): + self.palette = palette + + if ( + self._cachedOuterRadius != self._filing.outerRadius() or + self._cachedInnerRadius != self._filing.innerRadius() + ): + self._cachedOuterRadius = self._filing.outerRadius() + self._cachedInnerRadius = self._filing.innerRadius() + self._canvas = self._canvas.scaled(self.pieSize()) + + if self._mask is None: + self._mask = QtGui.QBitmap(self._canvas.size()) + self._mask.fill(QtCore.Qt.color0) + self._generate_mask(self._mask) + self._canvas.setMask(self._mask) + return self._mask + + def hide(self): + self.palette = None + + def paint(self, selectionIndex): + painter = QtGui.QPainter(self._canvas) + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) + + self.paintPainter(selectionIndex, painter) + + return self._canvas + + def paintPainter(self, selectionIndex, painter): + adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1) + + numChildren = len(self._filing) + if numChildren == 0: + self._paint_center_background(painter, adjustmentRect, selectionIndex) + self._paint_center_foreground(painter, adjustmentRect, selectionIndex) + return self._canvas + else: + for i in xrange(len(self._filing)): + self._paint_slice_background(painter, adjustmentRect, i, selectionIndex) + + self._paint_center_background(painter, adjustmentRect, selectionIndex) + self._paint_center_foreground(painter, adjustmentRect, selectionIndex) + + for i in xrange(len(self._filing)): + self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex) + + def _generate_mask(self, mask): + """ + Specifies on the mask the shape of the pie menu + """ + painter = QtGui.QPainter(mask) + painter.setPen(QtCore.Qt.color1) + painter.setBrush(QtCore.Qt.color1) + if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: + painter.drawRect(mask.rect()) + elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: + painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1)) + else: + raise NotImplementedError(self.DEFAULT_SHAPE) + + def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex): + if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: + currentWidth = adjustmentRect.width() + newWidth = math.sqrt(2) * currentWidth + dx = (newWidth - currentWidth) / 2 + adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx) + elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: + pass + else: + raise NotImplementedError(self.DEFAULT_SHAPE) + + if self._backgroundState == self.BACKGROUND_NOFILL: + painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent)) + painter.setPen(self.palette.highlight().color()) + else: + if i == selectionIndex and self._filing[i].isEnabled(): + painter.setBrush(self.palette.highlight()) + painter.setPen(self.palette.highlight().color()) + else: + painter.setBrush(self.palette.window()) + painter.setPen(self.palette.window().color()) + + a = self._filing._index_to_angle(i, True) + b = self._filing._index_to_angle(i + 1, True) + if b < a: + b += _TWOPI + size = b - a + if size < 0: + size += _TWOPI + + startAngleInDeg = (a * 360 * 16) / _TWOPI + sizeInDeg = (size * 360 * 16) / _TWOPI + painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg)) + + def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex): + child = self._filing[i] + + a = self._filing._index_to_angle(i, True) + b = self._filing._index_to_angle(i + 1, True) + if b < a: + b += _TWOPI + middleAngle = (a + b) / 2 + averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2 + + sliceX = averageRadius * math.cos(middleAngle) + sliceY = - averageRadius * math.sin(middleAngle) + + piePos = adjustmentRect.center() + pieX = piePos.x() + pieY = piePos.y() + self._paint_label( + painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY + ) + + def _paint_label(self, painter, action, isSelected, x, y): + text = action.text() + fontMetrics = painter.fontMetrics() + if text: + textBoundingRect = fontMetrics.boundingRect(text) + else: + textBoundingRect = QtCore.QRect() + textWidth = textBoundingRect.width() + textHeight = textBoundingRect.height() + + icon = action.icon().pixmap( + QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT), + QtGui.QIcon.Normal, + QtGui.QIcon.On, + ) + iconWidth = icon.width() + iconHeight = icon.width() + averageWidth = (iconWidth + textWidth)/2 + if not icon.isNull(): + iconRect = QtCore.QRect( + x - averageWidth, + y - iconHeight/2, + iconWidth, + iconHeight, + ) + + painter.drawPixmap(iconRect, icon) + + if text: + if isSelected: + if action.isEnabled(): + pen = self.palette.highlightedText() + brush = self.palette.highlight() + else: + pen = self.palette.mid() + brush = self.palette.window() + else: + if action.isEnabled(): + pen = self.palette.windowText() + else: + pen = self.palette.mid() + brush = self.palette.window() + + leftX = x - averageWidth + iconWidth + topY = y + textHeight/2 + painter.setPen(pen.color()) + painter.setBrush(brush) + painter.drawText(leftX, topY, text) + + def _paint_center_background(self, painter, adjustmentRect, selectionIndex): + if self._backgroundState == self.BACKGROUND_NOFILL: + return + if len(self._filing) == 0: + if self._backgroundState == self.BACKGROUND_NOFILL: + painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent)) + else: + if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled(): + painter.setBrush(self.palette.highlight()) + else: + painter.setBrush(self.palette.window()) + painter.setPen(self.palette.mid().color()) + + painter.drawRect(adjustmentRect) + else: + dark = self.palette.mid().color() + light = self.palette.light().color() + if self._backgroundState == self.BACKGROUND_NOFILL: + background = QtGui.QBrush(QtCore.Qt.transparent) + else: + if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled(): + background = self.palette.highlight().color() + else: + background = self.palette.window().color() + + innerRadius = self._cachedInnerRadius + adjustmentCenterPos = adjustmentRect.center() + innerRect = QtCore.QRect( + adjustmentCenterPos.x() - innerRadius, + adjustmentCenterPos.y() - innerRadius, + innerRadius * 2 + 1, + innerRadius * 2 + 1, + ) + + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(background) + painter.drawPie(innerRect, 0, 360 * 16) + + if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: + pass + elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: + painter.setPen(QtGui.QPen(dark, 1)) + painter.setBrush(QtCore.Qt.NoBrush) + painter.drawEllipse(adjustmentRect) + else: + raise NotImplementedError(self.DEFAULT_SHAPE) + + def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex): + centerPos = adjustmentRect.center() + pieX = centerPos.x() + pieY = centerPos.y() + + x = pieX + y = pieY + + self._paint_label( + painter, + self._filing.center().action(), + selectionIndex == PieFiling.SELECTION_CENTER, + x, y + ) + + +class QPieDisplay(QtGui.QWidget): + + def __init__(self, filing, parent = None, flags = QtCore.Qt.Window): + QtGui.QWidget.__init__(self, parent, flags) + self._filing = filing + self._artist = PieArtist(self._filing) + self._selectionIndex = PieFiling.SELECTION_NONE + + def popup(self, pos): + self._update_selection(pos) + self.show() + + def sizeHint(self): + return self._artist.pieSize() + + @misc_utils.log_exception(_moduleLogger) + def showEvent(self, showEvent): + mask = self._artist.show(self.palette()) + self.setMask(mask) + + QtGui.QWidget.showEvent(self, showEvent) + + @misc_utils.log_exception(_moduleLogger) + def hideEvent(self, hideEvent): + self._artist.hide() + self._selectionIndex = PieFiling.SELECTION_NONE + QtGui.QWidget.hideEvent(self, hideEvent) + + @misc_utils.log_exception(_moduleLogger) + def paintEvent(self, paintEvent): + canvas = self._artist.paint(self._selectionIndex) + offset = (self.size() - canvas.size()) / 2 + + screen = QtGui.QPainter(self) + screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas) + + QtGui.QWidget.paintEvent(self, paintEvent) + + def selectAt(self, index): + oldIndex = self._selectionIndex + self._selectionIndex = index + if self.isVisible(): + self.update() + + +class QPieButton(QtGui.QWidget): + + activated = qt_compat.Signal(int) + highlighted = qt_compat.Signal(int) + canceled = qt_compat.Signal() + aboutToShow = qt_compat.Signal() + aboutToHide = qt_compat.Signal() + + BUTTON_RADIUS = 24 + DELAY = 250 + + def __init__(self, buttonSlice, parent = None, buttonSlices = None): + # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these? + # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues + QtGui.QWidget.__init__(self, parent) + self._cachedCenterPosition = self.rect().center() + + self._filing = PieFiling() + self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen) + self._selectionIndex = PieFiling.SELECTION_NONE + + self._buttonFiling = PieFiling() + self._buttonFiling.set_center(buttonSlice) + if buttonSlices is not None: + for slice in buttonSlices: + self._buttonFiling.insertItem(slice) + self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS) + self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL) + self._poppedUp = False + self._pressed = False + + self._delayPopupTimer = QtCore.QTimer() + self._delayPopupTimer.setInterval(self.DELAY) + self._delayPopupTimer.setSingleShot(True) + self._delayPopupTimer.timeout.connect(self._on_delayed_popup) + self._popupLocation = None + + self._mousePosition = None + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setSizePolicy( + QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + ) + ) + + def insertItem(self, item, index = -1): + self._filing.insertItem(item, index) + + def removeItemAt(self, index): + self._filing.removeItemAt(index) + + def set_center(self, item): + self._filing.set_center(item) + + def set_button(self, item): + self.update() + + def clear(self): + self._filing.clear() + + def itemAt(self, index): + return self._filing.itemAt(index) + + def indexAt(self, point): + return self._filing.indexAt(self._cachedCenterPosition, point) + + def innerRadius(self): + return self._filing.innerRadius() + + def setInnerRadius(self, radius): + self._filing.setInnerRadius(radius) + + def outerRadius(self): + return self._filing.outerRadius() + + def setOuterRadius(self, radius): + self._filing.setOuterRadius(radius) + + def buttonRadius(self): + return self._buttonFiling.outerRadius() + + def setButtonRadius(self, radius): + self._buttonFiling.setOuterRadius(radius) + self._buttonFiling.setInnerRadius(radius / 2) + self._buttonArtist.show(self.palette()) + + def minimumSizeHint(self): + return self._buttonArtist.centerSize() + + @misc_utils.log_exception(_moduleLogger) + def mousePressEvent(self, mouseEvent): + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + self._mousePosition = lastMousePos + self._update_selection(self._cachedCenterPosition) + + self.highlighted.emit(self._selectionIndex) + + self._display.selectAt(self._selectionIndex) + self._pressed = True + self.update() + self._popupLocation = mouseEvent.globalPos() + self._delayPopupTimer.start() + + @misc_utils.log_exception(_moduleLogger) + def _on_delayed_popup(self): + assert self._popupLocation is not None, "Widget location abuse" + self._popup_child(self._popupLocation) + + @misc_utils.log_exception(_moduleLogger) + def mouseMoveEvent(self, mouseEvent): + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + if self._mousePosition is None: + # Absolute + self._update_selection(lastMousePos) + else: + # Relative + self._update_selection( + self._cachedCenterPosition + (lastMousePos - self._mousePosition), + ignoreOuter = True, + ) + + if lastSelection != self._selectionIndex: + self.highlighted.emit(self._selectionIndex) + self._display.selectAt(self._selectionIndex) + + if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive(): + self._on_delayed_popup() + + @misc_utils.log_exception(_moduleLogger) + def mouseReleaseEvent(self, mouseEvent): + self._delayPopupTimer.stop() + self._popupLocation = None + + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + if self._mousePosition is None: + # Absolute + self._update_selection(lastMousePos) + else: + # Relative + self._update_selection( + self._cachedCenterPosition + (lastMousePos - self._mousePosition), + ignoreOuter = True, + ) + self._mousePosition = None + + self._activate_at(self._selectionIndex) + self._pressed = False + self.update() + self._hide_child() + + @misc_utils.log_exception(_moduleLogger) + def keyPressEvent(self, keyEvent): + if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]: + self._popup_child(QtGui.QCursor.pos()) + if self._selectionIndex != len(self._filing) - 1: + nextSelection = self._selectionIndex + 1 + else: + nextSelection = 0 + self._select_at(nextSelection) + self._display.selectAt(self._selectionIndex) + elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]: + self._popup_child(QtGui.QCursor.pos()) + if 0 < self._selectionIndex: + nextSelection = self._selectionIndex - 1 + else: + nextSelection = len(self._filing) - 1 + self._select_at(nextSelection) + self._display.selectAt(self._selectionIndex) + elif keyEvent.key() in [QtCore.Qt.Key_Space]: + self._popup_child(QtGui.QCursor.pos()) + self._select_at(PieFiling.SELECTION_CENTER) + self._display.selectAt(self._selectionIndex) + elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]: + self._delayPopupTimer.stop() + self._popupLocation = None + self._activate_at(self._selectionIndex) + self._hide_child() + elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]: + self._delayPopupTimer.stop() + self._popupLocation = None + self._activate_at(PieFiling.SELECTION_NONE) + self._hide_child() + else: + QtGui.QWidget.keyPressEvent(self, keyEvent) + + @misc_utils.log_exception(_moduleLogger) + def resizeEvent(self, resizeEvent): + self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1) + QtGui.QWidget.resizeEvent(self, resizeEvent) + + @misc_utils.log_exception(_moduleLogger) + def showEvent(self, showEvent): + self._buttonArtist.show(self.palette()) + self._cachedCenterPosition = self.rect().center() + + QtGui.QWidget.showEvent(self, showEvent) + + @misc_utils.log_exception(_moduleLogger) + def hideEvent(self, hideEvent): + self._display.hide() + self._select_at(PieFiling.SELECTION_NONE) + QtGui.QWidget.hideEvent(self, hideEvent) + + @misc_utils.log_exception(_moduleLogger) + def paintEvent(self, paintEvent): + self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1) + if self._poppedUp: + selectionIndex = PieFiling.SELECTION_CENTER + else: + selectionIndex = PieFiling.SELECTION_NONE + + screen = QtGui.QStylePainter(self) + screen.setRenderHint(QtGui.QPainter.Antialiasing, True) + option = QtGui.QStyleOptionButton() + option.initFrom(self) + option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised + + screen.drawControl(QtGui.QStyle.CE_PushButton, option) + self._buttonArtist.paintPainter(selectionIndex, screen) + + QtGui.QWidget.paintEvent(self, paintEvent) + + def __iter__(self): + return iter(self._filing) + + def __len__(self): + return len(self._filing) + + def _popup_child(self, position): + self._poppedUp = True + self.aboutToShow.emit() + + self._delayPopupTimer.stop() + self._popupLocation = None + + position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius()) + self._display.move(position) + self._display.show() + + self.update() + + def _hide_child(self): + self._poppedUp = False + self.aboutToHide.emit() + self._display.hide() + self.update() + + def _select_at(self, index): + self._selectionIndex = index + + def _update_selection(self, lastMousePos, ignoreOuter = False): + radius = _radius_at(self._cachedCenterPosition, lastMousePos) + if radius < self._filing.innerRadius(): + self._select_at(PieFiling.SELECTION_CENTER) + elif radius <= self._filing.outerRadius() or ignoreOuter: + self._select_at(self.indexAt(lastMousePos)) + else: + self._select_at(PieFiling.SELECTION_NONE) + + def _activate_at(self, index): + if index == PieFiling.SELECTION_NONE: + self.canceled.emit() + return + elif index == PieFiling.SELECTION_CENTER: + child = self._filing.center() + else: + child = self.itemAt(index) + + if child.action().isEnabled(): + child.action().trigger() + self.activated.emit(index) + else: + self.canceled.emit() + + +class QPieMenu(QtGui.QWidget): + + activated = qt_compat.Signal(int) + highlighted = qt_compat.Signal(int) + canceled = qt_compat.Signal() + aboutToShow = qt_compat.Signal() + aboutToHide = qt_compat.Signal() + + def __init__(self, parent = None): + QtGui.QWidget.__init__(self, parent) + self._cachedCenterPosition = self.rect().center() + + self._filing = PieFiling() + self._artist = PieArtist(self._filing) + self._selectionIndex = PieFiling.SELECTION_NONE + + self._mousePosition = () + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + def popup(self, pos): + self._update_selection(pos) + self.show() + + def insertItem(self, item, index = -1): + self._filing.insertItem(item, index) + self.update() + + def removeItemAt(self, index): + self._filing.removeItemAt(index) + self.update() + + def set_center(self, item): + self._filing.set_center(item) + self.update() + + def clear(self): + self._filing.clear() + self.update() + + def itemAt(self, index): + return self._filing.itemAt(index) + + def indexAt(self, point): + return self._filing.indexAt(self._cachedCenterPosition, point) + + def innerRadius(self): + return self._filing.innerRadius() + + def setInnerRadius(self, radius): + self._filing.setInnerRadius(radius) + self.update() + + def outerRadius(self): + return self._filing.outerRadius() + + def setOuterRadius(self, radius): + self._filing.setOuterRadius(radius) + self.update() + + def sizeHint(self): + return self._artist.pieSize() + + @misc_utils.log_exception(_moduleLogger) + def mousePressEvent(self, mouseEvent): + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + self._update_selection(lastMousePos) + self._mousePosition = lastMousePos + + if lastSelection != self._selectionIndex: + self.highlighted.emit(self._selectionIndex) + self.update() + + @misc_utils.log_exception(_moduleLogger) + def mouseMoveEvent(self, mouseEvent): + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + self._update_selection(lastMousePos) + + if lastSelection != self._selectionIndex: + self.highlighted.emit(self._selectionIndex) + self.update() + + @misc_utils.log_exception(_moduleLogger) + def mouseReleaseEvent(self, mouseEvent): + lastSelection = self._selectionIndex + + lastMousePos = mouseEvent.pos() + self._update_selection(lastMousePos) + self._mousePosition = () + + self._activate_at(self._selectionIndex) + self.update() + + @misc_utils.log_exception(_moduleLogger) + def keyPressEvent(self, keyEvent): + if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]: + if self._selectionIndex != len(self._filing) - 1: + nextSelection = self._selectionIndex + 1 + else: + nextSelection = 0 + self._select_at(nextSelection) + self.update() + elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]: + if 0 < self._selectionIndex: + nextSelection = self._selectionIndex - 1 + else: + nextSelection = len(self._filing) - 1 + self._select_at(nextSelection) + self.update() + elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]: + self._activate_at(self._selectionIndex) + elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]: + self._activate_at(PieFiling.SELECTION_NONE) + else: + QtGui.QWidget.keyPressEvent(self, keyEvent) + + @misc_utils.log_exception(_moduleLogger) + def showEvent(self, showEvent): + self.aboutToShow.emit() + self._cachedCenterPosition = self.rect().center() + + mask = self._artist.show(self.palette()) + self.setMask(mask) + + lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos()) + self._update_selection(lastMousePos) + + QtGui.QWidget.showEvent(self, showEvent) + + @misc_utils.log_exception(_moduleLogger) + def hideEvent(self, hideEvent): + self._artist.hide() + self._selectionIndex = PieFiling.SELECTION_NONE + QtGui.QWidget.hideEvent(self, hideEvent) + + @misc_utils.log_exception(_moduleLogger) + def paintEvent(self, paintEvent): + canvas = self._artist.paint(self._selectionIndex) + + screen = QtGui.QPainter(self) + screen.drawPixmap(QtCore.QPoint(0, 0), canvas) + + QtGui.QWidget.paintEvent(self, paintEvent) + + def __iter__(self): + return iter(self._filing) + + def __len__(self): + return len(self._filing) + + def _select_at(self, index): + self._selectionIndex = index + + def _update_selection(self, lastMousePos): + radius = _radius_at(self._cachedCenterPosition, lastMousePos) + if radius < self._filing.innerRadius(): + self._selectionIndex = PieFiling.SELECTION_CENTER + elif radius <= self._filing.outerRadius(): + self._select_at(self.indexAt(lastMousePos)) + else: + self._selectionIndex = PieFiling.SELECTION_NONE + + def _activate_at(self, index): + if index == PieFiling.SELECTION_NONE: + self.canceled.emit() + self.aboutToHide.emit() + self.hide() + return + elif index == PieFiling.SELECTION_CENTER: + child = self._filing.center() + else: + child = self.itemAt(index) + + if child.isEnabled(): + child.action().trigger() + self.activated.emit(index) + else: + self.canceled.emit() + self.aboutToHide.emit() + self.hide() + + +def init_pies(): + PieFiling.NULL_CENTER.setEnabled(False) + + +def _print(msg): + print msg + + +def _on_about_to_hide(app): + app.exit() + + +if __name__ == "__main__": + app = QtGui.QApplication([]) + init_pies() + + if False: + pie = QPieMenu() + pie.show() + + if False: + singleAction = QtGui.QAction(None) + singleAction.setText("Boo") + singleItem = QActionPieItem(singleAction) + spie = QPieMenu() + spie.insertItem(singleItem) + spie.show() + + if False: + oneAction = QtGui.QAction(None) + oneAction.setText("Chew") + oneItem = QActionPieItem(oneAction) + twoAction = QtGui.QAction(None) + twoAction.setText("Foo") + twoItem = QActionPieItem(twoAction) + iconTextAction = QtGui.QAction(None) + iconTextAction.setText("Icon") + iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) + iconTextItem = QActionPieItem(iconTextAction) + mpie = QPieMenu() + mpie.insertItem(oneItem) + mpie.insertItem(twoItem) + mpie.insertItem(oneItem) + mpie.insertItem(iconTextItem) + mpie.show() + + if True: + oneAction = QtGui.QAction(None) + oneAction.setText("Chew") + oneAction.triggered.connect(lambda: _print("Chew")) + oneItem = QActionPieItem(oneAction) + twoAction = QtGui.QAction(None) + twoAction.setText("Foo") + twoAction.triggered.connect(lambda: _print("Foo")) + twoItem = QActionPieItem(twoAction) + iconAction = QtGui.QAction(None) + iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) + iconAction.triggered.connect(lambda: _print("Icon")) + iconItem = QActionPieItem(iconAction) + iconTextAction = QtGui.QAction(None) + iconTextAction.setText("Icon") + iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) + iconTextAction.triggered.connect(lambda: _print("Icon and text")) + iconTextItem = QActionPieItem(iconTextAction) + mpie = QPieMenu() + mpie.set_center(iconItem) + mpie.insertItem(oneItem) + mpie.insertItem(twoItem) + mpie.insertItem(oneItem) + mpie.insertItem(iconTextItem) + mpie.show() + mpie.aboutToHide.connect(lambda: _on_about_to_hide(app)) + mpie.canceled.connect(lambda: _print("Canceled")) + + if False: + oneAction = QtGui.QAction(None) + oneAction.setText("Chew") + oneAction.triggered.connect(lambda: _print("Chew")) + oneItem = QActionPieItem(oneAction) + twoAction = QtGui.QAction(None) + twoAction.setText("Foo") + twoAction.triggered.connect(lambda: _print("Foo")) + twoItem = QActionPieItem(twoAction) + iconAction = QtGui.QAction(None) + iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) + iconAction.triggered.connect(lambda: _print("Icon")) + iconItem = QActionPieItem(iconAction) + iconTextAction = QtGui.QAction(None) + iconTextAction.setText("Icon") + iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) + iconTextAction.triggered.connect(lambda: _print("Icon and text")) + iconTextItem = QActionPieItem(iconTextAction) + pieFiling = PieFiling() + pieFiling.set_center(iconItem) + pieFiling.insertItem(oneItem) + pieFiling.insertItem(twoItem) + pieFiling.insertItem(oneItem) + pieFiling.insertItem(iconTextItem) + mpie = QPieDisplay(pieFiling) + mpie.show() + + if False: + oneAction = QtGui.QAction(None) + oneAction.setText("Chew") + oneAction.triggered.connect(lambda: _print("Chew")) + oneItem = QActionPieItem(oneAction) + twoAction = QtGui.QAction(None) + twoAction.setText("Foo") + twoAction.triggered.connect(lambda: _print("Foo")) + twoItem = QActionPieItem(twoAction) + iconAction = QtGui.QAction(None) + iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) + iconAction.triggered.connect(lambda: _print("Icon")) + iconItem = QActionPieItem(iconAction) + iconTextAction = QtGui.QAction(None) + iconTextAction.setText("Icon") + iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) + iconTextAction.triggered.connect(lambda: _print("Icon and text")) + iconTextItem = QActionPieItem(iconTextAction) + mpie = QPieButton(iconItem) + mpie.set_center(iconItem) + mpie.insertItem(oneItem) + mpie.insertItem(twoItem) + mpie.insertItem(oneItem) + mpie.insertItem(iconTextItem) + mpie.show() + mpie.aboutToHide.connect(lambda: _on_about_to_hide(app)) + mpie.canceled.connect(lambda: _print("Canceled")) + + app.exec_() diff --git a/dialcentral/util/qtpieboard.py b/dialcentral/util/qtpieboard.py new file mode 100755 index 0000000..50ae9ae --- /dev/null +++ b/dialcentral/util/qtpieboard.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python + + +from __future__ import division + +import os +import warnings + +import qt_compat +QtGui = qt_compat.import_module("QtGui") + +import qtpie + + +class PieKeyboard(object): + + SLICE_CENTER = -1 + SLICE_NORTH = 0 + SLICE_NORTH_WEST = 1 + SLICE_WEST = 2 + SLICE_SOUTH_WEST = 3 + SLICE_SOUTH = 4 + SLICE_SOUTH_EAST = 5 + SLICE_EAST = 6 + SLICE_NORTH_EAST = 7 + + MAX_ANGULAR_SLICES = 8 + + SLICE_DIRECTIONS = [ + SLICE_CENTER, + SLICE_NORTH, + SLICE_NORTH_WEST, + SLICE_WEST, + SLICE_SOUTH_WEST, + SLICE_SOUTH, + SLICE_SOUTH_EAST, + SLICE_EAST, + SLICE_NORTH_EAST, + ] + + SLICE_DIRECTION_NAMES = [ + "CENTER", + "NORTH", + "NORTH_WEST", + "WEST", + "SOUTH_WEST", + "SOUTH", + "SOUTH_EAST", + "EAST", + "NORTH_EAST", + ] + + def __init__(self): + self._layout = QtGui.QGridLayout() + self._widget = QtGui.QWidget() + self._widget.setLayout(self._layout) + + self.__cells = {} + + @property + def toplevel(self): + return self._widget + + def add_pie(self, row, column, pieButton): + assert len(pieButton) == 8 + self._layout.addWidget(pieButton, row, column) + self.__cells[(row, column)] = pieButton + + def get_pie(self, row, column): + return self.__cells[(row, column)] + + +class KeyboardModifier(object): + + def __init__(self, name): + self.name = name + self.lock = False + self.once = False + + @property + def isActive(self): + return self.lock or self.once + + def on_toggle_lock(self, *args, **kwds): + self.lock = not self.lock + + def on_toggle_once(self, *args, **kwds): + self.once = not self.once + + def reset_once(self): + self.once = False + + +def parse_keyboard_data(text): + return eval(text) + + +def _enumerate_pie_slices(pieData, iconPaths): + for direction, directionName in zip( + PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES + ): + if directionName in pieData: + sliceData = pieData[directionName] + + action = QtGui.QAction(None) + try: + action.setText(sliceData["text"]) + except KeyError: + pass + try: + relativeIconPath = sliceData["path"] + except KeyError: + pass + else: + for iconPath in iconPaths: + absIconPath = os.path.join(iconPath, relativeIconPath) + if os.path.exists(absIconPath): + action.setIcon(QtGui.QIcon(absIconPath)) + break + pieItem = qtpie.QActionPieItem(action) + actionToken = sliceData["action"] + else: + pieItem = qtpie.PieFiling.NULL_CENTER + actionToken = "" + yield direction, pieItem, actionToken + + +def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths): + for (row, column), pieData in dataTree.iteritems(): + pieItems = list(_enumerate_pie_slices(pieData, iconPaths)) + assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0] + _, center, centerAction = pieItems.pop(0) + + pieButton = qtpie.QPieButton(center) + pieButton.set_center(center) + keyboardHandler.map_slice_action(center, centerAction) + for direction, pieItem, action in pieItems: + pieButton.insertItem(pieItem) + keyboardHandler.map_slice_action(pieItem, action) + keyboard.add_pie(row, column, pieButton) + + +class KeyboardHandler(object): + + def __init__(self, keyhandler): + self.__keyhandler = keyhandler + self.__commandHandlers = {} + self.__modifiers = {} + self.__sliceActions = {} + + self.register_modifier("Shift") + self.register_modifier("Super") + self.register_modifier("Control") + self.register_modifier("Alt") + + def register_command_handler(self, command, handler): + # @todo Look into hooking these up directly to the pie actions + self.__commandHandlers["[%s]" % command] = handler + + def unregister_command_handler(self, command): + # @todo Look into hooking these up directly to the pie actions + del self.__commandHandlers["[%s]" % command] + + def register_modifier(self, modifierName): + mod = KeyboardModifier(modifierName) + self.register_command_handler(modifierName, mod.on_toggle_lock) + self.__modifiers["<%s>" % modifierName] = mod + + def unregister_modifier(self, modifierName): + self.unregister_command_handler(modifierName) + del self.__modifiers["<%s>" % modifierName] + + def map_slice_action(self, slice, action): + callback = lambda direction: self(direction, action) + slice.action().triggered.connect(callback) + self.__sliceActions[slice] = (action, callback) + + def __call__(self, direction, action): + activeModifiers = [ + mod.name + for mod in self.__modifiers.itervalues() + if mod.isActive + ] + + needResetOnce = False + if action.startswith("[") and action.endswith("]"): + commandName = action[1:-1] + if action in self.__commandHandlers: + self.__commandHandlers[action](commandName, activeModifiers) + needResetOnce = True + else: + warnings.warn("Unknown command: [%s]" % commandName) + elif action.startswith("<") and action.endswith(">"): + modName = action[1:-1] + for mod in self.__modifiers.itervalues(): + if mod.name == modName: + mod.on_toggle_once() + break + else: + warnings.warn("Unknown modifier: <%s>" % modName) + else: + self.__keyhandler(action, activeModifiers) + needResetOnce = True + + if needResetOnce: + for mod in self.__modifiers.itervalues(): + mod.reset_once() diff --git a/dialcentral/util/qui_utils.py b/dialcentral/util/qui_utils.py new file mode 100644 index 0000000..11b3453 --- /dev/null +++ b/dialcentral/util/qui_utils.py @@ -0,0 +1,419 @@ +import sys +import contextlib +import datetime +import logging + +import qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +import misc + + +_moduleLogger = logging.getLogger(__name__) + + +@contextlib.contextmanager +def notify_error(log): + try: + yield + except: + log.push_exception() + + +@contextlib.contextmanager +def notify_busy(log, message): + log.push_busy(message) + try: + yield + finally: + log.pop(message) + + +class ErrorMessage(object): + + LEVEL_ERROR = 0 + LEVEL_BUSY = 1 + LEVEL_INFO = 2 + + def __init__(self, message, level): + self._message = message + self._level = level + self._time = datetime.datetime.now() + + @property + def level(self): + return self._level + + @property + def message(self): + return self._message + + def __repr__(self): + return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level) + + +class QErrorLog(QtCore.QObject): + + messagePushed = qt_compat.Signal() + messagePopped = qt_compat.Signal() + + def __init__(self): + QtCore.QObject.__init__(self) + self._messages = [] + + def push_busy(self, message): + _moduleLogger.info("Entering state: %s" % message) + self._push_message(message, ErrorMessage.LEVEL_BUSY) + + def push_message(self, message): + self._push_message(message, ErrorMessage.LEVEL_INFO) + + def push_error(self, message): + self._push_message(message, ErrorMessage.LEVEL_ERROR) + + def push_exception(self): + userMessage = str(sys.exc_info()[1]) + _moduleLogger.exception(userMessage) + self.push_error(userMessage) + + def pop(self, message = None): + if message is None: + del self._messages[0] + else: + _moduleLogger.info("Exiting state: %s" % message) + messageIndex = [ + i + for (i, error) in enumerate(self._messages) + if error.message == message + ] + # Might be removed out of order + if messageIndex: + del self._messages[messageIndex[0]] + self.messagePopped.emit() + + def peek_message(self): + return self._messages[0] + + def _push_message(self, message, level): + self._messages.append(ErrorMessage(message, level)) + # Sort is defined as stable, so this should be fine + self._messages.sort(key=lambda x: x.level) + self.messagePushed.emit() + + def __len__(self): + return len(self._messages) + + +class ErrorDisplay(object): + + _SENTINEL_ICON = QtGui.QIcon() + + def __init__(self, errorLog): + self._errorLog = errorLog + self._errorLog.messagePushed.connect(self._on_message_pushed) + self._errorLog.messagePopped.connect(self._on_message_popped) + + self._icons = None + self._severityLabel = QtGui.QLabel() + self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + + self._message = QtGui.QLabel() + self._message.setText("Boo") + self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self._message.setWordWrap(True) + + self._closeLabel = None + + self._controlLayout = QtGui.QHBoxLayout() + self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter) + self._controlLayout.addWidget(self._message, 1000) + + self._widget = QtGui.QWidget() + self._widget.setLayout(self._controlLayout) + self._widget.hide() + + @property + def toplevel(self): + return self._widget + + def _show_error(self): + if self._icons is None: + self._icons = { + ErrorMessage.LEVEL_BUSY: + get_theme_icon( + #("process-working", "view-refresh", "general_refresh", "gtk-refresh") + ("view-refresh", "general_refresh", "gtk-refresh", ) + ).pixmap(32, 32), + ErrorMessage.LEVEL_INFO: + get_theme_icon( + ("dialog-information", "general_notes", "gtk-info") + ).pixmap(32, 32), + ErrorMessage.LEVEL_ERROR: + get_theme_icon( + ("dialog-error", "app_install_error", "gtk-dialog-error") + ).pixmap(32, 32), + } + if self._closeLabel is None: + closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON) + if closeIcon is not self._SENTINEL_ICON: + self._closeLabel = QtGui.QPushButton(closeIcon, "") + else: + self._closeLabel = QtGui.QPushButton("X") + self._closeLabel.clicked.connect(self._on_close) + self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter) + error = self._errorLog.peek_message() + self._message.setText(error.message) + self._severityLabel.setPixmap(self._icons[error.level]) + self._widget.show() + + @qt_compat.Slot() + @qt_compat.Slot(bool) + @misc.log_exception(_moduleLogger) + def _on_close(self, checked = False): + self._errorLog.pop() + + @qt_compat.Slot() + @misc.log_exception(_moduleLogger) + def _on_message_pushed(self): + self._show_error() + + @qt_compat.Slot() + @misc.log_exception(_moduleLogger) + def _on_message_popped(self): + if len(self._errorLog) == 0: + self._message.setText("") + self._widget.hide() + else: + self._show_error() + + +class QHtmlDelegate(QtGui.QStyledItemDelegate): + + UNDEFINED_SIZE = -1 + + def __init__(self, *args, **kwd): + QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd) + self._width = self.UNDEFINED_SIZE + + def paint(self, painter, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + if newOption.widget is not None: + style = newOption.widget.style() + else: + style = QtGui.QApplication.style() + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + doc.setTextWidth(newOption.rect.width()) + + newOption.text = "" + style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter) + + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() + if newOption.state & QtGui.QStyle.State_Selected: + ctx.palette.setColor( + QtGui.QPalette.Text, + newOption.palette.color( + QtGui.QPalette.Active, + QtGui.QPalette.HighlightedText + ) + ) + else: + ctx.palette.setColor( + QtGui.QPalette.Text, + newOption.palette.color( + QtGui.QPalette.Active, + QtGui.QPalette.Text + ) + ) + + textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption) + painter.save() + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + doc.documentLayout().draw(painter, ctx) + painter.restore() + + def setWidth(self, width, model): + if self._width == width: + return + self._width = width + for c in xrange(model.rowCount()): + cItem = model.item(c, 0) + for r in xrange(model.rowCount()): + rItem = cItem.child(r, 0) + rIndex = model.indexFromItem(rItem) + self.sizeHintChanged.emit(rIndex) + return + + def sizeHint(self, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + if self._width != self.UNDEFINED_SIZE: + width = self._width + else: + width = newOption.rect.width() + doc.setTextWidth(width) + size = QtCore.QSize(doc.idealWidth(), doc.size().height()) + return size + + +class QSignalingMainWindow(QtGui.QMainWindow): + + closed = qt_compat.Signal() + hidden = qt_compat.Signal() + shown = qt_compat.Signal() + resized = qt_compat.Signal() + + def __init__(self, *args, **kwd): + QtGui.QMainWindow.__init__(*((self, )+args), **kwd) + + def closeEvent(self, event): + val = QtGui.QMainWindow.closeEvent(self, event) + self.closed.emit() + return val + + def hideEvent(self, event): + val = QtGui.QMainWindow.hideEvent(self, event) + self.hidden.emit() + return val + + def showEvent(self, event): + val = QtGui.QMainWindow.showEvent(self, event) + self.shown.emit() + return val + + def resizeEvent(self, event): + val = QtGui.QMainWindow.resizeEvent(self, event) + self.resized.emit() + return val + +def set_current_index(selector, itemText, default = 0): + for i in xrange(selector.count()): + if selector.itemText(i) == itemText: + selector.setCurrentIndex(i) + break + else: + itemText.setCurrentIndex(default) + + +def _null_set_stackable(window, isStackable): + pass + + +def _maemo_set_stackable(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5StackedWindow + set_stackable = _maemo_set_stackable +except AttributeError: + set_stackable = _null_set_stackable + + +def _null_set_autorient(window, doAutoOrient): + pass + + +def _maemo_set_autorient(window, doAutoOrient): + window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient) + + +try: + QtCore.Qt.WA_Maemo5AutoOrientation + set_autorient = _maemo_set_autorient +except AttributeError: + set_autorient = _null_set_autorient + + +def screen_orientation(): + geom = QtGui.QApplication.desktop().screenGeometry() + if geom.width() <= geom.height(): + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + +def _null_set_window_orientation(window, orientation): + pass + + +def _maemo_set_window_orientation(window, orientation): + if orientation == QtCore.Qt.Vertical: + window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False) + window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True) + elif orientation == QtCore.Qt.Horizontal: + window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True) + window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False) + elif orientation is None: + window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False) + window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False) + else: + raise RuntimeError("Unknown orientation: %r" % orientation) + + +try: + QtCore.Qt.WA_Maemo5LandscapeOrientation + QtCore.Qt.WA_Maemo5PortraitOrientation + set_window_orientation = _maemo_set_window_orientation +except AttributeError: + set_window_orientation = _null_set_window_orientation + + +def _null_show_progress_indicator(window, isStackable): + pass + + +def _maemo_show_progress_indicator(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable) + + +try: + QtCore.Qt.WA_Maemo5ShowProgressIndicator + show_progress_indicator = _maemo_show_progress_indicator +except AttributeError: + show_progress_indicator = _null_show_progress_indicator + + +def _null_mark_numbers_preferred(widget): + pass + + +def _newqt_mark_numbers_preferred(widget): + widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers) + + +try: + QtCore.Qt.ImhPreferNumbers + mark_numbers_preferred = _newqt_mark_numbers_preferred +except AttributeError: + mark_numbers_preferred = _null_mark_numbers_preferred + + +def _null_get_theme_icon(iconNames, fallback = None): + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +def _newqt_get_theme_icon(iconNames, fallback = None): + for iconName in iconNames: + if QtGui.QIcon.hasThemeIcon(iconName): + icon = QtGui.QIcon.fromTheme(iconName) + break + else: + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +try: + QtGui.QIcon.fromTheme + get_theme_icon = _newqt_get_theme_icon +except AttributeError: + get_theme_icon = _null_get_theme_icon + diff --git a/dialcentral/util/qwrappers.py b/dialcentral/util/qwrappers.py new file mode 100644 index 0000000..2c50c8a --- /dev/null +++ b/dialcentral/util/qwrappers.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python + +from __future__ import with_statement +from __future__ import division + +import logging + +import qt_compat +QtCore = qt_compat.QtCore +QtGui = qt_compat.import_module("QtGui") + +from util import qui_utils +from util import misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class ApplicationWrapper(object): + + DEFAULT_ORIENTATION = "Default" + AUTO_ORIENTATION = "Auto" + LANDSCAPE_ORIENTATION = "Landscape" + PORTRAIT_ORIENTATION = "Portrait" + + def __init__(self, qapp, constants): + self._constants = constants + self._qapp = qapp + self._clipboard = QtGui.QApplication.clipboard() + + self._errorLog = qui_utils.QErrorLog() + self._mainWindow = None + + self._fullscreenAction = QtGui.QAction(None) + self._fullscreenAction.setText("Fullscreen") + self._fullscreenAction.setCheckable(True) + self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter")) + self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen) + + self._orientation = self.DEFAULT_ORIENTATION + self._orientationAction = QtGui.QAction(None) + self._orientationAction.setText("Next Orientation") + self._orientationAction.setCheckable(True) + self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o")) + self._orientationAction.triggered.connect(self._on_next_orientation) + + self._logAction = QtGui.QAction(None) + self._logAction.setText("Log") + self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l")) + self._logAction.triggered.connect(self._on_log) + + self._quitAction = QtGui.QAction(None) + self._quitAction.setText("Quit") + self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q")) + self._quitAction.triggered.connect(self._on_quit) + + self._aboutAction = QtGui.QAction(None) + self._aboutAction.setText("About") + self._aboutAction.triggered.connect(self._on_about) + + self._qapp.lastWindowClosed.connect(self._on_app_quit) + self._mainWindow = self._new_main_window() + self._mainWindow.window.destroyed.connect(self._on_child_close) + + self.load_settings() + + self._mainWindow.show() + self._idleDelay = QtCore.QTimer() + self._idleDelay.setSingleShot(True) + self._idleDelay.setInterval(0) + self._idleDelay.timeout.connect(self._on_delayed_start) + self._idleDelay.start() + + def load_settings(self): + raise NotImplementedError("Booh") + + def save_settings(self): + raise NotImplementedError("Booh") + + def _new_main_window(self): + raise NotImplementedError("Booh") + + @property + def qapp(self): + return self._qapp + + @property + def constants(self): + return self._constants + + @property + def errorLog(self): + return self._errorLog + + @property + def fullscreenAction(self): + return self._fullscreenAction + + @property + def orientationAction(self): + return self._orientationAction + + @property + def orientation(self): + return self._orientation + + @property + def logAction(self): + return self._logAction + + @property + def aboutAction(self): + return self._aboutAction + + @property + def quitAction(self): + return self._quitAction + + def set_orientation(self, orientation): + self._orientation = orientation + self._mainWindow.update_orientation(self._orientation) + + @classmethod + def _next_orientation(cls, current): + return { + cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION, + cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION, + cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION, + cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION, + }[current] + + def _close_windows(self): + if self._mainWindow is not None: + self.save_settings() + self._mainWindow.window.destroyed.disconnect(self._on_child_close) + self._mainWindow.close() + self._mainWindow = None + + @misc_utils.log_exception(_moduleLogger) + def _on_delayed_start(self): + self._mainWindow.start() + + @misc_utils.log_exception(_moduleLogger) + def _on_app_quit(self, checked = False): + if self._mainWindow is not None: + self.save_settings() + self._mainWindow.destroy() + + @misc_utils.log_exception(_moduleLogger) + def _on_child_close(self, obj = None): + if self._mainWindow is not None: + self.save_settings() + self._mainWindow = None + + @misc_utils.log_exception(_moduleLogger) + def _on_toggle_fullscreen(self, checked = False): + with qui_utils.notify_error(self._errorLog): + self._mainWindow.set_fullscreen(checked) + + @misc_utils.log_exception(_moduleLogger) + def _on_next_orientation(self, checked = False): + with qui_utils.notify_error(self._errorLog): + self.set_orientation(self._next_orientation(self._orientation)) + + @misc_utils.log_exception(_moduleLogger) + def _on_about(self, checked = True): + raise NotImplementedError("Booh") + + @misc_utils.log_exception(_moduleLogger) + def _on_log(self, checked = False): + with qui_utils.notify_error(self._errorLog): + with open(self._constants._user_logpath_, "r") as f: + logLines = f.xreadlines() + log = "".join(logLines) + self._clipboard.setText(log) + + @misc_utils.log_exception(_moduleLogger) + def _on_quit(self, checked = False): + with qui_utils.notify_error(self._errorLog): + self._close_windows() + + +class WindowWrapper(object): + + def __init__(self, parent, app): + self._app = app + + self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog) + + self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight) + self._layout.setContentsMargins(0, 0, 0, 0) + + self._superLayout = QtGui.QVBoxLayout() + self._superLayout.addWidget(self._errorDisplay.toplevel) + self._superLayout.setContentsMargins(0, 0, 0, 0) + self._superLayout.addLayout(self._layout) + + centralWidget = QtGui.QWidget() + centralWidget.setLayout(self._superLayout) + centralWidget.setContentsMargins(0, 0, 0, 0) + + self._window = qui_utils.QSignalingMainWindow(parent) + self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + qui_utils.set_stackable(self._window, True) + self._window.setCentralWidget(centralWidget) + + self._closeWindowAction = QtGui.QAction(None) + self._closeWindowAction.setText("Close") + self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) + self._closeWindowAction.triggered.connect(self._on_close_window) + + self._window.addAction(self._closeWindowAction) + self._window.addAction(self._app.quitAction) + self._window.addAction(self._app.fullscreenAction) + self._window.addAction(self._app.orientationAction) + self._window.addAction(self._app.logAction) + + @property + def window(self): + return self._window + + @property + def windowOrientation(self): + geom = self._window.size() + if geom.width() <= geom.height(): + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + @property + def idealWindowOrientation(self): + if self._app.orientation == self._app.AUTO_ORIENTATION: + windowOrientation = self.windowOrientation + elif self._app.orientation == self._app.DEFAULT_ORIENTATION: + windowOrientation = qui_utils.screen_orientation() + elif self._app.orientation == self._app.LANDSCAPE_ORIENTATION: + windowOrientation = QtCore.Qt.Horizontal + elif self._app.orientation == self._app.PORTRAIT_ORIENTATION: + windowOrientation = QtCore.Qt.Vertical + else: + raise RuntimeError("Bad! No %r for you" % self._app.orientation) + return windowOrientation + + def walk_children(self): + return () + + def start(self): + pass + + def close(self): + for child in self.walk_children(): + child.window.destroyed.disconnect(self._on_child_close) + child.close() + self._window.close() + + def destroy(self): + pass + + def show(self): + self._window.show() + for child in self.walk_children(): + child.show() + self.set_fullscreen(self._app.fullscreenAction.isChecked()) + + def hide(self): + for child in self.walk_children(): + child.hide() + self._window.hide() + + def set_fullscreen(self, isFullscreen): + if self._window.isVisible(): + if isFullscreen: + self._window.showFullScreen() + else: + self._window.showNormal() + for child in self.walk_children(): + child.set_fullscreen(isFullscreen) + + def update_orientation(self, orientation): + if orientation == self._app.DEFAULT_ORIENTATION: + qui_utils.set_autorient(self.window, False) + qui_utils.set_window_orientation(self.window, None) + elif orientation == self._app.AUTO_ORIENTATION: + qui_utils.set_autorient(self.window, True) + qui_utils.set_window_orientation(self.window, None) + elif orientation == self._app.LANDSCAPE_ORIENTATION: + qui_utils.set_autorient(self.window, False) + qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal) + elif orientation == self._app.PORTRAIT_ORIENTATION: + qui_utils.set_autorient(self.window, False) + qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical) + else: + raise RuntimeError("Unknown orientation: %r" % orientation) + for child in self.walk_children(): + child.update_orientation(orientation) + + @misc_utils.log_exception(_moduleLogger) + def _on_child_close(self, obj = None): + raise NotImplementedError("Booh") + + @misc_utils.log_exception(_moduleLogger) + def _on_close_window(self, checked = True): + with qui_utils.notify_error(self._errorLog): + self.close() + + +class AutoFreezeWindowFeature(object): + + def __init__(self, app, window): + self._app = app + self._window = window + self._app.qapp.focusChanged.connect(self._on_focus_changed) + if self._app.qapp.focusWidget() is not None: + self._window.setUpdatesEnabled(True) + else: + self._window.setUpdatesEnabled(False) + + def close(self): + self._app.qapp.focusChanged.disconnect(self._on_focus_changed) + self._window.setUpdatesEnabled(True) + + @misc_utils.log_exception(_moduleLogger) + def _on_focus_changed(self, oldWindow, newWindow): + with qui_utils.notify_error(self._app.errorLog): + if oldWindow is None and newWindow is not None: + self._window.setUpdatesEnabled(True) + elif oldWindow is not None and newWindow is None: + self._window.setUpdatesEnabled(False) diff --git a/dialcentral/util/time_utils.py b/dialcentral/util/time_utils.py new file mode 100644 index 0000000..90ec84d --- /dev/null +++ b/dialcentral/util/time_utils.py @@ -0,0 +1,94 @@ +from datetime import tzinfo, timedelta, datetime + +ZERO = timedelta(0) +HOUR = timedelta(hours=1) + + +def first_sunday_on_or_after(dt): + days_to_go = 6 - dt.weekday() + if days_to_go: + dt += timedelta(days_to_go) + return dt + + +# US DST Rules +# +# This is a simplified (i.e., wrong for a few cases) set of rules for US +# DST start and end times. For a complete and up-to-date set of DST rules +# and timezone definitions, visit the Olson Database (or try pytz): +# http://www.twinsun.com/tz/tz-link.htm +# http://sourceforge.net/projects/pytz/ (might not be up-to-date) +# +# In the US, since 2007, DST starts at 2am (standard time) on the second +# Sunday in March, which is the first Sunday on or after Mar 8. +DSTSTART_2007 = datetime(1, 3, 8, 2) +# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov. +DSTEND_2007 = datetime(1, 11, 1, 1) +# From 1987 to 2006, DST used to start at 2am (standard time) on the first +# Sunday in April and to end at 2am (DST time; 1am standard time) on the last +# Sunday of October, which is the first Sunday on or after Oct 25. +DSTSTART_1987_2006 = datetime(1, 4, 1, 2) +DSTEND_1987_2006 = datetime(1, 10, 25, 1) +# From 1967 to 1986, DST used to start at 2am (standard time) on the last +# Sunday in April (the one on or after April 24) and to end at 2am (DST time; +# 1am standard time) on the last Sunday of October, which is the first Sunday +# on or after Oct 25. +DSTSTART_1967_1986 = datetime(1, 4, 24, 2) +DSTEND_1967_1986 = DSTEND_1987_2006 + + +class USTimeZone(tzinfo): + + def __init__(self, hours, reprname, stdname, dstname): + self.stdoffset = timedelta(hours=hours) + self.reprname = reprname + self.stdname = stdname + self.dstname = dstname + + def __repr__(self): + return self.reprname + + def tzname(self, dt): + if self.dst(dt): + return self.dstname + else: + return self.stdname + + def utcoffset(self, dt): + return self.stdoffset + self.dst(dt) + + def dst(self, dt): + if dt is None or dt.tzinfo is None: + # An exception may be sensible here, in one or both cases. + # It depends on how you want to treat them. The default + # fromutc() implementation (called by the default astimezone() + # implementation) passes a datetime with dt.tzinfo is self. + return ZERO + assert dt.tzinfo is self + + # Find start and end times for US DST. For years before 1967, return + # ZERO for no DST. + if 2006 < dt.year: + dststart, dstend = DSTSTART_2007, DSTEND_2007 + elif 1986 < dt.year < 2007: + dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 + elif 1966 < dt.year < 1987: + dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 + else: + return ZERO + + start = first_sunday_on_or_after(dststart.replace(year=dt.year)) + end = first_sunday_on_or_after(dstend.replace(year=dt.year)) + + # Can't compare naive to aware objects, so strip the timezone from + # dt first. + if start <= dt.replace(tzinfo=None) < end: + return HOUR + else: + return ZERO + + +Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") +Central = USTimeZone(-6, "Central", "CST", "CDT") +Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") +Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/dialcentral/util/tp_utils.py b/dialcentral/util/tp_utils.py new file mode 100644 index 0000000..7c55c42 --- /dev/null +++ b/dialcentral/util/tp_utils.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python + +import logging + +import dbus +import telepathy + +import util.go_utils as gobject_utils +import misc + + +_moduleLogger = logging.getLogger(__name__) +DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' + + +class WasMissedCall(object): + + def __init__(self, bus, conn, chan, on_success, on_error): + self.__on_success = on_success + self.__on_error = on_error + + self._requested = None + self._didMembersChange = False + self._didClose = False + self._didReport = False + + self._onTimeout = gobject_utils.Timeout(self._on_timeout) + self._onTimeout.start(seconds=60) + + chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal( + "MembersChanged", + self._on_members_changed, + ) + + chan[telepathy.interfaces.CHANNEL].connect_to_signal( + "Closed", + self._on_closed, + ) + + chan[DBUS_PROPERTIES].GetAll( + telepathy.interfaces.CHANNEL_INTERFACE, + reply_handler = self._on_got_all, + error_handler = self._on_error, + ) + + def cancel(self): + self._report_error("by request") + + def _report_missed_if_ready(self): + if self._didReport: + pass + elif self._requested is not None and (self._didMembersChange or self._didClose): + if self._requested: + self._report_error("wrong direction") + elif self._didClose: + self._report_success() + else: + self._report_error("members added") + else: + if self._didClose: + self._report_error("closed too early") + + def _report_success(self): + assert not self._didReport, "Double reporting a missed call" + self._didReport = True + self._onTimeout.cancel() + self.__on_success(self) + + def _report_error(self, reason): + assert not self._didReport, "Double reporting a missed call" + self._didReport = True + self._onTimeout.cancel() + self.__on_error(self, reason) + + @misc.log_exception(_moduleLogger) + def _on_got_all(self, properties): + self._requested = properties["Requested"] + self._report_missed_if_ready() + + @misc.log_exception(_moduleLogger) + def _on_members_changed(self, message, added, removed, lp, rp, actor, reason): + if added: + self._didMembersChange = True + self._report_missed_if_ready() + + @misc.log_exception(_moduleLogger) + def _on_closed(self): + self._didClose = True + self._report_missed_if_ready() + + @misc.log_exception(_moduleLogger) + def _on_error(self, *args): + self._report_error(args) + + @misc.log_exception(_moduleLogger) + def _on_timeout(self): + self._report_error("timeout") + return False + + +class NewChannelSignaller(object): + + def __init__(self, on_new_channel): + self._sessionBus = dbus.SessionBus() + self._on_user_new_channel = on_new_channel + + def start(self): + self._sessionBus.add_signal_receiver( + self._on_new_channel, + "NewChannel", + "org.freedesktop.Telepathy.Connection", + None, + None + ) + + def stop(self): + self._sessionBus.remove_signal_receiver( + self._on_new_channel, + "NewChannel", + "org.freedesktop.Telepathy.Connection", + None, + None + ) + + @misc.log_exception(_moduleLogger) + def _on_new_channel( + self, channelObjectPath, channelType, handleType, handle, supressHandler + ): + connObjectPath = channel_path_to_conn_path(channelObjectPath) + serviceName = path_to_service_name(channelObjectPath) + try: + self._on_user_new_channel( + self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType + ) + except Exception: + _moduleLogger.exception("Blocking exception from being passed up") + + +class EnableSystemContactIntegration(object): + + ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager" + ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager" + ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query" + ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat" + ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile" + DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' + + def __init__(self, profileName): + self._bus = dbus.SessionBus() + self._profileName = profileName + + def start(self): + self._accountManager = self._bus.get_object( + self.ACCOUNT_MGR_NAME, + self.ACCOUNT_MGR_PATH, + ) + self._accountManagerQuery = dbus.Interface( + self._accountManager, + dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY, + ) + + self._accountManagerQuery.FindAccounts( + { + self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName, + }, + reply_handler = self._on_found_accounts_reply, + error_handler = self._on_error, + ) + + @misc.log_exception(_moduleLogger) + def _on_found_accounts_reply(self, accountObjectPaths): + for accountObjectPath in accountObjectPaths: + print accountObjectPath + account = self._bus.get_object( + self.ACCOUNT_MGR_NAME, + accountObjectPath, + ) + accountProperties = dbus.Interface( + account, + self.DBUS_PROPERTIES, + ) + accountProperties.Set( + self.ACCOUNT_IFACE_COMPAT, + "SecondaryVCardFields", + ["TEL"], + reply_handler = self._on_field_set, + error_handler = self._on_error, + ) + + @misc.log_exception(_moduleLogger) + def _on_field_set(self): + _moduleLogger.info("SecondaryVCardFields Set") + + @misc.log_exception(_moduleLogger) + def _on_error(self, error): + _moduleLogger.error("%r" % (error, )) + + +def channel_path_to_conn_path(channelObjectPath): + """ + >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME' + """ + return channelObjectPath.rsplit("/", 1)[0] + + +def path_to_service_name(path): + """ + >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + 'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME' + """ + return ".".join(path[1:].split("/")[0:7]) + + +def cm_from_path(path): + """ + >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") + 'theonering' + """ + return path[1:].split("/")[4] diff --git a/src b/src new file mode 120000 index 0000000..14858ed --- /dev/null +++ b/src @@ -0,0 +1 @@ +dialcentral/ \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 4265cc3..0000000 --- a/src/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env python diff --git a/src/alarm_handler.py b/src/alarm_handler.py deleted file mode 100644 index a79f992..0000000 --- a/src/alarm_handler.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/env python - -import os -import time -import datetime -import ConfigParser -import logging - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore -import dbus - - -_FREMANTLE_ALARM = "Fremantle" -_DIABLO_ALARM = "Diablo" -_NO_ALARM = "None" - - -try: - import alarm - ALARM_TYPE = _FREMANTLE_ALARM -except (ImportError, OSError): - try: - import osso.alarmd as alarmd - ALARM_TYPE = _DIABLO_ALARM - except (ImportError, OSError): - ALARM_TYPE = _NO_ALARM - - -_moduleLogger = logging.getLogger(__name__) - - -def _get_start_time(recurrence): - now = datetime.datetime.now() - startTimeMinute = now.minute + max(recurrence, 5) # being safe - startTimeHour = now.hour + int(startTimeMinute / 60) - startTimeMinute = startTimeMinute % 59 - now.replace(minute=startTimeMinute) - timestamp = int(time.mktime(now.timetuple())) - return timestamp - - -def _create_recurrence_mask(recurrence, base): - """ - >>> bin(_create_recurrence_mask(60, 60)) - '0b1' - >>> bin(_create_recurrence_mask(30, 60)) - '0b1000000000000000000000000000001' - >>> bin(_create_recurrence_mask(2, 60)) - '0b10101010101010101010101010101010101010101010101010101010101' - >>> bin(_create_recurrence_mask(1, 60)) - '0b111111111111111111111111111111111111111111111111111111111111' - """ - mask = 0 - for i in xrange(base / recurrence): - mask |= 1 << (recurrence * i) - return mask - - -def _unpack_minutes(recurrence): - """ - >>> _unpack_minutes(0) - (0, 0, 0) - >>> _unpack_minutes(1) - (0, 0, 1) - >>> _unpack_minutes(59) - (0, 0, 59) - >>> _unpack_minutes(60) - (0, 1, 0) - >>> _unpack_minutes(129) - (0, 2, 9) - >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2) - (5, 3, 2) - >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2) - (5, 3, 2) - """ - minutesInAnHour = 60 - minutesInDay = 24 * minutesInAnHour - minutesInAWeek = minutesInDay * 7 - - days = recurrence / minutesInDay - daysOfWeek = days % 7 - recurrence -= days * minutesInDay - hours = recurrence / minutesInAnHour - recurrence -= hours * minutesInAnHour - mins = recurrence % minutesInAnHour - recurrence -= mins - assert recurrence == 0, "Recurrence %d" % recurrence - return daysOfWeek, hours, mins - - -class _FremantleAlarmHandler(object): - - _INVALID_COOKIE = -1 - _REPEAT_FOREVER = -1 - _TITLE = "Dialcentral Notifications" - _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py")) - - def __init__(self): - self._recurrence = 5 - - self._alarmCookie = self._INVALID_COOKIE - self._launcher = self._LAUNCHER - - def load_settings(self, config, sectionName): - try: - self._recurrence = config.getint(sectionName, "recurrence") - self._alarmCookie = config.getint(sectionName, "alarmCookie") - launcher = config.get(sectionName, "notifier") - if launcher: - self._launcher = launcher - except ConfigParser.NoOptionError: - pass - except ConfigParser.NoSectionError: - pass - - def save_settings(self, config, sectionName): - try: - config.set(sectionName, "recurrence", str(self._recurrence)) - config.set(sectionName, "alarmCookie", str(self._alarmCookie)) - launcher = self._launcher if self._launcher != self._LAUNCHER else "" - config.set(sectionName, "notifier", launcher) - except ConfigParser.NoOptionError: - pass - except ConfigParser.NoSectionError: - pass - - def apply_settings(self, enabled, recurrence): - if recurrence != self._recurrence or enabled != self.isEnabled: - if self.isEnabled: - self._clear_alarm() - if enabled: - self._set_alarm(recurrence) - self._recurrence = int(recurrence) - - @property - def recurrence(self): - return self._recurrence - - @property - def isEnabled(self): - return self._alarmCookie != self._INVALID_COOKIE - - def _set_alarm(self, recurrenceMins): - assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins - alarmTime = _get_start_time(recurrenceMins) - - event = alarm.Event() - event.appid = self._TITLE - event.alarm_time = alarmTime - event.recurrences_left = self._REPEAT_FOREVER - - action = event.add_actions(1)[0] - action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED - action.command = self._launcher - - recurrence = event.add_recurrences(1)[0] - recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60) - recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE - recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE - recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE - recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE - recurrence.special |= alarm.RECUR_SPECIAL_NONE - - assert event.is_sane() - self._alarmCookie = alarm.add_event(event) - - def _clear_alarm(self): - if self._alarmCookie == self._INVALID_COOKIE: - return - alarm.delete_event(self._alarmCookie) - self._alarmCookie = self._INVALID_COOKIE - - -class _DiabloAlarmHandler(object): - - _INVALID_COOKIE = -1 - _TITLE = "Dialcentral Notifications" - _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py")) - _REPEAT_FOREVER = -1 - - def __init__(self): - self._recurrence = 5 - - bus = dbus.SystemBus() - self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd"); - self._alarmCookie = self._INVALID_COOKIE - self._launcher = self._LAUNCHER - - def load_settings(self, config, sectionName): - try: - self._recurrence = config.getint(sectionName, "recurrence") - self._alarmCookie = config.getint(sectionName, "alarmCookie") - launcher = config.get(sectionName, "notifier") - if launcher: - self._launcher = launcher - except ConfigParser.NoOptionError: - pass - except ConfigParser.NoSectionError: - pass - - def save_settings(self, config, sectionName): - config.set(sectionName, "recurrence", str(self._recurrence)) - config.set(sectionName, "alarmCookie", str(self._alarmCookie)) - launcher = self._launcher if self._launcher != self._LAUNCHER else "" - config.set(sectionName, "notifier", launcher) - - def apply_settings(self, enabled, recurrence): - if recurrence != self._recurrence or enabled != self.isEnabled: - if self.isEnabled: - self._clear_alarm() - if enabled: - self._set_alarm(recurrence) - self._recurrence = int(recurrence) - - @property - def recurrence(self): - return self._recurrence - - @property - def isEnabled(self): - return self._alarmCookie != self._INVALID_COOKIE - - def _set_alarm(self, recurrence): - assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence - alarmTime = _get_start_time(recurrence) - - #Setup the alarm arguments so that they can be passed to the D-Bus add_event method - _DEFAULT_FLAGS = ( - alarmd.ALARM_EVENT_NO_DIALOG | - alarmd.ALARM_EVENT_NO_SNOOZE | - alarmd.ALARM_EVENT_CONNECTED - ) - action = [] - action.extend(['flags', _DEFAULT_FLAGS]) - action.extend(['title', self._TITLE]) - action.extend(['path', self._launcher]) - action.extend([ - 'arguments', - dbus.Array( - [alarmTime, int(27)], - signature=dbus.Signature('v') - ) - ]) #int(27) used in place of alarm_index - - event = [] - event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)]) - event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus - event.append(dbus.UInt32(len(action) / 2)) - event.extend(action) - event.extend(['time', dbus.Int64(alarmTime)]) - event.extend(['recurr_interval', dbus.UInt32(recurrence)]) - event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)]) - - self._alarmCookie = self._alarmdDBus.add_event(*event); - - def _clear_alarm(self): - if self._alarmCookie == self._INVALID_COOKIE: - return - deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie)) - self._alarmCookie = self._INVALID_COOKIE - assert deleteResult != -1, "Deleting of alarm event failed" - - -class _ApplicationAlarmHandler(object): - - _REPEAT_FOREVER = -1 - _MIN_TO_MS_FACTORY = 1000 * 60 - - def __init__(self): - self._timer = QtCore.QTimer() - self._timer.setSingleShot(False) - self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY) - - def load_settings(self, config, sectionName): - try: - self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY) - except ConfigParser.NoOptionError: - pass - except ConfigParser.NoSectionError: - pass - self._timer.start() - - def save_settings(self, config, sectionName): - config.set(sectionName, "recurrence", str(self.recurrence)) - - def apply_settings(self, enabled, recurrence): - self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY) - if enabled: - self._timer.start() - else: - self._timer.stop() - - @property - def notifySignal(self): - return self._timer.timeout - - @property - def recurrence(self): - return int(self._timer.interval() / self._MIN_TO_MS_FACTORY) - - @property - def isEnabled(self): - return self._timer.isActive() - - -class _NoneAlarmHandler(object): - - def __init__(self): - self._enabled = False - self._recurrence = 5 - - def load_settings(self, config, sectionName): - try: - self._recurrence = config.getint(sectionName, "recurrence") - self._enabled = True - except ConfigParser.NoOptionError: - pass - except ConfigParser.NoSectionError: - pass - - def save_settings(self, config, sectionName): - config.set(sectionName, "recurrence", str(self.recurrence)) - - def apply_settings(self, enabled, recurrence): - self._enabled = enabled - - @property - def recurrence(self): - return self._recurrence - - @property - def isEnabled(self): - return self._enabled - - -_BACKGROUND_ALARM_FACTORY = { - _FREMANTLE_ALARM: _FremantleAlarmHandler, - _DIABLO_ALARM: _DiabloAlarmHandler, - _NO_ALARM: None, -}[ALARM_TYPE] - - -class AlarmHandler(object): - - ALARM_NONE = "No Alert" - ALARM_BACKGROUND = "Background Alert" - ALARM_APPLICATION = "Application Alert" - ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION] - - ALARM_FACTORY = { - ALARM_NONE: _NoneAlarmHandler, - ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY, - ALARM_APPLICATION: _ApplicationAlarmHandler, - } - - def __init__(self): - self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()} - self._currentAlarmType = self.ALARM_NONE - - def load_settings(self, config, sectionName): - try: - self._currentAlarmType = config.get(sectionName, "alarm") - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - _moduleLogger.exception("Falling back to old style") - self._currentAlarmType = self.ALARM_BACKGROUND - if self._currentAlarmType not in self.ALARM_TYPES: - self._currentAlarmType = self.ALARM_NONE - - self._init_alarm(self._currentAlarmType) - if self._currentAlarmType in self._alarms: - self._alarms[self._currentAlarmType].load_settings(config, sectionName) - if not self._alarms[self._currentAlarmType].isEnabled: - _moduleLogger.info("Config file lied, not actually enabled") - self._currentAlarmType = self.ALARM_NONE - else: - _moduleLogger.info("Background alerts not supported") - self._currentAlarmType = self.ALARM_NONE - - def save_settings(self, config, sectionName): - config.set(sectionName, "alarm", self._currentAlarmType) - self._alarms[self._currentAlarmType].save_settings(config, sectionName) - - def apply_settings(self, t, recurrence): - self._init_alarm(t) - newHandler = self._alarms[t] - oldHandler = self._alarms[self._currentAlarmType] - if newHandler != oldHandler: - oldHandler.apply_settings(False, 0) - newHandler.apply_settings(True, recurrence) - self._currentAlarmType = t - - @property - def alarmType(self): - return self._currentAlarmType - - @property - def backgroundNotificationsSupported(self): - return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None - - @property - def applicationNotifySignal(self): - self._init_alarm(self.ALARM_APPLICATION) - return self._alarms[self.ALARM_APPLICATION].notifySignal - - @property - def recurrence(self): - return self._alarms[self._currentAlarmType].recurrence - - @property - def isEnabled(self): - return self._currentAlarmType != self.ALARM_NONE - - def _init_alarm(self, t): - if t not in self._alarms and self.ALARM_FACTORY[t] is not None: - self._alarms[t] = self.ALARM_FACTORY[t]() - - -def main(): - logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' - logging.basicConfig(level=logging.DEBUG, format=logFormat) - import constants - try: - import optparse - except ImportError: - return - - parser = optparse.OptionParser() - parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data") - parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False) - parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False) - parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5) - (commandOptions, commandArgs) = parser.parse_args() - - alarmHandler = AlarmHandler() - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - alarmHandler.load_settings(config, "alarm") - - if commandOptions.display: - print "Alarm (%s) is %s for every %d minutes" % ( - alarmHandler._alarmCookie, - "enabled" if alarmHandler.isEnabled else "disabled", - alarmHandler.recurrence, - ) - else: - isEnabled = commandOptions.enabled - recurrence = commandOptions.recurrence - alarmHandler.apply_settings(isEnabled, recurrence) - - alarmHandler.save_settings(config, "alarm") - configFile = open(constants._user_settings_, "wb") - try: - config.write(configFile) - finally: - configFile.close() - - -if __name__ == "__main__": - main() diff --git a/src/alarm_notify.py b/src/alarm_notify.py deleted file mode 100755 index bc6240e..0000000 --- a/src/alarm_notify.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python - -import os -import filecmp -import ConfigParser -import pprint -import logging -import logging.handlers - -import constants -from backends.gvoice import gvoice - - -def get_missed(backend): - missedPage = backend._browser.download(backend._XML_MISSED_URL) - missedJson = backend._grab_json(missedPage) - return missedJson - - -def get_voicemail(backend): - voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL) - voicemailJson = backend._grab_json(voicemailPage) - return voicemailJson - - -def get_sms(backend): - smsPage = backend._browser.download(backend._XML_SMS_URL) - smsJson = backend._grab_json(smsPage) - return smsJson - - -def remove_reltime(data): - for messageData in data["messages"].itervalues(): - for badPart in [ - "relTime", - "relativeStartTime", - "time", - "star", - "isArchived", - "isRead", - "isSpam", - "isTrash", - "labels", - ]: - if badPart in messageData: - del messageData[badPart] - for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]: - if globalBad in data: - del data[globalBad] - - -def is_type_changed(backend, type, get_material): - jsonMaterial = get_material(backend) - unreadCount = jsonMaterial["unreadCounts"][type] - - previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type) - currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type) - - try: - os.remove(previousSnapshotPath) - except OSError, e: - # check if failed purely because the old file didn't exist, which is fine - if e.errno != 2: - raise - try: - os.rename(currentSnapshotPath, previousSnapshotPath) - previousExists = True - except OSError, e: - # check if failed purely because the new old file didn't exist, which is fine - if e.errno != 2: - raise - previousExists = False - - remove_reltime(jsonMaterial) - textMaterial = pprint.pformat(jsonMaterial) - currentSnapshot = file(currentSnapshotPath, "w") - try: - currentSnapshot.write(textMaterial) - finally: - currentSnapshot.close() - - if unreadCount == 0 or not previousExists: - return False - - seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath) - return not seemEqual - - -def create_backend(config): - gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt") - backend = gvoice.GVoiceBackend(gvCookiePath) - - loggedIn = False - - if not loggedIn: - loggedIn = backend.refresh_account_info() is not None - - if not loggedIn: - import base64 - try: - blobs = ( - config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) - for i in xrange(2) - ) - creds = ( - base64.b64decode(blob) - for blob in blobs - ) - username, password = tuple(creds) - loggedIn = backend.login(username, password) is not None - except ConfigParser.NoOptionError, e: - pass - except ConfigParser.NoSectionError, e: - pass - - assert loggedIn - return backend - - -def is_changed(config, backend): - try: - notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed") - notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail") - notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms") - except ConfigParser.NoOptionError, e: - notifyOnMissed = False - notifyOnVoicemail = False - notifyOnSms = False - except ConfigParser.NoSectionError, e: - notifyOnMissed = False - notifyOnVoicemail = False - notifyOnSms = False - logging.debug( - "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms) - ) - - notifySources = [] - if notifyOnMissed: - notifySources.append(("missed", get_missed)) - if notifyOnVoicemail: - notifySources.append(("voicemail", get_voicemail)) - if notifyOnSms: - notifySources.append(("sms", get_sms)) - - notifyUser = False - for type, get_material in notifySources: - if is_type_changed(backend, type, get_material): - notifyUser = True - return notifyUser - - -def notify_on_change(): - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - backend = create_backend(config) - notifyUser = is_changed(config, backend) - - if notifyUser: - logging.info("Changed") - import led_handler - led = led_handler.LedHandler() - led.on() - else: - logging.info("No Change") - - -if __name__ == "__main__": - logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' - logging.basicConfig(level=logging.DEBUG, format=logFormat) - rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1) - rotating.setFormatter(logging.Formatter(logFormat)) - root = logging.getLogger() - root.addHandler(rotating) - logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__)) - logging.info("OS: %s" % (os.uname()[0], )) - logging.info("Kernel: %s (%s) for %s" % os.uname()[2:]) - logging.info("Hostname: %s" % os.uname()[1]) - try: - notify_on_change() - except: - logging.exception("Error") - raise diff --git a/src/backends/__init__.py b/src/backends/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backends/file_backend.py b/src/backends/file_backend.py deleted file mode 100644 index 9f8927a..0000000 --- a/src/backends/file_backend.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's Grand Central service. -Copyright (C) 2008 Eric Warnke ericew AT gmail 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 - -Filesystem backend for contact support -""" - -from __future__ import with_statement - -import os -import csv - - -def try_unicode(s): - try: - return s.decode("UTF-8") - except UnicodeDecodeError: - return s - - -class CsvAddressBook(object): - """ - Currently supported file format - @li Has the first line as a header - @li Escapes with quotes - @li Comma as delimiter - @li Column 0 is name, column 1 is number - """ - - def __init__(self, name, csvPath): - self._name = name - self._csvPath = csvPath - self._contacts = {} - - @property - def name(self): - return self._name - - def update_account(self, force = True): - if not force or not self._contacts: - return - self._contacts = dict( - self._read_csv(self._csvPath) - ) - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - if not self._contacts: - self._contacts = dict( - self._read_csv(self._csvPath) - ) - return self._contacts - - def _read_csv(self, csvPath): - try: - f = open(csvPath, "rU") - csvReader = iter(csv.reader(f)) - except IOError, e: - if e.errno == 2: - return - raise - - header = csvReader.next() - nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header) - - yieldCount = 0 - for row in csvReader: - contactDetails = [] - for (phoneType, phoneColumn) in phoneColumns: - try: - if len(row[phoneColumn]) == 0: - continue - contactDetails.append({ - "phoneType": try_unicode(phoneType), - "phoneNumber": row[phoneColumn], - }) - except IndexError: - pass - if 0 < len(contactDetails): - nameParts = (row[i].strip() for i in nameColumns) - nameParts = (part for part in nameParts if part) - fullName = " ".join(nameParts).strip() - if not fullName: - for fallbackColumn in nameFallbacks: - if row[fallbackColumn].strip(): - fullName = row[fallbackColumn].strip() - break - else: - fullName = "Unknown" - fullName = try_unicode(fullName) - yield str(yieldCount), { - "contactId": "%s-%d" % (self._name, yieldCount), - "name": fullName, - "numbers": contactDetails, - } - yieldCount += 1 - - @classmethod - def _guess_columns(cls, row): - firstMiddleLast = [-1, -1, -1] - names = [] - nameFallbacks = [] - phones = [] - for i, item in enumerate(row): - lowerItem = item.lower() - if 0 <= lowerItem.find("name"): - names.append((item, i)) - - if 0 <= lowerItem.find("couple"): - names.insert(0, (item, i)) - - if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"): - firstMiddleLast[0] = i - elif 0 <= lowerItem.find("middle"): - firstMiddleLast[1] = i - elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"): - firstMiddleLast[2] = i - elif 0 <= lowerItem.find("phone"): - phones.append((item, i)) - elif 0 <= lowerItem.find("mobile"): - phones.append((item, i)) - elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"): - nameFallbacks.append(i) - if len(names) == 0: - names.append(("Name", 0)) - if len(phones) == 0: - phones.append(("Phone", 1)) - - nameColumns = [i for i in firstMiddleLast if 0 <= i] - if len(nameColumns) < 2: - del nameColumns[:] - nameColumns.append(names[0][1]) - - return nameColumns, nameFallbacks, phones - - -class FilesystemAddressBookFactory(object): - - FILETYPE_SUPPORT = { - "csv": CsvAddressBook, - } - - def __init__(self, path): - self._path = path - - def get_addressbooks(self): - for root, dirs, filenames in os.walk(self._path): - for filename in filenames: - try: - name, ext = filename.rsplit(".", 1) - except ValueError: - continue - - try: - cls = self.FILETYPE_SUPPORT[ext] - except KeyError: - continue - yield cls(name, os.path.join(root, filename)) diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py deleted file mode 100644 index 17bbc90..0000000 --- a/src/backends/gv_backend.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's GoogleVoice service. -Copyright (C) 2008 Eric Warnke ericew AT gmail 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 - -Google Voice backend code - -Resources - http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ - http://posttopic.com/topic/google-voice-add-on-development -""" - -from __future__ import with_statement - -import itertools -import logging - -from gvoice import gvoice - -from util import io as io_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class GVDialer(object): - - MESSAGE_TEXTS = "Text" - MESSAGE_VOICEMAILS = "Voicemail" - MESSAGE_ALL = "All" - - HISTORY_RECEIVED = "Received" - HISTORY_MISSED = "Missed" - HISTORY_PLACED = "Placed" - HISTORY_ALL = "All" - - def __init__(self, cookieFile = None): - self._gvoice = gvoice.GVoiceBackend(cookieFile) - self._texts = [] - self._voicemails = [] - self._received = [] - self._missed = [] - self._placed = [] - - def is_quick_login_possible(self): - """ - @returns True then refresh_account_info might be enough to login, else full login is required - """ - return self._gvoice.is_quick_login_possible() - - def refresh_account_info(self): - return self._gvoice.refresh_account_info() - - def login(self, username, password): - """ - Attempt to login to GoogleVoice - @returns Whether login was successful or not - """ - return self._gvoice.login(username, password) - - def logout(self): - self._texts = [] - self._voicemails = [] - self._received = [] - self._missed = [] - self._placed = [] - return self._gvoice.logout() - - def persist(self): - return self._gvoice.persist() - - def is_dnd(self): - return self._gvoice.is_dnd() - - def set_dnd(self, doNotDisturb): - return self._gvoice.set_dnd(doNotDisturb) - - def call(self, outgoingNumber): - """ - This is the main function responsible for initating the callback - """ - return self._gvoice.call(outgoingNumber) - - def cancel(self, outgoingNumber=None): - """ - Cancels a call matching outgoing and forwarding numbers (if given). - Will raise an error if no matching call is being placed - """ - return self._gvoice.cancel(outgoingNumber) - - def send_sms(self, phoneNumbers, message): - self._gvoice.send_sms(phoneNumbers, message) - - def search(self, query): - """ - Search your Google Voice Account history for calls, voicemails, and sms - Returns ``Folder`` instance containting matching messages - """ - return self._gvoice.search(query) - - def get_feed(self, feed): - return self._gvoice.get_feed(feed) - - def download(self, messageId, targetPath): - """ - Download a voicemail or recorded call MP3 matching the given ``msg`` - which can either be a ``Message`` instance, or a SHA1 identifier. - Message hashes can be found in ``self.voicemail().messages`` for example. - Returns location of saved file. - """ - self._gvoice.download(messageId, targetPath) - - def is_valid_syntax(self, number): - """ - @returns If This number be called ( syntax validation only ) - """ - return self._gvoice.is_valid_syntax(number) - - def get_account_number(self): - """ - @returns The GoogleVoice phone number - """ - return self._gvoice.get_account_number() - - def get_callback_numbers(self): - """ - @returns a dictionary mapping call back numbers to descriptions - @note These results are cached for 30 minutes. - """ - return self._gvoice.get_callback_numbers() - - def set_callback_number(self, callbacknumber): - """ - Set the number that GoogleVoice calls - @param callbacknumber should be a proper 10 digit number - """ - return self._gvoice.set_callback_number(callbacknumber) - - def get_callback_number(self): - """ - @returns Current callback number or None - """ - return self._gvoice.get_callback_number() - - def get_call_history(self, historyType): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - """ - history = list(self._get_call_history(historyType)) - history.sort(key=lambda item: item["time"]) - return history - - def _get_call_history(self, historyType): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - """ - if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received: - self._received = list(self._gvoice.get_received_calls()) - for item in self._received: - item["action"] = self.HISTORY_RECEIVED - if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed: - self._missed = list(self._gvoice.get_missed_calls()) - for item in self._missed: - item["action"] = self.HISTORY_MISSED - if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed: - self._placed = list(self._gvoice.get_placed_calls()) - for item in self._placed: - item["action"] = self.HISTORY_PLACED - received = self._received - missed = self._missed - placed = self._placed - for item in received: - yield item - for item in missed: - yield item - for item in placed: - yield item - - def get_messages(self, messageType): - messages = list(self._get_messages(messageType)) - messages.sort(key=lambda message: message["time"]) - return messages - - def _get_messages(self, messageType): - if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails: - self._voicemails = list(self._gvoice.get_voicemails()) - if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts: - self._texts = list(self._gvoice.get_texts()) - voicemails = self._voicemails - smss = self._texts - - conversations = itertools.chain(voicemails, smss) - for conversation in conversations: - messages = conversation.messages - messageParts = [ - (message.whoFrom, self._format_message(message), message.when) - for message in messages - ] - - messageDetails = { - "id": conversation.id, - "contactId": conversation.contactId, - "name": conversation.name, - "time": conversation.time, - "relTime": conversation.relTime, - "prettyNumber": conversation.prettyNumber, - "number": conversation.number, - "location": conversation.location, - "messageParts": messageParts, - "type": conversation.type, - "isRead": conversation.isRead, - "isTrash": conversation.isTrash, - "isSpam": conversation.isSpam, - "isArchived": conversation.isArchived, - } - yield messageDetails - - def clear_caches(self): - pass - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "" - - def open_addressbook(self, bookId): - return self - - @staticmethod - def contact_source_short_name(contactId): - return "GV" - - @staticmethod - def factory_name(): - return "Google Voice" - - def _format_message(self, message): - messagePartFormat = { - "med1": "%s", - "med2": "%s", - "high": "%s", - } - return " ".join( - messagePartFormat[text.accuracy] % io_utils.escape(text.text) - for text in message.body - ) - - -def sort_messages(allMessages): - sortableAllMessages = [ - (message["time"], message) - for message in allMessages - ] - sortableAllMessages.sort(reverse=True) - return ( - message - for (exactTime, message) in sortableAllMessages - ) - - -def decorate_recent(recentCallData): - """ - @returns (personsName, phoneNumber, date, action) - """ - contactId = recentCallData["contactId"] - if recentCallData["name"]: - header = recentCallData["name"] - elif recentCallData["prettyNumber"]: - header = recentCallData["prettyNumber"] - elif recentCallData["location"]: - header = recentCallData["location"] - else: - header = "Unknown" - - number = recentCallData["number"] - relTime = recentCallData["relTime"] - action = recentCallData["action"] - return contactId, header, number, relTime, action - - -def decorate_message(messageData): - contactId = messageData["contactId"] - exactTime = messageData["time"] - if messageData["name"]: - header = messageData["name"] - elif messageData["prettyNumber"]: - header = messageData["prettyNumber"] - else: - header = "Unknown" - number = messageData["number"] - relativeTime = messageData["relTime"] - - messageParts = list(messageData["messageParts"]) - if len(messageParts) == 0: - messages = ("No Transcription", ) - elif len(messageParts) == 1: - messages = (messageParts[0][1], ) - else: - messages = [ - "%s: %s" % (messagePart[0], messagePart[1]) - for messagePart in messageParts - ] - - decoratedResults = contactId, header, number, relativeTime, messages - return decoratedResults diff --git a/src/backends/gvoice/__init__.py b/src/backends/gvoice/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backends/gvoice/browser_emu.py b/src/backends/gvoice/browser_emu.py deleted file mode 100644 index 4fef6e8..0000000 --- a/src/backends/gvoice/browser_emu.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -@author: Laszlo Nagy -@copyright: (c) 2005 by Szoftver Messias Bt. -@licence: BSD style - -Objects of the MozillaEmulator class can emulate a browser that is capable of: - - - cookie management - - configurable user agent string - - GET and POST - - multipart POST (send files) - - receive content into file - -I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: - - 1. Use firefox - 2. Install and open the livehttpheaders plugin - 3. Use the website manually with firefox - 4. Check the GET and POST requests in the livehttpheaders capture window - 5. Create an instance of the above class and send the same GET and POST requests to the server. - -Optional steps: - - - You can change user agent string in the build_opened method - - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files -""" - -import urllib2 -import cookielib -import logging - -import socket - - -_moduleLogger = logging.getLogger(__name__) -socket.setdefaulttimeout(25) - - -def add_proxy(protocol, url, port): - proxyInfo = "%s:%s" % (url, port) - proxy = urllib2.ProxyHandler( - {protocol: proxyInfo} - ) - opener = urllib2.build_opener(proxy) - urllib2.install_opener(opener) - - -class MozillaEmulator(object): - - USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)' - #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16" - - def __init__(self, trycount = 1): - """Create a new MozillaEmulator object. - - @param trycount: The download() method will retry the operation if it - fails. You can specify -1 for infinite retrying. A value of 0 means no - retrying. A value of 1 means one retry. etc.""" - self.debug = False - self.trycount = trycount - self._cookies = cookielib.LWPCookieJar() - self._loadedFromCookies = False - self._storeCookies = False - - def load_cookies(self, path): - assert not self._loadedFromCookies, "Load cookies only once" - if path is None: - return - - self._cookies.filename = path - try: - self._cookies.load() - except cookielib.LoadError: - _moduleLogger.exception("Bad cookie file") - except IOError: - _moduleLogger.exception("No cookie file") - except Exception, e: - _moduleLogger.exception("Unknown error with cookies") - else: - self._loadedFromCookies = True - self._storeCookies = True - - return self._loadedFromCookies - - def save_cookies(self): - if self._storeCookies: - self._cookies.save() - - def clear_cookies(self): - if self._storeCookies: - self._cookies.clear() - - def download(self, url, - postdata = None, extraheaders = None, forbidRedirect = False, - trycount = None, only_head = False, - ): - """Download an URL with GET or POST methods. - - @param postdata: It can be a string that will be POST-ed to the URL. - When None is given, the method will be GET instead. - @param extraheaders: You can add/modify HTTP headers with a dict here. - @param forbidRedirect: Set this flag if you do not want to handle - HTTP 301 and 302 redirects. - @param trycount: Specify the maximum number of retries here. - 0 means no retry on error. Using -1 means infinite retring. - None means the default value (that is self.trycount). - @param only_head: Create the openerdirector and return it. In other - words, this will not retrieve any content except HTTP headers. - - @return: The raw HTML page data - """ - _moduleLogger.debug("Performing download of %s" % url) - - if extraheaders is None: - extraheaders = {} - if trycount is None: - trycount = self.trycount - cnt = 0 - - while True: - try: - req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) - openerdirector = u.open(req) - if self.debug: - _moduleLogger.info("%r - %r" % (req.get_method(), url)) - _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) - _moduleLogger.info("%r" % (openerdirector.headers)) - self._cookies.extract_cookies(openerdirector, req) - if only_head: - return openerdirector - - return self._read(openerdirector, trycount) - except urllib2.URLError, e: - _moduleLogger.debug("%s: %s" % (e, url)) - cnt += 1 - if (-1 < trycount) and (trycount < cnt): - raise - - # Retry :-) - _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt) - - def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): - if extraheaders is None: - extraheaders = {} - - txheaders = { - 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', - 'Accept-Language': 'en,en-us;q=0.5', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', - 'User-Agent': self.USER_AGENT, - } - for key, value in extraheaders.iteritems(): - txheaders[key] = value - req = urllib2.Request(url, postdata, txheaders) - self._cookies.add_cookie_header(req) - if forbidRedirect: - redirector = HTTPNoRedirector() - #_moduleLogger.info("Redirection disabled") - else: - redirector = urllib2.HTTPRedirectHandler() - #_moduleLogger.info("Redirection enabled") - - http_handler = urllib2.HTTPHandler(debuglevel=self.debug) - https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) - - u = urllib2.build_opener( - http_handler, - https_handler, - urllib2.HTTPCookieProcessor(self._cookies), - redirector - ) - if not postdata is None: - req.add_data(postdata) - return (req, u) - - def _read(self, openerdirector, trycount): - chunks = [] - - chunk = openerdirector.read() - chunks.append(chunk) - #while chunk and cnt < trycount: - # time.sleep(1) - # cnt += 1 - # chunk = openerdirector.read() - # chunks.append(chunk) - - data = "".join(chunks) - - if "Content-Length" in openerdirector.info(): - assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( - openerdirector.info()["Content-Length"], - len(data), - ) - - return data - - -class HTTPNoRedirector(urllib2.HTTPRedirectHandler): - """This is a custom http redirect handler that FORBIDS redirection.""" - - def http_error_302(self, req, fp, code, msg, headers): - e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) - if e.code in (301, 302): - if 'location' in headers: - newurl = headers.getheaders('location')[0] - elif 'uri' in headers: - newurl = headers.getheaders('uri')[0] - e.newurl = newurl - _moduleLogger.info("New url: %s" % e.newurl) - raise e diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py deleted file mode 100755 index b0825ef..0000000 --- a/src/backends/gvoice/gvoice.py +++ /dev/null @@ -1,1050 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's GoogleVoice service. -Copyright (C) 2008 Eric Warnke ericew AT gmail 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 - -Google Voice backend code - -Resources - http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ - http://posttopic.com/topic/google-voice-add-on-development -""" - -from __future__ import with_statement - -import os -import re -import urllib -import urllib2 -import time -import datetime -import itertools -import logging -import inspect - -from xml.sax import saxutils -from xml.etree import ElementTree - -try: - import simplejson as _simplejson - simplejson = _simplejson -except ImportError: - simplejson = None - -import browser_emu - - -_moduleLogger = logging.getLogger(__name__) - - -class NetworkError(RuntimeError): - pass - - -class MessageText(object): - - ACCURACY_LOW = "med1" - ACCURACY_MEDIUM = "med2" - ACCURACY_HIGH = "high" - - def __init__(self): - self.accuracy = None - self.text = None - - def __str__(self): - return self.text - - def to_dict(self): - return to_dict(self) - - def __eq__(self, other): - return self.accuracy == other.accuracy and self.text == other.text - - -class Message(object): - - def __init__(self): - self.whoFrom = None - self.body = None - self.when = None - - def __str__(self): - return "%s (%s): %s" % ( - self.whoFrom, - self.when, - "".join(unicode(part) for part in self.body) - ) - - def to_dict(self): - selfDict = to_dict(self) - selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None - return selfDict - - def __eq__(self, other): - return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body - - -class Conversation(object): - - TYPE_VOICEMAIL = "Voicemail" - TYPE_SMS = "SMS" - - def __init__(self): - self.type = None - self.id = None - self.contactId = None - self.name = None - self.location = None - self.prettyNumber = None - self.number = None - - self.time = None - self.relTime = None - self.messages = None - self.isRead = None - self.isSpam = None - self.isTrash = None - self.isArchived = None - - def __cmp__(self, other): - cmpValue = cmp(self.contactId, other.contactId) - if cmpValue != 0: - return cmpValue - - cmpValue = cmp(self.time, other.time) - if cmpValue != 0: - return cmpValue - - cmpValue = cmp(self.id, other.id) - if cmpValue != 0: - return cmpValue - - def to_dict(self): - selfDict = to_dict(self) - selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None - return selfDict - - -class GVoiceBackend(object): - """ - This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers - the functions include login, setting up a callback number, and initalting a callback - """ - - PHONE_TYPE_HOME = 1 - PHONE_TYPE_MOBILE = 2 - PHONE_TYPE_WORK = 3 - PHONE_TYPE_GIZMO = 7 - - def __init__(self, cookieFile = None): - # Important items in this function are the setup of the browser emulation and cookie file - self._browser = browser_emu.MozillaEmulator(1) - self._loadedFromCookies = self._browser.load_cookies(cookieFile) - - self._token = "" - self._accountNum = "" - self._lastAuthed = 0.0 - self._callbackNumber = "" - self._callbackNumbers = {} - - # Suprisingly, moving all of these from class to self sped up startup time - - self._validateRe = re.compile("^\+?[0-9]{10,}$") - - self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" - - SECURE_URL_BASE = "https://www.google.com/voice/" - SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/" - self._tokenURL = SECURE_URL_BASE + "m" - self._callUrl = SECURE_URL_BASE + "call/connect" - self._callCancelURL = SECURE_URL_BASE + "call/cancel" - self._sendSmsURL = SECURE_URL_BASE + "sms/send" - - self._isDndURL = "https://www.google.com/voice/m/donotdisturb" - self._isDndRe = re.compile(r"""""") - self._setDndURL = "https://www.google.com/voice/m/savednd" - - self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/" - self._markAsReadURL = SECURE_URL_BASE + "m/mark" - self._archiveMessageURL = SECURE_URL_BASE + "m/archive" - - self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/" - self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/" - # HACK really this redirects to the main pge and we are grabbing some javascript - self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact" - self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export" - self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user" - self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/" - - self.XML_FEEDS = ( - 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms', - 'recorded', 'placed', 'received', 'missed' - ) - self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox" - self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred" - self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all" - self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam" - self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash" - self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/" - self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/" - self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/" - self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread" - self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/" - self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/" - self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/" - self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/" - - self._galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) - - self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) - self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) - self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) - self._messagesContactIDRegex = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) - self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) - self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - - def is_quick_login_possible(self): - """ - @returns True then refresh_account_info might be enough to login, else full login is required - """ - return self._loadedFromCookies or 0.0 < self._lastAuthed - - def refresh_account_info(self): - try: - page = self._get_page(self._JSON_CONTACTS_URL) - accountData = self._grab_account_info(page) - except Exception, e: - _moduleLogger.exception(str(e)) - return None - - self._browser.save_cookies() - self._lastAuthed = time.time() - return accountData - - def _get_token(self): - tokenPage = self._get_page(self._tokenURL) - - galxTokens = self._galxRe.search(tokenPage) - if galxTokens is not None: - galxToken = galxTokens.group(1) - else: - galxToken = "" - _moduleLogger.debug("Could not grab GALX token") - return galxToken - - def _login(self, username, password, token): - loginData = { - 'Email' : username, - 'Passwd' : password, - 'service': "grandcentral", - "ltmpl": "mobile", - "btmpl": "mobile", - "PersistentCookie": "yes", - "GALX": token, - "continue": self._JSON_CONTACTS_URL, - } - - loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData) - return loginSuccessOrFailurePage - - def login(self, username, password): - """ - Attempt to login to GoogleVoice - @returns Whether login was successful or not - @blocks - """ - self.logout() - galxToken = self._get_token() - loginSuccessOrFailurePage = self._login(username, password, galxToken) - - try: - accountData = self._grab_account_info(loginSuccessOrFailurePage) - except Exception, e: - # Retry in case the redirect failed - # luckily refresh_account_info does everything we need for a retry - accountData = self.refresh_account_info() - if accountData is None: - _moduleLogger.exception(str(e)) - return None - _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this") - - self._browser.save_cookies() - self._lastAuthed = time.time() - return accountData - - def persist(self): - self._browser.save_cookies() - - def shutdown(self): - self._browser.save_cookies() - self._token = None - self._lastAuthed = 0.0 - - def logout(self): - self._browser.clear_cookies() - self._browser.save_cookies() - self._token = None - self._lastAuthed = 0.0 - self._callbackNumbers = {} - - def is_dnd(self): - """ - @blocks - """ - isDndPage = self._get_page(self._isDndURL) - - dndGroup = self._isDndRe.search(isDndPage) - if dndGroup is None: - return False - dndStatus = dndGroup.group(1) - isDnd = True if dndStatus.strip().lower() == "true" else False - return isDnd - - def set_dnd(self, doNotDisturb): - """ - @blocks - """ - dndPostData = { - "doNotDisturb": 1 if doNotDisturb else 0, - } - - dndPage = self._get_page_with_token(self._setDndURL, dndPostData) - - def call(self, outgoingNumber): - """ - This is the main function responsible for initating the callback - @blocks - """ - outgoingNumber = self._send_validation(outgoingNumber) - subscriberNumber = None - phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack - - callData = { - 'outgoingNumber': outgoingNumber, - 'forwardingNumber': self._callbackNumber, - 'subscriberNumber': subscriberNumber or 'undefined', - 'phoneType': str(phoneType), - 'remember': '1', - } - _moduleLogger.info("%r" % callData) - - page = self._get_page_with_token( - self._callUrl, - callData, - ) - self._parse_with_validation(page) - return True - - def cancel(self, outgoingNumber=None): - """ - Cancels a call matching outgoing and forwarding numbers (if given). - Will raise an error if no matching call is being placed - @blocks - """ - page = self._get_page_with_token( - self._callCancelURL, - { - 'outgoingNumber': outgoingNumber or 'undefined', - 'forwardingNumber': self._callbackNumber or 'undefined', - 'cancelType': 'C2C', - }, - ) - self._parse_with_validation(page) - - def send_sms(self, phoneNumbers, message): - """ - @blocks - """ - validatedPhoneNumbers = [ - self._send_validation(phoneNumber) - for phoneNumber in phoneNumbers - ] - flattenedPhoneNumbers = ",".join(validatedPhoneNumbers) - page = self._get_page_with_token( - self._sendSmsURL, - { - 'phoneNumber': flattenedPhoneNumbers, - 'text': unicode(message).encode("utf-8"), - }, - ) - self._parse_with_validation(page) - - def search(self, query): - """ - Search your Google Voice Account history for calls, voicemails, and sms - Returns ``Folder`` instance containting matching messages - @blocks - """ - page = self._get_page( - self._XML_SEARCH_URL, - {"q": query}, - ) - json, html = extract_payload(page) - return json - - def get_feed(self, feed): - """ - @blocks - """ - actualFeed = "_XML_%s_URL" % feed.upper() - feedUrl = getattr(self, actualFeed) - - page = self._get_page(feedUrl) - json, html = extract_payload(page) - - return json - - def recording_url(self, messageId): - url = self._downloadVoicemailURL+messageId - return url - - def download(self, messageId, targetPath): - """ - Download a voicemail or recorded call MP3 matching the given ``msg`` - which can either be a ``Message`` instance, or a SHA1 identifier. - Message hashes can be found in ``self.voicemail().messages`` for example. - @returns location of saved file. - @blocks - """ - page = self._get_page(self.recording_url(messageId)) - with open(targetPath, 'wb') as fo: - fo.write(page) - - def is_valid_syntax(self, number): - """ - @returns If This number be called ( syntax validation only ) - """ - return self._validateRe.match(number) is not None - - def get_account_number(self): - """ - @returns The GoogleVoice phone number - """ - return self._accountNum - - def get_callback_numbers(self): - """ - @returns a dictionary mapping call back numbers to descriptions - @note These results are cached for 30 minutes. - """ - return self._callbackNumbers - - def set_callback_number(self, callbacknumber): - """ - Set the number that GoogleVoice calls - @param callbacknumber should be a proper 10 digit number - """ - self._callbackNumber = callbacknumber - _moduleLogger.info("Callback number changed: %r" % self._callbackNumber) - return True - - def get_callback_number(self): - """ - @returns Current callback number or None - """ - return self._callbackNumber - - def get_received_calls(self): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - @blocks - """ - return self._parse_recent(self._get_page(self._XML_RECEIVED_URL)) - - def get_missed_calls(self): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - @blocks - """ - return self._parse_recent(self._get_page(self._XML_MISSED_URL)) - - def get_placed_calls(self): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - @blocks - """ - return self._parse_recent(self._get_page(self._XML_PLACED_URL)) - - def get_csv_contacts(self): - data = { - "groupToExport": "mine", - "exportType": "ALL", - "out": "OUTLOOK_CSV", - } - encodedData = urllib.urlencode(data) - contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData) - return contacts - - def get_voicemails(self): - """ - @blocks - """ - voicemailPage = self._get_page(self._XML_VOICEMAIL_URL) - voicemailHtml = self._grab_html(voicemailPage) - voicemailJson = self._grab_json(voicemailPage) - if voicemailJson is None: - return () - parsedVoicemail = self._parse_voicemail(voicemailHtml) - voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson) - return voicemails - - def get_texts(self): - """ - @blocks - """ - smsPage = self._get_page(self._XML_SMS_URL) - smsHtml = self._grab_html(smsPage) - smsJson = self._grab_json(smsPage) - if smsJson is None: - return () - parsedSms = self._parse_sms(smsHtml) - smss = self._merge_conversation_sources(parsedSms, smsJson) - return smss - - def get_unread_counts(self): - countPage = self._get_page(self._JSON_SMS_COUNT_URL) - counts = parse_json(countPage) - counts = counts["unreadCounts"] - return counts - - def mark_message(self, messageId, asRead): - """ - @blocks - """ - postData = { - "read": 1 if asRead else 0, - "id": messageId, - } - - markPage = self._get_page(self._markAsReadURL, postData) - - def archive_message(self, messageId): - """ - @blocks - """ - postData = { - "id": messageId, - } - - markPage = self._get_page(self._archiveMessageURL, postData) - - def _grab_json(self, flatXml): - xmlTree = ElementTree.fromstring(flatXml) - jsonElement = xmlTree.getchildren()[0] - flatJson = jsonElement.text - jsonTree = parse_json(flatJson) - return jsonTree - - def _grab_html(self, flatXml): - xmlTree = ElementTree.fromstring(flatXml) - htmlElement = xmlTree.getchildren()[1] - flatHtml = htmlElement.text - return flatHtml - - def _grab_account_info(self, page): - accountData = parse_json(page) - self._token = accountData["r"] - self._accountNum = accountData["number"]["raw"] - for callback in accountData["phones"].itervalues(): - self._callbackNumbers[callback["phoneNumber"]] = callback["name"] - if len(self._callbackNumbers) == 0: - _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page) - return accountData - - def _send_validation(self, number): - if not self.is_valid_syntax(number): - raise ValueError('Number is not valid: "%s"' % number) - return number - - def _parse_recent(self, recentPage): - allRecentHtml = self._grab_html(recentPage) - allRecentData = self._parse_history(allRecentHtml) - for recentCallData in allRecentData: - yield recentCallData - - def _parse_history(self, historyHtml): - splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) - for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - exactTime = google_strptime(exactTime) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - locationGroup = self._voicemailLocationRegex.search(messageHtml) - location = locationGroup.group(1).strip() if locationGroup else "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - name = nameGroup.group(1).strip() if nameGroup else "" - numberGroup = self._voicemailNumberRegex.search(messageHtml) - number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - yield { - "id": messageId.strip(), - "contactId": contactId, - "name": unescape(name), - "time": exactTime, - "relTime": relativeTime, - "prettyNumber": prettyNumber, - "number": number, - "location": unescape(location), - } - - @staticmethod - def _interpret_voicemail_regex(group): - quality, content, number = group.group(2), group.group(3), group.group(4) - text = MessageText() - if quality is not None and content is not None: - text.accuracy = quality - text.text = unescape(content) - return text - elif number is not None: - text.accuracy = MessageText.ACCURACY_HIGH - text.text = number - return text - - def _parse_voicemail(self, voicemailHtml): - splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) - for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): - conv = Conversation() - conv.type = Conversation.TYPE_VOICEMAIL - conv.id = messageId.strip() - - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = google_strptime(exactTimeText) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - locationGroup = self._voicemailLocationRegex.search(messageHtml) - conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "") - - nameGroup = self._voicemailNameRegex.search(messageHtml) - conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") - numberGroup = self._voicemailNumberRegex.search(messageHtml) - conv.number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - messageGroups = self._voicemailMessageRegex.finditer(messageHtml) - messageParts = [ - self._interpret_voicemail_regex(group) - for group in messageGroups - ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), ) - message = Message() - message.body = messageParts - message.whoFrom = conv.name - try: - message.when = conv.time.strftime("%I:%M %p") - except ValueError: - _moduleLogger.exception("Confusing time provided: %r" % conv.time) - message.when = "Unknown" - conv.messages = (message, ) - - yield conv - - @staticmethod - def _interpret_sms_message_parts(fromPart, textPart, timePart): - text = MessageText() - text.accuracy = MessageText.ACCURACY_MEDIUM - text.text = unescape(textPart) - - message = Message() - message.body = (text, ) - message.whoFrom = fromPart - message.when = timePart - - return message - - def _parse_sms(self, smsHtml): - splitSms = self._seperateVoicemailsRegex.split(smsHtml) - for messageId, messageHtml in itergroup(splitSms[1:], 2): - conv = Conversation() - conv.type = Conversation.TYPE_SMS - conv.id = messageId.strip() - - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = google_strptime(exactTimeText) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - conv.location = "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") - numberGroup = self._voicemailNumberRegex.search(messageHtml) - conv.number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - fromGroups = self._smsFromRegex.finditer(messageHtml) - fromParts = (group.group(1).strip() for group in fromGroups) - textGroups = self._smsTextRegex.finditer(messageHtml) - textParts = (group.group(1).strip() for group in textGroups) - timeGroups = self._smsTimeRegex.finditer(messageHtml) - timeParts = (group.group(1).strip() for group in timeGroups) - - messageParts = itertools.izip(fromParts, textParts, timeParts) - messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts] - conv.messages = messages - - yield conv - - @staticmethod - def _merge_conversation_sources(parsedMessages, json): - for message in parsedMessages: - jsonItem = json["messages"][message.id] - message.isRead = jsonItem["isRead"] - message.isSpam = jsonItem["isSpam"] - message.isTrash = jsonItem["isTrash"] - message.isArchived = "inbox" not in jsonItem["labels"] - yield message - - def _get_page(self, url, data = None, refererUrl = None): - headers = {} - if refererUrl is not None: - headers["Referer"] = refererUrl - - encodedData = urllib.urlencode(data) if data is not None else None - - try: - page = self._browser.download(url, encodedData, None, headers) - except urllib2.URLError, e: - _moduleLogger.error("Translating error: %s" % str(e)) - raise NetworkError("%s is not accesible" % url) - - return page - - def _get_page_with_token(self, url, data = None, refererUrl = None): - if data is None: - data = {} - data['_rnr_se'] = self._token - - page = self._get_page(url, data, refererUrl) - - return page - - def _parse_with_validation(self, page): - json = parse_json(page) - self._validate_response(json) - return json - - def _validate_response(self, response): - """ - Validates that the JSON response is A-OK - """ - try: - assert response is not None, "Response not provided" - assert 'ok' in response, "Response lacks status" - assert response['ok'], "Response not good" - except AssertionError: - try: - if response["data"]["code"] == 20: - raise RuntimeError( -"""Ambiguous error 20 returned by Google Voice. -Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber) - except KeyError: - pass - raise RuntimeError('There was a problem with GV: %s' % response) - - -_UNESCAPE_ENTITIES = { - """: '"', - " ": " ", - "'": "'", -} - - -def unescape(text): - plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) - return plain - - -def google_strptime(time): - """ - Hack: Google always returns the time in the same locale. Sadly if the - local system's locale is different, there isn't a way to perfectly handle - the time. So instead we handle implement some time formatting - """ - abbrevTime = time[:-3] - parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M") - if time.endswith("PM"): - parsedTime += datetime.timedelta(hours=12) - return parsedTime - - -def itergroup(iterator, count, padValue = None): - """ - Iterate in groups of 'count' values. If there - aren't enough values, the last result is padded with - None. - - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print list(val) - [1, 2, 3] - [4, 5, 6] - >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - (7, None, None) - >>> for val in itergroup("123456", 3): - ... print tuple(val) - ('1', '2', '3') - ('4', '5', '6') - >>> for val in itergroup("123456", 3): - ... print repr("".join(val)) - '123' - '456' - """ - paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) - nIterators = (paddedIterator, ) * count - return itertools.izip(*nIterators) - - -def safe_eval(s): - _TRUE_REGEX = re.compile("true") - _FALSE_REGEX = re.compile("false") - _COMMENT_REGEX = re.compile("^\s+//.*$", re.M) - s = _TRUE_REGEX.sub("True", s) - s = _FALSE_REGEX.sub("False", s) - s = _COMMENT_REGEX.sub("#", s) - try: - results = eval(s, {}, {}) - except SyntaxError: - _moduleLogger.exception("Oops") - results = None - return results - - -def _fake_parse_json(flattened): - return safe_eval(flattened) - - -def _actual_parse_json(flattened): - return simplejson.loads(flattened) - - -if simplejson is None: - parse_json = _fake_parse_json -else: - parse_json = _actual_parse_json - - -def extract_payload(flatXml): - xmlTree = ElementTree.fromstring(flatXml) - - jsonElement = xmlTree.getchildren()[0] - flatJson = jsonElement.text - jsonTree = parse_json(flatJson) - - htmlElement = xmlTree.getchildren()[1] - flatHtml = htmlElement.text - - return jsonTree, flatHtml - - -def guess_phone_type(number): - if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): - return GVoiceBackend.PHONE_TYPE_GIZMO - else: - return GVoiceBackend.PHONE_TYPE_MOBILE - - -def get_sane_callback(backend): - """ - Try to set a sane default callback number on these preferences - 1) 1747 numbers ( Gizmo ) - 2) anything with gizmo in the name - 3) anything with computer in the name - 4) the first value - """ - numbers = backend.get_callback_numbers() - - priorityOrderedCriteria = [ - ("\+1747", None), - ("1747", None), - ("747", None), - (None, "gizmo"), - (None, "computer"), - (None, "sip"), - (None, None), - ] - - for numberCriteria, descriptionCriteria in priorityOrderedCriteria: - numberMatcher = None - descriptionMatcher = None - if numberCriteria is not None: - numberMatcher = re.compile(numberCriteria) - elif descriptionCriteria is not None: - descriptionMatcher = re.compile(descriptionCriteria, re.I) - - for number, description in numbers.iteritems(): - if numberMatcher is not None and numberMatcher.match(number) is None: - continue - if descriptionMatcher is not None and descriptionMatcher.match(description) is None: - continue - return number - - -def set_sane_callback(backend): - """ - Try to set a sane default callback number on these preferences - 1) 1747 numbers ( Gizmo ) - 2) anything with gizmo in the name - 3) anything with computer in the name - 4) the first value - """ - number = get_sane_callback(backend) - backend.set_callback_number(number) - - -def _is_not_special(name): - return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name - - -def to_dict(obj): - members = inspect.getmembers(obj) - return dict((name, value) for (name, value) in members if _is_not_special(name)) - - -def grab_debug_info(username, password): - cookieFile = os.path.join(".", "raw_cookies.txt") - try: - os.remove(cookieFile) - except OSError: - pass - - backend = GVoiceBackend(cookieFile) - browser = backend._browser - - _TEST_WEBPAGES = [ - ("token", backend._tokenURL), - ("login", backend._loginURL), - ("isdnd", backend._isDndURL), - ("account", backend._XML_ACCOUNT_URL), - ("html_contacts", backend._XML_CONTACTS_URL), - ("contacts", backend._JSON_CONTACTS_URL), - ("csv", backend._CSV_CONTACTS_URL), - - ("voicemail", backend._XML_VOICEMAIL_URL), - ("html_sms", backend._XML_SMS_URL), - ("sms", backend._JSON_SMS_URL), - ("count", backend._JSON_SMS_COUNT_URL), - - ("recent", backend._XML_RECENT_URL), - ("placed", backend._XML_PLACED_URL), - ("recieved", backend._XML_RECEIVED_URL), - ("missed", backend._XML_MISSED_URL), - ] - - # Get Pages - print "Grabbing pre-login pages" - for name, url in _TEST_WEBPAGES: - try: - page = browser.download(url) - except StandardError, e: - print e.message - continue - print "\tWriting to file" - with open("not_loggedin_%s.txt" % name, "w") as f: - f.write(page) - - # Login - print "Attempting login" - galxToken = backend._get_token() - loginSuccessOrFailurePage = backend._login(username, password, galxToken) - with open("loggingin.txt", "w") as f: - print "\tWriting to file" - f.write(loginSuccessOrFailurePage) - try: - backend._grab_account_info(loginSuccessOrFailurePage) - except Exception: - # Retry in case the redirect failed - # luckily refresh_account_info does everything we need for a retry - loggedIn = backend.refresh_account_info() is not None - if not loggedIn: - raise - - # Get Pages - print "Grabbing post-login pages" - for name, url in _TEST_WEBPAGES: - try: - page = browser.download(url) - except StandardError, e: - print str(e) - continue - print "\tWriting to file" - with open("loggedin_%s.txt" % name, "w") as f: - f.write(page) - - # Cookies - browser.save_cookies() - print "\tWriting cookies to file" - with open("cookies.txt", "w") as f: - f.writelines( - "%s: %s\n" % (c.name, c.value) - for c in browser._cookies - ) - - -def grab_voicemails(username, password): - cookieFile = os.path.join(".", "raw_cookies.txt") - try: - os.remove(cookieFile) - except OSError: - pass - - backend = GVoiceBackend(cookieFile) - backend.login(username, password) - voicemails = list(backend.get_voicemails()) - for voicemail in voicemails: - print voicemail.id - backend.download(voicemail.id, ".") - - -def main(): - import sys - logging.basicConfig(level=logging.DEBUG) - args = sys.argv - if 3 <= len(args): - username = args[1] - password = args[2] - - grab_debug_info(username, password) - grab_voicemails(username, password) - - -if __name__ == "__main__": - main() diff --git a/src/backends/null_backend.py b/src/backends/null_backend.py deleted file mode 100644 index ebaa932..0000000 --- a/src/backends/null_backend.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's Grand Central service. -Copyright (C) 2008 Eric Warnke ericew AT gmail 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 -""" - - -class NullAddressBook(object): - - @property - def name(self): - return "None" - - def update_account(self, force = True): - pass - - def get_contacts(self): - return {} - - -class NullAddressBookFactory(object): - - def get_addressbooks(self): - yield NullAddressBook() diff --git a/src/backends/qt_backend.py b/src/backends/qt_backend.py deleted file mode 100644 index 88e52fa..0000000 --- a/src/backends/qt_backend.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import logging - -import util.qt_compat as qt_compat -if qt_compat.USES_PYSIDE: - try: - import QtMobility.Contacts as _QtContacts - QtContacts = _QtContacts - except ImportError: - QtContacts = None -else: - QtContacts = None - -import null_backend - - -_moduleLogger = logging.getLogger(__name__) - - -class QtContactsAddressBook(object): - - def __init__(self, name, uri): - self._name = name - self._uri = uri - self._manager = QtContacts.QContactManager.fromUri(uri) - self._contacts = None - - @property - def name(self): - return self._name - - @property - def error(self): - return self._manager.error() - - def update_account(self, force = True): - if not force and self._contacts is not None: - return - self._contacts = dict(self._get_contacts()) - - def get_contacts(self): - if self._contacts is None: - self._contacts = dict(self._get_contacts()) - return self._contacts - - def _get_contacts(self): - contacts = self._manager.contacts() - for contact in contacts: - contactId = contact.localId() - contactName = contact.displayLabel() - phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName) - phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails] - contactDetails = phones - if 0 < len(contactDetails): - yield str(contactId), { - "contactId": str(contactId), - "name": contactName, - "numbers": contactDetails, - } - - -class _QtContactsAddressBookFactory(object): - - def __init__(self): - self._availableManagers = {} - - availableMgrs = QtContacts.QContactManager.availableManagers() - availableMgrs.remove("invalid") - for managerName in availableMgrs: - params = {} - managerUri = QtContacts.QContactManager.buildUri(managerName, params) - self._availableManagers[managerName] = managerUri - - def get_addressbooks(self): - for name, uri in self._availableManagers.iteritems(): - book = QtContactsAddressBook(name, uri) - if book.error: - _moduleLogger.info("Could not load %r due to %r" % (name, book.error)) - else: - yield book - - -class _EmptyAddressBookFactory(object): - - def get_addressbooks(self): - if False: - yield None - - -if QtContacts is not None: - QtContactsAddressBookFactory = _QtContactsAddressBookFactory -else: - QtContactsAddressBookFactory = _EmptyAddressBookFactory - _moduleLogger.info("QtContacts support not available") - - -if __name__ == "__main__": - factory = QtContactsAddressBookFactory() - books = factory.get_addressbooks() - for book in books: - print book.name - print book.get_contacts() diff --git a/src/call_handler.py b/src/call_handler.py deleted file mode 100644 index 9b9c47d..0000000 --- a/src/call_handler.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import logging - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore -import dbus -try: - import telepathy as _telepathy - import util.tp_utils as telepathy_utils - telepathy = _telepathy -except ImportError: - telepathy = None - -import util.misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class _FakeSignaller(object): - - def start(self): - pass - - def stop(self): - pass - - -class _MissedCallWatcher(QtCore.QObject): - - callMissed = qt_compat.Signal() - - def __init__(self): - QtCore.QObject.__init__(self) - self._isStarted = False - self._isSupported = True - - self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel) - self._outstandingRequests = [] - - @property - def isSupported(self): - return self._isSupported - - @property - def isStarted(self): - return self._isStarted - - def start(self): - if self._isStarted: - _moduleLogger.info("voicemail monitor already started") - return - try: - self._newChannelSignaller.start() - except RuntimeError: - _moduleLogger.exception("Missed call detection not supported") - self._newChannelSignaller = _FakeSignaller() - self._isSupported = False - self._isStarted = True - - def stop(self): - if not self._isStarted: - _moduleLogger.info("voicemail monitor stopped without starting") - return - _moduleLogger.info("Stopping voicemail refresh") - self._newChannelSignaller.stop() - - # I don't want to trust whether the cancel happens within the current - # callback or not which could be the deciding factor between invalid - # iterators or infinite loops - localRequests = [r for r in self._outstandingRequests] - for request in localRequests: - localRequests.cancel() - - self._isStarted = False - - @misc_utils.log_exception(_moduleLogger) - def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType): - if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA: - return - - conn = telepathy.client.Connection(serviceName, connObjectPath) - try: - chan = telepathy.client.Channel(serviceName, channelObjectPath) - except dbus.exceptions.UnknownMethodException: - _moduleLogger.exception("Client might not have implemented a deprecated method") - return - missDetection = telepathy_utils.WasMissedCall( - bus, conn, chan, self._on_missed_call, self._on_error_for_missed - ) - self._outstandingRequests.append(missDetection) - - @misc_utils.log_exception(_moduleLogger) - def _on_missed_call(self, missDetection): - _moduleLogger.info("Missed a call") - self.callMissed.emit() - self._outstandingRequests.remove(missDetection) - - @misc_utils.log_exception(_moduleLogger) - def _on_error_for_missed(self, missDetection, reason): - _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason)) - self._outstandingRequests.remove(missDetection) - - -class _DummyMissedCallWatcher(QtCore.QObject): - - callMissed = qt_compat.Signal() - - def __init__(self): - QtCore.QObject.__init__(self) - self._isStarted = False - - @property - def isSupported(self): - return False - - @property - def isStarted(self): - return self._isStarted - - def start(self): - self._isStarted = True - - def stop(self): - if not self._isStarted: - _moduleLogger.info("voicemail monitor stopped without starting") - return - _moduleLogger.info("Stopping voicemail refresh") - self._isStarted = False - - -if telepathy is not None: - MissedCallWatcher = _MissedCallWatcher -else: - MissedCallWatcher = _DummyMissedCallWatcher - - -if __name__ == "__main__": - pass - diff --git a/src/constants.py b/src/constants.py deleted file mode 100644 index b9d3c79..0000000 --- a/src/constants.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - -__pretty_app_name__ = "DialCentral" -__app_name__ = "dialcentral" -__version__ = "1.3.6" -__build__ = 0 -__app_magic__ = 0xdeadbeef -_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__) -_user_settings_ = "%s/settings.ini" % _data_path_ -_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_ -_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__) -_notifier_logpath_ = "%s/notifier.log" % _data_path_ -IS_MAEMO = True diff --git a/src/dialcentral.py b/src/dialcentral.py deleted file mode 100755 index a20d4fe..0000000 --- a/src/dialcentral.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -import sys - - -sys.path.append("/opt/dialcentral/lib") - - -import dialcentral_qt - - -if __name__ == "__main__": - dialcentral_qt.run() diff --git a/src/dialcentral_qt.py b/src/dialcentral_qt.py deleted file mode 100755 index a464ad6..0000000 --- a/src/dialcentral_qt.py +++ /dev/null @@ -1,812 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF8 -*- - -from __future__ import with_statement - -import os -import base64 -import ConfigParser -import functools -import logging -import logging.handlers - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -import constants -import alarm_handler -from util import qtpie -from util import qwrappers -from util import qui_utils -from util import misc as misc_utils - -import session - - -_moduleLogger = logging.getLogger(__name__) - - -class Dialcentral(qwrappers.ApplicationWrapper): - - _DATA_PATHS = [ - os.path.join(os.path.dirname(__file__), "../share"), - os.path.join(os.path.dirname(__file__), "../data"), - ] - - def __init__(self, app): - self._dataPath = None - self._aboutDialog = None - self.notifyOnMissed = False - self.notifyOnVoicemail = False - self.notifyOnSms = False - - self._streamHandler = None - self._ledHandler = None - self._alarmHandler = alarm_handler.AlarmHandler() - - qwrappers.ApplicationWrapper.__init__(self, app, constants) - - def load_settings(self): - try: - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - except IOError, e: - _moduleLogger.info("No settings") - return - except ValueError: - _moduleLogger.info("Settings were corrupt") - return - except ConfigParser.MissingSectionHeaderError: - _moduleLogger.info("Settings were corrupt") - return - except Exception: - _moduleLogger.exception("Unknown loading error") - - self._mainWindow.load_settings(config) - - def save_settings(self): - _moduleLogger.info("Saving settings") - config = ConfigParser.SafeConfigParser() - - self._mainWindow.save_settings(config) - - with open(constants._user_settings_, "wb") as configFile: - config.write(configFile) - - def get_icon(self, name): - if self._dataPath is None: - for path in self._DATA_PATHS: - if os.path.exists(os.path.join(path, name)): - self._dataPath = path - break - if self._dataPath is not None: - icon = QtGui.QIcon(os.path.join(self._dataPath, name)) - return icon - else: - return None - - def get_resource(self, name): - if self._dataPath is None: - for path in self._DATA_PATHS: - if os.path.exists(os.path.join(path, name)): - self._dataPath = path - break - if self._dataPath is not None: - return os.path.join(self._dataPath, name) - else: - return None - - def _close_windows(self): - qwrappers.ApplicationWrapper._close_windows(self) - if self._aboutDialog is not None: - self._aboutDialog.close() - - @property - def fsContactsPath(self): - return os.path.join(constants._data_path_, "contacts") - - @property - def streamHandler(self): - if self._streamHandler is None: - import stream_handler - self._streamHandler = stream_handler.StreamHandler() - return self._streamHandler - - @property - def alarmHandler(self): - return self._alarmHandler - - @property - def ledHandler(self): - if self._ledHandler is None: - import led_handler - self._ledHandler = led_handler.LedHandler() - return self._ledHandler - - def _new_main_window(self): - return MainWindow(None, self) - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_about(self, checked = True): - with qui_utils.notify_error(self._errorLog): - if self._aboutDialog is None: - import dialogs - self._aboutDialog = dialogs.AboutDialog(self) - response = self._aboutDialog.run(self._mainWindow.window) - - -class DelayedWidget(object): - - def __init__(self, app, settingsNames): - self._layout = QtGui.QVBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self._widget = QtGui.QWidget() - self._widget.setContentsMargins(0, 0, 0, 0) - self._widget.setLayout(self._layout) - self._settings = dict((name, "") for name in settingsNames) - - self._child = None - self._isEnabled = True - - @property - def toplevel(self): - return self._widget - - def has_child(self): - return self._child is not None - - def set_child(self, child): - if self._child is not None: - self._layout.removeWidget(self._child.toplevel) - self._child = child - if self._child is not None: - self._layout.addWidget(self._child.toplevel) - - self._child.set_settings(self._settings) - - if self._isEnabled: - self._child.enable() - else: - self._child.disable() - - @property - def child(self): - return self._child - - def enable(self): - self._isEnabled = True - if self._child is not None: - self._child.enable() - - def disable(self): - self._isEnabled = False - if self._child is not None: - self._child.disable() - - def clear(self): - if self._child is not None: - self._child.clear() - - def refresh(self, force=True): - if self._child is not None: - self._child.refresh(force) - - def get_settings(self): - if self._child is not None: - return self._child.get_settings() - else: - return self._settings - - def set_settings(self, settings): - if self._child is not None: - self._child.set_settings(settings) - else: - self._settings = settings - - -def _tab_factory(tab, app, session, errorLog): - import gv_views - return gv_views.__dict__[tab](app, session, errorLog) - - -class MainWindow(qwrappers.WindowWrapper): - - KEYPAD_TAB = 0 - RECENT_TAB = 1 - MESSAGES_TAB = 2 - CONTACTS_TAB = 3 - MAX_TABS = 4 - - _TAB_TITLES = [ - "Dialpad", - "History", - "Messages", - "Contacts", - ] - assert len(_TAB_TITLES) == MAX_TABS - - _TAB_ICONS = [ - "dialpad.png", - "history.png", - "messages.png", - "contacts.png", - ] - assert len(_TAB_ICONS) == MAX_TABS - - _TAB_CLASS = [ - functools.partial(_tab_factory, "Dialpad"), - functools.partial(_tab_factory, "History"), - functools.partial(_tab_factory, "Messages"), - functools.partial(_tab_factory, "Contacts"), - ] - assert len(_TAB_CLASS) == MAX_TABS - - # Hack to allow delay importing/loading of tabs - _TAB_SETTINGS_NAMES = [ - (), - ("filter", ), - ("status", "type"), - ("selectedAddressbook", ), - ] - assert len(_TAB_SETTINGS_NAMES) == MAX_TABS - - def __init__(self, parent, app): - qwrappers.WindowWrapper.__init__(self, parent, app) - self._window.setWindowTitle("%s" % constants.__pretty_app_name__) - self._window.resized.connect(self._on_window_resized) - self._errorLog = self._app.errorLog - - self._session = session.Session(self._errorLog, constants._data_path_) - self._session.error.connect(self._on_session_error) - self._session.loggedIn.connect(self._on_login) - self._session.loggedOut.connect(self._on_logout) - self._session.draft.recipientsChanged.connect(self._on_recipients_changed) - self._session.newMessages.connect(self._on_new_message_alert) - self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert) - self._voicemailRefreshDelay = QtCore.QTimer() - self._voicemailRefreshDelay.setInterval(30 * 1000) - self._voicemailRefreshDelay.timeout.connect(self._on_call_missed) - self._voicemailRefreshDelay.setSingleShot(True) - self._callHandler = None - self._updateVoicemailOnMissedCall = False - - self._defaultCredentials = "", "" - self._curentCredentials = "", "" - self._currentTab = 0 - - self._credentialsDialog = None - self._smsEntryDialog = None - self._accountDialog = None - - self._tabsContents = [ - DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i]) - for i in xrange(self.MAX_TABS) - ] - for tab in self._tabsContents: - tab.disable() - - self._tabWidget = QtGui.QTabWidget() - if qui_utils.screen_orientation() == QtCore.Qt.Vertical: - self._tabWidget.setTabPosition(QtGui.QTabWidget.South) - else: - self._tabWidget.setTabPosition(QtGui.QTabWidget.West) - defaultTabIconSize = self._tabWidget.iconSize() - defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height() - for tabIndex, (tabTitle, tabIcon) in enumerate( - zip(self._TAB_TITLES, self._TAB_ICONS) - ): - icon = self._app.get_icon(tabIcon) - if constants.IS_MAEMO and icon is not None: - tabTitle = "" - - if icon is None: - self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle) - else: - iconSize = icon.availableSizes()[0] - defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width()) - defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height()) - self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle) - defaultTabIconWidth = max(defaultTabIconWidth, 32) - defaultTabIconHeight = max(defaultTabIconHeight, 32) - self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight)) - self._tabWidget.currentChanged.connect(self._on_tab_changed) - self._tabWidget.setContentsMargins(0, 0, 0, 0) - - self._layout.addWidget(self._tabWidget) - - self._loginAction = QtGui.QAction(None) - self._loginAction.setText("Login") - self._loginAction.triggered.connect(self._on_login_requested) - - self._importAction = QtGui.QAction(None) - self._importAction.setText("Import") - self._importAction.triggered.connect(self._on_import) - - self._accountAction = QtGui.QAction(None) - self._accountAction.setText("Account") - self._accountAction.triggered.connect(self._on_account) - - self._refreshConnectionAction = QtGui.QAction(None) - self._refreshConnectionAction.setText("Refresh Connection") - self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a")) - self._refreshConnectionAction.triggered.connect(self._on_refresh_connection) - - self._refreshTabAction = QtGui.QAction(None) - self._refreshTabAction.setText("Refresh Tab") - self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r")) - self._refreshTabAction.triggered.connect(self._on_refresh) - - fileMenu = self._window.menuBar().addMenu("&File") - fileMenu.addAction(self._loginAction) - fileMenu.addAction(self._refreshTabAction) - fileMenu.addAction(self._refreshConnectionAction) - - toolsMenu = self._window.menuBar().addMenu("&Tools") - toolsMenu.addAction(self._accountAction) - toolsMenu.addAction(self._importAction) - toolsMenu.addAction(self._app.aboutAction) - - self._initialize_tab(self._tabWidget.currentIndex()) - self.set_fullscreen(self._app.fullscreenAction.isChecked()) - self.update_orientation(self._app.orientation) - - def _init_call_handler(self): - if self._callHandler is not None: - return - import call_handler - self._callHandler = call_handler.MissedCallWatcher() - self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start) - - def set_default_credentials(self, username, password): - self._defaultCredentials = username, password - - def get_default_credentials(self): - return self._defaultCredentials - - def walk_children(self): - if self._smsEntryDialog is not None: - return (self._smsEntryDialog, ) - else: - return () - - def start(self): - qwrappers.WindowWrapper.start(self) - assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up" - if self._defaultCredentials != ("", ""): - username, password = self._defaultCredentials[0], self._defaultCredentials[1] - self._curentCredentials = username, password - self._session.login(username, password) - else: - self._prompt_for_login() - - def close(self): - for diag in ( - self._credentialsDialog, - self._accountDialog, - ): - if diag is not None: - diag.close() - for child in self.walk_children(): - child.window.destroyed.disconnect(self._on_child_close) - child.window.closed.disconnect(self._on_child_close) - child.close() - self._window.close() - - def destroy(self): - qwrappers.WindowWrapper.destroy(self) - if self._session.state != self._session.LOGGEDOUT_STATE: - self._session.logout() - - def get_current_tab(self): - return self._currentTab - - def set_current_tab(self, tabIndex): - self._tabWidget.setCurrentIndex(tabIndex) - - def load_settings(self, config): - blobs = "", "" - isFullscreen = False - orientation = self._app.orientation - tabIndex = 0 - try: - blobs = [ - config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) - for i in xrange(len(self.get_default_credentials())) - ] - isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen") - tabIndex = config.getint(constants.__pretty_app_name__, "tab") - orientation = config.get(constants.__pretty_app_name__, "orientation") - except ConfigParser.NoOptionError, e: - _moduleLogger.info( - "Settings file %s is missing option %s" % ( - constants._user_settings_, - e.option, - ), - ) - except ConfigParser.NoSectionError, e: - _moduleLogger.info( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - except Exception: - _moduleLogger.exception("Unknown loading error") - - try: - self._app.alarmHandler.load_settings(config, "alarm") - self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed") - self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail") - self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms") - self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall") - except ConfigParser.NoOptionError, e: - _moduleLogger.info( - "Settings file %s is missing option %s" % ( - constants._user_settings_, - e.option, - ), - ) - except ConfigParser.NoSectionError, e: - _moduleLogger.info( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - except Exception: - _moduleLogger.exception("Unknown loading error") - - creds = ( - base64.b64decode(blob) - for blob in blobs - ) - self.set_default_credentials(*creds) - self._app.fullscreenAction.setChecked(isFullscreen) - self._app.set_orientation(orientation) - self.set_current_tab(tabIndex) - - backendId = 2 # For backwards compatibility - for tabIndex, tabTitle in enumerate(self._TAB_TITLES): - sectionName = "%s - %s" % (backendId, tabTitle) - settings = self._tabsContents[tabIndex].get_settings() - for settingName in settings.iterkeys(): - try: - settingValue = config.get(sectionName, settingName) - except ConfigParser.NoOptionError, e: - _moduleLogger.info( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - return - except ConfigParser.NoSectionError, e: - _moduleLogger.info( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - return - except Exception: - _moduleLogger.exception("Unknown loading error") - return - settings[settingName] = settingValue - self._tabsContents[tabIndex].set_settings(settings) - - def save_settings(self, config): - config.add_section(constants.__pretty_app_name__) - config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab())) - config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked())) - config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation)) - for i, value in enumerate(self.get_default_credentials()): - blob = base64.b64encode(value) - config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob) - - config.add_section("alarm") - self._app.alarmHandler.save_settings(config, "alarm") - config.add_section("2 - Account Info") - config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed)) - config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail)) - config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms)) - config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall)) - - backendId = 2 # For backwards compatibility - for tabIndex, tabTitle in enumerate(self._TAB_TITLES): - sectionName = "%s - %s" % (backendId, tabTitle) - config.add_section(sectionName) - tabSettings = self._tabsContents[tabIndex].get_settings() - for settingName, settingValue in tabSettings.iteritems(): - config.set(sectionName, settingName, settingValue) - - def update_orientation(self, orientation): - qwrappers.WindowWrapper.update_orientation(self, orientation) - windowOrientation = self.idealWindowOrientation - if windowOrientation == QtCore.Qt.Horizontal: - self._tabWidget.setTabPosition(QtGui.QTabWidget.West) - else: - self._tabWidget.setTabPosition(QtGui.QTabWidget.South) - - def _initialize_tab(self, index): - assert index < self.MAX_TABS, "Invalid tab" - if not self._tabsContents[index].has_child(): - tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog) - self._tabsContents[index].set_child(tab) - self._tabsContents[index].refresh(force=False) - - def _prompt_for_login(self): - if self._credentialsDialog is None: - import dialogs - self._credentialsDialog = dialogs.CredentialsDialog(self._app) - credentials = self._credentialsDialog.run( - self._defaultCredentials[0], self._defaultCredentials[1], self.window - ) - if credentials is None: - return - username, password = credentials - self._curentCredentials = username, password - self._session.login(username, password) - - def _show_account_dialog(self): - if self._accountDialog is None: - import dialogs - self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog) - self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported) - self._accountDialog.settingsApproved.connect(self._on_settings_approved) - - if self._callHandler is not None and not self._callHandler.isSupported: - self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED - elif self._updateVoicemailOnMissedCall: - self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED - else: - self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED - self._accountDialog.notifications = self._app.alarmHandler.alarmType - self._accountDialog.notificationTime = self._app.alarmHandler.recurrence - self._accountDialog.notifyOnMissed = self._app.notifyOnMissed - self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail - self._accountDialog.notifyOnSms = self._app.notifyOnSms - self._accountDialog.set_callbacks( - self._session.get_callback_numbers(), self._session.get_callback_number() - ) - accountNumberToDisplay = self._session.get_account_number() - if not accountNumberToDisplay: - accountNumberToDisplay = "Not Available (%s)" % self._session.state - self._accountDialog.set_account_number(accountNumberToDisplay) - self._accountDialog.orientation = self._app.orientation - - self._accountDialog.run() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_settings_approved(self): - if self._accountDialog.doClear: - self._session.logout_and_clear() - self._defaultCredentials = "", "" - self._curentCredentials = "", "" - for tab in self._tabsContents: - tab.disable() - else: - callbackNumber = self._accountDialog.selectedCallback - self._session.set_callback_number(callbackNumber) - - if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED: - pass - elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED: - self._updateVoicemailOnMissedCall = True - self._init_call_handler() - self._callHandler.start() - else: - self._updateVoicemailOnMissedCall = False - if self._callHandler is not None: - self._callHandler.stop() - if ( - self._accountDialog.notifyOnMissed or - self._accountDialog.notifyOnVoicemail or - self._accountDialog.notifyOnSms - ): - notifications = self._accountDialog.notifications - else: - notifications = self._accountDialog.ALARM_NONE - self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime) - - self._app.notifyOnMissed = self._accountDialog.notifyOnMissed - self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail - self._app.notifyOnSms = self._accountDialog.notifyOnSms - self._app.set_orientation(self._accountDialog.orientation) - self._app.save_settings() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_window_resized(self): - with qui_utils.notify_error(self._app.errorLog): - windowOrientation = self.idealWindowOrientation - if windowOrientation == QtCore.Qt.Horizontal: - self._tabWidget.setTabPosition(QtGui.QTabWidget.West) - else: - self._tabWidget.setTabPosition(QtGui.QTabWidget.South) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_new_message_alert(self): - with qui_utils.notify_error(self._errorLog): - if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION: - if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal: - self._errorLog.push_message("New messages") - else: - self._app.ledHandler.on() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_call_missed(self): - with qui_utils.notify_error(self._errorLog): - self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True) - - @qt_compat.Slot(str) - @misc_utils.log_exception(_moduleLogger) - def _on_session_error(self, message): - with qui_utils.notify_error(self._errorLog): - self._errorLog.push_error(message) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_login(self): - with qui_utils.notify_error(self._errorLog): - changedAccounts = self._defaultCredentials != self._curentCredentials - noCallback = not self._session.get_callback_number() - if changedAccounts or noCallback: - self._show_account_dialog() - - self._defaultCredentials = self._curentCredentials - - for tab in self._tabsContents: - tab.enable() - self._initialize_tab(self._currentTab) - if self._updateVoicemailOnMissedCall: - self._init_call_handler() - self._callHandler.start() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_logout(self): - with qui_utils.notify_error(self._errorLog): - for tab in self._tabsContents: - tab.disable() - if self._callHandler is not None: - self._callHandler.stop() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_app_alert(self): - with qui_utils.notify_error(self._errorLog): - if self._session.state == self._session.LOGGEDIN_STATE: - messageType = { - (True, True): self._session.MESSAGE_ALL, - (True, False): self._session.MESSAGE_TEXTS, - (False, True): self._session.MESSAGE_VOICEMAILS, - }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)] - self._session.update_messages(messageType, force=True) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_recipients_changed(self): - with qui_utils.notify_error(self._errorLog): - if self._session.draft.get_num_contacts() == 0: - return - - if self._smsEntryDialog is None: - import dialogs - self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog) - self._smsEntryDialog.window.destroyed.connect(self._on_child_close) - self._smsEntryDialog.window.closed.connect(self._on_child_close) - self._smsEntryDialog.window.show() - - @misc_utils.log_exception(_moduleLogger) - def _on_child_close(self, obj = None): - self._smsEntryDialog = None - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_login_requested(self, checked = True): - with qui_utils.notify_error(self._errorLog): - self._prompt_for_login() - - @qt_compat.Slot(int) - @misc_utils.log_exception(_moduleLogger) - def _on_tab_changed(self, index): - with qui_utils.notify_error(self._errorLog): - self._currentTab = index - self._initialize_tab(index) - if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION: - self._app.ledHandler.off() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_refresh(self, checked = True): - with qui_utils.notify_error(self._errorLog): - self._tabsContents[self._currentTab].refresh(force=True) - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_refresh_connection(self, checked = True): - with qui_utils.notify_error(self._errorLog): - self._session.refresh_connection() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_import(self, checked = True): - with qui_utils.notify_error(self._errorLog): - csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)") - csvName = unicode(csvName) - if not csvName: - return - import shutil - shutil.copy2(csvName, self._app.fsContactsPath) - if self._tabsContents[self.CONTACTS_TAB].has_child: - self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_account(self, checked = True): - with qui_utils.notify_error(self._errorLog): - assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings" - self._show_account_dialog() - - -def run(): - try: - os.makedirs(constants._data_path_) - except OSError, e: - if e.errno != 17: - raise - - logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s' - logging.basicConfig(level=logging.DEBUG, format=logFormat) - rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1) - rotating.setFormatter(logging.Formatter(logFormat)) - root = logging.getLogger() - root.addHandler(rotating) - _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__)) - _moduleLogger.info("OS: %s" % (os.uname()[0], )) - _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:]) - _moduleLogger.info("Hostname: %s" % os.uname()[1]) - - try: - import gobject - gobject.threads_init() - except ImportError: - _moduleLogger.info("GObject support not available") - try: - import dbus - try: - from dbus.mainloop.qt import DBusQtMainLoop - DBusQtMainLoop(set_as_default=True) - _moduleLogger.info("Using Qt mainloop") - except ImportError: - try: - from dbus.mainloop.glib import DBusGMainLoop - DBusGMainLoop(set_as_default=True) - _moduleLogger.info("Using GObject mainloop") - except ImportError: - _moduleLogger.info("Mainloop not available") - except ImportError: - _moduleLogger.info("DBus support not available") - - app = QtGui.QApplication([]) - handle = Dialcentral(app) - qtpie.init_pies() - return app.exec_() - - -if __name__ == "__main__": - import sys - - val = run() - sys.exit(val) diff --git a/src/dialogs.py b/src/dialogs.py deleted file mode 100644 index 8fbf328..0000000 --- a/src/dialogs.py +++ /dev/null @@ -1,1192 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import functools -import copy -import logging - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -import constants -from util import qwrappers -from util import qui_utils -from util import misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class CredentialsDialog(object): - - def __init__(self, app): - self._app = app - self._usernameField = QtGui.QLineEdit() - self._passwordField = QtGui.QLineEdit() - self._passwordField.setEchoMode(QtGui.QLineEdit.Password) - - self._credLayout = QtGui.QGridLayout() - self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0) - self._credLayout.addWidget(self._usernameField, 0, 1) - self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0) - self._credLayout.addWidget(self._passwordField, 1, 1) - - self._loginButton = QtGui.QPushButton("&Login") - self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel) - self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole) - - self._layout = QtGui.QVBoxLayout() - self._layout.addLayout(self._credLayout) - self._layout.addWidget(self._buttonLayout) - - self._dialog = QtGui.QDialog() - self._dialog.setWindowTitle("Login") - self._dialog.setLayout(self._layout) - self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - self._buttonLayout.accepted.connect(self._dialog.accept) - self._buttonLayout.rejected.connect(self._dialog.reject) - - self._closeWindowAction = QtGui.QAction(None) - self._closeWindowAction.setText("Close") - self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) - self._closeWindowAction.triggered.connect(self._on_close_window) - - self._dialog.addAction(self._closeWindowAction) - self._dialog.addAction(app.quitAction) - self._dialog.addAction(app.fullscreenAction) - - def run(self, defaultUsername, defaultPassword, parent=None): - self._dialog.setParent(parent, QtCore.Qt.Dialog) - try: - self._usernameField.setText(defaultUsername) - self._passwordField.setText(defaultPassword) - - response = self._dialog.exec_() - if response == QtGui.QDialog.Accepted: - return str(self._usernameField.text()), str(self._passwordField.text()) - elif response == QtGui.QDialog.Rejected: - return None - else: - _moduleLogger.error("Unknown response") - return None - finally: - self._dialog.setParent(None, QtCore.Qt.Dialog) - - def close(self): - try: - self._dialog.reject() - except RuntimeError: - _moduleLogger.exception("Oh well") - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_close_window(self, checked = True): - with qui_utils.notify_error(self._app.errorLog): - self._dialog.reject() - - -class AboutDialog(object): - - def __init__(self, app): - self._app = app - self._title = QtGui.QLabel( - "

%s

Version: %s

" % ( - constants.__pretty_app_name__, constants.__version__ - ) - ) - self._title.setTextFormat(QtCore.Qt.RichText) - self._title.setAlignment(QtCore.Qt.AlignCenter) - self._copyright = QtGui.QLabel("
Developed by Ed Page
Icons: See website
") - self._copyright.setTextFormat(QtCore.Qt.RichText) - self._copyright.setAlignment(QtCore.Qt.AlignCenter) - self._link = QtGui.QLabel('DialCentral Website') - self._link.setTextFormat(QtCore.Qt.RichText) - self._link.setAlignment(QtCore.Qt.AlignCenter) - self._link.setOpenExternalLinks(True) - - self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel) - - self._layout = QtGui.QVBoxLayout() - self._layout.addWidget(self._title) - self._layout.addWidget(self._copyright) - self._layout.addWidget(self._link) - self._layout.addWidget(self._buttonLayout) - - self._dialog = QtGui.QDialog() - self._dialog.setWindowTitle("About") - self._dialog.setLayout(self._layout) - self._buttonLayout.rejected.connect(self._dialog.reject) - - self._closeWindowAction = QtGui.QAction(None) - self._closeWindowAction.setText("Close") - self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) - self._closeWindowAction.triggered.connect(self._on_close_window) - - self._dialog.addAction(self._closeWindowAction) - self._dialog.addAction(app.quitAction) - self._dialog.addAction(app.fullscreenAction) - - def run(self, parent=None): - self._dialog.setParent(parent, QtCore.Qt.Dialog) - - response = self._dialog.exec_() - return response - - def close(self): - try: - self._dialog.reject() - except RuntimeError: - _moduleLogger.exception("Oh well") - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_close_window(self, checked = True): - with qui_utils.notify_error(self._app.errorLog): - self._dialog.reject() - - -class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper): - - # @bug Can't enter custom callback numbers - - _RECURRENCE_CHOICES = [ - (1, "1 minute"), - (2, "2 minutes"), - (3, "3 minutes"), - (5, "5 minutes"), - (8, "8 minutes"), - (10, "10 minutes"), - (15, "15 minutes"), - (30, "30 minutes"), - (45, "45 minutes"), - (60, "1 hour"), - (3*60, "3 hours"), - (6*60, "6 hours"), - (12*60, "12 hours"), - ] - - ALARM_NONE = "No Alert" - ALARM_BACKGROUND = "Background Alert" - ALARM_APPLICATION = "Application Alert" - - VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported" - VOICEMAIL_CHECK_DISABLED = "Disabled" - VOICEMAIL_CHECK_ENABLED = "Enabled" - - settingsApproved = qt_compat.Signal() - - def __init__(self, parent, app, errorLog): - QtCore.QObject.__init__(self) - qwrappers.WindowWrapper.__init__(self, parent, app) - self._app = app - self._doClear = False - - self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET") - self._notificationSelecter = QtGui.QComboBox() - self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change) - self._notificationTimeSelector = QtGui.QComboBox() - #self._notificationTimeSelector.setEditable(True) - self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop) - for _, label in self._RECURRENCE_CHOICES: - self._notificationTimeSelector.addItem(label) - self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls") - self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail") - self._smsNotificationButton = QtGui.QCheckBox("SMS") - self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls") - self._clearButton = QtGui.QPushButton("Clear Account") - self._clearButton.clicked.connect(self._on_clear) - self._callbackSelector = QtGui.QComboBox() - #self._callbackSelector.setEditable(True) - self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop) - self._orientationSelector = QtGui.QComboBox() - for orientationMode in [ - self._app.DEFAULT_ORIENTATION, - self._app.AUTO_ORIENTATION, - self._app.LANDSCAPE_ORIENTATION, - self._app.PORTRAIT_ORIENTATION, - ]: - self._orientationSelector.addItem(orientationMode) - - self._update_notification_state() - - self._credLayout = QtGui.QGridLayout() - self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0) - self._credLayout.addWidget(self._accountNumberLabel, 0, 1) - self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0) - self._credLayout.addWidget(self._callbackSelector, 1, 1) - self._credLayout.addWidget(self._notificationSelecter, 2, 0) - self._credLayout.addWidget(self._notificationTimeSelector, 2, 1) - self._credLayout.addWidget(QtGui.QLabel(""), 3, 0) - self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1) - self._credLayout.addWidget(QtGui.QLabel(""), 4, 0) - self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1) - self._credLayout.addWidget(QtGui.QLabel(""), 5, 0) - self._credLayout.addWidget(self._smsNotificationButton, 5, 1) - self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0) - self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1) - self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0) - self._credLayout.addWidget(self._orientationSelector, 7, 1) - self._credLayout.addWidget(QtGui.QLabel(""), 8, 0) - self._credLayout.addWidget(QtGui.QLabel(""), 9, 0) - self._credLayout.addWidget(self._clearButton, 9, 1) - - self._credWidget = QtGui.QWidget() - self._credWidget.setLayout(self._credLayout) - self._credWidget.setContentsMargins(0, 0, 0, 0) - self._scrollSettings = QtGui.QScrollArea() - self._scrollSettings.setWidget(self._credWidget) - self._scrollSettings.setWidgetResizable(True) - self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - - self._applyButton = QtGui.QPushButton("&Apply") - self._applyButton.clicked.connect(self._on_settings_apply) - self._cancelButton = QtGui.QPushButton("&Cancel") - self._cancelButton.clicked.connect(self._on_settings_cancel) - self._buttonLayout = QtGui.QHBoxLayout() - self._buttonLayout.addStretch() - self._buttonLayout.addWidget(self._cancelButton) - self._buttonLayout.addStretch() - self._buttonLayout.addWidget(self._applyButton) - self._buttonLayout.addStretch() - - self._layout.addWidget(self._scrollSettings) - self._layout.addLayout(self._buttonLayout) - self._layout.setDirection(QtGui.QBoxLayout.TopToBottom) - - self._window.setWindowTitle("Account") - self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - - @property - def doClear(self): - return self._doClear - - def setIfNotificationsSupported(self, isSupported): - if isSupported: - self._notificationSelecter.clear() - self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND]) - self._notificationTimeSelector.setEnabled(False) - self._missedCallsNotificationButton.setEnabled(False) - self._voicemailNotificationButton.setEnabled(False) - self._smsNotificationButton.setEnabled(False) - else: - self._notificationSelecter.clear() - self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION]) - self._notificationTimeSelector.setEnabled(False) - self._missedCallsNotificationButton.setEnabled(False) - self._voicemailNotificationButton.setEnabled(False) - self._smsNotificationButton.setEnabled(False) - - def set_account_number(self, num): - self._accountNumberLabel.setText(num) - - orientation = property( - lambda self: str(self._orientationSelector.currentText()), - lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode), - ) - - def _set_voicemail_on_missed(self, status): - if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED: - self._voicemailOnMissedButton.setChecked(False) - self._voicemailOnMissedButton.hide() - elif status == self.VOICEMAIL_CHECK_DISABLED: - self._voicemailOnMissedButton.setChecked(False) - self._voicemailOnMissedButton.show() - elif status == self.VOICEMAIL_CHECK_ENABLED: - self._voicemailOnMissedButton.setChecked(True) - self._voicemailOnMissedButton.show() - else: - raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status) - - def _get_voicemail_on_missed(self): - if not self._voicemailOnMissedButton.isVisible(): - return self.VOICEMAIL_CHECK_NOT_SUPPORTED - elif self._voicemailOnMissedButton.isChecked(): - return self.VOICEMAIL_CHECK_ENABLED - else: - return self.VOICEMAIL_CHECK_DISABLED - - updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed) - - notifications = property( - lambda self: str(self._notificationSelecter.currentText()), - lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled), - ) - - notifyOnMissed = property( - lambda self: self._missedCallsNotificationButton.isChecked(), - lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled), - ) - - notifyOnVoicemail = property( - lambda self: self._voicemailNotificationButton.isChecked(), - lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled), - ) - - notifyOnSms = property( - lambda self: self._smsNotificationButton.isChecked(), - lambda self, enabled: self._smsNotificationButton.setChecked(enabled), - ) - - def _get_notification_time(self): - index = self._notificationTimeSelector.currentIndex() - minutes = self._RECURRENCE_CHOICES[index][0] - return minutes - - def _set_notification_time(self, minutes): - for i, (time, _) in enumerate(self._RECURRENCE_CHOICES): - if time == minutes: - self._notificationTimeSelector.setCurrentIndex(i) - break - else: - self._notificationTimeSelector.setCurrentIndex(0) - - notificationTime = property(_get_notification_time, _set_notification_time) - - @property - def selectedCallback(self): - index = self._callbackSelector.currentIndex() - data = str(self._callbackSelector.itemData(index)) - return data - - def set_callbacks(self, choices, default): - self._callbackSelector.clear() - - self._callbackSelector.addItem("Not Set", "") - - uglyDefault = misc_utils.make_ugly(default) - if not uglyDefault: - uglyDefault = default - for number, description in choices.iteritems(): - prettyNumber = misc_utils.make_pretty(number) - uglyNumber = misc_utils.make_ugly(number) - if not uglyNumber: - prettyNumber = number - uglyNumber = number - - self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber) - if uglyNumber == uglyDefault: - self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1) - - def run(self): - self._doClear = False - self._window.show() - - def close(self): - try: - self._window.hide() - except RuntimeError: - _moduleLogger.exception("Oh well") - - def _update_notification_state(self): - currentText = str(self._notificationSelecter.currentText()) - if currentText == self.ALARM_BACKGROUND: - self._notificationTimeSelector.setEnabled(True) - - self._missedCallsNotificationButton.setEnabled(True) - self._voicemailNotificationButton.setEnabled(True) - self._smsNotificationButton.setEnabled(True) - elif currentText == self.ALARM_APPLICATION: - self._notificationTimeSelector.setEnabled(True) - - self._missedCallsNotificationButton.setEnabled(False) - self._voicemailNotificationButton.setEnabled(True) - self._smsNotificationButton.setEnabled(True) - - self._missedCallsNotificationButton.setChecked(False) - else: - self._notificationTimeSelector.setEnabled(False) - - self._missedCallsNotificationButton.setEnabled(False) - self._voicemailNotificationButton.setEnabled(False) - self._smsNotificationButton.setEnabled(False) - - self._missedCallsNotificationButton.setChecked(False) - self._voicemailNotificationButton.setChecked(False) - self._smsNotificationButton.setChecked(False) - - @qt_compat.Slot(int) - @misc_utils.log_exception(_moduleLogger) - def _on_notification_change(self, index): - with qui_utils.notify_error(self._app.errorLog): - self._update_notification_state() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_settings_cancel(self, checked = False): - with qui_utils.notify_error(self._app.errorLog): - self.hide() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - def _on_settings_apply(self, checked = False): - self.__on_settings_apply(checked) - - @misc_utils.log_exception(_moduleLogger) - def __on_settings_apply(self, checked = False): - with qui_utils.notify_error(self._app.errorLog): - self.settingsApproved.emit() - self.hide() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_clear(self, checked = False): - with qui_utils.notify_error(self._app.errorLog): - self._doClear = True - self.settingsApproved.emit() - self.hide() - - -class ContactList(object): - - _SENTINEL_ICON = QtGui.QIcon() - - def __init__(self, app, session): - self._app = app - self._session = session - self._targetLayout = QtGui.QVBoxLayout() - self._targetList = QtGui.QWidget() - self._targetList.setLayout(self._targetLayout) - self._uiItems = [] - self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON) - - @property - def toplevel(self): - return self._targetList - - def setVisible(self, isVisible): - self._targetList.setVisible(isVisible) - - def update(self): - cids = list(self._session.draft.get_contacts()) - amountCommon = min(len(cids), len(self._uiItems)) - - # Run through everything in common - for i in xrange(0, amountCommon): - cid = cids[i] - uiItem = self._uiItems[i] - title = self._session.draft.get_title(cid) - description = self._session.draft.get_description(cid) - numbers = self._session.draft.get_numbers(cid) - uiItem["cid"] = cid - uiItem["title"] = title - uiItem["description"] = description - uiItem["numbers"] = numbers - uiItem["label"].setText(title) - self._populate_number_selector(uiItem["selector"], cid, i, numbers) - uiItem["rowWidget"].setVisible(True) - - # More contacts than ui items - for i in xrange(amountCommon, len(cids)): - cid = cids[i] - title = self._session.draft.get_title(cid) - description = self._session.draft.get_description(cid) - numbers = self._session.draft.get_numbers(cid) - - titleLabel = QtGui.QLabel(title) - titleLabel.setWordWrap(True) - numberSelector = QtGui.QComboBox() - self._populate_number_selector(numberSelector, cid, i, numbers) - - callback = functools.partial( - self._on_change_number, - i - ) - callback.__name__ = "thanks partials for not having names and pyqt for requiring them" - numberSelector.activated.connect( - qt_compat.Slot(int)(callback) - ) - - if self._closeIcon is self._SENTINEL_ICON: - deleteButton = QtGui.QPushButton("Delete") - else: - deleteButton = QtGui.QPushButton(self._closeIcon, "") - deleteButton.setSizePolicy(QtGui.QSizePolicy( - QtGui.QSizePolicy.Minimum, - QtGui.QSizePolicy.Minimum, - QtGui.QSizePolicy.PushButton, - )) - callback = functools.partial( - self._on_remove_contact, - i - ) - callback.__name__ = "thanks partials for not having names and pyqt for requiring them" - deleteButton.clicked.connect(callback) - - rowLayout = QtGui.QHBoxLayout() - rowLayout.addWidget(titleLabel, 1000) - rowLayout.addWidget(numberSelector, 0) - rowLayout.addWidget(deleteButton, 0) - rowWidget = QtGui.QWidget() - rowWidget.setLayout(rowLayout) - self._targetLayout.addWidget(rowWidget) - - uiItem = {} - uiItem["cid"] = cid - uiItem["title"] = title - uiItem["description"] = description - uiItem["numbers"] = numbers - uiItem["label"] = titleLabel - uiItem["selector"] = numberSelector - uiItem["rowWidget"] = rowWidget - self._uiItems.append(uiItem) - amountCommon = i+1 - - # More UI items than contacts - for i in xrange(amountCommon, len(self._uiItems)): - uiItem = self._uiItems[i] - uiItem["rowWidget"].setVisible(False) - amountCommon = i+1 - - def _populate_number_selector(self, selector, cid, cidIndex, numbers): - selector.clear() - - selectedNumber = self._session.draft.get_selected_number(cid) - if len(numbers) == 1: - # If no alt numbers available, check the address book - numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1]) - else: - defaultIndex = _index_number(numbers, selectedNumber) - - for number, description in numbers: - if description: - label = "%s - %s" % (number, description) - else: - label = number - selector.addItem(label) - selector.setVisible(True) - if 1 < len(numbers): - selector.setEnabled(True) - selector.setCurrentIndex(defaultIndex) - else: - selector.setEnabled(False) - - @misc_utils.log_exception(_moduleLogger) - def _on_change_number(self, cidIndex, index): - with qui_utils.notify_error(self._app.errorLog): - # Exception thrown when the first item is removed - try: - cid = self._uiItems[cidIndex]["cid"] - numbers = self._session.draft.get_numbers(cid) - except IndexError: - _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) - return - except KeyError: - _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) - return - number = numbers[index][0] - self._session.draft.set_selected_number(cid, number) - - @misc_utils.log_exception(_moduleLogger) - def _on_remove_contact(self, index, toggled): - with qui_utils.notify_error(self._app.errorLog): - self._session.draft.remove_contact(self._uiItems[index]["cid"]) - - -class VoicemailPlayer(object): - - def __init__(self, app, session, errorLog): - self._app = app - self._session = session - self._errorLog = errorLog - self._token = None - self._session.voicemailAvailable.connect(self._on_voicemail_downloaded) - self._session.draft.recipientsChanged.connect(self._on_recipients_changed) - - self._playButton = QtGui.QPushButton("Play") - self._playButton.clicked.connect(self._on_voicemail_play) - self._pauseButton = QtGui.QPushButton("Pause") - self._pauseButton.clicked.connect(self._on_voicemail_pause) - self._pauseButton.hide() - self._resumeButton = QtGui.QPushButton("Resume") - self._resumeButton.clicked.connect(self._on_voicemail_resume) - self._resumeButton.hide() - self._stopButton = QtGui.QPushButton("Stop") - self._stopButton.clicked.connect(self._on_voicemail_stop) - self._stopButton.hide() - - self._downloadButton = QtGui.QPushButton("Download Voicemail") - self._downloadButton.clicked.connect(self._on_voicemail_download) - self._downloadLayout = QtGui.QHBoxLayout() - self._downloadLayout.addWidget(self._downloadButton) - self._downloadWidget = QtGui.QWidget() - self._downloadWidget.setLayout(self._downloadLayout) - - self._playLabel = QtGui.QLabel("Voicemail") - self._saveButton = QtGui.QPushButton("Save") - self._saveButton.clicked.connect(self._on_voicemail_save) - self._playerLayout = QtGui.QHBoxLayout() - self._playerLayout.addWidget(self._playLabel) - self._playerLayout.addWidget(self._playButton) - self._playerLayout.addWidget(self._pauseButton) - self._playerLayout.addWidget(self._resumeButton) - self._playerLayout.addWidget(self._stopButton) - self._playerLayout.addWidget(self._saveButton) - self._playerWidget = QtGui.QWidget() - self._playerWidget.setLayout(self._playerLayout) - - self._visibleWidget = None - self._layout = QtGui.QHBoxLayout() - self._layout.setContentsMargins(0, 0, 0, 0) - self._widget = QtGui.QWidget() - self._widget.setLayout(self._layout) - self._update_state() - - @property - def toplevel(self): - return self._widget - - def destroy(self): - self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded) - self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed) - self._invalidate_token() - - def _invalidate_token(self): - if self._token is not None: - self._token.invalidate() - self._token.error.disconnect(self._on_play_error) - self._token.stateChange.connect(self._on_play_state) - self._token.invalidated.connect(self._on_play_invalidated) - - def _show_download(self, messageId): - if self._visibleWidget is self._downloadWidget: - return - self._hide() - self._layout.addWidget(self._downloadWidget) - self._visibleWidget = self._downloadWidget - self._visibleWidget.show() - - def _show_player(self, messageId): - if self._visibleWidget is self._playerWidget: - return - self._hide() - self._layout.addWidget(self._playerWidget) - self._visibleWidget = self._playerWidget - self._visibleWidget.show() - - def _hide(self): - if self._visibleWidget is None: - return - self._visibleWidget.hide() - self._layout.removeWidget(self._visibleWidget) - self._visibleWidget = None - - def _update_play_state(self): - if self._token is not None and self._token.isValid: - self._playButton.setText("Stop") - else: - self._playButton.setText("Play") - - def _update_state(self): - if self._session.draft.get_num_contacts() != 1: - self._hide() - return - - (cid, ) = self._session.draft.get_contacts() - messageId = self._session.draft.get_message_id(cid) - if messageId is None: - self._hide() - return - - if self._session.is_available(messageId): - self._show_player(messageId) - else: - self._show_download(messageId) - if self._token is not None: - self._token.invalidate() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_save(self, arg): - with qui_utils.notify_error(self._app.errorLog): - targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)") - targetPath = unicode(targetPath) - if not targetPath: - return - - (cid, ) = self._session.draft.get_contacts() - messageId = self._session.draft.get_message_id(cid) - sourcePath = self._session.voicemail_path(messageId) - import shutil - shutil.copy2(sourcePath, targetPath) - - @misc_utils.log_exception(_moduleLogger) - def _on_play_error(self, error): - with qui_utils.notify_error(self._app.errorLog): - self._app.errorLog.push_error(error) - - @misc_utils.log_exception(_moduleLogger) - def _on_play_invalidated(self): - with qui_utils.notify_error(self._app.errorLog): - self._playButton.show() - self._pauseButton.hide() - self._resumeButton.hide() - self._stopButton.hide() - self._invalidate_token() - - @misc_utils.log_exception(_moduleLogger) - def _on_play_state(self, state): - with qui_utils.notify_error(self._app.errorLog): - if state == self._token.STATE_PLAY: - self._playButton.hide() - self._pauseButton.show() - self._resumeButton.hide() - self._stopButton.show() - elif state == self._token.STATE_PAUSE: - self._playButton.hide() - self._pauseButton.hide() - self._resumeButton.show() - self._stopButton.show() - elif state == self._token.STATE_STOP: - self._playButton.show() - self._pauseButton.hide() - self._resumeButton.hide() - self._stopButton.hide() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_play(self, arg): - with qui_utils.notify_error(self._app.errorLog): - (cid, ) = self._session.draft.get_contacts() - messageId = self._session.draft.get_message_id(cid) - sourcePath = self._session.voicemail_path(messageId) - - self._invalidate_token() - uri = "file://%s" % sourcePath - self._token = self._app.streamHandler.set_file(uri) - self._token.stateChange.connect(self._on_play_state) - self._token.invalidated.connect(self._on_play_invalidated) - self._token.error.connect(self._on_play_error) - self._token.play() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_pause(self, arg): - with qui_utils.notify_error(self._app.errorLog): - self._token.pause() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_resume(self, arg): - with qui_utils.notify_error(self._app.errorLog): - self._token.play() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_stop(self, arg): - with qui_utils.notify_error(self._app.errorLog): - self._token.stop() - - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_download(self, arg): - with qui_utils.notify_error(self._app.errorLog): - (cid, ) = self._session.draft.get_contacts() - messageId = self._session.draft.get_message_id(cid) - self._session.download_voicemail(messageId) - self._hide() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_recipients_changed(self): - with qui_utils.notify_error(self._app.errorLog): - self._update_state() - - @qt_compat.Slot(str, str) - @misc_utils.log_exception(_moduleLogger) - def _on_voicemail_downloaded(self, messageId, filepath): - with qui_utils.notify_error(self._app.errorLog): - self._update_state() - - -class SMSEntryWindow(qwrappers.WindowWrapper): - - MAX_CHAR = 160 - # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5 - - def __init__(self, parent, app, session, errorLog): - qwrappers.WindowWrapper.__init__(self, parent, app) - self._session = session - self._session.messagesUpdated.connect(self._on_refresh_history) - self._session.historyUpdated.connect(self._on_refresh_history) - self._session.draft.recipientsChanged.connect(self._on_recipients_changed) - - self._session.draft.sendingMessage.connect(self._on_op_started) - self._session.draft.calling.connect(self._on_op_started) - self._session.draft.calling.connect(self._on_calling_started) - self._session.draft.cancelling.connect(self._on_op_started) - - self._session.draft.sentMessage.connect(self._on_op_finished) - self._session.draft.called.connect(self._on_op_finished) - self._session.draft.cancelled.connect(self._on_op_finished) - self._session.draft.error.connect(self._on_op_error) - - self._errorLog = errorLog - - self._targetList = ContactList(self._app, self._session) - self._history = QtGui.QLabel() - self._history.setTextFormat(QtCore.Qt.RichText) - self._history.setWordWrap(True) - self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog) - self._smsEntry = QtGui.QTextEdit() - self._smsEntry.textChanged.connect(self._on_letter_count_changed) - - self._entryLayout = QtGui.QVBoxLayout() - self._entryLayout.addWidget(self._targetList.toplevel) - self._entryLayout.addWidget(self._history) - self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0) - self._entryLayout.addWidget(self._smsEntry) - self._entryLayout.setContentsMargins(0, 0, 0, 0) - self._entryWidget = QtGui.QWidget() - self._entryWidget.setLayout(self._entryLayout) - self._entryWidget.setContentsMargins(0, 0, 0, 0) - self._scrollEntry = QtGui.QScrollArea() - self._scrollEntry.setWidget(self._entryWidget) - self._scrollEntry.setWidgetResizable(True) - self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) - self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - - self._characterCountLabel = QtGui.QLabel("") - self._singleNumberSelector = QtGui.QComboBox() - self._cids = [] - self._singleNumberSelector.activated.connect(self._on_single_change_number) - self._smsButton = QtGui.QPushButton("SMS") - self._smsButton.clicked.connect(self._on_sms_clicked) - self._smsButton.setEnabled(False) - self._dialButton = QtGui.QPushButton("Dial") - self._dialButton.clicked.connect(self._on_call_clicked) - self._cancelButton = QtGui.QPushButton("Cancel Call") - self._cancelButton.clicked.connect(self._on_cancel_clicked) - self._cancelButton.setVisible(False) - - self._buttonLayout = QtGui.QHBoxLayout() - self._buttonLayout.addWidget(self._characterCountLabel) - self._buttonLayout.addStretch() - self._buttonLayout.addWidget(self._singleNumberSelector) - self._buttonLayout.addStretch() - self._buttonLayout.addWidget(self._smsButton) - self._buttonLayout.addWidget(self._dialButton) - self._buttonLayout.addWidget(self._cancelButton) - - self._layout.addWidget(self._errorDisplay.toplevel) - self._layout.addWidget(self._scrollEntry) - self._layout.addLayout(self._buttonLayout) - self._layout.setDirection(QtGui.QBoxLayout.TopToBottom) - - self._window.setWindowTitle("Contact") - self._window.closed.connect(self._on_close_window) - self._window.hidden.connect(self._on_close_window) - self._window.resized.connect(self._on_window_resized) - - self._scrollTimer = QtCore.QTimer() - self._scrollTimer.setInterval(100) - self._scrollTimer.setSingleShot(True) - self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom) - - self._smsEntry.setPlainText(self._session.draft.message) - self._update_letter_count() - self._update_target_fields() - self.set_fullscreen(self._app.fullscreenAction.isChecked()) - self.update_orientation(self._app.orientation) - - def close(self): - if self._window is None: - # Already closed - return - window = self._window - try: - message = unicode(self._smsEntry.toPlainText()) - self._session.draft.message = message - self.hide() - except AttributeError: - _moduleLogger.exception("Oh well") - except RuntimeError: - _moduleLogger.exception("Oh well") - - def destroy(self): - self._session.messagesUpdated.disconnect(self._on_refresh_history) - self._session.historyUpdated.disconnect(self._on_refresh_history) - self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed) - self._session.draft.sendingMessage.disconnect(self._on_op_started) - self._session.draft.calling.disconnect(self._on_op_started) - self._session.draft.calling.disconnect(self._on_calling_started) - self._session.draft.cancelling.disconnect(self._on_op_started) - self._session.draft.sentMessage.disconnect(self._on_op_finished) - self._session.draft.called.disconnect(self._on_op_finished) - self._session.draft.cancelled.disconnect(self._on_op_finished) - self._session.draft.error.disconnect(self._on_op_error) - self._voicemailPlayer.destroy() - window = self._window - self._window = None - try: - window.close() - window.destroy() - except AttributeError: - _moduleLogger.exception("Oh well") - except RuntimeError: - _moduleLogger.exception("Oh well") - - def update_orientation(self, orientation): - qwrappers.WindowWrapper.update_orientation(self, orientation) - self._scroll_to_bottom() - - def _update_letter_count(self): - count = len(self._smsEntry.toPlainText()) - numTexts, numCharInText = divmod(count, self.MAX_CHAR) - numTexts += 1 - numCharsLeftInText = self.MAX_CHAR - numCharInText - self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts)) - - def _update_button_state(self): - self._cancelButton.setEnabled(True) - if self._session.draft.get_num_contacts() == 0: - self._dialButton.setEnabled(False) - self._smsButton.setEnabled(False) - elif self._session.draft.get_num_contacts() == 1: - count = len(self._smsEntry.toPlainText()) - if count == 0: - self._dialButton.setEnabled(True) - self._smsButton.setEnabled(False) - else: - self._dialButton.setEnabled(False) - self._smsButton.setEnabled(True) - else: - self._dialButton.setEnabled(False) - count = len(self._smsEntry.toPlainText()) - if count == 0: - self._smsButton.setEnabled(False) - else: - self._smsButton.setEnabled(True) - - def _update_history(self, cid): - draftContactsCount = self._session.draft.get_num_contacts() - if draftContactsCount != 1: - self._history.setVisible(False) - else: - description = self._session.draft.get_description(cid) - - self._targetList.setVisible(False) - if description: - self._history.setText(description) - self._history.setVisible(True) - else: - self._history.setText("") - self._history.setVisible(False) - - def _update_target_fields(self): - draftContactsCount = self._session.draft.get_num_contacts() - if draftContactsCount == 0: - self.hide() - del self._cids[:] - elif draftContactsCount == 1: - (cid, ) = self._session.draft.get_contacts() - title = self._session.draft.get_title(cid) - numbers = self._session.draft.get_numbers(cid) - - self._targetList.setVisible(False) - self._update_history(cid) - self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers) - self._cids = [cid] - - self._scroll_to_bottom() - self._window.setWindowTitle(title) - self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason) - self.show() - self._window.raise_() - else: - self._targetList.setVisible(True) - self._targetList.update() - self._history.setText("") - self._history.setVisible(False) - self._singleNumberSelector.setVisible(False) - - self._scroll_to_bottom() - self._window.setWindowTitle("Contacts") - self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason) - self.show() - self._window.raise_() - - def _populate_number_selector(self, selector, cid, cidIndex, numbers): - selector.clear() - - selectedNumber = self._session.draft.get_selected_number(cid) - if len(numbers) == 1: - # If no alt numbers available, check the address book - numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1]) - else: - defaultIndex = _index_number(numbers, selectedNumber) - - for number, description in numbers: - if description: - label = "%s - %s" % (number, description) - else: - label = number - selector.addItem(label) - selector.setVisible(True) - if 1 < len(numbers): - selector.setEnabled(True) - selector.setCurrentIndex(defaultIndex) - else: - selector.setEnabled(False) - - def _scroll_to_bottom(self): - self._scrollTimer.start() - - @misc_utils.log_exception(_moduleLogger) - def _on_delayed_scroll_to_bottom(self): - with qui_utils.notify_error(self._app.errorLog): - self._scrollEntry.ensureWidgetVisible(self._smsEntry) - - @misc_utils.log_exception(_moduleLogger) - def _on_sms_clicked(self, arg): - with qui_utils.notify_error(self._app.errorLog): - message = unicode(self._smsEntry.toPlainText()) - self._session.draft.message = message - self._session.draft.send() - - @misc_utils.log_exception(_moduleLogger) - def _on_call_clicked(self, arg): - with qui_utils.notify_error(self._app.errorLog): - message = unicode(self._smsEntry.toPlainText()) - self._session.draft.message = message - self._session.draft.call() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_cancel_clicked(self, message): - with qui_utils.notify_error(self._app.errorLog): - self._session.draft.cancel() - - @misc_utils.log_exception(_moduleLogger) - def _on_single_change_number(self, index): - with qui_utils.notify_error(self._app.errorLog): - # Exception thrown when the first item is removed - cid = self._cids[0] - try: - numbers = self._session.draft.get_numbers(cid) - except KeyError: - _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index)) - return - number = numbers[index][0] - self._session.draft.set_selected_number(cid, number) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_refresh_history(self): - with qui_utils.notify_error(self._app.errorLog): - draftContactsCount = self._session.draft.get_num_contacts() - if draftContactsCount != 1: - # Changing contact count will automatically refresh it - return - (cid, ) = self._session.draft.get_contacts() - self._update_history(cid) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_recipients_changed(self): - with qui_utils.notify_error(self._app.errorLog): - self._update_target_fields() - self._update_button_state() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_op_started(self): - with qui_utils.notify_error(self._app.errorLog): - self._smsEntry.setReadOnly(True) - self._smsButton.setVisible(False) - self._dialButton.setVisible(False) - self.show() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_calling_started(self): - with qui_utils.notify_error(self._app.errorLog): - self._cancelButton.setVisible(True) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_op_finished(self): - with qui_utils.notify_error(self._app.errorLog): - self._smsEntry.setPlainText("") - self._smsEntry.setReadOnly(False) - self._cancelButton.setVisible(False) - self._smsButton.setVisible(True) - self._dialButton.setVisible(True) - self.close() - self.destroy() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_op_error(self, message): - with qui_utils.notify_error(self._app.errorLog): - self._smsEntry.setReadOnly(False) - self._cancelButton.setVisible(False) - self._smsButton.setVisible(True) - self._dialButton.setVisible(True) - - self._errorLog.push_error(message) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_letter_count_changed(self): - with qui_utils.notify_error(self._app.errorLog): - self._update_letter_count() - self._update_button_state() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_window_resized(self): - with qui_utils.notify_error(self._app.errorLog): - self._scroll_to_bottom() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc_utils.log_exception(_moduleLogger) - def _on_close_window(self, checked = True): - with qui_utils.notify_error(self._app.errorLog): - self.close() - - -def _index_number(numbers, default): - uglyDefault = misc_utils.make_ugly(default) - uglyContactNumbers = list( - misc_utils.make_ugly(contactNumber) - for (contactNumber, _) in numbers - ) - defaultMatches = [ - misc_utils.similar_ugly_numbers(uglyDefault, contactNumber) - for contactNumber in uglyContactNumbers - ] - try: - defaultIndex = defaultMatches.index(True) - except ValueError: - defaultIndex = -1 - _moduleLogger.warn( - "Could not find contact number %s among %r" % ( - default, numbers - ) - ) - return defaultIndex - - -def _get_contact_numbers(session, contactId, number, description): - contactPhoneNumbers = [] - if contactId and contactId != "0": - try: - contactDetails = copy.deepcopy(session.get_contacts()[contactId]) - contactPhoneNumbers = contactDetails["numbers"] - except KeyError: - contactPhoneNumbers = [] - contactPhoneNumbers = [ - (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown")) - for contactPhoneNumber in contactPhoneNumbers - ] - defaultIndex = _index_number(contactPhoneNumbers, number) - - if not contactPhoneNumbers or defaultIndex == -1: - contactPhoneNumbers += [(number, description)] - defaultIndex = 0 - - return contactPhoneNumbers, defaultIndex diff --git a/src/examples/log_notifier.py b/src/examples/log_notifier.py deleted file mode 100644 index 541ac18..0000000 --- a/src/examples/log_notifier.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement - -import sys -import datetime -import ConfigParser - - -sys.path.insert(0,"/usr/lib/dialcentral/") - - -import constants -import alarm_notify - - -def notify_on_change(): - with open(constants._notifier_logpath_, "a") as file: - file.write("Notification: %r\n" % (datetime.datetime.now(), )) - - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - backend = alarm_notify.create_backend(config) - notifyUser = alarm_notify.is_changed(config, backend) - - if notifyUser: - file.write("\tChange occurred\n") - - -if __name__ == "__main__": - notify_on_change() diff --git a/src/examples/sound_notifier.py b/src/examples/sound_notifier.py deleted file mode 100644 index c31e413..0000000 --- a/src/examples/sound_notifier.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import ConfigParser -import logging - - -sys.path.insert(0,"/usr/lib/dialcentral/") - - -import constants -import alarm_notify - - -def notify_on_change(): - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - backend = alarm_notify.create_backend(config) - notifyUser = alarm_notify.is_changed(config, backend) - - config = ConfigParser.SafeConfigParser() - config.read(constants._custom_notifier_settings_) - soundFile = config.get("Sound Notifier", "soundfile") - soundFile = "/usr/lib/gv-notifier/alert.mp3" - - if notifyUser: - import subprocess - import led_handler - logging.info("Changed, playing %s" % soundFile) - led = led_handler.LedHandler() - led.on() - soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True) - else: - logging.info("No Change") - - -if __name__ == "__main__": - logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_) - logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__)) - logging.info("OS: %s" % (os.uname()[0], )) - logging.info("Kernel: %s (%s) for %s" % os.uname()[2:]) - logging.info("Hostname: %s" % os.uname()[1]) - try: - notify_on_change() - except: - logging.exception("Error") - raise diff --git a/src/gv_views.py b/src/gv_views.py deleted file mode 100644 index 2bd0663..0000000 --- a/src/gv_views.py +++ /dev/null @@ -1,977 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import datetime -import string -import itertools -import logging - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -from util import qtpie -from util import qui_utils -from util import misc as misc_utils - -import backends.null_backend as null_backend -import backends.file_backend as file_backend -import backends.qt_backend as qt_backend - - -_moduleLogger = logging.getLogger(__name__) - - -_SENTINEL_ICON = QtGui.QIcon() - - -class Dialpad(object): - - 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, - ] - 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"), - ] - 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._smsButton.setEnabled(True) - self._callButton.setEnabled(True) - - def disable(self): - self._smsButton.setEnabled(False) - self._callButton.setEnabled(False) - - def get_settings(self): - return {} - - def set_settings(self, settings): - pass - - def clear(self): - pass - - def refresh(self, force = True): - pass - - 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() - - @qt_compat.Slot() - @qt_compat.Slot(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, None, title, description, numbersWithDescriptions) - - @qt_compat.Slot() - @qt_compat.Slot(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, None, 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) - - self._today = datetime.datetime(1900, 1, 1) - - self.prepare_for_update(self._today) - - 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: - section = self._REST_SECTION - self._timeItems[section].appendRow(row) - - def get_item(self, timeIndex, rowIndex, column): - timeItem = self._timeItems[timeIndex] - item = timeItem.child(rowIndex, column) - return item - - -class History(object): - - DETAILS_IDX = 0 - FROM_IDX = 1 - MAX_IDX = 2 - - HISTORY_RECEIVED = "Received" - HISTORY_MISSED = "Missed" - HISTORY_PLACED = "Placed" - HISTORY_ALL = "All" - - HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL] - HISTORY_COLUMNS = ["", "From"] - assert len(HISTORY_COLUMNS) == MAX_IDX - - 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._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed) - refreshIcon = qui_utils.get_theme_icon( - ("view-refresh", "general_refresh", "gtk-refresh", ), - _SENTINEL_ICON - ) - if refreshIcon is not _SENTINEL_ICON: - self._refreshButton = QtGui.QPushButton(refreshIcon, "") - else: - self._refreshButton = QtGui.QPushButton("Refresh") - 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.setIndentation(0) - 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._actionIcon = { - "Placed": self._app.get_icon("placed.png"), - "Missed": self._app.get_icon("missed.png"), - "Received": self._app.get_icon("received.png"), - } - - self._populate_items() - - @property - def toplevel(self): - return self._widget - - def enable(self): - self._itemView.setEnabled(True) - - def disable(self): - self._itemView.setEnabled(False) - - def get_settings(self): - return { - "filter": self._selectedFilter, - } - - 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._itemView.clear() - - def refresh(self, force=True): - self._itemView.setFocus(QtCore.Qt.OtherFocusReason) - - if self._selectedFilter == self.HISTORY_RECEIVED: - self._session.update_history(self._session.HISTORY_RECEIVED, force) - elif self._selectedFilter == self.HISTORY_MISSED: - self._session.update_history(self._session.HISTORY_MISSED, force) - elif self._selectedFilter == self.HISTORY_PLACED: - self._session.update_history(self._session.HISTORY_PLACED, force) - elif self._selectedFilter == self.HISTORY_ALL: - self._session.update_history(self._session.HISTORY_ALL, force) - else: - assert False, "How did we get here?" - - if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE: - 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 = event["relTime"] - action = event["action"] - number = event["number"] - prettyNumber = misc_utils.make_pretty(number) - if prettyNumber.startswith("+1 "): - prettyNumber = prettyNumber[len("+1 "):] - name = event["name"] - if not name or name == number: - name = event["location"] - if not name: - name = "Unknown" - - detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime)) - detailsFont = detailsItem.font() - detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 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[self.DETAILS_IDX].setData(event) - self._categoryManager.add_row(event["time"], row) - self._itemView.expandAll() - - @qt_compat.Slot(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() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_history_updated(self): - with qui_utils.notify_error(self._errorLog): - self._populate_items() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_refresh_clicked(self, arg = None): - with qui_utils.notify_error(self._errorLog): - self.refresh(force=True) - - @qt_compat.Slot(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() - - title = unicode(fromItem.text()) - number = str(contactDetails["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() - iNumber = str(iContactDetails["number"]) - if number != iNumber: - continue - relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"]) - action = str(iContactDetails["action"]) - number = str(iContactDetails["number"]) - prettyNumber = misc_utils.make_pretty(number) - rowItems = relTime, action, prettyNumber - descriptionRows.append("%s" % "".join(rowItems)) - description = "%s
" % "".join(descriptionRows) - numbersWithDescriptions = [(str(contactDetails["number"]), "")] - self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) - - -class Messages(object): - - NO_MESSAGES = "None" - VOICEMAIL_MESSAGES = "Voicemail" - TEXT_MESSAGES = "SMS" - ALL_TYPES = "All Messages" - MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES] - - UNREAD_STATUS = "Unread" - UNARCHIVED_STATUS = "Inbox" - ALL_STATUS = "Any" - MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS] - - _MIN_MESSAGES_SHOWN = 1 - - def __init__(self, app, session, errorLog): - self._selectedTypeFilter = self.ALL_TYPES - self._selectedStatusFilter = self.ALL_STATUS - self._app = app - self._session = session - self._session.messagesUpdated.connect(self._on_messages_updated) - self._errorLog = errorLog - - self._typeSelection = QtGui.QComboBox() - self._typeSelection.addItems(self.MESSAGE_TYPES) - self._typeSelection.setCurrentIndex( - self.MESSAGE_TYPES.index(self._selectedTypeFilter) - ) - self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed) - - self._statusSelection = QtGui.QComboBox() - self._statusSelection.addItems(self.MESSAGE_STATUSES) - self._statusSelection.setCurrentIndex( - self.MESSAGE_STATUSES.index(self._selectedStatusFilter) - ) - self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed) - - refreshIcon = qui_utils.get_theme_icon( - ("view-refresh", "general_refresh", "gtk-refresh", ), - _SENTINEL_ICON - ) - if refreshIcon is not _SENTINEL_ICON: - self._refreshButton = QtGui.QPushButton(refreshIcon, "") - else: - self._refreshButton = QtGui.QPushButton("Refresh") - self._refreshButton.clicked.connect(self._on_refresh_clicked) - self._refreshButton.setSizePolicy(QtGui.QSizePolicy( - QtGui.QSizePolicy.Minimum, - QtGui.QSizePolicy.Minimum, - QtGui.QSizePolicy.PushButton, - )) - - self._selectionLayout = QtGui.QHBoxLayout() - self._selectionLayout.addWidget(self._typeSelection, 1000) - self._selectionLayout.addWidget(self._statusSelection, 1000) - self._selectionLayout.addWidget(self._refreshButton, 0) - - self._itemStore = QtGui.QStandardItemModel() - self._itemStore.setHorizontalHeaderLabels(["Messages"]) - self._categoryManager = TimeCategories(self._itemStore) - - self._htmlDelegate = qui_utils.QHtmlDelegate() - self._itemView = QtGui.QTreeView() - self._itemView.setModel(self._itemStore) - self._itemView.setUniformRowHeights(False) - 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.setItemDelegate(self._htmlDelegate) - self._itemView.activated.connect(self._on_row_activated) - self._itemView.header().sectionResized.connect(self._on_column_resized) - - self._layout = QtGui.QVBoxLayout() - self._layout.addLayout(self._selectionLayout) - 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): - self._itemView.setEnabled(True) - - def disable(self): - self._itemView.setEnabled(False) - - def get_settings(self): - return { - "type": self._selectedTypeFilter, - "status": self._selectedStatusFilter, - } - - def set_settings(self, settings): - selectedType = settings.get("type", self.ALL_TYPES) - if selectedType in self.MESSAGE_TYPES: - self._selectedTypeFilter = selectedType - self._typeSelection.setCurrentIndex( - self.MESSAGE_TYPES.index(self._selectedTypeFilter) - ) - - selectedStatus = settings.get("status", self.ALL_STATUS) - if selectedStatus in self.MESSAGE_STATUSES: - self._selectedStatusFilter = selectedStatus - self._statusSelection.setCurrentIndex( - self.MESSAGE_STATUSES.index(self._selectedStatusFilter) - ) - - def clear(self): - self._itemView.clear() - - def refresh(self, force=True): - self._itemView.setFocus(QtCore.Qt.OtherFocusReason) - - if self._selectedTypeFilter == self.NO_MESSAGES: - pass - elif self._selectedTypeFilter == self.TEXT_MESSAGES: - self._session.update_messages(self._session.MESSAGE_TEXTS, force) - elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES: - self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force) - elif self._selectedTypeFilter == self.ALL_TYPES: - self._session.update_messages(self._session.MESSAGE_ALL, force) - else: - assert False, "How did we get here?" - - if (self._app.notifyOnSms or self._app.notifyOnVoicemail) and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE: - self._app.ledHandler.off() - - def _populate_items(self): - self._categoryManager.prepare_for_update(self._session.get_when_messages_updated()) - - rawMessages = self._session.get_messages() - rawMessages.sort(key=lambda item: item["time"], reverse=True) - for item in rawMessages: - isUnarchived = not item["isArchived"] - isUnread = not item["isRead"] - visibleStatus = { - self.UNREAD_STATUS: isUnarchived and isUnread, - self.UNARCHIVED_STATUS: isUnarchived, - self.ALL_STATUS: True, - }[self._selectedStatusFilter] - visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES] - if not (visibleType and visibleStatus): - continue - - relTime = misc_utils.abbrev_relative_date(item["relTime"]) - number = item["number"] - prettyNumber = misc_utils.make_pretty(number) - name = item["name"] - if not name or name == number: - name = item["location"] - if not name: - name = "Unknown" - - messageParts = list(item["messageParts"]) - if len(messageParts) == 0: - messages = ("No Transcription", ) - elif len(messageParts) == 1: - if messageParts[0][1]: - messages = (messageParts[0][1], ) - else: - messages = ("No Transcription", ) - else: - messages = [ - "%s: %s" % (messagePart[0], messagePart[1]) - for messagePart in messageParts - ] - - firstMessage = "%s
%s
(%s)" % (name, prettyNumber, relTime) - - expandedMessages = [firstMessage] - expandedMessages.extend(messages) - if self._MIN_MESSAGES_SHOWN < len(messages): - secondMessage = "%d Messages Hidden..." % (len(messages) - self._MIN_MESSAGES_SHOWN, ) - collapsedMessages = [firstMessage, secondMessage] - collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):]) - else: - collapsedMessages = expandedMessages - - item = dict(item.iteritems()) - item["collapsedMessages"] = "
\n".join(collapsedMessages) - item["expandedMessages"] = "
\n".join(expandedMessages) - - messageItem = QtGui.QStandardItem(item["collapsedMessages"]) - messageItem.setData(item) - messageItem.setEditable(False) - messageItem.setCheckable(False) - row = (messageItem, ) - self._categoryManager.add_row(item["time"], row) - self._itemView.expandAll() - - @qt_compat.Slot(str) - @misc_utils.log_exception(_moduleLogger) - def _on_type_filter_changed(self, newItem): - with qui_utils.notify_error(self._errorLog): - self._selectedTypeFilter = str(newItem) - self._populate_items() - - @qt_compat.Slot(str) - @misc_utils.log_exception(_moduleLogger) - def _on_status_filter_changed(self, newItem): - with qui_utils.notify_error(self._errorLog): - self._selectedStatusFilter = str(newItem) - self._populate_items() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_refresh_clicked(self, arg = None): - with qui_utils.notify_error(self._errorLog): - self.refresh(force=True) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_messages_updated(self): - with qui_utils.notify_error(self._errorLog): - self._populate_items() - - @qt_compat.Slot(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() - item = self._categoryManager.get_item(timeRow, row, 0) - contactDetails = item.data() - - name = unicode(contactDetails["name"]) - number = str(contactDetails["number"]) - if not name or name == number: - name = unicode(contactDetails["location"]) - if not name: - name = "Unknown" - - if str(contactDetails["type"]) == "Voicemail": - messageId = str(contactDetails["id"]) - else: - messageId = None - contactId = str(contactDetails["contactId"]) - title = name - description = unicode(contactDetails["expandedMessages"]) - numbersWithDescriptions = [(number, "")] - self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions) - - @qt_compat.Slot(QtCore.QModelIndex) - @misc_utils.log_exception(_moduleLogger) - def _on_column_resized(self, index, oldSize, newSize): - self._htmlDelegate.setWidth(newSize, self._itemStore) - - -class Contacts(object): - - # @todo Provide some sort of letter jump - - def __init__(self, app, session, errorLog): - self._app = app - self._session = session - self._session.accountUpdated.connect(self._on_contacts_updated) - self._errorLog = errorLog - self._addressBookFactories = [ - null_backend.NullAddressBookFactory(), - file_backend.FilesystemAddressBookFactory(app.fsContactsPath), - qt_backend.QtContactsAddressBookFactory(), - ] - self._addressBooks = [] - - self._listSelection = QtGui.QComboBox() - self._listSelection.addItems([]) - self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed) - self._activeList = "None" - refreshIcon = qui_utils.get_theme_icon( - ("view-refresh", "general_refresh", "gtk-refresh", ), - _SENTINEL_ICON - ) - if refreshIcon is not _SENTINEL_ICON: - self._refreshButton = QtGui.QPushButton(refreshIcon, "") - else: - self._refreshButton = QtGui.QPushButton("Refresh") - 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._listSelection, 1000) - self._managerLayout.addWidget(self._refreshButton, 0) - - self._itemStore = QtGui.QStandardItemModel() - self._itemStore.setHorizontalHeaderLabels(["Contacts"]) - self._alphaItem = {} - - 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.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.update_addressbooks() - self._populate_items() - - @property - def toplevel(self): - return self._widget - - def enable(self): - self._itemView.setEnabled(True) - - def disable(self): - self._itemView.setEnabled(False) - - def get_settings(self): - return { - "selectedAddressbook": self._activeList, - } - - def set_settings(self, settings): - currentItem = settings.get("selectedAddressbook", "None") - bookNames = [book["name"] for book in self._addressBooks] - try: - newIndex = bookNames.index(currentItem) - except ValueError: - # Switch over to None for the user - newIndex = 0 - self._listSelection.setCurrentIndex(newIndex) - self._activeList = currentItem - - def clear(self): - self._itemView.clear() - - def refresh(self, force=True): - self._itemView.setFocus(QtCore.Qt.OtherFocusReason) - self._backend.update_account(force) - - @property - def _backend(self): - return self._addressBooks[self._listSelection.currentIndex()]["book"] - - def update_addressbooks(self): - self._addressBooks = [ - {"book": book, "name": book.name} - for factory in self._addressBookFactories - for book in factory.get_addressbooks() - ] - self._addressBooks.append( - { - "book": self._session, - "name": "Google Voice", - } - ) - - currentItem = str(self._listSelection.currentText()) - self._activeList = currentItem - if currentItem == "": - # Not loaded yet - currentItem = "None" - self._listSelection.clear() - bookNames = [book["name"] for book in self._addressBooks] - try: - newIndex = bookNames.index(currentItem) - except ValueError: - # Switch over to None for the user - newIndex = 0 - self._itemStore.clear() - _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem) - self._listSelection.addItems(bookNames) - self._listSelection.setCurrentIndex(newIndex) - - def _populate_items(self): - self._itemStore.clear() - self._alphaItem = dict( - (letter, QtGui.QStandardItem(letter)) - for letter in self._prefixes() - ) - for letter in self._prefixes(): - item = self._alphaItem[letter] - item.setEditable(False) - item.setCheckable(False) - row = (item, ) - self._itemStore.appendRow(row) - - for item in self._get_contacts(): - name = item["name"] - if not name: - name = "Unknown" - numbers = item["numbers"] - - nameItem = QtGui.QStandardItem(name) - nameItem.setEditable(False) - nameItem.setCheckable(False) - nameItem.setData(item) - nameItemFont = nameItem.font() - nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5)) - nameItem.setFont(nameItemFont) - - row = (nameItem, ) - rowKey = name[0].upper() - rowKey = rowKey if rowKey in self._alphaItem else "#" - self._alphaItem[rowKey].appendRow(row) - self._itemView.expandAll() - - def _prefixes(self): - return itertools.chain(string.ascii_uppercase, ("#", )) - - def _jump_to_prefix(self, letter): - i = list(self._prefixes()).index(letter) - rootIndex = self._itemView.rootIndex() - currentIndex = self._itemView.model().index(i, 0, rootIndex) - self._itemView.scrollTo(currentIndex) - self._itemView.setItemSelected(self._itemView.topLevelItem(i), True) - - def _get_contacts(self): - contacts = list(self._backend.get_contacts().itervalues()) - contacts.sort(key=lambda contact: contact["name"].lower()) - return contacts - - @qt_compat.Slot(str) - @misc_utils.log_exception(_moduleLogger) - def _on_filter_changed(self, newItem): - with qui_utils.notify_error(self._errorLog): - self._activeList = str(newItem) - self.refresh(force=False) - self._populate_items() - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_refresh_clicked(self, arg = None): - with qui_utils.notify_error(self._errorLog): - self.refresh(force=True) - - @qt_compat.Slot() - @misc_utils.log_exception(_moduleLogger) - def _on_contacts_updated(self): - with qui_utils.notify_error(self._errorLog): - self._populate_items() - - @qt_compat.Slot(QtCore.QModelIndex) - @misc_utils.log_exception(_moduleLogger) - def _on_row_activated(self, index): - with qui_utils.notify_error(self._errorLog): - letterIndex = index.parent() - if not letterIndex.isValid(): - return - letterRow = letterIndex.row() - letter = list(self._prefixes())[letterRow] - letterItem = self._alphaItem[letter] - rowIndex = index.row() - item = letterItem.child(rowIndex, 0) - contactDetails = item.data() - - name = unicode(contactDetails["name"]) - if not name: - name = unicode(contactDetails["location"]) - if not name: - name = "Unknown" - - contactId = str(contactDetails["contactId"]) - numbers = contactDetails["numbers"] - numbers = [ - dict( - (str(k), str(v)) - for (k, v) in number.iteritems() - ) - for number in numbers - ] - numbersWithDescriptions = [ - ( - number["phoneNumber"], - self._choose_phonetype(number), - ) - for number in numbers - ] - title = name - description = name - self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions) - - @staticmethod - def _choose_phonetype(numberDetails): - if "phoneTypeName" in numberDetails: - return numberDetails["phoneTypeName"] - elif "phoneType" in numberDetails: - return numberDetails["phoneType"] - else: - return "" diff --git a/src/led_handler.py b/src/led_handler.py deleted file mode 100755 index 0914105..0000000 --- a/src/led_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python - -import dbus - - -class _NokiaLedHandler(object): - - def __init__(self): - self._bus = dbus.SystemBus() - self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request") - self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request") - - self._ledPattern = "PatternCommunicationChat" - - def on(self): - self._mceRequest.req_led_pattern_activate(self._ledPattern) - - def off(self): - self._mceRequest.req_led_pattern_deactivate(self._ledPattern) - - -class _NoLedHandler(object): - - def __init__(self): - pass - - def on(self): - pass - - def off(self): - pass - - -class LedHandler(object): - - def __init__(self): - self._actual = None - self._isReal = False - - def on(self): - self._lazy_init() - self._actual.on() - - def off(self): - self._lazy_init() - self._actual.off() - - @property - def isReal(self): - self._lazy_init() - self._isReal - - def _lazy_init(self): - if self._actual is not None: - return - try: - self._actual = _NokiaLedHandler() - self._isReal = True - except dbus.DBusException: - self._actual = _NoLedHandler() - self._isReal = False - - -if __name__ == "__main__": - leds = _NokiaLedHandler() - leds.off() diff --git a/src/session.py b/src/session.py deleted file mode 100644 index dbdc3e4..0000000 --- a/src/session.py +++ /dev/null @@ -1,830 +0,0 @@ -from __future__ import with_statement - -import os -import time -import datetime -import contextlib -import logging - -try: - import cPickle - pickle = cPickle -except ImportError: - import pickle - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore - -from util import qore_utils -from util import qui_utils -from util import concurrent -from util import misc as misc_utils - -import constants - - -_moduleLogger = logging.getLogger(__name__) - - -class _DraftContact(object): - - def __init__(self, messageId, title, description, numbersWithDescriptions): - self.messageId = messageId - self.title = title - self.description = description - self.numbers = numbersWithDescriptions - self.selectedNumber = numbersWithDescriptions[0][0] - - -class Draft(QtCore.QObject): - - sendingMessage = qt_compat.Signal() - sentMessage = qt_compat.Signal() - calling = qt_compat.Signal() - called = qt_compat.Signal() - cancelling = qt_compat.Signal() - cancelled = qt_compat.Signal() - error = qt_compat.Signal(str) - - recipientsChanged = qt_compat.Signal() - - def __init__(self, asyncQueue, backend, errorLog): - QtCore.QObject.__init__(self) - self._errorLog = errorLog - self._contacts = {} - self._asyncQueue = asyncQueue - self._backend = backend - self._busyReason = None - self._message = "" - - def send(self): - assert 0 < len(self._contacts), "No contacts selected" - assert 0 < len(self._message), "No message to send" - numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()] - le = self._asyncQueue.add_async(self._send) - le.start(numbers, self._message) - - def call(self): - assert len(self._contacts) == 1, "Must select 1 and only 1 contact" - assert len(self._message) == 0, "Cannot send message with call" - (contact, ) = self._contacts.itervalues() - number = misc_utils.make_ugly(contact.selectedNumber) - le = self._asyncQueue.add_async(self._call) - le.start(number) - - def cancel(self): - le = self._asyncQueue.add_async(self._cancel) - le.start() - - def _get_message(self): - return self._message - - def _set_message(self, message): - self._message = message - - message = property(_get_message, _set_message) - - def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions): - if self._busyReason is not None: - raise RuntimeError("Please wait for %r" % self._busyReason) - # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up - contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions) - self._contacts[contactId] = contactDetails - self.recipientsChanged.emit() - - def remove_contact(self, contactId): - if self._busyReason is not None: - raise RuntimeError("Please wait for %r" % self._busyReason) - assert contactId in self._contacts, "Contact missing" - del self._contacts[contactId] - self.recipientsChanged.emit() - - def get_contacts(self): - return self._contacts.iterkeys() - - def get_num_contacts(self): - return len(self._contacts) - - def get_message_id(self, cid): - return self._contacts[cid].messageId - - def get_title(self, cid): - return self._contacts[cid].title - - def get_description(self, cid): - return self._contacts[cid].description - - def get_numbers(self, cid): - return self._contacts[cid].numbers - - def get_selected_number(self, cid): - return self._contacts[cid].selectedNumber - - def set_selected_number(self, cid, number): - # @note I'm lazy, this isn't firing any kind of signal since only one - # controller right now and that is the viewer - assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable" - self._contacts[cid].selectedNumber = number - - def clear(self): - if self._busyReason is not None: - raise RuntimeError("Please wait for %r" % self._busyReason) - self._clear() - - def _clear(self): - oldContacts = self._contacts - self._contacts = {} - self._message = "" - if oldContacts: - self.recipientsChanged.emit() - - @contextlib.contextmanager - def _busy(self, message): - if self._busyReason is not None: - raise RuntimeError("Already busy doing %r" % self._busyReason) - try: - self._busyReason = message - yield - finally: - self._busyReason = None - - def _send(self, numbers, text): - self.sendingMessage.emit() - try: - with self._busy("Sending Text"): - with qui_utils.notify_busy(self._errorLog, "Sending Text"): - yield ( - self._backend[0].send_sms, - (numbers, text), - {}, - ) - self.sentMessage.emit() - self._clear() - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - - def _call(self, number): - self.calling.emit() - try: - with self._busy("Calling"): - with qui_utils.notify_busy(self._errorLog, "Calling"): - yield ( - self._backend[0].call, - (number, ), - {}, - ) - self.called.emit() - self._clear() - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - - def _cancel(self): - self.cancelling.emit() - try: - with qui_utils.notify_busy(self._errorLog, "Cancelling"): - yield ( - self._backend[0].cancel, - (), - {}, - ) - self.cancelled.emit() - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - - -class Session(QtCore.QObject): - - # @todo Somehow add support for csv contacts - # @BUG When loading without caches, downloads messages twice - - stateChange = qt_compat.Signal(str) - loggedOut = qt_compat.Signal() - loggedIn = qt_compat.Signal() - callbackNumberChanged = qt_compat.Signal(str) - - accountUpdated = qt_compat.Signal() - messagesUpdated = qt_compat.Signal() - newMessages = qt_compat.Signal() - historyUpdated = qt_compat.Signal() - dndStateChange = qt_compat.Signal(bool) - voicemailAvailable = qt_compat.Signal(str, str) - - error = qt_compat.Signal(str) - - LOGGEDOUT_STATE = "logged out" - LOGGINGIN_STATE = "logging in" - LOGGEDIN_STATE = "logged in" - - MESSAGE_TEXTS = "Text" - MESSAGE_VOICEMAILS = "Voicemail" - MESSAGE_ALL = "All" - - HISTORY_RECEIVED = "Received" - HISTORY_MISSED = "Missed" - HISTORY_PLACED = "Placed" - HISTORY_ALL = "All" - - _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0") - - _LOGGEDOUT_TIME = -1 - _LOGGINGIN_TIME = 0 - - def __init__(self, errorLog, cachePath): - QtCore.QObject.__init__(self) - self._errorLog = errorLog - self._pool = qore_utils.FutureThread() - self._asyncQueue = concurrent.AsyncTaskQueue(self._pool) - self._backend = [] - self._loggedInTime = self._LOGGEDOUT_TIME - self._loginOps = [] - self._cachePath = cachePath - self._voicemailCachePath = None - self._username = None - self._password = None - self._draft = Draft(self._asyncQueue, self._backend, self._errorLog) - self._delayedRelogin = QtCore.QTimer() - self._delayedRelogin.setInterval(0) - self._delayedRelogin.setSingleShot(True) - self._delayedRelogin.timeout.connect(self._on_delayed_relogin) - - self._contacts = {} - self._accountUpdateTime = datetime.datetime(1971, 1, 1) - self._messages = [] - self._cleanMessages = [] - self._messageUpdateTime = datetime.datetime(1971, 1, 1) - self._history = [] - self._historyUpdateTime = datetime.datetime(1971, 1, 1) - self._dnd = False - self._callback = "" - - @property - def state(self): - return { - self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE, - self._LOGGINGIN_TIME: self.LOGGINGIN_STATE, - }.get(self._loggedInTime, self.LOGGEDIN_STATE) - - @property - def draft(self): - return self._draft - - def login(self, username, password): - assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state - assert username != "", "No username specified" - if self._cachePath is not None: - cookiePath = os.path.join(self._cachePath, "%s.cookies" % username) - else: - cookiePath = None - - if self._username != username or not self._backend: - from backends import gv_backend - del self._backend[:] - self._backend[0:0] = [gv_backend.GVDialer(cookiePath)] - - self._pool.start() - le = self._asyncQueue.add_async(self._login) - le.start(username, password) - - def logout(self): - assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state - _moduleLogger.info("Logging out") - self._pool.stop() - self._loggedInTime = self._LOGGEDOUT_TIME - self._backend[0].persist() - self._save_to_cache() - self._clear_voicemail_cache() - self.stateChange.emit(self.LOGGEDOUT_STATE) - self.loggedOut.emit() - - def clear(self): - assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state - self._backend[0].logout() - del self._backend[0] - self._clear_cache() - self._draft.clear() - - def logout_and_clear(self): - assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state - _moduleLogger.info("Logging out and clearing the account") - self._pool.stop() - self._loggedInTime = self._LOGGEDOUT_TIME - self.clear() - self.stateChange.emit(self.LOGGEDOUT_STATE) - self.loggedOut.emit() - - def update_account(self, force = True): - if not force and self._contacts: - return - le = self._asyncQueue.add_async(self._update_account), (), {} - self._perform_op_while_loggedin(le) - - def refresh_connection(self): - le = self._asyncQueue.add_async(self._refresh_authentication) - le.start() - - def get_contacts(self): - return self._contacts - - def get_when_contacts_updated(self): - return self._accountUpdateTime - - def update_messages(self, messageType, force = True): - if not force and self._messages: - return - le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {} - self._perform_op_while_loggedin(le) - - def get_messages(self): - return self._messages - - def get_when_messages_updated(self): - return self._messageUpdateTime - - def update_history(self, historyType, force = True): - if not force and self._history: - return - le = self._asyncQueue.add_async(self._update_history), (historyType, ), {} - self._perform_op_while_loggedin(le) - - def get_history(self): - return self._history - - def get_when_history_updated(self): - return self._historyUpdateTime - - def update_dnd(self): - le = self._asyncQueue.add_async(self._update_dnd), (), {} - self._perform_op_while_loggedin(le) - - def set_dnd(self, dnd): - le = self._asyncQueue.add_async(self._set_dnd) - le.start(dnd) - - def is_available(self, messageId): - actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) - return os.path.exists(actualPath) - - def voicemail_path(self, messageId): - actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) - if not os.path.exists(actualPath): - raise RuntimeError("Voicemail not available") - return actualPath - - def download_voicemail(self, messageId): - le = self._asyncQueue.add_async(self._download_voicemail) - le.start(messageId) - - def _set_dnd(self, dnd): - oldDnd = self._dnd - try: - assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state - with qui_utils.notify_busy(self._errorLog, "Setting DND Status"): - yield ( - self._backend[0].set_dnd, - (dnd, ), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - self._dnd = dnd - if oldDnd != self._dnd: - self.dndStateChange.emit(self._dnd) - - def get_dnd(self): - return self._dnd - - def get_account_number(self): - if self.state != self.LOGGEDIN_STATE: - return "" - return self._backend[0].get_account_number() - - def get_callback_numbers(self): - if self.state != self.LOGGEDIN_STATE: - return {} - return self._backend[0].get_callback_numbers() - - def get_callback_number(self): - return self._callback - - def set_callback_number(self, callback): - le = self._asyncQueue.add_async(self._set_callback_number) - le.start(callback) - - def _set_callback_number(self, callback): - oldCallback = self._callback - try: - assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state - yield ( - self._backend[0].set_callback_number, - (callback, ), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - self._callback = callback - if oldCallback != self._callback: - self.callbackNumberChanged.emit(self._callback) - - def _login(self, username, password): - with qui_utils.notify_busy(self._errorLog, "Logging In"): - self._loggedInTime = self._LOGGINGIN_TIME - self.stateChange.emit(self.LOGGINGIN_STATE) - finalState = self.LOGGEDOUT_STATE - accountData = None - try: - if accountData is None and self._backend[0].is_quick_login_possible(): - accountData = yield ( - self._backend[0].refresh_account_info, - (), - {}, - ) - if accountData is not None: - _moduleLogger.info("Logged in through cookies") - else: - # Force a clearing of the cookies - yield ( - self._backend[0].logout, - (), - {}, - ) - - if accountData is None: - accountData = yield ( - self._backend[0].login, - (username, password), - {}, - ) - if accountData is not None: - _moduleLogger.info("Logged in through credentials") - - if accountData is not None: - self._loggedInTime = int(time.time()) - oldUsername = self._username - self._username = username - self._password = password - finalState = self.LOGGEDIN_STATE - if oldUsername != self._username: - needOps = not self._load() - else: - needOps = True - - self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username) - try: - os.makedirs(self._voicemailCachePath) - except OSError, e: - if e.errno != 17: - raise - - self.loggedIn.emit() - self.stateChange.emit(finalState) - finalState = None # Mark it as already set - self._process_account_data(accountData) - - if needOps: - loginOps = self._loginOps[:] - else: - loginOps = [] - del self._loginOps[:] - for asyncOp, args, kwds in loginOps: - asyncOp.start(*args, **kwds) - else: - self._loggedInTime = self._LOGGEDOUT_TIME - self.error.emit("Error logging in") - except Exception, e: - _moduleLogger.exception("Booh") - self._loggedInTime = self._LOGGEDOUT_TIME - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - finally: - if finalState is not None: - self.stateChange.emit(finalState) - if accountData is not None and self._callback: - self.set_callback_number(self._callback) - - def _update_account(self): - try: - with qui_utils.notify_busy(self._errorLog, "Updating Account"): - accountData = yield ( - self._backend[0].refresh_account_info, - (), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - self._loggedInTime = int(time.time()) - self._process_account_data(accountData) - - def _refresh_authentication(self): - try: - with qui_utils.notify_busy(self._errorLog, "Updating Account"): - accountData = yield ( - self._backend[0].refresh_account_info, - (), - {}, - ) - accountData = None - except Exception, e: - _moduleLogger.exception("Passing to user") - self.error.emit(str(e)) - # refresh_account_info does not normally throw, so it is fine if we - # just quit early because something seriously wrong is going on - return - - if accountData is not None: - self._loggedInTime = int(time.time()) - self._process_account_data(accountData) - else: - self._delayedRelogin.start() - - def _load(self): - updateMessages = len(self._messages) != 0 - updateHistory = len(self._history) != 0 - oldDnd = self._dnd - oldCallback = self._callback - - self._messages = [] - self._cleanMessages = [] - self._history = [] - self._dnd = False - self._callback = "" - - loadedFromCache = self._load_from_cache() - if loadedFromCache: - updateMessages = True - updateHistory = True - - if updateMessages: - self.messagesUpdated.emit() - if updateHistory: - self.historyUpdated.emit() - if oldDnd != self._dnd: - self.dndStateChange.emit(self._dnd) - if oldCallback != self._callback: - self.callbackNumberChanged.emit(self._callback) - - return loadedFromCache - - def _load_from_cache(self): - if self._cachePath is None: - return False - cachePath = os.path.join(self._cachePath, "%s.cache" % self._username) - - try: - with open(cachePath, "rb") as f: - dumpedData = pickle.load(f) - except (pickle.PickleError, IOError, EOFError, ValueError, ImportError): - _moduleLogger.exception("Pickle fun loading") - return False - except: - _moduleLogger.exception("Weirdness loading") - return False - - try: - version, build = dumpedData[0:2] - except ValueError: - _moduleLogger.exception("Upgrade/downgrade fun") - return False - except: - _moduleLogger.exception("Weirdlings") - return False - - if misc_utils.compare_versions( - self._OLDEST_COMPATIBLE_FORMAT_VERSION, - misc_utils.parse_version(version), - ) <= 0: - try: - ( - version, build, - messages, messageUpdateTime, - history, historyUpdateTime, - dnd, callback - ) = dumpedData - except ValueError: - _moduleLogger.exception("Upgrade/downgrade fun") - return False - except: - _moduleLogger.exception("Weirdlings") - return False - - _moduleLogger.info("Loaded cache") - self._messages = messages - self._alert_on_messages(self._messages) - self._messageUpdateTime = messageUpdateTime - self._history = history - self._historyUpdateTime = historyUpdateTime - self._dnd = dnd - self._callback = callback - return True - else: - _moduleLogger.debug( - "Skipping cache due to version mismatch (%s-%s)" % ( - version, build - ) - ) - return False - - def _save_to_cache(self): - _moduleLogger.info("Saving cache") - if self._cachePath is None: - return - cachePath = os.path.join(self._cachePath, "%s.cache" % self._username) - - try: - dataToDump = ( - constants.__version__, constants.__build__, - self._messages, self._messageUpdateTime, - self._history, self._historyUpdateTime, - self._dnd, self._callback - ) - with open(cachePath, "wb") as f: - pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL) - _moduleLogger.info("Cache saved") - except (pickle.PickleError, IOError): - _moduleLogger.exception("While saving") - - def _clear_cache(self): - updateMessages = len(self._messages) != 0 - updateHistory = len(self._history) != 0 - oldDnd = self._dnd - oldCallback = self._callback - - self._messages = [] - self._messageUpdateTime = datetime.datetime(1971, 1, 1) - self._history = [] - self._historyUpdateTime = datetime.datetime(1971, 1, 1) - self._dnd = False - self._callback = "" - - if updateMessages: - self.messagesUpdated.emit() - if updateHistory: - self.historyUpdated.emit() - if oldDnd != self._dnd: - self.dndStateChange.emit(self._dnd) - if oldCallback != self._callback: - self.callbackNumberChanged.emit(self._callback) - - self._save_to_cache() - self._clear_voicemail_cache() - - def _clear_voicemail_cache(self): - import shutil - shutil.rmtree(self._voicemailCachePath, True) - - def _update_messages(self, messageType): - try: - assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state - with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType): - self._messages = yield ( - self._backend[0].get_messages, - (messageType, ), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - self._messageUpdateTime = datetime.datetime.now() - self.messagesUpdated.emit() - self._alert_on_messages(self._messages) - - def _update_history(self, historyType): - try: - assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state - with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType): - self._history = yield ( - self._backend[0].get_call_history, - (historyType, ), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - self._historyUpdateTime = datetime.datetime.now() - self.historyUpdated.emit() - - def _update_dnd(self): - with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"): - oldDnd = self._dnd - try: - assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state - self._dnd = yield ( - self._backend[0].is_dnd, - (), - {}, - ) - except Exception, e: - _moduleLogger.exception("Reporting error to user") - self.error.emit(str(e)) - return - if oldDnd != self._dnd: - self.dndStateChange(self._dnd) - - def _download_voicemail(self, messageId): - actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) - targetPath = "%s.%s.part" % (actualPath, time.time()) - if os.path.exists(actualPath): - self.voicemailAvailable.emit(messageId, actualPath) - return - with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"): - try: - yield ( - self._backend[0].download, - (messageId, targetPath), - {}, - ) - except Exception, e: - _moduleLogger.exception("Passing to user") - self.error.emit(str(e)) - return - - if os.path.exists(actualPath): - try: - os.remove(targetPath) - except: - _moduleLogger.exception("Ignoring file problems with cache") - self.voicemailAvailable.emit(messageId, actualPath) - return - else: - os.rename(targetPath, actualPath) - self.voicemailAvailable.emit(messageId, actualPath) - - def _perform_op_while_loggedin(self, op): - if self.state == self.LOGGEDIN_STATE: - op, args, kwds = op - op.start(*args, **kwds) - else: - self._push_login_op(op) - - def _push_login_op(self, asyncOp): - assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out" - if asyncOp in self._loginOps: - _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp) - return - self._loginOps.append(asyncOp) - - def _process_account_data(self, accountData): - self._contacts = dict( - (contactId, contactDetails) - for contactId, contactDetails in accountData["contacts"].iteritems() - # A zero contact id is the catch all for unknown contacts - if contactId != "0" - ) - - self._accountUpdateTime = datetime.datetime.now() - self.accountUpdated.emit() - - def _alert_on_messages(self, messages): - cleanNewMessages = list(self._clean_messages(messages)) - cleanNewMessages.sort(key=lambda m: m["contactId"]) - if self._cleanMessages: - if self._cleanMessages != cleanNewMessages: - self.newMessages.emit() - self._cleanMessages = cleanNewMessages - - def _clean_messages(self, messages): - for message in messages: - cleaned = dict( - kv - for kv in message.iteritems() - if kv[0] not in - [ - "relTime", - "time", - "isArchived", - "isRead", - "isSpam", - "isTrash", - ] - ) - - # Don't let outbound messages cause alerts, especially if the package has only outbound - cleaned["messageParts"] = [ - tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:" - ] - if not cleaned["messageParts"]: - continue - - yield cleaned - - @misc_utils.log_exception(_moduleLogger) - def _on_delayed_relogin(self): - try: - username = self._username - password = self._password - self.logout() - self.login(username, password) - except Exception, e: - _moduleLogger.exception("Passing to user") - self.error.emit(str(e)) - return diff --git a/src/stream_gst.py b/src/stream_gst.py deleted file mode 100644 index ce97fb6..0000000 --- a/src/stream_gst.py +++ /dev/null @@ -1,145 +0,0 @@ -import logging - -import gobject -import gst - -import util.misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class Stream(gobject.GObject): - - # @bug Advertising state changes a bit early, should watch for GStreamer state change - - STATE_PLAY = "play" - STATE_PAUSE = "pause" - STATE_STOP = "stop" - - __gsignals__ = { - 'state-change' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING, ), - ), - 'eof' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING, ), - ), - 'error' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), - ), - } - - def __init__(self): - gobject.GObject.__init__(self) - #Fields - self._uri = "" - self._elapsed = 0 - self._duration = 0 - - #Set up GStreamer - self._player = gst.element_factory_make("playbin2", "player") - bus = self._player.get_bus() - bus.add_signal_watch() - bus.connect("message", self._on_message) - - #Constants - self._timeFormat = gst.Format(gst.FORMAT_TIME) - self._seekFlag = gst.SEEK_FLAG_FLUSH - - @property - def playing(self): - return self.state == self.STATE_PLAY - - @property - def has_file(self): - return 0 < len(self._uri) - - @property - def state(self): - state = self._player.get_state()[1] - return self._translate_state(state) - - def set_file(self, uri): - if self._uri != uri: - self._invalidate_cache() - if self.state != self.STATE_STOP: - self.stop() - - self._uri = uri - self._player.set_property("uri", uri) - - def play(self): - if self.state == self.STATE_PLAY: - _moduleLogger.info("Already play") - return - _moduleLogger.info("Play") - self._player.set_state(gst.STATE_PLAYING) - self.emit("state-change", self.STATE_PLAY) - - def pause(self): - if self.state == self.STATE_PAUSE: - _moduleLogger.info("Already pause") - return - _moduleLogger.info("Pause") - self._player.set_state(gst.STATE_PAUSED) - self.emit("state-change", self.STATE_PAUSE) - - def stop(self): - if self.state == self.STATE_STOP: - _moduleLogger.info("Already stop") - return - self._player.set_state(gst.STATE_NULL) - _moduleLogger.info("Stopped") - self.emit("state-change", self.STATE_STOP) - - @property - def elapsed(self): - try: - self._elapsed = self._player.query_position(self._timeFormat, None)[0] - except: - pass - return self._elapsed - - @property - def duration(self): - try: - self._duration = self._player.query_duration(self._timeFormat, None)[0] - except: - _moduleLogger.exception("Query failed") - return self._duration - - def seek_time(self, ns): - self._elapsed = ns - self._player.seek_simple(self._timeFormat, self._seekFlag, ns) - - def _invalidate_cache(self): - self._elapsed = 0 - self._duration = 0 - - def _translate_state(self, gstState): - return { - gst.STATE_NULL: self.STATE_STOP, - gst.STATE_PAUSED: self.STATE_PAUSE, - gst.STATE_PLAYING: self.STATE_PLAY, - }.get(gstState, self.STATE_STOP) - - @misc_utils.log_exception(_moduleLogger) - def _on_message(self, bus, message): - t = message.type - if t == gst.MESSAGE_EOS: - self._player.set_state(gst.STATE_NULL) - self.emit("eof", self._uri) - elif t == gst.MESSAGE_ERROR: - self._player.set_state(gst.STATE_NULL) - err, debug = message.parse_error() - _moduleLogger.error("Error: %s, (%s)" % (err, debug)) - self.emit("error", err, debug) - - -gobject.type_register(Stream) diff --git a/src/stream_handler.py b/src/stream_handler.py deleted file mode 100644 index 3c0c9e3..0000000 --- a/src/stream_handler.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import logging - -import util.qt_compat as qt_compat -QtCore = qt_compat.QtCore - -import util.misc as misc_utils -try: - import stream_gst - stream = stream_gst -except ImportError: - try: - import stream_osso - stream = stream_osso - except ImportError: - import stream_null - stream = stream_null - - -_moduleLogger = logging.getLogger(__name__) - - -class StreamToken(QtCore.QObject): - - stateChange = qt_compat.Signal(str) - invalidated = qt_compat.Signal() - error = qt_compat.Signal(str) - - STATE_PLAY = stream.Stream.STATE_PLAY - STATE_PAUSE = stream.Stream.STATE_PAUSE - STATE_STOP = stream.Stream.STATE_STOP - - def __init__(self, stream): - QtCore.QObject.__init__(self) - self._stream = stream - self._stream.connect("state-change", self._on_stream_state) - self._stream.connect("eof", self._on_stream_eof) - self._stream.connect("error", self._on_stream_error) - - @property - def state(self): - if self.isValid: - return self._stream.state - else: - return self.STATE_STOP - - @property - def isValid(self): - return self._stream is not None - - def play(self): - self._stream.play() - - def pause(self): - self._stream.pause() - - def stop(self): - self._stream.stop() - - def invalidate(self): - if self._stream is None: - return - _moduleLogger.info("Playback token invalidated") - self._stream = None - - @misc_utils.log_exception(_moduleLogger) - def _on_stream_state(self, s, state): - if not self.isValid: - return - if state == self.STATE_STOP: - self.invalidate() - self.stateChange.emit(state) - - @misc_utils.log_exception(_moduleLogger) - def _on_stream_eof(self, s, uri): - if not self.isValid: - return - self.invalidate() - self.stateChange.emit(self.STATE_STOP) - - @misc_utils.log_exception(_moduleLogger) - def _on_stream_error(self, s, error, debug): - if not self.isValid: - return - _moduleLogger.info("Error %s %s" % (error, debug)) - self.error.emit(str(error)) - - -class StreamHandler(QtCore.QObject): - - def __init__(self): - QtCore.QObject.__init__(self) - self._stream = stream.Stream() - self._token = StreamToken(self._stream) - - def set_file(self, path): - self._token.invalidate() - self._token = StreamToken(self._stream) - self._stream.set_file(path) - return self._token - - @misc_utils.log_exception(_moduleLogger) - def _on_stream_state(self, s, state): - _moduleLogger.info("State change %r" % state) - - -if __name__ == "__main__": - pass - diff --git a/src/stream_null.py b/src/stream_null.py deleted file mode 100644 index 44fbbed..0000000 --- a/src/stream_null.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import logging - - -_moduleLogger = logging.getLogger(__name__) - - -class Stream(object): - - STATE_PLAY = "play" - STATE_PAUSE = "pause" - STATE_STOP = "stop" - - def __init__(self): - pass - - def connect(self, signalName, slot): - pass - - @property - def playing(self): - return False - - @property - def has_file(self): - return False - - @property - def state(self): - return self.STATE_STOP - - def set_file(self, uri): - pass - - def play(self): - pass - - def pause(self): - pass - - def stop(self): - pass - - @property - def elapsed(self): - return 0 - - @property - def duration(self): - return 0 - - def seek_time(self, ns): - pass - - -if __name__ == "__main__": - pass - diff --git a/src/stream_osso.py b/src/stream_osso.py deleted file mode 100644 index abc453f..0000000 --- a/src/stream_osso.py +++ /dev/null @@ -1,181 +0,0 @@ -import logging - -import gobject -import dbus - -import util.misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class Stream(gobject.GObject): - - STATE_PLAY = "play" - STATE_PAUSE = "pause" - STATE_STOP = "stop" - - __gsignals__ = { - 'state-change' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING, ), - ), - 'eof' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING, ), - ), - 'error' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), - ), - } - - _SERVICE_NAME = "com.nokia.osso_media_server" - _OBJECT_PATH = "/com/nokia/osso_media_server" - _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music" - - def __init__(self): - gobject.GObject.__init__(self) - #Fields - self._state = self.STATE_STOP - self._nextState = self.STATE_STOP - self._uri = "" - self._elapsed = 0 - self._duration = 0 - - session_bus = dbus.SessionBus() - - # Get the osso-media-player proxy object - oms_object = session_bus.get_object( - self._SERVICE_NAME, - self._OBJECT_PATH, - introspect=False, - follow_name_owner_changes=True, - ) - # Use the audio interface - oms_audio_interface = dbus.Interface( - oms_object, - self._AUDIO_INTERFACE_NAME, - ) - self._audioProxy = oms_audio_interface - - self._audioProxy.connect_to_signal("state_changed", self._on_state_changed) - self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream) - - error_signals = [ - "no_media_selected", - "file_not_found", - "type_not_found", - "unsupported_type", - "gstreamer", - "dsp", - "device_unavailable", - "corrupted_file", - "out_of_memory", - "audio_codec_not_supported", - ] - for error in error_signals: - self._audioProxy.connect_to_signal(error, self._on_error) - - @property - def playing(self): - return self.state == self.STATE_PLAY - - @property - def has_file(self): - return 0 < len(self._uri) - - @property - def state(self): - return self._state - - def set_file(self, uri): - if self._uri != uri: - self._invalidate_cache() - if self.state != self.STATE_STOP: - self.stop() - - self._uri = uri - self._audioProxy.set_media_location(self._uri) - - def play(self): - if self._nextState == self.STATE_PLAY: - _moduleLogger.info("Already play") - return - _moduleLogger.info("Play") - self._audioProxy.play() - self._nextState = self.STATE_PLAY - #self.emit("state-change", self.STATE_PLAY) - - def pause(self): - if self._nextState == self.STATE_PAUSE: - _moduleLogger.info("Already pause") - return - _moduleLogger.info("Pause") - self._audioProxy.pause() - self._nextState = self.STATE_PAUSE - #self.emit("state-change", self.STATE_PLAY) - - def stop(self): - if self._nextState == self.STATE_STOP: - _moduleLogger.info("Already stop") - return - self._audioProxy.stop() - _moduleLogger.info("Stopped") - self._nextState = self.STATE_STOP - #self.emit("state-change", self.STATE_STOP) - - @property - def elapsed(self): - pos_info = self._audioProxy.get_position() - if isinstance(pos_info, tuple): - self._elapsed, self._duration = pos_info - return self._elapsed - - @property - def duration(self): - pos_info = self._audioProxy.get_position() - if isinstance(pos_info, tuple): - self._elapsed, self._duration = pos_info - return self._duration - - def seek_time(self, ns): - _moduleLogger.debug("Seeking to: %s", ns) - self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) ) - - def _invalidate_cache(self): - self._elapsed = 0 - self._duration = 0 - - @misc_utils.log_exception(_moduleLogger) - def _on_error(self, *args): - err, debug = "", repr(args) - _moduleLogger.error("Error: %s, (%s)" % (err, debug)) - self.emit("error", err, debug) - - @misc_utils.log_exception(_moduleLogger) - def _on_end_of_stream(self, *args): - self._state = self.STATE_STOP - self._nextState = self.STATE_STOP - self.emit("eof", self._uri) - - @misc_utils.log_exception(_moduleLogger) - def _on_state_changed(self, state): - _moduleLogger.info("State: %s", state) - state = { - "playing": self.STATE_PLAY, - "paused": self.STATE_PAUSE, - "stopped": self.STATE_STOP, - }[state] - if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP: - # They seem to want to advertise stop right as the stream is starting, breaking the owner of this - return - self._state = state - self._nextState = state - self.emit("state-change", state) - - -gobject.type_register(Stream) diff --git a/src/util/__init__.py b/src/util/__init__.py deleted file mode 100644 index 4265cc3..0000000 --- a/src/util/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env python diff --git a/src/util/algorithms.py b/src/util/algorithms.py deleted file mode 100644 index e94fb61..0000000 --- a/src/util/algorithms.py +++ /dev/null @@ -1,664 +0,0 @@ -#!/usr/bin/env python - -""" -@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448 -""" - -import itertools -import functools -import datetime -import types -import array -import random - - -def ordered_itr(collection): - """ - >>> [v for v in ordered_itr({"a": 1, "b": 2})] - [('a', 1), ('b', 2)] - >>> [v for v in ordered_itr([3, 1, 10, -20])] - [-20, 1, 3, 10] - """ - if isinstance(collection, types.DictType): - keys = list(collection.iterkeys()) - keys.sort() - for key in keys: - yield key, collection[key] - else: - values = list(collection) - values.sort() - for value in values: - yield value - - -def itercat(*iterators): - """ - Concatenate several iterators into one. - - >>> [v for v in itercat([1, 2, 3], [4, 1, 3])] - [1, 2, 3, 4, 1, 3] - """ - for i in iterators: - for x in i: - yield x - - -def product(*args, **kwds): - # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy - # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 - pools = map(tuple, args) * kwds.get('repeat', 1) - result = [[]] - for pool in pools: - result = [x+[y] for x in result for y in pool] - for prod in result: - yield tuple(prod) - - -def iterwhile(func, iterator): - """ - Iterate for as long as func(value) returns true. - >>> through = lambda b: b - >>> [v for v in iterwhile(through, [True, True, False])] - [True, True] - """ - iterator = iter(iterator) - while 1: - next = iterator.next() - if not func(next): - raise StopIteration - yield next - - -def iterfirst(iterator, count=1): - """ - Iterate through 'count' first values. - - >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)] - [1, 2, 3] - """ - iterator = iter(iterator) - for i in xrange(count): - yield iterator.next() - - -def iterstep(iterator, n): - """ - Iterate every nth value. - - >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)] - [1, 2, 3, 4, 5] - >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)] - [1, 3, 5] - >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)] - [1, 4] - """ - iterator = iter(iterator) - while True: - yield iterator.next() - # skip n-1 values - for dummy in xrange(n-1): - iterator.next() - - -def itergroup(iterator, count, padValue = None): - """ - Iterate in groups of 'count' values. If there - aren't enough values, the last result is padded with - None. - - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print list(val) - [1, 2, 3] - [4, 5, 6] - >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - (7, None, None) - >>> for val in itergroup("123456", 3): - ... print tuple(val) - ('1', '2', '3') - ('4', '5', '6') - >>> for val in itergroup("123456", 3): - ... print repr("".join(val)) - '123' - '456' - """ - paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) - nIterators = (paddedIterator, ) * count - return itertools.izip(*nIterators) - - -def xzip(*iterators): - """Iterative version of builtin 'zip'.""" - iterators = itertools.imap(iter, iterators) - while 1: - yield tuple([x.next() for x in iterators]) - - -def xmap(func, *iterators): - """Iterative version of builtin 'map'.""" - iterators = itertools.imap(iter, iterators) - values_left = [1] - - def values(): - # Emulate map behaviour, i.e. shorter - # sequences are padded with None when - # they run out of values. - values_left[0] = 0 - for i in range(len(iterators)): - iterator = iterators[i] - if iterator is None: - yield None - else: - try: - yield iterator.next() - values_left[0] = 1 - except StopIteration: - iterators[i] = None - yield None - while 1: - args = tuple(values()) - if not values_left[0]: - raise StopIteration - yield func(*args) - - -def xfilter(func, iterator): - """Iterative version of builtin 'filter'.""" - iterator = iter(iterator) - while 1: - next = iterator.next() - if func(next): - yield next - - -def xreduce(func, iterator, default=None): - """Iterative version of builtin 'reduce'.""" - iterator = iter(iterator) - try: - prev = iterator.next() - except StopIteration: - return default - single = 1 - for next in iterator: - single = 0 - prev = func(prev, next) - if single: - return func(prev, default) - return prev - - -def daterange(begin, end, delta = datetime.timedelta(1)): - """ - Form a range of dates and iterate over them. - - Arguments: - begin -- a date (or datetime) object; the beginning of the range. - end -- a date (or datetime) object; the end of the range. - delta -- (optional) a datetime.timedelta object; how much to step each iteration. - Default step is 1 day. - - Usage: - """ - if not isinstance(delta, datetime.timedelta): - delta = datetime.timedelta(delta) - - ZERO = datetime.timedelta(0) - - if begin < end: - if delta <= ZERO: - raise StopIteration - test = end.__gt__ - else: - if delta >= ZERO: - raise StopIteration - test = end.__lt__ - - while test(begin): - yield begin - begin += delta - - -class LazyList(object): - """ - A Sequence whose values are computed lazily by an iterator. - - Module for the creation and use of iterator-based lazy lists. - this module defines a class LazyList which can be used to represent sequences - of values generated lazily. One can also create recursively defined lazy lists - that generate their values based on ones previously generated. - - Backport to python 2.5 by Michael Pust - """ - - __author__ = 'Dan Spitz' - - def __init__(self, iterable): - self._exhausted = False - self._iterator = iter(iterable) - self._data = [] - - def __len__(self): - """Get the length of a LazyList's computed data.""" - return len(self._data) - - def __getitem__(self, i): - """Get an item from a LazyList. - i should be a positive integer or a slice object.""" - if isinstance(i, int): - #index has not yet been yielded by iterator (or iterator exhausted - #before reaching that index) - if i >= len(self): - self.exhaust(i) - elif i < 0: - raise ValueError('cannot index LazyList with negative number') - return self._data[i] - - #LazyList slices are iterators over a portion of the list. - elif isinstance(i, slice): - start, stop, step = i.start, i.stop, i.step - if any(x is not None and x < 0 for x in (start, stop, step)): - raise ValueError('cannot index or step through a LazyList with' - 'a negative number') - #set start and step to their integer defaults if they are None. - if start is None: - start = 0 - if step is None: - step = 1 - - def LazyListIterator(): - count = start - predicate = ( - (lambda: True) - if stop is None - else (lambda: count < stop) - ) - while predicate(): - try: - yield self[count] - #slices can go out of actual index range without raising an - #error - except IndexError: - break - count += step - return LazyListIterator() - - raise TypeError('i must be an integer or slice') - - def __iter__(self): - """return an iterator over each value in the sequence, - whether it has been computed yet or not.""" - return self[:] - - def computed(self): - """Return an iterator over the values in a LazyList that have - already been computed.""" - return self[:len(self)] - - def exhaust(self, index = None): - """Exhaust the iterator generating this LazyList's values. - if index is None, this will exhaust the iterator completely. - Otherwise, it will iterate over the iterator until either the list - has a value for index or the iterator is exhausted. - """ - if self._exhausted: - return - if index is None: - ind_range = itertools.count(len(self)) - else: - ind_range = range(len(self), index + 1) - - for ind in ind_range: - try: - self._data.append(self._iterator.next()) - except StopIteration: #iterator is fully exhausted - self._exhausted = True - break - - -class RecursiveLazyList(LazyList): - - def __init__(self, prod, *args, **kwds): - super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds)) - - -class RecursiveLazyListFactory: - - def __init__(self, producer): - self._gen = producer - - def __call__(self, *a, **kw): - return RecursiveLazyList(self._gen, *a, **kw) - - -def lazylist(gen): - """ - Decorator for creating a RecursiveLazyList subclass. - This should decorate a generator function taking the LazyList object as its - first argument which yields the contents of the list in order. - - >>> #fibonnacci sequence in a lazy list. - >>> @lazylist - ... def fibgen(lst): - ... yield 0 - ... yield 1 - ... for a, b in itertools.izip(lst, lst[1:]): - ... yield a + b - ... - >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence - >>> fibs = fibgen() - >>> - >>> #prime numbers in a lazy list. - >>> @lazylist - ... def primegen(lst): - ... yield 2 - ... for candidate in itertools.count(3): #start at next number after 2 - ... #if candidate is not divisible by any smaller prime numbers, - ... #it is a prime. - ... if all(candidate % p for p in lst.computed()): - ... yield candidate - ... - >>> #same for primes- treat it like an infinitely long list containing all prime numbers. - >>> primes = primegen() - >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2] - 0 1 1 2 3 5 - >>> print list(fibs[:10]), list(primes[:10]) - [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] - """ - return RecursiveLazyListFactory(gen) - - -def map_func(f): - """ - >>> import misc - >>> misc.validate_decorator(map_func) - """ - - @functools.wraps(f) - def wrapper(*args): - result = itertools.imap(f, args) - return result - return wrapper - - -def reduce_func(function): - """ - >>> import misc - >>> misc.validate_decorator(reduce_func(lambda x: x)) - """ - - def decorator(f): - - @functools.wraps(f) - def wrapper(*args): - result = reduce(function, f(args)) - return result - return wrapper - return decorator - - -def any_(iterable): - """ - @note Python Version <2.5 - - >>> any_([True, True]) - True - >>> any_([True, False]) - True - >>> any_([False, False]) - False - """ - - for element in iterable: - if element: - return True - return False - - -def all_(iterable): - """ - @note Python Version <2.5 - - >>> all_([True, True]) - True - >>> all_([True, False]) - False - >>> all_([False, False]) - False - """ - - for element in iterable: - if not element: - return False - return True - - -def for_every(pred, seq): - """ - for_every takes a one argument predicate function and a sequence. - @param pred The predicate function should return true or false. - @returns true if every element in seq returns true for predicate, else returns false. - - >>> for_every (lambda c: c > 5,(6,7,8,9)) - True - - @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907 - """ - - for i in seq: - if not pred(i): - return False - return True - - -def there_exists(pred, seq): - """ - there_exists takes a one argument predicate function and a sequence. - @param pred The predicate function should return true or false. - @returns true if any element in seq returns true for predicate, else returns false. - - >>> there_exists (lambda c: c > 5,(6,7,8,9)) - True - - @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907 - """ - - for i in seq: - if pred(i): - return True - return False - - -def func_repeat(quantity, func, *args, **kwd): - """ - Meant to be in connection with "reduce" - """ - for i in xrange(quantity): - yield func(*args, **kwd) - - -def function_map(preds, item): - """ - Meant to be in connection with "reduce" - """ - results = (pred(item) for pred in preds) - - return results - - -def functional_if(combiner, preds, item): - """ - Combines the result of a list of predicates applied to item according to combiner - - @see any, every for example combiners - """ - pass_bool = lambda b: b - - bool_results = function_map(preds, item) - return combiner(pass_bool, bool_results) - - -def pushback_itr(itr): - """ - >>> list(pushback_itr(xrange(5))) - [0, 1, 2, 3, 4] - >>> - >>> first = True - >>> itr = pushback_itr(xrange(5)) - >>> for i in itr: - ... print i - ... if first and i == 2: - ... first = False - ... print itr.send(i) - 0 - 1 - 2 - None - 2 - 3 - 4 - >>> - >>> first = True - >>> itr = pushback_itr(xrange(5)) - >>> for i in itr: - ... print i - ... if first and i == 2: - ... first = False - ... print itr.send(i) - ... print itr.send(i) - 0 - 1 - 2 - None - None - 2 - 2 - 3 - 4 - >>> - >>> itr = pushback_itr(xrange(5)) - >>> print itr.next() - 0 - >>> print itr.next() - 1 - >>> print itr.send(10) - None - >>> print itr.next() - 10 - >>> print itr.next() - 2 - >>> print itr.send(20) - None - >>> print itr.send(30) - None - >>> print itr.send(40) - None - >>> print itr.next() - 40 - >>> print itr.next() - 30 - >>> print itr.send(50) - None - >>> print itr.next() - 50 - >>> print itr.next() - 20 - >>> print itr.next() - 3 - >>> print itr.next() - 4 - """ - for item in itr: - maybePushedBack = yield item - queue = [] - while queue or maybePushedBack is not None: - if maybePushedBack is not None: - queue.append(maybePushedBack) - maybePushedBack = yield None - else: - item = queue.pop() - maybePushedBack = yield item - - -def itr_available(queue, initiallyBlock = False): - if initiallyBlock: - yield queue.get() - while not queue.empty(): - yield queue.get_nowait() - - -class BloomFilter(object): - """ - http://en.wikipedia.org/wiki/Bloom_filter - Sources: - http://code.activestate.com/recipes/577684-bloom-filter/ - http://code.activestate.com/recipes/577686-bloom-filter/ - - >>> from random import sample - >>> from string import ascii_letters - >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut - ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas - ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota - ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey - ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon - ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah - ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split() - >>> bf = BloomFilter(num_bits=1000, num_probes=14) - >>> for state in states: - ... bf.add(state) - >>> numStatesFound = sum(state in bf for state in states) - >>> numStatesFound, len(states) - (50, 50) - >>> trials = 100 - >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials)) - >>> numGarbageFound, trials - (0, 100) - """ - - def __init__(self, num_bits, num_probes): - num_words = (num_bits + 31) // 32 - self._arr = array.array('B', [0]) * num_words - self._num_probes = num_probes - - def add(self, key): - for i, mask in self._get_probes(key): - self._arr[i] |= mask - - def union(self, bfilter): - if self._match_template(bfilter): - for i, b in enumerate(bfilter._arr): - self._arr[i] |= b - else: - # Union b/w two unrelated bloom filter raises this - raise ValueError("Mismatched bloom filters") - - def intersection(self, bfilter): - if self._match_template(bfilter): - for i, b in enumerate(bfilter._arr): - self._arr[i] &= b - else: - # Intersection b/w two unrelated bloom filter raises this - raise ValueError("Mismatched bloom filters") - - def __contains__(self, key): - return all(self._arr[i] & mask for i, mask in self._get_probes(key)) - - def _match_template(self, bfilter): - return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes - - def _get_probes(self, key): - hasher = random.Random(key).randrange - for _ in range(self._num_probes): - array_index = hasher(len(self._arr)) - bit_index = hasher(32) - yield array_index, 1 << bit_index - - -if __name__ == "__main__": - import doctest - print doctest.testmod() diff --git a/src/util/concurrent.py b/src/util/concurrent.py deleted file mode 100644 index f5f6e1d..0000000 --- a/src/util/concurrent.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement - -import os -import errno -import time -import functools -import contextlib -import logging - -import misc - - -_moduleLogger = logging.getLogger(__name__) - - -class AsyncTaskQueue(object): - - def __init__(self, taskPool): - self._asyncs = [] - self._taskPool = taskPool - - def add_async(self, func): - self.flush() - a = AsyncGeneratorTask(self._taskPool, func) - self._asyncs.append(a) - return a - - def flush(self): - self._asyncs = [a for a in self._asyncs if not a.isDone] - - -class AsyncGeneratorTask(object): - - def __init__(self, pool, func): - self._pool = pool - self._func = func - self._run = None - self._isDone = False - - @property - def isDone(self): - return self._isDone - - def start(self, *args, **kwds): - assert self._run is None, "Task already started" - self._run = self._func(*args, **kwds) - trampoline, args, kwds = self._run.send(None) # priming the function - self._pool.add_task( - trampoline, - args, - kwds, - self.on_success, - self.on_error, - ) - - @misc.log_exception(_moduleLogger) - def on_success(self, result): - _moduleLogger.debug("Processing success for: %r", self._func) - try: - trampoline, args, kwds = self._run.send(result) - except StopIteration, e: - self._isDone = True - else: - self._pool.add_task( - trampoline, - args, - kwds, - self.on_success, - self.on_error, - ) - - @misc.log_exception(_moduleLogger) - def on_error(self, error): - _moduleLogger.debug("Processing error for: %r", self._func) - try: - trampoline, args, kwds = self._run.throw(error) - except StopIteration, e: - self._isDone = True - else: - self._pool.add_task( - trampoline, - args, - kwds, - self.on_success, - self.on_error, - ) - - def __repr__(self): - return "" % (self._func.__name__, id(self)) - - def __hash__(self): - return hash(self._func) - - def __eq__(self, other): - return self._func == other._func - - def __ne__(self, other): - return self._func != other._func - - -def synchronized(lock): - """ - Synchronization decorator. - - >>> import misc - >>> misc.validate_decorator(synchronized(object())) - """ - - def wrap(f): - - @functools.wraps(f) - def newFunction(*args, **kw): - lock.acquire() - try: - return f(*args, **kw) - finally: - lock.release() - return newFunction - return wrap - - -@contextlib.contextmanager -def qlock(queue, gblock = True, gtimeout = None, pblock = True, ptimeout = None): - """ - Locking with a queue, good for when you want to lock an item passed around - - >>> import Queue - >>> item = 5 - >>> lock = Queue.Queue() - >>> lock.put(item) - >>> with qlock(lock) as i: - ... print i - 5 - """ - item = queue.get(gblock, gtimeout) - try: - yield item - finally: - queue.put(item, pblock, ptimeout) - - -@contextlib.contextmanager -def flock(path, timeout=-1): - WAIT_FOREVER = -1 - DELAY = 0.1 - timeSpent = 0 - - acquired = False - - while timeSpent <= timeout or timeout == WAIT_FOREVER: - try: - fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) - acquired = True - break - except OSError, e: - if e.errno != errno.EEXIST: - raise - time.sleep(DELAY) - timeSpent += DELAY - - assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout) - - try: - yield fd - finally: - os.unlink(path) diff --git a/src/util/coroutines.py b/src/util/coroutines.py deleted file mode 100755 index b1e539e..0000000 --- a/src/util/coroutines.py +++ /dev/null @@ -1,623 +0,0 @@ -#!/usr/bin/env python - -""" -Uses for generators -* Pull pipelining (iterators) -* Push pipelining (coroutines) -* State machines (coroutines) -* "Cooperative multitasking" (coroutines) -* Algorithm -> Object transform for cohesiveness (for example context managers) (coroutines) - -Design considerations -* When should a stage pass on exceptions or have it thrown within it? -* When should a stage pass on GeneratorExits? -* Is there a way to either turn a push generator into a iterator or to use - comprehensions syntax for push generators (I doubt it) -* When should the stage try and send data in both directions -* Since pull generators (generators), push generators (coroutines), subroutines, and coroutines are all coroutines, maybe we should rename the push generators to not confuse them, like signals/slots? and then refer to two-way generators as coroutines -** If so, make s* and co* implementation of functions -""" - -import threading -import Queue -import pickle -import functools -import itertools -import xml.sax -import xml.parsers.expat - - -def autostart(func): - """ - >>> @autostart - ... def grep_sink(pattern): - ... print "Looking for %s" % pattern - ... while True: - ... line = yield - ... if pattern in line: - ... print line, - >>> g = grep_sink("python") - Looking for python - >>> g.send("Yeah but no but yeah but no") - >>> g.send("A series of tubes") - >>> g.send("python generators rock!") - python generators rock! - >>> g.close() - """ - - @functools.wraps(func) - def start(*args, **kwargs): - cr = func(*args, **kwargs) - cr.next() - return cr - - return start - - -@autostart -def printer_sink(format = "%s"): - """ - >>> pr = printer_sink("%r") - >>> pr.send("Hello") - 'Hello' - >>> pr.send("5") - '5' - >>> pr.send(5) - 5 - >>> p = printer_sink() - >>> p.send("Hello") - Hello - >>> p.send("World") - World - >>> # p.throw(RuntimeError, "Goodbye") - >>> # p.send("Meh") - >>> # p.close() - """ - while True: - item = yield - print format % (item, ) - - -@autostart -def null_sink(): - """ - Good for uses like with cochain to pick up any slack - """ - while True: - item = yield - - -def itr_source(itr, target): - """ - >>> itr_source(xrange(2), printer_sink()) - 0 - 1 - """ - for item in itr: - target.send(item) - - -@autostart -def cofilter(predicate, target): - """ - >>> p = printer_sink() - >>> cf = cofilter(None, p) - >>> cf.send("") - >>> cf.send("Hello") - Hello - >>> cf.send([]) - >>> cf.send([1, 2]) - [1, 2] - >>> cf.send(False) - >>> cf.send(True) - True - >>> cf.send(0) - >>> cf.send(1) - 1 - >>> # cf.throw(RuntimeError, "Goodbye") - >>> # cf.send(False) - >>> # cf.send(True) - >>> # cf.close() - """ - if predicate is None: - predicate = bool - - while True: - try: - item = yield - if predicate(item): - target.send(item) - except StandardError, e: - target.throw(e.__class__, e.message) - - -@autostart -def comap(function, target): - """ - >>> p = printer_sink() - >>> cm = comap(lambda x: x+1, p) - >>> cm.send(0) - 1 - >>> cm.send(1.0) - 2.0 - >>> cm.send(-2) - -1 - >>> # cm.throw(RuntimeError, "Goodbye") - >>> # cm.send(0) - >>> # cm.send(1.0) - >>> # cm.close() - """ - while True: - try: - item = yield - mappedItem = function(item) - target.send(mappedItem) - except StandardError, e: - target.throw(e.__class__, e.message) - - -def func_sink(function): - return comap(function, null_sink()) - - -def expand_positional(function): - - @functools.wraps(function) - def expander(item): - return function(*item) - - return expander - - -@autostart -def append_sink(l): - """ - >>> l = [] - >>> apps = append_sink(l) - >>> apps.send(1) - >>> apps.send(2) - >>> apps.send(3) - >>> print l - [1, 2, 3] - """ - while True: - item = yield - l.append(item) - - -@autostart -def last_n_sink(l, n = 1): - """ - >>> l = [] - >>> lns = last_n_sink(l) - >>> lns.send(1) - >>> lns.send(2) - >>> lns.send(3) - >>> print l - [3] - """ - del l[:] - while True: - item = yield - extraCount = len(l) - n + 1 - if 0 < extraCount: - del l[0:extraCount] - l.append(item) - - -@autostart -def coreduce(target, function, initializer = None): - """ - >>> reduceResult = [] - >>> lns = last_n_sink(reduceResult) - >>> cr = coreduce(lns, lambda x, y: x + y, 0) - >>> cr.send(1) - >>> cr.send(2) - >>> cr.send(3) - >>> print reduceResult - [6] - >>> cr = coreduce(lns, lambda x, y: x + y) - >>> cr.send(1) - >>> cr.send(2) - >>> cr.send(3) - >>> print reduceResult - [6] - """ - isFirst = True - cumulativeRef = initializer - while True: - item = yield - if isFirst and initializer is None: - cumulativeRef = item - else: - cumulativeRef = function(cumulativeRef, item) - target.send(cumulativeRef) - isFirst = False - - -@autostart -def cotee(targets): - """ - Takes a sequence of coroutines and sends the received items to all of them - - >>> ct = cotee((printer_sink("1 %s"), printer_sink("2 %s"))) - >>> ct.send("Hello") - 1 Hello - 2 Hello - >>> ct.send("World") - 1 World - 2 World - >>> # ct.throw(RuntimeError, "Goodbye") - >>> # ct.send("Meh") - >>> # ct.close() - """ - while True: - try: - item = yield - for target in targets: - target.send(item) - except StandardError, e: - for target in targets: - target.throw(e.__class__, e.message) - - -class CoTee(object): - """ - >>> ct = CoTee() - >>> ct.register_sink(printer_sink("1 %s")) - >>> ct.register_sink(printer_sink("2 %s")) - >>> ct.stage.send("Hello") - 1 Hello - 2 Hello - >>> ct.stage.send("World") - 1 World - 2 World - >>> ct.register_sink(printer_sink("3 %s")) - >>> ct.stage.send("Foo") - 1 Foo - 2 Foo - 3 Foo - >>> # ct.stage.throw(RuntimeError, "Goodbye") - >>> # ct.stage.send("Meh") - >>> # ct.stage.close() - """ - - def __init__(self): - self.stage = self._stage() - self._targets = [] - - def register_sink(self, sink): - self._targets.append(sink) - - def unregister_sink(self, sink): - self._targets.remove(sink) - - def restart(self): - self.stage = self._stage() - - @autostart - def _stage(self): - while True: - try: - item = yield - for target in self._targets: - target.send(item) - except StandardError, e: - for target in self._targets: - target.throw(e.__class__, e.message) - - -def _flush_queue(queue): - while not queue.empty(): - yield queue.get() - - -@autostart -def cocount(target, start = 0): - """ - >>> cc = cocount(printer_sink("%s")) - >>> cc.send("a") - 0 - >>> cc.send(None) - 1 - >>> cc.send([]) - 2 - >>> cc.send(0) - 3 - """ - for i in itertools.count(start): - item = yield - target.send(i) - - -@autostart -def coenumerate(target, start = 0): - """ - >>> ce = coenumerate(printer_sink("%r")) - >>> ce.send("a") - (0, 'a') - >>> ce.send(None) - (1, None) - >>> ce.send([]) - (2, []) - >>> ce.send(0) - (3, 0) - """ - for i in itertools.count(start): - item = yield - decoratedItem = i, item - target.send(decoratedItem) - - -@autostart -def corepeat(target, elem): - """ - >>> cr = corepeat(printer_sink("%s"), "Hello World") - >>> cr.send("a") - Hello World - >>> cr.send(None) - Hello World - >>> cr.send([]) - Hello World - >>> cr.send(0) - Hello World - """ - while True: - item = yield - target.send(elem) - - -@autostart -def cointercept(target, elems): - """ - >>> cr = cointercept(printer_sink("%s"), [1, 2, 3, 4]) - >>> cr.send("a") - 1 - >>> cr.send(None) - 2 - >>> cr.send([]) - 3 - >>> cr.send(0) - 4 - >>> cr.send("Bye") - Traceback (most recent call last): - File "/usr/lib/python2.5/doctest.py", line 1228, in __run - compileflags, 1) in test.globs - File "", line 1, in - cr.send("Bye") - StopIteration - """ - item = yield - for elem in elems: - target.send(elem) - item = yield - - -@autostart -def codropwhile(target, pred): - """ - >>> cdw = codropwhile(printer_sink("%s"), lambda x: x) - >>> cdw.send([0, 1, 2]) - >>> cdw.send(1) - >>> cdw.send(True) - >>> cdw.send(False) - >>> cdw.send([0, 1, 2]) - [0, 1, 2] - >>> cdw.send(1) - 1 - >>> cdw.send(True) - True - """ - while True: - item = yield - if not pred(item): - break - - while True: - item = yield - target.send(item) - - -@autostart -def cotakewhile(target, pred): - """ - >>> ctw = cotakewhile(printer_sink("%s"), lambda x: x) - >>> ctw.send([0, 1, 2]) - [0, 1, 2] - >>> ctw.send(1) - 1 - >>> ctw.send(True) - True - >>> ctw.send(False) - >>> ctw.send([0, 1, 2]) - >>> ctw.send(1) - >>> ctw.send(True) - """ - while True: - item = yield - if not pred(item): - break - target.send(item) - - while True: - item = yield - - -@autostart -def coslice(target, lower, upper): - """ - >>> cs = coslice(printer_sink("%r"), 3, 5) - >>> cs.send("0") - >>> cs.send("1") - >>> cs.send("2") - >>> cs.send("3") - '3' - >>> cs.send("4") - '4' - >>> cs.send("5") - >>> cs.send("6") - """ - for i in xrange(lower): - item = yield - for i in xrange(upper - lower): - item = yield - target.send(item) - while True: - item = yield - - -@autostart -def cochain(targets): - """ - >>> cr = cointercept(printer_sink("good %s"), [1, 2, 3, 4]) - >>> cc = cochain([cr, printer_sink("end %s")]) - >>> cc.send("a") - good 1 - >>> cc.send(None) - good 2 - >>> cc.send([]) - good 3 - >>> cc.send(0) - good 4 - >>> cc.send("Bye") - end Bye - """ - behind = [] - for target in targets: - try: - while behind: - item = behind.pop() - target.send(item) - while True: - item = yield - target.send(item) - except StopIteration: - behind.append(item) - - -@autostart -def queue_sink(queue): - """ - >>> q = Queue.Queue() - >>> qs = queue_sink(q) - >>> qs.send("Hello") - >>> qs.send("World") - >>> qs.throw(RuntimeError, "Goodbye") - >>> qs.send("Meh") - >>> qs.close() - >>> print [i for i in _flush_queue(q)] - [(None, 'Hello'), (None, 'World'), (, 'Goodbye'), (None, 'Meh'), (, None)] - """ - while True: - try: - item = yield - queue.put((None, item)) - except StandardError, e: - queue.put((e.__class__, e.message)) - except GeneratorExit: - queue.put((GeneratorExit, None)) - raise - - -def decode_item(item, target): - if item[0] is None: - target.send(item[1]) - return False - elif item[0] is GeneratorExit: - target.close() - return True - else: - target.throw(item[0], item[1]) - return False - - -def queue_source(queue, target): - """ - >>> q = Queue.Queue() - >>> for i in [ - ... (None, 'Hello'), - ... (None, 'World'), - ... (GeneratorExit, None), - ... ]: - ... q.put(i) - >>> qs = queue_source(q, printer_sink()) - Hello - World - """ - isDone = False - while not isDone: - item = queue.get() - isDone = decode_item(item, target) - - -def threaded_stage(target, thread_factory = threading.Thread): - messages = Queue.Queue() - - run_source = functools.partial(queue_source, messages, target) - thread_factory(target=run_source).start() - - # Sink running in current thread - return functools.partial(queue_sink, messages) - - -@autostart -def pickle_sink(f): - while True: - try: - item = yield - pickle.dump((None, item), f) - except StandardError, e: - pickle.dump((e.__class__, e.message), f) - except GeneratorExit: - pickle.dump((GeneratorExit, ), f) - raise - except StopIteration: - f.close() - return - - -def pickle_source(f, target): - try: - isDone = False - while not isDone: - item = pickle.load(f) - isDone = decode_item(item, target) - except EOFError: - target.close() - - -class EventHandler(object, xml.sax.ContentHandler): - - START = "start" - TEXT = "text" - END = "end" - - def __init__(self, target): - object.__init__(self) - xml.sax.ContentHandler.__init__(self) - self._target = target - - def startElement(self, name, attrs): - self._target.send((self.START, (name, attrs._attrs))) - - def characters(self, text): - self._target.send((self.TEXT, text)) - - def endElement(self, name): - self._target.send((self.END, name)) - - -def expat_parse(f, target): - parser = xml.parsers.expat.ParserCreate() - parser.buffer_size = 65536 - parser.buffer_text = True - parser.returns_unicode = False - parser.StartElementHandler = lambda name, attrs: target.send(('start', (name, attrs))) - parser.EndElementHandler = lambda name: target.send(('end', name)) - parser.CharacterDataHandler = lambda data: target.send(('text', data)) - parser.ParseFile(f) - - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/src/util/go_utils.py b/src/util/go_utils.py deleted file mode 100644 index 61e731d..0000000 --- a/src/util/go_utils.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement - -import time -import functools -import threading -import Queue -import logging - -import gobject - -import algorithms -import misc - - -_moduleLogger = logging.getLogger(__name__) - - -def make_idler(func): - """ - Decorator that makes a generator-function into a function that will continue execution on next call - """ - a = [] - - @functools.wraps(func) - def decorated_func(*args, **kwds): - if not a: - a.append(func(*args, **kwds)) - try: - a[0].next() - return True - except StopIteration: - del a[:] - return False - - return decorated_func - - -def async(func): - """ - Make a function mainloop friendly. the function will be called at the - next mainloop idle state. - - >>> import misc - >>> misc.validate_decorator(async) - """ - - @functools.wraps(func) - def new_function(*args, **kwargs): - - def async_function(): - func(*args, **kwargs) - return False - - gobject.idle_add(async_function) - - return new_function - - -class Async(object): - - def __init__(self, func, once = True): - self.__func = func - self.__idleId = None - self.__once = once - - def start(self): - assert self.__idleId is None - if self.__once: - self.__idleId = gobject.idle_add(self._on_once) - else: - self.__idleId = gobject.idle_add(self.__func) - - def is_running(self): - return self.__idleId is not None - - def cancel(self): - if self.__idleId is not None: - gobject.source_remove(self.__idleId) - self.__idleId = None - - def __call__(self): - return self.start() - - @misc.log_exception(_moduleLogger) - def _on_once(self): - self.cancel() - try: - self.__func() - except Exception: - pass - return False - - -class Timeout(object): - - def __init__(self, func, once = True): - self.__func = func - self.__timeoutId = None - self.__once = once - - def start(self, **kwds): - assert self.__timeoutId is None - - callback = self._on_once if self.__once else self.__func - - assert len(kwds) == 1 - timeoutInSeconds = kwds["seconds"] - assert 0 <= timeoutInSeconds - - if timeoutInSeconds == 0: - self.__timeoutId = gobject.idle_add(callback) - else: - self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback) - - def is_running(self): - return self.__timeoutId is not None - - def cancel(self): - if self.__timeoutId is not None: - gobject.source_remove(self.__timeoutId) - self.__timeoutId = None - - def __call__(self, **kwds): - return self.start(**kwds) - - @misc.log_exception(_moduleLogger) - def _on_once(self): - self.cancel() - try: - self.__func() - except Exception: - pass - return False - - -_QUEUE_EMPTY = object() - - -class FutureThread(object): - - def __init__(self): - self.__workQueue = Queue.Queue() - self.__thread = threading.Thread( - name = type(self).__name__, - target = self.__consume_queue, - ) - self.__isRunning = True - - def start(self): - self.__thread.start() - - def stop(self): - self.__isRunning = False - for _ in algorithms.itr_available(self.__workQueue): - pass # eat up queue to cut down dumb work - self.__workQueue.put(_QUEUE_EMPTY) - - def clear_tasks(self): - for _ in algorithms.itr_available(self.__workQueue): - pass # eat up queue to cut down dumb work - - def add_task(self, func, args, kwds, on_success, on_error): - task = func, args, kwds, on_success, on_error - self.__workQueue.put(task) - - @misc.log_exception(_moduleLogger) - def __trampoline_callback(self, on_success, on_error, isError, result): - if not self.__isRunning: - if isError: - _moduleLogger.error("Masking: %s" % (result, )) - isError = True - result = StopIteration("Cancelling all callbacks") - callback = on_success if not isError else on_error - try: - callback(result) - except Exception: - _moduleLogger.exception("Callback errored") - return False - - @misc.log_exception(_moduleLogger) - def __consume_queue(self): - while True: - task = self.__workQueue.get() - if task is _QUEUE_EMPTY: - break - func, args, kwds, on_success, on_error = task - - try: - result = func(*args, **kwds) - isError = False - except Exception, e: - _moduleLogger.error("Error, passing it back to the main thread") - result = e - isError = True - self.__workQueue.task_done() - - gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result) - _moduleLogger.debug("Shutting down worker thread") - - -class AutoSignal(object): - - def __init__(self, toplevel): - self.__disconnectPool = [] - toplevel.connect("destroy", self.__on_destroy) - - def connect_auto(self, widget, *args): - id = widget.connect(*args) - self.__disconnectPool.append((widget, id)) - - @misc.log_exception(_moduleLogger) - def __on_destroy(self, widget): - _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool))) - for widget, id in self.__disconnectPool: - widget.disconnect(id) - del self.__disconnectPool[:] - - -def throttled(minDelay, queue): - """ - Throttle the calls to a function by queueing all the calls that happen - before the minimum delay - - >>> import misc - >>> import Queue - >>> misc.validate_decorator(throttled(0, Queue.Queue())) - """ - - def actual_decorator(func): - - lastCallTime = [None] - - def process_queue(): - if 0 < len(queue): - func, args, kwargs = queue.pop(0) - lastCallTime[0] = time.time() * 1000 - func(*args, **kwargs) - return False - - @functools.wraps(func) - def new_function(*args, **kwargs): - now = time.time() * 1000 - if ( - lastCallTime[0] is None or - (now - lastCallTime >= minDelay) - ): - lastCallTime[0] = now - func(*args, **kwargs) - else: - queue.append((func, args, kwargs)) - lastCallDelta = now - lastCallTime[0] - processQueueTimeout = int(minDelay * len(queue) - lastCallDelta) - gobject.timeout_add(processQueueTimeout, process_queue) - - return new_function - - return actual_decorator - - -def _old_timeout_add_seconds(timeout, callback): - return gobject.timeout_add(timeout * 1000, callback) - - -def _timeout_add_seconds(timeout, callback): - return gobject.timeout_add_seconds(timeout, callback) - - -try: - gobject.timeout_add_seconds - timeout_add_seconds = _timeout_add_seconds -except AttributeError: - timeout_add_seconds = _old_timeout_add_seconds diff --git a/src/util/io.py b/src/util/io.py deleted file mode 100644 index 4198f4b..0000000 --- a/src/util/io.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python - - -from __future__ import with_statement - -import os -import pickle -import contextlib -import itertools -import codecs -from xml.sax import saxutils -import csv -try: - import cStringIO as StringIO -except ImportError: - import StringIO - - -@contextlib.contextmanager -def change_directory(directory): - previousDirectory = os.getcwd() - os.chdir(directory) - currentDirectory = os.getcwd() - - try: - yield previousDirectory, currentDirectory - finally: - os.chdir(previousDirectory) - - -@contextlib.contextmanager -def pickled(filename): - """ - Here is an example usage: - with pickled("foo.db") as p: - p("users", list).append(["srid", "passwd", 23]) - """ - - if os.path.isfile(filename): - data = pickle.load(open(filename)) - else: - data = {} - - def getter(item, factory): - if item in data: - return data[item] - else: - data[item] = factory() - return data[item] - - yield getter - - pickle.dump(data, open(filename, "w")) - - -@contextlib.contextmanager -def redirect(object_, attr, value): - """ - >>> import sys - ... with redirect(sys, 'stdout', open('stdout', 'w')): - ... print "hello" - ... - >>> print "we're back" - we're back - """ - orig = getattr(object_, attr) - setattr(object_, attr, value) - try: - yield - finally: - setattr(object_, attr, orig) - - -def pathsplit(path): - """ - >>> pathsplit("/a/b/c") - ['', 'a', 'b', 'c'] - >>> pathsplit("./plugins/builtins.ini") - ['.', 'plugins', 'builtins.ini'] - """ - pathParts = path.split(os.path.sep) - return pathParts - - -def commonpath(l1, l2, common=None): - """ - >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1')) - (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1']) - >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini")) - (['.', 'plugins'], [''], ['builtins.ini']) - >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins")) - (['.', 'plugins'], ['builtins'], []) - """ - if common is None: - common = [] - - if l1 == l2: - return l1, [], [] - - for i, (leftDir, rightDir) in enumerate(zip(l1, l2)): - if leftDir != rightDir: - return l1[0:i], l1[i:], l2[i:] - else: - if leftDir == rightDir: - i += 1 - return l1[0:i], l1[i:], l2[i:] - - -def relpath(p1, p2): - """ - >>> relpath('/', '/') - './' - >>> relpath('/a/b/c/d', '/') - '../../../../' - >>> relpath('/a/b/c/d', '/a/b/c1/d1') - '../../c1/d1' - >>> relpath('/a/b/c/d', '/a/b/c1/d1/') - '../../c1/d1' - >>> relpath("./plugins/builtins", "./plugins") - '../' - >>> relpath("./plugins/", "./plugins/builtins.ini") - 'builtins.ini' - """ - sourcePath = os.path.normpath(p1) - destPath = os.path.normpath(p2) - - (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath)) - if len(sourceOnly) or len(destOnly): - relParts = itertools.chain( - (('..' + os.sep) * len(sourceOnly), ), - destOnly, - ) - return os.path.join(*relParts) - else: - return "."+os.sep - - -class UTF8Recoder(object): - """ - Iterator that reads an encoded stream and reencodes the input to UTF-8 - """ - def __init__(self, f, encoding): - self.reader = codecs.getreader(encoding)(f) - - def __iter__(self): - return self - - def next(self): - return self.reader.next().encode("utf-8") - - -class UnicodeReader(object): - """ - A CSV reader which will iterate over lines in the CSV file "f", - which is encoded in the given encoding. - """ - - def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): - f = UTF8Recoder(f, encoding) - self.reader = csv.reader(f, dialect=dialect, **kwds) - - def next(self): - row = self.reader.next() - return [unicode(s, "utf-8") for s in row] - - def __iter__(self): - return self - -class UnicodeWriter(object): - """ - A CSV writer which will write rows to CSV file "f", - which is encoded in the given encoding. - """ - - def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): - # Redirect output to a queue - self.queue = StringIO.StringIO() - self.writer = csv.writer(self.queue, dialect=dialect, **kwds) - self.stream = f - self.encoder = codecs.getincrementalencoder(encoding)() - - def writerow(self, row): - self.writer.writerow([s.encode("utf-8") for s in row]) - # Fetch UTF-8 output from the queue ... - data = self.queue.getvalue() - data = data.decode("utf-8") - # ... and reencode it into the target encoding - data = self.encoder.encode(data) - # write to the target stream - self.stream.write(data) - # empty queue - self.queue.truncate(0) - - def writerows(self, rows): - for row in rows: - self.writerow(row) - - -def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs): - # csv.py doesn't do Unicode; encode temporarily as UTF-8: - csv_reader = csv.reader(utf_8_encoder(unicode_csv_data), - dialect=dialect, **kwargs) - for row in csv_reader: - # decode UTF-8 back to Unicode, cell by cell: - yield [unicode(cell, 'utf-8') for cell in row] - - -def utf_8_encoder(unicode_csv_data): - for line in unicode_csv_data: - yield line.encode('utf-8') - - -_UNESCAPE_ENTITIES = { - """: '"', - " ": " ", - "'": "'", -} - - -_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys())) -del _ESCAPE_ENTITIES[" "] - - -def unescape(text): - plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) - return plain - - -def escape(text): - fancy = saxutils.escape(text, _ESCAPE_ENTITIES) - return fancy diff --git a/src/util/linux.py b/src/util/linux.py deleted file mode 100644 index 4e77445..0000000 --- a/src/util/linux.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python - - -import os -import logging - -try: - from xdg import BaseDirectory as _BaseDirectory - BaseDirectory = _BaseDirectory -except ImportError: - BaseDirectory = None - - -_moduleLogger = logging.getLogger(__name__) - - -_libc = None - - -def set_process_name(name): - try: # change process name for killall - global _libc - if _libc is None: - import ctypes - _libc = ctypes.CDLL('libc.so.6') - _libc.prctl(15, name, 0, 0, 0) - except Exception, e: - _moduleLogger.warning('Unable to set processName: %s" % e') - - -def get_new_resource(resourceType, resource, name): - if BaseDirectory is not None: - if resourceType == "data": - base = BaseDirectory.xdg_data_home - if base == "/usr/share/mime": - # Ugly hack because somehow Maemo 4.1 seems to be set to this - base = os.path.join(os.path.expanduser("~"), ".%s" % resource) - elif resourceType == "config": - base = BaseDirectory.xdg_config_home - elif resourceType == "cache": - base = BaseDirectory.xdg_cache_home - else: - raise RuntimeError("Unknown type: "+resourceType) - else: - base = os.path.join(os.path.expanduser("~"), ".%s" % resource) - - filePath = os.path.join(base, resource, name) - dirPath = os.path.dirname(filePath) - if not os.path.exists(dirPath): - # Looking before I leap to not mask errors - os.makedirs(dirPath) - - return filePath - - -def get_existing_resource(resourceType, resource, name): - if BaseDirectory is not None: - if resourceType == "data": - base = BaseDirectory.xdg_data_home - elif resourceType == "config": - base = BaseDirectory.xdg_config_home - elif resourceType == "cache": - base = BaseDirectory.xdg_cache_home - else: - raise RuntimeError("Unknown type: "+resourceType) - else: - base = None - - if base is not None: - finalPath = os.path.join(base, name) - if os.path.exists(finalPath): - return finalPath - - altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource) - finalPath = os.path.join(altBase, name) - if os.path.exists(finalPath): - return finalPath - else: - raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), )) diff --git a/src/util/misc.py b/src/util/misc.py deleted file mode 100644 index 9b8d88c..0000000 --- a/src/util/misc.py +++ /dev/null @@ -1,900 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement - -import sys -import re -import cPickle - -import functools -import contextlib -import inspect - -import optparse -import traceback -import warnings -import string - - -class AnyData(object): - - pass - - -_indentationLevel = [0] - - -def log_call(logger): - - def log_call_decorator(func): - - @functools.wraps(func) - def wrapper(*args, **kwds): - logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, )) - _indentationLevel[0] += 1 - try: - return func(*args, **kwds) - finally: - _indentationLevel[0] -= 1 - logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, )) - - return wrapper - - return log_call_decorator - - -def log_exception(logger): - - def log_exception_decorator(func): - - @functools.wraps(func) - def wrapper(*args, **kwds): - try: - return func(*args, **kwds) - except Exception: - logger.exception(func.__name__) - raise - - return wrapper - - return log_exception_decorator - - -def printfmt(template): - """ - This hides having to create the Template object and call substitute/safe_substitute on it. For example: - - >>> num = 10 - >>> word = "spam" - >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP - I would like to order 10 units of spam, please - """ - frame = inspect.stack()[-1][0] - try: - print string.Template(template).safe_substitute(frame.f_locals) - finally: - del frame - - -def is_special(name): - return name.startswith("__") and name.endswith("__") - - -def is_private(name): - return name.startswith("_") and not is_special(name) - - -def privatize(clsName, attributeName): - """ - At runtime, make an attributeName private - - Example: - >>> class Test(object): - ... pass - ... - >>> try: - ... dir(Test).index("_Test__me") - ... print dir(Test) - ... except: - ... print "Not Found" - Not Found - >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World") - >>> try: - ... dir(Test).index("_Test__me") - ... print "Found" - ... except: - ... print dir(Test) - 0 - Found - >>> print getattr(Test, obfuscate(Test.__name__, "__me")) - Hello World - >>> - >>> is_private(privatize(Test.__name__, "me")) - True - >>> is_special(privatize(Test.__name__, "me")) - False - """ - return "".join(["_", clsName, "__", attributeName]) - - -def obfuscate(clsName, attributeName): - """ - At runtime, turn a private name into the obfuscated form - - Example: - >>> class Test(object): - ... __me = "Hello World" - ... - >>> try: - ... dir(Test).index("_Test__me") - ... print "Found" - ... except: - ... print dir(Test) - 0 - Found - >>> print getattr(Test, obfuscate(Test.__name__, "__me")) - Hello World - >>> is_private(obfuscate(Test.__name__, "__me")) - True - >>> is_special(obfuscate(Test.__name__, "__me")) - False - """ - return "".join(["_", clsName, attributeName]) - - -class PAOptionParser(optparse.OptionParser, object): - """ - >>> if __name__ == '__main__': - ... #parser = PAOptionParser("My usage str") - ... parser = PAOptionParser() - ... parser.add_posarg("Foo", help="Foo usage") - ... parser.add_posarg("Bar", dest="bar_dest") - ... parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other")) - ... parser.add_option('--stocksym', dest='symbol') - ... values, args = parser.parse_args() - ... print values, args - ... - - python mycp.py -h - python mycp.py - python mycp.py foo - python mycp.py foo bar - - python mycp.py foo bar lava - Usage: pa.py [options] - - Positional Arguments: - Foo: Foo usage - Bar: - Language: - - pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other' - """ - - def __init__(self, *args, **kw): - self.posargs = [] - super(PAOptionParser, self).__init__(*args, **kw) - - def add_posarg(self, *args, **kw): - pa_help = kw.get("help", "") - kw["help"] = optparse.SUPPRESS_HELP - o = self.add_option("--%s" % args[0], *args[1:], **kw) - self.posargs.append((args[0], pa_help)) - - def get_usage(self, *args, **kwargs): - params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs])) - self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params - return super(PAOptionParser, self).get_usage(*args, **kwargs) - - def parse_args(self, *args, **kwargs): - args = sys.argv[1:] - args0 = [] - for p, v in zip(self.posargs, args): - args0.append("--%s" % p[0]) - args0.append(v) - args = args0 + args - options, args = super(PAOptionParser, self).parse_args(args, **kwargs) - if len(args) < len(self.posargs): - msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):]) - self.error(msg) - return options, args - - -def explicitly(name, stackadd=0): - """ - This is an alias for adding to '__all__'. Less error-prone than using - __all__ itself, since setting __all__ directly is prone to stomping on - things implicitly exported via L{alias}. - - @note Taken from PyExport (which could turn out pretty cool): - @li @a http://codebrowse.launchpad.net/~glyph/ - @li @a http://glyf.livejournal.com/74356.html - """ - packageVars = sys._getframe(1+stackadd).f_locals - globalAll = packageVars.setdefault('__all__', []) - globalAll.append(name) - - -def public(thunk): - """ - This is a decorator, for convenience. Rather than typing the name of your - function twice, you can decorate a function with this. - - To be real, @public would need to work on methods as well, which gets into - supporting types... - - @note Taken from PyExport (which could turn out pretty cool): - @li @a http://codebrowse.launchpad.net/~glyph/ - @li @a http://glyf.livejournal.com/74356.html - """ - explicitly(thunk.__name__, 1) - return thunk - - -def _append_docstring(obj, message): - if obj.__doc__ is None: - obj.__doc__ = message - else: - obj.__doc__ += message - - -def validate_decorator(decorator): - - def simple(x): - return x - - f = simple - f.__name__ = "name" - f.__doc__ = "doc" - f.__dict__["member"] = True - - g = decorator(f) - - if f.__name__ != g.__name__: - print f.__name__, "!=", g.__name__ - - if g.__doc__ is None: - print decorator.__name__, "has no doc string" - elif not g.__doc__.startswith(f.__doc__): - print g.__doc__, "didn't start with", f.__doc__ - - if not ("member" in g.__dict__ and g.__dict__["member"]): - print "'member' not in ", g.__dict__ - - -def deprecated_api(func): - """ - This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used. - - >>> validate_decorator(deprecated_api) - """ - - @functools.wraps(func) - def newFunc(*args, **kwargs): - warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning) - return func(*args, **kwargs) - - _append_docstring(newFunc, "\n@deprecated") - return newFunc - - -def unstable_api(func): - """ - This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used. - - >>> validate_decorator(unstable_api) - """ - - @functools.wraps(func) - def newFunc(*args, **kwargs): - warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning) - return func(*args, **kwargs) - _append_docstring(newFunc, "\n@unstable") - return newFunc - - -def enabled(func): - """ - This decorator doesn't add any behavior - - >>> validate_decorator(enabled) - """ - return func - - -def disabled(func): - """ - This decorator disables the provided function, and does nothing - - >>> validate_decorator(disabled) - """ - - @functools.wraps(func) - def emptyFunc(*args, **kargs): - pass - _append_docstring(emptyFunc, "\n@note Temporarily Disabled") - return emptyFunc - - -def metadata(document=True, **kwds): - """ - >>> validate_decorator(metadata(author="Ed")) - """ - - def decorate(func): - for k, v in kwds.iteritems(): - setattr(func, k, v) - if document: - _append_docstring(func, "\n@"+k+" "+v) - return func - return decorate - - -def prop(func): - """Function decorator for defining property attributes - - The decorated function is expected to return a dictionary - containing one or more of the following pairs: - fget - function for getting attribute value - fset - function for setting attribute value - fdel - function for deleting attribute - This can be conveniently constructed by the locals() builtin - function; see: - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183 - @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html - - Example: - >>> #Due to transformation from function to property, does not need to be validated - >>> #validate_decorator(prop) - >>> class MyExampleClass(object): - ... @prop - ... def foo(): - ... "The foo property attribute's doc-string" - ... def fget(self): - ... print "GET" - ... return self._foo - ... def fset(self, value): - ... print "SET" - ... self._foo = value - ... return locals() - ... - >>> me = MyExampleClass() - >>> me.foo = 10 - SET - >>> print me.foo - GET - 10 - """ - return property(doc=func.__doc__, **func()) - - -def print_handler(e): - """ - @see ExpHandler - """ - print "%s: %s" % (type(e).__name__, e) - - -def print_ignore(e): - """ - @see ExpHandler - """ - print 'Ignoring %s exception: %s' % (type(e).__name__, e) - - -def print_traceback(e): - """ - @see ExpHandler - """ - #print sys.exc_info() - traceback.print_exc(file=sys.stdout) - - -def ExpHandler(handler = print_handler, *exceptions): - """ - An exception handling idiom using decorators - Examples - Specify exceptions in order, first one is handled first - last one last. - - >>> validate_decorator(ExpHandler()) - >>> @ExpHandler(print_ignore, ZeroDivisionError) - ... @ExpHandler(None, AttributeError, ValueError) - ... def f1(): - ... 1/0 - >>> @ExpHandler(print_traceback, ZeroDivisionError) - ... def f2(): - ... 1/0 - >>> @ExpHandler() - ... def f3(*pargs): - ... l = pargs - ... return l[10] - >>> @ExpHandler(print_traceback, ZeroDivisionError) - ... def f4(): - ... return 1 - >>> - >>> - >>> f1() - Ignoring ZeroDivisionError exception: integer division or modulo by zero - >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE - Traceback (most recent call last): - ... - ZeroDivisionError: integer division or modulo by zero - >>> f3() - IndexError: tuple index out of range - >>> f4() - 1 - """ - - def wrapper(f): - localExceptions = exceptions - if not localExceptions: - localExceptions = [Exception] - t = [(ex, handler) for ex in localExceptions] - t.reverse() - - def newfunc(t, *args, **kwargs): - ex, handler = t[0] - try: - if len(t) == 1: - return f(*args, **kwargs) - else: - #Recurse for embedded try/excepts - dec_func = functools.partial(newfunc, t[1:]) - dec_func = functools.update_wrapper(dec_func, f) - return dec_func(*args, **kwargs) - except ex, e: - return handler(e) - - dec_func = functools.partial(newfunc, t) - dec_func = functools.update_wrapper(dec_func, f) - return dec_func - return wrapper - - -def into_debugger(func): - """ - >>> validate_decorator(into_debugger) - """ - - @functools.wraps(func) - def newFunc(*args, **kwargs): - try: - return func(*args, **kwargs) - except: - import pdb - pdb.post_mortem() - - return newFunc - - -class bindclass(object): - """ - >>> validate_decorator(bindclass) - >>> class Foo(BoundObject): - ... @bindclass - ... def foo(this_class, self): - ... return this_class, self - ... - >>> class Bar(Foo): - ... @bindclass - ... def bar(this_class, self): - ... return this_class, self - ... - >>> f = Foo() - >>> b = Bar() - >>> - >>> f.foo() # doctest: +ELLIPSIS - (, <...Foo object at ...>) - >>> b.foo() # doctest: +ELLIPSIS - (, <...Bar object at ...>) - >>> b.bar() # doctest: +ELLIPSIS - (, <...Bar object at ...>) - """ - - def __init__(self, f): - self.f = f - self.__name__ = f.__name__ - self.__doc__ = f.__doc__ - self.__dict__.update(f.__dict__) - self.m = None - - def bind(self, cls, attr): - - def bound_m(*args, **kwargs): - return self.f(cls, *args, **kwargs) - bound_m.__name__ = attr - self.m = bound_m - - def __get__(self, obj, objtype=None): - return self.m.__get__(obj, objtype) - - -class ClassBindingSupport(type): - "@see bindclass" - - def __init__(mcs, name, bases, attrs): - type.__init__(mcs, name, bases, attrs) - for attr, val in attrs.iteritems(): - if isinstance(val, bindclass): - val.bind(mcs, attr) - - -class BoundObject(object): - "@see bindclass" - __metaclass__ = ClassBindingSupport - - -def bindfunction(f): - """ - >>> validate_decorator(bindfunction) - >>> @bindfunction - ... def factorial(thisfunction, n): - ... # Within this function the name 'thisfunction' refers to the factorial - ... # function(with only one argument), even after 'factorial' is bound - ... # to another object - ... if n > 0: - ... return n * thisfunction(n - 1) - ... else: - ... return 1 - ... - >>> factorial(3) - 6 - """ - - @functools.wraps(f) - def bound_f(*args, **kwargs): - return f(bound_f, *args, **kwargs) - return bound_f - - -class Memoize(object): - """ - Memoize(fn) - an instance which acts like fn but memoizes its arguments - Will only work on functions with non-mutable arguments - @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201 - - >>> validate_decorator(Memoize) - """ - - def __init__(self, fn): - self.fn = fn - self.__name__ = fn.__name__ - self.__doc__ = fn.__doc__ - self.__dict__.update(fn.__dict__) - self.memo = {} - - def __call__(self, *args): - if args not in self.memo: - self.memo[args] = self.fn(*args) - return self.memo[args] - - -class MemoizeMutable(object): - """Memoize(fn) - an instance which acts like fn but memoizes its arguments - Will work on functions with mutable arguments(slower than Memoize) - @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201 - - >>> validate_decorator(MemoizeMutable) - """ - - def __init__(self, fn): - self.fn = fn - self.__name__ = fn.__name__ - self.__doc__ = fn.__doc__ - self.__dict__.update(fn.__dict__) - self.memo = {} - - def __call__(self, *args, **kw): - text = cPickle.dumps((args, kw)) - if text not in self.memo: - self.memo[text] = self.fn(*args, **kw) - return self.memo[text] - - -callTraceIndentationLevel = 0 - - -def call_trace(f): - """ - Synchronization decorator. - - >>> validate_decorator(call_trace) - >>> @call_trace - ... def a(a, b, c): - ... pass - >>> a(1, 2, c=3) - Entering a((1, 2), {'c': 3}) - Exiting a((1, 2), {'c': 3}) - """ - - @functools.wraps(f) - def verboseTrace(*args, **kw): - global callTraceIndentationLevel - - print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) - callTraceIndentationLevel += 1 - try: - result = f(*args, **kw) - except: - callTraceIndentationLevel -= 1 - print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) - raise - callTraceIndentationLevel -= 1 - print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw) - return result - - @functools.wraps(f) - def smallTrace(*args, **kw): - global callTraceIndentationLevel - - print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__) - callTraceIndentationLevel += 1 - try: - result = f(*args, **kw) - except: - callTraceIndentationLevel -= 1 - print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__) - raise - callTraceIndentationLevel -= 1 - print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__) - return result - - #return smallTrace - return verboseTrace - - -@contextlib.contextmanager -def nested_break(): - """ - >>> with nested_break() as mylabel: - ... for i in xrange(3): - ... print "Outer", i - ... for j in xrange(3): - ... if i == 2: raise mylabel - ... if j == 2: break - ... print "Inner", j - ... print "more processing" - Outer 0 - Inner 0 - Inner 1 - Outer 1 - Inner 0 - Inner 1 - Outer 2 - """ - - class NestedBreakException(Exception): - pass - - try: - yield NestedBreakException - except NestedBreakException: - pass - - -@contextlib.contextmanager -def lexical_scope(*args): - """ - @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586 - Example: - >>> b = 0 - >>> with lexical_scope(1) as (a): - ... print a - ... - 1 - >>> with lexical_scope(1,2,3) as (a,b,c): - ... print a,b,c - ... - 1 2 3 - >>> with lexical_scope(): - ... d = 10 - ... def foo(): - ... pass - ... - >>> print b - 2 - """ - - frame = inspect.currentframe().f_back.f_back - saved = frame.f_locals.keys() - try: - if not args: - yield - elif len(args) == 1: - yield args[0] - else: - yield args - finally: - f_locals = frame.f_locals - for key in (x for x in f_locals.keys() if x not in saved): - del f_locals[key] - del frame - - -def normalize_number(prettynumber): - """ - function to take a phone number and strip out all non-numeric - characters - - >>> normalize_number("+012-(345)-678-90") - '+01234567890' - >>> normalize_number("1-(345)-678-9000") - '+13456789000' - >>> normalize_number("+1-(345)-678-9000") - '+13456789000' - """ - uglynumber = re.sub('[^0-9+]', '', prettynumber) - if uglynumber.startswith("+"): - pass - elif uglynumber.startswith("1"): - uglynumber = "+"+uglynumber - elif 10 <= len(uglynumber): - assert uglynumber[0] not in ("+", "1"), "Number format confusing" - uglynumber = "+1"+uglynumber - else: - pass - - return uglynumber - - -_VALIDATE_RE = re.compile("^\+?[0-9]{10,}$") - - -def is_valid_number(number): - """ - @returns If This number be called ( syntax validation only ) - """ - return _VALIDATE_RE.match(number) is not None - - -def make_ugly(prettynumber): - """ - function to take a phone number and strip out all non-numeric - characters - - >>> make_ugly("+012-(345)-678-90") - '+01234567890' - """ - return normalize_number(prettynumber) - - -def _make_pretty_with_areacode(phonenumber): - prettynumber = "(%s)" % (phonenumber[0:3], ) - if 3 < len(phonenumber): - prettynumber += " %s" % (phonenumber[3:6], ) - if 6 < len(phonenumber): - prettynumber += "-%s" % (phonenumber[6:], ) - return prettynumber - - -def _make_pretty_local(phonenumber): - prettynumber = "%s" % (phonenumber[0:3], ) - if 3 < len(phonenumber): - prettynumber += "-%s" % (phonenumber[3:], ) - return prettynumber - - -def _make_pretty_international(phonenumber): - prettynumber = phonenumber - if phonenumber.startswith("1"): - prettynumber = "1 " - prettynumber += _make_pretty_with_areacode(phonenumber[1:]) - return prettynumber - - -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") - '+1 (234) 567-8901' - >>> make_pretty("12345678901") - '+1 (234) 567-8901' - >>> make_pretty("01234567890") - '+012 (345) 678-90' - >>> make_pretty("+01234567890") - '+012 (345) 678-90' - >>> make_pretty("+12") - '+1 (2)' - >>> make_pretty("+123") - '+1 (23)' - >>> make_pretty("+1234") - '+1 (234)' - """ - if phonenumber is None or phonenumber == "": - return "" - - phonenumber = normalize_number(phonenumber) - - if phonenumber == "": - return "" - elif phonenumber[0] == "+": - prettynumber = _make_pretty_international(phonenumber[1:]) - if not prettynumber.startswith("+"): - prettynumber = "+"+prettynumber - elif 8 < len(phonenumber) and phonenumber[0] in ("1", ): - prettynumber = _make_pretty_international(phonenumber) - elif 7 < len(phonenumber): - prettynumber = _make_pretty_with_areacode(phonenumber) - elif 3 < len(phonenumber): - prettynumber = _make_pretty_local(phonenumber) - else: - prettynumber = phonenumber - return prettynumber.strip() - - -def similar_ugly_numbers(lhs, rhs): - return ( - lhs == rhs or - lhs[1:] == rhs and lhs.startswith("1") or - lhs[2:] == rhs and lhs.startswith("+1") or - lhs == rhs[1:] and rhs.startswith("1") or - lhs == rhs[2:] and rhs.startswith("+1") - ) - - -def abbrev_relative_date(date): - """ - >>> abbrev_relative_date("42 hours ago") - '42 h' - >>> abbrev_relative_date("2 days ago") - '2 d' - >>> abbrev_relative_date("4 weeks ago") - '4 w' - """ - parts = date.split(" ") - return "%s %s" % (parts[0], parts[1][0]) - - -def parse_version(versionText): - """ - >>> parse_version("0.5.2") - [0, 5, 2] - """ - return [ - int(number) - for number in versionText.split(".") - ] - - -def compare_versions(leftParsedVersion, rightParsedVersion): - """ - >>> compare_versions([0, 1, 2], [0, 1, 2]) - 0 - >>> compare_versions([0, 1, 2], [0, 1, 3]) - -1 - >>> compare_versions([0, 1, 2], [0, 2, 2]) - -1 - >>> compare_versions([0, 1, 2], [1, 1, 2]) - -1 - >>> compare_versions([0, 1, 3], [0, 1, 2]) - 1 - >>> compare_versions([0, 2, 2], [0, 1, 2]) - 1 - >>> compare_versions([1, 1, 2], [0, 1, 2]) - 1 - """ - for left, right in zip(leftParsedVersion, rightParsedVersion): - if left < right: - return -1 - elif right < left: - return 1 - else: - return 0 diff --git a/src/util/overloading.py b/src/util/overloading.py deleted file mode 100644 index 89cb738..0000000 --- a/src/util/overloading.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python -import new - -# Make the environment more like Python 3.0 -__metaclass__ = type -from itertools import izip as zip -import textwrap -import inspect - - -__all__ = [ - "AnyType", - "overloaded" -] - - -AnyType = object - - -class overloaded: - """ - Dynamically overloaded functions. - - This is an implementation of (dynamically, or run-time) overloaded - functions; also known as generic functions or multi-methods. - - The dispatch algorithm uses the types of all argument for dispatch, - similar to (compile-time) overloaded functions or methods in C++ and - Java. - - Most of the complexity in the algorithm comes from the need to support - subclasses in call signatures. For example, if an function is - registered for a signature (T1, T2), then a call with a signature (S1, - S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass - of T2, and there are no other more specific matches (see below). - - If there are multiple matches and one of those doesn't *dominate* all - others, the match is deemed ambiguous and an exception is raised. A - subtlety here: if, after removing the dominated matches, there are - still multiple matches left, but they all map to the same function, - then the match is not deemed ambiguous and that function is used. - Read the method find_func() below for details. - - @note Python 2.5 is required due to the use of predicates any() and all(). - @note only supports positional arguments - - @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514 - - >>> import misc - >>> misc.validate_decorator (overloaded) - >>> - >>> - >>> - >>> - >>> ################# - >>> #Basics, with reusing names and without - >>> @overloaded - ... def foo(x): - ... "prints x" - ... print x - ... - >>> @foo.register(int) - ... def foo(x): - ... "prints the hex representation of x" - ... print hex(x) - ... - >>> from types import DictType - >>> @foo.register(DictType) - ... def foo_dict(x): - ... "prints the keys of x" - ... print [k for k in x.iterkeys()] - ... - >>> #combines all of the doc strings to help keep track of the specializations - >>> foo.__doc__ # doctest: +ELLIPSIS - "prints x\\n\\n...overloading.foo ():\\n\\tprints the hex representation of x\\n\\n...overloading.foo_dict ():\\n\\tprints the keys of x" - >>> foo ("text") - text - >>> foo (10) #calling the specialized foo - 0xa - >>> foo ({3:5, 6:7}) #calling the specialization foo_dict - [3, 6] - >>> foo_dict ({3:5, 6:7}) #with using a unique name, you still have the option of calling the function directly - [3, 6] - >>> - >>> - >>> - >>> - >>> ################# - >>> #Multiple arguments, accessing the default, and function finding - >>> @overloaded - ... def two_arg (x, y): - ... print x,y - ... - >>> @two_arg.register(int, int) - ... def two_arg_int_int (x, y): - ... print hex(x), hex(y) - ... - >>> @two_arg.register(float, int) - ... def two_arg_float_int (x, y): - ... print x, hex(y) - ... - >>> @two_arg.register(int, float) - ... def two_arg_int_float (x, y): - ... print hex(x), y - ... - >>> two_arg.__doc__ # doctest: +ELLIPSIS - "...overloading.two_arg_int_int (, ):\\n\\n...overloading.two_arg_float_int (, ):\\n\\n...overloading.two_arg_int_float (, ):" - >>> two_arg(9, 10) - 0x9 0xa - >>> two_arg(9.0, 10) - 9.0 0xa - >>> two_arg(15, 16.0) - 0xf 16.0 - >>> two_arg.default_func(9, 10) - 9 10 - >>> two_arg.find_func ((int, float)) == two_arg_int_float - True - >>> (int, float) in two_arg - True - >>> (str, int) in two_arg - False - >>> - >>> - >>> - >>> ################# - >>> #wildcard - >>> @two_arg.register(AnyType, str) - ... def two_arg_any_str (x, y): - ... print x, y.lower() - ... - >>> two_arg("Hello", "World") - Hello world - >>> two_arg(500, "World") - 500 world - """ - - def __init__(self, default_func): - # Decorator to declare new overloaded function. - self.registry = {} - self.cache = {} - self.default_func = default_func - self.__name__ = self.default_func.__name__ - self.__doc__ = self.default_func.__doc__ - self.__dict__.update (self.default_func.__dict__) - - def __get__(self, obj, type=None): - if obj is None: - return self - return new.instancemethod(self, obj) - - def register(self, *types): - """ - Decorator to register an implementation for a specific set of types. - - .register(t1, t2)(f) is equivalent to .register_func((t1, t2), f). - """ - - def helper(func): - self.register_func(types, func) - - originalDoc = self.__doc__ if self.__doc__ is not None else "" - typeNames = ", ".join ([str(type) for type in types]) - typeNames = "".join ([func.__module__+".", func.__name__, " (", typeNames, "):"]) - overloadedDoc = "" - if func.__doc__ is not None: - overloadedDoc = textwrap.fill (func.__doc__, width=60, initial_indent="\t", subsequent_indent="\t") - self.__doc__ = "\n".join ([originalDoc, "", typeNames, overloadedDoc]).strip() - - new_func = func - - #Masking the function, so we want to take on its traits - if func.__name__ == self.__name__: - self.__dict__.update (func.__dict__) - new_func = self - return new_func - - return helper - - def register_func(self, types, func): - """Helper to register an implementation.""" - self.registry[tuple(types)] = func - self.cache = {} # Clear the cache (later we can optimize this). - - def __call__(self, *args): - """Call the overloaded function.""" - types = tuple(map(type, args)) - func = self.cache.get(types) - if func is None: - self.cache[types] = func = self.find_func(types) - return func(*args) - - def __contains__ (self, types): - return self.find_func(types) is not self.default_func - - def find_func(self, types): - """Find the appropriate overloaded function; don't call it. - - @note This won't work for old-style classes or classes without __mro__ - """ - func = self.registry.get(types) - if func is not None: - # Easy case -- direct hit in registry. - return func - - # Phillip Eby suggests to use issubclass() instead of __mro__. - # There are advantages and disadvantages. - - # I can't help myself -- this is going to be intense functional code. - # Find all possible candidate signatures. - mros = tuple(inspect.getmro(t) for t in types) - n = len(mros) - candidates = [sig for sig in self.registry - if len(sig) == n and - all(t in mro for t, mro in zip(sig, mros))] - - if not candidates: - # No match at all -- use the default function. - return self.default_func - elif len(candidates) == 1: - # Unique match -- that's an easy case. - return self.registry[candidates[0]] - - # More than one match -- weed out the subordinate ones. - - def dominates(dom, sub, - orders=tuple(dict((t, i) for i, t in enumerate(mro)) - for mro in mros)): - # Predicate to decide whether dom strictly dominates sub. - # Strict domination is defined as domination without equality. - # The arguments dom and sub are type tuples of equal length. - # The orders argument is a precomputed auxiliary data structure - # giving dicts of ordering information corresponding to the - # positions in the type tuples. - # A type d dominates a type s iff order[d] <= order[s]. - # A type tuple (d1, d2, ...) dominates a type tuple of equal length - # (s1, s2, ...) iff d1 dominates s1, d2 dominates s2, etc. - if dom is sub: - return False - return all(order[d] <= order[s] for d, s, order in zip(dom, sub, orders)) - - # I suppose I could inline dominates() but it wouldn't get any clearer. - candidates = [cand - for cand in candidates - if not any(dominates(dom, cand) for dom in candidates)] - if len(candidates) == 1: - # There's exactly one candidate left. - return self.registry[candidates[0]] - - # Perhaps these multiple candidates all have the same implementation? - funcs = set(self.registry[cand] for cand in candidates) - if len(funcs) == 1: - return funcs.pop() - - # No, the situation is irreducibly ambiguous. - raise TypeError("ambigous call; types=%r; candidates=%r" % - (types, candidates)) diff --git a/src/util/qore_utils.py b/src/util/qore_utils.py deleted file mode 100644 index 153558d..0000000 --- a/src/util/qore_utils.py +++ /dev/null @@ -1,99 +0,0 @@ -import logging - -import qt_compat -QtCore = qt_compat.QtCore - -import misc - - -_moduleLogger = logging.getLogger(__name__) - - -class QThread44(QtCore.QThread): - """ - This is to imitate QThread in Qt 4.4+ for when running on older version - See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong - (On Lucid I have Qt 4.7 and this is still an issue) - """ - - def __init__(self, parent = None): - QtCore.QThread.__init__(self, parent) - - def run(self): - self.exec_() - - -class _WorkerThread(QtCore.QObject): - - _taskComplete = qt_compat.Signal(object) - - def __init__(self, futureThread): - QtCore.QObject.__init__(self) - self._futureThread = futureThread - self._futureThread._addTask.connect(self._on_task_added) - self._taskComplete.connect(self._futureThread._on_task_complete) - - @qt_compat.Slot(object) - def _on_task_added(self, task): - self.__on_task_added(task) - - @misc.log_exception(_moduleLogger) - def __on_task_added(self, task): - if not self._futureThread._isRunning: - _moduleLogger.error("Dropping task") - - func, args, kwds, on_success, on_error = task - - try: - result = func(*args, **kwds) - isError = False - except Exception, e: - _moduleLogger.error("Error, passing it back to the main thread") - result = e - isError = True - - taskResult = on_success, on_error, isError, result - self._taskComplete.emit(taskResult) - - -class FutureThread(QtCore.QObject): - - _addTask = qt_compat.Signal(object) - - def __init__(self): - QtCore.QObject.__init__(self) - self._thread = QThread44() - self._isRunning = False - self._worker = _WorkerThread(self) - self._worker.moveToThread(self._thread) - - def start(self): - self._thread.start() - self._isRunning = True - - def stop(self): - self._isRunning = False - self._thread.quit() - - def add_task(self, func, args, kwds, on_success, on_error): - assert self._isRunning, "Task queue not started" - task = func, args, kwds, on_success, on_error - self._addTask.emit(task) - - @qt_compat.Slot(object) - def _on_task_complete(self, taskResult): - self.__on_task_complete(taskResult) - - @misc.log_exception(_moduleLogger) - def __on_task_complete(self, taskResult): - on_success, on_error, isError, result = taskResult - if not self._isRunning: - if isError: - _moduleLogger.error("Masking: %s" % (result, )) - isError = True - result = StopIteration("Cancelling all callbacks") - callback = on_success if not isError else on_error - try: - callback(result) - except Exception: - _moduleLogger.exception("Callback errored") diff --git a/src/util/qt_compat.py b/src/util/qt_compat.py deleted file mode 100644 index 2ab7fa4..0000000 --- a/src/util/qt_compat.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -#try: -# import PySide.QtCore as _QtCore -# QtCore = _QtCore -# USES_PYSIDE = True -#except ImportError: -if True: - import sip - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) - import PyQt4.QtCore as _QtCore - QtCore = _QtCore - USES_PYSIDE = False - - -def _pyside_import_module(moduleName): - pyside = __import__('PySide', globals(), locals(), [moduleName], -1) - return getattr(pyside, moduleName) - - -def _pyqt4_import_module(moduleName): - pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1) - return getattr(pyside, moduleName) - - -if USES_PYSIDE: - import_module = _pyside_import_module - - Signal = QtCore.Signal - Slot = QtCore.Slot - Property = QtCore.Property -else: - import_module = _pyqt4_import_module - - Signal = QtCore.pyqtSignal - Slot = QtCore.pyqtSlot - Property = QtCore.pyqtProperty - - -if __name__ == "__main__": - pass - diff --git a/src/util/qtpie.py b/src/util/qtpie.py deleted file mode 100755 index 6b77d5d..0000000 --- a/src/util/qtpie.py +++ /dev/null @@ -1,1094 +0,0 @@ -#!/usr/bin/env python - -import math -import logging - -import qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -import misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -_TWOPI = 2 * math.pi - - -def _radius_at(center, pos): - delta = pos - center - xDelta = delta.x() - yDelta = delta.y() - - radius = math.sqrt(xDelta ** 2 + yDelta ** 2) - return radius - - -def _angle_at(center, pos): - delta = pos - center - xDelta = delta.x() - yDelta = delta.y() - - radius = math.sqrt(xDelta ** 2 + yDelta ** 2) - angle = math.acos(xDelta / radius) - if 0 <= yDelta: - angle = _TWOPI - angle - - return angle - - -class QActionPieItem(object): - - def __init__(self, action, weight = 1): - self._action = action - self._weight = weight - - def action(self): - return self._action - - def setWeight(self, weight): - self._weight = weight - - def weight(self): - return self._weight - - def setEnabled(self, enabled = True): - self._action.setEnabled(enabled) - - def isEnabled(self): - return self._action.isEnabled() - - -class PieFiling(object): - - INNER_RADIUS_DEFAULT = 64 - OUTER_RADIUS_DEFAULT = 192 - - SELECTION_CENTER = -1 - SELECTION_NONE = -2 - - NULL_CENTER = QActionPieItem(QtGui.QAction(None)) - - def __init__(self): - self._innerRadius = self.INNER_RADIUS_DEFAULT - self._outerRadius = self.OUTER_RADIUS_DEFAULT - self._children = [] - self._center = self.NULL_CENTER - - self._cacheIndexToAngle = {} - self._cacheTotalWeight = 0 - - def insertItem(self, item, index = -1): - self._children.insert(index, item) - self._invalidate_cache() - - def removeItemAt(self, index): - item = self._children.pop(index) - self._invalidate_cache() - - def set_center(self, item): - if item is None: - item = self.NULL_CENTER - self._center = item - - def center(self): - return self._center - - def clear(self): - del self._children[:] - self._center = self.NULL_CENTER - self._invalidate_cache() - - def itemAt(self, index): - return self._children[index] - - def indexAt(self, center, point): - return self._angle_to_index(_angle_at(center, point)) - - def innerRadius(self): - return self._innerRadius - - def setInnerRadius(self, radius): - self._innerRadius = radius - - def outerRadius(self): - return self._outerRadius - - def setOuterRadius(self, radius): - self._outerRadius = radius - - def __iter__(self): - return iter(self._children) - - def __len__(self): - return len(self._children) - - def __getitem__(self, index): - return self._children[index] - - def _invalidate_cache(self): - self._cacheIndexToAngle.clear() - self._cacheTotalWeight = sum(child.weight() for child in self._children) - if self._cacheTotalWeight == 0: - self._cacheTotalWeight = 1 - - def _index_to_angle(self, index, isShifted): - key = index, isShifted - if key in self._cacheIndexToAngle: - return self._cacheIndexToAngle[key] - index = index % len(self._children) - - baseAngle = _TWOPI / self._cacheTotalWeight - - angle = math.pi / 2 - if isShifted: - if self._children: - angle -= (self._children[0].weight() * baseAngle) / 2 - else: - angle -= baseAngle / 2 - while angle < 0: - angle += _TWOPI - - for i, child in enumerate(self._children): - if index < i: - break - angle += child.weight() * baseAngle - while _TWOPI < angle: - angle -= _TWOPI - - self._cacheIndexToAngle[key] = angle - return angle - - def _angle_to_index(self, angle): - numChildren = len(self._children) - if numChildren == 0: - return self.SELECTION_CENTER - - baseAngle = _TWOPI / self._cacheTotalWeight - - iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2 - while iterAngle < 0: - iterAngle += _TWOPI - - oldIterAngle = iterAngle - for index, child in enumerate(self._children): - iterAngle += child.weight() * baseAngle - if oldIterAngle < angle and angle <= iterAngle: - return index - 1 if index != 0 else numChildren - 1 - elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle): - return index - 1 if index != 0 else numChildren - 1 - oldIterAngle = iterAngle - - -class PieArtist(object): - - ICON_SIZE_DEFAULT = 48 - - SHAPE_CIRCLE = "circle" - SHAPE_SQUARE = "square" - DEFAULT_SHAPE = SHAPE_SQUARE - - BACKGROUND_FILL = "fill" - BACKGROUND_NOFILL = "no fill" - - def __init__(self, filing, background = BACKGROUND_FILL): - self._filing = filing - - self._cachedOuterRadius = self._filing.outerRadius() - self._cachedInnerRadius = self._filing.innerRadius() - canvasSize = self._cachedOuterRadius * 2 + 1 - self._canvas = QtGui.QPixmap(canvasSize, canvasSize) - self._mask = None - self._backgroundState = background - self.palette = None - - def pieSize(self): - diameter = self._filing.outerRadius() * 2 + 1 - return QtCore.QSize(diameter, diameter) - - def centerSize(self): - painter = QtGui.QPainter(self._canvas) - text = self._filing.center().action().text() - fontMetrics = painter.fontMetrics() - if text: - textBoundingRect = fontMetrics.boundingRect(text) - else: - textBoundingRect = QtCore.QRect() - textWidth = textBoundingRect.width() - textHeight = textBoundingRect.height() - - return QtCore.QSize( - textWidth + self.ICON_SIZE_DEFAULT, - max(textHeight, self.ICON_SIZE_DEFAULT), - ) - - def show(self, palette): - self.palette = palette - - if ( - self._cachedOuterRadius != self._filing.outerRadius() or - self._cachedInnerRadius != self._filing.innerRadius() - ): - self._cachedOuterRadius = self._filing.outerRadius() - self._cachedInnerRadius = self._filing.innerRadius() - self._canvas = self._canvas.scaled(self.pieSize()) - - if self._mask is None: - self._mask = QtGui.QBitmap(self._canvas.size()) - self._mask.fill(QtCore.Qt.color0) - self._generate_mask(self._mask) - self._canvas.setMask(self._mask) - return self._mask - - def hide(self): - self.palette = None - - def paint(self, selectionIndex): - painter = QtGui.QPainter(self._canvas) - painter.setRenderHint(QtGui.QPainter.Antialiasing, True) - - self.paintPainter(selectionIndex, painter) - - return self._canvas - - def paintPainter(self, selectionIndex, painter): - adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1) - - numChildren = len(self._filing) - if numChildren == 0: - self._paint_center_background(painter, adjustmentRect, selectionIndex) - self._paint_center_foreground(painter, adjustmentRect, selectionIndex) - return self._canvas - else: - for i in xrange(len(self._filing)): - self._paint_slice_background(painter, adjustmentRect, i, selectionIndex) - - self._paint_center_background(painter, adjustmentRect, selectionIndex) - self._paint_center_foreground(painter, adjustmentRect, selectionIndex) - - for i in xrange(len(self._filing)): - self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex) - - def _generate_mask(self, mask): - """ - Specifies on the mask the shape of the pie menu - """ - painter = QtGui.QPainter(mask) - painter.setPen(QtCore.Qt.color1) - painter.setBrush(QtCore.Qt.color1) - if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: - painter.drawRect(mask.rect()) - elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: - painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1)) - else: - raise NotImplementedError(self.DEFAULT_SHAPE) - - def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex): - if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: - currentWidth = adjustmentRect.width() - newWidth = math.sqrt(2) * currentWidth - dx = (newWidth - currentWidth) / 2 - adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx) - elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: - pass - else: - raise NotImplementedError(self.DEFAULT_SHAPE) - - if self._backgroundState == self.BACKGROUND_NOFILL: - painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent)) - painter.setPen(self.palette.highlight().color()) - else: - if i == selectionIndex and self._filing[i].isEnabled(): - painter.setBrush(self.palette.highlight()) - painter.setPen(self.palette.highlight().color()) - else: - painter.setBrush(self.palette.window()) - painter.setPen(self.palette.window().color()) - - a = self._filing._index_to_angle(i, True) - b = self._filing._index_to_angle(i + 1, True) - if b < a: - b += _TWOPI - size = b - a - if size < 0: - size += _TWOPI - - startAngleInDeg = (a * 360 * 16) / _TWOPI - sizeInDeg = (size * 360 * 16) / _TWOPI - painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg)) - - def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex): - child = self._filing[i] - - a = self._filing._index_to_angle(i, True) - b = self._filing._index_to_angle(i + 1, True) - if b < a: - b += _TWOPI - middleAngle = (a + b) / 2 - averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2 - - sliceX = averageRadius * math.cos(middleAngle) - sliceY = - averageRadius * math.sin(middleAngle) - - piePos = adjustmentRect.center() - pieX = piePos.x() - pieY = piePos.y() - self._paint_label( - painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY - ) - - def _paint_label(self, painter, action, isSelected, x, y): - text = action.text() - fontMetrics = painter.fontMetrics() - if text: - textBoundingRect = fontMetrics.boundingRect(text) - else: - textBoundingRect = QtCore.QRect() - textWidth = textBoundingRect.width() - textHeight = textBoundingRect.height() - - icon = action.icon().pixmap( - QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT), - QtGui.QIcon.Normal, - QtGui.QIcon.On, - ) - iconWidth = icon.width() - iconHeight = icon.width() - averageWidth = (iconWidth + textWidth)/2 - if not icon.isNull(): - iconRect = QtCore.QRect( - x - averageWidth, - y - iconHeight/2, - iconWidth, - iconHeight, - ) - - painter.drawPixmap(iconRect, icon) - - if text: - if isSelected: - if action.isEnabled(): - pen = self.palette.highlightedText() - brush = self.palette.highlight() - else: - pen = self.palette.mid() - brush = self.palette.window() - else: - if action.isEnabled(): - pen = self.palette.windowText() - else: - pen = self.palette.mid() - brush = self.palette.window() - - leftX = x - averageWidth + iconWidth - topY = y + textHeight/2 - painter.setPen(pen.color()) - painter.setBrush(brush) - painter.drawText(leftX, topY, text) - - def _paint_center_background(self, painter, adjustmentRect, selectionIndex): - if self._backgroundState == self.BACKGROUND_NOFILL: - return - if len(self._filing) == 0: - if self._backgroundState == self.BACKGROUND_NOFILL: - painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent)) - else: - if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled(): - painter.setBrush(self.palette.highlight()) - else: - painter.setBrush(self.palette.window()) - painter.setPen(self.palette.mid().color()) - - painter.drawRect(adjustmentRect) - else: - dark = self.palette.mid().color() - light = self.palette.light().color() - if self._backgroundState == self.BACKGROUND_NOFILL: - background = QtGui.QBrush(QtCore.Qt.transparent) - else: - if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled(): - background = self.palette.highlight().color() - else: - background = self.palette.window().color() - - innerRadius = self._cachedInnerRadius - adjustmentCenterPos = adjustmentRect.center() - innerRect = QtCore.QRect( - adjustmentCenterPos.x() - innerRadius, - adjustmentCenterPos.y() - innerRadius, - innerRadius * 2 + 1, - innerRadius * 2 + 1, - ) - - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(background) - painter.drawPie(innerRect, 0, 360 * 16) - - if self.DEFAULT_SHAPE == self.SHAPE_SQUARE: - pass - elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE: - painter.setPen(QtGui.QPen(dark, 1)) - painter.setBrush(QtCore.Qt.NoBrush) - painter.drawEllipse(adjustmentRect) - else: - raise NotImplementedError(self.DEFAULT_SHAPE) - - def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex): - centerPos = adjustmentRect.center() - pieX = centerPos.x() - pieY = centerPos.y() - - x = pieX - y = pieY - - self._paint_label( - painter, - self._filing.center().action(), - selectionIndex == PieFiling.SELECTION_CENTER, - x, y - ) - - -class QPieDisplay(QtGui.QWidget): - - def __init__(self, filing, parent = None, flags = QtCore.Qt.Window): - QtGui.QWidget.__init__(self, parent, flags) - self._filing = filing - self._artist = PieArtist(self._filing) - self._selectionIndex = PieFiling.SELECTION_NONE - - def popup(self, pos): - self._update_selection(pos) - self.show() - - def sizeHint(self): - return self._artist.pieSize() - - @misc_utils.log_exception(_moduleLogger) - def showEvent(self, showEvent): - mask = self._artist.show(self.palette()) - self.setMask(mask) - - QtGui.QWidget.showEvent(self, showEvent) - - @misc_utils.log_exception(_moduleLogger) - def hideEvent(self, hideEvent): - self._artist.hide() - self._selectionIndex = PieFiling.SELECTION_NONE - QtGui.QWidget.hideEvent(self, hideEvent) - - @misc_utils.log_exception(_moduleLogger) - def paintEvent(self, paintEvent): - canvas = self._artist.paint(self._selectionIndex) - offset = (self.size() - canvas.size()) / 2 - - screen = QtGui.QPainter(self) - screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas) - - QtGui.QWidget.paintEvent(self, paintEvent) - - def selectAt(self, index): - oldIndex = self._selectionIndex - self._selectionIndex = index - if self.isVisible(): - self.update() - - -class QPieButton(QtGui.QWidget): - - activated = qt_compat.Signal(int) - highlighted = qt_compat.Signal(int) - canceled = qt_compat.Signal() - aboutToShow = qt_compat.Signal() - aboutToHide = qt_compat.Signal() - - BUTTON_RADIUS = 24 - DELAY = 250 - - def __init__(self, buttonSlice, parent = None, buttonSlices = None): - # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these? - # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues - QtGui.QWidget.__init__(self, parent) - self._cachedCenterPosition = self.rect().center() - - self._filing = PieFiling() - self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen) - self._selectionIndex = PieFiling.SELECTION_NONE - - self._buttonFiling = PieFiling() - self._buttonFiling.set_center(buttonSlice) - if buttonSlices is not None: - for slice in buttonSlices: - self._buttonFiling.insertItem(slice) - self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS) - self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL) - self._poppedUp = False - self._pressed = False - - self._delayPopupTimer = QtCore.QTimer() - self._delayPopupTimer.setInterval(self.DELAY) - self._delayPopupTimer.setSingleShot(True) - self._delayPopupTimer.timeout.connect(self._on_delayed_popup) - self._popupLocation = None - - self._mousePosition = None - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setSizePolicy( - QtGui.QSizePolicy( - QtGui.QSizePolicy.MinimumExpanding, - QtGui.QSizePolicy.MinimumExpanding, - ) - ) - - def insertItem(self, item, index = -1): - self._filing.insertItem(item, index) - - def removeItemAt(self, index): - self._filing.removeItemAt(index) - - def set_center(self, item): - self._filing.set_center(item) - - def set_button(self, item): - self.update() - - def clear(self): - self._filing.clear() - - def itemAt(self, index): - return self._filing.itemAt(index) - - def indexAt(self, point): - return self._filing.indexAt(self._cachedCenterPosition, point) - - def innerRadius(self): - return self._filing.innerRadius() - - def setInnerRadius(self, radius): - self._filing.setInnerRadius(radius) - - def outerRadius(self): - return self._filing.outerRadius() - - def setOuterRadius(self, radius): - self._filing.setOuterRadius(radius) - - def buttonRadius(self): - return self._buttonFiling.outerRadius() - - def setButtonRadius(self, radius): - self._buttonFiling.setOuterRadius(radius) - self._buttonFiling.setInnerRadius(radius / 2) - self._buttonArtist.show(self.palette()) - - def minimumSizeHint(self): - return self._buttonArtist.centerSize() - - @misc_utils.log_exception(_moduleLogger) - def mousePressEvent(self, mouseEvent): - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - self._mousePosition = lastMousePos - self._update_selection(self._cachedCenterPosition) - - self.highlighted.emit(self._selectionIndex) - - self._display.selectAt(self._selectionIndex) - self._pressed = True - self.update() - self._popupLocation = mouseEvent.globalPos() - self._delayPopupTimer.start() - - @misc_utils.log_exception(_moduleLogger) - def _on_delayed_popup(self): - assert self._popupLocation is not None, "Widget location abuse" - self._popup_child(self._popupLocation) - - @misc_utils.log_exception(_moduleLogger) - def mouseMoveEvent(self, mouseEvent): - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - if self._mousePosition is None: - # Absolute - self._update_selection(lastMousePos) - else: - # Relative - self._update_selection( - self._cachedCenterPosition + (lastMousePos - self._mousePosition), - ignoreOuter = True, - ) - - if lastSelection != self._selectionIndex: - self.highlighted.emit(self._selectionIndex) - self._display.selectAt(self._selectionIndex) - - if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive(): - self._on_delayed_popup() - - @misc_utils.log_exception(_moduleLogger) - def mouseReleaseEvent(self, mouseEvent): - self._delayPopupTimer.stop() - self._popupLocation = None - - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - if self._mousePosition is None: - # Absolute - self._update_selection(lastMousePos) - else: - # Relative - self._update_selection( - self._cachedCenterPosition + (lastMousePos - self._mousePosition), - ignoreOuter = True, - ) - self._mousePosition = None - - self._activate_at(self._selectionIndex) - self._pressed = False - self.update() - self._hide_child() - - @misc_utils.log_exception(_moduleLogger) - def keyPressEvent(self, keyEvent): - if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]: - self._popup_child(QtGui.QCursor.pos()) - if self._selectionIndex != len(self._filing) - 1: - nextSelection = self._selectionIndex + 1 - else: - nextSelection = 0 - self._select_at(nextSelection) - self._display.selectAt(self._selectionIndex) - elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]: - self._popup_child(QtGui.QCursor.pos()) - if 0 < self._selectionIndex: - nextSelection = self._selectionIndex - 1 - else: - nextSelection = len(self._filing) - 1 - self._select_at(nextSelection) - self._display.selectAt(self._selectionIndex) - elif keyEvent.key() in [QtCore.Qt.Key_Space]: - self._popup_child(QtGui.QCursor.pos()) - self._select_at(PieFiling.SELECTION_CENTER) - self._display.selectAt(self._selectionIndex) - elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]: - self._delayPopupTimer.stop() - self._popupLocation = None - self._activate_at(self._selectionIndex) - self._hide_child() - elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]: - self._delayPopupTimer.stop() - self._popupLocation = None - self._activate_at(PieFiling.SELECTION_NONE) - self._hide_child() - else: - QtGui.QWidget.keyPressEvent(self, keyEvent) - - @misc_utils.log_exception(_moduleLogger) - def resizeEvent(self, resizeEvent): - self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1) - QtGui.QWidget.resizeEvent(self, resizeEvent) - - @misc_utils.log_exception(_moduleLogger) - def showEvent(self, showEvent): - self._buttonArtist.show(self.palette()) - self._cachedCenterPosition = self.rect().center() - - QtGui.QWidget.showEvent(self, showEvent) - - @misc_utils.log_exception(_moduleLogger) - def hideEvent(self, hideEvent): - self._display.hide() - self._select_at(PieFiling.SELECTION_NONE) - QtGui.QWidget.hideEvent(self, hideEvent) - - @misc_utils.log_exception(_moduleLogger) - def paintEvent(self, paintEvent): - self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1) - if self._poppedUp: - selectionIndex = PieFiling.SELECTION_CENTER - else: - selectionIndex = PieFiling.SELECTION_NONE - - screen = QtGui.QStylePainter(self) - screen.setRenderHint(QtGui.QPainter.Antialiasing, True) - option = QtGui.QStyleOptionButton() - option.initFrom(self) - option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised - - screen.drawControl(QtGui.QStyle.CE_PushButton, option) - self._buttonArtist.paintPainter(selectionIndex, screen) - - QtGui.QWidget.paintEvent(self, paintEvent) - - def __iter__(self): - return iter(self._filing) - - def __len__(self): - return len(self._filing) - - def _popup_child(self, position): - self._poppedUp = True - self.aboutToShow.emit() - - self._delayPopupTimer.stop() - self._popupLocation = None - - position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius()) - self._display.move(position) - self._display.show() - - self.update() - - def _hide_child(self): - self._poppedUp = False - self.aboutToHide.emit() - self._display.hide() - self.update() - - def _select_at(self, index): - self._selectionIndex = index - - def _update_selection(self, lastMousePos, ignoreOuter = False): - radius = _radius_at(self._cachedCenterPosition, lastMousePos) - if radius < self._filing.innerRadius(): - self._select_at(PieFiling.SELECTION_CENTER) - elif radius <= self._filing.outerRadius() or ignoreOuter: - self._select_at(self.indexAt(lastMousePos)) - else: - self._select_at(PieFiling.SELECTION_NONE) - - def _activate_at(self, index): - if index == PieFiling.SELECTION_NONE: - self.canceled.emit() - return - elif index == PieFiling.SELECTION_CENTER: - child = self._filing.center() - else: - child = self.itemAt(index) - - if child.action().isEnabled(): - child.action().trigger() - self.activated.emit(index) - else: - self.canceled.emit() - - -class QPieMenu(QtGui.QWidget): - - activated = qt_compat.Signal(int) - highlighted = qt_compat.Signal(int) - canceled = qt_compat.Signal() - aboutToShow = qt_compat.Signal() - aboutToHide = qt_compat.Signal() - - def __init__(self, parent = None): - QtGui.QWidget.__init__(self, parent) - self._cachedCenterPosition = self.rect().center() - - self._filing = PieFiling() - self._artist = PieArtist(self._filing) - self._selectionIndex = PieFiling.SELECTION_NONE - - self._mousePosition = () - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - def popup(self, pos): - self._update_selection(pos) - self.show() - - def insertItem(self, item, index = -1): - self._filing.insertItem(item, index) - self.update() - - def removeItemAt(self, index): - self._filing.removeItemAt(index) - self.update() - - def set_center(self, item): - self._filing.set_center(item) - self.update() - - def clear(self): - self._filing.clear() - self.update() - - def itemAt(self, index): - return self._filing.itemAt(index) - - def indexAt(self, point): - return self._filing.indexAt(self._cachedCenterPosition, point) - - def innerRadius(self): - return self._filing.innerRadius() - - def setInnerRadius(self, radius): - self._filing.setInnerRadius(radius) - self.update() - - def outerRadius(self): - return self._filing.outerRadius() - - def setOuterRadius(self, radius): - self._filing.setOuterRadius(radius) - self.update() - - def sizeHint(self): - return self._artist.pieSize() - - @misc_utils.log_exception(_moduleLogger) - def mousePressEvent(self, mouseEvent): - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - self._update_selection(lastMousePos) - self._mousePosition = lastMousePos - - if lastSelection != self._selectionIndex: - self.highlighted.emit(self._selectionIndex) - self.update() - - @misc_utils.log_exception(_moduleLogger) - def mouseMoveEvent(self, mouseEvent): - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - self._update_selection(lastMousePos) - - if lastSelection != self._selectionIndex: - self.highlighted.emit(self._selectionIndex) - self.update() - - @misc_utils.log_exception(_moduleLogger) - def mouseReleaseEvent(self, mouseEvent): - lastSelection = self._selectionIndex - - lastMousePos = mouseEvent.pos() - self._update_selection(lastMousePos) - self._mousePosition = () - - self._activate_at(self._selectionIndex) - self.update() - - @misc_utils.log_exception(_moduleLogger) - def keyPressEvent(self, keyEvent): - if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]: - if self._selectionIndex != len(self._filing) - 1: - nextSelection = self._selectionIndex + 1 - else: - nextSelection = 0 - self._select_at(nextSelection) - self.update() - elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]: - if 0 < self._selectionIndex: - nextSelection = self._selectionIndex - 1 - else: - nextSelection = len(self._filing) - 1 - self._select_at(nextSelection) - self.update() - elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]: - self._activate_at(self._selectionIndex) - elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]: - self._activate_at(PieFiling.SELECTION_NONE) - else: - QtGui.QWidget.keyPressEvent(self, keyEvent) - - @misc_utils.log_exception(_moduleLogger) - def showEvent(self, showEvent): - self.aboutToShow.emit() - self._cachedCenterPosition = self.rect().center() - - mask = self._artist.show(self.palette()) - self.setMask(mask) - - lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos()) - self._update_selection(lastMousePos) - - QtGui.QWidget.showEvent(self, showEvent) - - @misc_utils.log_exception(_moduleLogger) - def hideEvent(self, hideEvent): - self._artist.hide() - self._selectionIndex = PieFiling.SELECTION_NONE - QtGui.QWidget.hideEvent(self, hideEvent) - - @misc_utils.log_exception(_moduleLogger) - def paintEvent(self, paintEvent): - canvas = self._artist.paint(self._selectionIndex) - - screen = QtGui.QPainter(self) - screen.drawPixmap(QtCore.QPoint(0, 0), canvas) - - QtGui.QWidget.paintEvent(self, paintEvent) - - def __iter__(self): - return iter(self._filing) - - def __len__(self): - return len(self._filing) - - def _select_at(self, index): - self._selectionIndex = index - - def _update_selection(self, lastMousePos): - radius = _radius_at(self._cachedCenterPosition, lastMousePos) - if radius < self._filing.innerRadius(): - self._selectionIndex = PieFiling.SELECTION_CENTER - elif radius <= self._filing.outerRadius(): - self._select_at(self.indexAt(lastMousePos)) - else: - self._selectionIndex = PieFiling.SELECTION_NONE - - def _activate_at(self, index): - if index == PieFiling.SELECTION_NONE: - self.canceled.emit() - self.aboutToHide.emit() - self.hide() - return - elif index == PieFiling.SELECTION_CENTER: - child = self._filing.center() - else: - child = self.itemAt(index) - - if child.isEnabled(): - child.action().trigger() - self.activated.emit(index) - else: - self.canceled.emit() - self.aboutToHide.emit() - self.hide() - - -def init_pies(): - PieFiling.NULL_CENTER.setEnabled(False) - - -def _print(msg): - print msg - - -def _on_about_to_hide(app): - app.exit() - - -if __name__ == "__main__": - app = QtGui.QApplication([]) - init_pies() - - if False: - pie = QPieMenu() - pie.show() - - if False: - singleAction = QtGui.QAction(None) - singleAction.setText("Boo") - singleItem = QActionPieItem(singleAction) - spie = QPieMenu() - spie.insertItem(singleItem) - spie.show() - - if False: - oneAction = QtGui.QAction(None) - oneAction.setText("Chew") - oneItem = QActionPieItem(oneAction) - twoAction = QtGui.QAction(None) - twoAction.setText("Foo") - twoItem = QActionPieItem(twoAction) - iconTextAction = QtGui.QAction(None) - iconTextAction.setText("Icon") - iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) - iconTextItem = QActionPieItem(iconTextAction) - mpie = QPieMenu() - mpie.insertItem(oneItem) - mpie.insertItem(twoItem) - mpie.insertItem(oneItem) - mpie.insertItem(iconTextItem) - mpie.show() - - if True: - oneAction = QtGui.QAction(None) - oneAction.setText("Chew") - oneAction.triggered.connect(lambda: _print("Chew")) - oneItem = QActionPieItem(oneAction) - twoAction = QtGui.QAction(None) - twoAction.setText("Foo") - twoAction.triggered.connect(lambda: _print("Foo")) - twoItem = QActionPieItem(twoAction) - iconAction = QtGui.QAction(None) - iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) - iconAction.triggered.connect(lambda: _print("Icon")) - iconItem = QActionPieItem(iconAction) - iconTextAction = QtGui.QAction(None) - iconTextAction.setText("Icon") - iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) - iconTextAction.triggered.connect(lambda: _print("Icon and text")) - iconTextItem = QActionPieItem(iconTextAction) - mpie = QPieMenu() - mpie.set_center(iconItem) - mpie.insertItem(oneItem) - mpie.insertItem(twoItem) - mpie.insertItem(oneItem) - mpie.insertItem(iconTextItem) - mpie.show() - mpie.aboutToHide.connect(lambda: _on_about_to_hide(app)) - mpie.canceled.connect(lambda: _print("Canceled")) - - if False: - oneAction = QtGui.QAction(None) - oneAction.setText("Chew") - oneAction.triggered.connect(lambda: _print("Chew")) - oneItem = QActionPieItem(oneAction) - twoAction = QtGui.QAction(None) - twoAction.setText("Foo") - twoAction.triggered.connect(lambda: _print("Foo")) - twoItem = QActionPieItem(twoAction) - iconAction = QtGui.QAction(None) - iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) - iconAction.triggered.connect(lambda: _print("Icon")) - iconItem = QActionPieItem(iconAction) - iconTextAction = QtGui.QAction(None) - iconTextAction.setText("Icon") - iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) - iconTextAction.triggered.connect(lambda: _print("Icon and text")) - iconTextItem = QActionPieItem(iconTextAction) - pieFiling = PieFiling() - pieFiling.set_center(iconItem) - pieFiling.insertItem(oneItem) - pieFiling.insertItem(twoItem) - pieFiling.insertItem(oneItem) - pieFiling.insertItem(iconTextItem) - mpie = QPieDisplay(pieFiling) - mpie.show() - - if False: - oneAction = QtGui.QAction(None) - oneAction.setText("Chew") - oneAction.triggered.connect(lambda: _print("Chew")) - oneItem = QActionPieItem(oneAction) - twoAction = QtGui.QAction(None) - twoAction.setText("Foo") - twoAction.triggered.connect(lambda: _print("Foo")) - twoItem = QActionPieItem(twoAction) - iconAction = QtGui.QAction(None) - iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open")) - iconAction.triggered.connect(lambda: _print("Icon")) - iconItem = QActionPieItem(iconAction) - iconTextAction = QtGui.QAction(None) - iconTextAction.setText("Icon") - iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close")) - iconTextAction.triggered.connect(lambda: _print("Icon and text")) - iconTextItem = QActionPieItem(iconTextAction) - mpie = QPieButton(iconItem) - mpie.set_center(iconItem) - mpie.insertItem(oneItem) - mpie.insertItem(twoItem) - mpie.insertItem(oneItem) - mpie.insertItem(iconTextItem) - mpie.show() - mpie.aboutToHide.connect(lambda: _on_about_to_hide(app)) - mpie.canceled.connect(lambda: _print("Canceled")) - - app.exec_() diff --git a/src/util/qtpieboard.py b/src/util/qtpieboard.py deleted file mode 100755 index 50ae9ae..0000000 --- a/src/util/qtpieboard.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python - - -from __future__ import division - -import os -import warnings - -import qt_compat -QtGui = qt_compat.import_module("QtGui") - -import qtpie - - -class PieKeyboard(object): - - SLICE_CENTER = -1 - SLICE_NORTH = 0 - SLICE_NORTH_WEST = 1 - SLICE_WEST = 2 - SLICE_SOUTH_WEST = 3 - SLICE_SOUTH = 4 - SLICE_SOUTH_EAST = 5 - SLICE_EAST = 6 - SLICE_NORTH_EAST = 7 - - MAX_ANGULAR_SLICES = 8 - - SLICE_DIRECTIONS = [ - SLICE_CENTER, - SLICE_NORTH, - SLICE_NORTH_WEST, - SLICE_WEST, - SLICE_SOUTH_WEST, - SLICE_SOUTH, - SLICE_SOUTH_EAST, - SLICE_EAST, - SLICE_NORTH_EAST, - ] - - SLICE_DIRECTION_NAMES = [ - "CENTER", - "NORTH", - "NORTH_WEST", - "WEST", - "SOUTH_WEST", - "SOUTH", - "SOUTH_EAST", - "EAST", - "NORTH_EAST", - ] - - def __init__(self): - self._layout = QtGui.QGridLayout() - self._widget = QtGui.QWidget() - self._widget.setLayout(self._layout) - - self.__cells = {} - - @property - def toplevel(self): - return self._widget - - def add_pie(self, row, column, pieButton): - assert len(pieButton) == 8 - self._layout.addWidget(pieButton, row, column) - self.__cells[(row, column)] = pieButton - - def get_pie(self, row, column): - return self.__cells[(row, column)] - - -class KeyboardModifier(object): - - def __init__(self, name): - self.name = name - self.lock = False - self.once = False - - @property - def isActive(self): - return self.lock or self.once - - def on_toggle_lock(self, *args, **kwds): - self.lock = not self.lock - - def on_toggle_once(self, *args, **kwds): - self.once = not self.once - - def reset_once(self): - self.once = False - - -def parse_keyboard_data(text): - return eval(text) - - -def _enumerate_pie_slices(pieData, iconPaths): - for direction, directionName in zip( - PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES - ): - if directionName in pieData: - sliceData = pieData[directionName] - - action = QtGui.QAction(None) - try: - action.setText(sliceData["text"]) - except KeyError: - pass - try: - relativeIconPath = sliceData["path"] - except KeyError: - pass - else: - for iconPath in iconPaths: - absIconPath = os.path.join(iconPath, relativeIconPath) - if os.path.exists(absIconPath): - action.setIcon(QtGui.QIcon(absIconPath)) - break - pieItem = qtpie.QActionPieItem(action) - actionToken = sliceData["action"] - else: - pieItem = qtpie.PieFiling.NULL_CENTER - actionToken = "" - yield direction, pieItem, actionToken - - -def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths): - for (row, column), pieData in dataTree.iteritems(): - pieItems = list(_enumerate_pie_slices(pieData, iconPaths)) - assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0] - _, center, centerAction = pieItems.pop(0) - - pieButton = qtpie.QPieButton(center) - pieButton.set_center(center) - keyboardHandler.map_slice_action(center, centerAction) - for direction, pieItem, action in pieItems: - pieButton.insertItem(pieItem) - keyboardHandler.map_slice_action(pieItem, action) - keyboard.add_pie(row, column, pieButton) - - -class KeyboardHandler(object): - - def __init__(self, keyhandler): - self.__keyhandler = keyhandler - self.__commandHandlers = {} - self.__modifiers = {} - self.__sliceActions = {} - - self.register_modifier("Shift") - self.register_modifier("Super") - self.register_modifier("Control") - self.register_modifier("Alt") - - def register_command_handler(self, command, handler): - # @todo Look into hooking these up directly to the pie actions - self.__commandHandlers["[%s]" % command] = handler - - def unregister_command_handler(self, command): - # @todo Look into hooking these up directly to the pie actions - del self.__commandHandlers["[%s]" % command] - - def register_modifier(self, modifierName): - mod = KeyboardModifier(modifierName) - self.register_command_handler(modifierName, mod.on_toggle_lock) - self.__modifiers["<%s>" % modifierName] = mod - - def unregister_modifier(self, modifierName): - self.unregister_command_handler(modifierName) - del self.__modifiers["<%s>" % modifierName] - - def map_slice_action(self, slice, action): - callback = lambda direction: self(direction, action) - slice.action().triggered.connect(callback) - self.__sliceActions[slice] = (action, callback) - - def __call__(self, direction, action): - activeModifiers = [ - mod.name - for mod in self.__modifiers.itervalues() - if mod.isActive - ] - - needResetOnce = False - if action.startswith("[") and action.endswith("]"): - commandName = action[1:-1] - if action in self.__commandHandlers: - self.__commandHandlers[action](commandName, activeModifiers) - needResetOnce = True - else: - warnings.warn("Unknown command: [%s]" % commandName) - elif action.startswith("<") and action.endswith(">"): - modName = action[1:-1] - for mod in self.__modifiers.itervalues(): - if mod.name == modName: - mod.on_toggle_once() - break - else: - warnings.warn("Unknown modifier: <%s>" % modName) - else: - self.__keyhandler(action, activeModifiers) - needResetOnce = True - - if needResetOnce: - for mod in self.__modifiers.itervalues(): - mod.reset_once() diff --git a/src/util/qui_utils.py b/src/util/qui_utils.py deleted file mode 100644 index 11b3453..0000000 --- a/src/util/qui_utils.py +++ /dev/null @@ -1,419 +0,0 @@ -import sys -import contextlib -import datetime -import logging - -import qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -import misc - - -_moduleLogger = logging.getLogger(__name__) - - -@contextlib.contextmanager -def notify_error(log): - try: - yield - except: - log.push_exception() - - -@contextlib.contextmanager -def notify_busy(log, message): - log.push_busy(message) - try: - yield - finally: - log.pop(message) - - -class ErrorMessage(object): - - LEVEL_ERROR = 0 - LEVEL_BUSY = 1 - LEVEL_INFO = 2 - - def __init__(self, message, level): - self._message = message - self._level = level - self._time = datetime.datetime.now() - - @property - def level(self): - return self._level - - @property - def message(self): - return self._message - - def __repr__(self): - return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level) - - -class QErrorLog(QtCore.QObject): - - messagePushed = qt_compat.Signal() - messagePopped = qt_compat.Signal() - - def __init__(self): - QtCore.QObject.__init__(self) - self._messages = [] - - def push_busy(self, message): - _moduleLogger.info("Entering state: %s" % message) - self._push_message(message, ErrorMessage.LEVEL_BUSY) - - def push_message(self, message): - self._push_message(message, ErrorMessage.LEVEL_INFO) - - def push_error(self, message): - self._push_message(message, ErrorMessage.LEVEL_ERROR) - - def push_exception(self): - userMessage = str(sys.exc_info()[1]) - _moduleLogger.exception(userMessage) - self.push_error(userMessage) - - def pop(self, message = None): - if message is None: - del self._messages[0] - else: - _moduleLogger.info("Exiting state: %s" % message) - messageIndex = [ - i - for (i, error) in enumerate(self._messages) - if error.message == message - ] - # Might be removed out of order - if messageIndex: - del self._messages[messageIndex[0]] - self.messagePopped.emit() - - def peek_message(self): - return self._messages[0] - - def _push_message(self, message, level): - self._messages.append(ErrorMessage(message, level)) - # Sort is defined as stable, so this should be fine - self._messages.sort(key=lambda x: x.level) - self.messagePushed.emit() - - def __len__(self): - return len(self._messages) - - -class ErrorDisplay(object): - - _SENTINEL_ICON = QtGui.QIcon() - - def __init__(self, errorLog): - self._errorLog = errorLog - self._errorLog.messagePushed.connect(self._on_message_pushed) - self._errorLog.messagePopped.connect(self._on_message_popped) - - self._icons = None - self._severityLabel = QtGui.QLabel() - self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) - - self._message = QtGui.QLabel() - self._message.setText("Boo") - self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) - self._message.setWordWrap(True) - - self._closeLabel = None - - self._controlLayout = QtGui.QHBoxLayout() - self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter) - self._controlLayout.addWidget(self._message, 1000) - - self._widget = QtGui.QWidget() - self._widget.setLayout(self._controlLayout) - self._widget.hide() - - @property - def toplevel(self): - return self._widget - - def _show_error(self): - if self._icons is None: - self._icons = { - ErrorMessage.LEVEL_BUSY: - get_theme_icon( - #("process-working", "view-refresh", "general_refresh", "gtk-refresh") - ("view-refresh", "general_refresh", "gtk-refresh", ) - ).pixmap(32, 32), - ErrorMessage.LEVEL_INFO: - get_theme_icon( - ("dialog-information", "general_notes", "gtk-info") - ).pixmap(32, 32), - ErrorMessage.LEVEL_ERROR: - get_theme_icon( - ("dialog-error", "app_install_error", "gtk-dialog-error") - ).pixmap(32, 32), - } - if self._closeLabel is None: - closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON) - if closeIcon is not self._SENTINEL_ICON: - self._closeLabel = QtGui.QPushButton(closeIcon, "") - else: - self._closeLabel = QtGui.QPushButton("X") - self._closeLabel.clicked.connect(self._on_close) - self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter) - error = self._errorLog.peek_message() - self._message.setText(error.message) - self._severityLabel.setPixmap(self._icons[error.level]) - self._widget.show() - - @qt_compat.Slot() - @qt_compat.Slot(bool) - @misc.log_exception(_moduleLogger) - def _on_close(self, checked = False): - self._errorLog.pop() - - @qt_compat.Slot() - @misc.log_exception(_moduleLogger) - def _on_message_pushed(self): - self._show_error() - - @qt_compat.Slot() - @misc.log_exception(_moduleLogger) - def _on_message_popped(self): - if len(self._errorLog) == 0: - self._message.setText("") - self._widget.hide() - else: - self._show_error() - - -class QHtmlDelegate(QtGui.QStyledItemDelegate): - - UNDEFINED_SIZE = -1 - - def __init__(self, *args, **kwd): - QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd) - self._width = self.UNDEFINED_SIZE - - def paint(self, painter, option, index): - newOption = QtGui.QStyleOptionViewItemV4(option) - self.initStyleOption(newOption, index) - if newOption.widget is not None: - style = newOption.widget.style() - else: - style = QtGui.QApplication.style() - - doc = QtGui.QTextDocument() - doc.setHtml(newOption.text) - doc.setTextWidth(newOption.rect.width()) - - newOption.text = "" - style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter) - - ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() - if newOption.state & QtGui.QStyle.State_Selected: - ctx.palette.setColor( - QtGui.QPalette.Text, - newOption.palette.color( - QtGui.QPalette.Active, - QtGui.QPalette.HighlightedText - ) - ) - else: - ctx.palette.setColor( - QtGui.QPalette.Text, - newOption.palette.color( - QtGui.QPalette.Active, - QtGui.QPalette.Text - ) - ) - - textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption) - painter.save() - painter.translate(textRect.topLeft()) - painter.setClipRect(textRect.translated(-textRect.topLeft())) - doc.documentLayout().draw(painter, ctx) - painter.restore() - - def setWidth(self, width, model): - if self._width == width: - return - self._width = width - for c in xrange(model.rowCount()): - cItem = model.item(c, 0) - for r in xrange(model.rowCount()): - rItem = cItem.child(r, 0) - rIndex = model.indexFromItem(rItem) - self.sizeHintChanged.emit(rIndex) - return - - def sizeHint(self, option, index): - newOption = QtGui.QStyleOptionViewItemV4(option) - self.initStyleOption(newOption, index) - - doc = QtGui.QTextDocument() - doc.setHtml(newOption.text) - if self._width != self.UNDEFINED_SIZE: - width = self._width - else: - width = newOption.rect.width() - doc.setTextWidth(width) - size = QtCore.QSize(doc.idealWidth(), doc.size().height()) - return size - - -class QSignalingMainWindow(QtGui.QMainWindow): - - closed = qt_compat.Signal() - hidden = qt_compat.Signal() - shown = qt_compat.Signal() - resized = qt_compat.Signal() - - def __init__(self, *args, **kwd): - QtGui.QMainWindow.__init__(*((self, )+args), **kwd) - - def closeEvent(self, event): - val = QtGui.QMainWindow.closeEvent(self, event) - self.closed.emit() - return val - - def hideEvent(self, event): - val = QtGui.QMainWindow.hideEvent(self, event) - self.hidden.emit() - return val - - def showEvent(self, event): - val = QtGui.QMainWindow.showEvent(self, event) - self.shown.emit() - return val - - def resizeEvent(self, event): - val = QtGui.QMainWindow.resizeEvent(self, event) - self.resized.emit() - return val - -def set_current_index(selector, itemText, default = 0): - for i in xrange(selector.count()): - if selector.itemText(i) == itemText: - selector.setCurrentIndex(i) - break - else: - itemText.setCurrentIndex(default) - - -def _null_set_stackable(window, isStackable): - pass - - -def _maemo_set_stackable(window, isStackable): - window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) - - -try: - QtCore.Qt.WA_Maemo5StackedWindow - set_stackable = _maemo_set_stackable -except AttributeError: - set_stackable = _null_set_stackable - - -def _null_set_autorient(window, doAutoOrient): - pass - - -def _maemo_set_autorient(window, doAutoOrient): - window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient) - - -try: - QtCore.Qt.WA_Maemo5AutoOrientation - set_autorient = _maemo_set_autorient -except AttributeError: - set_autorient = _null_set_autorient - - -def screen_orientation(): - geom = QtGui.QApplication.desktop().screenGeometry() - if geom.width() <= geom.height(): - return QtCore.Qt.Vertical - else: - return QtCore.Qt.Horizontal - - -def _null_set_window_orientation(window, orientation): - pass - - -def _maemo_set_window_orientation(window, orientation): - if orientation == QtCore.Qt.Vertical: - window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False) - window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True) - elif orientation == QtCore.Qt.Horizontal: - window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True) - window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False) - elif orientation is None: - window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False) - window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False) - else: - raise RuntimeError("Unknown orientation: %r" % orientation) - - -try: - QtCore.Qt.WA_Maemo5LandscapeOrientation - QtCore.Qt.WA_Maemo5PortraitOrientation - set_window_orientation = _maemo_set_window_orientation -except AttributeError: - set_window_orientation = _null_set_window_orientation - - -def _null_show_progress_indicator(window, isStackable): - pass - - -def _maemo_show_progress_indicator(window, isStackable): - window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable) - - -try: - QtCore.Qt.WA_Maemo5ShowProgressIndicator - show_progress_indicator = _maemo_show_progress_indicator -except AttributeError: - show_progress_indicator = _null_show_progress_indicator - - -def _null_mark_numbers_preferred(widget): - pass - - -def _newqt_mark_numbers_preferred(widget): - widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers) - - -try: - QtCore.Qt.ImhPreferNumbers - mark_numbers_preferred = _newqt_mark_numbers_preferred -except AttributeError: - mark_numbers_preferred = _null_mark_numbers_preferred - - -def _null_get_theme_icon(iconNames, fallback = None): - icon = fallback if fallback is not None else QtGui.QIcon() - return icon - - -def _newqt_get_theme_icon(iconNames, fallback = None): - for iconName in iconNames: - if QtGui.QIcon.hasThemeIcon(iconName): - icon = QtGui.QIcon.fromTheme(iconName) - break - else: - icon = fallback if fallback is not None else QtGui.QIcon() - return icon - - -try: - QtGui.QIcon.fromTheme - get_theme_icon = _newqt_get_theme_icon -except AttributeError: - get_theme_icon = _null_get_theme_icon - diff --git a/src/util/qwrappers.py b/src/util/qwrappers.py deleted file mode 100644 index 2c50c8a..0000000 --- a/src/util/qwrappers.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python - -from __future__ import with_statement -from __future__ import division - -import logging - -import qt_compat -QtCore = qt_compat.QtCore -QtGui = qt_compat.import_module("QtGui") - -from util import qui_utils -from util import misc as misc_utils - - -_moduleLogger = logging.getLogger(__name__) - - -class ApplicationWrapper(object): - - DEFAULT_ORIENTATION = "Default" - AUTO_ORIENTATION = "Auto" - LANDSCAPE_ORIENTATION = "Landscape" - PORTRAIT_ORIENTATION = "Portrait" - - def __init__(self, qapp, constants): - self._constants = constants - self._qapp = qapp - self._clipboard = QtGui.QApplication.clipboard() - - self._errorLog = qui_utils.QErrorLog() - self._mainWindow = None - - self._fullscreenAction = QtGui.QAction(None) - self._fullscreenAction.setText("Fullscreen") - self._fullscreenAction.setCheckable(True) - self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter")) - self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen) - - self._orientation = self.DEFAULT_ORIENTATION - self._orientationAction = QtGui.QAction(None) - self._orientationAction.setText("Next Orientation") - self._orientationAction.setCheckable(True) - self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o")) - self._orientationAction.triggered.connect(self._on_next_orientation) - - self._logAction = QtGui.QAction(None) - self._logAction.setText("Log") - self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l")) - self._logAction.triggered.connect(self._on_log) - - self._quitAction = QtGui.QAction(None) - self._quitAction.setText("Quit") - self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q")) - self._quitAction.triggered.connect(self._on_quit) - - self._aboutAction = QtGui.QAction(None) - self._aboutAction.setText("About") - self._aboutAction.triggered.connect(self._on_about) - - self._qapp.lastWindowClosed.connect(self._on_app_quit) - self._mainWindow = self._new_main_window() - self._mainWindow.window.destroyed.connect(self._on_child_close) - - self.load_settings() - - self._mainWindow.show() - self._idleDelay = QtCore.QTimer() - self._idleDelay.setSingleShot(True) - self._idleDelay.setInterval(0) - self._idleDelay.timeout.connect(self._on_delayed_start) - self._idleDelay.start() - - def load_settings(self): - raise NotImplementedError("Booh") - - def save_settings(self): - raise NotImplementedError("Booh") - - def _new_main_window(self): - raise NotImplementedError("Booh") - - @property - def qapp(self): - return self._qapp - - @property - def constants(self): - return self._constants - - @property - def errorLog(self): - return self._errorLog - - @property - def fullscreenAction(self): - return self._fullscreenAction - - @property - def orientationAction(self): - return self._orientationAction - - @property - def orientation(self): - return self._orientation - - @property - def logAction(self): - return self._logAction - - @property - def aboutAction(self): - return self._aboutAction - - @property - def quitAction(self): - return self._quitAction - - def set_orientation(self, orientation): - self._orientation = orientation - self._mainWindow.update_orientation(self._orientation) - - @classmethod - def _next_orientation(cls, current): - return { - cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION, - cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION, - cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION, - cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION, - }[current] - - def _close_windows(self): - if self._mainWindow is not None: - self.save_settings() - self._mainWindow.window.destroyed.disconnect(self._on_child_close) - self._mainWindow.close() - self._mainWindow = None - - @misc_utils.log_exception(_moduleLogger) - def _on_delayed_start(self): - self._mainWindow.start() - - @misc_utils.log_exception(_moduleLogger) - def _on_app_quit(self, checked = False): - if self._mainWindow is not None: - self.save_settings() - self._mainWindow.destroy() - - @misc_utils.log_exception(_moduleLogger) - def _on_child_close(self, obj = None): - if self._mainWindow is not None: - self.save_settings() - self._mainWindow = None - - @misc_utils.log_exception(_moduleLogger) - def _on_toggle_fullscreen(self, checked = False): - with qui_utils.notify_error(self._errorLog): - self._mainWindow.set_fullscreen(checked) - - @misc_utils.log_exception(_moduleLogger) - def _on_next_orientation(self, checked = False): - with qui_utils.notify_error(self._errorLog): - self.set_orientation(self._next_orientation(self._orientation)) - - @misc_utils.log_exception(_moduleLogger) - def _on_about(self, checked = True): - raise NotImplementedError("Booh") - - @misc_utils.log_exception(_moduleLogger) - def _on_log(self, checked = False): - with qui_utils.notify_error(self._errorLog): - with open(self._constants._user_logpath_, "r") as f: - logLines = f.xreadlines() - log = "".join(logLines) - self._clipboard.setText(log) - - @misc_utils.log_exception(_moduleLogger) - def _on_quit(self, checked = False): - with qui_utils.notify_error(self._errorLog): - self._close_windows() - - -class WindowWrapper(object): - - def __init__(self, parent, app): - self._app = app - - self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog) - - self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight) - self._layout.setContentsMargins(0, 0, 0, 0) - - self._superLayout = QtGui.QVBoxLayout() - self._superLayout.addWidget(self._errorDisplay.toplevel) - self._superLayout.setContentsMargins(0, 0, 0, 0) - self._superLayout.addLayout(self._layout) - - centralWidget = QtGui.QWidget() - centralWidget.setLayout(self._superLayout) - centralWidget.setContentsMargins(0, 0, 0, 0) - - self._window = qui_utils.QSignalingMainWindow(parent) - self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - qui_utils.set_stackable(self._window, True) - self._window.setCentralWidget(centralWidget) - - self._closeWindowAction = QtGui.QAction(None) - self._closeWindowAction.setText("Close") - self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) - self._closeWindowAction.triggered.connect(self._on_close_window) - - self._window.addAction(self._closeWindowAction) - self._window.addAction(self._app.quitAction) - self._window.addAction(self._app.fullscreenAction) - self._window.addAction(self._app.orientationAction) - self._window.addAction(self._app.logAction) - - @property - def window(self): - return self._window - - @property - def windowOrientation(self): - geom = self._window.size() - if geom.width() <= geom.height(): - return QtCore.Qt.Vertical - else: - return QtCore.Qt.Horizontal - - @property - def idealWindowOrientation(self): - if self._app.orientation == self._app.AUTO_ORIENTATION: - windowOrientation = self.windowOrientation - elif self._app.orientation == self._app.DEFAULT_ORIENTATION: - windowOrientation = qui_utils.screen_orientation() - elif self._app.orientation == self._app.LANDSCAPE_ORIENTATION: - windowOrientation = QtCore.Qt.Horizontal - elif self._app.orientation == self._app.PORTRAIT_ORIENTATION: - windowOrientation = QtCore.Qt.Vertical - else: - raise RuntimeError("Bad! No %r for you" % self._app.orientation) - return windowOrientation - - def walk_children(self): - return () - - def start(self): - pass - - def close(self): - for child in self.walk_children(): - child.window.destroyed.disconnect(self._on_child_close) - child.close() - self._window.close() - - def destroy(self): - pass - - def show(self): - self._window.show() - for child in self.walk_children(): - child.show() - self.set_fullscreen(self._app.fullscreenAction.isChecked()) - - def hide(self): - for child in self.walk_children(): - child.hide() - self._window.hide() - - def set_fullscreen(self, isFullscreen): - if self._window.isVisible(): - if isFullscreen: - self._window.showFullScreen() - else: - self._window.showNormal() - for child in self.walk_children(): - child.set_fullscreen(isFullscreen) - - def update_orientation(self, orientation): - if orientation == self._app.DEFAULT_ORIENTATION: - qui_utils.set_autorient(self.window, False) - qui_utils.set_window_orientation(self.window, None) - elif orientation == self._app.AUTO_ORIENTATION: - qui_utils.set_autorient(self.window, True) - qui_utils.set_window_orientation(self.window, None) - elif orientation == self._app.LANDSCAPE_ORIENTATION: - qui_utils.set_autorient(self.window, False) - qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal) - elif orientation == self._app.PORTRAIT_ORIENTATION: - qui_utils.set_autorient(self.window, False) - qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical) - else: - raise RuntimeError("Unknown orientation: %r" % orientation) - for child in self.walk_children(): - child.update_orientation(orientation) - - @misc_utils.log_exception(_moduleLogger) - def _on_child_close(self, obj = None): - raise NotImplementedError("Booh") - - @misc_utils.log_exception(_moduleLogger) - def _on_close_window(self, checked = True): - with qui_utils.notify_error(self._errorLog): - self.close() - - -class AutoFreezeWindowFeature(object): - - def __init__(self, app, window): - self._app = app - self._window = window - self._app.qapp.focusChanged.connect(self._on_focus_changed) - if self._app.qapp.focusWidget() is not None: - self._window.setUpdatesEnabled(True) - else: - self._window.setUpdatesEnabled(False) - - def close(self): - self._app.qapp.focusChanged.disconnect(self._on_focus_changed) - self._window.setUpdatesEnabled(True) - - @misc_utils.log_exception(_moduleLogger) - def _on_focus_changed(self, oldWindow, newWindow): - with qui_utils.notify_error(self._app.errorLog): - if oldWindow is None and newWindow is not None: - self._window.setUpdatesEnabled(True) - elif oldWindow is not None and newWindow is None: - self._window.setUpdatesEnabled(False) diff --git a/src/util/time_utils.py b/src/util/time_utils.py deleted file mode 100644 index 90ec84d..0000000 --- a/src/util/time_utils.py +++ /dev/null @@ -1,94 +0,0 @@ -from datetime import tzinfo, timedelta, datetime - -ZERO = timedelta(0) -HOUR = timedelta(hours=1) - - -def first_sunday_on_or_after(dt): - days_to_go = 6 - dt.weekday() - if days_to_go: - dt += timedelta(days_to_go) - return dt - - -# US DST Rules -# -# This is a simplified (i.e., wrong for a few cases) set of rules for US -# DST start and end times. For a complete and up-to-date set of DST rules -# and timezone definitions, visit the Olson Database (or try pytz): -# http://www.twinsun.com/tz/tz-link.htm -# http://sourceforge.net/projects/pytz/ (might not be up-to-date) -# -# In the US, since 2007, DST starts at 2am (standard time) on the second -# Sunday in March, which is the first Sunday on or after Mar 8. -DSTSTART_2007 = datetime(1, 3, 8, 2) -# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov. -DSTEND_2007 = datetime(1, 11, 1, 1) -# From 1987 to 2006, DST used to start at 2am (standard time) on the first -# Sunday in April and to end at 2am (DST time; 1am standard time) on the last -# Sunday of October, which is the first Sunday on or after Oct 25. -DSTSTART_1987_2006 = datetime(1, 4, 1, 2) -DSTEND_1987_2006 = datetime(1, 10, 25, 1) -# From 1967 to 1986, DST used to start at 2am (standard time) on the last -# Sunday in April (the one on or after April 24) and to end at 2am (DST time; -# 1am standard time) on the last Sunday of October, which is the first Sunday -# on or after Oct 25. -DSTSTART_1967_1986 = datetime(1, 4, 24, 2) -DSTEND_1967_1986 = DSTEND_1987_2006 - - -class USTimeZone(tzinfo): - - def __init__(self, hours, reprname, stdname, dstname): - self.stdoffset = timedelta(hours=hours) - self.reprname = reprname - self.stdname = stdname - self.dstname = dstname - - def __repr__(self): - return self.reprname - - def tzname(self, dt): - if self.dst(dt): - return self.dstname - else: - return self.stdname - - def utcoffset(self, dt): - return self.stdoffset + self.dst(dt) - - def dst(self, dt): - if dt is None or dt.tzinfo is None: - # An exception may be sensible here, in one or both cases. - # It depends on how you want to treat them. The default - # fromutc() implementation (called by the default astimezone() - # implementation) passes a datetime with dt.tzinfo is self. - return ZERO - assert dt.tzinfo is self - - # Find start and end times for US DST. For years before 1967, return - # ZERO for no DST. - if 2006 < dt.year: - dststart, dstend = DSTSTART_2007, DSTEND_2007 - elif 1986 < dt.year < 2007: - dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 - elif 1966 < dt.year < 1987: - dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 - else: - return ZERO - - start = first_sunday_on_or_after(dststart.replace(year=dt.year)) - end = first_sunday_on_or_after(dstend.replace(year=dt.year)) - - # Can't compare naive to aware objects, so strip the timezone from - # dt first. - if start <= dt.replace(tzinfo=None) < end: - return HOUR - else: - return ZERO - - -Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") -Central = USTimeZone(-6, "Central", "CST", "CDT") -Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") -Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/src/util/tp_utils.py b/src/util/tp_utils.py deleted file mode 100644 index 7c55c42..0000000 --- a/src/util/tp_utils.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python - -import logging - -import dbus -import telepathy - -import util.go_utils as gobject_utils -import misc - - -_moduleLogger = logging.getLogger(__name__) -DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' - - -class WasMissedCall(object): - - def __init__(self, bus, conn, chan, on_success, on_error): - self.__on_success = on_success - self.__on_error = on_error - - self._requested = None - self._didMembersChange = False - self._didClose = False - self._didReport = False - - self._onTimeout = gobject_utils.Timeout(self._on_timeout) - self._onTimeout.start(seconds=60) - - chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal( - "MembersChanged", - self._on_members_changed, - ) - - chan[telepathy.interfaces.CHANNEL].connect_to_signal( - "Closed", - self._on_closed, - ) - - chan[DBUS_PROPERTIES].GetAll( - telepathy.interfaces.CHANNEL_INTERFACE, - reply_handler = self._on_got_all, - error_handler = self._on_error, - ) - - def cancel(self): - self._report_error("by request") - - def _report_missed_if_ready(self): - if self._didReport: - pass - elif self._requested is not None and (self._didMembersChange or self._didClose): - if self._requested: - self._report_error("wrong direction") - elif self._didClose: - self._report_success() - else: - self._report_error("members added") - else: - if self._didClose: - self._report_error("closed too early") - - def _report_success(self): - assert not self._didReport, "Double reporting a missed call" - self._didReport = True - self._onTimeout.cancel() - self.__on_success(self) - - def _report_error(self, reason): - assert not self._didReport, "Double reporting a missed call" - self._didReport = True - self._onTimeout.cancel() - self.__on_error(self, reason) - - @misc.log_exception(_moduleLogger) - def _on_got_all(self, properties): - self._requested = properties["Requested"] - self._report_missed_if_ready() - - @misc.log_exception(_moduleLogger) - def _on_members_changed(self, message, added, removed, lp, rp, actor, reason): - if added: - self._didMembersChange = True - self._report_missed_if_ready() - - @misc.log_exception(_moduleLogger) - def _on_closed(self): - self._didClose = True - self._report_missed_if_ready() - - @misc.log_exception(_moduleLogger) - def _on_error(self, *args): - self._report_error(args) - - @misc.log_exception(_moduleLogger) - def _on_timeout(self): - self._report_error("timeout") - return False - - -class NewChannelSignaller(object): - - def __init__(self, on_new_channel): - self._sessionBus = dbus.SessionBus() - self._on_user_new_channel = on_new_channel - - def start(self): - self._sessionBus.add_signal_receiver( - self._on_new_channel, - "NewChannel", - "org.freedesktop.Telepathy.Connection", - None, - None - ) - - def stop(self): - self._sessionBus.remove_signal_receiver( - self._on_new_channel, - "NewChannel", - "org.freedesktop.Telepathy.Connection", - None, - None - ) - - @misc.log_exception(_moduleLogger) - def _on_new_channel( - self, channelObjectPath, channelType, handleType, handle, supressHandler - ): - connObjectPath = channel_path_to_conn_path(channelObjectPath) - serviceName = path_to_service_name(channelObjectPath) - try: - self._on_user_new_channel( - self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType - ) - except Exception: - _moduleLogger.exception("Blocking exception from being passed up") - - -class EnableSystemContactIntegration(object): - - ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager" - ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager" - ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query" - ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat" - ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile" - DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' - - def __init__(self, profileName): - self._bus = dbus.SessionBus() - self._profileName = profileName - - def start(self): - self._accountManager = self._bus.get_object( - self.ACCOUNT_MGR_NAME, - self.ACCOUNT_MGR_PATH, - ) - self._accountManagerQuery = dbus.Interface( - self._accountManager, - dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY, - ) - - self._accountManagerQuery.FindAccounts( - { - self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName, - }, - reply_handler = self._on_found_accounts_reply, - error_handler = self._on_error, - ) - - @misc.log_exception(_moduleLogger) - def _on_found_accounts_reply(self, accountObjectPaths): - for accountObjectPath in accountObjectPaths: - print accountObjectPath - account = self._bus.get_object( - self.ACCOUNT_MGR_NAME, - accountObjectPath, - ) - accountProperties = dbus.Interface( - account, - self.DBUS_PROPERTIES, - ) - accountProperties.Set( - self.ACCOUNT_IFACE_COMPAT, - "SecondaryVCardFields", - ["TEL"], - reply_handler = self._on_field_set, - error_handler = self._on_error, - ) - - @misc.log_exception(_moduleLogger) - def _on_field_set(self): - _moduleLogger.info("SecondaryVCardFields Set") - - @misc.log_exception(_moduleLogger) - def _on_error(self, error): - _moduleLogger.error("%r" % (error, )) - - -def channel_path_to_conn_path(channelObjectPath): - """ - >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") - '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME' - """ - return channelObjectPath.rsplit("/", 1)[0] - - -def path_to_service_name(path): - """ - >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") - 'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME' - """ - return ".".join(path[1:].split("/")[0:7]) - - -def cm_from_path(path): - """ - >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1") - 'theonering' - """ - return path[1:].split("/")[4] diff --git a/support/dialcentral.desktop b/support/dialcentral.desktop deleted file mode 100644 index 3b446d7..0000000 --- a/support/dialcentral.desktop +++ /dev/null @@ -1,8 +0,0 @@ -[Desktop Entry] -Encoding=UTF-8 -Version=1.0 -Type=Application -Name=DialCentral -Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py -Icon=dialcentral -Categories=Network;InstantMessaging;Qt; diff --git a/support/icons/hicolor/26x26/hildon/dialcentral.png b/support/icons/hicolor/26x26/hildon/dialcentral.png deleted file mode 100644 index df50c66..0000000 Binary files a/support/icons/hicolor/26x26/hildon/dialcentral.png and /dev/null differ diff --git a/support/icons/hicolor/64x64/hildon/dialcentral.png b/support/icons/hicolor/64x64/hildon/dialcentral.png deleted file mode 100644 index 8d98390..0000000 Binary files a/support/icons/hicolor/64x64/hildon/dialcentral.png and /dev/null differ diff --git a/support/icons/hicolor/scalable/hildon/dialcentral.png b/support/icons/hicolor/scalable/hildon/dialcentral.png deleted file mode 100644 index a875350..0000000 Binary files a/support/icons/hicolor/scalable/hildon/dialcentral.png and /dev/null differ