SOURCE_PATH=src
SOURCE=$(shell find $(SOURCE_PATH) -iname "*.py")
PROGRAM=$(SOURCE_PATH)/$(PROJECT_NAME).py
+DATA_PATH=data
DATA_TYPES=*.ini *.map *.glade *.png
-DATA=$(foreach type, $(DATA_TYPES), $(shell find $(SOURCE_PATH) -iname "$(type)"))
+DATA=$(foreach type, $(DATA_TYPES), $(shell find $(DATA_PATH) -iname "$(type)"))
OBJ=$(SOURCE:.py=.pyc)
BUILD_PATH=./build
TAG_FILE=~/.ctags/$(PROJECT_NAME).tags
all: test
run: $(OBJ)
- $(SOURCE_PATH)/dc_glade.py
+ $(SOURCE_PATH)/$(PROJECT_NAME)_qt.py
profile: $(OBJ)
$(PROFILE_GEN) $(PROGRAM)
--- /dev/null
+http://www.gentleface.com/free_icon_set.html
+The Creative Commons Attribution-NonCommercial -- FREE
+http://creativecommons.org/licenses/by-nc-nd/3.0/
+++ /dev/null
-"""
-@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(45)
-
-
-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)'
-
- 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
-
- 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")
- self._loadedFromCookies = True
-
- return self._loadedFromCookies
-
- def save_cookies(self):
- if self._loadedFromCookies:
- self._cookies.save()
-
- def clear_cookies(self):
- if self._loadedFromCookies:
- 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
import os
-import re
import csv
@li Column 0 is name, column 1 is number
"""
- _nameRe = re.compile("name", re.IGNORECASE)
- _phoneRe = re.compile("phone", re.IGNORECASE)
- _mobileRe = re.compile("mobile", re.IGNORECASE)
+ def __init__(self, name, csvPath):
+ self._name = name
+ self._csvPath = csvPath
+ self._contacts = {}
- def __init__(self, csvPath):
- self.__csvPath = csvPath
- self.__contacts = list(
- self.read_csv(csvPath)
+ @property
+ def name(self):
+ return self._name
+
+ def update_contacts(self, force = True):
+ if not force or not self._contacts:
+ return
+ self._contacts = dict(
+ self._read_csv(self._csvPath)
)
- @classmethod
- def read_csv(cls, 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:
csvReader = iter(csv.reader(open(csvPath, "rU")))
except IOError, e:
return
header = csvReader.next()
- nameColumn, phoneColumns = cls._guess_columns(header)
+ nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
yieldCount = 0
for row in csvReader:
try:
if len(row[phoneColumn]) == 0:
continue
- contactDetails.append((phoneType, row[phoneColumn]))
+ contactDetails.append({
+ "phoneType": phoneType,
+ "phoneNumber": row[phoneColumn],
+ })
except IndexError:
pass
- if len(contactDetails) != 0:
- yield str(yieldCount), row[nameColumn], contactDetails
+ 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"
+ 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):
- if cls._nameRe.search(item) is not None:
+ if 0 <= item.lower().find("name"):
names.append((item, i))
- elif cls._phoneRe.search(item) is not None:
+ if 0 <= item.lower().find("first") or 0 <= item.lower().find("given"):
+ firstMiddleLast[0] = i
+ elif 0 <= item.lower().find("middle"):
+ firstMiddleLast[1] = i
+ elif 0 <= item.lower().find("last") or 0 <= item.lower().find("family"):
+ firstMiddleLast[2] = i
+ elif 0 <= item.lower().find("phone"):
phones.append((item, i))
- elif cls._mobileRe.search(item) is not None:
+ elif 0 <= item.lower().find("mobile"):
phones.append((item, i))
+ elif 0 <= item.lower().find("email") or 0 <= item.lower().find("e-mail"):
+ nameFallbacks.append(i)
if len(names) == 0:
names.append(("Name", 0))
if len(phones) == 0:
phones.append(("Phone", 1))
- return names[0][1], phones
-
- def clear_caches(self):
- pass
-
- @staticmethod
- def factory_name():
- return "csv"
-
- @staticmethod
- def contact_source_short_name(contactId):
- return "csv"
-
- def get_contacts(self):
- """
- @returns Iterable of (contact id, contact name)
- """
- for contact in self.__contacts:
- yield contact[0:2]
+ nameColumns = [i for i in firstMiddleLast if 0 <= i]
+ if not nameColumns:
+ nameColumns.append(names[0][1])
- def get_contact_details(self, contactId):
- """
- @returns Iterable of (Phone Type, Phone Number)
- """
- contactId = int(contactId)
- return iter(self.__contacts[contactId][2])
+ return nameColumns, nameFallbacks, phones
class FilesystemAddressBookFactory(object):
}
def __init__(self, path):
- self.__path = path
-
- def clear_caches(self):
- pass
+ self._path = path
def get_addressbooks(self):
- """
- @returns Iterable of (Address Book Factory, Book Id, Book Name)
- """
- for root, dirs, filenames in os.walk(self.__path):
+ for root, dirs, filenames in os.walk(self._path):
for filename in filenames:
try:
name, ext = filename.rsplit(".", 1)
except ValueError:
continue
- if ext in self.FILETYPE_SUPPORT:
- yield self, os.path.join(root, filename), name
-
- def open_addressbook(self, bookId):
- name, ext = bookId.rsplit(".", 1)
- assert ext in self.FILETYPE_SUPPORT, "Unsupported file extension %s" % ext
- return self.FILETYPE_SUPPORT[ext](bookId)
-
- @staticmethod
- def factory_name():
- return "File"
-
-
-def print_filebooks(contactPath = None):
- """
- Included here for debugging.
-
- Either insert it into the code or launch python with the "-i" flag
- """
- if contactPath is None:
- contactPath = os.path.join(os.path.expanduser("~"), ".dialcentral", "contacts")
-
- abf = FilesystemAddressBookFactory(contactPath)
- for book in abf.get_addressbooks():
- ab = abf.open_addressbook(book[1])
- print book
- for contact in ab.get_contacts():
- print "\t", contact
- for details in ab.get_contact_details(contact[0]):
- print "\t\t", details
+ try:
+ cls = self.FILETYPE_SUPPORT[ext]
+ except KeyError:
+ continue
+ yield cls(name, os.path.join(root, filename))
import itertools
import logging
-import gvoice
+from gvoice import gvoice
-_moduleLogger = logging.getLogger("gv_backend")
+_moduleLogger = logging.getLogger(__name__)
class GVDialer(object):
def __init__(self, cookieFile = None):
self._gvoice = gvoice.GVoiceBackend(cookieFile)
- self._contacts = None
-
def is_quick_login_possible(self):
"""
@returns True then is_authed might be enough to login, else full login is required
def logout(self):
return self._gvoice.logout()
+ def persist(self):
+ return self._gvoice.persist()
+
def is_dnd(self):
return self._gvoice.is_dnd()
"""
@returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
"""
- return self._gvoice.get_recent()
+ return list(self._gvoice.get_recent())
def get_contacts(self):
"""
- @returns Iterable of (contact id, contact name)
- """
- self._update_contacts_cache()
- contactsToSort = [
- (contactDetails["name"], contactId)
- for contactId, contactDetails in self._contacts.iteritems()
- ]
- contactsToSort.sort()
- return (
- (contactId, contactName)
- for (contactName, contactId) in contactsToSort
- )
-
- def get_contact_details(self, contactId):
- """
- @returns Iterable of (Phone Type, Phone Number)
+ @returns Fresh dictionary of items
"""
- if self._contacts is None:
- self._update_contacts_cache()
- contactDetails = self._contacts[contactId]
- # Defaulting phoneTypes because those are just things like faxes
- return (
- (number.get("phoneType", ""), number["phoneNumber"])
- for number in contactDetails["numbers"]
- )
+ return dict(self._gvoice.get_contacts())
def get_messages(self):
+ return list(self._get_messages())
+
+ def _get_messages(self):
voicemails = self._gvoice.get_voicemails()
smss = self._gvoice.get_texts()
conversations = itertools.chain(voicemails, smss)
for conversation in conversations:
messages = conversation.messages
- messageParts = (
+ messageParts = [
(message.whoFrom, self._format_message(message), message.when)
for message in messages
- )
+ ]
messageDetails = {
"id": conversation.id,
def factory_name():
return "Google Voice"
- def _update_contacts_cache(self):
- self._contacts = dict(self._gvoice.get_contacts())
-
def _format_message(self, message):
messagePartFormat = {
"med1": "<i>%s</i>",
+++ /dev/null
-#!/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._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
- 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"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
- 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._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._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"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
- self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
- self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
- self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
-
- self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
- self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
- self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
- self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
- self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
- self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
- self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
- self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
- self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
- self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
- self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-
- def is_quick_login_possible(self):
- """
- @returns True then is_authed might be enough to login, else full login is required
- """
- return self._loadedFromCookies or 0.0 < self._lastAuthed
-
- def is_authed(self, force = False):
- """
- Attempts to detect a current session
- @note Once logged in try not to reauth more than once a minute.
- @returns If authenticated
- @blocks
- """
- isRecentledAuthed = (time.time() - self._lastAuthed) < 120
- isPreviouslyAuthed = self._token is not None
- if isRecentledAuthed and isPreviouslyAuthed and not force:
- return True
-
- try:
- page = self._get_page(self._forwardURL)
- self._grab_account_info(page)
- except Exception, e:
- _moduleLogger.exception(str(e))
- return False
-
- self._browser.save_cookies()
- self._lastAuthed = time.time()
- return True
-
- 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._forwardURL,
- }
-
- 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:
- self._grab_account_info(loginSuccessOrFailurePage)
- except Exception, e:
- # Retry in case the redirect failed
- # luckily is_authed does everything we need for a retry
- loggedIn = self.is_authed(True)
- if not loggedIn:
- _moduleLogger.exception(str(e))
- return False
- _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
-
- self._browser.save_cookies()
- self._lastAuthed = time.time()
- return True
-
- 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
-
- 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 download(self, messageId, adir):
- """
- Download a voicemail or recorded call MP3 matching the given ``msg``
- which can either be a ``Message`` instance, or a SHA1 identifier.
- Saves files to ``adir`` (defaults to current directory).
- Message hashes can be found in ``self.voicemail().messages`` for example.
- @returns location of saved file.
- @blocks
- """
- page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
- fn = os.path.join(adir, '%s.mp3' % messageId)
- with open(fn, 'wb') as fo:
- fo.write(page)
- return fn
-
- 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.
- """
- if not self.is_authed():
- return {}
- 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_recent(self):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- @blocks
- """
- recentPages = [
- (action, self._get_page(url))
- for action, url in (
- ("Received", self._XML_RECEIVED_URL),
- ("Missed", self._XML_MISSED_URL),
- ("Placed", self._XML_PLACED_URL),
- )
- ]
- return self._parse_recent(recentPages)
-
- def get_contacts(self):
- """
- @returns Iterable of (contact id, contact name)
- @blocks
- """
- page = self._get_page(self._XML_CONTACTS_URL)
- return self._process_contacts(page)
-
- 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 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):
- tokenGroup = self._tokenRe.search(page)
- if tokenGroup is None:
- raise RuntimeError("Could not extract authentication token from GoogleVoice")
- self._token = tokenGroup.group(1)
-
- anGroup = self._accountNumRe.search(page)
- if anGroup is not None:
- self._accountNum = anGroup.group(1)
- else:
- _moduleLogger.debug("Could not extract account number from GoogleVoice")
-
- self._callbackNumbers = {}
- for match in self._callbackRe.finditer(page):
- callbackNumber = match.group(2)
- callbackName = match.group(1)
- self._callbackNumbers[callbackNumber] = callbackName
- if len(self._callbackNumbers) == 0:
- _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
-
- def _send_validation(self, number):
- if not self.is_valid_syntax(number):
- raise ValueError('Number is not valid: "%s"' % number)
- elif not self.is_authed():
- raise RuntimeError("Not Authenticated")
- return number
-
- def _parse_recent(self, recentPages):
- for action, flatXml in recentPages:
- allRecentHtml = self._grab_html(flatXml)
- allRecentData = self._parse_history(allRecentHtml)
- for recentCallData in allRecentData:
- recentCallData["action"] = action
- yield recentCallData
-
- def _process_contacts(self, page):
- contactsBody = self._contactsBodyRe.search(page)
- if contactsBody is None:
- raise RuntimeError("Could not extract contact information")
- accountData = _fake_parse_json(contactsBody.group(1))
- for contactId, contactDetails in accountData["contacts"].iteritems():
- # A zero contact id is the catch all for unknown contacts
- if contactId != "0":
- yield contactId, contactDetails
-
- 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
- message.when = conv.time.strftime("%I:%M %p")
- 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)
- validate_response(json)
- return json
-
-
-_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 validate_response(response):
- """
- Validates that the JSON response is A-OK
- """
- try:
- assert response is not None
- assert 'ok' in response
- assert response['ok']
- except AssertionError:
- raise RuntimeError('There was a problem with GV: %s' % response)
-
-
-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 = [
- ("forward", backend._forwardURL),
- ("token", backend._tokenURL),
- ("login", backend._loginURL),
- ("isdnd", backend._isDndURL),
- ("account", backend._XML_ACCOUNT_URL),
- ("contacts", backend._XML_CONTACTS_URL),
- ("csv", backend._CSV_CONTACTS_URL),
-
- ("voicemail", backend._XML_VOICEMAIL_URL),
- ("sms", backend._XML_SMS_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 is_authed does everything we need for a retry
- loggedIn = backend.is_authed(True)
- 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 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)
-
-
-if __name__ == "__main__":
- main()
--- /dev/null
+"""
+@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(45)
+
+
+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)'
+
+ 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
--- /dev/null
+#!/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._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
+ 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"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
+ 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"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
+ self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+ self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
+ self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
+
+ self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
+ self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+ self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+ self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+ self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+ self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+ self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
+ self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
+ self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
+ self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+
+ def is_quick_login_possible(self):
+ """
+ @returns True then is_authed might be enough to login, else full login is required
+ """
+ return self._loadedFromCookies or 0.0 < self._lastAuthed
+
+ def is_authed(self, force = False):
+ """
+ Attempts to detect a current session
+ @note Once logged in try not to reauth more than once a minute.
+ @returns If authenticated
+ @blocks
+ """
+ isRecentledAuthed = (time.time() - self._lastAuthed) < 120
+ isPreviouslyAuthed = self._token is not None
+ if isRecentledAuthed and isPreviouslyAuthed and not force:
+ return True
+
+ try:
+ page = self._get_page(self._forwardURL)
+ self._grab_account_info(page)
+ except Exception, e:
+ _moduleLogger.exception(str(e))
+ return False
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return True
+
+ 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._forwardURL,
+ }
+
+ 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:
+ self._grab_account_info(loginSuccessOrFailurePage)
+ except Exception, e:
+ # Retry in case the redirect failed
+ # luckily is_authed does everything we need for a retry
+ loggedIn = self.is_authed(True)
+ if not loggedIn:
+ _moduleLogger.exception(str(e))
+ return False
+ _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return True
+
+ 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
+
+ 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 download(self, messageId, adir):
+ """
+ Download a voicemail or recorded call MP3 matching the given ``msg``
+ which can either be a ``Message`` instance, or a SHA1 identifier.
+ Saves files to ``adir`` (defaults to current directory).
+ Message hashes can be found in ``self.voicemail().messages`` for example.
+ @returns location of saved file.
+ @blocks
+ """
+ page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
+ fn = os.path.join(adir, '%s.mp3' % messageId)
+ with open(fn, 'wb') as fo:
+ fo.write(page)
+ return fn
+
+ 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.
+ """
+ if not self.is_authed():
+ return {}
+ 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_recent(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ recentPages = [
+ (action, self._get_page(url))
+ for action, url in (
+ ("Received", self._XML_RECEIVED_URL),
+ ("Missed", self._XML_MISSED_URL),
+ ("Placed", self._XML_PLACED_URL),
+ )
+ ]
+ return self._parse_recent(recentPages)
+
+ def get_contacts(self):
+ """
+ @returns Iterable of (contact id, contact name)
+ @blocks
+ """
+ page = self._get_page(self._JSON_CONTACTS_URL)
+ return self._process_contacts(page)
+
+ 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):
+ tokenGroup = self._tokenRe.search(page)
+ if tokenGroup is None:
+ raise RuntimeError("Could not extract authentication token from GoogleVoice")
+ self._token = tokenGroup.group(1)
+
+ anGroup = self._accountNumRe.search(page)
+ if anGroup is not None:
+ self._accountNum = anGroup.group(1)
+ else:
+ _moduleLogger.debug("Could not extract account number from GoogleVoice")
+
+ self._callbackNumbers = {}
+ for match in self._callbackRe.finditer(page):
+ callbackNumber = match.group(2)
+ callbackName = match.group(1)
+ self._callbackNumbers[callbackNumber] = callbackName
+ if len(self._callbackNumbers) == 0:
+ _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
+
+ def _send_validation(self, number):
+ if not self.is_valid_syntax(number):
+ raise ValueError('Number is not valid: "%s"' % number)
+ elif not self.is_authed():
+ raise RuntimeError("Not Authenticated")
+ return number
+
+ def _parse_recent(self, recentPages):
+ for action, flatXml in recentPages:
+ allRecentHtml = self._grab_html(flatXml)
+ allRecentData = self._parse_history(allRecentHtml)
+ for recentCallData in allRecentData:
+ recentCallData["action"] = action
+ yield recentCallData
+
+ def _process_contacts(self, page):
+ accountData = parse_json(page)
+ for contactId, contactDetails in accountData["contacts"].iteritems():
+ # A zero contact id is the catch all for unknown contacts
+ if contactId != "0":
+ yield contactId, contactDetails
+
+ 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 = [
+ ("forward", backend._forwardURL),
+ ("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 is_authed does everything we need for a retry
+ loggedIn = backend.is_authed(True)
+ 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 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)
+
+
+if __name__ == "__main__":
+ main()
+++ /dev/null
-import logging
-
-
-_moduleLogger = logging.getLogger("merge_backend")
-
-
-class MergedAddressBook(object):
- """
- Merger of all addressbooks
- """
-
- def __init__(self, addressbookFactories, sorter = None):
- self.__addressbookFactories = addressbookFactories
- self.__addressbooks = None
- self.__sort_contacts = sorter if sorter is not None else self.null_sorter
-
- def clear_caches(self):
- self.__addressbooks = None
- for factory in self.__addressbookFactories:
- factory.clear_caches()
-
- def get_addressbooks(self):
- """
- @returns Iterable of (Address Book Factory, Book Id, Book Name)
- """
- yield self, "", ""
-
- def open_addressbook(self, bookId):
- return self
-
- def contact_source_short_name(self, contactId):
- if self.__addressbooks is None:
- return ""
- bookIndex, originalId = contactId.split("-", 1)
- return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
-
- @staticmethod
- def factory_name():
- return "All Contacts"
-
- def get_contacts(self):
- """
- @returns Iterable of (contact id, contact name)
- """
- if self.__addressbooks is None:
- self.__addressbooks = list(
- factory.open_addressbook(id)
- for factory in self.__addressbookFactories
- for (f, id, name) in factory.get_addressbooks()
- )
- contacts = (
- ("-".join([str(bookIndex), contactId]), contactName)
- for (bookIndex, addressbook) in enumerate(self.__addressbooks)
- for (contactId, contactName) in addressbook.get_contacts()
- )
- sortedContacts = self.__sort_contacts(contacts)
- return sortedContacts
-
- def get_contact_details(self, contactId):
- """
- @returns Iterable of (Phone Type, Phone Number)
- """
- if self.__addressbooks is None:
- return []
- bookIndex, originalId = contactId.split("-", 1)
- return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
-
- @staticmethod
- def null_sorter(contacts):
- """
- Good for speed/low memory
- """
- return contacts
-
- @staticmethod
- def basic_firtname_sorter(contacts):
- """
- Expects names in "First Last" format
- """
- contactsWithKey = [
- (contactName.rsplit(" ", 1)[0], (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
-
- @staticmethod
- def basic_lastname_sorter(contacts):
- """
- Expects names in "First Last" format
- """
- contactsWithKey = [
- (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
-
- @staticmethod
- def reversed_firtname_sorter(contacts):
- """
- Expects names in "Last, First" format
- """
- contactsWithKey = [
- (contactName.split(", ", 1)[-1], (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
-
- @staticmethod
- def reversed_lastname_sorter(contacts):
- """
- Expects names in "Last, First" format
- """
- contactsWithKey = [
- (contactName.split(", ", 1)[0], (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
-
- @staticmethod
- def guess_firstname(name):
- if ", " in name:
- return name.split(", ", 1)[-1]
- else:
- return name.rsplit(" ", 1)[0]
-
- @staticmethod
- def guess_lastname(name):
- if ", " in name:
- return name.split(", ", 1)[0]
- else:
- return name.rsplit(" ", 1)[-1]
-
- @classmethod
- def advanced_firstname_sorter(cls, contacts):
- contactsWithKey = [
- (cls.guess_firstname(contactName), (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
-
- @classmethod
- def advanced_lastname_sorter(cls, contacts):
- contactsWithKey = [
- (cls.guess_lastname(contactName), (contactId, contactName))
- for (contactId, contactName) in contacts
- ]
- contactsWithKey.sort()
- return (contactData for (lastName, contactData) in contactsWithKey)
"""
-class NullDialer(object):
-
- def __init__(self):
- pass
-
- def is_quick_login_possible(self):
- return False
-
- def is_authed(self, force = False):
- return False
-
- def login(self, username, password):
- return self.is_authed()
-
- def logout(self):
- self.clear_caches()
-
- def call(self, number):
- return True
-
- def cancel(self, outgoingNumber=None):
- pass
-
- def send_sms(self, number, message):
- raise NotImplementedError("SMS Is Not Supported")
-
- def search(self, query):
- return []
-
- def get_feed(self, feed):
- return {}
+class NullAddressBook(object):
- def download(self, messageId, adir):
- return ""
+ @property
+ def name(self):
+ return "None"
- def clear_caches(self):
+ def update_contacts(self, force = True):
pass
- def is_valid_syntax(self, number):
- """
- @returns If This number be called ( syntax validation only )
- """
- return False
-
- def get_account_number(self):
- """
- @returns The grand central phone number
- """
- return ""
-
- def get_callback_numbers(self):
- return {}
-
- def set_callback_number(self, callbacknumber):
- return True
-
- def get_callback_number(self):
- return ""
-
- def get_recent(self):
- return ()
-
- def get_addressbooks(self):
- return ()
-
- def open_addressbook(self, bookId):
- return self
-
- @staticmethod
- def contact_source_short_name(contactId):
- return "ERROR"
-
- @staticmethod
- def factory_name():
- return "ERROR"
-
def get_contacts(self):
- return ()
-
- def get_contact_details(self, contactId):
- return ()
-
- def get_messages(self):
- return ()
+ return {}
-class NullAddressBook(object):
- """
- Minimal example of both an addressbook factory and an addressbook
- """
-
- def clear_caches(self):
- pass
+class NullAddressBookFactory(object):
def get_addressbooks(self):
- """
- @returns Iterable of (Address Book Factory, Book Id, Book Name)
- """
- yield self, "", "None"
-
- def open_addressbook(self, bookId):
- return self
-
- @staticmethod
- def contact_source_short_name(contactId):
- return ""
-
- @staticmethod
- def factory_name():
- return ""
-
- @staticmethod
- def get_contacts():
- """
- @returns Iterable of (contact id, contact name)
- """
- return []
-
- @staticmethod
- def get_contact_details(contactId):
- """
- @returns Iterable of (Phone Type, Phone Number)
- """
- return []
+ yield NullAddressBook()
+++ /dev/null
-#!/usr/bin/env python
-
-"""
-DialCentral - Front end for Google's GoogleVoice service.
-Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-"""
-
-
-from __future__ import with_statement
-
-import sys
-import gc
-import os
-import threading
-import base64
-import ConfigParser
-import itertools
-import shutil
-import logging
-
-import gtk
-import gtk.glade
-
-import constants
-import hildonize
-import gtk_toolbox
-
-
-_moduleLogger = logging.getLogger("dc_glade")
-PROFILE_STARTUP = False
-
-
-def getmtime_nothrow(path):
- try:
- return os.path.getmtime(path)
- except Exception:
- return 0
-
-
-def display_error_message(msg):
- error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
-
- def close(dialog, response):
- dialog.destroy()
- error_dialog.connect("response", close)
- error_dialog.run()
-
-
-class Dialcentral(object):
-
- _glade_files = [
- os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
- os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
- '/usr/lib/dialcentral/dialcentral.glade',
- ]
-
- KEYPAD_TAB = 0
- RECENT_TAB = 1
- MESSAGES_TAB = 2
- CONTACTS_TAB = 3
- ACCOUNT_TAB = 4
-
- NULL_BACKEND = 0
- # 1 Was GrandCentral support so the gap was maintained for compatibility
- GV_BACKEND = 2
- BACKENDS = (NULL_BACKEND, GV_BACKEND)
-
- def __init__(self):
- self._initDone = False
- self._connection = None
- self._osso = None
- self._deviceState = None
- self._clipboard = gtk.clipboard_get()
-
- self._credentials = ("", "")
- self._selectedBackendId = self.NULL_BACKEND
- self._defaultBackendId = self.GV_BACKEND
- self._phoneBackends = None
- self._dialpads = None
- self._accountViews = None
- self._messagesViews = None
- self._historyViews = None
- self._contactsViews = None
- self._alarmHandler = None
- self._ledHandler = None
- self._originalCurrentLabels = []
- self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
-
- for path in self._glade_files:
- if os.path.isfile(path):
- self._widgetTree = gtk.glade.XML(path)
- break
- else:
- display_error_message("Cannot find dialcentral.glade")
- gtk.main_quit()
- return
-
- self._window = self._widgetTree.get_widget("mainWindow")
- self._notebook = self._widgetTree.get_widget("notebook")
- errorBox = self._widgetTree.get_widget("errorEventBox")
- errorDescription = self._widgetTree.get_widget("errorDescription")
- errorClose = self._widgetTree.get_widget("errorClose")
- self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
- self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
- self._smsEntryWindow = None
-
- self._isFullScreen = False
- self.__isPortrait = False
- self._app = hildonize.get_app_class()()
- self._window = hildonize.hildonize_window(self._app, self._window)
- hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
- hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
-
- for scrollingWidgetName in (
- 'history_scrolledwindow',
- 'message_scrolledwindow',
- 'contacts_scrolledwindow',
- "smsMessages_scrolledwindow",
- ):
- scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
- assert scrollingWidget is not None, scrollingWidgetName
- hildonize.hildonize_scrollwindow(scrollingWidget)
- for scrollingWidgetName in (
- "smsMessage_scrolledEntry",
- ):
- scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
- assert scrollingWidget is not None, scrollingWidgetName
- hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
-
- for buttonName in (
- "back",
- "addressbookSelectButton",
- "sendSmsButton",
- "dialButton",
- "callbackSelectButton",
- "minutesEntryButton",
- "clearcookies",
- "phoneTypeSelection",
- ):
- button = self._widgetTree.get_widget(buttonName)
- assert button is not None, buttonName
- hildonize.set_button_thumb_selectable(button)
-
- menu = hildonize.hildonize_menu(
- self._window,
- self._widgetTree.get_widget("dialpad_menubar"),
- )
- if not hildonize.GTK_MENU_USED:
- button = gtk.Button("New Login")
- button.connect("clicked", self._on_clearcookies_clicked)
- menu.append(button)
-
- button = gtk.Button("Refresh")
- button.connect("clicked", self._on_menu_refresh)
- menu.append(button)
-
- menu.show_all()
-
- self._window.connect("key-press-event", self._on_key_press)
- self._window.connect("window-state-event", self._on_window_state_change)
- if not hildonize.IS_HILDON_SUPPORTED:
- _moduleLogger.warning("No hildonization support")
-
- hildonize.set_application_name("%s" % constants.__pretty_app_name__)
-
- self._window.connect("destroy", self._on_close)
- self._window.set_default_size(800, 300)
- self._window.show_all()
-
- self._loginSink = gtk_toolbox.threaded_stage(
- gtk_toolbox.comap(
- self._attempt_login,
- gtk_toolbox.null_sink(),
- )
- )
-
- if not PROFILE_STARTUP:
- backgroundSetup = threading.Thread(target=self._idle_setup)
- backgroundSetup.setDaemon(True)
- backgroundSetup.start()
- else:
- self._idle_setup()
-
- def _idle_setup(self):
- """
- If something can be done after the UI loads, push it here so it's not blocking the UI
- """
- # Barebones UI handlers
- try:
- from backends import null_backend
- import null_views
-
- self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
- with gtk_toolbox.gtk_lock():
- self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
- self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
- self._historyViews = {self.NULL_BACKEND: null_views.CallHistoryView(self._widgetTree)}
- self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
- self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
-
- self._dialpads[self._selectedBackendId].enable()
- self._accountViews[self._selectedBackendId].enable()
- self._historyViews[self._selectedBackendId].enable()
- self._messagesViews[self._selectedBackendId].enable()
- self._contactsViews[self._selectedBackendId].enable()
- except Exception, e:
- with gtk_toolbox.gtk_lock():
- self._errorDisplay.push_exception()
-
- # Setup maemo specifics
- try:
- try:
- import osso
- except (ImportError, OSError):
- osso = None
- self._osso = None
- self._deviceState = None
- if osso is not None:
- self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
- self._deviceState = osso.DeviceState(self._osso)
- self._deviceState.set_device_state_callback(self._on_device_state_change, 0)
- else:
- _moduleLogger.warning("No device state support")
-
- try:
- import alarm_handler
- if alarm_handler.AlarmHandler is not alarm_handler._NoneAlarmHandler:
- self._alarmHandler = alarm_handler.AlarmHandler()
- else:
- self._alarmHandler = None
- except (ImportError, OSError):
- alarm_handler = None
- except Exception:
- with gtk_toolbox.gtk_lock():
- self._errorDisplay.push_exception()
- alarm_handler = None
- if alarm_handler is None:
- _moduleLogger.warning("No notification support")
- if hildonize.IS_HILDON_SUPPORTED:
- try:
- import led_handler
- self._ledHandler = led_handler.LedHandler()
- except Exception, e:
- _moduleLogger.exception('LED Handling failed: "%s"' % str(e))
- self._ledHandler = None
- else:
- self._ledHandler = None
-
- try:
- import conic
- except (ImportError, OSError):
- conic = None
- self._connection = None
- if conic is not None:
- self._connection = conic.Connection()
- self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
- self._connection.request_connection(conic.CONNECT_FLAG_NONE)
- else:
- _moduleLogger.warning("No connection support")
- except Exception, e:
- with gtk_toolbox.gtk_lock():
- self._errorDisplay.push_exception()
-
- # Setup costly backends
- try:
- from backends import gv_backend
- from backends import file_backend
- import gv_views
- from backends import merge_backend
-
- with gtk_toolbox.gtk_lock():
- self._smsEntryWindow = gv_views.SmsEntryWindow(self._widgetTree, self._window, self._app)
- try:
- os.makedirs(constants._data_path_)
- except OSError, e:
- if e.errno != 17:
- raise
- gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
-
- self._phoneBackends.update({
- self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
- })
- with gtk_toolbox.gtk_lock():
- unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
- self._dialpads.update({
- self.GV_BACKEND: unifiedDialpad,
- })
- self._accountViews.update({
- self.GV_BACKEND: gv_views.AccountInfo(
- self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
- ),
- })
- self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
- self._historyViews.update({
- self.GV_BACKEND: gv_views.CallHistoryView(
- self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
- ),
- })
- self._messagesViews.update({
- self.GV_BACKEND: gv_views.MessagesView(
- self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
- ),
- })
- self._contactsViews.update({
- self.GV_BACKEND: gv_views.ContactsView(
- self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
- ),
- })
-
- fileBackend = file_backend.FilesystemAddressBookFactory(self._fsContactsPath)
-
- self._smsEntryWindow.send_sms = self._on_sms_clicked
- self._smsEntryWindow.dial = self._on_dial_clicked
- self._dialpads[self.GV_BACKEND].add_contact = self._add_contact
- self._dialpads[self.GV_BACKEND].dial = self._on_dial_clicked
- self._historyViews[self.GV_BACKEND].add_contact = self._add_contact
- self._messagesViews[self.GV_BACKEND].add_contact = self._add_contact
- self._contactsViews[self.GV_BACKEND].add_contact = self._add_contact
-
- addressBooks = [
- self._phoneBackends[self.GV_BACKEND],
- fileBackend,
- ]
- mergedBook = merge_backend.MergedAddressBook(addressBooks, merge_backend.MergedAddressBook.basic_firtname_sorter)
- self._contactsViews[self.GV_BACKEND].append(mergedBook)
- self._contactsViews[self.GV_BACKEND].extend(addressBooks)
- self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
-
- callbackMapping = {
- "on_paste": self._on_paste,
- "on_refresh": self._on_menu_refresh,
- "on_clearcookies_clicked": self._on_clearcookies_clicked,
- "on_about_activate": self._on_about_activate,
- }
- if hildonize.GTK_MENU_USED:
- self._widgetTree.signal_autoconnect(callbackMapping)
- self._notebook.connect("switch-page", self._on_notebook_switch_page)
- self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
-
- with gtk_toolbox.gtk_lock():
- self._originalCurrentLabels = [
- self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
- for pageIndex in xrange(self._notebook.get_n_pages())
- ]
- self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
- self._notebookTapHandler.enable()
- self._notebookTapHandler.on_tap = self._reset_tab_refresh
- self._notebookTapHandler.on_hold = self._on_tab_refresh
- self._notebookTapHandler.on_holding = self._set_tab_refresh
- self._notebookTapHandler.on_cancel = self._reset_tab_refresh
-
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- with gtk_toolbox.gtk_lock():
- self.load_settings(config)
- except Exception, e:
- with gtk_toolbox.gtk_lock():
- self._errorDisplay.push_exception()
- finally:
- self._initDone = True
- self._spawn_attempt_login()
-
- def _spawn_attempt_login(self, *args):
- self._loginSink.send(args)
-
- def _attempt_login(self, force = False):
- """
- @note This must be run outside of the UI lock
- """
- try:
- assert self._initDone, "Attempting login before app is fully loaded"
-
- serviceId = self.NULL_BACKEND
- loggedIn = False
- if not force and self._defaultBackendId != self.NULL_BACKEND:
- with gtk_toolbox.gtk_lock():
- banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
- try:
- self.refresh_session()
- serviceId = self._defaultBackendId
- loggedIn = True
- except Exception, e:
- _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e))
- finally:
- with gtk_toolbox.gtk_lock():
- hildonize.show_busy_banner_end(banner)
-
- if not loggedIn:
- loggedIn, serviceId = self._login_by_user()
-
- with gtk_toolbox.gtk_lock():
- self._change_loggedin_status(serviceId)
- if loggedIn:
- hildonize.show_information_banner(self._window, "Logged In")
- else:
- hildonize.show_information_banner(self._window, "Login Failed")
- if not self._phoneBackends[self._defaultBackendId].get_callback_number():
- # subtle reminder to the users to configure things
- self._notebook.set_current_page(self.ACCOUNT_TAB)
-
- except Exception, e:
- with gtk_toolbox.gtk_lock():
- self._errorDisplay.push_exception()
-
- def refresh_session(self):
- """
- @note Thread agnostic
- """
- assert self._initDone, "Attempting login before app is fully loaded"
-
- loggedIn = False
- if not loggedIn:
- loggedIn = self._login_by_cookie()
- if not loggedIn:
- loggedIn = self._login_by_settings()
-
- if not loggedIn:
- raise RuntimeError("Login Failed")
-
- def _login_by_cookie(self):
- """
- @note Thread agnostic
- """
- loggedIn = False
-
- isQuickLoginPossible = self._phoneBackends[self._defaultBackendId].is_quick_login_possible()
- if self._credentials != ("", "") and isQuickLoginPossible:
- if not loggedIn:
- loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
-
- if loggedIn:
- _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
- else:
- # If the cookies are bad, scratch them completely
- self._phoneBackends[self._defaultBackendId].logout()
-
- return loggedIn
-
- def _login_by_settings(self):
- """
- @note Thread agnostic
- """
- if self._credentials == ("", ""):
- # Don't bother with the settings if they are blank
- return False
-
- username, password = self._credentials
- loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
- if loggedIn:
- self._credentials = username, password
- _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
- return loggedIn
-
- def _login_by_user(self):
- """
- @note This must be run outside of the UI lock
- """
- loggedIn, (username, password) = False, self._credentials
- tmpServiceId = self.GV_BACKEND
- while not loggedIn:
- with gtk_toolbox.gtk_lock():
- credentials = self._credentialsDialog.request_credentials(
- defaultCredentials = self._credentials
- )
- banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
- try:
- username, password = credentials
- loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
- finally:
- with gtk_toolbox.gtk_lock():
- hildonize.show_busy_banner_end(banner)
-
- if loggedIn:
- serviceId = tmpServiceId
- self._credentials = username, password
- _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId])
- else:
- # Hint to the user that they are not logged in
- serviceId = self.NULL_BACKEND
- self._notebook.set_current_page(self.ACCOUNT_TAB)
-
- return loggedIn, serviceId
-
- def _add_contact(self, *args, **kwds):
- self._smsEntryWindow.add_contact(*args, **kwds)
-
- def _change_loggedin_status(self, newStatus):
- oldStatus = self._selectedBackendId
- if oldStatus == newStatus:
- return
-
- _moduleLogger.debug("Changing from %s to %s" % (oldStatus, newStatus))
- self._dialpads[oldStatus].disable()
- self._accountViews[oldStatus].disable()
- self._historyViews[oldStatus].disable()
- self._messagesViews[oldStatus].disable()
- self._contactsViews[oldStatus].disable()
-
- self._dialpads[newStatus].enable()
- self._accountViews[newStatus].enable()
- self._historyViews[newStatus].enable()
- self._messagesViews[newStatus].enable()
- self._contactsViews[newStatus].enable()
-
- self._selectedBackendId = newStatus
-
- self._accountViews[self._selectedBackendId].update()
- self._refresh_active_tab()
- self._refresh_orientation()
-
- def load_settings(self, config):
- """
- @note UI Thread
- """
- try:
- if not PROFILE_STARTUP:
- self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
- else:
- self._defaultBackendId = self.NULL_BACKEND
- blobs = (
- config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
- for i in xrange(len(self._credentials))
- )
- creds = (
- base64.b64decode(blob)
- for blob in blobs
- )
- self._credentials = tuple(creds)
-
- if self._alarmHandler is not None:
- self._alarmHandler.load_settings(config, "alarm")
-
- isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
- if isFullscreen:
- self._window.fullscreen()
-
- isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait")
- if isPortrait ^ self.__isPortrait:
- if isPortrait:
- orientation = gtk.ORIENTATION_VERTICAL
- else:
- orientation = gtk.ORIENTATION_HORIZONTAL
- self.set_orientation(orientation)
- except ConfigParser.NoOptionError, e:
- _moduleLogger.exception(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- except ConfigParser.NoSectionError, e:
- _moduleLogger.exception(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
-
- for backendId, view in itertools.chain(
- self._dialpads.iteritems(),
- self._accountViews.iteritems(),
- self._messagesViews.iteritems(),
- self._historyViews.iteritems(),
- self._contactsViews.iteritems(),
- ):
- sectionName = "%s - %s" % (backendId, view.name())
- try:
- view.load_settings(config, sectionName)
- except ConfigParser.NoOptionError, e:
- _moduleLogger.exception(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- except ConfigParser.NoSectionError, e:
- _moduleLogger.exception(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
-
- def save_settings(self, config):
- """
- @note Thread Agnostic
- """
- # Because we now only support GVoice, if there are user credentials,
- # always assume its using the GVoice backend
- if self._credentials[0] and self._credentials[1]:
- backend = self.GV_BACKEND
- else:
- backend = self.NULL_BACKEND
-
- config.add_section(constants.__pretty_app_name__)
- config.set(constants.__pretty_app_name__, "active", str(backend))
- config.set(constants.__pretty_app_name__, "portrait", str(self.__isPortrait))
- config.set(constants.__pretty_app_name__, "fullscreen", str(self._isFullScreen))
- for i, value in enumerate(self._credentials):
- blob = base64.b64encode(value)
- config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
- config.add_section("alarm")
- if self._alarmHandler is not None:
- self._alarmHandler.save_settings(config, "alarm")
-
- for backendId, view in itertools.chain(
- self._dialpads.iteritems(),
- self._accountViews.iteritems(),
- self._messagesViews.iteritems(),
- self._historyViews.iteritems(),
- self._contactsViews.iteritems(),
- ):
- sectionName = "%s - %s" % (backendId, view.name())
- config.add_section(sectionName)
- view.save_settings(config, sectionName)
-
- def _save_settings(self):
- """
- @note Thread Agnostic
- """
- config = ConfigParser.SafeConfigParser()
- self.save_settings(config)
- with open(constants._user_settings_, "wb") as configFile:
- config.write(configFile)
-
- def _refresh_active_tab(self):
- pageIndex = self._notebook.get_current_page()
- if pageIndex == self.CONTACTS_TAB:
- self._contactsViews[self._selectedBackendId].update(force=True)
- elif pageIndex == self.RECENT_TAB:
- self._historyViews[self._selectedBackendId].update(force=True)
- elif pageIndex == self.MESSAGES_TAB:
- self._messagesViews[self._selectedBackendId].update(force=True)
-
- if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
- if self._ledHandler is not None:
- self._ledHandler.off()
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- hildonize.window_to_portrait(self._window)
- self._notebook.set_property("tab-pos", gtk.POS_BOTTOM)
- self.__isPortrait = True
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- hildonize.window_to_landscape(self._window)
- self._notebook.set_property("tab-pos", gtk.POS_LEFT)
- self.__isPortrait = False
- else:
- raise NotImplementedError(orientation)
-
- def get_orientation(self):
- return gtk.ORIENTATION_VERTICAL if self.__isPortrait else gtk.ORIENTATION_HORIZONTAL
-
- def _toggle_rotate(self):
- if self.__isPortrait:
- self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
- else:
- self.set_orientation(gtk.ORIENTATION_VERTICAL)
-
- def _refresh_orientation(self):
- """
- Mostly meant to be used when switching backends
- """
- if self.__isPortrait:
- self.set_orientation(gtk.ORIENTATION_VERTICAL)
- else:
- self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
-
- @gtk_toolbox.log_exception(_moduleLogger)
- def _on_close(self, *args, **kwds):
- try:
- if self._initDone:
- self._save_settings()
-
- try:
- self._deviceState.close()
- except AttributeError:
- pass # Either None or close was removed (in Fremantle)
- try:
- self._osso.close()
- except AttributeError:
- pass # Either None or close was removed (in Fremantle)
- finally:
- gtk.main_quit()
-
- def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
- """
- For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
- For system_inactivity, we have no background tasks to pause
-
- @note Hildon specific
- """
- try:
- if memory_low:
- for backendId in self.BACKENDS:
- self._phoneBackends[backendId].clear_caches()
- self._contactsViews[self._selectedBackendId].clear_caches()
- gc.collect()
-
- if save_unsaved_data or shutdown:
- self._save_settings()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_connection_change(self, connection, event, magicIdentifier):
- """
- @note Hildon specific
- """
- try:
- import conic
-
- status = event.get_status()
- error = event.get_error()
- iap_id = event.get_iap_id()
- bearer = event.get_bearer_type()
-
- if status == conic.STATUS_CONNECTED:
- if self._initDone:
- self._spawn_attempt_login()
- elif status == conic.STATUS_DISCONNECTED:
- if self._initDone:
- self._defaultBackendId = self._selectedBackendId
- self._change_loggedin_status(self.NULL_BACKEND)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_window_state_change(self, widget, event, *args):
- """
- @note Hildon specific
- """
- try:
- if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
- self._isFullScreen = True
- else:
- self._isFullScreen = False
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_key_press(self, widget, event, *args):
- """
- @note Hildon specific
- """
- RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
- try:
- if (
- event.keyval == gtk.keysyms.F6 or
- event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- if self._isFullScreen:
- self._window.unfullscreen()
- else:
- self._window.fullscreen()
- elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
- with open(constants._user_logpath_, "r") as f:
- logLines = f.xreadlines()
- log = "".join(logLines)
- self._clipboard.set_text(str(log))
- elif (
- event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and
- event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- self._window.destroy()
- elif event.keyval == gtk.keysyms.o and event.get_state() & gtk.gdk.CONTROL_MASK:
- self._toggle_rotate()
- return True
- elif event.keyval == gtk.keysyms.r and event.get_state() & gtk.gdk.CONTROL_MASK:
- self._refresh_active_tab()
- elif event.keyval == gtk.keysyms.i and event.get_state() & gtk.gdk.CONTROL_MASK:
- self._import_contacts()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_clearcookies_clicked(self, *args):
- try:
- self._phoneBackends[self._selectedBackendId].logout()
- self._accountViews[self._selectedBackendId].clear()
- self._historyViews[self._selectedBackendId].clear()
- self._messagesViews[self._selectedBackendId].clear()
- self._contactsViews[self._selectedBackendId].clear()
- self._change_loggedin_status(self.NULL_BACKEND)
-
- self._spawn_attempt_login(True)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_notebook_switch_page(self, notebook, page, pageIndex):
- try:
- self._reset_tab_refresh()
-
- didRecentUpdate = False
- didMessagesUpdate = False
-
- if pageIndex == self.RECENT_TAB:
- didRecentUpdate = self._historyViews[self._selectedBackendId].update()
- elif pageIndex == self.MESSAGES_TAB:
- didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
- elif pageIndex == self.CONTACTS_TAB:
- self._contactsViews[self._selectedBackendId].update()
- elif pageIndex == self.ACCOUNT_TAB:
- self._accountViews[self._selectedBackendId].update()
-
- if didRecentUpdate or didMessagesUpdate:
- if self._ledHandler is not None:
- self._ledHandler.off()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _set_tab_refresh(self, *args):
- try:
- pageIndex = self._notebook.get_current_page()
- child = self._notebook.get_nth_page(pageIndex)
- self._notebook.get_tab_label(child).set_text("Refresh?")
- except Exception, e:
- self._errorDisplay.push_exception()
- return False
-
- def _reset_tab_refresh(self, *args):
- try:
- pageIndex = self._notebook.get_current_page()
- child = self._notebook.get_nth_page(pageIndex)
- self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
- except Exception, e:
- self._errorDisplay.push_exception()
- return False
-
- def _on_tab_refresh(self, *args):
- try:
- self._refresh_active_tab()
- self._reset_tab_refresh()
- except Exception, e:
- self._errorDisplay.push_exception()
- return False
-
- def _on_sms_clicked(self, numbers, message):
- try:
- assert numbers, "No number specified"
- assert message, "Empty message"
- self.refresh_session()
- try:
- loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
- except Exception, e:
- loggedIn = False
- self._errorDisplay.push_exception()
- return
-
- if not loggedIn:
- self._errorDisplay.push_message(
- "Backend link with GoogleVoice is not working, please try again"
- )
- return
-
- dialed = False
- try:
- self._phoneBackends[self._selectedBackendId].send_sms(numbers, message)
- hildonize.show_information_banner(self._window, "Sending to %s" % ", ".join(numbers))
- _moduleLogger.info("Sending SMS to %r" % numbers)
- dialed = True
- except Exception, e:
- self._errorDisplay.push_exception()
-
- if dialed:
- self._dialpads[self._selectedBackendId].clear()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_dial_clicked(self, number):
- try:
- assert number, "No number to call"
- self.refresh_session()
- try:
- loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
- except Exception, e:
- loggedIn = False
- self._errorDisplay.push_exception()
- return
-
- if not loggedIn:
- self._errorDisplay.push_message(
- "Backend link with GoogleVoice is not working, please try again"
- )
- return
-
- dialed = False
- try:
- assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
- self._phoneBackends[self._selectedBackendId].call(number)
- hildonize.show_information_banner(self._window, "Calling %s" % number)
- _moduleLogger.info("Calling %s" % number)
- dialed = True
- except Exception, e:
- self._errorDisplay.push_exception()
-
- if dialed:
- self._dialpads[self._selectedBackendId].clear()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _import_contacts(self):
- csvFilter = gtk.FileFilter()
- csvFilter.set_name("Contacts")
- csvFilter.add_pattern("*.csv")
- importFileChooser = gtk.FileChooserDialog(
- title="Contacts",
- parent=self._window,
- )
- importFileChooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
- importFileChooser.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
-
- importFileChooser.set_property("filter", csvFilter)
- userResponse = importFileChooser.run()
- importFileChooser.hide()
- if userResponse == gtk.RESPONSE_OK:
- filename = importFileChooser.get_filename()
- shutil.copy2(filename, self._fsContactsPath)
-
- def _on_menu_refresh(self, *args):
- try:
- self._refresh_active_tab()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_paste(self, *args):
- try:
- contents = self._clipboard.wait_for_text()
- if contents is not None:
- self._dialpads[self._selectedBackendId].set_number(contents)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_about_activate(self, *args):
- try:
- dlg = gtk.AboutDialog()
- dlg.set_name(constants.__pretty_app_name__)
- dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
- dlg.set_copyright("Copyright 2008 - LGPL")
- dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way")
- dlg.set_website("http://gc-dialer.garage.maemo.org/")
- dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <eopage@byu.net>"])
- dlg.run()
- dlg.destroy()
- except Exception, e:
- self._errorDisplay.push_exception()
-
-
-def run_doctest():
- import doctest
-
- failureCount, testCount = doctest.testmod()
- if not failureCount:
- print "Tests Successful"
- sys.exit(0)
- else:
- sys.exit(1)
-
-
-def run_dialpad():
- gtk.gdk.threads_init()
-
- handle = Dialcentral()
- if not PROFILE_STARTUP:
- gtk.main()
-
-
-class DummyOptions(object):
-
- def __init__(self):
- self.test = False
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.DEBUG)
- try:
- if len(sys.argv) > 1:
- try:
- import optparse
- except ImportError:
- optparse = None
-
- if optparse is not None:
- parser = optparse.OptionParser()
- parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
- (commandOptions, commandArgs) = parser.parse_args()
- else:
- commandOptions = DummyOptions()
- commandArgs = []
-
- if commandOptions.test:
- run_doctest()
- else:
- run_dialpad()
- finally:
- logging.shutdown()
+++ /dev/null
-<?xml version="1.0"?>
-<glade-interface>
- <!-- interface-requires gtk+ 2.16 -->
- <!-- interface-naming-policy toplevel-contextual -->
- <widget class="GtkWindow" id="mainWindow">
- <property name="title" translatable="yes">Dialer</property>
- <property name="default_width">800</property>
- <property name="default_height">480</property>
- <child>
- <widget class="GtkVBox" id="mainLayout">
- <property name="visible">True</property>
- <child>
- <widget class="GtkMenuBar" id="dialpad_menubar">
- <property name="visible">True</property>
- <child>
- <widget class="GtkMenuItem" id="login_menu_item">
- <property name="visible">True</property>
- <property name="label">_Login</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_clearcookies_clicked"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="paste_menu_item">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Paste</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_paste"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="refreshMenuItem">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Refresh</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_refresh"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="about">
- <property name="visible">True</property>
- <property name="label" translatable="yes">_About</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_about_activate"/>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEventBox" id="errorEventBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="errorBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="errorImage">
- <property name="visible">True</property>
- <property name="stock">gtk-dialog-error</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="errorDescription">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Unknown Error</property>
- <property name="use_markup">True</property>
- <property name="ellipsize">end</property>
- <property name="single_line_mode">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkImage" id="errorClose">
- <property name="visible">True</property>
- <property name="stock">gtk-close</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkNotebook" id="notebook">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="tab_pos">left</property>
- <property name="show_border">False</property>
- <property name="homogeneous">True</property>
- <child>
- <widget class="GtkVBox" id="keypad_vbox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="hbox3">
- <property name="visible">True</property>
- <child>
- <widget class="GtkButton" id="plus">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="focus_on_click">False</property>
- <accelerator key="0" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox5">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image3">
- <property name="visible">True</property>
- <property name="tooltip" translatable="yes">+</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-add</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="numberdisplay">
- <property name="height_request">50</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="35000" weight="bold"></span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="back">
- <property name="label" translatable="yes">gtk-go-back</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- <property name="focus_on_click">False</property>
- <accelerator key="BackSpace" signal="clicked"/>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="keypadview">
- <property name="visible">True</property>
- <property name="n_rows">4</property>
- <property name="n_columns">3</property>
- <property name="homogeneous">True</property>
- <child>
- <widget class="GtkButton" id="digit1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="1" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label12">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="33000" weight="bold">1</span>
-<span size="9000"> </span></property>
- <property name="use_markup">True</property>
- </widget>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkButton" id="digit2">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="c" signal="clicked"/>
- <accelerator key="b" signal="clicked"/>
- <accelerator key="a" signal="clicked"/>
- <accelerator key="2" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label10">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">2</span>
-<span size="12000">ABC</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="f" signal="clicked"/>
- <accelerator key="e" signal="clicked"/>
- <accelerator key="d" signal="clicked"/>
- <accelerator key="3" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label11">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold" stretch="ultraexpanded">3</span>
-<span size="12000">DEF</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="i" signal="clicked"/>
- <accelerator key="h" signal="clicked"/>
- <accelerator key="g" signal="clicked"/>
- <accelerator key="4" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label13">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">4</span>
-<span size="12000">GHI</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit5">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="l" signal="clicked"/>
- <accelerator key="k" signal="clicked"/>
- <accelerator key="j" signal="clicked"/>
- <accelerator key="5" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label14">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">5</span>
-<span size="12000">JKL</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit6">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="o" signal="clicked"/>
- <accelerator key="n" signal="clicked"/>
- <accelerator key="m" signal="clicked"/>
- <accelerator key="6" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label15">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">6</span>
-<span size="12000">MNO</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit7">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="s" signal="clicked"/>
- <accelerator key="r" signal="clicked"/>
- <accelerator key="q" signal="clicked"/>
- <accelerator key="p" signal="clicked"/>
- <accelerator key="7" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label16">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">7</span>
-<span size="12000">PQRS</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit8">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="v" signal="clicked"/>
- <accelerator key="u" signal="clicked"/>
- <accelerator key="t" signal="clicked"/>
- <accelerator key="8" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label17">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">8</span>
-<span size="12000">TUV</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit9">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="z" signal="clicked"/>
- <accelerator key="y" signal="clicked"/>
- <accelerator key="x" signal="clicked"/>
- <accelerator key="w" signal="clicked"/>
- <accelerator key="9" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label18">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">9</span>
-<span size="12000">WXYZ</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit0">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="0" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label19">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="33000" weight="bold">0</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialpadCall">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <accelerator key="Return" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox1">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image1">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-yes</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label8">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="xpad">5</property>
- <property name="label" translatable="yes"><span size="17000" weight="bold">Call</span></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialpadSMS">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <accelerator key="Return" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox2">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image2">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-select-font</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="xpad">5</property>
- <property name="label" translatable="yes"><span size="17000" weight="bold">SMS</span></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="keypad">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">Keypad</property>
- </widget>
- <packing>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox2">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkButton" id="historyFilterSelector">
- <property name="label" translatable="yes">All</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="history_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <child>
- <widget class="GtkTreeView" id="historyview">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="rules_hint">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="history">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">History</property>
- </widget>
- <packing>
- <property name="position">1</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox3">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkHBox" id="hbox4">
- <property name="visible">True</property>
- <child>
- <widget class="GtkButton" id="messageTypeButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="messageStatusButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="message_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkViewport" id="viewport1">
- <property name="visible">True</property>
- <property name="resize_mode">queue</property>
- <child>
- <widget class="GtkVBox" id="vbox4">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkTreeView" id="messages_view">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="rules_hint">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="messages">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Messages</property>
- </widget>
- <packing>
- <property name="position">2</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox5">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkButton" id="addressbookSelectButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="contacts_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <child>
- <widget class="GtkTreeView" id="contactsview">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="headers_visible">False</property>
- <property name="rules_hint">True</property>
- <property name="fixed_height_mode">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="contacts">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Contacts</property>
- </widget>
- <packing>
- <property name="position">3</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="accountview">
- <property name="visible">True</property>
- <property name="border_width">11</property>
- <property name="n_rows">7</property>
- <property name="n_columns">2</property>
- <child>
- <widget class="GtkLabel" id="gcnumber_display">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">No Number Available</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="gcnumber_label">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="ypad">10</property>
- <property name="label" translatable="yes">Account Number:</property>
- </widget>
- <packing>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="callback_number_label">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="ypad">10</property>
- <property name="label" translatable="yes">Callback Number:</property>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label4">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">4</property>
- <property name="bottom_attach">5</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox1">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkCheckButton" id="missedCheckbox">
- <property name="label" translatable="yes">Missed Calls</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="voicemailCheckbox">
- <property name="label" translatable="yes">Voicemail</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="smsCheckbox">
- <property name="label" translatable="yes">SMS</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">4</property>
- <property name="bottom_attach">5</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="notifyCheckbox">
- <property name="label" translatable="yes">Notifications</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="yalign">0</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="clearcookies">
- <property name="label" translatable="yes">New Login</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="focus_on_click">False</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">6</property>
- <property name="bottom_attach">7</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">5</property>
- <property name="bottom_attach">6</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="minutesEntryButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="y_options">GTK_FILL</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="callbackSelectButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- </widget>
- <packing>
- <property name="position">4</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="account">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">Account</property>
- </widget>
- <packing>
- <property name="position">4</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <widget class="GtkDialog" id="loginDialog">
- <property name="border_width">5</property>
- <property name="title" translatable="yes">Login</property>
- <property name="resizable">False</property>
- <property name="modal">True</property>
- <property name="window_position">center-on-parent</property>
- <property name="destroy_with_parent">True</property>
- <property name="type_hint">dialog</property>
- <property name="skip_taskbar_hint">True</property>
- <property name="skip_pager_hint">True</property>
- <property name="deletable">False</property>
- <property name="transient_for">mainWindow</property>
- <property name="has_separator">False</property>
- <child internal-child="vbox">
- <widget class="GtkVBox" id="loginLayout">
- <property name="visible">True</property>
- <property name="spacing">2</property>
- <child>
- <widget class="GtkComboBox" id="serviceCombo">
- <property name="visible">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="table1">
- <property name="visible">True</property>
- <property name="n_rows">2</property>
- <property name="n_columns">2</property>
- <child>
- <widget class="GtkLabel" id="username_label">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Username</property>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="password_label">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Password</property>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEntry" id="usernameentry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEntry" id="passwordentry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="visibility">False</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- <child internal-child="action_area">
- <widget class="GtkHButtonBox" id="dialog-action_area1">
- <property name="visible">True</property>
- <property name="layout_style">end</property>
- <child>
- <widget class="GtkButton" id="logins_close_button">
- <property name="label" translatable="yes">gtk-cancel</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="loginbutton">
- <property name="label" translatable="yes">gtk-ok</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="pack_type">end</property>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <widget class="GtkWindow" id="smsWindow">
- <property name="title" translatable="yes">SMS Entry</property>
- <child>
- <widget class="GtkVBox" id="smsLayout">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkEventBox" id="smsErrorEventBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="smsErrorBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="smsErrorImage">
- <property name="visible">True</property>
- <property name="stock">gtk-dialog-error</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="smsErrorDescription">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Unknown Error</property>
- <property name="use_markup">True</property>
- <property name="ellipsize">end</property>
- <property name="single_line_mode">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkImage" id="smsErrorClose">
- <property name="visible">True</property>
- <property name="stock">gtk-close</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox1">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkScrolledWindow" id="smsMessages_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">automatic</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkViewport" id="smsMessagesViewPort">
- <property name="visible">True</property>
- <property name="resize_mode">queue</property>
- <child>
- <widget class="GtkVBox" id="smsMessagesLayout">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkTreeView" id="smsMessages">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkHSeparator" id="hseparator1">
- <property name="visible">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="smsTargetList">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">3</property>
- <child>
- <placeholder/>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkFrame" id="frame1">
- <property name="visible">True</property>
- <property name="border_width">5</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <widget class="GtkVBox" id="vbox2">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkScrolledWindow" id="smsMessage_scrolledEntry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkTextView" id="smsEntry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="wrap_mode">word</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="label3">
- <property name="label" translatable="yes"><b>frame1</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkFrame" id="frame2">
- <property name="visible">True</property>
- <property name="border_width">5</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <widget class="GtkHBox" id="smsButtonLayout">
- <property name="visible">True</property>
- <property name="spacing">5</property>
- <child>
- <widget class="GtkHBox" id="smsCountBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkLabel" id="smsLetterCount1">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Letters:</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="smsLetterCount">
- <property name="visible">True</property>
- <property name="label" translatable="yes">0</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="xalign">0.47999998927116394</property>
- <property name="yalign">0.49000000953674316</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="phoneTypeSelection">
- <property name="label" translatable="yes">No Phone Types Available</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label6">
- <property name="visible">True</property>
- <property name="xalign">0.47999998927116394</property>
- <property name="yalign">0.49000000953674316</property>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="sendSmsButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <child>
- <widget class="GtkHBox" id="hbox1">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image1">
- <property name="visible">True</property>
- <property name="stock">gtk-select-font</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="label" translatable="yes">SMS</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <child>
- <widget class="GtkHBox" id="hbox2">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image2">
- <property name="visible">True</property>
- <property name="stock">gtk-apply</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label4">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Dial</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">5</property>
- </packing>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="label1">
- <property name="label" translatable="yes"><b>frame2</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="pack_type">end</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
-</glade-interface>
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Copyright (C) 2007 Christoph Würstle
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+"""
+
import os
import sys
import logging
-_moduleLogger = logging.getLogger("dialcentral")
-sys.path.insert(0,"/opt/dialcentral/lib")
+_moduleLogger = logging.getLogger(__name__)
+sys.path.append("/opt/dialcentral/lib")
import constants
-import dc_glade
+import dialcentral_qt
-try:
- os.makedirs(constants._data_path_)
-except OSError, e:
- if e.errno != 17:
- raise
+if __name__ == "__main__":
+ try:
+ os.makedirs(constants._data_path_)
+ except OSError, e:
+ if e.errno != 17:
+ raise
-logging.basicConfig(level=logging.DEBUG, filename=constants._user_logpath_)
-_moduleLogger.info("Dialcentral %s-%s" % (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])
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, filename=constants._user_logpath_, format=logFormat)
+ _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:
- dc_glade.run_dialpad()
-finally:
- logging.shutdown()
+ dialcentral_qt.run()
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: UTF8 -*-
+
+from __future__ import with_statement
+
+import os
+import base64
+import ConfigParser
+import functools
+import logging
+
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+import constants
+from util import qtpie
+from util import qui_utils
+from util import misc as misc_utils
+
+import session
+
+
+_moduleLogger = logging.getLogger(__name__)
+IS_MAEMO = True
+
+
+class Dialcentral(object):
+
+ _DATA_PATHS = [
+ os.path.join(os.path.dirname(__file__), "../share"),
+ os.path.join(os.path.dirname(__file__), "../data"),
+ ]
+
+ def __init__(self, app):
+ self._app = app
+ self._recent = []
+ self._hiddenCategories = set()
+ self._hiddenUnits = {}
+ self._clipboard = QtGui.QApplication.clipboard()
+ self._dataPath = None
+
+ 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._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._app.lastWindowClosed.connect(self._on_app_quit)
+ self._mainWindow = MainWindow(None, self)
+ 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(lambda: self._mainWindow.start())
+ self._idleDelay.start()
+
+ 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")
+
+ blobs = "", ""
+ isFullscreen = False
+ tabIndex = 0
+ try:
+ blobs = (
+ config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+ for i in xrange(len(self._mainWindow.get_default_credentials()))
+ )
+ isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
+ tabIndex = config.getint(constants.__pretty_app_name__, "tab")
+ 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,
+ ),
+ )
+ return
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+ return
+
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ self._mainWindow.set_default_credentials(*creds)
+ self._fullscreenAction.setChecked(isFullscreen)
+ self._mainWindow.set_current_tab(tabIndex)
+ self._mainWindow.load_settings(config)
+
+ def save_settings(self):
+ _moduleLogger.info("Saving settings")
+ config = ConfigParser.SafeConfigParser()
+
+ config.add_section(constants.__pretty_app_name__)
+ config.set(constants.__pretty_app_name__, "tab", str(self._mainWindow.get_current_tab()))
+ config.set(constants.__pretty_app_name__, "fullscreen", str(self._fullscreenAction.isChecked()))
+ for i, value in enumerate(self._mainWindow.get_default_credentials()):
+ blob = base64.b64encode(value)
+ config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+
+ 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
+
+ @property
+ def fsContactsPath(self):
+ return os.path.join(constants._data_path_, "contacts")
+
+ @property
+ def fullscreenAction(self):
+ return self._fullscreenAction
+
+ @property
+ def logAction(self):
+ return self._logAction
+
+ @property
+ def quitAction(self):
+ return self._quitAction
+
+ 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
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_app_quit(self, checked = False):
+ if self._mainWindow is not None:
+ self.save_settings()
+ self._mainWindow.destroy()
+
+ @QtCore.pyqtSlot(QtCore.QObject)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_child_close(self, obj = None):
+ if self._mainWindow is not None:
+ self.save_settings()
+ self._mainWindow = None
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_toggle_fullscreen(self, checked = False):
+ for window in self._walk_children():
+ window.set_fullscreen(checked)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_log(self, checked = False):
+ with open(constants._user_logpath_, "r") as f:
+ logLines = f.xreadlines()
+ log = "".join(logLines)
+ self._clipboard.setText(log)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_quit(self, checked = False):
+ self._close_windows()
+
+
+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()
+
+ 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(object):
+
+ 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):
+ self._app = app
+
+ self._errorLog = qui_utils.QErrorLog()
+ self._errorDisplay = qui_utils.ErrorDisplay(self._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._defaultCredentials = "", ""
+ self._curentCredentials = "", ""
+ self._currentTab = 0
+
+ self._credentialsDialog = None
+ self._smsEntryDialog = None
+ self._accountDialog = None
+ self._aboutDialog = 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)
+ for tabIndex, (tabTitle, tabIcon) in enumerate(
+ zip(self._TAB_TITLES, self._TAB_ICONS)
+ ):
+ if IS_MAEMO:
+ icon = self._app.get_icon(tabIcon)
+ if icon is None:
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
+ else:
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, "")
+ else:
+ icon = self._app.get_icon(tabIcon)
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
+ self._tabWidget.currentChanged.connect(self._on_tab_changed)
+ self._tabWidget.setContentsMargins(0, 0, 0, 0)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._layout.addWidget(self._errorDisplay.toplevel)
+ self._layout.addWidget(self._tabWidget)
+
+ centralWidget = QtGui.QWidget()
+ centralWidget.setLayout(self._layout)
+ centralWidget.setContentsMargins(0, 0, 0, 0)
+
+ self._window = QtGui.QMainWindow(parent)
+ self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
+ qui_utils.set_autorient(self._window, True)
+ qui_utils.set_stackable(self._window, True)
+ self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
+ self._window.setCentralWidget(centralWidget)
+
+ self._loginTabAction = QtGui.QAction(None)
+ self._loginTabAction.setText("Login")
+ self._loginTabAction.triggered.connect(self._on_login_requested)
+
+ self._importTabAction = QtGui.QAction(None)
+ self._importTabAction.setText("Import")
+ self._importTabAction.triggered.connect(self._on_import)
+
+ self._accountTabAction = QtGui.QAction(None)
+ self._accountTabAction.setText("Account")
+ self._accountTabAction.triggered.connect(self._on_account)
+
+ self._refreshTabAction = QtGui.QAction(None)
+ self._refreshTabAction.setText("Refresh")
+ self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
+ self._refreshTabAction.triggered.connect(self._on_refresh)
+
+ self._aboutAction = QtGui.QAction(None)
+ self._aboutAction.setText("About")
+ self._aboutAction.triggered.connect(self._on_about)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ if IS_MAEMO:
+ fileMenu = self._window.menuBar().addMenu("&File")
+ fileMenu.addAction(self._loginTabAction)
+ fileMenu.addAction(self._refreshTabAction)
+
+ toolsMenu = self._window.menuBar().addMenu("&Tools")
+ toolsMenu.addAction(self._accountTabAction)
+ toolsMenu.addAction(self._importTabAction)
+ toolsMenu.addAction(self._aboutAction)
+
+ self._window.addAction(self._closeWindowAction)
+ self._window.addAction(self._app.quitAction)
+ self._window.addAction(self._app.fullscreenAction)
+ else:
+ fileMenu = self._window.menuBar().addMenu("&File")
+ fileMenu.addAction(self._loginTabAction)
+ fileMenu.addAction(self._refreshTabAction)
+ fileMenu.addAction(self._closeWindowAction)
+ fileMenu.addAction(self._app.quitAction)
+
+ viewMenu = self._window.menuBar().addMenu("&View")
+ viewMenu.addAction(self._app.fullscreenAction)
+
+ toolsMenu = self._window.menuBar().addMenu("&Tools")
+ toolsMenu.addAction(self._accountTabAction)
+ toolsMenu.addAction(self._importTabAction)
+ toolsMenu.addAction(self._aboutAction)
+
+ self._window.addAction(self._app.logAction)
+
+ self._initialize_tab(self._tabWidget.currentIndex())
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+
+ @property
+ def window(self):
+ return self._window
+
+ def set_default_credentials(self, username, password):
+ self._defaultCredentials = username, password
+
+ def get_default_credentials(self):
+ return self._defaultCredentials
+
+ def walk_children(self):
+ return (diag for diag in (
+ self._credentialsDialog,
+ self._smsEntryDialog,
+ self._accountDialog,
+ self._aboutDialog,
+ )
+ if diag is not None
+ )
+
+ def 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 child in self.walk_children():
+ child.close()
+ self._window.close()
+
+ def 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):
+ 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):
+ 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 show(self):
+ self._window.show()
+ for child in self.walk_children():
+ child.show()
+
+ def hide(self):
+ for child in self.walk_children():
+ child.hide()
+ self._window.hide()
+
+ def set_fullscreen(self, isFullscreen):
+ if isFullscreen:
+ self._window.showFullScreen()
+ else:
+ self._window.showNormal()
+ for child in self.walk_children():
+ child.set_fullscreen(isFullscreen)
+
+ 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)
+ username, password = self._credentialsDialog.run(
+ self._defaultCredentials[0], self._defaultCredentials[1], self.window
+ )
+ 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._app)
+ self._accountDialog.set_callbacks(
+ self._session.get_callback_numbers(), self._session.get_callback_number()
+ )
+ self._accountDialog.accountNumber = self._session.get_account_number()
+ response = self._accountDialog.run()
+ if response == QtGui.QDialog.Accepted:
+ if self._accountDialog.doClear:
+ self._session.logout_and_clear()
+ else:
+ callbackNumber = self._accountDialog.selectedCallback
+ self._session.set_callback_number(callbackNumber)
+ elif response == QtGui.QDialog.Rejected:
+ _moduleLogger.info("Cancelled")
+ else:
+ _moduleLogger.info("Unknown response")
+
+ @QtCore.pyqtSlot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_session_error(self, message):
+ with qui_utils.notify_error(self._errorLog):
+ self._errorLog.push_error(message)
+
+ @QtCore.pyqtSlot()
+ @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()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_logout(self):
+ with qui_utils.notify_error(self._errorLog):
+ for tab in self._tabsContents:
+ tab.disable()
+
+ @QtCore.pyqtSlot()
+ @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)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_login_requested(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._prompt_for_login()
+
+ @QtCore.pyqtSlot(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)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(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)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(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)")
+ if not csvName:
+ return
+ import shutil
+ shutil.copy2(csvName, self._app.fsContactsPath)
+ self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_account(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._show_account_dialog()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ 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._app)
+ response = self._aboutDialog.run()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ self.close()
+
+
+def run():
+ app = QtGui.QApplication([])
+ handle = Dialcentral(app)
+ qtpie.init_pies()
+ return app.exec_()
+
+
+if __name__ == "__main__":
+ import sys
+
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ try:
+ os.makedirs(constants._data_path_)
+ except OSError, e:
+ if e.errno != 17:
+ raise
+
+ val = run()
+ sys.exit(val)
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import functools
+import copy
+import logging
+
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+import constants
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class CredentialsDialog(object):
+
+ def __init__(self, app):
+ self._usernameField = QtGui.QLineEdit()
+ self._passwordField = QtGui.QLineEdit()
+ self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
+
+ 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)
+ qui_utils.set_autorient(self._dialog, True)
+ 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:
+ raise RuntimeError("Login Cancelled")
+ else:
+ raise RuntimeError("Unknown Response")
+ finally:
+ self._dialog.setParent(None, QtCore.Qt.Dialog)
+
+ def close(self):
+ self._dialog.reject()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ self._dialog.reject()
+
+
+class AboutDialog(object):
+
+ def __init__(self, app):
+ self._title = QtGui.QLabel(
+ "<h1>%s</h1><h3>Version: %s</h3>" % (
+ constants.__pretty_app_name__, constants.__version__
+ )
+ )
+ self._title.setTextFormat(QtCore.Qt.RichText)
+ self._title.setAlignment(QtCore.Qt.AlignCenter)
+ self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
+ self._copyright.setTextFormat(QtCore.Qt.RichText)
+ self._copyright.setAlignment(QtCore.Qt.AlignCenter)
+ self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
+ self._link.setTextFormat(QtCore.Qt.RichText)
+ self._link.setAlignment(QtCore.Qt.AlignCenter)
+ self._link.setOpenExternalLinks(True)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addWidget(self._title)
+ self._layout.addWidget(self._copyright)
+ self._layout.addWidget(self._link)
+
+ self._dialog = QtGui.QDialog()
+ self._dialog.setWindowTitle("About")
+ self._dialog.setLayout(self._layout)
+ qui_utils.set_autorient(self._dialog, True)
+
+ 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)
+
+ response = self._dialog.exec_()
+ return response
+
+ def close(self):
+ self._dialog.reject()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ self._dialog.reject()
+
+
+class AccountDialog(object):
+
+ # @bug Can't enter custom callback numbers
+
+ def __init__(self, app):
+ self._doClear = False
+
+ self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
+ 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._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(QtGui.QLabel(""), 2, 0)
+ self._credLayout.addWidget(self._clearButton, 2, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
+
+ self._loginButton = QtGui.QPushButton("&Apply")
+ 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("Account")
+ self._dialog.setLayout(self._layout)
+ qui_utils.set_autorient(self._dialog, True)
+ 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)
+
+ @property
+ def doClear(self):
+ return self._doClear
+
+ accountNumber = property(
+ lambda self: str(self._accountNumberLabel.text()),
+ lambda self, num: self._accountNumberLabel.setText(num),
+ )
+
+ @property
+ def selectedCallback(self):
+ index = self._callbackSelector.currentIndex()
+ data = str(self._callbackSelector.itemData(index).toPyObject())
+ return data
+
+ def set_callbacks(self, choices, default):
+ self._callbackSelector.clear()
+
+ self._callbackSelector.addItem("Not Set", "")
+
+ uglyDefault = misc_utils.make_ugly(default)
+ for i, (number, description) in enumerate(choices.iteritems()):
+ prettyNumber = misc_utils.make_pretty(number)
+ uglyNumber = misc_utils.make_ugly(number)
+ if not uglyNumber:
+ continue
+
+ self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
+ if uglyNumber == uglyDefault:
+ self._callbackSelector.setCurrentIndex(i)
+
+
+ def run(self, parent=None):
+ self._doClear = False
+ self._dialog.setParent(parent)
+
+ response = self._dialog.exec_()
+ return response
+
+ def close(self):
+ self._dialog.reject()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ def _on_clear(self, checked = False):
+ self._doClear = True
+ self._dialog.accept()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ self._dialog.reject()
+
+
+class SMSEntryWindow(object):
+
+ MAX_CHAR = 160
+
+ def __init__(self, parent, app, session, errorLog):
+ self._session = session
+ 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.cancelling.connect(self._on_op_started)
+ self._session.draft.calling.connect(self._on_calling_started)
+ self._session.draft.called.connect(self._on_op_finished)
+ self._session.draft.sentMessage.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._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
+
+ self._targetLayout = QtGui.QVBoxLayout()
+ self._targetList = QtGui.QWidget()
+ self._targetList.setLayout(self._targetLayout)
+ self._history = QtGui.QLabel()
+ self._history.setTextFormat(QtCore.Qt.RichText)
+ self._history.setWordWrap(True)
+ self._smsEntry = QtGui.QTextEdit()
+ self._smsEntry.textChanged.connect(self._on_letter_count_changed)
+
+ self._entryLayout = QtGui.QVBoxLayout()
+ self._entryLayout.addWidget(self._targetList)
+ self._entryLayout.addWidget(self._history)
+ 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("0 (0)")
+ self._singleNumberSelector = QtGui.QComboBox()
+ 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.addWidget(self._singleNumberSelector)
+ self._buttonLayout.addWidget(self._smsButton)
+ self._buttonLayout.addWidget(self._dialButton)
+ self._buttonLayout.addWidget(self._cancelButton)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addWidget(self._errorDisplay.toplevel)
+ self._layout.addWidget(self._scrollEntry)
+ self._layout.addLayout(self._buttonLayout)
+
+ centralWidget = QtGui.QWidget()
+ centralWidget.setLayout(self._layout)
+
+ self._window = QtGui.QMainWindow(parent)
+ qui_utils.set_autorient(self._window, True)
+ qui_utils.set_stackable(self._window, True)
+ self._window.setWindowTitle("Contact")
+ 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)
+
+ fileMenu = self._window.menuBar().addMenu("&File")
+ fileMenu.addAction(self._closeWindowAction)
+ fileMenu.addAction(app.quitAction)
+ viewMenu = self._window.menuBar().addMenu("&View")
+ viewMenu.addAction(app.fullscreenAction)
+
+ self._scrollTimer = QtCore.QTimer()
+ self._scrollTimer.setInterval(0)
+ self._scrollTimer.setSingleShot(True)
+ self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
+
+ self._window.show()
+ self._update_recipients()
+
+ def close(self):
+ self._window.destroy()
+ self._window = None
+
+ def _update_letter_count(self):
+ count = self._smsEntry.toPlainText().size()
+ 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):
+ 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 = self._smsEntry.toPlainText().size()
+ 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)
+ self._smsButton.setEnabled(True)
+
+ def _update_recipients(self):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount == 0:
+ self._window.hide()
+ elif draftContactsCount == 1:
+ (cid, ) = self._session.draft.get_contacts()
+ title = self._session.draft.get_title(cid)
+ description = self._session.draft.get_description(cid)
+ numbers = self._session.draft.get_numbers(cid)
+
+ self._targetList.setVisible(False)
+ if description:
+ self._history.setText(description)
+ self._history.setVisible(True)
+ else:
+ self._history.setText("")
+ self._history.setVisible(False)
+ self._populate_number_selector(self._singleNumberSelector, cid, numbers)
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle(title)
+ self._window.show()
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+ else:
+ self._targetList.setVisible(True)
+ while self._targetLayout.count():
+ removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
+ removedWidget = removedLayoutItem.widget()
+ removedWidget.close()
+ for cid in self._session.draft.get_contacts():
+ 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)
+ numberSelector = QtGui.QComboBox()
+ self._populate_number_selector(numberSelector, cid, numbers)
+ deleteButton = QtGui.QPushButton("Delete")
+ callback = functools.partial(
+ self._on_remove_contact,
+ cid
+ )
+ callback.__name__ = "b"
+ deleteButton.clicked.connect(callback)
+
+ rowLayout = QtGui.QHBoxLayout()
+ rowLayout.addWidget(titleLabel)
+ rowLayout.addWidget(numberSelector)
+ rowLayout.addWidget(deleteButton)
+ rowWidget = QtGui.QWidget()
+ rowWidget.setLayout(rowLayout)
+ self._targetLayout.addWidget(rowWidget)
+ self._history.setText("")
+ self._history.setVisible(False)
+ self._singleNumberSelector.setVisible(False)
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle("Contacts")
+ self._window.show()
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+
+ def _populate_number_selector(self, selector, cid, numbers):
+ selector.clear()
+
+ if len(numbers) == 1:
+ numbers, defaultIndex = _get_contact_numbers(self._session, cid, numbers[0])
+ else:
+ defaultIndex = 0
+
+ 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)
+ callback = functools.partial(
+ self._on_change_number,
+ cid
+ )
+ callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
+ selector.currentIndexChanged.connect(
+ QtCore.pyqtSlot(int)(callback)
+ )
+
+ def _scroll_to_bottom(self):
+ self._scrollTimer.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_scroll_to_bottom(self):
+ self._scrollEntry.ensureWidgetVisible(self._smsEntry)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, arg):
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.send(message)
+ self._smsEntry.setPlainText("")
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, arg):
+ self._session.draft.call()
+ self._smsEntry.setPlainText("")
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_cancel_clicked(self, message):
+ self._session.draft.cancel()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_remove_contact(self, cid, toggled):
+ self._session.draft.remove_contact(cid)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_change_number(self, cid, index):
+ # Exception thrown when the first item is removed
+ numbers = self._session.draft.get_numbers(cid)
+ number = numbers[index][0]
+ self._session.draft.set_selected_number(cid, number)
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ self._update_recipients()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_started(self):
+ self._smsEntry.setReadOnly(True)
+ self._smsButton.setVisible(False)
+ self._dialButton.setVisible(False)
+ self._window.show()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_calling_started(self):
+ self._cancelButton.setVisible(True)
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_finished(self):
+ self._window.hide()
+
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_error(self, message):
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+
+ self._errorLog.push_error(message)
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_letter_count_changed(self):
+ self._update_letter_count()
+ self._update_button_state()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ self._window.hide()
+
+
+def _get_contact_numbers(session, contactId, numberDescription):
+ contactPhoneNumbers = []
+ if contactId and contactId != "0":
+ try:
+ contactDetails = copy.deepcopy(session.get_contacts()[contactId])
+ contactPhoneNumbers = contactDetails["numbers"]
+ except KeyError:
+ contactPhoneNumbers = []
+ contactPhoneNumbers = [
+ (contactPhoneNumber["phoneNumber"], contactPhoneNumber["phoneType"])
+ for contactPhoneNumber in contactPhoneNumbers
+ ]
+ if contactPhoneNumbers:
+ uglyContactNumbers = (
+ misc_utils.make_ugly(contactNumber)
+ for (contactNumber, _) in contactPhoneNumbers
+ )
+ defaultMatches = [
+ misc_utils.similar_ugly_numbers(numberDescription[0], contactNumber)
+ for contactNumber in uglyContactNumbers
+ ]
+ try:
+ defaultIndex = defaultMatches.index(True)
+ except ValueError:
+ contactPhoneNumbers.append(numberDescription)
+ defaultIndex = len(contactPhoneNumbers)-1
+ _moduleLogger.warn(
+ "Could not find contact %r's number %s among %r" % (
+ contactId, numberDescription, contactPhoneNumbers
+ )
+ )
+
+ if not contactPhoneNumbers:
+ contactPhoneNumbers = [numberDescription]
+ defaultIndex = -1
+
+ return contactPhoneNumbers, defaultIndex
+++ /dev/null
-#!/usr/bin/python
-
-from __future__ import with_statement
-
-import os
-import errno
-import sys
-import time
-import itertools
-import functools
-import contextlib
-import logging
-import threading
-import Queue
-
-import gobject
-import gtk
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-def get_screen_orientation():
- width, height = gtk.gdk.get_default_root_window().get_size()
- if width < height:
- return gtk.ORIENTATION_VERTICAL
- else:
- return gtk.ORIENTATION_HORIZONTAL
-
-
-def orientation_change_connect(handler, *args):
- """
- @param handler(orientation, *args) -> None(?)
- """
- initialScreenOrientation = get_screen_orientation()
- orientationAndArgs = list(itertools.chain((initialScreenOrientation, ), args))
-
- def _on_screen_size_changed(screen):
- newScreenOrientation = get_screen_orientation()
- if newScreenOrientation != orientationAndArgs[0]:
- orientationAndArgs[0] = newScreenOrientation
- handler(*orientationAndArgs)
-
- rootScreen = gtk.gdk.get_default_root_window()
- return gtk.connect(rootScreen, "size-changed", _on_screen_size_changed)
-
-
-@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)
-
-
-@contextlib.contextmanager
-def gtk_lock():
- gtk.gdk.threads_enter()
- try:
- yield
- finally:
- gtk.gdk.threads_leave()
-
-
-def find_parent_window(widget):
- while True:
- parent = widget.get_parent()
- if isinstance(parent, gtk.Window):
- return parent
- widget = parent
-
-
-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 asynchronous_gtk_message(original_func):
- """
- @note Idea came from http://www.aclevername.com/articles/python-webgui/
- """
-
- def execute(allArgs):
- args, kwargs = allArgs
- with gtk_lock():
- original_func(*args, **kwargs)
- return False
-
- @functools.wraps(original_func)
- def delayed_func(*args, **kwargs):
- gobject.idle_add(execute, (args, kwargs))
-
- return delayed_func
-
-
-def synchronous_gtk_message(original_func):
- """
- @note Idea came from http://www.aclevername.com/articles/python-webgui/
- """
-
- @functools.wraps(original_func)
- def immediate_func(*args, **kwargs):
- with gtk_lock():
- return original_func(*args, **kwargs)
-
- return immediate_func
-
-
-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
-
-
-@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
- """
- while True:
- try:
- item = yield
- mappedItem = function(*item)
- target.send(mappedItem)
- except Exception, e:
- _moduleLogger.exception("Forwarding exception!")
- target.throw(e.__class__, str(e))
-
-
-def _flush_queue(queue):
- while not queue.empty():
- yield queue.get()
-
-
-@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'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]
- """
- while True:
- try:
- item = yield
- queue.put((None, item))
- except Exception, e:
- queue.put((e.__class__, str(e)))
- 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 nonqueue_source(queue, target):
- isDone = False
- while not isDone:
- item = queue.get()
- isDone = decode_item(item, target)
- while not queue.empty():
- queue.get_nowait()
-
-
-def threaded_stage(target, thread_factory = threading.Thread):
- messages = Queue.Queue()
-
- run_source = functools.partial(nonqueue_source, messages, target)
- thread = thread_factory(target=run_source)
- thread.setDaemon(True)
- thread.start()
-
- # Sink running in current thread
- return queue_sink(messages)
-
-
-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__)
-
- return wrapper
-
- return log_exception_decorator
-
-
-class LoginWindow(object):
-
- def __init__(self, widgetTree):
- """
- @note Thread agnostic
- """
- self._dialog = widgetTree.get_widget("loginDialog")
- self._parentWindow = widgetTree.get_widget("mainWindow")
- self._serviceCombo = widgetTree.get_widget("serviceCombo")
- self._usernameEntry = widgetTree.get_widget("usernameentry")
- self._passwordEntry = widgetTree.get_widget("passwordentry")
-
- self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
- self._serviceCombo.set_model(self._serviceList)
- cell = gtk.CellRendererText()
- self._serviceCombo.pack_start(cell, True)
- self._serviceCombo.add_attribute(cell, 'text', 1)
- self._serviceCombo.set_active(0)
-
- widgetTree.get_widget("loginbutton").connect("clicked", self._on_loginbutton_clicked)
- widgetTree.get_widget("logins_close_button").connect("clicked", self._on_loginclose_clicked)
-
- def request_credentials(self,
- parentWindow = None,
- defaultCredentials = ("", "")
- ):
- """
- @note UI Thread
- """
- if parentWindow is None:
- parentWindow = self._parentWindow
-
- self._serviceCombo.hide()
- self._serviceList.clear()
-
- self._usernameEntry.set_text(defaultCredentials[0])
- self._passwordEntry.set_text(defaultCredentials[1])
-
- try:
- self._dialog.set_transient_for(parentWindow)
- self._dialog.set_default_response(gtk.RESPONSE_OK)
- response = self._dialog.run()
- if response != gtk.RESPONSE_OK:
- raise RuntimeError("Login Cancelled")
-
- username = self._usernameEntry.get_text()
- password = self._passwordEntry.get_text()
- self._passwordEntry.set_text("")
- finally:
- self._dialog.hide()
-
- return username, password
-
- def request_credentials_from(self,
- services,
- parentWindow = None,
- defaultCredentials = ("", "")
- ):
- """
- @note UI Thread
- """
- if parentWindow is None:
- parentWindow = self._parentWindow
-
- self._serviceList.clear()
- for serviceIdserviceName in services:
- self._serviceList.append(serviceIdserviceName)
- self._serviceCombo.set_active(0)
- self._serviceCombo.show()
-
- self._usernameEntry.set_text(defaultCredentials[0])
- self._passwordEntry.set_text(defaultCredentials[1])
-
- try:
- self._dialog.set_transient_for(parentWindow)
- self._dialog.set_default_response(gtk.RESPONSE_OK)
- response = self._dialog.run()
- if response != gtk.RESPONSE_OK:
- raise RuntimeError("Login Cancelled")
-
- username = self._usernameEntry.get_text()
- password = self._passwordEntry.get_text()
- finally:
- self._dialog.hide()
-
- itr = self._serviceCombo.get_active_iter()
- serviceId = int(self._serviceList.get_value(itr, 0))
- self._serviceList.clear()
- return serviceId, username, password
-
- def _on_loginbutton_clicked(self, *args):
- self._dialog.response(gtk.RESPONSE_OK)
-
- def _on_loginclose_clicked(self, *args):
- self._dialog.response(gtk.RESPONSE_CANCEL)
-
-
-def safecall(f, errorDisplay=None, default=None, exception=Exception):
- '''
- Returns modified f. When the modified f is called and throws an
- exception, the default value is returned
- '''
- def _safecall(*args, **argv):
- try:
- return f(*args,**argv)
- except exception, e:
- if errorDisplay is not None:
- errorDisplay.push_exception(e)
- return default
- return _safecall
-
-
-class ErrorDisplay(object):
-
- def __init__(self, errorBox, errorDescription, errorClose):
- super(ErrorDisplay, self).__init__()
- self.__errorBox = errorBox
- self.__errorDescription = errorDescription
- self.__errorClose = errorClose
- self.__parentBox = self.__errorBox.get_parent()
-
- self.__errorBox.connect("button_release_event", self._on_close)
-
- self.__messages = []
- self.__parentBox.remove(self.__errorBox)
-
- def push_message_with_lock(self, message):
- with gtk_lock():
- self.push_message(message)
-
- def push_message(self, message):
- self.__messages.append(message)
- if 1 == len(self.__messages):
- self.__show_message(message)
-
- def push_exception_with_lock(self):
- with gtk_lock():
- self.push_exception()
-
- def push_exception(self):
- userMessage = str(sys.exc_info()[1])
- self.push_message(userMessage)
- _moduleLogger.exception(userMessage)
-
- def pop_message(self):
- del self.__messages[0]
- if 0 == len(self.__messages):
- self.__hide_message()
- else:
- self.__errorDescription.set_text(self.__messages[0])
-
- def _on_close(self, *args):
- self.pop_message()
-
- def __show_message(self, message):
- self.__errorDescription.set_text(message)
- self.__parentBox.pack_start(self.__errorBox, False, False)
- self.__parentBox.reorder_child(self.__errorBox, 1)
-
- def __hide_message(self):
- self.__errorDescription.set_text("")
- self.__parentBox.remove(self.__errorBox)
-
-
-class DummyErrorDisplay(object):
-
- def __init__(self):
- super(DummyErrorDisplay, self).__init__()
-
- self.__messages = []
-
- def push_message_with_lock(self, message):
- self.push_message(message)
-
- def push_message(self, message):
- if 0 < len(self.__messages):
- self.__messages.append(message)
- else:
- self.__show_message(message)
-
- def push_exception(self, exception = None):
- userMessage = str(sys.exc_value)
- _moduleLogger.exception(userMessage)
-
- def pop_message(self):
- if 0 < len(self.__messages):
- self.__show_message(self.__messages[0])
- del self.__messages[0]
-
- def __show_message(self, message):
- _moduleLogger.debug(message)
-
-
-class MessageBox(gtk.MessageDialog):
-
- def __init__(self, message):
- parent = None
- gtk.MessageDialog.__init__(
- self,
- parent,
- gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
- gtk.MESSAGE_ERROR,
- gtk.BUTTONS_OK,
- message,
- )
- self.set_default_response(gtk.RESPONSE_OK)
- self.connect('response', self._handle_clicked)
-
- def _handle_clicked(self, *args):
- self.destroy()
-
-
-class MessageBox2(gtk.MessageDialog):
-
- def __init__(self, message):
- parent = None
- gtk.MessageDialog.__init__(
- self,
- parent,
- gtk.DIALOG_DESTROY_WITH_PARENT,
- gtk.MESSAGE_ERROR,
- gtk.BUTTONS_OK,
- message,
- )
- self.set_default_response(gtk.RESPONSE_OK)
- self.connect('response', self._handle_clicked)
-
- def _handle_clicked(self, *args):
- self.destroy()
-
-
-class PopupCalendar(object):
-
- def __init__(self, parent, displayDate, title = ""):
- self._displayDate = displayDate
-
- self._calendar = gtk.Calendar()
- self._calendar.select_month(self._displayDate.month, self._displayDate.year)
- self._calendar.select_day(self._displayDate.day)
- self._calendar.set_display_options(
- gtk.CALENDAR_SHOW_HEADING |
- gtk.CALENDAR_SHOW_DAY_NAMES |
- gtk.CALENDAR_NO_MONTH_CHANGE |
- 0
- )
- self._calendar.connect("day-selected", self._on_day_selected)
-
- self._popupWindow = gtk.Window()
- self._popupWindow.set_title(title)
- self._popupWindow.add(self._calendar)
- self._popupWindow.set_transient_for(parent)
- self._popupWindow.set_modal(True)
- self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
- self._popupWindow.set_skip_pager_hint(True)
- self._popupWindow.set_skip_taskbar_hint(True)
-
- def run(self):
- self._popupWindow.show_all()
-
- def _on_day_selected(self, *args):
- try:
- self._calendar.select_month(self._displayDate.month, self._displayDate.year)
- self._calendar.select_day(self._displayDate.day)
- except Exception, e:
- _moduleLogger.exception(e)
-
-
-class QuickAddView(object):
-
- def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
- self._errorDisplay = errorDisplay
- self._manager = None
- self._signalSink = signalSink
-
- self._clipboard = gtk.clipboard_get()
-
- self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
- self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
- self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
- self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
- self._onAddId = None
- self._onAddClickedId = None
- self._onAddReleasedId = None
- self._addToEditTimerId = None
- self._onClearId = None
- self._onPasteId = None
-
- def enable(self, manager):
- self._manager = manager
-
- self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
- self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
- self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
- self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
- self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
-
- def disable(self):
- self._manager = None
-
- self._addTaskButton.disconnect(self._onAddId)
- self._addTaskButton.disconnect(self._onAddClickedId)
- self._addTaskButton.disconnect(self._onAddReleasedId)
- self._pasteTaskNameButton.disconnect(self._onPasteId)
- self._clearTaskNameButton.disconnect(self._onClearId)
-
- def set_addability(self, addability):
- self._addTaskButton.set_sensitive(addability)
-
- def _on_add(self, *args):
- try:
- name = self._taskNameEntry.get_text()
- self._taskNameEntry.set_text("")
-
- self._signalSink.stage.send(("add", name))
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_add_edit(self, *args):
- try:
- name = self._taskNameEntry.get_text()
- self._taskNameEntry.set_text("")
-
- self._signalSink.stage.send(("add-edit", name))
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_add_pressed(self, widget):
- try:
- self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_add_released(self, widget):
- try:
- if self._addToEditTimerId is not None:
- gobject.source_remove(self._addToEditTimerId)
- self._addToEditTimerId = None
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_paste(self, *args):
- try:
- entry = self._taskNameEntry.get_text()
- addedText = self._clipboard.wait_for_text()
- if addedText:
- entry += addedText
- self._taskNameEntry.set_text(entry)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_clear(self, *args):
- try:
- self._taskNameEntry.set_text("")
- except Exception, e:
- self._errorDisplay.push_exception()
-
-
-class TapOrHold(object):
-
- def __init__(self, widget):
- self._widget = widget
- self._isTap = True
- self._isPointerInside = True
- self._holdTimeoutId = None
- self._tapTimeoutId = None
- self._taps = 0
-
- self._bpeId = None
- self._breId = None
- self._eneId = None
- self._lneId = None
-
- def enable(self):
- self._bpeId = self._widget.connect("button-press-event", self._on_button_press)
- self._breId = self._widget.connect("button-release-event", self._on_button_release)
- self._eneId = self._widget.connect("enter-notify-event", self._on_enter)
- self._lneId = self._widget.connect("leave-notify-event", self._on_leave)
-
- def disable(self):
- self._widget.disconnect(self._bpeId)
- self._widget.disconnect(self._breId)
- self._widget.disconnect(self._eneId)
- self._widget.disconnect(self._lneId)
-
- def on_tap(self, taps):
- print "TAP", taps
-
- def on_hold(self, taps):
- print "HOLD", taps
-
- def on_holding(self):
- print "HOLDING"
-
- def on_cancel(self):
- print "CANCEL"
-
- def _on_button_press(self, *args):
- # Hack to handle weird notebook behavior
- self._isPointerInside = True
- self._isTap = True
-
- if self._tapTimeoutId is not None:
- gobject.source_remove(self._tapTimeoutId)
- self._tapTimeoutId = None
-
- # Handle double taps
- if self._holdTimeoutId is None:
- self._tapTimeoutId = None
-
- self._taps = 1
- self._holdTimeoutId = gobject.timeout_add(1000, self._on_hold_timeout)
- else:
- self._taps = 2
-
- def _on_button_release(self, *args):
- assert self._tapTimeoutId is None
- # Handle release after timeout if user hasn't double-clicked
- self._tapTimeoutId = gobject.timeout_add(100, self._on_tap_timeout)
-
- def _on_actual_press(self, *args):
- if self._holdTimeoutId is not None:
- gobject.source_remove(self._holdTimeoutId)
- self._holdTimeoutId = None
-
- if self._isPointerInside:
- if self._isTap:
- self.on_tap(self._taps)
- else:
- self.on_hold(self._taps)
- else:
- self.on_cancel()
-
- def _on_tap_timeout(self, *args):
- self._tapTimeoutId = None
- self._on_actual_press()
- return False
-
- def _on_hold_timeout(self, *args):
- self._holdTimeoutId = None
- self._isTap = False
- self.on_holding()
- return False
-
- def _on_enter(self, *args):
- self._isPointerInside = True
-
- def _on_leave(self, *args):
- self._isPointerInside = False
-
-
-if __name__ == "__main__":
- if False:
- import datetime
- cal = PopupCalendar(None, datetime.datetime.now())
- cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
- cal.run()
-
- gtk.main()
#!/usr/bin/env python
-"""
-DialCentral - Front end for Google's GoogleVoice service.
-Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-@todo Collapse voicemails
-"""
-
from __future__ import with_statement
+from __future__ import division
-import re
-import ConfigParser
+import datetime
+import string
import itertools
import logging
-import gobject
-import pango
-import gtk
-
-import gtk_toolbox
-import hildonize
-from backends import gv_backend
-from backends import null_backend
-
-
-_moduleLogger = logging.getLogger("gv_views")
-
-
-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 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")
- uglynumber = "+1"+uglynumber
- else:
- pass
-
- return uglynumber
-
-
-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 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 _collapse_message(messageLines, maxCharsPerLine, maxLines):
- lines = 0
-
- numLines = len(messageLines)
- for line in messageLines[0:min(maxLines, numLines)]:
- linesPerLine = max(1, int(len(line) / maxCharsPerLine))
- allowedLines = maxLines - lines
- acceptedLines = min(allowedLines, linesPerLine)
- acceptedChars = acceptedLines * maxCharsPerLine
-
- if acceptedChars < (len(line) + 3):
- suffix = "..."
- else:
- acceptedChars = len(line) # eh, might as well complete the line
- suffix = ""
- abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
- yield abbrevMessage
-
- lines += acceptedLines
- if maxLines <= lines:
- break
-
-
-def collapse_message(message, maxCharsPerLine, maxLines):
- r"""
- >>> collapse_message("Hello", 60, 2)
- 'Hello'
- >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
- 'Hello world how are you doing today? 01234567890123456789012...'
- >>> collapse_message('''Hello world how are you doing today?
- ... 01234567890123456789
- ... 01234567890123456789
- ... 01234567890123456789
- ... 01234567890123456789''', 60, 2)
- 'Hello world how are you doing today?\n01234567890123456789'
- >>> collapse_message('''
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
- ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
- '\nHello world how are you doing today? 01234567890123456789012...'
- """
- messageLines = message.split("\n")
- return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
-
-
-def _get_contact_numbers(backend, contactId, number):
- contactPhoneNumbers = []
- if contactId and contactId != '0':
- try:
- contactPhoneNumbers = list(backend.get_contact_details(contactId))
- except KeyError:
- contactPhoneNumbers = []
- if contactPhoneNumbers:
- uglyContactNumbers = (
- make_ugly(contactNumber)
- for (numberDescription, contactNumber) in contactPhoneNumbers
- )
- defaultMatches = [
- (
- number == contactNumber or
- number[1:] == contactNumber and number.startswith("1") or
- number[2:] == contactNumber and number.startswith("+1") or
- number == contactNumber[1:] and contactNumber.startswith("1") or
- number == contactNumber[2:] and contactNumber.startswith("+1")
- )
- for contactNumber in uglyContactNumbers
- ]
- try:
- defaultIndex = defaultMatches.index(True)
- except ValueError:
- contactPhoneNumbers.append(("Other", number))
- defaultIndex = len(contactPhoneNumbers)-1
- _moduleLogger.warn(
- "Could not find contact %r's number %s among %r" % (
- contactId, number, contactPhoneNumbers
- )
- )
-
- if not contactPhoneNumbers:
- contactPhoneNumbers = [("Phone", number)]
- defaultIndex = -1
-
- return contactPhoneNumbers, defaultIndex
-
-
-class SmsEntryWindow(object):
-
- MAX_CHAR = 160
-
- def __init__(self, widgetTree, parent, app):
- self._clipboard = gtk.clipboard_get()
- self._widgetTree = widgetTree
- self._parent = parent
- self._app = app
- self._isFullScreen = False
-
- self._window = self._widgetTree.get_widget("smsWindow")
- self._window = hildonize.hildonize_window(self._app, self._window)
- self._window.set_title("SMS")
- self._window.connect("delete-event", self._on_delete)
- self._window.connect("key-press-event", self._on_key_press)
- self._window.connect("window-state-event", self._on_window_state_change)
- self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
-
- errorBox = self._widgetTree.get_widget("smsErrorEventBox")
- errorDescription = self._widgetTree.get_widget("smsErrorDescription")
- errorClose = self._widgetTree.get_widget("smsErrorClose")
- self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
-
- self._smsButton = self._widgetTree.get_widget("sendSmsButton")
- self._smsButton.connect("clicked", self._on_send)
- self._dialButton = self._widgetTree.get_widget("dialButton")
- self._dialButton.connect("clicked", self._on_dial)
-
- self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
-
- self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
- self._messagesView = self._widgetTree.get_widget("smsMessages")
-
- textrenderer = gtk.CellRendererText()
- textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
- textrenderer.set_property("wrap-width", 450)
- messageColumn = gtk.TreeViewColumn("")
- messageColumn.pack_start(textrenderer, expand=True)
- messageColumn.add_attribute(textrenderer, "markup", 0)
- messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
- self._messagesView.append_column(messageColumn)
- self._messagesView.set_headers_visible(False)
- self._messagesView.set_model(self._messagemodel)
- self._messagesView.set_fixed_height_mode(False)
-
- self._conversationView = self._messagesView.get_parent()
- self._conversationViewPort = self._conversationView.get_parent()
- self._scrollWindow = self._conversationViewPort.get_parent()
-
- self._targetList = self._widgetTree.get_widget("smsTargetList")
- self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
- self._phoneButton.connect("clicked", self._on_phone)
- self._smsEntry = self._widgetTree.get_widget("smsEntry")
- self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
- self._smsEntrySize = None
-
- self._contacts = []
-
- def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
- contactNumbers = list(self._to_contact_numbers(contactDetails))
- assert contactNumbers, "Contact must have at least one number"
- contactIndex = defaultIndex if defaultIndex != -1 else 0
- contact = contactNumbers, contactIndex, messages
- self._contacts.append(contact)
-
- nameLabel = gtk.Label(name)
- selector = gtk.Button(contactNumbers[0][1])
- if len(contactNumbers) == 1:
- selector.set_sensitive(False)
- removeContact = gtk.Button(stock="gtk-delete")
- row = gtk.HBox()
- row.pack_start(nameLabel, True, True)
- row.pack_start(selector, True, True)
- row.pack_start(removeContact, False, False)
- row.show_all()
- self._targetList.pack_start(row)
- selector.connect("clicked", self._on_choose_phone_n, row)
- removeContact.connect("clicked", self._on_remove_phone_n, row)
- self._update_button_state()
- self._update_context()
-
- parentSize = self._parent.get_size()
- self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
- self._window.show()
- self._window.present()
-
- self._smsEntry.grab_focus()
- self._scroll_to_bottom()
-
- def clear(self):
- del self._contacts[:]
-
- for row in list(self._targetList.get_children()):
- self._targetList.remove(row)
- self._smsEntry.get_buffer().set_text("")
- self._update_letter_count()
- self._update_context()
-
- def fullscreen(self):
- self._window.fullscreen()
-
- def unfullscreen(self):
- self._window.unfullscreen()
-
- def _remove_contact(self, contactIndex):
- del self._contacts[contactIndex]
-
- row = list(self._targetList.get_children())[contactIndex]
- self._targetList.remove(row)
- self._update_button_state()
- self._update_context()
- self._scroll_to_bottom()
-
- def _scroll_to_bottom(self):
- dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
- dx = max(dx, 0)
- adjustment = self._scrollWindow.get_vadjustment()
- adjustment.value = dx
-
- def _update_letter_count(self):
- if self._smsEntrySize is None:
- self._smsEntrySize = self._smsEntry.size_request()
- else:
- self._smsEntry.set_size_request(*self._smsEntrySize)
- entryLength = self._smsEntry.get_buffer().get_char_count()
-
- numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
- if numTexts:
- self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
- else:
- self._letterCountLabel.set_text("%s" % (numCharInText, ))
-
- self._update_button_state()
-
- def _update_context(self):
- self._messagemodel.clear()
- if len(self._contacts) == 0:
- self._messagesView.hide()
- self._targetList.hide()
- self._phoneButton.hide()
- self._phoneButton.set_label("Error: You shouldn't see this")
- elif len(self._contacts) == 1:
- contactNumbers, index, messages = self._contacts[0]
- if messages:
- self._messagesView.show()
- for message in messages:
- row = (message, )
- self._messagemodel.append(row)
- messagesSelection = self._messagesView.get_selection()
- messagesSelection.select_path((len(messages)-1, ))
- else:
- self._messagesView.hide()
- self._targetList.hide()
- self._phoneButton.show()
- self._phoneButton.set_label(contactNumbers[index][1])
- if 1 < len(contactNumbers):
- self._phoneButton.set_sensitive(True)
- else:
- self._phoneButton.set_sensitive(False)
- else:
- self._messagesView.hide()
- self._targetList.show()
- self._phoneButton.hide()
- self._phoneButton.set_label("Error: You shouldn't see this")
-
- def _update_button_state(self):
- if len(self._contacts) == 0:
- self._dialButton.set_sensitive(False)
- self._smsButton.set_sensitive(False)
- elif len(self._contacts) == 1:
- entryLength = self._smsEntry.get_buffer().get_char_count()
- if entryLength == 0:
- self._dialButton.set_sensitive(True)
- self._smsButton.set_sensitive(False)
- else:
- self._dialButton.set_sensitive(False)
- self._smsButton.set_sensitive(True)
- else:
- self._dialButton.set_sensitive(False)
- self._smsButton.set_sensitive(True)
-
- def _to_contact_numbers(self, contactDetails):
- for phoneType, phoneNumber in contactDetails:
- display = " - ".join((make_pretty(phoneNumber), phoneType))
- yield (phoneNumber, display)
-
- def _pseudo_destroy(self):
- self.clear()
- self._window.hide()
-
- def _request_number(self, contactIndex):
- contactNumbers, index, messages = self._contacts[contactIndex]
- assert 0 <= index, "%r" % index
-
- index = hildonize.touch_selector(
- self._window,
- "Phone Numbers",
- (description for (number, description) in contactNumbers),
- index,
- )
- self._contacts[contactIndex] = contactNumbers, index, messages
-
- def send_sms(self, numbers, message):
- raise NotImplementedError()
-
- def dial(self, number):
- raise NotImplementedError()
-
- def _on_phone(self, *args):
- try:
- assert len(self._contacts) == 1, "One and only one contact is required"
- self._request_number(0)
-
- contactNumbers, numberIndex, messages = self._contacts[0]
- self._phoneButton.set_label(contactNumbers[numberIndex][1])
- row = list(self._targetList.get_children())[0]
- phoneButton = list(row.get_children())[1]
- phoneButton.set_label(contactNumbers[numberIndex][1])
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_choose_phone_n(self, button, row):
- try:
- assert 1 < len(self._contacts), "More than one contact required"
- targetList = list(self._targetList.get_children())
- index = targetList.index(row)
- self._request_number(index)
-
- contactNumbers, numberIndex, messages = self._contacts[0]
- phoneButton = list(row.get_children())[1]
- phoneButton.set_label(contactNumbers[numberIndex][1])
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_remove_phone_n(self, button, row):
- try:
- assert 1 < len(self._contacts), "More than one contact required"
- targetList = list(self._targetList.get_children())
- index = targetList.index(row)
-
- del self._contacts[index]
- self._targetList.remove(row)
- self._update_context()
- self._update_button_state()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_entry_changed(self, *args):
- try:
- self._update_letter_count()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_send(self, *args):
- try:
- assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
- phoneNumbers = [
- make_ugly(contact[0][contact[1]][0])
- for contact in self._contacts
- ]
+from PyQt4 import QtGui
+from PyQt4 import QtCore
- entryBuffer = self._smsEntry.get_buffer()
- enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
- enteredMessage = enteredMessage.strip()
- assert enteredMessage, "No message provided"
- self.send_sms(phoneNumbers, enteredMessage)
- self._pseudo_destroy()
- except Exception, e:
- self._errorDisplay.push_exception()
+from util import qtpie
+from util import qui_utils
+from util import misc as misc_utils
- def _on_dial(self, *args):
- try:
- assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
- contact = self._contacts[0]
- contactNumber = contact[0][contact[1]][0]
- phoneNumber = make_ugly(contactNumber)
- self.dial(phoneNumber)
- self._pseudo_destroy()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_delete(self, *args):
- try:
- self._window.emit_stop_by_name("delete-event")
- if hildonize.IS_FREMANTLE_SUPPORTED:
- self._window.hide()
- else:
- self._pseudo_destroy()
- except Exception, e:
- self._errorDisplay.push_exception()
- return True
+import backends.null_backend as null_backend
+import backends.file_backend as file_backend
- def _on_window_state_change(self, widget, event, *args):
- try:
- if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
- self._isFullScreen = True
- else:
- self._isFullScreen = False
- except Exception, e:
- self._errorDisplay.push_exception()
- def _on_key_press(self, widget, event):
- RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
- try:
- if (
- event.keyval == gtk.keysyms.F6 or
- event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- if self._isFullScreen:
- self._window.unfullscreen()
- else:
- self._window.fullscreen()
- elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
- message = "\n".join(
- messagePart[0]
- for messagePart in self._messagemodel
- )
- self._clipboard.set_text(str(message))
- elif (
- event.keyval == gtk.keysyms.h and
- event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- self._window.hide()
- elif (
- event.keyval == gtk.keysyms.w and
- event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- self._pseudo_destroy()
- elif (
- event.keyval == gtk.keysyms.q and
- event.get_state() & gtk.gdk.CONTROL_MASK
- ):
- self._parent.destroy()
- except Exception, e:
- self._errorDisplay.push_exception()
+_moduleLogger = logging.getLogger(__name__)
class Dialpad(object):
- def __init__(self, widgetTree, errorDisplay):
- self._clipboard = gtk.clipboard_get()
- self._errorDisplay = errorDisplay
-
- self._numberdisplay = widgetTree.get_widget("numberdisplay")
- self._callButton = widgetTree.get_widget("dialpadCall")
- self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
- self._backButton = widgetTree.get_widget("back")
- self._plusButton = widgetTree.get_widget("plus")
- self._phonenumber = ""
- self._prettynumber = ""
-
- callbackMapping = {
- "on_digit_clicked": self._on_digit_clicked,
- }
- widgetTree.signal_autoconnect(callbackMapping)
- self._sendSMSButton.connect("clicked", self._on_sms_clicked)
- self._callButton.connect("clicked", self._on_call_clicked)
- self._plusButton.connect("clicked", self._on_plus)
-
- self._originalLabel = self._backButton.get_label()
- self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
- self._backTapHandler.on_tap = self._on_backspace
- self._backTapHandler.on_hold = self._on_clearall
- self._backTapHandler.on_holding = self._set_clear_button
- self._backTapHandler.on_cancel = self._reset_back_button
-
- self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
- self._keyPressEventId = 0
+ 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._sendSMSButton.grab_focus()
- self._backTapHandler.enable()
- self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
+ self._smsButton.setEnabled(True)
+ self._callButton.setEnabled(True)
def disable(self):
- self._window.disconnect(self._keyPressEventId)
- self._keyPressEventId = 0
- self._reset_back_button()
- self._backTapHandler.disable()
-
- def add_contact(self, *args, **kwds):
- """
- @note Actual function is patched in later
- """
- raise NotImplementedError("Horrible unknown error has occurred")
-
- def dial(self, number):
- """
- @note Actual function is patched in later
- """
- raise NotImplementedError("Horrible unknown error has occurred")
-
- def get_number(self):
- return self._phonenumber
-
- def set_number(self, number):
- """
- Set the number to dial
- """
- try:
- self._phonenumber = make_ugly(number)
- self._prettynumber = make_pretty(self._phonenumber)
- self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
- if self._phonenumber:
- self._plusButton.set_sensitive(False)
- else:
- self._plusButton.set_sensitive(True)
- except TypeError, e:
- self._errorDisplay.push_exception()
-
- def clear(self):
- self.set_number("")
-
- @staticmethod
- def name():
- return "Dialpad"
+ self._smsButton.setEnabled(False)
+ self._callButton.setEnabled(False)
- def load_settings(self, config, section):
- pass
+ def get_settings(self):
+ return {}
- def save_settings(self, config, section):
- """
- @note Thread Agnostic
- """
+ def set_settings(self, settings):
pass
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
- def _on_key_press(self, widget, event):
- try:
- if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
- contents = self._clipboard.wait_for_text()
- if contents is not None:
- self.set_number(contents)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_call_clicked(self, widget):
- try:
- phoneNumber = self.get_number()
- self.dial(phoneNumber)
- self.set_number("")
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_sms_clicked(self, widget):
- try:
- phoneNumber = self.get_number()
- self.add_contact(
- "(Dialpad)",
- [("Dialer", phoneNumber)], ()
- )
- self.set_number("")
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_digit_clicked(self, widget):
- try:
- self.set_number(self._phonenumber + widget.get_name()[-1])
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_plus(self, *args):
- try:
- self.set_number(self._phonenumber + "+")
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_backspace(self, taps):
- try:
- self.set_number(self._phonenumber[:-taps])
- self._reset_back_button()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_clearall(self, taps):
- try:
- self.clear()
- self._reset_back_button()
- except Exception, e:
- self._errorDisplay.push_exception()
- return False
-
- def _set_clear_button(self):
- try:
- self._backButton.set_label("gtk-clear")
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _reset_back_button(self):
- try:
- self._backButton.set_label(self._originalLabel)
- except Exception, e:
- self._errorDisplay.push_exception()
-
-
-class AccountInfo(object):
-
- def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
- self._errorDisplay = errorDisplay
- self._backend = backend
- self._isPopulated = False
- self._alarmHandler = alarmHandler
- self._notifyOnMissed = False
- self._notifyOnVoicemail = False
- self._notifyOnSms = False
-
- self._callbackList = []
- self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
- self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
- self._onCallbackSelectChangedId = 0
-
- self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
- self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
- self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
- self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
- self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
- self._onNotifyToggled = 0
- self._onMinutesChanged = 0
- self._onMissedToggled = 0
- self._onVoicemailToggled = 0
- self._onSmsToggled = 0
- self._applyAlarmTimeoutId = None
-
- self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
- self._callbackNumber = ""
-
- def enable(self):
- assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
-
- self._accountViewNumberDisplay.set_use_markup(True)
- self.set_account_number("")
-
- del self._callbackList[:]
- self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
- self._set_callback_label("")
-
- if self._alarmHandler is not None:
- self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
- self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
- self._missedCheckbox.set_active(self._notifyOnMissed)
- self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
- self._smsCheckbox.set_active(self._notifyOnSms)
-
- self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
- self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
- self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
- self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
- self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
- else:
- self._notifyCheckbox.set_sensitive(False)
- self._minutesEntryButton.set_sensitive(False)
- self._missedCheckbox.set_sensitive(False)
- self._voicemailCheckbox.set_sensitive(False)
- self._smsCheckbox.set_sensitive(False)
-
- self.update(force=True)
-
- def disable(self):
- self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
- self._onCallbackSelectChangedId = 0
- self._set_callback_label("")
-
- if self._alarmHandler is not None:
- self._notifyCheckbox.disconnect(self._onNotifyToggled)
- self._minutesEntryButton.disconnect(self._onMinutesChanged)
- self._missedCheckbox.disconnect(self._onNotifyToggled)
- self._voicemailCheckbox.disconnect(self._onNotifyToggled)
- self._smsCheckbox.disconnect(self._onNotifyToggled)
- self._onNotifyToggled = 0
- self._onMinutesChanged = 0
- self._onMissedToggled = 0
- self._onVoicemailToggled = 0
- self._onSmsToggled = 0
- else:
- self._notifyCheckbox.set_sensitive(True)
- self._minutesEntryButton.set_sensitive(True)
- self._missedCheckbox.set_sensitive(True)
- self._voicemailCheckbox.set_sensitive(True)
- self._smsCheckbox.set_sensitive(True)
-
- self.clear()
- del self._callbackList[:]
-
- def set_account_number(self, number):
- """
- Displays current account number
- """
- self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
-
- def update(self, force = False):
- if not force and self._isPopulated:
- return False
- self._populate_callback_combo()
- self.set_account_number(self._backend.get_account_number())
- return True
-
def clear(self):
- self._set_callback_label("")
- self.set_account_number("")
- self._isPopulated = False
-
- def save_everything(self):
- raise NotImplementedError
-
- @staticmethod
- def name():
- return "Account Info"
-
- def load_settings(self, config, section):
- self._callbackNumber = make_ugly(config.get(section, "callback"))
- self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
- self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
- self._notifyOnSms = config.getboolean(section, "notifyOnSms")
-
- def save_settings(self, config, section):
- """
- @note Thread Agnostic
- """
- config.set(section, "callback", self._callbackNumber)
- config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
- config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
- config.set(section, "notifyOnSms", repr(self._notifyOnSms))
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
- def _populate_callback_combo(self):
- self._isPopulated = True
- del self._callbackList[:]
- try:
- callbackNumbers = self._backend.get_callback_numbers()
- except Exception, e:
- self._errorDisplay.push_exception()
- self._isPopulated = False
- return
-
- if len(callbackNumbers) == 0:
- callbackNumbers = {"": "No callback numbers available"}
-
- for number, description in callbackNumbers.iteritems():
- numberDisplay = make_pretty(number)
- if not numberDisplay:
- continue
- self._callbackList.append((numberDisplay, description))
-
- self._set_callback_number(self._callbackNumber)
-
- def _set_callback_number(self, number):
- try:
- if not self._backend.is_valid_syntax(number) and 0 < len(number):
- self._errorDisplay.push_message("%s is not a valid callback number" % number)
- elif number == self._backend.get_callback_number() and 0 < len(number):
- _moduleLogger.warning(
- "Callback number already is %s" % (
- self._backend.get_callback_number(),
- ),
- )
- self._set_callback_label(number)
- else:
- if number.startswith("1747"): number = "+" + number
- self._backend.set_callback_number(number)
- assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
- make_pretty(number), make_pretty(self._backend.get_callback_number())
- )
- self._callbackNumber = make_ugly(number)
- self._set_callback_label(number)
- _moduleLogger.info(
- "Callback number set to %s" % (
- self._backend.get_callback_number(),
- ),
- )
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _set_callback_label(self, uglyNumber):
- prettyNumber = make_pretty(uglyNumber)
- if len(prettyNumber) == 0:
- prettyNumber = "No Callback Number"
- self._callbackSelectButton.set_label(prettyNumber)
-
- def _update_alarm_settings(self, recurrence):
- try:
- isEnabled = self._notifyCheckbox.get_active()
- if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
- self._alarmHandler.apply_settings(isEnabled, recurrence)
- finally:
- self.save_everything()
- self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
- self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
-
- def _on_callbackentry_clicked(self, *args):
- try:
- actualSelection = make_pretty(self._callbackNumber)
+ pass
- userOptions = dict(
- (number, "%s (%s)" % (number, description))
- for (number, description) in self._callbackList
- )
- defaultSelection = userOptions.get(actualSelection, actualSelection)
+ def refresh(self, force = True):
+ pass
- userSelection = hildonize.touch_selector_entry(
- self._window,
- "Callback Number",
- list(userOptions.itervalues()),
- defaultSelection,
+ def _generate_key_button(self, center, letters):
+ button = QtGui.QPushButton("%s\n%s" % (center, letters))
+ button.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ button.clicked.connect(lambda: self._on_keypress(center))
+ return button
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_keypress(self, key):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.insert(key)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_backspace(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.backspace()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_clear_text(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.clear()
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.clear()
+ self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+ self._session.draft.call()
+
+
+class TimeCategories(object):
+
+ _NOW_SECTION = 0
+ _TODAY_SECTION = 1
+ _WEEK_SECTION = 2
+ _MONTH_SECTION = 3
+ _REST_SECTION = 4
+ _MAX_SECTIONS = 5
+
+ _NO_ELAPSED = datetime.timedelta(hours=1)
+ _WEEK_ELAPSED = datetime.timedelta(weeks=1)
+ _MONTH_ELAPSED = datetime.timedelta(days=30)
+
+ def __init__(self, parentItem):
+ self._timeItems = [
+ QtGui.QStandardItem(description)
+ for (i, description) in zip(
+ xrange(self._MAX_SECTIONS),
+ ["Now", "Today", "Week", "Month", "Past"],
)
- reversedUserOptions = dict(
- itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
- )
- selectedNumber = reversedUserOptions.get(userSelection, userSelection)
-
- number = make_ugly(selectedNumber)
- self._set_callback_number(number)
- except RuntimeError, e:
- _moduleLogger.exception("%s" % str(e))
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_notify_toggled(self, *args):
- try:
- if self._applyAlarmTimeoutId is not None:
- gobject.source_remove(self._applyAlarmTimeoutId)
- self._applyAlarmTimeoutId = None
- self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_minutes_clicked(self, *args):
- recurrenceChoices = [
- (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"),
]
- try:
- actualSelection = self._alarmHandler.recurrence
-
- closestSelectionIndex = 0
- for i, possible in enumerate(recurrenceChoices):
- if possible[0] <= actualSelection:
- closestSelectionIndex = i
- recurrenceIndex = hildonize.touch_selector(
- self._window,
- "Minutes",
- (("%s" % m[1]) for m in recurrenceChoices),
- closestSelectionIndex,
- )
- recurrence = recurrenceChoices[recurrenceIndex][0]
-
- self._update_alarm_settings(recurrence)
- except RuntimeError, e:
- _moduleLogger.exception("%s" % str(e))
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_apply_timeout(self, *args):
- try:
- self._applyAlarmTimeoutId = None
-
- self._update_alarm_settings(self._alarmHandler.recurrence)
- except Exception, e:
- self._errorDisplay.push_exception()
- return False
-
- def _on_missed_toggled(self, *args):
- try:
- self._notifyOnMissed = self._missedCheckbox.get_active()
- self.save_everything()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_voicemail_toggled(self, *args):
- try:
- self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
- self.save_everything()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_sms_toggled(self, *args):
- try:
- self._notifyOnSms = self._smsCheckbox.get_active()
- self.save_everything()
- except Exception, e:
- self._errorDisplay.push_exception()
+ 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 CallHistoryView(object):
- NUMBER_IDX = 0
- DATE_IDX = 1
- ACTION_IDX = 2
- FROM_IDX = 3
- FROM_ID_IDX = 4
+class History(object):
- HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
+ DETAILS_IDX = 0
+ FROM_IDX = 1
+ MAX_IDX = 2
- def __init__(self, widgetTree, backend, errorDisplay):
- self._errorDisplay = errorDisplay
- self._backend = backend
+ HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
+ HISTORY_COLUMNS = ["Details", "From"]
+ assert len(HISTORY_COLUMNS) == MAX_IDX
- self._isPopulated = False
- self._historymodel = gtk.ListStore(
- gobject.TYPE_STRING, # number
- gobject.TYPE_STRING, # date
- gobject.TYPE_STRING, # action
- gobject.TYPE_STRING, # from
- gobject.TYPE_STRING, # from id
- )
- self._historymodelfiltered = self._historymodel.filter_new()
- self._historymodelfiltered.set_visible_func(self._is_history_visible)
- self._historyview = widgetTree.get_widget("historyview")
- self._historyviewselection = None
- self._onRecentviewRowActivatedId = 0
-
- textrenderer = gtk.CellRendererText()
- textrenderer.set_property("yalign", 0)
- self._dateColumn = gtk.TreeViewColumn("Date")
- self._dateColumn.pack_start(textrenderer, expand=True)
- self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
-
- textrenderer = gtk.CellRendererText()
- textrenderer.set_property("yalign", 0)
- self._actionColumn = gtk.TreeViewColumn("Action")
- self._actionColumn.pack_start(textrenderer, expand=True)
- self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
-
- textrenderer = gtk.CellRendererText()
- textrenderer.set_property("yalign", 0)
- textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
- textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
- self._numberColumn = gtk.TreeViewColumn("Number")
- self._numberColumn.pack_start(textrenderer, expand=True)
- self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
-
- textrenderer = gtk.CellRendererText()
- textrenderer.set_property("yalign", 0)
- hildonize.set_cell_thumb_selectable(textrenderer)
- self._nameColumn = gtk.TreeViewColumn("From")
- self._nameColumn.pack_start(textrenderer, expand=True)
- self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
- self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
-
- self._window = gtk_toolbox.find_parent_window(self._historyview)
-
- self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
- self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
- self._selectedFilter = "All"
-
- self._updateSink = gtk_toolbox.threaded_stage(
- gtk_toolbox.comap(
- self._idly_populate_historyview,
- gtk_toolbox.null_sink(),
- )
+ 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)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
+ self._categoryManager = TimeCategories(self._itemStore)
+
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(True)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
+ self._itemView.activated.connect(self._on_row_activated)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addWidget(self._typeSelection)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
def enable(self):
- assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
- self._historyFilterSelector.set_label(self._selectedFilter)
-
- self._historyview.set_model(self._historymodelfiltered)
- self._historyview.set_fixed_height_mode(False)
-
- self._historyview.append_column(self._dateColumn)
- self._historyview.append_column(self._actionColumn)
- self._historyview.append_column(self._numberColumn)
- self._historyview.append_column(self._nameColumn)
- self._historyviewselection = self._historyview.get_selection()
- self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
-
- self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
+ self._itemView.setEnabled(True)
def disable(self):
- self._historyview.disconnect(self._onRecentviewRowActivatedId)
+ self._itemView.setEnabled(False)
- self.clear()
-
- self._historyview.remove_column(self._dateColumn)
- self._historyview.remove_column(self._actionColumn)
- self._historyview.remove_column(self._nameColumn)
- self._historyview.remove_column(self._numberColumn)
- self._historyview.set_model(None)
-
- def add_contact(self, *args, **kwds):
- """
- @note Actual dial function is patched in later
- """
- raise NotImplementedError("Horrible unknown error has occurred")
+ def get_settings(self):
+ return {
+ "filter": self._selectedFilter,
+ }
- def update(self, force = False):
- if not force and self._isPopulated:
- return False
- self._updateSink.send(())
- return True
+ def set_settings(self, settings):
+ selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
+ if selectedFilter in self.HISTORY_ITEM_TYPES:
+ self._selectedFilter = selectedFilter
+ self._typeSelection.setCurrentIndex(
+ self.HISTORY_ITEM_TYPES.index(selectedFilter)
+ )
def clear(self):
- self._isPopulated = False
- self._historymodel.clear()
+ self._itemView.clear()
- @staticmethod
- def name():
- return "Recent Calls"
-
- def load_settings(self, config, sectionName):
- try:
- self._selectedFilter = config.get(sectionName, "filter")
- if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
- self._messageType = self.HISTORY_ITEM_TYPES[0]
- except ConfigParser.NoOptionError:
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- config.set(sectionName, "filter", self._selectedFilter)
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
+ def refresh(self, force=True):
+ self._session.update_history(force)
- def _is_history_visible(self, model, iter):
- try:
- action = model.get_value(iter, self.ACTION_IDX)
- if action is None:
- return False # this seems weird but oh well
-
- if self._selectedFilter in [action, "All"]:
- return True
- else:
- return False
- except Exception, e:
- self._errorDisplay.push_exception()
+ def _populate_items(self):
+ self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
- def _idly_populate_historyview(self):
- with gtk_toolbox.gtk_lock():
- banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
- try:
- self._historymodel.clear()
- self._isPopulated = True
-
- try:
- historyItems = self._backend.get_recent()
- except Exception, e:
- self._errorDisplay.push_exception_with_lock()
- self._isPopulated = False
- historyItems = []
-
- historyItems = (
- gv_backend.decorate_recent(data)
- for data in gv_backend.sort_messages(historyItems)
- )
-
- for contactId, personName, phoneNumber, date, action in historyItems:
- if not personName:
- personName = "Unknown"
- date = abbrev_relative_date(date)
- prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
- prettyNumber = make_pretty(prettyNumber)
- item = (prettyNumber, date, action.capitalize(), personName, contactId)
- with gtk_toolbox.gtk_lock():
- self._historymodel.append(item)
- except Exception, e:
- self._errorDisplay.push_exception_with_lock()
- finally:
- with gtk_toolbox.gtk_lock():
- hildonize.show_busy_banner_end(banner)
-
- return False
-
- def _on_history_filter_clicked(self, *args, **kwds):
- try:
- selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
-
- try:
- newSelectedComboIndex = hildonize.touch_selector(
- self._window,
- "History",
- self.HISTORY_ITEM_TYPES,
- selectedComboIndex,
- )
- except RuntimeError:
- return
-
- option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
- self._selectedFilter = option
- self._historyFilterSelector.set_label(self._selectedFilter)
- self._historymodelfiltered.refilter()
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _history_summary(self, expectedNumber):
- for number, action, date, whoFrom, whoFromId in self._historymodel:
- if expectedNumber is not None and expectedNumber == number:
- yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
-
- def _on_historyview_row_activated(self, treeview, path, view_column):
- try:
- childPath = self._historymodelfiltered.convert_path_to_child_path(path)
- itr = self._historymodel.get_iter(childPath)
- if not itr:
- return
-
- prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
- number = make_ugly(prettyNumber)
- description = list(self._history_summary(prettyNumber))
- contactName = self._historymodel.get_value(itr, self.FROM_IDX)
- contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
- contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
-
- self.add_contact(
- contactName,
- contactPhoneNumbers,
- messages = description,
- defaultIndex = defaultIndex,
- )
- self._historyviewselection.unselect_all()
- except Exception, e:
- self._errorDisplay.push_exception()
-
-
-class MessagesView(object):
+ 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
- NUMBER_IDX = 0
- DATE_IDX = 1
- HEADER_IDX = 2
- MESSAGE_IDX = 3
- MESSAGES_IDX = 4
- FROM_ID_IDX = 5
- MESSAGE_DATA_IDX = 6
+ relTime = misc_utils.abbrev_relative_date(event["relTime"])
+ action = event["action"]
+ number = event["number"]
+ prettyNumber = misc_utils.make_pretty(number)
+ name = event["name"]
+ if not name or name == number:
+ name = event["location"]
+ if not name:
+ name = "Unknown"
+
+ detailsItem = QtGui.QStandardItem("%s - %s\n%s" % (relTime, action, prettyNumber))
+ detailsFont = detailsItem.font()
+ detailsFont.setPointSize(max(detailsFont.pointSize() - 7, 5))
+ detailsItem.setFont(detailsFont)
+ nameItem = QtGui.QStandardItem(name)
+ nameFont = nameItem.font()
+ nameFont.setPointSize(nameFont.pointSize() + 4)
+ nameItem.setFont(nameFont)
+ row = detailsItem, nameItem
+ for item in row:
+ item.setEditable(False)
+ item.setCheckable(False)
+ row[0].setData(event)
+ self._categoryManager.add_row(event["time"], row)
+ self._itemView.expandAll()
+
+ @QtCore.pyqtSlot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedFilter = str(newItem)
+ self._populate_items()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_history_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @QtCore.pyqtSlot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ assert timeIndex.isValid(), "Invalid row"
+ timeRow = timeIndex.row()
+ row = index.row()
+ detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
+ fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
+ contactDetails = detailsItem.data().toPyObject()
+
+ title = unicode(fromItem.text())
+ number = str(contactDetails[QtCore.QString("number")])
+ contactId = number # ids don't seem too unique so using numbers
+
+ descriptionRows = []
+ for t in xrange(self._itemStore.rowCount()):
+ randomTimeItem = self._itemStore.item(t, 0)
+ for i in xrange(randomTimeItem.rowCount()):
+ iItem = randomTimeItem.child(i, 0)
+ iContactDetails = iItem.data().toPyObject()
+ iNumber = str(iContactDetails[QtCore.QString("number")])
+ if number != iNumber:
+ continue
+ relTime = misc_utils.abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
+ action = str(iContactDetails[QtCore.QString("action")])
+ number = str(iContactDetails[QtCore.QString("number")])
+ prettyNumber = misc_utils.make_pretty(number)
+ rowItems = relTime, action, prettyNumber
+ descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
+ description = "<table>%s</table>" % "".join(descriptionRows)
+ numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
+ self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+
+
+class Messages(object):
NO_MESSAGES = "None"
VOICEMAIL_MESSAGES = "Voicemail"
ALL_STATUS = "Any"
MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
- def __init__(self, widgetTree, backend, errorDisplay):
- self._errorDisplay = errorDisplay
- self._backend = backend
-
- self._isPopulated = False
- self._messagemodel = gtk.ListStore(
- gobject.TYPE_STRING, # number
- gobject.TYPE_STRING, # date
- gobject.TYPE_STRING, # header
- gobject.TYPE_STRING, # message
- object, # messages
- gobject.TYPE_STRING, # from id
- object, # message data
+ _MIN_MESSAGES_SHOWN = 4
+
+ 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._messagemodelfiltered = self._messagemodel.filter_new()
- self._messagemodelfiltered.set_visible_func(self._is_message_visible)
- self._messageview = widgetTree.get_widget("messages_view")
- self._messageviewselection = None
- self._onMessageviewRowActivatedId = 0
-
- self._messageRenderer = gtk.CellRendererText()
- self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
- self._messageRenderer.set_property("wrap-width", 500)
- self._messageColumn = gtk.TreeViewColumn("Messages")
- self._messageColumn.pack_start(self._messageRenderer, expand=True)
- self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
- self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
-
- self._window = gtk_toolbox.find_parent_window(self._messageview)
-
- self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
- self._onMessageTypeClickedId = 0
- self._messageType = self.ALL_TYPES
- self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
- self._onMessageStatusClickedId = 0
- self._messageStatus = self.ALL_STATUS
-
- self._updateSink = gtk_toolbox.threaded_stage(
- gtk_toolbox.comap(
- self._idly_populate_messageview,
- gtk_toolbox.null_sink(),
- )
+ 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)
+
+ self._selectionLayout = QtGui.QHBoxLayout()
+ self._selectionLayout.addWidget(self._typeSelection)
+ self._selectionLayout.addWidget(self._statusSelection)
+
+ 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._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):
- assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
- self._messageview.set_model(self._messagemodelfiltered)
- self._messageview.set_headers_visible(False)
- self._messageview.set_fixed_height_mode(False)
-
- self._messageview.append_column(self._messageColumn)
- self._messageviewselection = self._messageview.get_selection()
- self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
-
- self._messageTypeButton.set_label(self._messageType)
- self._messageStatusButton.set_label(self._messageStatus)
-
- self._onMessageviewRowActivatedId = self._messageview.connect(
- "row-activated", self._on_messageview_row_activated
- )
- self._onMessageTypeClickedId = self._messageTypeButton.connect(
- "clicked", self._on_message_type_clicked
- )
- self._onMessageStatusClickedId = self._messageStatusButton.connect(
- "clicked", self._on_message_status_clicked
- )
+ self._itemView.setEnabled(True)
def disable(self):
- self._messageview.disconnect(self._onMessageviewRowActivatedId)
- self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
- self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
+ self._itemView.setEnabled(False)
- self.clear()
-
- self._messageview.remove_column(self._messageColumn)
- self._messageview.set_model(None)
+ def get_settings(self):
+ return {
+ "type": self._selectedTypeFilter,
+ "status": self._selectedStatusFilter,
+ }
- def add_contact(self, *args, **kwds):
- """
- @note Actual dial function is patched in later
- """
- raise NotImplementedError("Horrible unknown error has occurred")
+ 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)
+ )
- def update(self, force = False):
- if not force and self._isPopulated:
- return False
- self._updateSink.send(())
- return True
+ 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._isPopulated = False
- self._messagemodel.clear()
-
- @staticmethod
- def name():
- return "Messages"
-
- def load_settings(self, config, sectionName):
- try:
- self._messageType = config.get(sectionName, "type")
- if self._messageType not in self.MESSAGE_TYPES:
- self._messageType = self.ALL_TYPES
- self._messageStatus = config.get(sectionName, "status")
- if self._messageStatus not in self.MESSAGE_STATUSES:
- self._messageStatus = self.ALL_STATUS
- except ConfigParser.NoOptionError:
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- config.set(sectionName, "status", self._messageStatus)
- config.set(sectionName, "type", self._messageType)
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
- def _is_message_visible(self, model, iter):
- try:
- message = model.get_value(iter, self.MESSAGE_DATA_IDX)
- if message is None:
- return False # this seems weird but oh well
- return self._filter_messages(message, self._messageType, self._messageStatus)
- except Exception, e:
- self._errorDisplay.push_exception()
-
- @classmethod
- def _filter_messages(cls, message, type, status):
- if type == cls.ALL_TYPES:
- isType = True
- else:
- messageType = message["type"]
- isType = messageType == type
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._session.update_messages(force)
+
+ 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
- if status == cls.ALL_STATUS:
- isStatus = True
- else:
- isUnarchived = not message["isArchived"]
- isUnread = not message["isRead"]
- if status == cls.UNREAD_STATUS:
- isStatus = isUnarchived and isUnread
- elif status == cls.UNARCHIVED_STATUS:
- isStatus = isUnarchived
+ 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:
- assert "Status %s is bad for %r" % (status, message)
-
- return isType and isStatus
-
- _MIN_MESSAGES_SHOWN = 4
-
- def _idly_populate_messageview(self):
- with gtk_toolbox.gtk_lock():
- banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
- try:
- self._messagemodel.clear()
- self._isPopulated = True
-
- if self._messageType == self.NO_MESSAGES:
- messageItems = []
+ messages = [
+ "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+ for messagePart in messageParts
+ ]
+
+ firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
+
+ expandedMessages = [firstMessage]
+ expandedMessages.extend(messages)
+ if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
+ secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
+ collapsedMessages = [firstMessage, secondMessage]
+ collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
else:
- try:
- messageItems = self._backend.get_messages()
- except Exception, e:
- self._errorDisplay.push_exception_with_lock()
- self._isPopulated = False
- messageItems = []
-
- messageItems = (
- (gv_backend.decorate_message(message), message)
- for message in gv_backend.sort_messages(messageItems)
- )
-
- for (contactId, header, number, relativeDate, messages), messageData in messageItems:
- prettyNumber = number[2:] if number.startswith("+1") else number
- prettyNumber = make_pretty(prettyNumber)
-
- firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
- expandedMessages = [firstMessage]
- expandedMessages.extend(messages)
- if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
- firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
- secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
- collapsedMessages = [firstMessage, secondMessage]
- collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
- else:
- collapsedMessages = expandedMessages
- #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
-
- number = make_ugly(number)
-
- row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
- with gtk_toolbox.gtk_lock():
- self._messagemodel.append(row)
- except Exception, e:
- self._errorDisplay.push_exception_with_lock()
- finally:
- with gtk_toolbox.gtk_lock():
- hildonize.show_busy_banner_end(banner)
- self._messagemodelfiltered.refilter()
-
- return False
+ collapsedMessages = expandedMessages
+
+ item = dict(item.iteritems())
+ item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
+ item["expandedMessages"] = "<br/>\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()
+
+ @QtCore.pyqtSlot(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()
+
+ @QtCore.pyqtSlot(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()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_messages_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @QtCore.pyqtSlot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ assert timeIndex.isValid(), "Invalid row"
+ timeRow = timeIndex.row()
+ row = index.row()
+ item = self._categoryManager.get_item(timeRow, row, 0)
+ contactDetails = item.data().toPyObject()
+
+ name = unicode(contactDetails[QtCore.QString("name")])
+ number = str(contactDetails[QtCore.QString("number")])
+ if not name or name == number:
+ name = unicode(contactDetails[QtCore.QString("location")])
+ if not name:
+ name = "Unknown"
+
+ contactId = str(contactDetails[QtCore.QString("id")])
+ title = name
+ description = unicode(contactDetails[QtCore.QString("expandedMessages")])
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+
+
+class Contacts(object):
+
+ # @todo Provide some sort of letter jump
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._session.contactsUpdated.connect(self._on_contacts_updated)
+ self._errorLog = errorLog
+ self._addressBookFactories = [
+ null_backend.NullAddressBookFactory(),
+ file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
+ ]
+ self._addressBooks = []
+
+ self._listSelection = QtGui.QComboBox()
+ self._listSelection.addItems([])
+ self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+ self._activeList = "None"
+
+ 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.addWidget(self._listSelection)
+ 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 _on_messageview_row_activated(self, treeview, path, view_column):
- try:
- childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
- itr = self._messagemodel.get_iter(childPath)
- if not itr:
- return
-
- number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
- description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
-
- contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
- header = self._messagemodel.get_value(itr, self.HEADER_IDX)
- contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
-
- self.add_contact(
- header,
- contactPhoneNumbers,
- messages = description,
- defaultIndex = defaultIndex,
- )
- self._messageviewselection.unselect_all()
- except Exception, e:
- self._errorDisplay.push_exception()
+ def enable(self):
+ self._itemView.setEnabled(True)
- def _on_message_type_clicked(self, *args, **kwds):
- try:
- selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
-
- try:
- newSelectedIndex = hildonize.touch_selector(
- self._window,
- "Message Type",
- self.MESSAGE_TYPES,
- selectedIndex,
- )
- except RuntimeError:
- return
+ def disable(self):
+ self._itemView.setEnabled(False)
- if selectedIndex != newSelectedIndex:
- self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
- self._messageTypeButton.set_label(self._messageType)
- self._messagemodelfiltered.refilter()
- except Exception, e:
- self._errorDisplay.push_exception()
+ def get_settings(self):
+ return {
+ "selectedAddressbook": self._activeList,
+ }
- def _on_message_status_clicked(self, *args, **kwds):
+ def set_settings(self, settings):
+ currentItem = settings.get("selectedAddressbook", "None")
+ bookNames = [book["name"] for book in self._addressBooks]
try:
- selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
-
- try:
- newSelectedIndex = hildonize.touch_selector(
- self._window,
- "Message Status",
- self.MESSAGE_STATUSES,
- selectedIndex,
- )
- except RuntimeError:
- return
-
- if selectedIndex != newSelectedIndex:
- self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
- self._messageStatusButton.set_label(self._messageStatus)
- self._messagemodelfiltered.refilter()
- except Exception, e:
- self._errorDisplay.push_exception()
-
+ newIndex = bookNames.index(currentItem)
+ except ValueError:
+ # Switch over to None for the user
+ newIndex = 0
+ self._listSelection.setCurrentIndex(newIndex)
+ self._activeList = currentItem
-class ContactsView(object):
-
- CONTACT_TYPE_IDX = 0
- CONTACT_NAME_IDX = 1
- CONTACT_ID_IDX = 2
-
- def __init__(self, widgetTree, backend, errorDisplay):
- self._errorDisplay = errorDisplay
- self._backend = backend
+ def clear(self):
+ self._itemView.clear()
- self._addressBook = None
- self._selectedComboIndex = 0
- self._addressBookFactories = [null_backend.NullAddressBook()]
+ def refresh(self, force=True):
+ self._backend.update_contacts(force)
- self._booksList = []
- self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
+ @property
+ def _backend(self):
+ return self._addressBooks[self._listSelection.currentIndex()]["book"]
- self._isPopulated = False
- self._contactsmodel = gtk.ListStore(
- gobject.TYPE_STRING, # Contact Type
- gobject.TYPE_STRING, # Contact Name
- gobject.TYPE_STRING, # Contact ID
- )
- self._contactsviewselection = None
- self._contactsview = widgetTree.get_widget("contactsview")
-
- self._contactColumn = gtk.TreeViewColumn("Contact")
- displayContactSource = False
- if displayContactSource:
- textrenderer = gtk.CellRendererText()
- self._contactColumn.pack_start(textrenderer, expand=False)
- self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
- textrenderer = gtk.CellRendererText()
- hildonize.set_cell_thumb_selectable(textrenderer)
- self._contactColumn.pack_start(textrenderer, expand=True)
- self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
- self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
- self._contactColumn.set_sort_column_id(1)
- self._contactColumn.set_visible(True)
-
- self._onContactsviewRowActivatedId = 0
- self._onAddressbookButtonChangedId = 0
- self._window = gtk_toolbox.find_parent_window(self._contactsview)
-
- self._updateSink = gtk_toolbox.threaded_stage(
- gtk_toolbox.comap(
- self._idly_populate_contactsview,
- gtk_toolbox.null_sink(),
- )
+ 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",
+ }
)
- def enable(self):
- assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
-
- self._contactsview.set_model(self._contactsmodel)
- self._contactsview.set_fixed_height_mode(False)
- self._contactsview.append_column(self._contactColumn)
- self._contactsviewselection = self._contactsview.get_selection()
- self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
-
- del self._booksList[:]
- for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
- if factoryName and bookName:
- entryName = "%s: %s" % (factoryName, bookName)
- elif factoryName:
- entryName = factoryName
- elif bookName:
- entryName = bookName
- else:
- entryName = "Bad name (%d)" % factoryId
- row = (str(factoryId), bookId, entryName)
- self._booksList.append(row)
-
- self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
- self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
-
- if len(self._booksList) <= self._selectedComboIndex:
- self._selectedComboIndex = 0
- self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
-
- selectedFactoryId = self._booksList[self._selectedComboIndex][0]
- selectedBookId = self._booksList[self._selectedComboIndex][1]
- self.open_addressbook(selectedFactoryId, selectedBookId)
-
- def disable(self):
- self._contactsview.disconnect(self._onContactsviewRowActivatedId)
- self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
-
- self.clear()
-
- self._bookSelectionButton.set_label("")
- self._contactsview.set_model(None)
- self._contactsview.remove_column(self._contactColumn)
-
- def add_contact(self, *args, **kwds):
- """
- @note Actual dial function is patched in later
- """
- raise NotImplementedError("Horrible unknown error has occurred")
-
- def get_addressbooks(self):
- """
- @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
- """
- for i, factory in enumerate(self._addressBookFactories):
- for bookFactory, bookId, bookName in factory.get_addressbooks():
- yield (str(i), bookId), (factory.factory_name(), bookName)
-
- def open_addressbook(self, bookFactoryId, bookId):
- bookFactoryIndex = int(bookFactoryId)
- addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
- self._addressBook = addressBook
-
- def update(self, force = False):
- if not force and self._isPopulated:
- return False
- self._updateSink.send(())
- return True
-
- def clear(self):
- self._isPopulated = False
- self._contactsmodel.clear()
- for factory in self._addressBookFactories:
- factory.clear_caches()
- self._addressBook.clear_caches()
-
- def append(self, book):
- self._addressBookFactories.append(book)
-
- def extend(self, books):
- self._addressBookFactories.extend(books)
+ 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
+
+ @QtCore.pyqtSlot(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()
+
+ @QtCore.pyqtSlot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_contacts_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @QtCore.pyqtSlot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ letterIndex = index.parent()
+ assert letterIndex.isValid(), "Invalid row"
+ letterRow = letterIndex.row()
+ letter = list(self._prefixes())[letterRow]
+ letterItem = self._alphaItem[letter]
+ rowIndex = index.row()
+ item = letterItem.child(rowIndex, 0)
+ contactDetails = item.data().toPyObject()
+
+ name = unicode(contactDetails[QtCore.QString("name")])
+ if not name:
+ name = unicode(contactDetails[QtCore.QString("location")])
+ if not name:
+ name = "Unknown"
+
+ contactId = str(contactDetails[QtCore.QString("contactId")])
+ numbers = contactDetails[QtCore.QString("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, title, description, numbersWithDescriptions)
@staticmethod
- def name():
- return "Contacts"
-
- def load_settings(self, config, sectionName):
- try:
- self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
- except ConfigParser.NoOptionError:
- self._selectedComboIndex = 0
-
- def save_settings(self, config, sectionName):
- config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
+ def _choose_phonetype(numberDetails):
+ if "phoneTypeName" in numberDetails:
+ return numberDetails["phoneTypeName"]
+ elif "phoneType" in numberDetails:
+ return numberDetails["phoneType"]
else:
- raise NotImplementedError(orientation)
-
- def _idly_populate_contactsview(self):
- with gtk_toolbox.gtk_lock():
- banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
- try:
- addressBook = None
- while addressBook is not self._addressBook:
- addressBook = self._addressBook
- with gtk_toolbox.gtk_lock():
- self._contactsview.set_model(None)
- self.clear()
-
- try:
- contacts = addressBook.get_contacts()
- except Exception, e:
- contacts = []
- self._isPopulated = False
- self._errorDisplay.push_exception_with_lock()
- for contactId, contactName in contacts:
- contactType = addressBook.contact_source_short_name(contactId)
- row = contactType, contactName, contactId
- self._contactsmodel.append(row)
-
- with gtk_toolbox.gtk_lock():
- self._contactsview.set_model(self._contactsmodel)
-
- self._isPopulated = True
- except Exception, e:
- self._errorDisplay.push_exception_with_lock()
- finally:
- with gtk_toolbox.gtk_lock():
- hildonize.show_busy_banner_end(banner)
- return False
-
- def _on_addressbook_button_changed(self, *args, **kwds):
- try:
- try:
- newSelectedComboIndex = hildonize.touch_selector(
- self._window,
- "Addressbook",
- (("%s" % m[2]) for m in self._booksList),
- self._selectedComboIndex,
- )
- except RuntimeError:
- return
-
- selectedFactoryId = self._booksList[newSelectedComboIndex][0]
- selectedBookId = self._booksList[newSelectedComboIndex][1]
-
- oldAddressbook = self._addressBook
- self.open_addressbook(selectedFactoryId, selectedBookId)
- forceUpdate = True if oldAddressbook is not self._addressBook else False
- self.update(force=forceUpdate)
-
- self._selectedComboIndex = newSelectedComboIndex
- self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
- except Exception, e:
- self._errorDisplay.push_exception()
-
- def _on_contactsview_row_activated(self, treeview, path, view_column):
- try:
- itr = self._contactsmodel.get_iter(path)
- if not itr:
- return
-
- contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
- contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
- try:
- contactDetails = self._addressBook.get_contact_details(contactId)
- except Exception, e:
- contactDetails = []
- self._errorDisplay.push_exception()
- contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
-
- if len(contactPhoneNumbers) == 0:
- return
-
- self.add_contact(
- contactName,
- contactPhoneNumbers,
- messages = (contactName, ),
- )
- self._contactsviewselection.unselect_all()
- except Exception, e:
- self._errorDisplay.push_exception()
+ return ""
+++ /dev/null
-#!/usr/bin/env python
-
-"""
-Open Issues
- @bug not all of a message is shown
- @bug Buttons are too small
-"""
-
-
-import gobject
-import gtk
-import dbus
-
-
-class _NullHildonModule(object):
- pass
-
-
-try:
- import hildon as _hildon
- hildon = _hildon # Dumb but gets around pyflakiness
-except (ImportError, OSError):
- hildon = _NullHildonModule
-
-
-IS_HILDON_SUPPORTED = hildon is not _NullHildonModule
-
-
-class _NullHildonProgram(object):
-
- def add_window(self, window):
- pass
-
-
-def _hildon_get_app_class():
- return hildon.Program
-
-
-def _null_get_app_class():
- return _NullHildonProgram
-
-
-try:
- hildon.Program
- get_app_class = _hildon_get_app_class
-except AttributeError:
- get_app_class = _null_get_app_class
-
-
-def _hildon_set_application_name(name):
- gtk.set_application_name(name)
-
-
-def _null_set_application_name(name):
- pass
-
-
-try:
- gtk.set_application_name
- set_application_name = _hildon_set_application_name
-except AttributeError:
- set_application_name = _null_set_application_name
-
-
-def _fremantle_hildonize_window(app, window):
- oldWindow = window
- newWindow = hildon.StackableWindow()
- if oldWindow.get_child() is not None:
- oldWindow.get_child().reparent(newWindow)
- app.add_window(newWindow)
- return newWindow
-
-
-def _hildon_hildonize_window(app, window):
- oldWindow = window
- newWindow = hildon.Window()
- if oldWindow.get_child() is not None:
- oldWindow.get_child().reparent(newWindow)
- app.add_window(newWindow)
- return newWindow
-
-
-def _null_hildonize_window(app, window):
- return window
-
-
-try:
- hildon.StackableWindow
- hildonize_window = _fremantle_hildonize_window
-except AttributeError:
- try:
- hildon.Window
- hildonize_window = _hildon_hildonize_window
- except AttributeError:
- hildonize_window = _null_hildonize_window
-
-
-def _fremantle_hildonize_menu(window, gtkMenu):
- appMenu = hildon.AppMenu()
- window.set_app_menu(appMenu)
- gtkMenu.get_parent().remove(gtkMenu)
- return appMenu
-
-
-def _hildon_hildonize_menu(window, gtkMenu):
- hildonMenu = gtk.Menu()
- for child in gtkMenu.get_children():
- child.reparent(hildonMenu)
- window.set_menu(hildonMenu)
- gtkMenu.destroy()
- return hildonMenu
-
-
-def _null_hildonize_menu(window, gtkMenu):
- return gtkMenu
-
-
-try:
- hildon.AppMenu
- GTK_MENU_USED = False
- IS_FREMANTLE_SUPPORTED = True
- hildonize_menu = _fremantle_hildonize_menu
-except AttributeError:
- GTK_MENU_USED = True
- IS_FREMANTLE_SUPPORTED = False
- if IS_HILDON_SUPPORTED:
- hildonize_menu = _hildon_hildonize_menu
- else:
- hildonize_menu = _null_hildonize_menu
-
-
-def _hildon_set_button_auto_selectable(button):
- button.set_theme_size(hildon.HILDON_SIZE_AUTO_HEIGHT)
-
-
-def _null_set_button_auto_selectable(button):
- pass
-
-
-try:
- hildon.HILDON_SIZE_AUTO_HEIGHT
- gtk.Button.set_theme_size
- set_button_auto_selectable = _hildon_set_button_auto_selectable
-except AttributeError:
- set_button_auto_selectable = _null_set_button_auto_selectable
-
-
-def _hildon_set_button_finger_selectable(button):
- button.set_theme_size(hildon.HILDON_SIZE_FINGER_HEIGHT)
-
-
-def _null_set_button_finger_selectable(button):
- pass
-
-
-try:
- hildon.HILDON_SIZE_FINGER_HEIGHT
- gtk.Button.set_theme_size
- set_button_finger_selectable = _hildon_set_button_finger_selectable
-except AttributeError:
- set_button_finger_selectable = _null_set_button_finger_selectable
-
-
-def _hildon_set_button_thumb_selectable(button):
- button.set_theme_size(hildon.HILDON_SIZE_THUMB_HEIGHT)
-
-
-def _null_set_button_thumb_selectable(button):
- pass
-
-
-try:
- hildon.HILDON_SIZE_THUMB_HEIGHT
- gtk.Button.set_theme_size
- set_button_thumb_selectable = _hildon_set_button_thumb_selectable
-except AttributeError:
- set_button_thumb_selectable = _null_set_button_thumb_selectable
-
-
-def _hildon_set_cell_thumb_selectable(renderer):
- renderer.set_property("scale", 1.5)
-
-
-def _null_set_cell_thumb_selectable(renderer):
- pass
-
-
-if IS_HILDON_SUPPORTED:
- set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable
-else:
- set_cell_thumb_selectable = _null_set_cell_thumb_selectable
-
-
-def _hildon_set_pix_cell_thumb_selectable(renderer):
- renderer.set_property("stock-size", 48)
-
-
-def _null_set_pix_cell_thumb_selectable(renderer):
- pass
-
-
-if IS_HILDON_SUPPORTED:
- set_pix_cell_thumb_selectable = _hildon_set_pix_cell_thumb_selectable
-else:
- set_pix_cell_thumb_selectable = _null_set_pix_cell_thumb_selectable
-
-
-def _fremantle_show_information_banner(parent, message):
- hildon.hildon_banner_show_information(parent, "", message)
-
-
-def _hildon_show_information_banner(parent, message):
- hildon.hildon_banner_show_information(parent, None, message)
-
-
-def _null_show_information_banner(parent, message):
- pass
-
-
-if IS_FREMANTLE_SUPPORTED:
- show_information_banner = _fremantle_show_information_banner
-else:
- try:
- hildon.hildon_banner_show_information
- show_information_banner = _hildon_show_information_banner
- except AttributeError:
- show_information_banner = _null_show_information_banner
-
-
-def _fremantle_show_busy_banner_start(parent, message):
- hildon.hildon_gtk_window_set_progress_indicator(parent, True)
- return parent
-
-
-def _fremantle_show_busy_banner_end(banner):
- hildon.hildon_gtk_window_set_progress_indicator(banner, False)
-
-
-def _hildon_show_busy_banner_start(parent, message):
- return hildon.hildon_banner_show_animation(parent, None, message)
-
-
-def _hildon_show_busy_banner_end(banner):
- banner.destroy()
-
-
-def _null_show_busy_banner_start(parent, message):
- return None
-
-
-def _null_show_busy_banner_end(banner):
- assert banner is None
-
-
-try:
- hildon.hildon_gtk_window_set_progress_indicator
- show_busy_banner_start = _fremantle_show_busy_banner_start
- show_busy_banner_end = _fremantle_show_busy_banner_end
-except AttributeError:
- try:
- hildon.hildon_banner_show_animation
- show_busy_banner_start = _hildon_show_busy_banner_start
- show_busy_banner_end = _hildon_show_busy_banner_end
- except AttributeError:
- show_busy_banner_start = _null_show_busy_banner_start
- show_busy_banner_end = _null_show_busy_banner_end
-
-
-def _hildon_hildonize_text_entry(textEntry):
- textEntry.set_property('hildon-input-mode', 7)
-
-
-def _null_hildonize_text_entry(textEntry):
- pass
-
-
-if IS_HILDON_SUPPORTED:
- hildonize_text_entry = _hildon_hildonize_text_entry
-else:
- hildonize_text_entry = _null_hildonize_text_entry
-
-
-def _hildon_window_to_portrait(window):
- # gtk documentation is unclear whether this does a "=" or a "|="
- flags = hildon.PORTRAIT_MODE_SUPPORT | hildon.PORTRAIT_MODE_REQUEST
- hildon.hildon_gtk_window_set_portrait_flags(window, flags)
-
-
-def _hildon_window_to_landscape(window):
- # gtk documentation is unclear whether this does a "=" or a "&= ~"
- flags = hildon.PORTRAIT_MODE_SUPPORT
- hildon.hildon_gtk_window_set_portrait_flags(window, flags)
-
-
-def _null_window_to_portrait(window):
- pass
-
-
-def _null_window_to_landscape(window):
- pass
-
-
-try:
- hildon.PORTRAIT_MODE_SUPPORT
- hildon.PORTRAIT_MODE_REQUEST
- hildon.hildon_gtk_window_set_portrait_flags
-
- window_to_portrait = _hildon_window_to_portrait
- window_to_landscape = _hildon_window_to_landscape
-except AttributeError:
- window_to_portrait = _null_window_to_portrait
- window_to_landscape = _null_window_to_landscape
-
-
-def get_device_orientation():
- bus = dbus.SystemBus()
- try:
- rawMceRequest = bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
- mceRequest = dbus.Interface(rawMceRequest, dbus_interface="com.nokia.mce.request")
- orientation, standState, faceState, xAxis, yAxis, zAxis = mceRequest.get_device_orientation()
- except dbus.exception.DBusException:
- # catching for documentation purposes that when a system doesn't
- # support this, this is what to expect
- raise
-
- if orientation == "":
- return gtk.ORIENTATION_HORIZONTAL
- elif orientation == "":
- return gtk.ORIENTATION_VERTICAL
- else:
- raise RuntimeError("Unknown orientation: %s" % orientation)
-
-
-def _hildon_hildonize_password_entry(textEntry):
- textEntry.set_property('hildon-input-mode', 7 | (1 << 29))
-
-
-def _null_hildonize_password_entry(textEntry):
- pass
-
-
-if IS_HILDON_SUPPORTED:
- hildonize_password_entry = _hildon_hildonize_password_entry
-else:
- hildonize_password_entry = _null_hildonize_password_entry
-
-
-def _hildon_hildonize_combo_entry(comboEntry):
- comboEntry.set_property('hildon-input-mode', 1 << 4)
-
-
-def _null_hildonize_combo_entry(textEntry):
- pass
-
-
-if IS_HILDON_SUPPORTED:
- hildonize_combo_entry = _hildon_hildonize_combo_entry
-else:
- hildonize_combo_entry = _null_hildonize_combo_entry
-
-
-def _null_create_seekbar():
- adjustment = gtk.Adjustment(0, 0, 101, 1, 5, 1)
- seek = gtk.HScale(adjustment)
- seek.set_draw_value(False)
- return seek
-
-
-def _fremantle_create_seekbar():
- seek = hildon.Seekbar()
- seek.set_range(0.0, 100)
- seek.set_draw_value(False)
- seek.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
- return seek
-
-
-try:
- hildon.Seekbar
- create_seekbar = _fremantle_create_seekbar
-except AttributeError:
- create_seekbar = _null_create_seekbar
-
-
-def _fremantle_hildonize_scrollwindow(scrolledWindow):
- pannableWindow = hildon.PannableArea()
-
- child = scrolledWindow.get_child()
- scrolledWindow.remove(child)
- pannableWindow.add(child)
-
- parent = scrolledWindow.get_parent()
- if parent is not None:
- parent.remove(scrolledWindow)
- parent.add(pannableWindow)
-
- return pannableWindow
-
-
-def _hildon_hildonize_scrollwindow(scrolledWindow):
- hildon.hildon_helper_set_thumb_scrollbar(scrolledWindow, True)
- return scrolledWindow
-
-
-def _null_hildonize_scrollwindow(scrolledWindow):
- return scrolledWindow
-
-
-try:
- hildon.PannableArea
- hildonize_scrollwindow = _fremantle_hildonize_scrollwindow
- hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow
-except AttributeError:
- try:
- hildon.hildon_helper_set_thumb_scrollbar
- hildonize_scrollwindow = _hildon_hildonize_scrollwindow
- hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow
- except AttributeError:
- hildonize_scrollwindow = _null_hildonize_scrollwindow
- hildonize_scrollwindow_with_viewport = _null_hildonize_scrollwindow
-
-
-def _hildon_request_number(parent, title, range, default):
- spinner = hildon.NumberEditor(*range)
- spinner.set_value(default)
-
- dialog = gtk.Dialog(
- title,
- parent,
- gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
- (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
- )
- dialog.set_default_response(gtk.RESPONSE_CANCEL)
- dialog.get_child().add(spinner)
-
- try:
- dialog.show_all()
- response = dialog.run()
-
- if response == gtk.RESPONSE_OK:
- return spinner.get_value()
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
- finally:
- dialog.hide()
- dialog.destroy()
-
-
-def _null_request_number(parent, title, range, default):
- adjustment = gtk.Adjustment(default, range[0], range[1], 1, 5, 0)
- spinner = gtk.SpinButton(adjustment, 0, 0)
- spinner.set_wrap(False)
-
- dialog = gtk.Dialog(
- title,
- parent,
- gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
- (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
- )
- dialog.set_default_response(gtk.RESPONSE_CANCEL)
- dialog.get_child().add(spinner)
-
- try:
- dialog.show_all()
- response = dialog.run()
-
- if response == gtk.RESPONSE_OK:
- return spinner.get_value_as_int()
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
- finally:
- dialog.hide()
- dialog.destroy()
-
-
-try:
- hildon.NumberEditor # TODO deprecated in fremantle
- request_number = _hildon_request_number
-except AttributeError:
- request_number = _null_request_number
-
-
-def _hildon_touch_selector(parent, title, items, defaultIndex):
- model = gtk.ListStore(gobject.TYPE_STRING)
- for item in items:
- model.append((item, ))
-
- selector = hildon.TouchSelector()
- selector.append_text_column(model, True)
- selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
- selector.set_active(0, defaultIndex)
-
- dialog = hildon.PickerDialog(parent)
- dialog.set_selector(selector)
-
- try:
- dialog.show_all()
- response = dialog.run()
-
- if response == gtk.RESPONSE_OK:
- return selector.get_active(0)
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
- finally:
- dialog.hide()
- dialog.destroy()
-
-
-def _on_null_touch_selector_activated(treeView, path, column, dialog, pathData):
- dialog.response(gtk.RESPONSE_OK)
- pathData[0] = path
-
-
-def _null_touch_selector(parent, title, items, defaultIndex = -1):
- parentSize = parent.get_size()
-
- model = gtk.ListStore(gobject.TYPE_STRING)
- for item in items:
- model.append((item, ))
-
- cell = gtk.CellRendererText()
- set_cell_thumb_selectable(cell)
- column = gtk.TreeViewColumn(title)
- column.pack_start(cell, expand=True)
- column.add_attribute(cell, "text", 0)
-
- treeView = gtk.TreeView()
- treeView.set_model(model)
- treeView.append_column(column)
- selection = treeView.get_selection()
- selection.set_mode(gtk.SELECTION_SINGLE)
- if 0 < defaultIndex:
- selection.select_path((defaultIndex, ))
-
- scrolledWin = gtk.ScrolledWindow()
- scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scrolledWin.add(treeView)
-
- dialog = gtk.Dialog(
- title,
- parent,
- gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
- (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
- )
- dialog.set_default_response(gtk.RESPONSE_CANCEL)
- dialog.get_child().add(scrolledWin)
- dialog.resize(parentSize[0], max(parentSize[1]-100, 100))
-
- scrolledWin = hildonize_scrollwindow(scrolledWin)
- pathData = [None]
- treeView.connect("row-activated", _on_null_touch_selector_activated, dialog, pathData)
-
- try:
- dialog.show_all()
- response = dialog.run()
-
- if response == gtk.RESPONSE_OK:
- if pathData[0] is None:
- raise RuntimeError("No selection made")
- return pathData[0][0]
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
- finally:
- dialog.hide()
- dialog.destroy()
-
-
-try:
- hildon.PickerDialog
- hildon.TouchSelector
- touch_selector = _hildon_touch_selector
-except AttributeError:
- touch_selector = _null_touch_selector
-
-
-def _hildon_touch_selector_entry(parent, title, items, defaultItem):
- # Got a segfault when using append_text_column with TouchSelectorEntry, so using this way
- try:
- selector = hildon.TouchSelectorEntry(text=True)
- except TypeError:
- selector = hildon.hildon_touch_selector_entry_new_text()
- defaultIndex = -1
- for i, item in enumerate(items):
- selector.append_text(item)
- if item == defaultItem:
- defaultIndex = i
-
- dialog = hildon.PickerDialog(parent)
- dialog.set_selector(selector)
-
- if 0 < defaultIndex:
- selector.set_active(0, defaultIndex)
- else:
- selector.get_entry().set_text(defaultItem)
-
- try:
- dialog.show_all()
- response = dialog.run()
- finally:
- dialog.hide()
-
- if response == gtk.RESPONSE_OK:
- return selector.get_entry().get_text()
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
-
-
-def _on_null_touch_selector_entry_entry_changed(entry, result, selection, defaultIndex):
- custom = entry.get_text().strip()
- if custom:
- result[0] = custom
- selection.unselect_all()
- else:
- result[0] = None
- selection.select_path((defaultIndex, ))
-
-
-def _on_null_touch_selector_entry_entry_activated(customEntry, dialog, result):
- dialog.response(gtk.RESPONSE_OK)
- result[0] = customEntry.get_text()
-
-
-def _on_null_touch_selector_entry_tree_activated(treeView, path, column, dialog, result):
- dialog.response(gtk.RESPONSE_OK)
- model = treeView.get_model()
- itr = model.get_iter(path)
- if itr is not None:
- result[0] = model.get_value(itr, 0)
-
-
-def _null_touch_selector_entry(parent, title, items, defaultItem):
- parentSize = parent.get_size()
-
- model = gtk.ListStore(gobject.TYPE_STRING)
- defaultIndex = -1
- for i, item in enumerate(items):
- model.append((item, ))
- if item == defaultItem:
- defaultIndex = i
-
- cell = gtk.CellRendererText()
- set_cell_thumb_selectable(cell)
- column = gtk.TreeViewColumn(title)
- column.pack_start(cell, expand=True)
- column.add_attribute(cell, "text", 0)
-
- treeView = gtk.TreeView()
- treeView.set_model(model)
- treeView.append_column(column)
- selection = treeView.get_selection()
- selection.set_mode(gtk.SELECTION_SINGLE)
-
- scrolledWin = gtk.ScrolledWindow()
- scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scrolledWin.add(treeView)
-
- customEntry = gtk.Entry()
-
- layout = gtk.VBox()
- layout.pack_start(customEntry, expand=False)
- layout.pack_start(scrolledWin)
-
- dialog = gtk.Dialog(
- title,
- parent,
- gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
- (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
- )
- dialog.set_default_response(gtk.RESPONSE_CANCEL)
- dialog.get_child().add(layout)
- dialog.resize(parentSize[0], max(parentSize[1]-100, 100))
-
- scrolledWin = hildonize_scrollwindow(scrolledWin)
-
- result = [None]
- if 0 < defaultIndex:
- selection.select_path((defaultIndex, ))
- result[0] = defaultItem
- else:
- customEntry.set_text(defaultItem)
-
- customEntry.connect("activate", _on_null_touch_selector_entry_entry_activated, dialog, result)
- customEntry.connect("changed", _on_null_touch_selector_entry_entry_changed, result, selection, defaultIndex)
- treeView.connect("row-activated", _on_null_touch_selector_entry_tree_activated, dialog, result)
-
- try:
- dialog.show_all()
- response = dialog.run()
-
- if response == gtk.RESPONSE_OK:
- _, itr = selection.get_selected()
- if itr is not None:
- return model.get_value(itr, 0)
- else:
- enteredText = customEntry.get_text().strip()
- if enteredText:
- return enteredText
- elif result[0] is not None:
- return result[0]
- else:
- raise RuntimeError("No selection made")
- elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
- raise RuntimeError("User cancelled request")
- else:
- raise RuntimeError("Unrecognized response %r", response)
- finally:
- dialog.hide()
- dialog.destroy()
-
-
-try:
- hildon.PickerDialog
- hildon.TouchSelectorEntry
- touch_selector_entry = _hildon_touch_selector_entry
-except AttributeError:
- touch_selector_entry = _null_touch_selector_entry
-
-
-if __name__ == "__main__":
- app = get_app_class()()
-
- label = gtk.Label("Hello World from a Label!")
-
- win = gtk.Window()
- win.add(label)
- win = hildonize_window(app, win)
- if False and IS_FREMANTLE_SUPPORTED:
- appMenu = hildon.AppMenu()
- for i in xrange(5):
- b = gtk.Button(str(i))
- appMenu.append(b)
- win.set_app_menu(appMenu)
- win.show_all()
- appMenu.show_all()
- gtk.main()
- elif False:
- print touch_selector(win, "Test", ["A", "B", "C", "D"], 2)
- elif False:
- print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "C")
- print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "Blah")
- elif False:
- import pprint
- name, value = "", ""
- goodLocals = [
- (name, value) for (name, value) in locals().iteritems()
- if not name.startswith("_")
- ]
- pprint.pprint(goodLocals)
- elif False:
- import time
- show_information_banner(win, "Hello World")
- time.sleep(5)
- elif False:
- import time
- banner = show_busy_banner_start(win, "Hello World")
- time.sleep(5)
- show_busy_banner_end(banner)
+++ /dev/null
-#!/usr/bin/python2.5
-
-"""
-DialCentral - Front end for Google's Grand Central service.
-Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-"""
-
-import gobject
-import gtk
-
-
-class Dialpad(object):
-
- def __init__(self, widgetTree):
- self._buttons = [
- widgetTree.get_widget(buttonName)
- for buttonName in ("dialpadCall", "dialpadSMS")
- ]
- self._numberdisplay = widgetTree.get_widget("numberdisplay")
-
- def enable(self):
- for button in self._buttons:
- button.set_sensitive(False)
-
- def disable(self):
- for button in self._buttons:
- button.set_sensitive(True)
-
- @staticmethod
- def name():
- return "Dialpad"
-
- def load_settings(self, config, sectionName):
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- pass
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
-
-class AccountInfo(object):
-
- def __init__(self, widgetTree):
- self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
- self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
- self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
- self._clearCookiesButton = widgetTree.get_widget("clearcookies")
-
- self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
- self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
- self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
- self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
- self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
-
- def enable(self):
- self._callbackSelectButton.set_sensitive(False)
- self._clearCookiesButton.set_sensitive(False)
-
- self._notifyCheckbox.set_sensitive(False)
- self._minutesEntryButton.set_sensitive(False)
- self._missedCheckbox.set_sensitive(False)
- self._voicemailCheckbox.set_sensitive(False)
- self._smsCheckbox.set_sensitive(False)
-
- self._accountViewNumberDisplay.set_label("")
-
- def disable(self):
- self._callbackSelectButton.set_sensitive(True)
- self._clearCookiesButton.set_sensitive(True)
-
- self._notifyCheckbox.set_sensitive(True)
- self._minutesEntryButton.set_sensitive(True)
- self._missedCheckbox.set_sensitive(True)
- self._voicemailCheckbox.set_sensitive(True)
- self._smsCheckbox.set_sensitive(True)
-
- @staticmethod
- def update(force = False):
- return False
-
- @staticmethod
- def clear():
- pass
-
- @staticmethod
- def name():
- return "Account Info"
-
- def load_settings(self, config, sectionName):
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- pass
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
-
-class CallHistoryView(object):
-
- def __init__(self, widgetTree):
- self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
-
- def enable(self):
- self._historyFilterSelector.set_sensitive(False)
-
- def disable(self):
- self._historyFilterSelector.set_sensitive(True)
-
- def update(self, force = False):
- return False
-
- @staticmethod
- def clear():
- pass
-
- @staticmethod
- def name():
- return "Recent Calls"
-
- def load_settings(self, config, sectionName):
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- pass
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
-
-class MessagesView(object):
-
- def __init__(self, widgetTree):
- self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
- self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
-
- def enable(self):
- self._messageTypeButton.set_sensitive(False)
- self._messageStatusButton.set_sensitive(False)
-
- def disable(self):
- self._messageTypeButton.set_sensitive(True)
- self._messageStatusButton.set_sensitive(True)
-
- def update(self, force = False):
- return False
-
- @staticmethod
- def clear():
- pass
-
- @staticmethod
- def name():
- return "Messages"
-
- def load_settings(self, config, sectionName):
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- pass
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
-
-
-class ContactsView(object):
-
- def __init__(self, widgetTree):
- self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
-
- def enable(self):
- self._bookSelectionButton.set_sensitive(False)
-
- def disable(self):
- self._bookSelectionButton.set_sensitive(True)
-
- def update(self, force = False):
- return False
-
- @staticmethod
- def clear():
- pass
-
- @staticmethod
- def name():
- return "Contacts"
-
- def load_settings(self, config, sectionName):
- pass
-
- def save_settings(self, config, sectionName):
- """
- @note Thread Agnostic
- """
- pass
-
- def set_orientation(self, orientation):
- if orientation == gtk.ORIENTATION_VERTICAL:
- pass
- elif orientation == gtk.ORIENTATION_HORIZONTAL:
- pass
- else:
- raise NotImplementedError(orientation)
--- /dev/null
+from __future__ import with_statement
+
+import os
+import time
+import datetime
+import contextlib
+import logging
+
+try:
+ import cPickle
+ pickle = cPickle
+except ImportError:
+ import pickle
+
+from PyQt4 import QtCore
+
+from util import qore_utils
+from util import concurrent
+from util import misc as misc_utils
+
+import constants
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def notify_busy(log, message):
+ log.push_busy(message)
+ try:
+ yield
+ finally:
+ log.pop(message)
+
+
+class _DraftContact(object):
+
+ def __init__(self, title, description, numbersWithDescriptions):
+ self.title = title
+ self.description = description
+ self.numbers = numbersWithDescriptions
+ self.selectedNumber = numbersWithDescriptions[0][0]
+
+
+class Draft(QtCore.QObject):
+
+ sendingMessage = QtCore.pyqtSignal()
+ sentMessage = QtCore.pyqtSignal()
+ calling = QtCore.pyqtSignal()
+ called = QtCore.pyqtSignal()
+ cancelling = QtCore.pyqtSignal()
+ cancelled = QtCore.pyqtSignal()
+ error = QtCore.pyqtSignal(str)
+
+ recipientsChanged = QtCore.pyqtSignal()
+
+ def __init__(self, pool, backend, errorLog):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._contacts = {}
+ self._pool = pool
+ self._backend = backend
+
+ def send(self, text):
+ assert 0 < len(self._contacts), "No contacts selected"
+ numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
+ le = concurrent.AsyncLinearExecution(self._pool, self._send)
+ le.start(numbers, text)
+
+ def call(self):
+ assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
+ (contact, ) = self._contacts.itervalues()
+ number = misc_utils.make_ugly(contact.selectedNumber)
+ le = concurrent.AsyncLinearExecution(self._pool, self._call)
+ le.start(number)
+
+ def cancel(self):
+ le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
+ le.start()
+
+ def add_contact(self, contactId, title, description, numbersWithDescriptions):
+ if contactId in self._contacts:
+ _moduleLogger.info("Adding duplicate contact %r" % contactId)
+ # @todo Remove this evil hack to re-popup the dialog
+ self.recipientsChanged.emit()
+ return
+ contactDetails = _DraftContact(title, description, numbersWithDescriptions)
+ self._contacts[contactId] = contactDetails
+ self.recipientsChanged.emit()
+
+ def remove_contact(self, contactId):
+ 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_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):
+ oldContacts = self._contacts
+ self._contacts = {}
+ if oldContacts:
+ self.recipientsChanged.emit()
+
+ def _send(self, numbers, text):
+ self.sendingMessage.emit()
+ try:
+ with notify_busy(self._errorLog, "Sending Text"):
+ yield (
+ self._backend[0].send_sms,
+ (numbers, text),
+ {},
+ )
+ self.sentMessage.emit()
+ self.clear()
+ except Exception, e:
+ self.error.emit(str(e))
+
+ def _call(self, number):
+ self.calling.emit()
+ try:
+ with notify_busy(self._errorLog, "Calling"):
+ yield (
+ self._backend[0].call,
+ (number, ),
+ {},
+ )
+ self.called.emit()
+ self.clear()
+ except Exception, e:
+ self.error.emit(str(e))
+
+ def _cancel(self):
+ self.cancelling.emit()
+ try:
+ with notify_busy(self._errorLog, "Cancelling"):
+ yield (
+ self._backend[0].cancel,
+ (),
+ {},
+ )
+ self.cancelled.emit()
+ except Exception, e:
+ self.error.emit(str(e))
+
+
+class Session(QtCore.QObject):
+
+ # @todo Somehow add support for csv contacts
+
+ stateChange = QtCore.pyqtSignal(str)
+ loggedOut = QtCore.pyqtSignal()
+ loggedIn = QtCore.pyqtSignal()
+ callbackNumberChanged = QtCore.pyqtSignal(str)
+
+ contactsUpdated = QtCore.pyqtSignal()
+ messagesUpdated = QtCore.pyqtSignal()
+ historyUpdated = QtCore.pyqtSignal()
+ dndStateChange = QtCore.pyqtSignal(bool)
+
+ error = QtCore.pyqtSignal(str)
+
+ LOGGEDOUT_STATE = "logged out"
+ LOGGINGIN_STATE = "logging in"
+ LOGGEDIN_STATE = "logged in"
+
+ _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
+
+ _LOGGEDOUT_TIME = -1
+ _LOGGINGIN_TIME = 0
+
+ def __init__(self, errorLog, cachePath = None):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._pool = qore_utils.AsyncPool()
+ self._backend = []
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._loginOps = []
+ self._cachePath = cachePath
+ self._username = None
+ self._draft = Draft(self._pool, self._backend, self._errorLog)
+
+ self._contacts = {}
+ self._contactUpdateTime = datetime.datetime(1971, 1, 1)
+ self._messages = []
+ 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"
+ 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 = concurrent.AsyncLinearExecution(self._pool, self._login)
+ le.start(username, password)
+
+ def logout(self):
+ assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in"
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._backend[0].persist()
+ self._save_to_cache()
+
+ def clear(self):
+ assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out"
+ 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"
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self.clear()
+
+ def update_contacts(self, force = True):
+ if not force and self._contacts:
+ return
+ le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
+ self._perform_op_while_loggedin(le)
+
+ def get_contacts(self):
+ return self._contacts
+
+ def get_when_contacts_updated(self):
+ return self._contactUpdateTime
+
+ def update_messages(self, force = True):
+ if not force and self._messages:
+ return
+ le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
+ 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, force = True):
+ if not force and self._history:
+ return
+ le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
+ 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 = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
+ self._perform_op_while_loggedin(le)
+
+ def set_dnd(self, dnd):
+ le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
+ le.start(dnd)
+
+ def _set_dnd(self, dnd):
+ # I'm paranoid about our state geting out of sync so we set no matter
+ # what but act as if we have the cannonical state
+ assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in"
+ oldDnd = self._dnd
+ try:
+ with notify_busy(self._errorLog, "Setting DND Status"):
+ yield (
+ self._backend[0].set_dnd,
+ (dnd, ),
+ {},
+ )
+ except Exception, e:
+ 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):
+ return self._backend[0].get_account_number()
+
+ def get_callback_numbers(self):
+ # @todo Remove evilness (might call is_authed which can block)
+ return self._backend[0].get_callback_numbers()
+
+ def get_callback_number(self):
+ return self._callback
+
+ def set_callback_number(self, callback):
+ le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
+ le.start(callback)
+
+ def _set_callback_number(self, callback):
+ # I'm paranoid about our state geting out of sync so we set no matter
+ # what but act as if we have the cannonical state
+ assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in"
+ oldCallback = self._callback
+ try:
+ with notify_busy(self._errorLog, "Setting Callback"):
+ yield (
+ self._backend[0].set_callback_number,
+ (callback, ),
+ {},
+ )
+ except Exception, e:
+ self.error.emit(str(e))
+ return
+ self._callback = callback
+ if oldCallback != self._callback:
+ self.callbackNumberChanged.emit(self._callback)
+
+ def _login(self, username, password):
+ with notify_busy(self._errorLog, "Logging In"):
+ self._loggedInTime = self._LOGGINGIN_TIME
+ self.stateChange.emit(self.LOGGINGIN_STATE)
+ finalState = self.LOGGEDOUT_STATE
+ isLoggedIn = False
+ try:
+ if not isLoggedIn and self._backend[0].is_quick_login_possible():
+ isLoggedIn = yield (
+ self._backend[0].is_authed,
+ (),
+ {},
+ )
+ if isLoggedIn:
+ _moduleLogger.info("Logged in through cookies")
+ else:
+ # Force a clearing of the cookies
+ yield (
+ self._backend[0].logout,
+ (),
+ {},
+ )
+
+ if not isLoggedIn:
+ isLoggedIn = yield (
+ self._backend[0].login,
+ (username, password),
+ {},
+ )
+ if isLoggedIn:
+ _moduleLogger.info("Logged in through credentials")
+
+ if isLoggedIn:
+ self._loggedInTime = int(time.time())
+ oldUsername = self._username
+ self._username = username
+ finalState = self.LOGGEDIN_STATE
+ if oldUsername != self._username:
+ needOps = not self._load()
+ else:
+ needOps = True
+
+ self.loggedIn.emit()
+
+ if needOps:
+ loginOps = self._loginOps[:]
+ else:
+ loginOps = []
+ del self._loginOps[:]
+ for asyncOp in loginOps:
+ asyncOp.start()
+ except Exception, e:
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self.error.emit(str(e))
+ finally:
+ self.stateChange.emit(finalState)
+ if isLoggedIn and self._callback:
+ self.set_callback_number(self._callback)
+
+ def _load(self):
+ updateContacts = len(self._contacts) != 0
+ updateMessages = len(self._messages) != 0
+ updateHistory = len(self._history) != 0
+ oldDnd = self._dnd
+ oldCallback = self._callback
+
+ self._contacts = {}
+ self._messages = []
+ self._history = []
+ self._dnd = False
+ self._callback = ""
+
+ loadedFromCache = self._load_from_cache()
+ if loadedFromCache:
+ updateContacts = True
+ updateMessages = True
+ updateHistory = True
+
+ if updateContacts:
+ self.contactsUpdated.emit()
+ 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):
+ _moduleLogger.exception("Pickle fun loading")
+ return False
+ except:
+ _moduleLogger.exception("Weirdness loading")
+ return False
+
+ (
+ version, build,
+ contacts, contactUpdateTime,
+ messages, messageUpdateTime,
+ history, historyUpdateTime,
+ dnd, callback
+ ) = dumpedData
+
+ if misc_utils.compare_versions(
+ self._OLDEST_COMPATIBLE_FORMAT_VERSION,
+ misc_utils.parse_version(version),
+ ) <= 0:
+ _moduleLogger.info("Loaded cache")
+ self._contacts = contacts
+ self._contactUpdateTime = contactUpdateTime
+ self._messages = 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._contacts, self._contactUpdateTime,
+ 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):
+ updateContacts = len(self._contacts) != 0
+ updateMessages = len(self._messages) != 0
+ updateHistory = len(self._history) != 0
+ oldDnd = self._dnd
+ oldCallback = self._callback
+
+ self._contacts = {}
+ self._contactUpdateTime = datetime.datetime(1, 1, 1)
+ self._messages = []
+ self._messageUpdateTime = datetime.datetime(1, 1, 1)
+ self._history = []
+ self._historyUpdateTime = datetime.datetime(1, 1, 1)
+ self._dnd = False
+ self._callback = ""
+
+ if updateContacts:
+ self.contactsUpdated.emit()
+ 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()
+
+ def _update_contacts(self):
+ try:
+ with notify_busy(self._errorLog, "Updating Contacts"):
+ self._contacts = yield (
+ self._backend[0].get_contacts,
+ (),
+ {},
+ )
+ except Exception, e:
+ self.error.emit(str(e))
+ return
+ self._contactUpdateTime = datetime.datetime.now()
+ self.contactsUpdated.emit()
+
+ def _update_messages(self):
+ try:
+ with notify_busy(self._errorLog, "Updating Messages"):
+ self._messages = yield (
+ self._backend[0].get_messages,
+ (),
+ {},
+ )
+ except Exception, e:
+ self.error.emit(str(e))
+ return
+ self._messageUpdateTime = datetime.datetime.now()
+ self.messagesUpdated.emit()
+
+ def _update_history(self):
+ try:
+ with notify_busy(self._errorLog, "Updating History"):
+ self._history = yield (
+ self._backend[0].get_recent,
+ (),
+ {},
+ )
+ except Exception, e:
+ self.error.emit(str(e))
+ return
+ self._historyUpdateTime = datetime.datetime.now()
+ self.historyUpdated.emit()
+
+ def _update_dnd(self):
+ oldDnd = self._dnd
+ try:
+ self._dnd = yield (
+ self._backend[0].is_dnd,
+ (),
+ {},
+ )
+ except Exception, e:
+ self.error.emit(str(e))
+ return
+ if oldDnd != self._dnd:
+ self.dndStateChange(self._dnd)
+
+ def _perform_op_while_loggedin(self, op):
+ if self.state == self.LOGGEDIN_STATE:
+ op.start()
+ 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)
import time
import functools
import contextlib
+import logging
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class AsyncLinearExecution(object):
+
+ def __init__(self, pool, func):
+ self._pool = pool
+ self._func = func
+ self._run = None
+
+ 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:
+ pass
+ 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:
+ pass
+ else:
+ self._pool.add_task(
+ trampoline,
+ args,
+ kwds,
+ self.on_success,
+ self.on_error,
+ )
+
+ def __repr__(self):
+ return "<async %s at 0x%x>" % (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):
_moduleLogger.debug("Shutting down worker thread")
-class AsyncLinearExecution(object):
-
- def __init__(self, pool, func):
- self._pool = pool
- self._func = func
- self._run = None
-
- def start(self, *args, **kwds):
- assert self._run is None
- 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:
- pass
- 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:
- pass
- else:
- self._pool.add_task(
- trampoline,
- args,
- kwds,
- self.on_success,
- self.on_error,
- )
-
-
class AutoSignal(object):
def __init__(self, toplevel):
uglynumber = re.sub('[^0-9+]', '', prettynumber)
if uglynumber.startswith("+"):
pass
- elif uglynumber.startswith("1") and len(uglynumber) == 11:
+ elif uglynumber.startswith("1"):
uglynumber = "+"+uglynumber
- elif len(uglynumber) == 10:
+ elif 10 <= len(uglynumber):
+ assert uglynumber[0] not in ("+", "1"), "Number format confusing"
uglynumber = "+1"+uglynumber
else:
pass
- #validateRe = re.compile("^\+?[0-9]{10,}$")
- #assert validateRe.match(uglynumber) is not None
-
return uglynumber
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")
--- /dev/null
+import logging
+
+from PyQt4 import 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 _ParentThread(QtCore.QObject):
+
+ def __init__(self, pool):
+ QtCore.QObject.__init__(self)
+ self._pool = pool
+
+ @QtCore.pyqtSlot(object)
+ @misc.log_exception(_moduleLogger)
+ def _on_task_complete(self, taskResult):
+ on_success, on_error, isError, result = taskResult
+ if not self._pool._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")
+
+
+class _WorkerThread(QtCore.QObject):
+
+ taskComplete = QtCore.pyqtSignal(object)
+
+ def __init__(self, pool):
+ QtCore.QObject.__init__(self)
+ self._pool = pool
+
+ @QtCore.pyqtSlot(object)
+ @misc.log_exception(_moduleLogger)
+ def _on_task_added(self, task):
+ if not self._pool._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)
+
+ @QtCore.pyqtSlot()
+ @misc.log_exception(_moduleLogger)
+ def _on_stop_requested(self):
+ self._pool._thread.quit()
+
+
+class AsyncPool(QtCore.QObject):
+
+ _addTask = QtCore.pyqtSignal(object)
+ _stopPool = QtCore.pyqtSignal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._thread = QThread44()
+ self._isRunning = True
+ self._parent = _ParentThread(self)
+ self._worker = _WorkerThread(self)
+ self._worker.moveToThread(self._thread)
+
+ self._addTask.connect(self._worker._on_task_added)
+ self._worker.taskComplete.connect(self._parent._on_task_complete)
+ self._stopPool.connect(self._worker._on_stop_requested)
+
+ def start(self):
+ self._thread.start()
+
+ def stop(self):
+ self._isRunning = False
+ self._stopPool.emit()
+
+ 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)
--- /dev/null
+#!/usr/bin/env python
+
+import math
+import logging
+
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+try:
+ from util import misc as misc_utils
+except ImportError:
+ class misc_utils(object):
+
+ @staticmethod
+ def log_exception(logger):
+
+ def wrapper(func):
+ return func
+ return wrapper
+
+
+_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
+
+ def __init__(self, filing):
+ 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.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)
+
+ adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
+
+ numChildren = len(self._filing)
+ if numChildren == 0:
+ 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(self._canvas.rect())
+ self._paint_center_foreground(painter, selectionIndex)
+ return self._canvas
+ elif numChildren == 1:
+ if selectionIndex == 0 and self._filing[0].isEnabled():
+ painter.setBrush(self.palette.highlight())
+ else:
+ painter.setBrush(self.palette.window())
+
+ painter.fillRect(self._canvas.rect(), painter.brush())
+ 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, selectionIndex)
+
+ for i in xrange(len(self._filing)):
+ self._paint_slice_foreground(painter, i, selectionIndex)
+
+ return self._canvas
+
+ 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 i == selectionIndex and self._filing[i].isEnabled():
+ painter.setBrush(self.palette.highlight())
+ else:
+ painter.setBrush(self.palette.window())
+ painter.setPen(self.palette.mid().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, 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 = self._canvas.rect().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):
+ dark = self.palette.mid().color()
+ light = self.palette.light().color()
+ 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)
+
+ painter.setPen(QtGui.QPen(dark, 1))
+ painter.setBrush(QtCore.Qt.NoBrush)
+ painter.drawEllipse(innerRect)
+
+ 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, selectionIndex):
+ centerPos = self._canvas.rect().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)
+
+ screen = QtGui.QPainter(self)
+ screen.drawPixmap(QtCore.QPoint(0, 0), 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 = QtCore.pyqtSignal(int)
+ highlighted = QtCore.pyqtSignal(int)
+ canceled = QtCore.pyqtSignal()
+ aboutToShow = QtCore.pyqtSignal()
+ aboutToHide = QtCore.pyqtSignal()
+
+ 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)
+ self._poppedUp = 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 sizeHint(self):
+ return self._buttonArtist.pieSize()
+
+ 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._popupLocation = mouseEvent.globalPos()
+ self._delayPopupTimer.start()
+
+ @QtCore.pyqtSlot()
+ @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._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:
+ canvas = self._buttonArtist.paint(PieFiling.SELECTION_CENTER)
+ else:
+ canvas = self._buttonArtist.paint(PieFiling.SELECTION_NONE)
+
+ 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 _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 = QtCore.pyqtSignal(int)
+ highlighted = QtCore.pyqtSignal(int)
+ canceled = QtCore.pyqtSignal()
+ aboutToShow = QtCore.pyqtSignal()
+ aboutToHide = QtCore.pyqtSignal()
+
+ 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_()
--- /dev/null
+#!/usr/bin/env python
+
+
+from __future__ import division
+
+import os
+import warnings
+
+from PyQt4 import 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, rows, columns):
+ 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()
--- /dev/null
+import sys
+import contextlib
+import logging
+
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def notify_error(log):
+ try:
+ yield
+ except:
+ log.push_exception()
+
+
+class ErrorMessage(object):
+
+ LEVEL_BUSY = "busy"
+ LEVEL_INFO = "info"
+ LEVEL_ERROR = "error"
+
+ def __init__(self, message, level):
+ self._message = message
+ self._level = level
+
+ @property
+ def level(self):
+ return self._level
+
+ @property
+ def message(self):
+ return self._message
+
+
+class QErrorLog(QtCore.QObject):
+
+ messagePushed = QtCore.pyqtSignal()
+ messagePopped = QtCore.pyqtSignal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._messages = []
+
+ def push_busy(self, 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:
+ 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))
+ 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 = {
+ ErrorMessage.LEVEL_BUSY:
+ get_theme_icon(
+ #("process-working", "gtk-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),
+ }
+ 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)
+
+ 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 = QtGui.QHBoxLayout()
+ self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
+ self._controlLayout.addWidget(self._message, 1000)
+ self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
+
+ self._topLevelLayout = QtGui.QHBoxLayout()
+ self._topLevelLayout.addLayout(self._controlLayout)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._topLevelLayout)
+ self._widget.hide()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ @QtCore.pyqtSlot()
+ @QtCore.pyqtSlot(bool)
+ @misc.log_exception(_moduleLogger)
+ def _on_close(self, checked = False):
+ self._errorLog.pop()
+
+ @QtCore.pyqtSlot()
+ @misc.log_exception(_moduleLogger)
+ def _on_message_pushed(self):
+ if 1 <= len(self._errorLog) and self._widget.isHidden():
+ error = self._errorLog.peek_message()
+ self._message.setText(error.message)
+ self._severityLabel.setPixmap(self._icons[error.level])
+ self._widget.show()
+
+ @QtCore.pyqtSlot()
+ @misc.log_exception(_moduleLogger)
+ def _on_message_popped(self):
+ if len(self._errorLog) == 0:
+ self._message.setText("")
+ self._widget.hide()
+ else:
+ error = self._errorLog.peek_message()
+ self._message.setText(error.message)
+ self._severityLabel.setPixmap(self._icons[error.level])
+
+
+class QHtmlDelegate(QtGui.QStyledItemDelegate):
+
+ # @bug Not showing all of a message
+
+ 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 sizeHint(self, option, index):
+ newOption = QtGui.QStyleOptionViewItemV4(option)
+ self.initStyleOption(newOption, index)
+
+ doc = QtGui.QTextDocument()
+ doc.setHtml(newOption.text)
+ doc.setTextWidth(newOption.rect.width())
+ size = QtCore.QSize(doc.idealWidth(), doc.size().height())
+ return size
+
+
+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, isStackable):
+ pass
+
+
+def _maemo_set_autorient(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+ QtCore.Qt.WA_Maemo5AutoOrientation
+ set_autorient = _maemo_set_autorient
+except AttributeError:
+ set_autorient = _null_set_autorient
+
+
+def _null_set_landscape(window, isStackable):
+ pass
+
+
+def _maemo_set_landscape(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+ QtCore.Qt.WA_Maemo5LandscapeOrientation
+ set_landscape = _maemo_set_landscape
+except AttributeError:
+ set_landscape = _null_set_landscape
+
+
+def _null_set_portrait(window, isStackable):
+ pass
+
+
+def _maemo_set_portrait(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+ QtCore.Qt.WA_Maemo5PortraitOrientation
+ set_portrait = _maemo_set_portrait
+except AttributeError:
+ set_portrait = _null_set_portrait
+
+
+def _null_show_progress_indicator(window, isStackable):
+ pass
+
+
+def _maemo_show_progress_indicator(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, 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 screen_orientation():
+ geom = QtGui.QApplication.desktop().screenGeometry()
+ if geom.width() <= geom.height():
+ return QtCore.Qt.Vertical
+ else:
+ return QtCore.Qt.Horizontal
+
+
+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
+
self._report_error("closed too early")
def _report_success(self):
- assert not self._didReport
+ 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
+ assert not self._didReport, "Double reporting a missed call"
self._didReport = True
self._onTimeout.cancel()
self.__on_error(self, reason)
p.license = "lgpl"
p.depends = ", ".join([
"python2.6 | python2.5",
- "python-gtk2 | python2.5-gtk2",
- "python-xml | python2.5-xml",
- "python-dbus | python2.5-dbus",
+ "python-simplejson",
])
- maemoSpecificDepends = ", python-osso | python2.5-osso, python-hildon | python2.5-hildon"
p.depends += {
- "debian": ", python-glade2",
- "diablo": maemoSpecificDepends + ", python2.5-conic",
- "fremantle": maemoSpecificDepends + ", python-glade2, python-alarm",
+ "debian": ", python-qt4",
+ "diablo": ", python2.5-qt4-core, python2.5-qt4-gui",
+ "fremantle": ", python2.5-qt4-core, python2.5-qt4-gui, python2.5-qt4-maemo5",
}[distribution]
p.recommends = ", ".join([
])
"|".join((oldName, newName))
for (oldName, newName) in files
)
+ for relPath, files in unflatten_files(find_files("data", ".")).iteritems():
+ fullPath = "/opt/%s/share" % __appname__
+ if relPath:
+ fullPath += os.sep+relPath
+ p[fullPath] = list(
+ "|".join((oldName, newName))
+ for (oldName, newName) in files
+ )
p["/usr/share/applications/hildon"] = ["dialcentral.desktop"]
p["/usr/share/icons/hicolor/26x26/hildon"] = ["26x26-dialcentral.png|dialcentral.png"]
p["/usr/share/icons/hicolor/64x64/hildon"] = ["64x64-dialcentral.png|dialcentral.png"]
self.__files[path]=nfiles
+ def __getitem__(self, k):
+ return self.__files[k]
+
def __delitem__(self, k):
del self.__files[k]