--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import sys
+
+
+sys.path.append("/opt/dialcentral/lib")
+
+
+import dialcentral_qt
+
+
+if __name__ == "__main__":
+ dialcentral_qt.run()
+++ /dev/null
-Building a package
-===================
-Run
- make PLATFORM=... package
-which will create a "./pkg-.../..." heirarchy. Move this structure to somewhere on the tablet, then run pypackager.
-
-Supported PLATFORMs include
- desktop
- os2007
- os2008
-
-SDK Enviroment
-===================
-
-Native
-
-Follow install instructions
- Ubuntu: http://www.linuxuk.org/node/38
-Install Nokia stuff (for each target)
- fakeroot apt-get install maemo-explicit
-
-Userful commands
-Login
- /scratchbox/login
-Change targets
- sb-conf select DIABLO_ARMEL
- sb-conf select DIABLO_X86
-Fixing it
- fakeroot apt-get -f install
-
-Starting scratchbox
- Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite
- scratchbox
- export DISPLAY=:2
- af-sb-init.sh start
-Then running a command in the "Maemo" terminal will launch it in the Xephyr session
- Tip: run with "run-standalone.sh" for niceness?
+++ /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/
-
-Sound:
-http://www.freesound.org/samplesViewSingle.php?id=2166
-http://creativecommons.org/licenses/sampling+/1.0/
-
-placed.png, received.png, placed.png
-Free for commercial use
-http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
--- /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/
+
+Sound:
+http://www.freesound.org/samplesViewSingle.php?id=2166
+http://creativecommons.org/licenses/sampling+/1.0/
+
+placed.png, received.png, placed.png
+Free for commercial use
+http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="128pt"
+ height="128pt"
+ viewBox="0 0 128 128"
+ version="1.1"
+ id="svg2"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="dialcentral-base.svg">
+ <metadata
+ id="metadata26">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs24">
+ <linearGradient
+ id="linearGradient3805">
+ <stop
+ style="stop-color:#acdd6e;stop-opacity:1;"
+ offset="0"
+ id="stop3807" />
+ <stop
+ style="stop-color:#2f720c;stop-opacity:1;"
+ offset="1"
+ id="stop3809" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3787">
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:1;"
+ offset="0"
+ id="stop3789" />
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:0;"
+ offset="1"
+ id="stop3791" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3779">
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:1;"
+ offset="0"
+ id="stop3781" />
+ <stop
+ style="stop-color:#f6f6f6;stop-opacity:1;"
+ offset="1"
+ id="stop3783" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3771">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3773" />
+ <stop
+ style="stop-color:#f1f1f1;stop-opacity:1;"
+ offset="1"
+ id="stop3775" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3771"
+ id="linearGradient3802"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="scale(1.25,1.25)"
+ x1="10.26"
+ y1="30.534513"
+ x2="118.7"
+ y2="30.534513" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3805"
+ id="linearGradient3811"
+ x1="57.842336"
+ y1="48.798807"
+ x2="72.206342"
+ y2="48.798807"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3805"
+ id="linearGradient3819"
+ x1="58.040001"
+ y1="83.000084"
+ x2="71.960167"
+ y2="83.000084"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1176"
+ id="namedview22"
+ showgrid="false"
+ inkscape:zoom="3.6"
+ inkscape:cx="60.941186"
+ inkscape:cy="98.385892"
+ inkscape:window-x="0"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <path
+ fill="#c6c6c6"
+ d=" M 27.64 22.78 C 43.74 11.36 65.26 8.46 84.02 14.31 C 98.03 18.53 111.02 27.95 117.18 41.51 C 126.19 60.23 118.29 84.06 101.48 95.55 C 97.45 98.54 92.91 100.73 88.35 102.77 C 88.14 107.99 88.76 113.45 86.50 118.33 C 80.46 115.90 76.13 110.76 70.41 107.83 C 62.51 106.56 54.28 107.18 46.58 104.67 C 34.37 101.14 22.66 94.09 15.66 83.24 C 8.87 73.38 6.41 60.68 9.17 49.02 C 11.84 38.40 18.67 28.99 27.64 22.78 Z"
+ id="path6"
+ style="fill:#606060;fill-opacity:1" />
+ <path
+ style="fill:url(#linearGradient3802);fill-opacity:1"
+ d="M 80.9375,15.03125 C 66.292503,14.983928 51.629297,18.840625 39.09375,26.5 26.44375,34.125 16.1625,46.35 12.8125,60.9375 l -0.40625,0 c -1.05,6.325 -1.54375,12.8125 -0.21875,19.125 1.8875,8.7 5.95,16.73125 11.5,23.65625 8.65,9.9125 20.18125,17.14375 32.78125,20.90625 9.9625,3.3125 20.53125,3.6375 30.90625,4.125 7.7375,4.175 14.38125,10.225 21.65625,15.1875 0.2375,-6.725 -0.56875,-13.4625 -0.21875,-20.1875 2.3125,-1.5375 4.91875,-2.5625 7.40625,-3.75 8.175,-3.7625 15.25,-9.55 21.25,-16.1875 5.575,-6.9625 9.69375,-15.04375 11.59375,-23.78125 1.275,-6.35 0.80625,-12.81875 -0.21875,-19.15625 l 0.0174,-0.03472 C 144.54861,41.752778 128.0875,27.4125 110.3125,20.625 100.98398,16.933203 90.957761,15.063628 80.9375,15.03125 z m 0.15625,16.65625 c 4.188721,-0.13501 8.539062,2.65625 9.03125,7.03125 0.6875,7.45 -1.4875,14.79375 -2.3125,22.15625 -0.9375,6.3875 -1.8875,12.75625 -2.75,19.15625 -1.15,3.3625 0.2875,10.9125 -4.875,10.25 -2.025,-2.9875 -1.975,-6.825 -2.625,-10.25 -0.825,-6.2625 -1.775,-12.46875 -2.6875,-18.71875 -0.8625,-7.075 -2.8125,-14.10625 -2.5625,-21.28125 -0.0125,-3.9625 3.05,-7.56875 7,-8.09375 0.579688,-0.142187 1.182861,-0.230713 1.78125,-0.25 z m -0.71875,63.25 c 0.297791,-0.01522 0.589844,-0.025 0.90625,0 5.0375,-0.3375 7.68125,4.6 8.65625,8.8125 -0.975,4.225 -3.64375,9.1875 -8.71875,8.8125 -5.0375,0.3125 -7.81875,-4.575 -8.65625,-8.875 0.9375,-3.949219 3.345642,-8.521713 7.8125,-8.75 z"
+ transform="scale(0.8,0.8)"
+ id="path8"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="scccccccccccccccssccccccccssccccs" />
+ <path
+ fill="#636363"
+ d=" M 63.44 25.54 C 67.15 24.63 71.65 26.98 72.10 30.98 C 72.65 36.94 70.92 42.80 70.26 48.69 C 69.51 53.80 68.73 58.90 68.04 64.02 C 67.12 66.71 68.28 72.75 64.15 72.22 C 62.53 69.83 62.57 66.77 62.05 64.03 C 61.39 59.02 60.63 54.04 59.90 49.04 C 59.21 43.38 57.66 37.77 57.86 32.03 C 57.85 28.86 60.28 25.96 63.44 25.54 Z"
+ id="path10"
+ style="fill:url(#linearGradient3811);fill-opacity:1" />
+ <path
+ d="m 58.04,82.96 c 0.05,-3.453333 2.94,-6.978889 6.99,-7.02 4.113333,0.0078 6.927778,3.672222 6.93,7.07 0.02556,3.546667 -2.836667,7.016667 -6.98,7.05 -4.057778,0.02778 -6.936667,-3.493333 -6.94,-7.1 z"
+ id="path18"
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient3819);fill-opacity:1.0"
+ sodipodi:nodetypes="ccccc" />
+</svg>
--- /dev/null
+Your icon's dominant color is #72ab40
+A suggested disabled color is #d1ffa9
+A suggested pressed color is #57743e
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="80" width="80">
+ <defs>
+ <linearGradient id="hicg_overlay_grad" gradientUnits="userSpaceOnUse" x1="39.9995" y1="5.1816" x2="39.9995" y2="58.8019">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </linearGradient>
+ <filter id="hicg_drop_shadow">
+ <feOffset in="SourceAlpha" dx="0" dy="4"/>
+ <feGaussianBlur stdDeviation="4"/>
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0" result="shadow"/>
+ <feBlend in="SourceGraphic" in2="shadow" mode="normal"/>
+ </filter>
+ </defs>
+ <g>
+ <path id="hicg_background" fill="#72ab40" d="M79,40c0,28.893-10.105,39-39,39S1,68.893,1,40C1,11.106,11.105,1,40,1S79,11.106,79,40z"/>
+ <path id="hicg_highlight" fill="#fff" opacity="0.25" d="M39.999,1C11.105,1,1,11.106,1,40c0,28.893,10.105,39,38.999,39 C68.896,79,79,68.893,79,40C79,11.106,68.896,1,39.999,1z M39.999,78.025C11.57,78.025,1.976,68.43,1.976,40 c0-28.429,9.595-38.024,38.023-38.024c28.43,0,38.024,9.596,38.024,38.024C78.023,68.43,68.429,78.025,39.999,78.025z"/>
+ <path id="hicg_overlay" opacity="0.4" fill="url(#hicg_overlay_grad)" d="M78.977,40c0,28.893-10.1,39-38.977,39S1.023,68.893,1.023,40c0-28.894,10.1-39,38.977-39S78.977,11.106,78.977,40z"/>
+ </g>
+<g xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" transform="translate(12, 12) scale(0.4375)" filter="url(#hicg_drop_shadow)">
+ <metadata xmlns="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" id="metadata26">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" id="defs24">
+ <linearGradient id="linearGradient3805">
+ <stop style="stop-color:#acdd6e;stop-opacity:1;" offset="0" id="stop3807"/>
+ <stop style="stop-color:#2f720c;stop-opacity:1;" offset="1" id="stop3809"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3787">
+ <stop style="stop-color:#fbfbfb;stop-opacity:1;" offset="0" id="stop3789"/>
+ <stop style="stop-color:#fbfbfb;stop-opacity:0;" offset="1" id="stop3791"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3779">
+ <stop style="stop-color:#fbfbfb;stop-opacity:1;" offset="0" id="stop3781"/>
+ <stop style="stop-color:#f6f6f6;stop-opacity:1;" offset="1" id="stop3783"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3771">
+ <stop style="stop-color:#ffffff;stop-opacity:1;" offset="0" id="stop3773"/>
+ <stop style="stop-color:#f1f1f1;stop-opacity:1;" offset="1" id="stop3775"/>
+ </linearGradient>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3771" id="linearGradient3802" gradientUnits="userSpaceOnUse" gradientTransform="scale(1.25,1.25)" x1="10.26" y1="30.534513" x2="118.7" y2="30.534513"/>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3805" id="linearGradient3811" x1="57.842336" y1="48.798807" x2="72.206342" y2="48.798807" gradientUnits="userSpaceOnUse"/>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3805" id="linearGradient3819" x1="58.040001" y1="83.000084" x2="71.960167" y2="83.000084" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <sodipodi:namedview xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1176" id="namedview22" showgrid="false" inkscape:zoom="3.6" inkscape:cx="60.941186" inkscape:cy="98.385892" inkscape:window-x="0" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg2"/>
+ <path xmlns="http://www.w3.org/2000/svg" fill="#c6c6c6" d=" M 27.64 22.78 C 43.74 11.36 65.26 8.46 84.02 14.31 C 98.03 18.53 111.02 27.95 117.18 41.51 C 126.19 60.23 118.29 84.06 101.48 95.55 C 97.45 98.54 92.91 100.73 88.35 102.77 C 88.14 107.99 88.76 113.45 86.50 118.33 C 80.46 115.90 76.13 110.76 70.41 107.83 C 62.51 106.56 54.28 107.18 46.58 104.67 C 34.37 101.14 22.66 94.09 15.66 83.24 C 8.87 73.38 6.41 60.68 9.17 49.02 C 11.84 38.40 18.67 28.99 27.64 22.78 Z" id="path6" style="fill:#606060;fill-opacity:1"/>
+ <path xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" style="fill:url(#linearGradient3802);fill-opacity:1" d="M 80.9375,15.03125 C 66.292503,14.983928 51.629297,18.840625 39.09375,26.5 26.44375,34.125 16.1625,46.35 12.8125,60.9375 l -0.40625,0 c -1.05,6.325 -1.54375,12.8125 -0.21875,19.125 1.8875,8.7 5.95,16.73125 11.5,23.65625 8.65,9.9125 20.18125,17.14375 32.78125,20.90625 9.9625,3.3125 20.53125,3.6375 30.90625,4.125 7.7375,4.175 14.38125,10.225 21.65625,15.1875 0.2375,-6.725 -0.56875,-13.4625 -0.21875,-20.1875 2.3125,-1.5375 4.91875,-2.5625 7.40625,-3.75 8.175,-3.7625 15.25,-9.55 21.25,-16.1875 5.575,-6.9625 9.69375,-15.04375 11.59375,-23.78125 1.275,-6.35 0.80625,-12.81875 -0.21875,-19.15625 l 0.0174,-0.03472 C 144.54861,41.752778 128.0875,27.4125 110.3125,20.625 100.98398,16.933203 90.957761,15.063628 80.9375,15.03125 z m 0.15625,16.65625 c 4.188721,-0.13501 8.539062,2.65625 9.03125,7.03125 0.6875,7.45 -1.4875,14.79375 -2.3125,22.15625 -0.9375,6.3875 -1.8875,12.75625 -2.75,19.15625 -1.15,3.3625 0.2875,10.9125 -4.875,10.25 -2.025,-2.9875 -1.975,-6.825 -2.625,-10.25 -0.825,-6.2625 -1.775,-12.46875 -2.6875,-18.71875 -0.8625,-7.075 -2.8125,-14.10625 -2.5625,-21.28125 -0.0125,-3.9625 3.05,-7.56875 7,-8.09375 0.579688,-0.142187 1.182861,-0.230713 1.78125,-0.25 z m -0.71875,63.25 c 0.297791,-0.01522 0.589844,-0.025 0.90625,0 5.0375,-0.3375 7.68125,4.6 8.65625,8.8125 -0.975,4.225 -3.64375,9.1875 -8.71875,8.8125 -5.0375,0.3125 -7.81875,-4.575 -8.65625,-8.875 0.9375,-3.949219 3.345642,-8.521713 7.8125,-8.75 z" transform="scale(0.8,0.8)" id="path8" inkscape:connector-curvature="0" sodipodi:nodetypes="scccccccccccccccssccccccccssccccs"/>
+ <path xmlns="http://www.w3.org/2000/svg" fill="#636363" d=" M 63.44 25.54 C 67.15 24.63 71.65 26.98 72.10 30.98 C 72.65 36.94 70.92 42.80 70.26 48.69 C 69.51 53.80 68.73 58.90 68.04 64.02 C 67.12 66.71 68.28 72.75 64.15 72.22 C 62.53 69.83 62.57 66.77 62.05 64.03 C 61.39 59.02 60.63 54.04 59.90 49.04 C 59.21 43.38 57.66 37.77 57.86 32.03 C 57.85 28.86 60.28 25.96 63.44 25.54 Z" id="path10" style="fill:url(#linearGradient3811);fill-opacity:1"/>
+ <path xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" d="m 58.04,82.96 c 0.05,-3.453333 2.94,-6.978889 6.99,-7.02 4.113333,0.0078 6.927778,3.672222 6.93,7.07 0.02556,3.546667 -2.836667,7.016667 -6.98,7.05 -4.057778,0.02778 -6.936667,-3.493333 -6.94,-7.1 z" id="path18" inkscape:connector-curvature="0" style="fill:url(#linearGradient3819);fill-opacity:1.0" sodipodi:nodetypes="ccccc"/>
+</g></svg>
--- /dev/null
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=DialCentral
+Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py
+Icon=dialcentral
+Categories=Network;InstantMessaging;Qt;
--- /dev/null
+#!/usr/bin/env python
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import time
+import datetime
+import ConfigParser
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+
+
+_FREMANTLE_ALARM = "Fremantle"
+_DIABLO_ALARM = "Diablo"
+_NO_ALARM = "None"
+
+
+try:
+ import alarm
+ ALARM_TYPE = _FREMANTLE_ALARM
+except (ImportError, OSError):
+ try:
+ import osso.alarmd as alarmd
+ ALARM_TYPE = _DIABLO_ALARM
+ except (ImportError, OSError):
+ ALARM_TYPE = _NO_ALARM
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+def _get_start_time(recurrence):
+ now = datetime.datetime.now()
+ startTimeMinute = now.minute + max(recurrence, 5) # being safe
+ startTimeHour = now.hour + int(startTimeMinute / 60)
+ startTimeMinute = startTimeMinute % 59
+ now.replace(minute=startTimeMinute)
+ timestamp = int(time.mktime(now.timetuple()))
+ return timestamp
+
+
+def _create_recurrence_mask(recurrence, base):
+ """
+ >>> bin(_create_recurrence_mask(60, 60))
+ '0b1'
+ >>> bin(_create_recurrence_mask(30, 60))
+ '0b1000000000000000000000000000001'
+ >>> bin(_create_recurrence_mask(2, 60))
+ '0b10101010101010101010101010101010101010101010101010101010101'
+ >>> bin(_create_recurrence_mask(1, 60))
+ '0b111111111111111111111111111111111111111111111111111111111111'
+ """
+ mask = 0
+ for i in xrange(base / recurrence):
+ mask |= 1 << (recurrence * i)
+ return mask
+
+
+def _unpack_minutes(recurrence):
+ """
+ >>> _unpack_minutes(0)
+ (0, 0, 0)
+ >>> _unpack_minutes(1)
+ (0, 0, 1)
+ >>> _unpack_minutes(59)
+ (0, 0, 59)
+ >>> _unpack_minutes(60)
+ (0, 1, 0)
+ >>> _unpack_minutes(129)
+ (0, 2, 9)
+ >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
+ (5, 3, 2)
+ >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
+ (5, 3, 2)
+ """
+ minutesInAnHour = 60
+ minutesInDay = 24 * minutesInAnHour
+ minutesInAWeek = minutesInDay * 7
+
+ days = recurrence / minutesInDay
+ daysOfWeek = days % 7
+ recurrence -= days * minutesInDay
+ hours = recurrence / minutesInAnHour
+ recurrence -= hours * minutesInAnHour
+ mins = recurrence % minutesInAnHour
+ recurrence -= mins
+ assert recurrence == 0, "Recurrence %d" % recurrence
+ return daysOfWeek, hours, mins
+
+
+class _FremantleAlarmHandler(object):
+
+ _INVALID_COOKIE = -1
+ _REPEAT_FOREVER = -1
+ _TITLE = "Dialcentral Notifications"
+ _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+
+ def __init__(self):
+ self._recurrence = 5
+
+ self._alarmCookie = self._INVALID_COOKIE
+ self._launcher = self._LAUNCHER
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._alarmCookie = config.getint(sectionName, "alarmCookie")
+ launcher = config.get(sectionName, "notifier")
+ if launcher:
+ self._launcher = launcher
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ try:
+ config.set(sectionName, "recurrence", str(self._recurrence))
+ config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+ launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+ config.set(sectionName, "notifier", launcher)
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def apply_settings(self, enabled, recurrence):
+ if recurrence != self._recurrence or enabled != self.isEnabled:
+ if self.isEnabled:
+ self._clear_alarm()
+ if enabled:
+ self._set_alarm(recurrence)
+ self._recurrence = int(recurrence)
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._alarmCookie != self._INVALID_COOKIE
+
+ def _set_alarm(self, recurrenceMins):
+ assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
+ alarmTime = _get_start_time(recurrenceMins)
+
+ event = alarm.Event()
+ event.appid = self._TITLE
+ event.alarm_time = alarmTime
+ event.recurrences_left = self._REPEAT_FOREVER
+
+ action = event.add_actions(1)[0]
+ action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
+ action.command = self._launcher
+
+ recurrence = event.add_recurrences(1)[0]
+ recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
+ recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
+ recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
+ recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
+ recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
+ recurrence.special |= alarm.RECUR_SPECIAL_NONE
+
+ assert event.is_sane()
+ self._alarmCookie = alarm.add_event(event)
+
+ def _clear_alarm(self):
+ if self._alarmCookie == self._INVALID_COOKIE:
+ return
+ alarm.delete_event(self._alarmCookie)
+ self._alarmCookie = self._INVALID_COOKIE
+
+
+class _DiabloAlarmHandler(object):
+
+ _INVALID_COOKIE = -1
+ _TITLE = "Dialcentral Notifications"
+ _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+ _REPEAT_FOREVER = -1
+
+ def __init__(self):
+ self._recurrence = 5
+
+ bus = dbus.SystemBus()
+ self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
+ self._alarmCookie = self._INVALID_COOKIE
+ self._launcher = self._LAUNCHER
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._alarmCookie = config.getint(sectionName, "alarmCookie")
+ launcher = config.get(sectionName, "notifier")
+ if launcher:
+ self._launcher = launcher
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self._recurrence))
+ config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+ launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+ config.set(sectionName, "notifier", launcher)
+
+ def apply_settings(self, enabled, recurrence):
+ if recurrence != self._recurrence or enabled != self.isEnabled:
+ if self.isEnabled:
+ self._clear_alarm()
+ if enabled:
+ self._set_alarm(recurrence)
+ self._recurrence = int(recurrence)
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._alarmCookie != self._INVALID_COOKIE
+
+ def _set_alarm(self, recurrence):
+ assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
+ alarmTime = _get_start_time(recurrence)
+
+ #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
+ _DEFAULT_FLAGS = (
+ alarmd.ALARM_EVENT_NO_DIALOG |
+ alarmd.ALARM_EVENT_NO_SNOOZE |
+ alarmd.ALARM_EVENT_CONNECTED
+ )
+ action = []
+ action.extend(['flags', _DEFAULT_FLAGS])
+ action.extend(['title', self._TITLE])
+ action.extend(['path', self._launcher])
+ action.extend([
+ 'arguments',
+ dbus.Array(
+ [alarmTime, int(27)],
+ signature=dbus.Signature('v')
+ )
+ ]) #int(27) used in place of alarm_index
+
+ event = []
+ event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
+ event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus
+ event.append(dbus.UInt32(len(action) / 2))
+ event.extend(action)
+ event.extend(['time', dbus.Int64(alarmTime)])
+ event.extend(['recurr_interval', dbus.UInt32(recurrence)])
+ event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
+
+ self._alarmCookie = self._alarmdDBus.add_event(*event);
+
+ def _clear_alarm(self):
+ if self._alarmCookie == self._INVALID_COOKIE:
+ return
+ deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
+ self._alarmCookie = self._INVALID_COOKIE
+ assert deleteResult != -1, "Deleting of alarm event failed"
+
+
+class _ApplicationAlarmHandler(object):
+
+ _REPEAT_FOREVER = -1
+ _MIN_TO_MS_FACTORY = 1000 * 60
+
+ def __init__(self):
+ self._timer = QtCore.QTimer()
+ self._timer.setSingleShot(False)
+ self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+ self._timer.start()
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self.recurrence))
+
+ def apply_settings(self, enabled, recurrence):
+ self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
+ if enabled:
+ self._timer.start()
+ else:
+ self._timer.stop()
+
+ @property
+ def notifySignal(self):
+ return self._timer.timeout
+
+ @property
+ def recurrence(self):
+ return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
+
+ @property
+ def isEnabled(self):
+ return self._timer.isActive()
+
+
+class _NoneAlarmHandler(object):
+
+ def __init__(self):
+ self._enabled = False
+ self._recurrence = 5
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._enabled = True
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self.recurrence))
+
+ def apply_settings(self, enabled, recurrence):
+ self._enabled = enabled
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._enabled
+
+
+_BACKGROUND_ALARM_FACTORY = {
+ _FREMANTLE_ALARM: _FremantleAlarmHandler,
+ _DIABLO_ALARM: _DiabloAlarmHandler,
+ _NO_ALARM: None,
+}[ALARM_TYPE]
+
+
+class AlarmHandler(object):
+
+ ALARM_NONE = "No Alert"
+ ALARM_BACKGROUND = "Background Alert"
+ ALARM_APPLICATION = "Application Alert"
+ ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
+
+ ALARM_FACTORY = {
+ ALARM_NONE: _NoneAlarmHandler,
+ ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
+ ALARM_APPLICATION: _ApplicationAlarmHandler,
+ }
+
+ def __init__(self):
+ self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
+ self._currentAlarmType = self.ALARM_NONE
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._currentAlarmType = config.get(sectionName, "alarm")
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+ _moduleLogger.exception("Falling back to old style")
+ self._currentAlarmType = self.ALARM_BACKGROUND
+ if self._currentAlarmType not in self.ALARM_TYPES:
+ self._currentAlarmType = self.ALARM_NONE
+
+ self._init_alarm(self._currentAlarmType)
+ if self._currentAlarmType in self._alarms:
+ self._alarms[self._currentAlarmType].load_settings(config, sectionName)
+ if not self._alarms[self._currentAlarmType].isEnabled:
+ _moduleLogger.info("Config file lied, not actually enabled")
+ self._currentAlarmType = self.ALARM_NONE
+ else:
+ _moduleLogger.info("Background alerts not supported")
+ self._currentAlarmType = self.ALARM_NONE
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "alarm", self._currentAlarmType)
+ self._alarms[self._currentAlarmType].save_settings(config, sectionName)
+
+ def apply_settings(self, t, recurrence):
+ self._init_alarm(t)
+ newHandler = self._alarms[t]
+ oldHandler = self._alarms[self._currentAlarmType]
+ if newHandler != oldHandler:
+ oldHandler.apply_settings(False, 0)
+ newHandler.apply_settings(True, recurrence)
+ self._currentAlarmType = t
+
+ @property
+ def alarmType(self):
+ return self._currentAlarmType
+
+ @property
+ def backgroundNotificationsSupported(self):
+ return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
+
+ @property
+ def applicationNotifySignal(self):
+ self._init_alarm(self.ALARM_APPLICATION)
+ return self._alarms[self.ALARM_APPLICATION].notifySignal
+
+ @property
+ def recurrence(self):
+ return self._alarms[self._currentAlarmType].recurrence
+
+ @property
+ def isEnabled(self):
+ return self._currentAlarmType != self.ALARM_NONE
+
+ def _init_alarm(self, t):
+ if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
+ self._alarms[t] = self.ALARM_FACTORY[t]()
+
+
+def main():
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ import constants
+ try:
+ import optparse
+ except ImportError:
+ return
+
+ parser = optparse.OptionParser()
+ parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
+ parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+ parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+ parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
+ (commandOptions, commandArgs) = parser.parse_args()
+
+ alarmHandler = AlarmHandler()
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ alarmHandler.load_settings(config, "alarm")
+
+ if commandOptions.display:
+ print "Alarm (%s) is %s for every %d minutes" % (
+ alarmHandler._alarmCookie,
+ "enabled" if alarmHandler.isEnabled else "disabled",
+ alarmHandler.recurrence,
+ )
+ else:
+ isEnabled = commandOptions.enabled
+ recurrence = commandOptions.recurrence
+ alarmHandler.apply_settings(isEnabled, recurrence)
+
+ alarmHandler.save_settings(config, "alarm")
+ configFile = open(constants._user_settings_, "wb")
+ try:
+ config.write(configFile)
+ finally:
+ configFile.close()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import filecmp
+import ConfigParser
+import pprint
+import logging
+import logging.handlers
+
+import constants
+from backends.gvoice import gvoice
+
+
+def get_missed(backend):
+ missedPage = backend._browser.download(backend._XML_MISSED_URL)
+ missedJson = backend._grab_json(missedPage)
+ return missedJson
+
+
+def get_voicemail(backend):
+ voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL)
+ voicemailJson = backend._grab_json(voicemailPage)
+ return voicemailJson
+
+
+def get_sms(backend):
+ smsPage = backend._browser.download(backend._XML_SMS_URL)
+ smsJson = backend._grab_json(smsPage)
+ return smsJson
+
+
+def remove_reltime(data):
+ for messageData in data["messages"].itervalues():
+ for badPart in [
+ "relTime",
+ "relativeStartTime",
+ "time",
+ "star",
+ "isArchived",
+ "isRead",
+ "isSpam",
+ "isTrash",
+ "labels",
+ ]:
+ if badPart in messageData:
+ del messageData[badPart]
+ for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
+ if globalBad in data:
+ del data[globalBad]
+
+
+def is_type_changed(backend, type, get_material):
+ jsonMaterial = get_material(backend)
+ unreadCount = jsonMaterial["unreadCounts"][type]
+
+ previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
+ currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
+
+ try:
+ os.remove(previousSnapshotPath)
+ except OSError, e:
+ # check if failed purely because the old file didn't exist, which is fine
+ if e.errno != 2:
+ raise
+ try:
+ os.rename(currentSnapshotPath, previousSnapshotPath)
+ previousExists = True
+ except OSError, e:
+ # check if failed purely because the new old file didn't exist, which is fine
+ if e.errno != 2:
+ raise
+ previousExists = False
+
+ remove_reltime(jsonMaterial)
+ textMaterial = pprint.pformat(jsonMaterial)
+ currentSnapshot = file(currentSnapshotPath, "w")
+ try:
+ currentSnapshot.write(textMaterial)
+ finally:
+ currentSnapshot.close()
+
+ if unreadCount == 0 or not previousExists:
+ return False
+
+ seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
+ return not seemEqual
+
+
+def create_backend(config):
+ gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+ backend = gvoice.GVoiceBackend(gvCookiePath)
+
+ loggedIn = False
+
+ if not loggedIn:
+ loggedIn = backend.refresh_account_info() is not None
+
+ if not loggedIn:
+ import base64
+ try:
+ blobs = (
+ config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+ for i in xrange(2)
+ )
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ username, password = tuple(creds)
+ loggedIn = backend.login(username, password) is not None
+ except ConfigParser.NoOptionError, e:
+ pass
+ except ConfigParser.NoSectionError, e:
+ pass
+
+ assert loggedIn
+ return backend
+
+
+def is_changed(config, backend):
+ try:
+ notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+ notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+ notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+ except ConfigParser.NoOptionError, e:
+ notifyOnMissed = False
+ notifyOnVoicemail = False
+ notifyOnSms = False
+ except ConfigParser.NoSectionError, e:
+ notifyOnMissed = False
+ notifyOnVoicemail = False
+ notifyOnSms = False
+ logging.debug(
+ "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms)
+ )
+
+ notifySources = []
+ if notifyOnMissed:
+ notifySources.append(("missed", get_missed))
+ if notifyOnVoicemail:
+ notifySources.append(("voicemail", get_voicemail))
+ if notifyOnSms:
+ notifySources.append(("sms", get_sms))
+
+ notifyUser = False
+ for type, get_material in notifySources:
+ if is_type_changed(backend, type, get_material):
+ notifyUser = True
+ return notifyUser
+
+
+def notify_on_change():
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = create_backend(config)
+ notifyUser = is_changed(config, backend)
+
+ if notifyUser:
+ logging.info("Changed")
+ import led_handler
+ led = led_handler.LedHandler()
+ led.on()
+ else:
+ logging.info("No Change")
+
+
+if __name__ == "__main__":
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
+ rotating.setFormatter(logging.Formatter(logFormat))
+ root = logging.getLogger()
+ root.addHandler(rotating)
+ logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
+ logging.info("OS: %s" % (os.uname()[0], ))
+ logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ logging.info("Hostname: %s" % os.uname()[1])
+ try:
+ notify_on_change()
+ except:
+ logging.exception("Error")
+ raise
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Filesystem backend for contact support
+"""
+
+from __future__ import with_statement
+
+import os
+import csv
+
+
+def try_unicode(s):
+ try:
+ return s.decode("UTF-8")
+ except UnicodeDecodeError:
+ return s
+
+
+class CsvAddressBook(object):
+ """
+ Currently supported file format
+ @li Has the first line as a header
+ @li Escapes with quotes
+ @li Comma as delimiter
+ @li Column 0 is name, column 1 is number
+ """
+
+ def __init__(self, name, csvPath):
+ self._name = name
+ self._csvPath = csvPath
+ self._contacts = {}
+
+ @property
+ def name(self):
+ return self._name
+
+ def update_account(self, force = True):
+ if not force or not self._contacts:
+ return
+ self._contacts = dict(
+ self._read_csv(self._csvPath)
+ )
+
+ def get_contacts(self):
+ """
+ @returns Iterable of (contact id, contact name)
+ """
+ if not self._contacts:
+ self._contacts = dict(
+ self._read_csv(self._csvPath)
+ )
+ return self._contacts
+
+ def _read_csv(self, csvPath):
+ try:
+ f = open(csvPath, "rU")
+ csvReader = iter(csv.reader(f))
+ except IOError, e:
+ if e.errno == 2:
+ return
+ raise
+
+ header = csvReader.next()
+ nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
+
+ yieldCount = 0
+ for row in csvReader:
+ contactDetails = []
+ for (phoneType, phoneColumn) in phoneColumns:
+ try:
+ if len(row[phoneColumn]) == 0:
+ continue
+ contactDetails.append({
+ "phoneType": try_unicode(phoneType),
+ "phoneNumber": row[phoneColumn],
+ })
+ except IndexError:
+ pass
+ if 0 < len(contactDetails):
+ nameParts = (row[i].strip() for i in nameColumns)
+ nameParts = (part for part in nameParts if part)
+ fullName = " ".join(nameParts).strip()
+ if not fullName:
+ for fallbackColumn in nameFallbacks:
+ if row[fallbackColumn].strip():
+ fullName = row[fallbackColumn].strip()
+ break
+ else:
+ fullName = "Unknown"
+ fullName = try_unicode(fullName)
+ yield str(yieldCount), {
+ "contactId": "%s-%d" % (self._name, yieldCount),
+ "name": fullName,
+ "numbers": contactDetails,
+ }
+ yieldCount += 1
+
+ @classmethod
+ def _guess_columns(cls, row):
+ firstMiddleLast = [-1, -1, -1]
+ names = []
+ nameFallbacks = []
+ phones = []
+ for i, item in enumerate(row):
+ lowerItem = item.lower()
+ if 0 <= lowerItem.find("name"):
+ names.append((item, i))
+
+ if 0 <= lowerItem.find("couple"):
+ names.insert(0, (item, i))
+
+ if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"):
+ firstMiddleLast[0] = i
+ elif 0 <= lowerItem.find("middle"):
+ firstMiddleLast[1] = i
+ elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"):
+ firstMiddleLast[2] = i
+ elif 0 <= lowerItem.find("phone"):
+ phones.append((item, i))
+ elif 0 <= lowerItem.find("mobile"):
+ phones.append((item, i))
+ elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"):
+ nameFallbacks.append(i)
+ if len(names) == 0:
+ names.append(("Name", 0))
+ if len(phones) == 0:
+ phones.append(("Phone", 1))
+
+ nameColumns = [i for i in firstMiddleLast if 0 <= i]
+ if len(nameColumns) < 2:
+ del nameColumns[:]
+ nameColumns.append(names[0][1])
+
+ return nameColumns, nameFallbacks, phones
+
+
+class FilesystemAddressBookFactory(object):
+
+ FILETYPE_SUPPORT = {
+ "csv": CsvAddressBook,
+ }
+
+ def __init__(self, path):
+ self._path = path
+
+ def get_addressbooks(self):
+ for root, dirs, filenames in os.walk(self._path):
+ for filename in filenames:
+ try:
+ name, ext = filename.rsplit(".", 1)
+ except ValueError:
+ continue
+
+ try:
+ cls = self.FILETYPE_SUPPORT[ext]
+ except KeyError:
+ continue
+ yield cls(name, os.path.join(root, filename))
--- /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 itertools
+import logging
+
+from gvoice import gvoice
+
+from util import io as io_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class GVDialer(object):
+
+ MESSAGE_TEXTS = "Text"
+ MESSAGE_VOICEMAILS = "Voicemail"
+ MESSAGE_ALL = "All"
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ def __init__(self, cookieFile = None):
+ self._gvoice = gvoice.GVoiceBackend(cookieFile)
+ self._texts = []
+ self._voicemails = []
+ self._received = []
+ self._missed = []
+ self._placed = []
+
+ def is_quick_login_possible(self):
+ """
+ @returns True then refresh_account_info might be enough to login, else full login is required
+ """
+ return self._gvoice.is_quick_login_possible()
+
+ def refresh_account_info(self):
+ return self._gvoice.refresh_account_info()
+
+ def login(self, username, password):
+ """
+ Attempt to login to GoogleVoice
+ @returns Whether login was successful or not
+ """
+ return self._gvoice.login(username, password)
+
+ def logout(self):
+ self._texts = []
+ self._voicemails = []
+ self._received = []
+ self._missed = []
+ self._placed = []
+ return self._gvoice.logout()
+
+ def persist(self):
+ return self._gvoice.persist()
+
+ def is_dnd(self):
+ return self._gvoice.is_dnd()
+
+ def set_dnd(self, doNotDisturb):
+ return self._gvoice.set_dnd(doNotDisturb)
+
+ def call(self, outgoingNumber):
+ """
+ This is the main function responsible for initating the callback
+ """
+ return self._gvoice.call(outgoingNumber)
+
+ def cancel(self, outgoingNumber=None):
+ """
+ Cancels a call matching outgoing and forwarding numbers (if given).
+ Will raise an error if no matching call is being placed
+ """
+ return self._gvoice.cancel(outgoingNumber)
+
+ def send_sms(self, phoneNumbers, message):
+ self._gvoice.send_sms(phoneNumbers, message)
+
+ def search(self, query):
+ """
+ Search your Google Voice Account history for calls, voicemails, and sms
+ Returns ``Folder`` instance containting matching messages
+ """
+ return self._gvoice.search(query)
+
+ def get_feed(self, feed):
+ return self._gvoice.get_feed(feed)
+
+ def download(self, messageId, targetPath):
+ """
+ Download a voicemail or recorded call MP3 matching the given ``msg``
+ which can either be a ``Message`` instance, or a SHA1 identifier.
+ Message hashes can be found in ``self.voicemail().messages`` for example.
+ Returns location of saved file.
+ """
+ self._gvoice.download(messageId, targetPath)
+
+ def is_valid_syntax(self, number):
+ """
+ @returns If This number be called ( syntax validation only )
+ """
+ return self._gvoice.is_valid_syntax(number)
+
+ def get_account_number(self):
+ """
+ @returns The GoogleVoice phone number
+ """
+ return self._gvoice.get_account_number()
+
+ def get_callback_numbers(self):
+ """
+ @returns a dictionary mapping call back numbers to descriptions
+ @note These results are cached for 30 minutes.
+ """
+ return self._gvoice.get_callback_numbers()
+
+ def set_callback_number(self, callbacknumber):
+ """
+ Set the number that GoogleVoice calls
+ @param callbacknumber should be a proper 10 digit number
+ """
+ return self._gvoice.set_callback_number(callbacknumber)
+
+ def get_callback_number(self):
+ """
+ @returns Current callback number or None
+ """
+ return self._gvoice.get_callback_number()
+
+ def get_call_history(self, historyType):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ """
+ history = list(self._get_call_history(historyType))
+ history.sort(key=lambda item: item["time"])
+ return history
+
+ def _get_call_history(self, historyType):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ """
+ if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
+ self._received = list(self._gvoice.get_received_calls())
+ for item in self._received:
+ item["action"] = self.HISTORY_RECEIVED
+ if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
+ self._missed = list(self._gvoice.get_missed_calls())
+ for item in self._missed:
+ item["action"] = self.HISTORY_MISSED
+ if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
+ self._placed = list(self._gvoice.get_placed_calls())
+ for item in self._placed:
+ item["action"] = self.HISTORY_PLACED
+ received = self._received
+ missed = self._missed
+ placed = self._placed
+ for item in received:
+ yield item
+ for item in missed:
+ yield item
+ for item in placed:
+ yield item
+
+ def get_messages(self, messageType):
+ messages = list(self._get_messages(messageType))
+ messages.sort(key=lambda message: message["time"])
+ return messages
+
+ def _get_messages(self, messageType):
+ if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
+ self._voicemails = list(self._gvoice.get_voicemails())
+ if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
+ self._texts = list(self._gvoice.get_texts())
+ voicemails = self._voicemails
+ smss = self._texts
+
+ conversations = itertools.chain(voicemails, smss)
+ for conversation in conversations:
+ messages = conversation.messages
+ messageParts = [
+ (message.whoFrom, self._format_message(message), message.when)
+ for message in messages
+ ]
+
+ messageDetails = {
+ "id": conversation.id,
+ "contactId": conversation.contactId,
+ "name": conversation.name,
+ "time": conversation.time,
+ "relTime": conversation.relTime,
+ "prettyNumber": conversation.prettyNumber,
+ "number": conversation.number,
+ "location": conversation.location,
+ "messageParts": messageParts,
+ "type": conversation.type,
+ "isRead": conversation.isRead,
+ "isTrash": conversation.isTrash,
+ "isSpam": conversation.isSpam,
+ "isArchived": conversation.isArchived,
+ }
+ yield messageDetails
+
+ def clear_caches(self):
+ pass
+
+ def get_addressbooks(self):
+ """
+ @returns Iterable of (Address Book Factory, Book Id, Book Name)
+ """
+ yield self, "", ""
+
+ def open_addressbook(self, bookId):
+ return self
+
+ @staticmethod
+ def contact_source_short_name(contactId):
+ return "GV"
+
+ @staticmethod
+ def factory_name():
+ return "Google Voice"
+
+ def _format_message(self, message):
+ messagePartFormat = {
+ "med1": "<i>%s</i>",
+ "med2": "%s",
+ "high": "<b>%s</b>",
+ }
+ return " ".join(
+ messagePartFormat[text.accuracy] % io_utils.escape(text.text)
+ for text in message.body
+ )
+
+
+def sort_messages(allMessages):
+ sortableAllMessages = [
+ (message["time"], message)
+ for message in allMessages
+ ]
+ sortableAllMessages.sort(reverse=True)
+ return (
+ message
+ for (exactTime, message) in sortableAllMessages
+ )
+
+
+def decorate_recent(recentCallData):
+ """
+ @returns (personsName, phoneNumber, date, action)
+ """
+ contactId = recentCallData["contactId"]
+ if recentCallData["name"]:
+ header = recentCallData["name"]
+ elif recentCallData["prettyNumber"]:
+ header = recentCallData["prettyNumber"]
+ elif recentCallData["location"]:
+ header = recentCallData["location"]
+ else:
+ header = "Unknown"
+
+ number = recentCallData["number"]
+ relTime = recentCallData["relTime"]
+ action = recentCallData["action"]
+ return contactId, header, number, relTime, action
+
+
+def decorate_message(messageData):
+ contactId = messageData["contactId"]
+ exactTime = messageData["time"]
+ if messageData["name"]:
+ header = messageData["name"]
+ elif messageData["prettyNumber"]:
+ header = messageData["prettyNumber"]
+ else:
+ header = "Unknown"
+ number = messageData["number"]
+ relativeTime = messageData["relTime"]
+
+ messageParts = list(messageData["messageParts"])
+ if len(messageParts) == 0:
+ messages = ("No Transcription", )
+ elif len(messageParts) == 1:
+ messages = (messageParts[0][1], )
+ else:
+ messages = [
+ "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+ for messagePart in messageParts
+ ]
+
+ decoratedResults = contactId, header, number, relativeTime, messages
+ return decoratedResults
--- /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(25)
+
+
+def add_proxy(protocol, url, port):
+ proxyInfo = "%s:%s" % (url, port)
+ proxy = urllib2.ProxyHandler(
+ {protocol: proxyInfo}
+ )
+ opener = urllib2.build_opener(proxy)
+ urllib2.install_opener(opener)
+
+
+class MozillaEmulator(object):
+
+ USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
+ #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
+
+ def __init__(self, trycount = 1):
+ """Create a new MozillaEmulator object.
+
+ @param trycount: The download() method will retry the operation if it
+ fails. You can specify -1 for infinite retrying. A value of 0 means no
+ retrying. A value of 1 means one retry. etc."""
+ self.debug = False
+ self.trycount = trycount
+ self._cookies = cookielib.LWPCookieJar()
+ self._loadedFromCookies = False
+ self._storeCookies = False
+
+ def load_cookies(self, path):
+ assert not self._loadedFromCookies, "Load cookies only once"
+ if path is None:
+ return
+
+ self._cookies.filename = path
+ try:
+ self._cookies.load()
+ except cookielib.LoadError:
+ _moduleLogger.exception("Bad cookie file")
+ except IOError:
+ _moduleLogger.exception("No cookie file")
+ except Exception, e:
+ _moduleLogger.exception("Unknown error with cookies")
+ else:
+ self._loadedFromCookies = True
+ self._storeCookies = True
+
+ return self._loadedFromCookies
+
+ def save_cookies(self):
+ if self._storeCookies:
+ self._cookies.save()
+
+ def clear_cookies(self):
+ if self._storeCookies:
+ self._cookies.clear()
+
+ def download(self, url,
+ postdata = None, extraheaders = None, forbidRedirect = False,
+ trycount = None, only_head = False,
+ ):
+ """Download an URL with GET or POST methods.
+
+ @param postdata: It can be a string that will be POST-ed to the URL.
+ When None is given, the method will be GET instead.
+ @param extraheaders: You can add/modify HTTP headers with a dict here.
+ @param forbidRedirect: Set this flag if you do not want to handle
+ HTTP 301 and 302 redirects.
+ @param trycount: Specify the maximum number of retries here.
+ 0 means no retry on error. Using -1 means infinite retring.
+ None means the default value (that is self.trycount).
+ @param only_head: Create the openerdirector and return it. In other
+ words, this will not retrieve any content except HTTP headers.
+
+ @return: The raw HTML page data
+ """
+ _moduleLogger.debug("Performing download of %s" % url)
+
+ if extraheaders is None:
+ extraheaders = {}
+ if trycount is None:
+ trycount = self.trycount
+ cnt = 0
+
+ while True:
+ try:
+ req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect)
+ openerdirector = u.open(req)
+ if self.debug:
+ _moduleLogger.info("%r - %r" % (req.get_method(), url))
+ _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg))
+ _moduleLogger.info("%r" % (openerdirector.headers))
+ self._cookies.extract_cookies(openerdirector, req)
+ if only_head:
+ return openerdirector
+
+ return self._read(openerdirector, trycount)
+ except urllib2.URLError, e:
+ _moduleLogger.debug("%s: %s" % (e, url))
+ cnt += 1
+ if (-1 < trycount) and (trycount < cnt):
+ raise
+
+ # Retry :-)
+ _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
+
+ def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
+ if extraheaders is None:
+ extraheaders = {}
+
+ txheaders = {
+ 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
+ 'Accept-Language': 'en,en-us;q=0.5',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'User-Agent': self.USER_AGENT,
+ }
+ for key, value in extraheaders.iteritems():
+ txheaders[key] = value
+ req = urllib2.Request(url, postdata, txheaders)
+ self._cookies.add_cookie_header(req)
+ if forbidRedirect:
+ redirector = HTTPNoRedirector()
+ #_moduleLogger.info("Redirection disabled")
+ else:
+ redirector = urllib2.HTTPRedirectHandler()
+ #_moduleLogger.info("Redirection enabled")
+
+ http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
+ https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
+
+ u = urllib2.build_opener(
+ http_handler,
+ https_handler,
+ urllib2.HTTPCookieProcessor(self._cookies),
+ redirector
+ )
+ if not postdata is None:
+ req.add_data(postdata)
+ return (req, u)
+
+ def _read(self, openerdirector, trycount):
+ chunks = []
+
+ chunk = openerdirector.read()
+ chunks.append(chunk)
+ #while chunk and cnt < trycount:
+ # time.sleep(1)
+ # cnt += 1
+ # chunk = openerdirector.read()
+ # chunks.append(chunk)
+
+ data = "".join(chunks)
+
+ if "Content-Length" in openerdirector.info():
+ assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
+ openerdirector.info()["Content-Length"],
+ len(data),
+ )
+
+ return data
+
+
+class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
+ """This is a custom http redirect handler that FORBIDS redirection."""
+
+ def http_error_302(self, req, fp, code, msg, headers):
+ e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
+ if e.code in (301, 302):
+ if 'location' in headers:
+ newurl = headers.getheaders('location')[0]
+ elif 'uri' in headers:
+ newurl = headers.getheaders('uri')[0]
+ e.newurl = newurl
+ _moduleLogger.info("New url: %s" % e.newurl)
+ raise e
--- /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._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._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 refresh_account_info might be enough to login, else full login is required
+ """
+ return self._loadedFromCookies or 0.0 < self._lastAuthed
+
+ def refresh_account_info(self):
+ try:
+ page = self._get_page(self._JSON_CONTACTS_URL)
+ accountData = self._grab_account_info(page)
+ except Exception, e:
+ _moduleLogger.exception(str(e))
+ return None
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return accountData
+
+ def _get_token(self):
+ tokenPage = self._get_page(self._tokenURL)
+
+ galxTokens = self._galxRe.search(tokenPage)
+ if galxTokens is not None:
+ galxToken = galxTokens.group(1)
+ else:
+ galxToken = ""
+ _moduleLogger.debug("Could not grab GALX token")
+ return galxToken
+
+ def _login(self, username, password, token):
+ loginData = {
+ 'Email' : username,
+ 'Passwd' : password,
+ 'service': "grandcentral",
+ "ltmpl": "mobile",
+ "btmpl": "mobile",
+ "PersistentCookie": "yes",
+ "GALX": token,
+ "continue": self._JSON_CONTACTS_URL,
+ }
+
+ loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
+ return loginSuccessOrFailurePage
+
+ def login(self, username, password):
+ """
+ Attempt to login to GoogleVoice
+ @returns Whether login was successful or not
+ @blocks
+ """
+ self.logout()
+ galxToken = self._get_token()
+ loginSuccessOrFailurePage = self._login(username, password, galxToken)
+
+ try:
+ accountData = self._grab_account_info(loginSuccessOrFailurePage)
+ except Exception, e:
+ # Retry in case the redirect failed
+ # luckily refresh_account_info does everything we need for a retry
+ accountData = self.refresh_account_info()
+ if accountData is None:
+ _moduleLogger.exception(str(e))
+ return None
+ _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return accountData
+
+ def persist(self):
+ self._browser.save_cookies()
+
+ def shutdown(self):
+ self._browser.save_cookies()
+ self._token = None
+ self._lastAuthed = 0.0
+
+ def logout(self):
+ self._browser.clear_cookies()
+ self._browser.save_cookies()
+ self._token = None
+ self._lastAuthed = 0.0
+ self._callbackNumbers = {}
+
+ def is_dnd(self):
+ """
+ @blocks
+ """
+ isDndPage = self._get_page(self._isDndURL)
+
+ dndGroup = self._isDndRe.search(isDndPage)
+ if dndGroup is None:
+ return False
+ dndStatus = dndGroup.group(1)
+ isDnd = True if dndStatus.strip().lower() == "true" else False
+ return isDnd
+
+ def set_dnd(self, doNotDisturb):
+ """
+ @blocks
+ """
+ dndPostData = {
+ "doNotDisturb": 1 if doNotDisturb else 0,
+ }
+
+ dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
+
+ def call(self, outgoingNumber):
+ """
+ This is the main function responsible for initating the callback
+ @blocks
+ """
+ outgoingNumber = self._send_validation(outgoingNumber)
+ subscriberNumber = None
+ phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
+
+ callData = {
+ 'outgoingNumber': outgoingNumber,
+ 'forwardingNumber': self._callbackNumber,
+ 'subscriberNumber': subscriberNumber or 'undefined',
+ 'phoneType': str(phoneType),
+ 'remember': '1',
+ }
+ _moduleLogger.info("%r" % callData)
+
+ page = self._get_page_with_token(
+ self._callUrl,
+ callData,
+ )
+ self._parse_with_validation(page)
+ return True
+
+ def cancel(self, outgoingNumber=None):
+ """
+ Cancels a call matching outgoing and forwarding numbers (if given).
+ Will raise an error if no matching call is being placed
+ @blocks
+ """
+ page = self._get_page_with_token(
+ self._callCancelURL,
+ {
+ 'outgoingNumber': outgoingNumber or 'undefined',
+ 'forwardingNumber': self._callbackNumber or 'undefined',
+ 'cancelType': 'C2C',
+ },
+ )
+ self._parse_with_validation(page)
+
+ def send_sms(self, phoneNumbers, message):
+ """
+ @blocks
+ """
+ validatedPhoneNumbers = [
+ self._send_validation(phoneNumber)
+ for phoneNumber in phoneNumbers
+ ]
+ flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
+ page = self._get_page_with_token(
+ self._sendSmsURL,
+ {
+ 'phoneNumber': flattenedPhoneNumbers,
+ 'text': unicode(message).encode("utf-8"),
+ },
+ )
+ self._parse_with_validation(page)
+
+ def search(self, query):
+ """
+ Search your Google Voice Account history for calls, voicemails, and sms
+ Returns ``Folder`` instance containting matching messages
+ @blocks
+ """
+ page = self._get_page(
+ self._XML_SEARCH_URL,
+ {"q": query},
+ )
+ json, html = extract_payload(page)
+ return json
+
+ def get_feed(self, feed):
+ """
+ @blocks
+ """
+ actualFeed = "_XML_%s_URL" % feed.upper()
+ feedUrl = getattr(self, actualFeed)
+
+ page = self._get_page(feedUrl)
+ json, html = extract_payload(page)
+
+ return json
+
+ def recording_url(self, messageId):
+ url = self._downloadVoicemailURL+messageId
+ return url
+
+ def download(self, messageId, targetPath):
+ """
+ Download a voicemail or recorded call MP3 matching the given ``msg``
+ which can either be a ``Message`` instance, or a SHA1 identifier.
+ Message hashes can be found in ``self.voicemail().messages`` for example.
+ @returns location of saved file.
+ @blocks
+ """
+ page = self._get_page(self.recording_url(messageId))
+ with open(targetPath, 'wb') as fo:
+ fo.write(page)
+
+ def is_valid_syntax(self, number):
+ """
+ @returns If This number be called ( syntax validation only )
+ """
+ return self._validateRe.match(number) is not None
+
+ def get_account_number(self):
+ """
+ @returns The GoogleVoice phone number
+ """
+ return self._accountNum
+
+ def get_callback_numbers(self):
+ """
+ @returns a dictionary mapping call back numbers to descriptions
+ @note These results are cached for 30 minutes.
+ """
+ return self._callbackNumbers
+
+ def set_callback_number(self, callbacknumber):
+ """
+ Set the number that GoogleVoice calls
+ @param callbacknumber should be a proper 10 digit number
+ """
+ self._callbackNumber = callbacknumber
+ _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
+ return True
+
+ def get_callback_number(self):
+ """
+ @returns Current callback number or None
+ """
+ return self._callbackNumber
+
+ def get_received_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
+
+ def get_missed_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_MISSED_URL))
+
+ def get_placed_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_PLACED_URL))
+
+ def get_csv_contacts(self):
+ data = {
+ "groupToExport": "mine",
+ "exportType": "ALL",
+ "out": "OUTLOOK_CSV",
+ }
+ encodedData = urllib.urlencode(data)
+ contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
+ return contacts
+
+ def get_voicemails(self):
+ """
+ @blocks
+ """
+ voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
+ voicemailHtml = self._grab_html(voicemailPage)
+ voicemailJson = self._grab_json(voicemailPage)
+ if voicemailJson is None:
+ return ()
+ parsedVoicemail = self._parse_voicemail(voicemailHtml)
+ voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
+ return voicemails
+
+ def get_texts(self):
+ """
+ @blocks
+ """
+ smsPage = self._get_page(self._XML_SMS_URL)
+ smsHtml = self._grab_html(smsPage)
+ smsJson = self._grab_json(smsPage)
+ if smsJson is None:
+ return ()
+ parsedSms = self._parse_sms(smsHtml)
+ smss = self._merge_conversation_sources(parsedSms, smsJson)
+ return smss
+
+ def get_unread_counts(self):
+ countPage = self._get_page(self._JSON_SMS_COUNT_URL)
+ counts = parse_json(countPage)
+ counts = counts["unreadCounts"]
+ return counts
+
+ def mark_message(self, messageId, asRead):
+ """
+ @blocks
+ """
+ postData = {
+ "read": 1 if asRead else 0,
+ "id": messageId,
+ }
+
+ markPage = self._get_page(self._markAsReadURL, postData)
+
+ def archive_message(self, messageId):
+ """
+ @blocks
+ """
+ postData = {
+ "id": messageId,
+ }
+
+ markPage = self._get_page(self._archiveMessageURL, postData)
+
+ def _grab_json(self, flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+ return jsonTree
+
+ def _grab_html(self, flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+ htmlElement = xmlTree.getchildren()[1]
+ flatHtml = htmlElement.text
+ return flatHtml
+
+ def _grab_account_info(self, page):
+ accountData = parse_json(page)
+ self._token = accountData["r"]
+ self._accountNum = accountData["number"]["raw"]
+ for callback in accountData["phones"].itervalues():
+ self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
+ if len(self._callbackNumbers) == 0:
+ _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
+ return accountData
+
+ def _send_validation(self, number):
+ if not self.is_valid_syntax(number):
+ raise ValueError('Number is not valid: "%s"' % number)
+ return number
+
+ def _parse_recent(self, recentPage):
+ allRecentHtml = self._grab_html(recentPage)
+ allRecentData = self._parse_history(allRecentHtml)
+ for recentCallData in allRecentData:
+ yield recentCallData
+
+ def _parse_history(self, historyHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
+ for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ exactTime = google_strptime(exactTime)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ locationGroup = self._voicemailLocationRegex.search(messageHtml)
+ location = locationGroup.group(1).strip() if locationGroup else ""
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ name = nameGroup.group(1).strip() if nameGroup else ""
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ yield {
+ "id": messageId.strip(),
+ "contactId": contactId,
+ "name": unescape(name),
+ "time": exactTime,
+ "relTime": relativeTime,
+ "prettyNumber": prettyNumber,
+ "number": number,
+ "location": unescape(location),
+ }
+
+ @staticmethod
+ def _interpret_voicemail_regex(group):
+ quality, content, number = group.group(2), group.group(3), group.group(4)
+ text = MessageText()
+ if quality is not None and content is not None:
+ text.accuracy = quality
+ text.text = unescape(content)
+ return text
+ elif number is not None:
+ text.accuracy = MessageText.ACCURACY_HIGH
+ text.text = number
+ return text
+
+ def _parse_voicemail(self, voicemailHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+ for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_VOICEMAIL
+ conv.id = messageId.strip()
+
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = google_strptime(exactTimeText)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ locationGroup = self._voicemailLocationRegex.search(messageHtml)
+ conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
+ messageParts = [
+ self._interpret_voicemail_regex(group)
+ for group in messageGroups
+ ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
+ message = Message()
+ message.body = messageParts
+ message.whoFrom = conv.name
+ try:
+ message.when = conv.time.strftime("%I:%M %p")
+ except ValueError:
+ _moduleLogger.exception("Confusing time provided: %r" % conv.time)
+ message.when = "Unknown"
+ conv.messages = (message, )
+
+ yield conv
+
+ @staticmethod
+ def _interpret_sms_message_parts(fromPart, textPart, timePart):
+ text = MessageText()
+ text.accuracy = MessageText.ACCURACY_MEDIUM
+ text.text = unescape(textPart)
+
+ message = Message()
+ message.body = (text, )
+ message.whoFrom = fromPart
+ message.when = timePart
+
+ return message
+
+ def _parse_sms(self, smsHtml):
+ splitSms = self._seperateVoicemailsRegex.split(smsHtml)
+ for messageId, messageHtml in itergroup(splitSms[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_SMS
+ conv.id = messageId.strip()
+
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = google_strptime(exactTimeText)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ conv.location = ""
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ fromGroups = self._smsFromRegex.finditer(messageHtml)
+ fromParts = (group.group(1).strip() for group in fromGroups)
+ textGroups = self._smsTextRegex.finditer(messageHtml)
+ textParts = (group.group(1).strip() for group in textGroups)
+ timeGroups = self._smsTimeRegex.finditer(messageHtml)
+ timeParts = (group.group(1).strip() for group in timeGroups)
+
+ messageParts = itertools.izip(fromParts, textParts, timeParts)
+ messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
+ conv.messages = messages
+
+ yield conv
+
+ @staticmethod
+ def _merge_conversation_sources(parsedMessages, json):
+ for message in parsedMessages:
+ jsonItem = json["messages"][message.id]
+ message.isRead = jsonItem["isRead"]
+ message.isSpam = jsonItem["isSpam"]
+ message.isTrash = jsonItem["isTrash"]
+ message.isArchived = "inbox" not in jsonItem["labels"]
+ yield message
+
+ def _get_page(self, url, data = None, refererUrl = None):
+ headers = {}
+ if refererUrl is not None:
+ headers["Referer"] = refererUrl
+
+ encodedData = urllib.urlencode(data) if data is not None else None
+
+ try:
+ page = self._browser.download(url, encodedData, None, headers)
+ except urllib2.URLError, e:
+ _moduleLogger.error("Translating error: %s" % str(e))
+ raise NetworkError("%s is not accesible" % url)
+
+ return page
+
+ def _get_page_with_token(self, url, data = None, refererUrl = None):
+ if data is None:
+ data = {}
+ data['_rnr_se'] = self._token
+
+ page = self._get_page(url, data, refererUrl)
+
+ return page
+
+ def _parse_with_validation(self, page):
+ json = parse_json(page)
+ self._validate_response(json)
+ return json
+
+ def _validate_response(self, response):
+ """
+ Validates that the JSON response is A-OK
+ """
+ try:
+ assert response is not None, "Response not provided"
+ assert 'ok' in response, "Response lacks status"
+ assert response['ok'], "Response not good"
+ except AssertionError:
+ try:
+ if response["data"]["code"] == 20:
+ raise RuntimeError(
+"""Ambiguous error 20 returned by Google Voice.
+Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
+ except KeyError:
+ pass
+ raise RuntimeError('There was a problem with GV: %s' % response)
+
+
+_UNESCAPE_ENTITIES = {
+ """: '"',
+ " ": " ",
+ "'": "'",
+}
+
+
+def unescape(text):
+ plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+ return plain
+
+
+def google_strptime(time):
+ """
+ Hack: Google always returns the time in the same locale. Sadly if the
+ local system's locale is different, there isn't a way to perfectly handle
+ the time. So instead we handle implement some time formatting
+ """
+ abbrevTime = time[:-3]
+ parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
+ if time.endswith("PM"):
+ parsedTime += datetime.timedelta(hours=12)
+ return parsedTime
+
+
+def itergroup(iterator, count, padValue = None):
+ """
+ Iterate in groups of 'count' values. If there
+ aren't enough values, the last result is padded with
+ None.
+
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print list(val)
+ [1, 2, 3]
+ [4, 5, 6]
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ (7, None, None)
+ >>> for val in itergroup("123456", 3):
+ ... print tuple(val)
+ ('1', '2', '3')
+ ('4', '5', '6')
+ >>> for val in itergroup("123456", 3):
+ ... print repr("".join(val))
+ '123'
+ '456'
+ """
+ paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+ nIterators = (paddedIterator, ) * count
+ return itertools.izip(*nIterators)
+
+
+def safe_eval(s):
+ _TRUE_REGEX = re.compile("true")
+ _FALSE_REGEX = re.compile("false")
+ _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
+ s = _TRUE_REGEX.sub("True", s)
+ s = _FALSE_REGEX.sub("False", s)
+ s = _COMMENT_REGEX.sub("#", s)
+ try:
+ results = eval(s, {}, {})
+ except SyntaxError:
+ _moduleLogger.exception("Oops")
+ results = None
+ return results
+
+
+def _fake_parse_json(flattened):
+ return safe_eval(flattened)
+
+
+def _actual_parse_json(flattened):
+ return simplejson.loads(flattened)
+
+
+if simplejson is None:
+ parse_json = _fake_parse_json
+else:
+ parse_json = _actual_parse_json
+
+
+def extract_payload(flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+
+ htmlElement = xmlTree.getchildren()[1]
+ flatHtml = htmlElement.text
+
+ return jsonTree, flatHtml
+
+
+def guess_phone_type(number):
+ if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
+ return GVoiceBackend.PHONE_TYPE_GIZMO
+ else:
+ return GVoiceBackend.PHONE_TYPE_MOBILE
+
+
+def get_sane_callback(backend):
+ """
+ Try to set a sane default callback number on these preferences
+ 1) 1747 numbers ( Gizmo )
+ 2) anything with gizmo in the name
+ 3) anything with computer in the name
+ 4) the first value
+ """
+ numbers = backend.get_callback_numbers()
+
+ priorityOrderedCriteria = [
+ ("\+1747", None),
+ ("1747", None),
+ ("747", None),
+ (None, "gizmo"),
+ (None, "computer"),
+ (None, "sip"),
+ (None, None),
+ ]
+
+ for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
+ numberMatcher = None
+ descriptionMatcher = None
+ if numberCriteria is not None:
+ numberMatcher = re.compile(numberCriteria)
+ elif descriptionCriteria is not None:
+ descriptionMatcher = re.compile(descriptionCriteria, re.I)
+
+ for number, description in numbers.iteritems():
+ if numberMatcher is not None and numberMatcher.match(number) is None:
+ continue
+ if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
+ continue
+ return number
+
+
+def set_sane_callback(backend):
+ """
+ Try to set a sane default callback number on these preferences
+ 1) 1747 numbers ( Gizmo )
+ 2) anything with gizmo in the name
+ 3) anything with computer in the name
+ 4) the first value
+ """
+ number = get_sane_callback(backend)
+ backend.set_callback_number(number)
+
+
+def _is_not_special(name):
+ return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
+
+
+def to_dict(obj):
+ members = inspect.getmembers(obj)
+ return dict((name, value) for (name, value) in members if _is_not_special(name))
+
+
+def grab_debug_info(username, password):
+ cookieFile = os.path.join(".", "raw_cookies.txt")
+ try:
+ os.remove(cookieFile)
+ except OSError:
+ pass
+
+ backend = GVoiceBackend(cookieFile)
+ browser = backend._browser
+
+ _TEST_WEBPAGES = [
+ ("token", backend._tokenURL),
+ ("login", backend._loginURL),
+ ("isdnd", backend._isDndURL),
+ ("account", backend._XML_ACCOUNT_URL),
+ ("html_contacts", backend._XML_CONTACTS_URL),
+ ("contacts", backend._JSON_CONTACTS_URL),
+ ("csv", backend._CSV_CONTACTS_URL),
+
+ ("voicemail", backend._XML_VOICEMAIL_URL),
+ ("html_sms", backend._XML_SMS_URL),
+ ("sms", backend._JSON_SMS_URL),
+ ("count", backend._JSON_SMS_COUNT_URL),
+
+ ("recent", backend._XML_RECENT_URL),
+ ("placed", backend._XML_PLACED_URL),
+ ("recieved", backend._XML_RECEIVED_URL),
+ ("missed", backend._XML_MISSED_URL),
+ ]
+
+ # Get Pages
+ print "Grabbing pre-login pages"
+ for name, url in _TEST_WEBPAGES:
+ try:
+ page = browser.download(url)
+ except StandardError, e:
+ print e.message
+ continue
+ print "\tWriting to file"
+ with open("not_loggedin_%s.txt" % name, "w") as f:
+ f.write(page)
+
+ # Login
+ print "Attempting login"
+ galxToken = backend._get_token()
+ loginSuccessOrFailurePage = backend._login(username, password, galxToken)
+ with open("loggingin.txt", "w") as f:
+ print "\tWriting to file"
+ f.write(loginSuccessOrFailurePage)
+ try:
+ backend._grab_account_info(loginSuccessOrFailurePage)
+ except Exception:
+ # Retry in case the redirect failed
+ # luckily refresh_account_info does everything we need for a retry
+ loggedIn = backend.refresh_account_info() is not None
+ if not loggedIn:
+ raise
+
+ # Get Pages
+ print "Grabbing post-login pages"
+ for name, url in _TEST_WEBPAGES:
+ try:
+ page = browser.download(url)
+ except StandardError, e:
+ print str(e)
+ continue
+ print "\tWriting to file"
+ with open("loggedin_%s.txt" % name, "w") as f:
+ f.write(page)
+
+ # Cookies
+ browser.save_cookies()
+ print "\tWriting cookies to file"
+ with open("cookies.txt", "w") as f:
+ f.writelines(
+ "%s: %s\n" % (c.name, c.value)
+ for c in browser._cookies
+ )
+
+
+def grab_voicemails(username, password):
+ cookieFile = os.path.join(".", "raw_cookies.txt")
+ try:
+ os.remove(cookieFile)
+ except OSError:
+ pass
+
+ backend = GVoiceBackend(cookieFile)
+ backend.login(username, password)
+ voicemails = list(backend.get_voicemails())
+ for voicemail in voicemails:
+ print voicemail.id
+ backend.download(voicemail.id, ".")
+
+
+def main():
+ import sys
+ logging.basicConfig(level=logging.DEBUG)
+ args = sys.argv
+ if 3 <= len(args):
+ username = args[1]
+ password = args[2]
+
+ grab_debug_info(username, password)
+ grab_voicemails(username, password)
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+
+
+class NullAddressBook(object):
+
+ @property
+ def name(self):
+ return "None"
+
+ def update_account(self, force = True):
+ pass
+
+ def get_contacts(self):
+ return {}
+
+
+class NullAddressBookFactory(object):
+
+ def get_addressbooks(self):
+ yield NullAddressBook()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+if qt_compat.USES_PYSIDE:
+ try:
+ import QtMobility.Contacts as _QtContacts
+ QtContacts = _QtContacts
+ except ImportError:
+ QtContacts = None
+else:
+ QtContacts = None
+
+import null_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QtContactsAddressBook(object):
+
+ def __init__(self, name, uri):
+ self._name = name
+ self._uri = uri
+ self._manager = QtContacts.QContactManager.fromUri(uri)
+ self._contacts = None
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def error(self):
+ return self._manager.error()
+
+ def update_account(self, force = True):
+ if not force and self._contacts is not None:
+ return
+ self._contacts = dict(self._get_contacts())
+
+ def get_contacts(self):
+ if self._contacts is None:
+ self._contacts = dict(self._get_contacts())
+ return self._contacts
+
+ def _get_contacts(self):
+ contacts = self._manager.contacts()
+ for contact in contacts:
+ contactId = contact.localId()
+ contactName = contact.displayLabel()
+ phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
+ phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
+ contactDetails = phones
+ if 0 < len(contactDetails):
+ yield str(contactId), {
+ "contactId": str(contactId),
+ "name": contactName,
+ "numbers": contactDetails,
+ }
+
+
+class _QtContactsAddressBookFactory(object):
+
+ def __init__(self):
+ self._availableManagers = {}
+
+ availableMgrs = QtContacts.QContactManager.availableManagers()
+ availableMgrs.remove("invalid")
+ for managerName in availableMgrs:
+ params = {}
+ managerUri = QtContacts.QContactManager.buildUri(managerName, params)
+ self._availableManagers[managerName] = managerUri
+
+ def get_addressbooks(self):
+ for name, uri in self._availableManagers.iteritems():
+ book = QtContactsAddressBook(name, uri)
+ if book.error:
+ _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
+ else:
+ yield book
+
+
+class _EmptyAddressBookFactory(object):
+
+ def get_addressbooks(self):
+ if False:
+ yield None
+
+
+if QtContacts is not None:
+ QtContactsAddressBookFactory = _QtContactsAddressBookFactory
+else:
+ QtContactsAddressBookFactory = _EmptyAddressBookFactory
+ _moduleLogger.info("QtContacts support not available")
+
+
+if __name__ == "__main__":
+ factory = QtContactsAddressBookFactory()
+ books = factory.get_addressbooks()
+ for book in books:
+ print book.name
+ print book.get_contacts()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+try:
+ import telepathy as _telepathy
+ import util.tp_utils as telepathy_utils
+ telepathy = _telepathy
+except ImportError:
+ telepathy = None
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _FakeSignaller(object):
+
+ def start(self):
+ pass
+
+ def stop(self):
+ pass
+
+
+class _MissedCallWatcher(QtCore.QObject):
+
+ callMissed = qt_compat.Signal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._isStarted = False
+ self._isSupported = True
+
+ self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
+ self._outstandingRequests = []
+
+ @property
+ def isSupported(self):
+ return self._isSupported
+
+ @property
+ def isStarted(self):
+ return self._isStarted
+
+ def start(self):
+ if self._isStarted:
+ _moduleLogger.info("voicemail monitor already started")
+ return
+ try:
+ self._newChannelSignaller.start()
+ except RuntimeError:
+ _moduleLogger.exception("Missed call detection not supported")
+ self._newChannelSignaller = _FakeSignaller()
+ self._isSupported = False
+ self._isStarted = True
+
+ def stop(self):
+ if not self._isStarted:
+ _moduleLogger.info("voicemail monitor stopped without starting")
+ return
+ _moduleLogger.info("Stopping voicemail refresh")
+ self._newChannelSignaller.stop()
+
+ # I don't want to trust whether the cancel happens within the current
+ # callback or not which could be the deciding factor between invalid
+ # iterators or infinite loops
+ localRequests = [r for r in self._outstandingRequests]
+ for request in localRequests:
+ localRequests.cancel()
+
+ self._isStarted = False
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
+ if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
+ return
+
+ conn = telepathy.client.Connection(serviceName, connObjectPath)
+ try:
+ chan = telepathy.client.Channel(serviceName, channelObjectPath)
+ except dbus.exceptions.UnknownMethodException:
+ _moduleLogger.exception("Client might not have implemented a deprecated method")
+ return
+ missDetection = telepathy_utils.WasMissedCall(
+ bus, conn, chan, self._on_missed_call, self._on_error_for_missed
+ )
+ self._outstandingRequests.append(missDetection)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_missed_call(self, missDetection):
+ _moduleLogger.info("Missed a call")
+ self.callMissed.emit()
+ self._outstandingRequests.remove(missDetection)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_error_for_missed(self, missDetection, reason):
+ _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
+ self._outstandingRequests.remove(missDetection)
+
+
+class _DummyMissedCallWatcher(QtCore.QObject):
+
+ callMissed = qt_compat.Signal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._isStarted = False
+
+ @property
+ def isSupported(self):
+ return False
+
+ @property
+ def isStarted(self):
+ return self._isStarted
+
+ def start(self):
+ self._isStarted = True
+
+ def stop(self):
+ if not self._isStarted:
+ _moduleLogger.info("voicemail monitor stopped without starting")
+ return
+ _moduleLogger.info("Stopping voicemail refresh")
+ self._isStarted = False
+
+
+if telepathy is not None:
+ MissedCallWatcher = _MissedCallWatcher
+else:
+ MissedCallWatcher = _DummyMissedCallWatcher
+
+
+if __name__ == "__main__":
+ pass
+
--- /dev/null
+import os
+
+__pretty_app_name__ = "DialCentral"
+__app_name__ = "dialcentral"
+__version__ = "1.3.6"
+__build__ = 0
+__app_magic__ = 0xdeadbeef
+_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
+_user_settings_ = "%s/settings.ini" % _data_path_
+_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_
+_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
+_notifier_logpath_ = "%s/notifier.log" % _data_path_
+IS_MAEMO = True
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: UTF8 -*-
+
+from __future__ import with_statement
+
+import os
+import base64
+import ConfigParser
+import functools
+import logging
+import logging.handlers
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+import alarm_handler
+from util import qtpie
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+import session
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Dialcentral(qwrappers.ApplicationWrapper):
+
+ _DATA_PATHS = [
+ os.path.join(os.path.dirname(__file__), "../share"),
+ os.path.join(os.path.dirname(__file__), "../data"),
+ ]
+
+ def __init__(self, app):
+ self._dataPath = None
+ self._aboutDialog = None
+ self.notifyOnMissed = False
+ self.notifyOnVoicemail = False
+ self.notifyOnSms = False
+
+ self._streamHandler = None
+ self._ledHandler = None
+ self._alarmHandler = alarm_handler.AlarmHandler()
+
+ qwrappers.ApplicationWrapper.__init__(self, app, constants)
+
+ def load_settings(self):
+ try:
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ except IOError, e:
+ _moduleLogger.info("No settings")
+ return
+ except ValueError:
+ _moduleLogger.info("Settings were corrupt")
+ return
+ except ConfigParser.MissingSectionHeaderError:
+ _moduleLogger.info("Settings were corrupt")
+ return
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ self._mainWindow.load_settings(config)
+
+ def save_settings(self):
+ _moduleLogger.info("Saving settings")
+ config = ConfigParser.SafeConfigParser()
+
+ self._mainWindow.save_settings(config)
+
+ with open(constants._user_settings_, "wb") as configFile:
+ config.write(configFile)
+
+ def get_icon(self, name):
+ if self._dataPath is None:
+ for path in self._DATA_PATHS:
+ if os.path.exists(os.path.join(path, name)):
+ self._dataPath = path
+ break
+ if self._dataPath is not None:
+ icon = QtGui.QIcon(os.path.join(self._dataPath, name))
+ return icon
+ else:
+ return None
+
+ def get_resource(self, name):
+ if self._dataPath is None:
+ for path in self._DATA_PATHS:
+ if os.path.exists(os.path.join(path, name)):
+ self._dataPath = path
+ break
+ if self._dataPath is not None:
+ return os.path.join(self._dataPath, name)
+ else:
+ return None
+
+ def _close_windows(self):
+ qwrappers.ApplicationWrapper._close_windows(self)
+ if self._aboutDialog is not None:
+ self._aboutDialog.close()
+
+ @property
+ def fsContactsPath(self):
+ return os.path.join(constants._data_path_, "contacts")
+
+ @property
+ def streamHandler(self):
+ if self._streamHandler is None:
+ import stream_handler
+ self._streamHandler = stream_handler.StreamHandler()
+ return self._streamHandler
+
+ @property
+ def alarmHandler(self):
+ return self._alarmHandler
+
+ @property
+ def ledHandler(self):
+ if self._ledHandler is None:
+ import led_handler
+ self._ledHandler = led_handler.LedHandler()
+ return self._ledHandler
+
+ def _new_main_window(self):
+ return MainWindow(None, self)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_about(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ if self._aboutDialog is None:
+ import dialogs
+ self._aboutDialog = dialogs.AboutDialog(self)
+ response = self._aboutDialog.run(self._mainWindow.window)
+
+
+class DelayedWidget(object):
+
+ def __init__(self, app, settingsNames):
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._widget = QtGui.QWidget()
+ self._widget.setContentsMargins(0, 0, 0, 0)
+ self._widget.setLayout(self._layout)
+ self._settings = dict((name, "") for name in settingsNames)
+
+ self._child = None
+ self._isEnabled = True
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def has_child(self):
+ return self._child is not None
+
+ def set_child(self, child):
+ if self._child is not None:
+ self._layout.removeWidget(self._child.toplevel)
+ self._child = child
+ if self._child is not None:
+ self._layout.addWidget(self._child.toplevel)
+
+ self._child.set_settings(self._settings)
+
+ if self._isEnabled:
+ self._child.enable()
+ else:
+ self._child.disable()
+
+ @property
+ def child(self):
+ return self._child
+
+ def enable(self):
+ self._isEnabled = True
+ if self._child is not None:
+ self._child.enable()
+
+ def disable(self):
+ self._isEnabled = False
+ if self._child is not None:
+ self._child.disable()
+
+ def clear(self):
+ if self._child is not None:
+ self._child.clear()
+
+ def refresh(self, force=True):
+ if self._child is not None:
+ self._child.refresh(force)
+
+ def get_settings(self):
+ if self._child is not None:
+ return self._child.get_settings()
+ else:
+ return self._settings
+
+ def set_settings(self, settings):
+ if self._child is not None:
+ self._child.set_settings(settings)
+ else:
+ self._settings = settings
+
+
+def _tab_factory(tab, app, session, errorLog):
+ import gv_views
+ return gv_views.__dict__[tab](app, session, errorLog)
+
+
+class MainWindow(qwrappers.WindowWrapper):
+
+ KEYPAD_TAB = 0
+ RECENT_TAB = 1
+ MESSAGES_TAB = 2
+ CONTACTS_TAB = 3
+ MAX_TABS = 4
+
+ _TAB_TITLES = [
+ "Dialpad",
+ "History",
+ "Messages",
+ "Contacts",
+ ]
+ assert len(_TAB_TITLES) == MAX_TABS
+
+ _TAB_ICONS = [
+ "dialpad.png",
+ "history.png",
+ "messages.png",
+ "contacts.png",
+ ]
+ assert len(_TAB_ICONS) == MAX_TABS
+
+ _TAB_CLASS = [
+ functools.partial(_tab_factory, "Dialpad"),
+ functools.partial(_tab_factory, "History"),
+ functools.partial(_tab_factory, "Messages"),
+ functools.partial(_tab_factory, "Contacts"),
+ ]
+ assert len(_TAB_CLASS) == MAX_TABS
+
+ # Hack to allow delay importing/loading of tabs
+ _TAB_SETTINGS_NAMES = [
+ (),
+ ("filter", ),
+ ("status", "type"),
+ ("selectedAddressbook", ),
+ ]
+ assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
+
+ def __init__(self, parent, app):
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
+ self._window.resized.connect(self._on_window_resized)
+ self._errorLog = self._app.errorLog
+
+ self._session = session.Session(self._errorLog, constants._data_path_)
+ self._session.error.connect(self._on_session_error)
+ self._session.loggedIn.connect(self._on_login)
+ self._session.loggedOut.connect(self._on_logout)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+ self._session.newMessages.connect(self._on_new_message_alert)
+ self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
+ self._voicemailRefreshDelay = QtCore.QTimer()
+ self._voicemailRefreshDelay.setInterval(30 * 1000)
+ self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
+ self._voicemailRefreshDelay.setSingleShot(True)
+ self._callHandler = None
+ self._updateVoicemailOnMissedCall = False
+
+ self._defaultCredentials = "", ""
+ self._curentCredentials = "", ""
+ self._currentTab = 0
+
+ self._credentialsDialog = None
+ self._smsEntryDialog = None
+ self._accountDialog = None
+
+ self._tabsContents = [
+ DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
+ for i in xrange(self.MAX_TABS)
+ ]
+ for tab in self._tabsContents:
+ tab.disable()
+
+ self._tabWidget = QtGui.QTabWidget()
+ if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ defaultTabIconSize = self._tabWidget.iconSize()
+ defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height()
+ for tabIndex, (tabTitle, tabIcon) in enumerate(
+ zip(self._TAB_TITLES, self._TAB_ICONS)
+ ):
+ icon = self._app.get_icon(tabIcon)
+ if constants.IS_MAEMO and icon is not None:
+ tabTitle = ""
+
+ if icon is None:
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
+ else:
+ iconSize = icon.availableSizes()[0]
+ defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width())
+ defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height())
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
+ defaultTabIconWidth = max(defaultTabIconWidth, 32)
+ defaultTabIconHeight = max(defaultTabIconHeight, 32)
+ self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight))
+ self._tabWidget.currentChanged.connect(self._on_tab_changed)
+ self._tabWidget.setContentsMargins(0, 0, 0, 0)
+
+ self._layout.addWidget(self._tabWidget)
+
+ self._loginAction = QtGui.QAction(None)
+ self._loginAction.setText("Login")
+ self._loginAction.triggered.connect(self._on_login_requested)
+
+ self._importAction = QtGui.QAction(None)
+ self._importAction.setText("Import")
+ self._importAction.triggered.connect(self._on_import)
+
+ self._accountAction = QtGui.QAction(None)
+ self._accountAction.setText("Account")
+ self._accountAction.triggered.connect(self._on_account)
+
+ self._refreshConnectionAction = QtGui.QAction(None)
+ self._refreshConnectionAction.setText("Refresh Connection")
+ self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
+ self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
+
+ self._refreshTabAction = QtGui.QAction(None)
+ self._refreshTabAction.setText("Refresh Tab")
+ self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
+ self._refreshTabAction.triggered.connect(self._on_refresh)
+
+ fileMenu = self._window.menuBar().addMenu("&File")
+ fileMenu.addAction(self._loginAction)
+ fileMenu.addAction(self._refreshTabAction)
+ fileMenu.addAction(self._refreshConnectionAction)
+
+ toolsMenu = self._window.menuBar().addMenu("&Tools")
+ toolsMenu.addAction(self._accountAction)
+ toolsMenu.addAction(self._importAction)
+ toolsMenu.addAction(self._app.aboutAction)
+
+ self._initialize_tab(self._tabWidget.currentIndex())
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+ self.update_orientation(self._app.orientation)
+
+ def _init_call_handler(self):
+ if self._callHandler is not None:
+ return
+ import call_handler
+ self._callHandler = call_handler.MissedCallWatcher()
+ self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
+
+ def set_default_credentials(self, username, password):
+ self._defaultCredentials = username, password
+
+ def get_default_credentials(self):
+ return self._defaultCredentials
+
+ def walk_children(self):
+ if self._smsEntryDialog is not None:
+ return (self._smsEntryDialog, )
+ else:
+ return ()
+
+ def start(self):
+ qwrappers.WindowWrapper.start(self)
+ assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up"
+ if self._defaultCredentials != ("", ""):
+ username, password = self._defaultCredentials[0], self._defaultCredentials[1]
+ self._curentCredentials = username, password
+ self._session.login(username, password)
+ else:
+ self._prompt_for_login()
+
+ def close(self):
+ for diag in (
+ self._credentialsDialog,
+ self._accountDialog,
+ ):
+ if diag is not None:
+ diag.close()
+ for child in self.walk_children():
+ child.window.destroyed.disconnect(self._on_child_close)
+ child.window.closed.disconnect(self._on_child_close)
+ child.close()
+ self._window.close()
+
+ def destroy(self):
+ qwrappers.WindowWrapper.destroy(self)
+ if self._session.state != self._session.LOGGEDOUT_STATE:
+ self._session.logout()
+
+ def get_current_tab(self):
+ return self._currentTab
+
+ def set_current_tab(self, tabIndex):
+ self._tabWidget.setCurrentIndex(tabIndex)
+
+ def load_settings(self, config):
+ blobs = "", ""
+ isFullscreen = False
+ orientation = self._app.orientation
+ tabIndex = 0
+ try:
+ blobs = [
+ config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+ for i in xrange(len(self.get_default_credentials()))
+ ]
+ isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
+ tabIndex = config.getint(constants.__pretty_app_name__, "tab")
+ orientation = config.get(constants.__pretty_app_name__, "orientation")
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing option %s" % (
+ constants._user_settings_,
+ e.option,
+ ),
+ )
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ try:
+ self._app.alarmHandler.load_settings(config, "alarm")
+ self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+ self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+ self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+ self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing option %s" % (
+ constants._user_settings_,
+ e.option,
+ ),
+ )
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ self.set_default_credentials(*creds)
+ self._app.fullscreenAction.setChecked(isFullscreen)
+ self._app.set_orientation(orientation)
+ self.set_current_tab(tabIndex)
+
+ backendId = 2 # For backwards compatibility
+ for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+ sectionName = "%s - %s" % (backendId, tabTitle)
+ settings = self._tabsContents[tabIndex].get_settings()
+ for settingName in settings.iterkeys():
+ try:
+ settingValue = config.get(sectionName, settingName)
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ return
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ return
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+ return
+ settings[settingName] = settingValue
+ self._tabsContents[tabIndex].set_settings(settings)
+
+ def save_settings(self, config):
+ config.add_section(constants.__pretty_app_name__)
+ config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
+ config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
+ config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
+ for i, value in enumerate(self.get_default_credentials()):
+ blob = base64.b64encode(value)
+ config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+
+ config.add_section("alarm")
+ self._app.alarmHandler.save_settings(config, "alarm")
+ config.add_section("2 - Account Info")
+ config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
+ config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
+ config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
+ config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
+
+ backendId = 2 # For backwards compatibility
+ for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+ sectionName = "%s - %s" % (backendId, tabTitle)
+ config.add_section(sectionName)
+ tabSettings = self._tabsContents[tabIndex].get_settings()
+ for settingName, settingValue in tabSettings.iteritems():
+ config.set(sectionName, settingName, settingValue)
+
+ def update_orientation(self, orientation):
+ qwrappers.WindowWrapper.update_orientation(self, orientation)
+ windowOrientation = self.idealWindowOrientation
+ if windowOrientation == QtCore.Qt.Horizontal:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+ def _initialize_tab(self, index):
+ assert index < self.MAX_TABS, "Invalid tab"
+ if not self._tabsContents[index].has_child():
+ tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
+ self._tabsContents[index].set_child(tab)
+ self._tabsContents[index].refresh(force=False)
+
+ def _prompt_for_login(self):
+ if self._credentialsDialog is None:
+ import dialogs
+ self._credentialsDialog = dialogs.CredentialsDialog(self._app)
+ credentials = self._credentialsDialog.run(
+ self._defaultCredentials[0], self._defaultCredentials[1], self.window
+ )
+ if credentials is None:
+ return
+ username, password = credentials
+ self._curentCredentials = username, password
+ self._session.login(username, password)
+
+ def _show_account_dialog(self):
+ if self._accountDialog is None:
+ import dialogs
+ self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
+ self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
+ self._accountDialog.settingsApproved.connect(self._on_settings_approved)
+
+ if self._callHandler is not None and not self._callHandler.isSupported:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
+ elif self._updateVoicemailOnMissedCall:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
+ else:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
+ self._accountDialog.notifications = self._app.alarmHandler.alarmType
+ self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
+ self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
+ self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
+ self._accountDialog.notifyOnSms = self._app.notifyOnSms
+ self._accountDialog.set_callbacks(
+ self._session.get_callback_numbers(), self._session.get_callback_number()
+ )
+ accountNumberToDisplay = self._session.get_account_number()
+ if not accountNumberToDisplay:
+ accountNumberToDisplay = "Not Available (%s)" % self._session.state
+ self._accountDialog.set_account_number(accountNumberToDisplay)
+ self._accountDialog.orientation = self._app.orientation
+
+ self._accountDialog.run()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_settings_approved(self):
+ if self._accountDialog.doClear:
+ self._session.logout_and_clear()
+ self._defaultCredentials = "", ""
+ self._curentCredentials = "", ""
+ for tab in self._tabsContents:
+ tab.disable()
+ else:
+ callbackNumber = self._accountDialog.selectedCallback
+ self._session.set_callback_number(callbackNumber)
+
+ if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED:
+ pass
+ elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
+ self._updateVoicemailOnMissedCall = True
+ self._init_call_handler()
+ self._callHandler.start()
+ else:
+ self._updateVoicemailOnMissedCall = False
+ if self._callHandler is not None:
+ self._callHandler.stop()
+ if (
+ self._accountDialog.notifyOnMissed or
+ self._accountDialog.notifyOnVoicemail or
+ self._accountDialog.notifyOnSms
+ ):
+ notifications = self._accountDialog.notifications
+ else:
+ notifications = self._accountDialog.ALARM_NONE
+ self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
+
+ self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
+ self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
+ self._app.notifyOnSms = self._accountDialog.notifyOnSms
+ self._app.set_orientation(self._accountDialog.orientation)
+ self._app.save_settings()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_window_resized(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ windowOrientation = self.idealWindowOrientation
+ if windowOrientation == QtCore.Qt.Horizontal:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_new_message_alert(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+ if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
+ self._errorLog.push_message("New messages")
+ else:
+ self._app.ledHandler.on()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_missed(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_session_error(self, message):
+ with qui_utils.notify_error(self._errorLog):
+ self._errorLog.push_error(message)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_login(self):
+ with qui_utils.notify_error(self._errorLog):
+ changedAccounts = self._defaultCredentials != self._curentCredentials
+ noCallback = not self._session.get_callback_number()
+ if changedAccounts or noCallback:
+ self._show_account_dialog()
+
+ self._defaultCredentials = self._curentCredentials
+
+ for tab in self._tabsContents:
+ tab.enable()
+ self._initialize_tab(self._currentTab)
+ if self._updateVoicemailOnMissedCall:
+ self._init_call_handler()
+ self._callHandler.start()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_logout(self):
+ with qui_utils.notify_error(self._errorLog):
+ for tab in self._tabsContents:
+ tab.disable()
+ if self._callHandler is not None:
+ self._callHandler.stop()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_app_alert(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._session.state == self._session.LOGGEDIN_STATE:
+ messageType = {
+ (True, True): self._session.MESSAGE_ALL,
+ (True, False): self._session.MESSAGE_TEXTS,
+ (False, True): self._session.MESSAGE_VOICEMAILS,
+ }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
+ self._session.update_messages(messageType, force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._session.draft.get_num_contacts() == 0:
+ return
+
+ if self._smsEntryDialog is None:
+ import dialogs
+ self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
+ self._smsEntryDialog.window.destroyed.connect(self._on_child_close)
+ self._smsEntryDialog.window.closed.connect(self._on_child_close)
+ self._smsEntryDialog.window.show()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_child_close(self, obj = None):
+ self._smsEntryDialog = None
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_login_requested(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._prompt_for_login()
+
+ @qt_compat.Slot(int)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_tab_changed(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ self._currentTab = index
+ self._initialize_tab(index)
+ if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+ self._app.ledHandler.off()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._tabsContents[self._currentTab].refresh(force=True)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_connection(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._session.refresh_connection()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_import(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
+ csvName = unicode(csvName)
+ if not csvName:
+ return
+ import shutil
+ shutil.copy2(csvName, self._app.fsContactsPath)
+ if self._tabsContents[self.CONTACTS_TAB].has_child:
+ self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_account(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings"
+ self._show_account_dialog()
+
+
+def run():
+ try:
+ os.makedirs(constants._data_path_)
+ except OSError, e:
+ if e.errno != 17:
+ raise
+
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
+ rotating.setFormatter(logging.Formatter(logFormat))
+ root = logging.getLogger()
+ root.addHandler(rotating)
+ _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
+ _moduleLogger.info("OS: %s" % (os.uname()[0], ))
+ _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ _moduleLogger.info("Hostname: %s" % os.uname()[1])
+
+ try:
+ import gobject
+ gobject.threads_init()
+ except ImportError:
+ _moduleLogger.info("GObject support not available")
+ try:
+ import dbus
+ try:
+ from dbus.mainloop.qt import DBusQtMainLoop
+ DBusQtMainLoop(set_as_default=True)
+ _moduleLogger.info("Using Qt mainloop")
+ except ImportError:
+ try:
+ from dbus.mainloop.glib import DBusGMainLoop
+ DBusGMainLoop(set_as_default=True)
+ _moduleLogger.info("Using GObject mainloop")
+ except ImportError:
+ _moduleLogger.info("Mainloop not available")
+ except ImportError:
+ _moduleLogger.info("DBus support not available")
+
+ app = QtGui.QApplication([])
+ handle = Dialcentral(app)
+ qtpie.init_pies()
+ return app.exec_()
+
+
+if __name__ == "__main__":
+ import sys
+
+ val = run()
+ sys.exit(val)
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import functools
+import copy
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class CredentialsDialog(object):
+
+ def __init__(self, app):
+ self._app = app
+ self._usernameField = QtGui.QLineEdit()
+ self._passwordField = QtGui.QLineEdit()
+ self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
+
+ self._credLayout = QtGui.QGridLayout()
+ self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
+ self._credLayout.addWidget(self._usernameField, 0, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
+ self._credLayout.addWidget(self._passwordField, 1, 1)
+
+ self._loginButton = QtGui.QPushButton("&Login")
+ self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
+ self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._credLayout)
+ self._layout.addWidget(self._buttonLayout)
+
+ self._dialog = QtGui.QDialog()
+ self._dialog.setWindowTitle("Login")
+ self._dialog.setLayout(self._layout)
+ self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
+ self._buttonLayout.accepted.connect(self._dialog.accept)
+ self._buttonLayout.rejected.connect(self._dialog.reject)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ self._dialog.addAction(self._closeWindowAction)
+ self._dialog.addAction(app.quitAction)
+ self._dialog.addAction(app.fullscreenAction)
+
+ def run(self, defaultUsername, defaultPassword, parent=None):
+ self._dialog.setParent(parent, QtCore.Qt.Dialog)
+ try:
+ self._usernameField.setText(defaultUsername)
+ self._passwordField.setText(defaultPassword)
+
+ response = self._dialog.exec_()
+ if response == QtGui.QDialog.Accepted:
+ return str(self._usernameField.text()), str(self._passwordField.text())
+ elif response == QtGui.QDialog.Rejected:
+ return None
+ else:
+ _moduleLogger.error("Unknown response")
+ return None
+ finally:
+ self._dialog.setParent(None, QtCore.Qt.Dialog)
+
+ def close(self):
+ try:
+ self._dialog.reject()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._dialog.reject()
+
+
+class AboutDialog(object):
+
+ def __init__(self, app):
+ self._app = app
+ self._title = QtGui.QLabel(
+ "<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._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addWidget(self._title)
+ self._layout.addWidget(self._copyright)
+ self._layout.addWidget(self._link)
+ self._layout.addWidget(self._buttonLayout)
+
+ self._dialog = QtGui.QDialog()
+ self._dialog.setWindowTitle("About")
+ self._dialog.setLayout(self._layout)
+ self._buttonLayout.rejected.connect(self._dialog.reject)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ self._dialog.addAction(self._closeWindowAction)
+ self._dialog.addAction(app.quitAction)
+ self._dialog.addAction(app.fullscreenAction)
+
+ def run(self, parent=None):
+ self._dialog.setParent(parent, QtCore.Qt.Dialog)
+
+ response = self._dialog.exec_()
+ return response
+
+ def close(self):
+ try:
+ self._dialog.reject()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._dialog.reject()
+
+
+class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper):
+
+ # @bug Can't enter custom callback numbers
+
+ _RECURRENCE_CHOICES = [
+ (1, "1 minute"),
+ (2, "2 minutes"),
+ (3, "3 minutes"),
+ (5, "5 minutes"),
+ (8, "8 minutes"),
+ (10, "10 minutes"),
+ (15, "15 minutes"),
+ (30, "30 minutes"),
+ (45, "45 minutes"),
+ (60, "1 hour"),
+ (3*60, "3 hours"),
+ (6*60, "6 hours"),
+ (12*60, "12 hours"),
+ ]
+
+ ALARM_NONE = "No Alert"
+ ALARM_BACKGROUND = "Background Alert"
+ ALARM_APPLICATION = "Application Alert"
+
+ VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
+ VOICEMAIL_CHECK_DISABLED = "Disabled"
+ VOICEMAIL_CHECK_ENABLED = "Enabled"
+
+ settingsApproved = qt_compat.Signal()
+
+ def __init__(self, parent, app, errorLog):
+ QtCore.QObject.__init__(self)
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._app = app
+ self._doClear = False
+
+ self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
+ self._notificationSelecter = QtGui.QComboBox()
+ self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
+ self._notificationTimeSelector = QtGui.QComboBox()
+ #self._notificationTimeSelector.setEditable(True)
+ self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
+ for _, label in self._RECURRENCE_CHOICES:
+ self._notificationTimeSelector.addItem(label)
+ self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
+ self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
+ self._smsNotificationButton = QtGui.QCheckBox("SMS")
+ self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
+ self._clearButton = QtGui.QPushButton("Clear Account")
+ self._clearButton.clicked.connect(self._on_clear)
+ self._callbackSelector = QtGui.QComboBox()
+ #self._callbackSelector.setEditable(True)
+ self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
+ self._orientationSelector = QtGui.QComboBox()
+ for orientationMode in [
+ self._app.DEFAULT_ORIENTATION,
+ self._app.AUTO_ORIENTATION,
+ self._app.LANDSCAPE_ORIENTATION,
+ self._app.PORTRAIT_ORIENTATION,
+ ]:
+ self._orientationSelector.addItem(orientationMode)
+
+ self._update_notification_state()
+
+ self._credLayout = QtGui.QGridLayout()
+ self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
+ self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
+ self._credLayout.addWidget(self._callbackSelector, 1, 1)
+ self._credLayout.addWidget(self._notificationSelecter, 2, 0)
+ self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
+ self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
+ self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
+ self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0)
+ self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0)
+ self._credLayout.addWidget(self._orientationSelector, 7, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 8, 0)
+ self._credLayout.addWidget(QtGui.QLabel(""), 9, 0)
+ self._credLayout.addWidget(self._clearButton, 9, 1)
+
+ self._credWidget = QtGui.QWidget()
+ self._credWidget.setLayout(self._credLayout)
+ self._credWidget.setContentsMargins(0, 0, 0, 0)
+ self._scrollSettings = QtGui.QScrollArea()
+ self._scrollSettings.setWidget(self._credWidget)
+ self._scrollSettings.setWidgetResizable(True)
+ self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+ self._applyButton = QtGui.QPushButton("&Apply")
+ self._applyButton.clicked.connect(self._on_settings_apply)
+ self._cancelButton = QtGui.QPushButton("&Cancel")
+ self._cancelButton.clicked.connect(self._on_settings_cancel)
+ self._buttonLayout = QtGui.QHBoxLayout()
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._cancelButton)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._applyButton)
+ self._buttonLayout.addStretch()
+
+ self._layout.addWidget(self._scrollSettings)
+ self._layout.addLayout(self._buttonLayout)
+ self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
+
+ self._window.setWindowTitle("Account")
+ self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
+
+ @property
+ def doClear(self):
+ return self._doClear
+
+ def setIfNotificationsSupported(self, isSupported):
+ if isSupported:
+ self._notificationSelecter.clear()
+ self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
+ self._notificationTimeSelector.setEnabled(False)
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+ else:
+ self._notificationSelecter.clear()
+ self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
+ self._notificationTimeSelector.setEnabled(False)
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+
+ def set_account_number(self, num):
+ self._accountNumberLabel.setText(num)
+
+ orientation = property(
+ lambda self: str(self._orientationSelector.currentText()),
+ lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode),
+ )
+
+ def _set_voicemail_on_missed(self, status):
+ if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
+ self._voicemailOnMissedButton.setChecked(False)
+ self._voicemailOnMissedButton.hide()
+ elif status == self.VOICEMAIL_CHECK_DISABLED:
+ self._voicemailOnMissedButton.setChecked(False)
+ self._voicemailOnMissedButton.show()
+ elif status == self.VOICEMAIL_CHECK_ENABLED:
+ self._voicemailOnMissedButton.setChecked(True)
+ self._voicemailOnMissedButton.show()
+ else:
+ raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
+
+ def _get_voicemail_on_missed(self):
+ if not self._voicemailOnMissedButton.isVisible():
+ return self.VOICEMAIL_CHECK_NOT_SUPPORTED
+ elif self._voicemailOnMissedButton.isChecked():
+ return self.VOICEMAIL_CHECK_ENABLED
+ else:
+ return self.VOICEMAIL_CHECK_DISABLED
+
+ updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
+
+ notifications = property(
+ lambda self: str(self._notificationSelecter.currentText()),
+ lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled),
+ )
+
+ notifyOnMissed = property(
+ lambda self: self._missedCallsNotificationButton.isChecked(),
+ lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
+ )
+
+ notifyOnVoicemail = property(
+ lambda self: self._voicemailNotificationButton.isChecked(),
+ lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
+ )
+
+ notifyOnSms = property(
+ lambda self: self._smsNotificationButton.isChecked(),
+ lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
+ )
+
+ def _get_notification_time(self):
+ index = self._notificationTimeSelector.currentIndex()
+ minutes = self._RECURRENCE_CHOICES[index][0]
+ return minutes
+
+ def _set_notification_time(self, minutes):
+ for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
+ if time == minutes:
+ self._notificationTimeSelector.setCurrentIndex(i)
+ break
+ else:
+ self._notificationTimeSelector.setCurrentIndex(0)
+
+ notificationTime = property(_get_notification_time, _set_notification_time)
+
+ @property
+ def selectedCallback(self):
+ index = self._callbackSelector.currentIndex()
+ data = str(self._callbackSelector.itemData(index))
+ return data
+
+ def set_callbacks(self, choices, default):
+ self._callbackSelector.clear()
+
+ self._callbackSelector.addItem("Not Set", "")
+
+ uglyDefault = misc_utils.make_ugly(default)
+ if not uglyDefault:
+ uglyDefault = default
+ for number, description in choices.iteritems():
+ prettyNumber = misc_utils.make_pretty(number)
+ uglyNumber = misc_utils.make_ugly(number)
+ if not uglyNumber:
+ prettyNumber = number
+ uglyNumber = number
+
+ self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
+ if uglyNumber == uglyDefault:
+ self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
+
+ def run(self):
+ self._doClear = False
+ self._window.show()
+
+ def close(self):
+ try:
+ self._window.hide()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def _update_notification_state(self):
+ currentText = str(self._notificationSelecter.currentText())
+ if currentText == self.ALARM_BACKGROUND:
+ self._notificationTimeSelector.setEnabled(True)
+
+ self._missedCallsNotificationButton.setEnabled(True)
+ self._voicemailNotificationButton.setEnabled(True)
+ self._smsNotificationButton.setEnabled(True)
+ elif currentText == self.ALARM_APPLICATION:
+ self._notificationTimeSelector.setEnabled(True)
+
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(True)
+ self._smsNotificationButton.setEnabled(True)
+
+ self._missedCallsNotificationButton.setChecked(False)
+ else:
+ self._notificationTimeSelector.setEnabled(False)
+
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+
+ self._missedCallsNotificationButton.setChecked(False)
+ self._voicemailNotificationButton.setChecked(False)
+ self._smsNotificationButton.setChecked(False)
+
+ @qt_compat.Slot(int)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_notification_change(self, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_notification_state()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_settings_cancel(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.hide()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ def _on_settings_apply(self, checked = False):
+ self.__on_settings_apply(checked)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def __on_settings_apply(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.settingsApproved.emit()
+ self.hide()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_clear(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._doClear = True
+ self.settingsApproved.emit()
+ self.hide()
+
+
+class ContactList(object):
+
+ _SENTINEL_ICON = QtGui.QIcon()
+
+ def __init__(self, app, session):
+ self._app = app
+ self._session = session
+ self._targetLayout = QtGui.QVBoxLayout()
+ self._targetList = QtGui.QWidget()
+ self._targetList.setLayout(self._targetLayout)
+ self._uiItems = []
+ self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+
+ @property
+ def toplevel(self):
+ return self._targetList
+
+ def setVisible(self, isVisible):
+ self._targetList.setVisible(isVisible)
+
+ def update(self):
+ cids = list(self._session.draft.get_contacts())
+ amountCommon = min(len(cids), len(self._uiItems))
+
+ # Run through everything in common
+ for i in xrange(0, amountCommon):
+ cid = cids[i]
+ uiItem = self._uiItems[i]
+ title = self._session.draft.get_title(cid)
+ description = self._session.draft.get_description(cid)
+ numbers = self._session.draft.get_numbers(cid)
+ uiItem["cid"] = cid
+ uiItem["title"] = title
+ uiItem["description"] = description
+ uiItem["numbers"] = numbers
+ uiItem["label"].setText(title)
+ self._populate_number_selector(uiItem["selector"], cid, i, numbers)
+ uiItem["rowWidget"].setVisible(True)
+
+ # More contacts than ui items
+ for i in xrange(amountCommon, len(cids)):
+ cid = cids[i]
+ title = self._session.draft.get_title(cid)
+ description = self._session.draft.get_description(cid)
+ numbers = self._session.draft.get_numbers(cid)
+
+ titleLabel = QtGui.QLabel(title)
+ titleLabel.setWordWrap(True)
+ numberSelector = QtGui.QComboBox()
+ self._populate_number_selector(numberSelector, cid, i, numbers)
+
+ callback = functools.partial(
+ self._on_change_number,
+ i
+ )
+ callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
+ numberSelector.activated.connect(
+ qt_compat.Slot(int)(callback)
+ )
+
+ if self._closeIcon is self._SENTINEL_ICON:
+ deleteButton = QtGui.QPushButton("Delete")
+ else:
+ deleteButton = QtGui.QPushButton(self._closeIcon, "")
+ deleteButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ callback = functools.partial(
+ self._on_remove_contact,
+ i
+ )
+ callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
+ deleteButton.clicked.connect(callback)
+
+ rowLayout = QtGui.QHBoxLayout()
+ rowLayout.addWidget(titleLabel, 1000)
+ rowLayout.addWidget(numberSelector, 0)
+ rowLayout.addWidget(deleteButton, 0)
+ rowWidget = QtGui.QWidget()
+ rowWidget.setLayout(rowLayout)
+ self._targetLayout.addWidget(rowWidget)
+
+ uiItem = {}
+ uiItem["cid"] = cid
+ uiItem["title"] = title
+ uiItem["description"] = description
+ uiItem["numbers"] = numbers
+ uiItem["label"] = titleLabel
+ uiItem["selector"] = numberSelector
+ uiItem["rowWidget"] = rowWidget
+ self._uiItems.append(uiItem)
+ amountCommon = i+1
+
+ # More UI items than contacts
+ for i in xrange(amountCommon, len(self._uiItems)):
+ uiItem = self._uiItems[i]
+ uiItem["rowWidget"].setVisible(False)
+ amountCommon = i+1
+
+ def _populate_number_selector(self, selector, cid, cidIndex, numbers):
+ selector.clear()
+
+ selectedNumber = self._session.draft.get_selected_number(cid)
+ if len(numbers) == 1:
+ # If no alt numbers available, check the address book
+ numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
+ else:
+ defaultIndex = _index_number(numbers, selectedNumber)
+
+ for number, description in numbers:
+ if description:
+ label = "%s - %s" % (number, description)
+ else:
+ label = number
+ selector.addItem(label)
+ selector.setVisible(True)
+ if 1 < len(numbers):
+ selector.setEnabled(True)
+ selector.setCurrentIndex(defaultIndex)
+ else:
+ selector.setEnabled(False)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_change_number(self, cidIndex, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ # Exception thrown when the first item is removed
+ try:
+ cid = self._uiItems[cidIndex]["cid"]
+ numbers = self._session.draft.get_numbers(cid)
+ except IndexError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ except KeyError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ number = numbers[index][0]
+ self._session.draft.set_selected_number(cid, number)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_remove_contact(self, index, toggled):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._session.draft.remove_contact(self._uiItems[index]["cid"])
+
+
+class VoicemailPlayer(object):
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._errorLog = errorLog
+ self._token = None
+ self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+
+ self._playButton = QtGui.QPushButton("Play")
+ self._playButton.clicked.connect(self._on_voicemail_play)
+ self._pauseButton = QtGui.QPushButton("Pause")
+ self._pauseButton.clicked.connect(self._on_voicemail_pause)
+ self._pauseButton.hide()
+ self._resumeButton = QtGui.QPushButton("Resume")
+ self._resumeButton.clicked.connect(self._on_voicemail_resume)
+ self._resumeButton.hide()
+ self._stopButton = QtGui.QPushButton("Stop")
+ self._stopButton.clicked.connect(self._on_voicemail_stop)
+ self._stopButton.hide()
+
+ self._downloadButton = QtGui.QPushButton("Download Voicemail")
+ self._downloadButton.clicked.connect(self._on_voicemail_download)
+ self._downloadLayout = QtGui.QHBoxLayout()
+ self._downloadLayout.addWidget(self._downloadButton)
+ self._downloadWidget = QtGui.QWidget()
+ self._downloadWidget.setLayout(self._downloadLayout)
+
+ self._playLabel = QtGui.QLabel("Voicemail")
+ self._saveButton = QtGui.QPushButton("Save")
+ self._saveButton.clicked.connect(self._on_voicemail_save)
+ self._playerLayout = QtGui.QHBoxLayout()
+ self._playerLayout.addWidget(self._playLabel)
+ self._playerLayout.addWidget(self._playButton)
+ self._playerLayout.addWidget(self._pauseButton)
+ self._playerLayout.addWidget(self._resumeButton)
+ self._playerLayout.addWidget(self._stopButton)
+ self._playerLayout.addWidget(self._saveButton)
+ self._playerWidget = QtGui.QWidget()
+ self._playerWidget.setLayout(self._playerLayout)
+
+ self._visibleWidget = None
+ self._layout = QtGui.QHBoxLayout()
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+ self._update_state()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def destroy(self):
+ self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
+ self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
+ self._invalidate_token()
+
+ def _invalidate_token(self):
+ if self._token is not None:
+ self._token.invalidate()
+ self._token.error.disconnect(self._on_play_error)
+ self._token.stateChange.connect(self._on_play_state)
+ self._token.invalidated.connect(self._on_play_invalidated)
+
+ def _show_download(self, messageId):
+ if self._visibleWidget is self._downloadWidget:
+ return
+ self._hide()
+ self._layout.addWidget(self._downloadWidget)
+ self._visibleWidget = self._downloadWidget
+ self._visibleWidget.show()
+
+ def _show_player(self, messageId):
+ if self._visibleWidget is self._playerWidget:
+ return
+ self._hide()
+ self._layout.addWidget(self._playerWidget)
+ self._visibleWidget = self._playerWidget
+ self._visibleWidget.show()
+
+ def _hide(self):
+ if self._visibleWidget is None:
+ return
+ self._visibleWidget.hide()
+ self._layout.removeWidget(self._visibleWidget)
+ self._visibleWidget = None
+
+ def _update_play_state(self):
+ if self._token is not None and self._token.isValid:
+ self._playButton.setText("Stop")
+ else:
+ self._playButton.setText("Play")
+
+ def _update_state(self):
+ if self._session.draft.get_num_contacts() != 1:
+ self._hide()
+ return
+
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ if messageId is None:
+ self._hide()
+ return
+
+ if self._session.is_available(messageId):
+ self._show_player(messageId)
+ else:
+ self._show_download(messageId)
+ if self._token is not None:
+ self._token.invalidate()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_save(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
+ targetPath = unicode(targetPath)
+ if not targetPath:
+ return
+
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ sourcePath = self._session.voicemail_path(messageId)
+ import shutil
+ shutil.copy2(sourcePath, targetPath)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_error(self, error):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._app.errorLog.push_error(error)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_invalidated(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._playButton.show()
+ self._pauseButton.hide()
+ self._resumeButton.hide()
+ self._stopButton.hide()
+ self._invalidate_token()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_state(self, state):
+ with qui_utils.notify_error(self._app.errorLog):
+ if state == self._token.STATE_PLAY:
+ self._playButton.hide()
+ self._pauseButton.show()
+ self._resumeButton.hide()
+ self._stopButton.show()
+ elif state == self._token.STATE_PAUSE:
+ self._playButton.hide()
+ self._pauseButton.hide()
+ self._resumeButton.show()
+ self._stopButton.show()
+ elif state == self._token.STATE_STOP:
+ self._playButton.show()
+ self._pauseButton.hide()
+ self._resumeButton.hide()
+ self._stopButton.hide()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_play(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ sourcePath = self._session.voicemail_path(messageId)
+
+ self._invalidate_token()
+ uri = "file://%s" % sourcePath
+ self._token = self._app.streamHandler.set_file(uri)
+ self._token.stateChange.connect(self._on_play_state)
+ self._token.invalidated.connect(self._on_play_invalidated)
+ self._token.error.connect(self._on_play_error)
+ self._token.play()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_pause(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.pause()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_resume(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.play()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_stop(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.stop()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_download(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ self._session.download_voicemail(messageId)
+ self._hide()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_state()
+
+ @qt_compat.Slot(str, str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_downloaded(self, messageId, filepath):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_state()
+
+
+class SMSEntryWindow(qwrappers.WindowWrapper):
+
+ MAX_CHAR = 160
+ # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
+
+ def __init__(self, parent, app, session, errorLog):
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._session = session
+ self._session.messagesUpdated.connect(self._on_refresh_history)
+ self._session.historyUpdated.connect(self._on_refresh_history)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+
+ self._session.draft.sendingMessage.connect(self._on_op_started)
+ self._session.draft.calling.connect(self._on_op_started)
+ self._session.draft.calling.connect(self._on_calling_started)
+ self._session.draft.cancelling.connect(self._on_op_started)
+
+ self._session.draft.sentMessage.connect(self._on_op_finished)
+ self._session.draft.called.connect(self._on_op_finished)
+ self._session.draft.cancelled.connect(self._on_op_finished)
+ self._session.draft.error.connect(self._on_op_error)
+
+ self._errorLog = errorLog
+
+ self._targetList = ContactList(self._app, self._session)
+ self._history = QtGui.QLabel()
+ self._history.setTextFormat(QtCore.Qt.RichText)
+ self._history.setWordWrap(True)
+ self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
+ self._smsEntry = QtGui.QTextEdit()
+ self._smsEntry.textChanged.connect(self._on_letter_count_changed)
+
+ self._entryLayout = QtGui.QVBoxLayout()
+ self._entryLayout.addWidget(self._targetList.toplevel)
+ self._entryLayout.addWidget(self._history)
+ self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
+ self._entryLayout.addWidget(self._smsEntry)
+ self._entryLayout.setContentsMargins(0, 0, 0, 0)
+ self._entryWidget = QtGui.QWidget()
+ self._entryWidget.setLayout(self._entryLayout)
+ self._entryWidget.setContentsMargins(0, 0, 0, 0)
+ self._scrollEntry = QtGui.QScrollArea()
+ self._scrollEntry.setWidget(self._entryWidget)
+ self._scrollEntry.setWidgetResizable(True)
+ self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
+ self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+ self._characterCountLabel = QtGui.QLabel("")
+ self._singleNumberSelector = QtGui.QComboBox()
+ self._cids = []
+ self._singleNumberSelector.activated.connect(self._on_single_change_number)
+ self._smsButton = QtGui.QPushButton("SMS")
+ self._smsButton.clicked.connect(self._on_sms_clicked)
+ self._smsButton.setEnabled(False)
+ self._dialButton = QtGui.QPushButton("Dial")
+ self._dialButton.clicked.connect(self._on_call_clicked)
+ self._cancelButton = QtGui.QPushButton("Cancel Call")
+ self._cancelButton.clicked.connect(self._on_cancel_clicked)
+ self._cancelButton.setVisible(False)
+
+ self._buttonLayout = QtGui.QHBoxLayout()
+ self._buttonLayout.addWidget(self._characterCountLabel)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._singleNumberSelector)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._smsButton)
+ self._buttonLayout.addWidget(self._dialButton)
+ self._buttonLayout.addWidget(self._cancelButton)
+
+ self._layout.addWidget(self._errorDisplay.toplevel)
+ self._layout.addWidget(self._scrollEntry)
+ self._layout.addLayout(self._buttonLayout)
+ self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
+
+ self._window.setWindowTitle("Contact")
+ self._window.closed.connect(self._on_close_window)
+ self._window.hidden.connect(self._on_close_window)
+ self._window.resized.connect(self._on_window_resized)
+
+ self._scrollTimer = QtCore.QTimer()
+ self._scrollTimer.setInterval(100)
+ self._scrollTimer.setSingleShot(True)
+ self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
+
+ self._smsEntry.setPlainText(self._session.draft.message)
+ self._update_letter_count()
+ self._update_target_fields()
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+ self.update_orientation(self._app.orientation)
+
+ def close(self):
+ if self._window is None:
+ # Already closed
+ return
+ window = self._window
+ try:
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self.hide()
+ except AttributeError:
+ _moduleLogger.exception("Oh well")
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def destroy(self):
+ self._session.messagesUpdated.disconnect(self._on_refresh_history)
+ self._session.historyUpdated.disconnect(self._on_refresh_history)
+ self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
+ self._session.draft.sendingMessage.disconnect(self._on_op_started)
+ self._session.draft.calling.disconnect(self._on_op_started)
+ self._session.draft.calling.disconnect(self._on_calling_started)
+ self._session.draft.cancelling.disconnect(self._on_op_started)
+ self._session.draft.sentMessage.disconnect(self._on_op_finished)
+ self._session.draft.called.disconnect(self._on_op_finished)
+ self._session.draft.cancelled.disconnect(self._on_op_finished)
+ self._session.draft.error.disconnect(self._on_op_error)
+ self._voicemailPlayer.destroy()
+ window = self._window
+ self._window = None
+ try:
+ window.close()
+ window.destroy()
+ except AttributeError:
+ _moduleLogger.exception("Oh well")
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def update_orientation(self, orientation):
+ qwrappers.WindowWrapper.update_orientation(self, orientation)
+ self._scroll_to_bottom()
+
+ def _update_letter_count(self):
+ count = len(self._smsEntry.toPlainText())
+ numTexts, numCharInText = divmod(count, self.MAX_CHAR)
+ numTexts += 1
+ numCharsLeftInText = self.MAX_CHAR - numCharInText
+ self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
+
+ def _update_button_state(self):
+ self._cancelButton.setEnabled(True)
+ if self._session.draft.get_num_contacts() == 0:
+ self._dialButton.setEnabled(False)
+ self._smsButton.setEnabled(False)
+ elif self._session.draft.get_num_contacts() == 1:
+ count = len(self._smsEntry.toPlainText())
+ if count == 0:
+ self._dialButton.setEnabled(True)
+ self._smsButton.setEnabled(False)
+ else:
+ self._dialButton.setEnabled(False)
+ self._smsButton.setEnabled(True)
+ else:
+ self._dialButton.setEnabled(False)
+ count = len(self._smsEntry.toPlainText())
+ if count == 0:
+ self._smsButton.setEnabled(False)
+ else:
+ self._smsButton.setEnabled(True)
+
+ def _update_history(self, cid):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount != 1:
+ self._history.setVisible(False)
+ else:
+ description = self._session.draft.get_description(cid)
+
+ self._targetList.setVisible(False)
+ if description:
+ self._history.setText(description)
+ self._history.setVisible(True)
+ else:
+ self._history.setText("")
+ self._history.setVisible(False)
+
+ def _update_target_fields(self):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount == 0:
+ self.hide()
+ del self._cids[:]
+ elif draftContactsCount == 1:
+ (cid, ) = self._session.draft.get_contacts()
+ title = self._session.draft.get_title(cid)
+ numbers = self._session.draft.get_numbers(cid)
+
+ self._targetList.setVisible(False)
+ self._update_history(cid)
+ self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
+ self._cids = [cid]
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle(title)
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+ self.show()
+ self._window.raise_()
+ else:
+ self._targetList.setVisible(True)
+ self._targetList.update()
+ self._history.setText("")
+ self._history.setVisible(False)
+ self._singleNumberSelector.setVisible(False)
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle("Contacts")
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+ self.show()
+ self._window.raise_()
+
+ def _populate_number_selector(self, selector, cid, cidIndex, numbers):
+ selector.clear()
+
+ selectedNumber = self._session.draft.get_selected_number(cid)
+ if len(numbers) == 1:
+ # If no alt numbers available, check the address book
+ numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
+ else:
+ defaultIndex = _index_number(numbers, selectedNumber)
+
+ for number, description in numbers:
+ if description:
+ label = "%s - %s" % (number, description)
+ else:
+ label = number
+ selector.addItem(label)
+ selector.setVisible(True)
+ if 1 < len(numbers):
+ selector.setEnabled(True)
+ selector.setCurrentIndex(defaultIndex)
+ else:
+ selector.setEnabled(False)
+
+ def _scroll_to_bottom(self):
+ self._scrollTimer.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_scroll_to_bottom(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._scrollEntry.ensureWidgetVisible(self._smsEntry)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self._session.draft.send()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self._session.draft.call()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_cancel_clicked(self, message):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._session.draft.cancel()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_single_change_number(self, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ # Exception thrown when the first item is removed
+ cid = self._cids[0]
+ try:
+ numbers = self._session.draft.get_numbers(cid)
+ except KeyError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ number = numbers[index][0]
+ self._session.draft.set_selected_number(cid, number)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_history(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount != 1:
+ # Changing contact count will automatically refresh it
+ return
+ (cid, ) = self._session.draft.get_contacts()
+ self._update_history(cid)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_target_fields()
+ self._update_button_state()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_started(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setReadOnly(True)
+ self._smsButton.setVisible(False)
+ self._dialButton.setVisible(False)
+ self.show()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_calling_started(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._cancelButton.setVisible(True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_finished(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setPlainText("")
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+ self.close()
+ self.destroy()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_error(self, message):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+
+ self._errorLog.push_error(message)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_letter_count_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_letter_count()
+ self._update_button_state()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_window_resized(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._scroll_to_bottom()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.close()
+
+
+def _index_number(numbers, default):
+ uglyDefault = misc_utils.make_ugly(default)
+ uglyContactNumbers = list(
+ misc_utils.make_ugly(contactNumber)
+ for (contactNumber, _) in numbers
+ )
+ defaultMatches = [
+ misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
+ for contactNumber in uglyContactNumbers
+ ]
+ try:
+ defaultIndex = defaultMatches.index(True)
+ except ValueError:
+ defaultIndex = -1
+ _moduleLogger.warn(
+ "Could not find contact number %s among %r" % (
+ default, numbers
+ )
+ )
+ return defaultIndex
+
+
+def _get_contact_numbers(session, contactId, number, description):
+ contactPhoneNumbers = []
+ if contactId and contactId != "0":
+ try:
+ contactDetails = copy.deepcopy(session.get_contacts()[contactId])
+ contactPhoneNumbers = contactDetails["numbers"]
+ except KeyError:
+ contactPhoneNumbers = []
+ contactPhoneNumbers = [
+ (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
+ for contactPhoneNumber in contactPhoneNumbers
+ ]
+ defaultIndex = _index_number(contactPhoneNumbers, number)
+
+ if not contactPhoneNumbers or defaultIndex == -1:
+ contactPhoneNumbers += [(number, description)]
+ defaultIndex = 0
+
+ return contactPhoneNumbers, defaultIndex
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import datetime
+import ConfigParser
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+ with open(constants._notifier_logpath_, "a") as file:
+ file.write("Notification: %r\n" % (datetime.datetime.now(), ))
+
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = alarm_notify.create_backend(config)
+ notifyUser = alarm_notify.is_changed(config, backend)
+
+ if notifyUser:
+ file.write("\tChange occurred\n")
+
+
+if __name__ == "__main__":
+ notify_on_change()
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import sys
+import ConfigParser
+import logging
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = alarm_notify.create_backend(config)
+ notifyUser = alarm_notify.is_changed(config, backend)
+
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._custom_notifier_settings_)
+ soundFile = config.get("Sound Notifier", "soundfile")
+ soundFile = "/usr/lib/gv-notifier/alert.mp3"
+
+ if notifyUser:
+ import subprocess
+ import led_handler
+ logging.info("Changed, playing %s" % soundFile)
+ led = led_handler.LedHandler()
+ led.on()
+ soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True)
+ else:
+ logging.info("No Change")
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
+ logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__))
+ logging.info("OS: %s" % (os.uname()[0], ))
+ logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ logging.info("Hostname: %s" % os.uname()[1])
+ try:
+ notify_on_change()
+ except:
+ logging.exception("Error")
+ raise
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import datetime
+import string
+import itertools
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+from util import qtpie
+from util import qui_utils
+from util import misc as misc_utils
+
+import backends.null_backend as null_backend
+import backends.file_backend as file_backend
+import backends.qt_backend as qt_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_SENTINEL_ICON = QtGui.QIcon()
+
+
+class Dialpad(object):
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._errorLog = errorLog
+
+ self._plus = QtGui.QPushButton("+")
+ self._plus.clicked.connect(lambda: self._on_keypress("+"))
+ self._entry = QtGui.QLineEdit()
+
+ backAction = QtGui.QAction(None)
+ backAction.setText("Back")
+ backAction.triggered.connect(self._on_backspace)
+ backPieItem = qtpie.QActionPieItem(backAction)
+ clearAction = QtGui.QAction(None)
+ clearAction.setText("Clear")
+ clearAction.triggered.connect(self._on_clear_text)
+ clearPieItem = qtpie.QActionPieItem(clearAction)
+ backSlices = [
+ qtpie.PieFiling.NULL_CENTER,
+ clearPieItem,
+ qtpie.PieFiling.NULL_CENTER,
+ qtpie.PieFiling.NULL_CENTER,
+ ]
+ self._back = qtpie.QPieButton(backPieItem)
+ self._back.set_center(backPieItem)
+ for slice in backSlices:
+ self._back.insertItem(slice)
+
+ self._entryLayout = QtGui.QHBoxLayout()
+ self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
+ self._entryLayout.addWidget(self._entry, 1000)
+ self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
+
+ smsIcon = self._app.get_icon("messages.png")
+ self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
+ self._smsButton.clicked.connect(self._on_sms_clicked)
+ self._smsButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ callIcon = self._app.get_icon("dialpad.png")
+ self._callButton = QtGui.QPushButton(callIcon, "Call")
+ self._callButton.clicked.connect(self._on_call_clicked)
+ self._callButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+
+ self._padLayout = QtGui.QGridLayout()
+ rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
+ columns = [0, 1, 2] * 3
+ keys = [
+ ("1", ""),
+ ("2", "ABC"),
+ ("3", "DEF"),
+ ("4", "GHI"),
+ ("5", "JKL"),
+ ("6", "MNO"),
+ ("7", "PQRS"),
+ ("8", "TUV"),
+ ("9", "WXYZ"),
+ ]
+ for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
+ self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
+ self._zerothButton = QtGui.QPushButton("0")
+ self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
+ self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._padLayout.addWidget(self._smsButton, 3, 0)
+ self._padLayout.addWidget(self._zerothButton)
+ self._padLayout.addWidget(self._callButton, 3, 2)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._entryLayout, 0)
+ self._layout.addLayout(self._padLayout, 1000000)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._smsButton.setEnabled(True)
+ self._callButton.setEnabled(True)
+
+ def disable(self):
+ self._smsButton.setEnabled(False)
+ self._callButton.setEnabled(False)
+
+ def get_settings(self):
+ return {}
+
+ def set_settings(self, settings):
+ pass
+
+ def clear(self):
+ pass
+
+ def refresh(self, force = True):
+ pass
+
+ def _generate_key_button(self, center, letters):
+ button = QtGui.QPushButton("%s\n%s" % (center, letters))
+ button.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ button.clicked.connect(lambda: self._on_keypress(center))
+ return button
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_keypress(self, key):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.insert(key)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_backspace(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.backspace()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_clear_text(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.clear()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.clear()
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+ self._session.draft.call()
+
+
+class TimeCategories(object):
+
+ _NOW_SECTION = 0
+ _TODAY_SECTION = 1
+ _WEEK_SECTION = 2
+ _MONTH_SECTION = 3
+ _REST_SECTION = 4
+ _MAX_SECTIONS = 5
+
+ _NO_ELAPSED = datetime.timedelta(hours=1)
+ _WEEK_ELAPSED = datetime.timedelta(weeks=1)
+ _MONTH_ELAPSED = datetime.timedelta(days=30)
+
+ def __init__(self, parentItem):
+ self._timeItems = [
+ QtGui.QStandardItem(description)
+ for (i, description) in zip(
+ xrange(self._MAX_SECTIONS),
+ ["Now", "Today", "Week", "Month", "Past"],
+ )
+ ]
+ for item in self._timeItems:
+ item.setEditable(False)
+ item.setCheckable(False)
+ row = (item, )
+ parentItem.appendRow(row)
+
+ self._today = datetime.datetime(1900, 1, 1)
+
+ self.prepare_for_update(self._today)
+
+ def prepare_for_update(self, newToday):
+ self._today = newToday
+ for item in self._timeItems:
+ item.removeRows(0, item.rowCount())
+ try:
+ hour = self._today.strftime("%X")
+ day = self._today.strftime("%x")
+ except ValueError:
+ _moduleLogger.exception("Can't format times")
+ hour = "Now"
+ day = "Today"
+ self._timeItems[self._NOW_SECTION].setText(hour)
+ self._timeItems[self._TODAY_SECTION].setText(day)
+
+ def add_row(self, rowDate, row):
+ elapsedTime = self._today - rowDate
+ todayTuple = self._today.timetuple()
+ rowTuple = rowDate.timetuple()
+ if elapsedTime < self._NO_ELAPSED:
+ section = self._NOW_SECTION
+ elif todayTuple[0:3] == rowTuple[0:3]:
+ section = self._TODAY_SECTION
+ elif elapsedTime < self._WEEK_ELAPSED:
+ section = self._WEEK_SECTION
+ elif elapsedTime < self._MONTH_ELAPSED:
+ section = self._MONTH_SECTION
+ else:
+ section = self._REST_SECTION
+ self._timeItems[section].appendRow(row)
+
+ def get_item(self, timeIndex, rowIndex, column):
+ timeItem = self._timeItems[timeIndex]
+ item = timeItem.child(rowIndex, column)
+ return item
+
+
+class History(object):
+
+ DETAILS_IDX = 0
+ FROM_IDX = 1
+ MAX_IDX = 2
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
+ HISTORY_COLUMNS = ["", "From"]
+ assert len(HISTORY_COLUMNS) == MAX_IDX
+
+ def __init__(self, app, session, errorLog):
+ self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
+ self._app = app
+ self._session = session
+ self._session.historyUpdated.connect(self._on_history_updated)
+ self._errorLog = errorLog
+
+ self._typeSelection = QtGui.QComboBox()
+ self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
+ self._typeSelection.setCurrentIndex(
+ self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
+ )
+ self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._managerLayout = QtGui.QHBoxLayout()
+ self._managerLayout.addWidget(self._typeSelection, 1000)
+ self._managerLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
+ self._categoryManager = TimeCategories(self._itemStore)
+
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(True)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setIndentation(0)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
+ self._itemView.activated.connect(self._on_row_activated)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._managerLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self._actionIcon = {
+ "Placed": self._app.get_icon("placed.png"),
+ "Missed": self._app.get_icon("missed.png"),
+ "Received": self._app.get_icon("received.png"),
+ }
+
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "filter": self._selectedFilter,
+ }
+
+ def set_settings(self, settings):
+ selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
+ if selectedFilter in self.HISTORY_ITEM_TYPES:
+ self._selectedFilter = selectedFilter
+ self._typeSelection.setCurrentIndex(
+ self.HISTORY_ITEM_TYPES.index(selectedFilter)
+ )
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+
+ if self._selectedFilter == self.HISTORY_RECEIVED:
+ self._session.update_history(self._session.HISTORY_RECEIVED, force)
+ elif self._selectedFilter == self.HISTORY_MISSED:
+ self._session.update_history(self._session.HISTORY_MISSED, force)
+ elif self._selectedFilter == self.HISTORY_PLACED:
+ self._session.update_history(self._session.HISTORY_PLACED, force)
+ elif self._selectedFilter == self.HISTORY_ALL:
+ self._session.update_history(self._session.HISTORY_ALL, force)
+ else:
+ assert False, "How did we get here?"
+
+ if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
+ self._app.ledHandler.off()
+
+ def _populate_items(self):
+ self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
+
+ history = self._session.get_history()
+ history.sort(key=lambda item: item["time"], reverse=True)
+ for event in history:
+ if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
+ continue
+
+ relTime = event["relTime"]
+ action = event["action"]
+ number = event["number"]
+ prettyNumber = misc_utils.make_pretty(number)
+ if prettyNumber.startswith("+1 "):
+ prettyNumber = prettyNumber[len("+1 "):]
+ name = event["name"]
+ if not name or name == number:
+ name = event["location"]
+ if not name:
+ name = "Unknown"
+
+ detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime))
+ detailsFont = detailsItem.font()
+ detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 5))
+ detailsItem.setFont(detailsFont)
+ nameItem = QtGui.QStandardItem(name)
+ nameFont = nameItem.font()
+ nameFont.setPointSize(nameFont.pointSize() + 4)
+ nameItem.setFont(nameFont)
+ row = detailsItem, nameItem
+ for item in row:
+ item.setEditable(False)
+ item.setCheckable(False)
+ row[self.DETAILS_IDX].setData(event)
+ self._categoryManager.add_row(event["time"], row)
+ self._itemView.expandAll()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_history_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ if not timeIndex.isValid():
+ return
+ timeRow = timeIndex.row()
+ row = index.row()
+ detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
+ fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
+ contactDetails = detailsItem.data()
+
+ title = unicode(fromItem.text())
+ number = str(contactDetails["number"])
+ contactId = number # ids don't seem too unique so using numbers
+
+ descriptionRows = []
+ for t in xrange(self._itemStore.rowCount()):
+ randomTimeItem = self._itemStore.item(t, 0)
+ for i in xrange(randomTimeItem.rowCount()):
+ iItem = randomTimeItem.child(i, 0)
+ iContactDetails = iItem.data()
+ iNumber = str(iContactDetails["number"])
+ if number != iNumber:
+ continue
+ relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
+ action = str(iContactDetails["action"])
+ number = str(iContactDetails["number"])
+ prettyNumber = misc_utils.make_pretty(number)
+ rowItems = relTime, action, prettyNumber
+ descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
+ description = "<table>%s</table>" % "".join(descriptionRows)
+ numbersWithDescriptions = [(str(contactDetails["number"]), "")]
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+
+class Messages(object):
+
+ NO_MESSAGES = "None"
+ VOICEMAIL_MESSAGES = "Voicemail"
+ TEXT_MESSAGES = "SMS"
+ ALL_TYPES = "All Messages"
+ MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
+
+ UNREAD_STATUS = "Unread"
+ UNARCHIVED_STATUS = "Inbox"
+ ALL_STATUS = "Any"
+ MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
+
+ _MIN_MESSAGES_SHOWN = 1
+
+ def __init__(self, app, session, errorLog):
+ self._selectedTypeFilter = self.ALL_TYPES
+ self._selectedStatusFilter = self.ALL_STATUS
+ self._app = app
+ self._session = session
+ self._session.messagesUpdated.connect(self._on_messages_updated)
+ self._errorLog = errorLog
+
+ self._typeSelection = QtGui.QComboBox()
+ self._typeSelection.addItems(self.MESSAGE_TYPES)
+ self._typeSelection.setCurrentIndex(
+ self.MESSAGE_TYPES.index(self._selectedTypeFilter)
+ )
+ self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
+
+ self._statusSelection = QtGui.QComboBox()
+ self._statusSelection.addItems(self.MESSAGE_STATUSES)
+ self._statusSelection.setCurrentIndex(
+ self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
+ )
+ self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
+
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+
+ self._selectionLayout = QtGui.QHBoxLayout()
+ self._selectionLayout.addWidget(self._typeSelection, 1000)
+ self._selectionLayout.addWidget(self._statusSelection, 1000)
+ self._selectionLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(["Messages"])
+ self._categoryManager = TimeCategories(self._itemStore)
+
+ self._htmlDelegate = qui_utils.QHtmlDelegate()
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(False)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.setItemDelegate(self._htmlDelegate)
+ self._itemView.activated.connect(self._on_row_activated)
+ self._itemView.header().sectionResized.connect(self._on_column_resized)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._selectionLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "type": self._selectedTypeFilter,
+ "status": self._selectedStatusFilter,
+ }
+
+ def set_settings(self, settings):
+ selectedType = settings.get("type", self.ALL_TYPES)
+ if selectedType in self.MESSAGE_TYPES:
+ self._selectedTypeFilter = selectedType
+ self._typeSelection.setCurrentIndex(
+ self.MESSAGE_TYPES.index(self._selectedTypeFilter)
+ )
+
+ selectedStatus = settings.get("status", self.ALL_STATUS)
+ if selectedStatus in self.MESSAGE_STATUSES:
+ self._selectedStatusFilter = selectedStatus
+ self._statusSelection.setCurrentIndex(
+ self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
+ )
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+
+ if self._selectedTypeFilter == self.NO_MESSAGES:
+ pass
+ elif self._selectedTypeFilter == self.TEXT_MESSAGES:
+ self._session.update_messages(self._session.MESSAGE_TEXTS, force)
+ elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES:
+ self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force)
+ elif self._selectedTypeFilter == self.ALL_TYPES:
+ self._session.update_messages(self._session.MESSAGE_ALL, force)
+ else:
+ assert False, "How did we get here?"
+
+ if (self._app.notifyOnSms or self._app.notifyOnVoicemail) and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
+ self._app.ledHandler.off()
+
+ def _populate_items(self):
+ self._categoryManager.prepare_for_update(self._session.get_when_messages_updated())
+
+ rawMessages = self._session.get_messages()
+ rawMessages.sort(key=lambda item: item["time"], reverse=True)
+ for item in rawMessages:
+ isUnarchived = not item["isArchived"]
+ isUnread = not item["isRead"]
+ visibleStatus = {
+ self.UNREAD_STATUS: isUnarchived and isUnread,
+ self.UNARCHIVED_STATUS: isUnarchived,
+ self.ALL_STATUS: True,
+ }[self._selectedStatusFilter]
+ visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
+ if not (visibleType and visibleStatus):
+ continue
+
+ relTime = misc_utils.abbrev_relative_date(item["relTime"])
+ number = item["number"]
+ prettyNumber = misc_utils.make_pretty(number)
+ name = item["name"]
+ if not name or name == number:
+ name = item["location"]
+ if not name:
+ name = "Unknown"
+
+ messageParts = list(item["messageParts"])
+ if len(messageParts) == 0:
+ messages = ("No Transcription", )
+ elif len(messageParts) == 1:
+ if messageParts[0][1]:
+ messages = (messageParts[0][1], )
+ else:
+ messages = ("No Transcription", )
+ else:
+ messages = [
+ "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+ for messagePart in messageParts
+ ]
+
+ firstMessage = "<b>%s<br/>%s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
+
+ expandedMessages = [firstMessage]
+ expandedMessages.extend(messages)
+ if self._MIN_MESSAGES_SHOWN < 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:
+ 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()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_type_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedTypeFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_status_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedStatusFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_messages_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ if not timeIndex.isValid():
+ return
+ timeRow = timeIndex.row()
+ row = index.row()
+ item = self._categoryManager.get_item(timeRow, row, 0)
+ contactDetails = item.data()
+
+ name = unicode(contactDetails["name"])
+ number = str(contactDetails["number"])
+ if not name or name == number:
+ name = unicode(contactDetails["location"])
+ if not name:
+ name = "Unknown"
+
+ if str(contactDetails["type"]) == "Voicemail":
+ messageId = str(contactDetails["id"])
+ else:
+ messageId = None
+ contactId = str(contactDetails["contactId"])
+ title = name
+ description = unicode(contactDetails["expandedMessages"])
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions)
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_column_resized(self, index, oldSize, newSize):
+ self._htmlDelegate.setWidth(newSize, self._itemStore)
+
+
+class Contacts(object):
+
+ # @todo Provide some sort of letter jump
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._session.accountUpdated.connect(self._on_contacts_updated)
+ self._errorLog = errorLog
+ self._addressBookFactories = [
+ null_backend.NullAddressBookFactory(),
+ file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
+ qt_backend.QtContactsAddressBookFactory(),
+ ]
+ self._addressBooks = []
+
+ self._listSelection = QtGui.QComboBox()
+ self._listSelection.addItems([])
+ self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+ self._activeList = "None"
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._managerLayout = QtGui.QHBoxLayout()
+ self._managerLayout.addWidget(self._listSelection, 1000)
+ self._managerLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(["Contacts"])
+ self._alphaItem = {}
+
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(True)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.activated.connect(self._on_row_activated)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._managerLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self.update_addressbooks()
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "selectedAddressbook": self._activeList,
+ }
+
+ def set_settings(self, settings):
+ currentItem = settings.get("selectedAddressbook", "None")
+ bookNames = [book["name"] for book in self._addressBooks]
+ try:
+ newIndex = bookNames.index(currentItem)
+ except ValueError:
+ # Switch over to None for the user
+ newIndex = 0
+ self._listSelection.setCurrentIndex(newIndex)
+ self._activeList = currentItem
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+ self._backend.update_account(force)
+
+ @property
+ def _backend(self):
+ return self._addressBooks[self._listSelection.currentIndex()]["book"]
+
+ def update_addressbooks(self):
+ self._addressBooks = [
+ {"book": book, "name": book.name}
+ for factory in self._addressBookFactories
+ for book in factory.get_addressbooks()
+ ]
+ self._addressBooks.append(
+ {
+ "book": self._session,
+ "name": "Google Voice",
+ }
+ )
+
+ currentItem = str(self._listSelection.currentText())
+ self._activeList = currentItem
+ if currentItem == "":
+ # Not loaded yet
+ currentItem = "None"
+ self._listSelection.clear()
+ bookNames = [book["name"] for book in self._addressBooks]
+ try:
+ newIndex = bookNames.index(currentItem)
+ except ValueError:
+ # Switch over to None for the user
+ newIndex = 0
+ self._itemStore.clear()
+ _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
+ self._listSelection.addItems(bookNames)
+ self._listSelection.setCurrentIndex(newIndex)
+
+ def _populate_items(self):
+ self._itemStore.clear()
+ self._alphaItem = dict(
+ (letter, QtGui.QStandardItem(letter))
+ for letter in self._prefixes()
+ )
+ for letter in self._prefixes():
+ item = self._alphaItem[letter]
+ item.setEditable(False)
+ item.setCheckable(False)
+ row = (item, )
+ self._itemStore.appendRow(row)
+
+ for item in self._get_contacts():
+ name = item["name"]
+ if not name:
+ name = "Unknown"
+ numbers = item["numbers"]
+
+ nameItem = QtGui.QStandardItem(name)
+ nameItem.setEditable(False)
+ nameItem.setCheckable(False)
+ nameItem.setData(item)
+ nameItemFont = nameItem.font()
+ nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5))
+ nameItem.setFont(nameItemFont)
+
+ row = (nameItem, )
+ rowKey = name[0].upper()
+ rowKey = rowKey if rowKey in self._alphaItem else "#"
+ self._alphaItem[rowKey].appendRow(row)
+ self._itemView.expandAll()
+
+ def _prefixes(self):
+ return itertools.chain(string.ascii_uppercase, ("#", ))
+
+ def _jump_to_prefix(self, letter):
+ i = list(self._prefixes()).index(letter)
+ rootIndex = self._itemView.rootIndex()
+ currentIndex = self._itemView.model().index(i, 0, rootIndex)
+ self._itemView.scrollTo(currentIndex)
+ self._itemView.setItemSelected(self._itemView.topLevelItem(i), True)
+
+ def _get_contacts(self):
+ contacts = list(self._backend.get_contacts().itervalues())
+ contacts.sort(key=lambda contact: contact["name"].lower())
+ return contacts
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._activeList = str(newItem)
+ self.refresh(force=False)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_contacts_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ letterIndex = index.parent()
+ if not letterIndex.isValid():
+ return
+ letterRow = letterIndex.row()
+ letter = list(self._prefixes())[letterRow]
+ letterItem = self._alphaItem[letter]
+ rowIndex = index.row()
+ item = letterItem.child(rowIndex, 0)
+ contactDetails = item.data()
+
+ name = unicode(contactDetails["name"])
+ if not name:
+ name = unicode(contactDetails["location"])
+ if not name:
+ name = "Unknown"
+
+ contactId = str(contactDetails["contactId"])
+ numbers = contactDetails["numbers"]
+ numbers = [
+ dict(
+ (str(k), str(v))
+ for (k, v) in number.iteritems()
+ )
+ for number in numbers
+ ]
+ numbersWithDescriptions = [
+ (
+ number["phoneNumber"],
+ self._choose_phonetype(number),
+ )
+ for number in numbers
+ ]
+ title = name
+ description = name
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+ @staticmethod
+ def _choose_phonetype(numberDetails):
+ if "phoneTypeName" in numberDetails:
+ return numberDetails["phoneTypeName"]
+ elif "phoneType" in numberDetails:
+ return numberDetails["phoneType"]
+ else:
+ return ""
--- /dev/null
+#!/usr/bin/env python
+
+import dbus
+
+
+class _NokiaLedHandler(object):
+
+ def __init__(self):
+ self._bus = dbus.SystemBus()
+ self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
+ self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
+
+ self._ledPattern = "PatternCommunicationChat"
+
+ def on(self):
+ self._mceRequest.req_led_pattern_activate(self._ledPattern)
+
+ def off(self):
+ self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
+
+
+class _NoLedHandler(object):
+
+ def __init__(self):
+ pass
+
+ def on(self):
+ pass
+
+ def off(self):
+ pass
+
+
+class LedHandler(object):
+
+ def __init__(self):
+ self._actual = None
+ self._isReal = False
+
+ def on(self):
+ self._lazy_init()
+ self._actual.on()
+
+ def off(self):
+ self._lazy_init()
+ self._actual.off()
+
+ @property
+ def isReal(self):
+ self._lazy_init()
+ self._isReal
+
+ def _lazy_init(self):
+ if self._actual is not None:
+ return
+ try:
+ self._actual = _NokiaLedHandler()
+ self._isReal = True
+ except dbus.DBusException:
+ self._actual = _NoLedHandler()
+ self._isReal = False
+
+
+if __name__ == "__main__":
+ leds = _NokiaLedHandler()
+ leds.off()
--- /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
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+from util import qore_utils
+from util import qui_utils
+from util import concurrent
+from util import misc as misc_utils
+
+import constants
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _DraftContact(object):
+
+ def __init__(self, messageId, title, description, numbersWithDescriptions):
+ self.messageId = messageId
+ self.title = title
+ self.description = description
+ self.numbers = numbersWithDescriptions
+ self.selectedNumber = numbersWithDescriptions[0][0]
+
+
+class Draft(QtCore.QObject):
+
+ sendingMessage = qt_compat.Signal()
+ sentMessage = qt_compat.Signal()
+ calling = qt_compat.Signal()
+ called = qt_compat.Signal()
+ cancelling = qt_compat.Signal()
+ cancelled = qt_compat.Signal()
+ error = qt_compat.Signal(str)
+
+ recipientsChanged = qt_compat.Signal()
+
+ def __init__(self, asyncQueue, backend, errorLog):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._contacts = {}
+ self._asyncQueue = asyncQueue
+ self._backend = backend
+ self._busyReason = None
+ self._message = ""
+
+ def send(self):
+ assert 0 < len(self._contacts), "No contacts selected"
+ assert 0 < len(self._message), "No message to send"
+ numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
+ le = self._asyncQueue.add_async(self._send)
+ le.start(numbers, self._message)
+
+ def call(self):
+ assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
+ assert len(self._message) == 0, "Cannot send message with call"
+ (contact, ) = self._contacts.itervalues()
+ number = misc_utils.make_ugly(contact.selectedNumber)
+ le = self._asyncQueue.add_async(self._call)
+ le.start(number)
+
+ def cancel(self):
+ le = self._asyncQueue.add_async(self._cancel)
+ le.start()
+
+ def _get_message(self):
+ return self._message
+
+ def _set_message(self, message):
+ self._message = message
+
+ message = property(_get_message, _set_message)
+
+ def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
+ contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
+ self._contacts[contactId] = contactDetails
+ self.recipientsChanged.emit()
+
+ def remove_contact(self, contactId):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ assert contactId in self._contacts, "Contact missing"
+ del self._contacts[contactId]
+ self.recipientsChanged.emit()
+
+ def get_contacts(self):
+ return self._contacts.iterkeys()
+
+ def get_num_contacts(self):
+ return len(self._contacts)
+
+ def get_message_id(self, cid):
+ return self._contacts[cid].messageId
+
+ def get_title(self, cid):
+ return self._contacts[cid].title
+
+ def get_description(self, cid):
+ return self._contacts[cid].description
+
+ def get_numbers(self, cid):
+ return self._contacts[cid].numbers
+
+ def get_selected_number(self, cid):
+ return self._contacts[cid].selectedNumber
+
+ def set_selected_number(self, cid, number):
+ # @note I'm lazy, this isn't firing any kind of signal since only one
+ # controller right now and that is the viewer
+ assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
+ self._contacts[cid].selectedNumber = number
+
+ def clear(self):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ self._clear()
+
+ def _clear(self):
+ oldContacts = self._contacts
+ self._contacts = {}
+ self._message = ""
+ if oldContacts:
+ self.recipientsChanged.emit()
+
+ @contextlib.contextmanager
+ def _busy(self, message):
+ if self._busyReason is not None:
+ raise RuntimeError("Already busy doing %r" % self._busyReason)
+ try:
+ self._busyReason = message
+ yield
+ finally:
+ self._busyReason = None
+
+ def _send(self, numbers, text):
+ self.sendingMessage.emit()
+ try:
+ with self._busy("Sending Text"):
+ with qui_utils.notify_busy(self._errorLog, "Sending Text"):
+ yield (
+ self._backend[0].send_sms,
+ (numbers, text),
+ {},
+ )
+ self.sentMessage.emit()
+ self._clear()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+ def _call(self, number):
+ self.calling.emit()
+ try:
+ with self._busy("Calling"):
+ with qui_utils.notify_busy(self._errorLog, "Calling"):
+ yield (
+ self._backend[0].call,
+ (number, ),
+ {},
+ )
+ self.called.emit()
+ self._clear()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+ def _cancel(self):
+ self.cancelling.emit()
+ try:
+ with qui_utils.notify_busy(self._errorLog, "Cancelling"):
+ yield (
+ self._backend[0].cancel,
+ (),
+ {},
+ )
+ self.cancelled.emit()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+
+class Session(QtCore.QObject):
+
+ # @todo Somehow add support for csv contacts
+ # @BUG When loading without caches, downloads messages twice
+
+ stateChange = qt_compat.Signal(str)
+ loggedOut = qt_compat.Signal()
+ loggedIn = qt_compat.Signal()
+ callbackNumberChanged = qt_compat.Signal(str)
+
+ accountUpdated = qt_compat.Signal()
+ messagesUpdated = qt_compat.Signal()
+ newMessages = qt_compat.Signal()
+ historyUpdated = qt_compat.Signal()
+ dndStateChange = qt_compat.Signal(bool)
+ voicemailAvailable = qt_compat.Signal(str, str)
+
+ error = qt_compat.Signal(str)
+
+ LOGGEDOUT_STATE = "logged out"
+ LOGGINGIN_STATE = "logging in"
+ LOGGEDIN_STATE = "logged in"
+
+ MESSAGE_TEXTS = "Text"
+ MESSAGE_VOICEMAILS = "Voicemail"
+ MESSAGE_ALL = "All"
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
+
+ _LOGGEDOUT_TIME = -1
+ _LOGGINGIN_TIME = 0
+
+ def __init__(self, errorLog, cachePath):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._pool = qore_utils.FutureThread()
+ self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
+ self._backend = []
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._loginOps = []
+ self._cachePath = cachePath
+ self._voicemailCachePath = None
+ self._username = None
+ self._password = None
+ self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
+ self._delayedRelogin = QtCore.QTimer()
+ self._delayedRelogin.setInterval(0)
+ self._delayedRelogin.setSingleShot(True)
+ self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
+
+ self._contacts = {}
+ self._accountUpdateTime = datetime.datetime(1971, 1, 1)
+ self._messages = []
+ self._cleanMessages = []
+ self._messageUpdateTime = datetime.datetime(1971, 1, 1)
+ self._history = []
+ self._historyUpdateTime = datetime.datetime(1971, 1, 1)
+ self._dnd = False
+ self._callback = ""
+
+ @property
+ def state(self):
+ return {
+ self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
+ self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
+ }.get(self._loggedInTime, self.LOGGEDIN_STATE)
+
+ @property
+ def draft(self):
+ return self._draft
+
+ def login(self, username, password):
+ assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
+ assert username != "", "No username specified"
+ if self._cachePath is not None:
+ cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
+ else:
+ cookiePath = None
+
+ if self._username != username or not self._backend:
+ from backends import gv_backend
+ del self._backend[:]
+ self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
+
+ self._pool.start()
+ le = self._asyncQueue.add_async(self._login)
+ le.start(username, password)
+
+ def logout(self):
+ assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+ _moduleLogger.info("Logging out")
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._backend[0].persist()
+ self._save_to_cache()
+ self._clear_voicemail_cache()
+ self.stateChange.emit(self.LOGGEDOUT_STATE)
+ self.loggedOut.emit()
+
+ def clear(self):
+ assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
+ self._backend[0].logout()
+ del self._backend[0]
+ self._clear_cache()
+ self._draft.clear()
+
+ def logout_and_clear(self):
+ assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+ _moduleLogger.info("Logging out and clearing the account")
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self.clear()
+ self.stateChange.emit(self.LOGGEDOUT_STATE)
+ self.loggedOut.emit()
+
+ def update_account(self, force = True):
+ if not force and self._contacts:
+ return
+ le = self._asyncQueue.add_async(self._update_account), (), {}
+ self._perform_op_while_loggedin(le)
+
+ def refresh_connection(self):
+ le = self._asyncQueue.add_async(self._refresh_authentication)
+ le.start()
+
+ def get_contacts(self):
+ return self._contacts
+
+ def get_when_contacts_updated(self):
+ return self._accountUpdateTime
+
+ def update_messages(self, messageType, force = True):
+ if not force and self._messages:
+ return
+ le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
+ self._perform_op_while_loggedin(le)
+
+ def get_messages(self):
+ return self._messages
+
+ def get_when_messages_updated(self):
+ return self._messageUpdateTime
+
+ def update_history(self, historyType, force = True):
+ if not force and self._history:
+ return
+ le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
+ self._perform_op_while_loggedin(le)
+
+ def get_history(self):
+ return self._history
+
+ def get_when_history_updated(self):
+ return self._historyUpdateTime
+
+ def update_dnd(self):
+ le = self._asyncQueue.add_async(self._update_dnd), (), {}
+ self._perform_op_while_loggedin(le)
+
+ def set_dnd(self, dnd):
+ le = self._asyncQueue.add_async(self._set_dnd)
+ le.start(dnd)
+
+ def is_available(self, messageId):
+ actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+ return os.path.exists(actualPath)
+
+ def voicemail_path(self, messageId):
+ actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+ if not os.path.exists(actualPath):
+ raise RuntimeError("Voicemail not available")
+ return actualPath
+
+ def download_voicemail(self, messageId):
+ le = self._asyncQueue.add_async(self._download_voicemail)
+ le.start(messageId)
+
+ def _set_dnd(self, dnd):
+ oldDnd = self._dnd
+ try:
+ assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
+ with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
+ yield (
+ self._backend[0].set_dnd,
+ (dnd, ),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ self._dnd = dnd
+ if oldDnd != self._dnd:
+ self.dndStateChange.emit(self._dnd)
+
+ def get_dnd(self):
+ return self._dnd
+
+ def get_account_number(self):
+ if self.state != self.LOGGEDIN_STATE:
+ return ""
+ return self._backend[0].get_account_number()
+
+ def get_callback_numbers(self):
+ if self.state != self.LOGGEDIN_STATE:
+ return {}
+ return self._backend[0].get_callback_numbers()
+
+ def get_callback_number(self):
+ return self._callback
+
+ def set_callback_number(self, callback):
+ le = self._asyncQueue.add_async(self._set_callback_number)
+ le.start(callback)
+
+ def _set_callback_number(self, callback):
+ oldCallback = self._callback
+ try:
+ assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
+ yield (
+ self._backend[0].set_callback_number,
+ (callback, ),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ self._callback = callback
+ if oldCallback != self._callback:
+ self.callbackNumberChanged.emit(self._callback)
+
+ def _login(self, username, password):
+ with qui_utils.notify_busy(self._errorLog, "Logging In"):
+ self._loggedInTime = self._LOGGINGIN_TIME
+ self.stateChange.emit(self.LOGGINGIN_STATE)
+ finalState = self.LOGGEDOUT_STATE
+ accountData = None
+ try:
+ if accountData is None and self._backend[0].is_quick_login_possible():
+ accountData = yield (
+ self._backend[0].refresh_account_info,
+ (),
+ {},
+ )
+ if accountData is not None:
+ _moduleLogger.info("Logged in through cookies")
+ else:
+ # Force a clearing of the cookies
+ yield (
+ self._backend[0].logout,
+ (),
+ {},
+ )
+
+ if accountData is None:
+ accountData = yield (
+ self._backend[0].login,
+ (username, password),
+ {},
+ )
+ if accountData is not None:
+ _moduleLogger.info("Logged in through credentials")
+
+ if accountData is not None:
+ self._loggedInTime = int(time.time())
+ oldUsername = self._username
+ self._username = username
+ self._password = password
+ finalState = self.LOGGEDIN_STATE
+ if oldUsername != self._username:
+ needOps = not self._load()
+ else:
+ needOps = True
+
+ self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
+ try:
+ os.makedirs(self._voicemailCachePath)
+ except OSError, e:
+ if e.errno != 17:
+ raise
+
+ self.loggedIn.emit()
+ self.stateChange.emit(finalState)
+ finalState = None # Mark it as already set
+ self._process_account_data(accountData)
+
+ if needOps:
+ loginOps = self._loginOps[:]
+ else:
+ loginOps = []
+ del self._loginOps[:]
+ for asyncOp, args, kwds in loginOps:
+ asyncOp.start(*args, **kwds)
+ else:
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self.error.emit("Error logging in")
+ except Exception, e:
+ _moduleLogger.exception("Booh")
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ finally:
+ if finalState is not None:
+ self.stateChange.emit(finalState)
+ if accountData is not None and self._callback:
+ self.set_callback_number(self._callback)
+
+ def _update_account(self):
+ try:
+ with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+ accountData = yield (
+ self._backend[0].refresh_account_info,
+ (),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ self._loggedInTime = int(time.time())
+ self._process_account_data(accountData)
+
+ def _refresh_authentication(self):
+ try:
+ with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+ accountData = yield (
+ self._backend[0].refresh_account_info,
+ (),
+ {},
+ )
+ accountData = None
+ except Exception, e:
+ _moduleLogger.exception("Passing to user")
+ self.error.emit(str(e))
+ # refresh_account_info does not normally throw, so it is fine if we
+ # just quit early because something seriously wrong is going on
+ return
+
+ if accountData is not None:
+ self._loggedInTime = int(time.time())
+ self._process_account_data(accountData)
+ else:
+ self._delayedRelogin.start()
+
+ def _load(self):
+ updateMessages = len(self._messages) != 0
+ updateHistory = len(self._history) != 0
+ oldDnd = self._dnd
+ oldCallback = self._callback
+
+ self._messages = []
+ self._cleanMessages = []
+ self._history = []
+ self._dnd = False
+ self._callback = ""
+
+ loadedFromCache = self._load_from_cache()
+ if loadedFromCache:
+ updateMessages = True
+ updateHistory = True
+
+ if updateMessages:
+ self.messagesUpdated.emit()
+ if updateHistory:
+ self.historyUpdated.emit()
+ if oldDnd != self._dnd:
+ self.dndStateChange.emit(self._dnd)
+ if oldCallback != self._callback:
+ self.callbackNumberChanged.emit(self._callback)
+
+ return loadedFromCache
+
+ def _load_from_cache(self):
+ if self._cachePath is None:
+ return False
+ cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
+
+ try:
+ with open(cachePath, "rb") as f:
+ dumpedData = pickle.load(f)
+ except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
+ _moduleLogger.exception("Pickle fun loading")
+ return False
+ except:
+ _moduleLogger.exception("Weirdness loading")
+ return False
+
+ try:
+ version, build = dumpedData[0:2]
+ except ValueError:
+ _moduleLogger.exception("Upgrade/downgrade fun")
+ return False
+ except:
+ _moduleLogger.exception("Weirdlings")
+ return False
+
+ if misc_utils.compare_versions(
+ self._OLDEST_COMPATIBLE_FORMAT_VERSION,
+ misc_utils.parse_version(version),
+ ) <= 0:
+ try:
+ (
+ version, build,
+ messages, messageUpdateTime,
+ history, historyUpdateTime,
+ dnd, callback
+ ) = dumpedData
+ except ValueError:
+ _moduleLogger.exception("Upgrade/downgrade fun")
+ return False
+ except:
+ _moduleLogger.exception("Weirdlings")
+ return False
+
+ _moduleLogger.info("Loaded cache")
+ self._messages = messages
+ self._alert_on_messages(self._messages)
+ self._messageUpdateTime = messageUpdateTime
+ self._history = history
+ self._historyUpdateTime = historyUpdateTime
+ self._dnd = dnd
+ self._callback = callback
+ return True
+ else:
+ _moduleLogger.debug(
+ "Skipping cache due to version mismatch (%s-%s)" % (
+ version, build
+ )
+ )
+ return False
+
+ def _save_to_cache(self):
+ _moduleLogger.info("Saving cache")
+ if self._cachePath is None:
+ return
+ cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
+
+ try:
+ dataToDump = (
+ constants.__version__, constants.__build__,
+ self._messages, self._messageUpdateTime,
+ self._history, self._historyUpdateTime,
+ self._dnd, self._callback
+ )
+ with open(cachePath, "wb") as f:
+ pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
+ _moduleLogger.info("Cache saved")
+ except (pickle.PickleError, IOError):
+ _moduleLogger.exception("While saving")
+
+ def _clear_cache(self):
+ updateMessages = len(self._messages) != 0
+ updateHistory = len(self._history) != 0
+ oldDnd = self._dnd
+ oldCallback = self._callback
+
+ self._messages = []
+ self._messageUpdateTime = datetime.datetime(1971, 1, 1)
+ self._history = []
+ self._historyUpdateTime = datetime.datetime(1971, 1, 1)
+ self._dnd = False
+ self._callback = ""
+
+ if updateMessages:
+ self.messagesUpdated.emit()
+ if updateHistory:
+ self.historyUpdated.emit()
+ if oldDnd != self._dnd:
+ self.dndStateChange.emit(self._dnd)
+ if oldCallback != self._callback:
+ self.callbackNumberChanged.emit(self._callback)
+
+ self._save_to_cache()
+ self._clear_voicemail_cache()
+
+ def _clear_voicemail_cache(self):
+ import shutil
+ shutil.rmtree(self._voicemailCachePath, True)
+
+ def _update_messages(self, messageType):
+ try:
+ assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
+ with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
+ self._messages = yield (
+ self._backend[0].get_messages,
+ (messageType, ),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ self._messageUpdateTime = datetime.datetime.now()
+ self.messagesUpdated.emit()
+ self._alert_on_messages(self._messages)
+
+ def _update_history(self, historyType):
+ try:
+ assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
+ with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType):
+ self._history = yield (
+ self._backend[0].get_call_history,
+ (historyType, ),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ self._historyUpdateTime = datetime.datetime.now()
+ self.historyUpdated.emit()
+
+ def _update_dnd(self):
+ with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
+ oldDnd = self._dnd
+ try:
+ assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
+ self._dnd = yield (
+ self._backend[0].is_dnd,
+ (),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+ return
+ if oldDnd != self._dnd:
+ self.dndStateChange(self._dnd)
+
+ def _download_voicemail(self, messageId):
+ actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+ targetPath = "%s.%s.part" % (actualPath, time.time())
+ if os.path.exists(actualPath):
+ self.voicemailAvailable.emit(messageId, actualPath)
+ return
+ with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
+ try:
+ yield (
+ self._backend[0].download,
+ (messageId, targetPath),
+ {},
+ )
+ except Exception, e:
+ _moduleLogger.exception("Passing to user")
+ self.error.emit(str(e))
+ return
+
+ if os.path.exists(actualPath):
+ try:
+ os.remove(targetPath)
+ except:
+ _moduleLogger.exception("Ignoring file problems with cache")
+ self.voicemailAvailable.emit(messageId, actualPath)
+ return
+ else:
+ os.rename(targetPath, actualPath)
+ self.voicemailAvailable.emit(messageId, actualPath)
+
+ def _perform_op_while_loggedin(self, op):
+ if self.state == self.LOGGEDIN_STATE:
+ op, args, kwds = op
+ op.start(*args, **kwds)
+ else:
+ self._push_login_op(op)
+
+ def _push_login_op(self, asyncOp):
+ assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
+ if asyncOp in self._loginOps:
+ _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
+ return
+ self._loginOps.append(asyncOp)
+
+ def _process_account_data(self, accountData):
+ self._contacts = dict(
+ (contactId, contactDetails)
+ for contactId, contactDetails in accountData["contacts"].iteritems()
+ # A zero contact id is the catch all for unknown contacts
+ if contactId != "0"
+ )
+
+ self._accountUpdateTime = datetime.datetime.now()
+ self.accountUpdated.emit()
+
+ def _alert_on_messages(self, messages):
+ cleanNewMessages = list(self._clean_messages(messages))
+ cleanNewMessages.sort(key=lambda m: m["contactId"])
+ if self._cleanMessages:
+ if self._cleanMessages != cleanNewMessages:
+ self.newMessages.emit()
+ self._cleanMessages = cleanNewMessages
+
+ def _clean_messages(self, messages):
+ for message in messages:
+ cleaned = dict(
+ kv
+ for kv in message.iteritems()
+ if kv[0] not in
+ [
+ "relTime",
+ "time",
+ "isArchived",
+ "isRead",
+ "isSpam",
+ "isTrash",
+ ]
+ )
+
+ # Don't let outbound messages cause alerts, especially if the package has only outbound
+ cleaned["messageParts"] = [
+ tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
+ ]
+ if not cleaned["messageParts"]:
+ continue
+
+ yield cleaned
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_relogin(self):
+ try:
+ username = self._username
+ password = self._password
+ self.logout()
+ self.login(username, password)
+ except Exception, e:
+ _moduleLogger.exception("Passing to user")
+ self.error.emit(str(e))
+ return
--- /dev/null
+import logging
+
+import gobject
+import gst
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+ # @bug Advertising state changes a bit early, should watch for GStreamer state change
+
+ STATE_PLAY = "play"
+ STATE_PAUSE = "pause"
+ STATE_STOP = "stop"
+
+ __gsignals__ = {
+ 'state-change' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING, ),
+ ),
+ 'eof' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING, ),
+ ),
+ 'error' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+ ),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ #Fields
+ self._uri = ""
+ self._elapsed = 0
+ self._duration = 0
+
+ #Set up GStreamer
+ self._player = gst.element_factory_make("playbin2", "player")
+ bus = self._player.get_bus()
+ bus.add_signal_watch()
+ bus.connect("message", self._on_message)
+
+ #Constants
+ self._timeFormat = gst.Format(gst.FORMAT_TIME)
+ self._seekFlag = gst.SEEK_FLAG_FLUSH
+
+ @property
+ def playing(self):
+ return self.state == self.STATE_PLAY
+
+ @property
+ def has_file(self):
+ return 0 < len(self._uri)
+
+ @property
+ def state(self):
+ state = self._player.get_state()[1]
+ return self._translate_state(state)
+
+ def set_file(self, uri):
+ if self._uri != uri:
+ self._invalidate_cache()
+ if self.state != self.STATE_STOP:
+ self.stop()
+
+ self._uri = uri
+ self._player.set_property("uri", uri)
+
+ def play(self):
+ if self.state == self.STATE_PLAY:
+ _moduleLogger.info("Already play")
+ return
+ _moduleLogger.info("Play")
+ self._player.set_state(gst.STATE_PLAYING)
+ self.emit("state-change", self.STATE_PLAY)
+
+ def pause(self):
+ if self.state == self.STATE_PAUSE:
+ _moduleLogger.info("Already pause")
+ return
+ _moduleLogger.info("Pause")
+ self._player.set_state(gst.STATE_PAUSED)
+ self.emit("state-change", self.STATE_PAUSE)
+
+ def stop(self):
+ if self.state == self.STATE_STOP:
+ _moduleLogger.info("Already stop")
+ return
+ self._player.set_state(gst.STATE_NULL)
+ _moduleLogger.info("Stopped")
+ self.emit("state-change", self.STATE_STOP)
+
+ @property
+ def elapsed(self):
+ try:
+ self._elapsed = self._player.query_position(self._timeFormat, None)[0]
+ except:
+ pass
+ return self._elapsed
+
+ @property
+ def duration(self):
+ try:
+ self._duration = self._player.query_duration(self._timeFormat, None)[0]
+ except:
+ _moduleLogger.exception("Query failed")
+ return self._duration
+
+ def seek_time(self, ns):
+ self._elapsed = ns
+ self._player.seek_simple(self._timeFormat, self._seekFlag, ns)
+
+ def _invalidate_cache(self):
+ self._elapsed = 0
+ self._duration = 0
+
+ def _translate_state(self, gstState):
+ return {
+ gst.STATE_NULL: self.STATE_STOP,
+ gst.STATE_PAUSED: self.STATE_PAUSE,
+ gst.STATE_PLAYING: self.STATE_PLAY,
+ }.get(gstState, self.STATE_STOP)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_message(self, bus, message):
+ t = message.type
+ if t == gst.MESSAGE_EOS:
+ self._player.set_state(gst.STATE_NULL)
+ self.emit("eof", self._uri)
+ elif t == gst.MESSAGE_ERROR:
+ self._player.set_state(gst.STATE_NULL)
+ err, debug = message.parse_error()
+ _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+ self.emit("error", err, debug)
+
+
+gobject.type_register(Stream)
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+import util.misc as misc_utils
+try:
+ import stream_gst
+ stream = stream_gst
+except ImportError:
+ try:
+ import stream_osso
+ stream = stream_osso
+ except ImportError:
+ import stream_null
+ stream = stream_null
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class StreamToken(QtCore.QObject):
+
+ stateChange = qt_compat.Signal(str)
+ invalidated = qt_compat.Signal()
+ error = qt_compat.Signal(str)
+
+ STATE_PLAY = stream.Stream.STATE_PLAY
+ STATE_PAUSE = stream.Stream.STATE_PAUSE
+ STATE_STOP = stream.Stream.STATE_STOP
+
+ def __init__(self, stream):
+ QtCore.QObject.__init__(self)
+ self._stream = stream
+ self._stream.connect("state-change", self._on_stream_state)
+ self._stream.connect("eof", self._on_stream_eof)
+ self._stream.connect("error", self._on_stream_error)
+
+ @property
+ def state(self):
+ if self.isValid:
+ return self._stream.state
+ else:
+ return self.STATE_STOP
+
+ @property
+ def isValid(self):
+ return self._stream is not None
+
+ def play(self):
+ self._stream.play()
+
+ def pause(self):
+ self._stream.pause()
+
+ def stop(self):
+ self._stream.stop()
+
+ def invalidate(self):
+ if self._stream is None:
+ return
+ _moduleLogger.info("Playback token invalidated")
+ self._stream = None
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_stream_state(self, s, state):
+ if not self.isValid:
+ return
+ if state == self.STATE_STOP:
+ self.invalidate()
+ self.stateChange.emit(state)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_stream_eof(self, s, uri):
+ if not self.isValid:
+ return
+ self.invalidate()
+ self.stateChange.emit(self.STATE_STOP)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_stream_error(self, s, error, debug):
+ if not self.isValid:
+ return
+ _moduleLogger.info("Error %s %s" % (error, debug))
+ self.error.emit(str(error))
+
+
+class StreamHandler(QtCore.QObject):
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._stream = stream.Stream()
+ self._token = StreamToken(self._stream)
+
+ def set_file(self, path):
+ self._token.invalidate()
+ self._token = StreamToken(self._stream)
+ self._stream.set_file(path)
+ return self._token
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_stream_state(self, s, state):
+ _moduleLogger.info("State change %r" % state)
+
+
+if __name__ == "__main__":
+ pass
+
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(object):
+
+ STATE_PLAY = "play"
+ STATE_PAUSE = "pause"
+ STATE_STOP = "stop"
+
+ def __init__(self):
+ pass
+
+ def connect(self, signalName, slot):
+ pass
+
+ @property
+ def playing(self):
+ return False
+
+ @property
+ def has_file(self):
+ return False
+
+ @property
+ def state(self):
+ return self.STATE_STOP
+
+ def set_file(self, uri):
+ pass
+
+ def play(self):
+ pass
+
+ def pause(self):
+ pass
+
+ def stop(self):
+ pass
+
+ @property
+ def elapsed(self):
+ return 0
+
+ @property
+ def duration(self):
+ return 0
+
+ def seek_time(self, ns):
+ pass
+
+
+if __name__ == "__main__":
+ pass
+
--- /dev/null
+import logging
+
+import gobject
+import dbus
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+ STATE_PLAY = "play"
+ STATE_PAUSE = "pause"
+ STATE_STOP = "stop"
+
+ __gsignals__ = {
+ 'state-change' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING, ),
+ ),
+ 'eof' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING, ),
+ ),
+ 'error' : (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+ ),
+ }
+
+ _SERVICE_NAME = "com.nokia.osso_media_server"
+ _OBJECT_PATH = "/com/nokia/osso_media_server"
+ _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ #Fields
+ self._state = self.STATE_STOP
+ self._nextState = self.STATE_STOP
+ self._uri = ""
+ self._elapsed = 0
+ self._duration = 0
+
+ session_bus = dbus.SessionBus()
+
+ # Get the osso-media-player proxy object
+ oms_object = session_bus.get_object(
+ self._SERVICE_NAME,
+ self._OBJECT_PATH,
+ introspect=False,
+ follow_name_owner_changes=True,
+ )
+ # Use the audio interface
+ oms_audio_interface = dbus.Interface(
+ oms_object,
+ self._AUDIO_INTERFACE_NAME,
+ )
+ self._audioProxy = oms_audio_interface
+
+ self._audioProxy.connect_to_signal("state_changed", self._on_state_changed)
+ self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream)
+
+ error_signals = [
+ "no_media_selected",
+ "file_not_found",
+ "type_not_found",
+ "unsupported_type",
+ "gstreamer",
+ "dsp",
+ "device_unavailable",
+ "corrupted_file",
+ "out_of_memory",
+ "audio_codec_not_supported",
+ ]
+ for error in error_signals:
+ self._audioProxy.connect_to_signal(error, self._on_error)
+
+ @property
+ def playing(self):
+ return self.state == self.STATE_PLAY
+
+ @property
+ def has_file(self):
+ return 0 < len(self._uri)
+
+ @property
+ def state(self):
+ return self._state
+
+ def set_file(self, uri):
+ if self._uri != uri:
+ self._invalidate_cache()
+ if self.state != self.STATE_STOP:
+ self.stop()
+
+ self._uri = uri
+ self._audioProxy.set_media_location(self._uri)
+
+ def play(self):
+ if self._nextState == self.STATE_PLAY:
+ _moduleLogger.info("Already play")
+ return
+ _moduleLogger.info("Play")
+ self._audioProxy.play()
+ self._nextState = self.STATE_PLAY
+ #self.emit("state-change", self.STATE_PLAY)
+
+ def pause(self):
+ if self._nextState == self.STATE_PAUSE:
+ _moduleLogger.info("Already pause")
+ return
+ _moduleLogger.info("Pause")
+ self._audioProxy.pause()
+ self._nextState = self.STATE_PAUSE
+ #self.emit("state-change", self.STATE_PLAY)
+
+ def stop(self):
+ if self._nextState == self.STATE_STOP:
+ _moduleLogger.info("Already stop")
+ return
+ self._audioProxy.stop()
+ _moduleLogger.info("Stopped")
+ self._nextState = self.STATE_STOP
+ #self.emit("state-change", self.STATE_STOP)
+
+ @property
+ def elapsed(self):
+ pos_info = self._audioProxy.get_position()
+ if isinstance(pos_info, tuple):
+ self._elapsed, self._duration = pos_info
+ return self._elapsed
+
+ @property
+ def duration(self):
+ pos_info = self._audioProxy.get_position()
+ if isinstance(pos_info, tuple):
+ self._elapsed, self._duration = pos_info
+ return self._duration
+
+ def seek_time(self, ns):
+ _moduleLogger.debug("Seeking to: %s", ns)
+ self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) )
+
+ def _invalidate_cache(self):
+ self._elapsed = 0
+ self._duration = 0
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_error(self, *args):
+ err, debug = "", repr(args)
+ _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+ self.emit("error", err, debug)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_end_of_stream(self, *args):
+ self._state = self.STATE_STOP
+ self._nextState = self.STATE_STOP
+ self.emit("eof", self._uri)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_state_changed(self, state):
+ _moduleLogger.info("State: %s", state)
+ state = {
+ "playing": self.STATE_PLAY,
+ "paused": self.STATE_PAUSE,
+ "stopped": self.STATE_STOP,
+ }[state]
+ if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP:
+ # They seem to want to advertise stop right as the stream is starting, breaking the owner of this
+ return
+ self._state = state
+ self._nextState = state
+ self.emit("state-change", state)
+
+
+gobject.type_register(Stream)
--- /dev/null
+#!/usr/bin/env python
--- /dev/null
+#!/usr/bin/env python
+
+"""
+@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448
+"""
+
+import itertools
+import functools
+import datetime
+import types
+import array
+import random
+
+
+def ordered_itr(collection):
+ """
+ >>> [v for v in ordered_itr({"a": 1, "b": 2})]
+ [('a', 1), ('b', 2)]
+ >>> [v for v in ordered_itr([3, 1, 10, -20])]
+ [-20, 1, 3, 10]
+ """
+ if isinstance(collection, types.DictType):
+ keys = list(collection.iterkeys())
+ keys.sort()
+ for key in keys:
+ yield key, collection[key]
+ else:
+ values = list(collection)
+ values.sort()
+ for value in values:
+ yield value
+
+
+def itercat(*iterators):
+ """
+ Concatenate several iterators into one.
+
+ >>> [v for v in itercat([1, 2, 3], [4, 1, 3])]
+ [1, 2, 3, 4, 1, 3]
+ """
+ for i in iterators:
+ for x in i:
+ yield x
+
+
+def product(*args, **kwds):
+ # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
+ # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
+ pools = map(tuple, args) * kwds.get('repeat', 1)
+ result = [[]]
+ for pool in pools:
+ result = [x+[y] for x in result for y in pool]
+ for prod in result:
+ yield tuple(prod)
+
+
+def iterwhile(func, iterator):
+ """
+ Iterate for as long as func(value) returns true.
+ >>> through = lambda b: b
+ >>> [v for v in iterwhile(through, [True, True, False])]
+ [True, True]
+ """
+ iterator = iter(iterator)
+ while 1:
+ next = iterator.next()
+ if not func(next):
+ raise StopIteration
+ yield next
+
+
+def iterfirst(iterator, count=1):
+ """
+ Iterate through 'count' first values.
+
+ >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)]
+ [1, 2, 3]
+ """
+ iterator = iter(iterator)
+ for i in xrange(count):
+ yield iterator.next()
+
+
+def iterstep(iterator, n):
+ """
+ Iterate every nth value.
+
+ >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)]
+ [1, 2, 3, 4, 5]
+ >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)]
+ [1, 3, 5]
+ >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)]
+ [1, 4]
+ """
+ iterator = iter(iterator)
+ while True:
+ yield iterator.next()
+ # skip n-1 values
+ for dummy in xrange(n-1):
+ iterator.next()
+
+
+def itergroup(iterator, count, padValue = None):
+ """
+ Iterate in groups of 'count' values. If there
+ aren't enough values, the last result is padded with
+ None.
+
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print list(val)
+ [1, 2, 3]
+ [4, 5, 6]
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ (7, None, None)
+ >>> for val in itergroup("123456", 3):
+ ... print tuple(val)
+ ('1', '2', '3')
+ ('4', '5', '6')
+ >>> for val in itergroup("123456", 3):
+ ... print repr("".join(val))
+ '123'
+ '456'
+ """
+ paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+ nIterators = (paddedIterator, ) * count
+ return itertools.izip(*nIterators)
+
+
+def xzip(*iterators):
+ """Iterative version of builtin 'zip'."""
+ iterators = itertools.imap(iter, iterators)
+ while 1:
+ yield tuple([x.next() for x in iterators])
+
+
+def xmap(func, *iterators):
+ """Iterative version of builtin 'map'."""
+ iterators = itertools.imap(iter, iterators)
+ values_left = [1]
+
+ def values():
+ # Emulate map behaviour, i.e. shorter
+ # sequences are padded with None when
+ # they run out of values.
+ values_left[0] = 0
+ for i in range(len(iterators)):
+ iterator = iterators[i]
+ if iterator is None:
+ yield None
+ else:
+ try:
+ yield iterator.next()
+ values_left[0] = 1
+ except StopIteration:
+ iterators[i] = None
+ yield None
+ while 1:
+ args = tuple(values())
+ if not values_left[0]:
+ raise StopIteration
+ yield func(*args)
+
+
+def xfilter(func, iterator):
+ """Iterative version of builtin 'filter'."""
+ iterator = iter(iterator)
+ while 1:
+ next = iterator.next()
+ if func(next):
+ yield next
+
+
+def xreduce(func, iterator, default=None):
+ """Iterative version of builtin 'reduce'."""
+ iterator = iter(iterator)
+ try:
+ prev = iterator.next()
+ except StopIteration:
+ return default
+ single = 1
+ for next in iterator:
+ single = 0
+ prev = func(prev, next)
+ if single:
+ return func(prev, default)
+ return prev
+
+
+def daterange(begin, end, delta = datetime.timedelta(1)):
+ """
+ Form a range of dates and iterate over them.
+
+ Arguments:
+ begin -- a date (or datetime) object; the beginning of the range.
+ end -- a date (or datetime) object; the end of the range.
+ delta -- (optional) a datetime.timedelta object; how much to step each iteration.
+ Default step is 1 day.
+
+ Usage:
+ """
+ if not isinstance(delta, datetime.timedelta):
+ delta = datetime.timedelta(delta)
+
+ ZERO = datetime.timedelta(0)
+
+ if begin < end:
+ if delta <= ZERO:
+ raise StopIteration
+ test = end.__gt__
+ else:
+ if delta >= ZERO:
+ raise StopIteration
+ test = end.__lt__
+
+ while test(begin):
+ yield begin
+ begin += delta
+
+
+class LazyList(object):
+ """
+ A Sequence whose values are computed lazily by an iterator.
+
+ Module for the creation and use of iterator-based lazy lists.
+ this module defines a class LazyList which can be used to represent sequences
+ of values generated lazily. One can also create recursively defined lazy lists
+ that generate their values based on ones previously generated.
+
+ Backport to python 2.5 by Michael Pust
+ """
+
+ __author__ = 'Dan Spitz'
+
+ def __init__(self, iterable):
+ self._exhausted = False
+ self._iterator = iter(iterable)
+ self._data = []
+
+ def __len__(self):
+ """Get the length of a LazyList's computed data."""
+ return len(self._data)
+
+ def __getitem__(self, i):
+ """Get an item from a LazyList.
+ i should be a positive integer or a slice object."""
+ if isinstance(i, int):
+ #index has not yet been yielded by iterator (or iterator exhausted
+ #before reaching that index)
+ if i >= len(self):
+ self.exhaust(i)
+ elif i < 0:
+ raise ValueError('cannot index LazyList with negative number')
+ return self._data[i]
+
+ #LazyList slices are iterators over a portion of the list.
+ elif isinstance(i, slice):
+ start, stop, step = i.start, i.stop, i.step
+ if any(x is not None and x < 0 for x in (start, stop, step)):
+ raise ValueError('cannot index or step through a LazyList with'
+ 'a negative number')
+ #set start and step to their integer defaults if they are None.
+ if start is None:
+ start = 0
+ if step is None:
+ step = 1
+
+ def LazyListIterator():
+ count = start
+ predicate = (
+ (lambda: True)
+ if stop is None
+ else (lambda: count < stop)
+ )
+ while predicate():
+ try:
+ yield self[count]
+ #slices can go out of actual index range without raising an
+ #error
+ except IndexError:
+ break
+ count += step
+ return LazyListIterator()
+
+ raise TypeError('i must be an integer or slice')
+
+ def __iter__(self):
+ """return an iterator over each value in the sequence,
+ whether it has been computed yet or not."""
+ return self[:]
+
+ def computed(self):
+ """Return an iterator over the values in a LazyList that have
+ already been computed."""
+ return self[:len(self)]
+
+ def exhaust(self, index = None):
+ """Exhaust the iterator generating this LazyList's values.
+ if index is None, this will exhaust the iterator completely.
+ Otherwise, it will iterate over the iterator until either the list
+ has a value for index or the iterator is exhausted.
+ """
+ if self._exhausted:
+ return
+ if index is None:
+ ind_range = itertools.count(len(self))
+ else:
+ ind_range = range(len(self), index + 1)
+
+ for ind in ind_range:
+ try:
+ self._data.append(self._iterator.next())
+ except StopIteration: #iterator is fully exhausted
+ self._exhausted = True
+ break
+
+
+class RecursiveLazyList(LazyList):
+
+ def __init__(self, prod, *args, **kwds):
+ super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds))
+
+
+class RecursiveLazyListFactory:
+
+ def __init__(self, producer):
+ self._gen = producer
+
+ def __call__(self, *a, **kw):
+ return RecursiveLazyList(self._gen, *a, **kw)
+
+
+def lazylist(gen):
+ """
+ Decorator for creating a RecursiveLazyList subclass.
+ This should decorate a generator function taking the LazyList object as its
+ first argument which yields the contents of the list in order.
+
+ >>> #fibonnacci sequence in a lazy list.
+ >>> @lazylist
+ ... def fibgen(lst):
+ ... yield 0
+ ... yield 1
+ ... for a, b in itertools.izip(lst, lst[1:]):
+ ... yield a + b
+ ...
+ >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence
+ >>> fibs = fibgen()
+ >>>
+ >>> #prime numbers in a lazy list.
+ >>> @lazylist
+ ... def primegen(lst):
+ ... yield 2
+ ... for candidate in itertools.count(3): #start at next number after 2
+ ... #if candidate is not divisible by any smaller prime numbers,
+ ... #it is a prime.
+ ... if all(candidate % p for p in lst.computed()):
+ ... yield candidate
+ ...
+ >>> #same for primes- treat it like an infinitely long list containing all prime numbers.
+ >>> primes = primegen()
+ >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2]
+ 0 1 1 2 3 5
+ >>> print list(fibs[:10]), list(primes[:10])
+ [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
+ """
+ return RecursiveLazyListFactory(gen)
+
+
+def map_func(f):
+ """
+ >>> import misc
+ >>> misc.validate_decorator(map_func)
+ """
+
+ @functools.wraps(f)
+ def wrapper(*args):
+ result = itertools.imap(f, args)
+ return result
+ return wrapper
+
+
+def reduce_func(function):
+ """
+ >>> import misc
+ >>> misc.validate_decorator(reduce_func(lambda x: x))
+ """
+
+ def decorator(f):
+
+ @functools.wraps(f)
+ def wrapper(*args):
+ result = reduce(function, f(args))
+ return result
+ return wrapper
+ return decorator
+
+
+def any_(iterable):
+ """
+ @note Python Version <2.5
+
+ >>> any_([True, True])
+ True
+ >>> any_([True, False])
+ True
+ >>> any_([False, False])
+ False
+ """
+
+ for element in iterable:
+ if element:
+ return True
+ return False
+
+
+def all_(iterable):
+ """
+ @note Python Version <2.5
+
+ >>> all_([True, True])
+ True
+ >>> all_([True, False])
+ False
+ >>> all_([False, False])
+ False
+ """
+
+ for element in iterable:
+ if not element:
+ return False
+ return True
+
+
+def for_every(pred, seq):
+ """
+ for_every takes a one argument predicate function and a sequence.
+ @param pred The predicate function should return true or false.
+ @returns true if every element in seq returns true for predicate, else returns false.
+
+ >>> for_every (lambda c: c > 5,(6,7,8,9))
+ True
+
+ @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+ """
+
+ for i in seq:
+ if not pred(i):
+ return False
+ return True
+
+
+def there_exists(pred, seq):
+ """
+ there_exists takes a one argument predicate function and a sequence.
+ @param pred The predicate function should return true or false.
+ @returns true if any element in seq returns true for predicate, else returns false.
+
+ >>> there_exists (lambda c: c > 5,(6,7,8,9))
+ True
+
+ @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+ """
+
+ for i in seq:
+ if pred(i):
+ return True
+ return False
+
+
+def func_repeat(quantity, func, *args, **kwd):
+ """
+ Meant to be in connection with "reduce"
+ """
+ for i in xrange(quantity):
+ yield func(*args, **kwd)
+
+
+def function_map(preds, item):
+ """
+ Meant to be in connection with "reduce"
+ """
+ results = (pred(item) for pred in preds)
+
+ return results
+
+
+def functional_if(combiner, preds, item):
+ """
+ Combines the result of a list of predicates applied to item according to combiner
+
+ @see any, every for example combiners
+ """
+ pass_bool = lambda b: b
+
+ bool_results = function_map(preds, item)
+ return combiner(pass_bool, bool_results)
+
+
+def pushback_itr(itr):
+ """
+ >>> list(pushback_itr(xrange(5)))
+ [0, 1, 2, 3, 4]
+ >>>
+ >>> first = True
+ >>> itr = pushback_itr(xrange(5))
+ >>> for i in itr:
+ ... print i
+ ... if first and i == 2:
+ ... first = False
+ ... print itr.send(i)
+ 0
+ 1
+ 2
+ None
+ 2
+ 3
+ 4
+ >>>
+ >>> first = True
+ >>> itr = pushback_itr(xrange(5))
+ >>> for i in itr:
+ ... print i
+ ... if first and i == 2:
+ ... first = False
+ ... print itr.send(i)
+ ... print itr.send(i)
+ 0
+ 1
+ 2
+ None
+ None
+ 2
+ 2
+ 3
+ 4
+ >>>
+ >>> itr = pushback_itr(xrange(5))
+ >>> print itr.next()
+ 0
+ >>> print itr.next()
+ 1
+ >>> print itr.send(10)
+ None
+ >>> print itr.next()
+ 10
+ >>> print itr.next()
+ 2
+ >>> print itr.send(20)
+ None
+ >>> print itr.send(30)
+ None
+ >>> print itr.send(40)
+ None
+ >>> print itr.next()
+ 40
+ >>> print itr.next()
+ 30
+ >>> print itr.send(50)
+ None
+ >>> print itr.next()
+ 50
+ >>> print itr.next()
+ 20
+ >>> print itr.next()
+ 3
+ >>> print itr.next()
+ 4
+ """
+ for item in itr:
+ maybePushedBack = yield item
+ queue = []
+ while queue or maybePushedBack is not None:
+ if maybePushedBack is not None:
+ queue.append(maybePushedBack)
+ maybePushedBack = yield None
+ else:
+ item = queue.pop()
+ maybePushedBack = yield item
+
+
+def itr_available(queue, initiallyBlock = False):
+ if initiallyBlock:
+ yield queue.get()
+ while not queue.empty():
+ yield queue.get_nowait()
+
+
+class BloomFilter(object):
+ """
+ http://en.wikipedia.org/wiki/Bloom_filter
+ Sources:
+ http://code.activestate.com/recipes/577684-bloom-filter/
+ http://code.activestate.com/recipes/577686-bloom-filter/
+
+ >>> from random import sample
+ >>> from string import ascii_letters
+ >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut
+ ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas
+ ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota
+ ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey
+ ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon
+ ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah
+ ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split()
+ >>> bf = BloomFilter(num_bits=1000, num_probes=14)
+ >>> for state in states:
+ ... bf.add(state)
+ >>> numStatesFound = sum(state in bf for state in states)
+ >>> numStatesFound, len(states)
+ (50, 50)
+ >>> trials = 100
+ >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials))
+ >>> numGarbageFound, trials
+ (0, 100)
+ """
+
+ def __init__(self, num_bits, num_probes):
+ num_words = (num_bits + 31) // 32
+ self._arr = array.array('B', [0]) * num_words
+ self._num_probes = num_probes
+
+ def add(self, key):
+ for i, mask in self._get_probes(key):
+ self._arr[i] |= mask
+
+ def union(self, bfilter):
+ if self._match_template(bfilter):
+ for i, b in enumerate(bfilter._arr):
+ self._arr[i] |= b
+ else:
+ # Union b/w two unrelated bloom filter raises this
+ raise ValueError("Mismatched bloom filters")
+
+ def intersection(self, bfilter):
+ if self._match_template(bfilter):
+ for i, b in enumerate(bfilter._arr):
+ self._arr[i] &= b
+ else:
+ # Intersection b/w two unrelated bloom filter raises this
+ raise ValueError("Mismatched bloom filters")
+
+ def __contains__(self, key):
+ return all(self._arr[i] & mask for i, mask in self._get_probes(key))
+
+ def _match_template(self, bfilter):
+ return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes
+
+ def _get_probes(self, key):
+ hasher = random.Random(key).randrange
+ for _ in range(self._num_probes):
+ array_index = hasher(len(self._arr))
+ bit_index = hasher(32)
+ yield array_index, 1 << bit_index
+
+
+if __name__ == "__main__":
+ import doctest
+ print doctest.testmod()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import os
+import errno
+import time
+import functools
+import contextlib
+import logging
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class AsyncTaskQueue(object):
+
+ def __init__(self, taskPool):
+ self._asyncs = []
+ self._taskPool = taskPool
+
+ def add_async(self, func):
+ self.flush()
+ a = AsyncGeneratorTask(self._taskPool, func)
+ self._asyncs.append(a)
+ return a
+
+ def flush(self):
+ self._asyncs = [a for a in self._asyncs if not a.isDone]
+
+
+class AsyncGeneratorTask(object):
+
+ def __init__(self, pool, func):
+ self._pool = pool
+ self._func = func
+ self._run = None
+ self._isDone = False
+
+ @property
+ def isDone(self):
+ return self._isDone
+
+ def start(self, *args, **kwds):
+ assert self._run is None, "Task already started"
+ self._run = self._func(*args, **kwds)
+ trampoline, args, kwds = self._run.send(None) # priming the function
+ self._pool.add_task(
+ trampoline,
+ args,
+ kwds,
+ self.on_success,
+ self.on_error,
+ )
+
+ @misc.log_exception(_moduleLogger)
+ def on_success(self, result):
+ _moduleLogger.debug("Processing success for: %r", self._func)
+ try:
+ trampoline, args, kwds = self._run.send(result)
+ except StopIteration, e:
+ self._isDone = True
+ else:
+ self._pool.add_task(
+ trampoline,
+ args,
+ kwds,
+ self.on_success,
+ self.on_error,
+ )
+
+ @misc.log_exception(_moduleLogger)
+ def on_error(self, error):
+ _moduleLogger.debug("Processing error for: %r", self._func)
+ try:
+ trampoline, args, kwds = self._run.throw(error)
+ except StopIteration, e:
+ self._isDone = True
+ else:
+ self._pool.add_task(
+ trampoline,
+ args,
+ kwds,
+ self.on_success,
+ self.on_error,
+ )
+
+ def __repr__(self):
+ return "<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):
+ """
+ Synchronization decorator.
+
+ >>> import misc
+ >>> misc.validate_decorator(synchronized(object()))
+ """
+
+ def wrap(f):
+
+ @functools.wraps(f)
+ def newFunction(*args, **kw):
+ lock.acquire()
+ try:
+ return f(*args, **kw)
+ finally:
+ lock.release()
+ return newFunction
+ return wrap
+
+
+@contextlib.contextmanager
+def qlock(queue, gblock = True, gtimeout = None, pblock = True, ptimeout = None):
+ """
+ Locking with a queue, good for when you want to lock an item passed around
+
+ >>> import Queue
+ >>> item = 5
+ >>> lock = Queue.Queue()
+ >>> lock.put(item)
+ >>> with qlock(lock) as i:
+ ... print i
+ 5
+ """
+ item = queue.get(gblock, gtimeout)
+ try:
+ yield item
+ finally:
+ queue.put(item, pblock, ptimeout)
+
+
+@contextlib.contextmanager
+def flock(path, timeout=-1):
+ WAIT_FOREVER = -1
+ DELAY = 0.1
+ timeSpent = 0
+
+ acquired = False
+
+ while timeSpent <= timeout or timeout == WAIT_FOREVER:
+ try:
+ fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+ acquired = True
+ break
+ except OSError, e:
+ if e.errno != errno.EEXIST:
+ raise
+ time.sleep(DELAY)
+ timeSpent += DELAY
+
+ assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
+
+ try:
+ yield fd
+ finally:
+ os.unlink(path)
--- /dev/null
+#!/usr/bin/env python\r
+\r
+"""\r
+Uses for generators\r
+* Pull pipelining (iterators)\r
+* Push pipelining (coroutines)\r
+* State machines (coroutines)\r
+* "Cooperative multitasking" (coroutines)\r
+* Algorithm -> Object transform for cohesiveness (for example context managers) (coroutines)\r
+\r
+Design considerations\r
+* When should a stage pass on exceptions or have it thrown within it?\r
+* When should a stage pass on GeneratorExits?\r
+* Is there a way to either turn a push generator into a iterator or to use\r
+ comprehensions syntax for push generators (I doubt it)\r
+* When should the stage try and send data in both directions\r
+* Since pull generators (generators), push generators (coroutines), subroutines, and coroutines are all coroutines, maybe we should rename the push generators to not confuse them, like signals/slots? and then refer to two-way generators as coroutines\r
+** If so, make s* and co* implementation of functions\r
+"""\r
+\r
+import threading\r
+import Queue\r
+import pickle\r
+import functools\r
+import itertools\r
+import xml.sax\r
+import xml.parsers.expat\r
+\r
+\r
+def autostart(func):\r
+ """\r
+ >>> @autostart\r
+ ... def grep_sink(pattern):\r
+ ... print "Looking for %s" % pattern\r
+ ... while True:\r
+ ... line = yield\r
+ ... if pattern in line:\r
+ ... print line,\r
+ >>> g = grep_sink("python")\r
+ Looking for python\r
+ >>> g.send("Yeah but no but yeah but no")\r
+ >>> g.send("A series of tubes")\r
+ >>> g.send("python generators rock!")\r
+ python generators rock!\r
+ >>> g.close()\r
+ """\r
+\r
+ @functools.wraps(func)\r
+ def start(*args, **kwargs):\r
+ cr = func(*args, **kwargs)\r
+ cr.next()\r
+ return cr\r
+\r
+ return start\r
+\r
+\r
+@autostart\r
+def printer_sink(format = "%s"):\r
+ """\r
+ >>> pr = printer_sink("%r")\r
+ >>> pr.send("Hello")\r
+ 'Hello'\r
+ >>> pr.send("5")\r
+ '5'\r
+ >>> pr.send(5)\r
+ 5\r
+ >>> p = printer_sink()\r
+ >>> p.send("Hello")\r
+ Hello\r
+ >>> p.send("World")\r
+ World\r
+ >>> # p.throw(RuntimeError, "Goodbye")\r
+ >>> # p.send("Meh")\r
+ >>> # p.close()\r
+ """\r
+ while True:\r
+ item = yield\r
+ print format % (item, )\r
+\r
+\r
+@autostart\r
+def null_sink():\r
+ """\r
+ Good for uses like with cochain to pick up any slack\r
+ """\r
+ while True:\r
+ item = yield\r
+\r
+\r
+def itr_source(itr, target):\r
+ """\r
+ >>> itr_source(xrange(2), printer_sink())\r
+ 0\r
+ 1\r
+ """\r
+ for item in itr:\r
+ target.send(item)\r
+\r
+\r
+@autostart\r
+def cofilter(predicate, target):\r
+ """\r
+ >>> p = printer_sink()\r
+ >>> cf = cofilter(None, p)\r
+ >>> cf.send("")\r
+ >>> cf.send("Hello")\r
+ Hello\r
+ >>> cf.send([])\r
+ >>> cf.send([1, 2])\r
+ [1, 2]\r
+ >>> cf.send(False)\r
+ >>> cf.send(True)\r
+ True\r
+ >>> cf.send(0)\r
+ >>> cf.send(1)\r
+ 1\r
+ >>> # cf.throw(RuntimeError, "Goodbye")\r
+ >>> # cf.send(False)\r
+ >>> # cf.send(True)\r
+ >>> # cf.close()\r
+ """\r
+ if predicate is None:\r
+ predicate = bool\r
+\r
+ while True:\r
+ try:\r
+ item = yield\r
+ if predicate(item):\r
+ target.send(item)\r
+ except StandardError, e:\r
+ target.throw(e.__class__, e.message)\r
+\r
+\r
+@autostart\r
+def comap(function, target):\r
+ """\r
+ >>> p = printer_sink()\r
+ >>> cm = comap(lambda x: x+1, p)\r
+ >>> cm.send(0)\r
+ 1\r
+ >>> cm.send(1.0)\r
+ 2.0\r
+ >>> cm.send(-2)\r
+ -1\r
+ >>> # cm.throw(RuntimeError, "Goodbye")\r
+ >>> # cm.send(0)\r
+ >>> # cm.send(1.0)\r
+ >>> # cm.close()\r
+ """\r
+ while True:\r
+ try:\r
+ item = yield\r
+ mappedItem = function(item)\r
+ target.send(mappedItem)\r
+ except StandardError, e:\r
+ target.throw(e.__class__, e.message)\r
+\r
+\r
+def func_sink(function):\r
+ return comap(function, null_sink())\r
+\r
+\r
+def expand_positional(function):\r
+\r
+ @functools.wraps(function)\r
+ def expander(item):\r
+ return function(*item)\r
+\r
+ return expander\r
+\r
+\r
+@autostart\r
+def append_sink(l):\r
+ """\r
+ >>> l = []\r
+ >>> apps = append_sink(l)\r
+ >>> apps.send(1)\r
+ >>> apps.send(2)\r
+ >>> apps.send(3)\r
+ >>> print l\r
+ [1, 2, 3]\r
+ """\r
+ while True:\r
+ item = yield\r
+ l.append(item)\r
+\r
+\r
+@autostart\r
+def last_n_sink(l, n = 1):\r
+ """\r
+ >>> l = []\r
+ >>> lns = last_n_sink(l)\r
+ >>> lns.send(1)\r
+ >>> lns.send(2)\r
+ >>> lns.send(3)\r
+ >>> print l\r
+ [3]\r
+ """\r
+ del l[:]\r
+ while True:\r
+ item = yield\r
+ extraCount = len(l) - n + 1\r
+ if 0 < extraCount:\r
+ del l[0:extraCount]\r
+ l.append(item)\r
+\r
+\r
+@autostart\r
+def coreduce(target, function, initializer = None):\r
+ """\r
+ >>> reduceResult = []\r
+ >>> lns = last_n_sink(reduceResult)\r
+ >>> cr = coreduce(lns, lambda x, y: x + y, 0)\r
+ >>> cr.send(1)\r
+ >>> cr.send(2)\r
+ >>> cr.send(3)\r
+ >>> print reduceResult\r
+ [6]\r
+ >>> cr = coreduce(lns, lambda x, y: x + y)\r
+ >>> cr.send(1)\r
+ >>> cr.send(2)\r
+ >>> cr.send(3)\r
+ >>> print reduceResult\r
+ [6]\r
+ """\r
+ isFirst = True\r
+ cumulativeRef = initializer\r
+ while True:\r
+ item = yield\r
+ if isFirst and initializer is None:\r
+ cumulativeRef = item\r
+ else:\r
+ cumulativeRef = function(cumulativeRef, item)\r
+ target.send(cumulativeRef)\r
+ isFirst = False\r
+\r
+\r
+@autostart\r
+def cotee(targets):\r
+ """\r
+ Takes a sequence of coroutines and sends the received items to all of them\r
+\r
+ >>> ct = cotee((printer_sink("1 %s"), printer_sink("2 %s")))\r
+ >>> ct.send("Hello")\r
+ 1 Hello\r
+ 2 Hello\r
+ >>> ct.send("World")\r
+ 1 World\r
+ 2 World\r
+ >>> # ct.throw(RuntimeError, "Goodbye")\r
+ >>> # ct.send("Meh")\r
+ >>> # ct.close()\r
+ """\r
+ while True:\r
+ try:\r
+ item = yield\r
+ for target in targets:\r
+ target.send(item)\r
+ except StandardError, e:\r
+ for target in targets:\r
+ target.throw(e.__class__, e.message)\r
+\r
+\r
+class CoTee(object):\r
+ """\r
+ >>> ct = CoTee()\r
+ >>> ct.register_sink(printer_sink("1 %s"))\r
+ >>> ct.register_sink(printer_sink("2 %s"))\r
+ >>> ct.stage.send("Hello")\r
+ 1 Hello\r
+ 2 Hello\r
+ >>> ct.stage.send("World")\r
+ 1 World\r
+ 2 World\r
+ >>> ct.register_sink(printer_sink("3 %s"))\r
+ >>> ct.stage.send("Foo")\r
+ 1 Foo\r
+ 2 Foo\r
+ 3 Foo\r
+ >>> # ct.stage.throw(RuntimeError, "Goodbye")\r
+ >>> # ct.stage.send("Meh")\r
+ >>> # ct.stage.close()\r
+ """\r
+\r
+ def __init__(self):\r
+ self.stage = self._stage()\r
+ self._targets = []\r
+\r
+ def register_sink(self, sink):\r
+ self._targets.append(sink)\r
+\r
+ def unregister_sink(self, sink):\r
+ self._targets.remove(sink)\r
+\r
+ def restart(self):\r
+ self.stage = self._stage()\r
+\r
+ @autostart\r
+ def _stage(self):\r
+ while True:\r
+ try:\r
+ item = yield\r
+ for target in self._targets:\r
+ target.send(item)\r
+ except StandardError, e:\r
+ for target in self._targets:\r
+ target.throw(e.__class__, e.message)\r
+\r
+\r
+def _flush_queue(queue):\r
+ while not queue.empty():\r
+ yield queue.get()\r
+\r
+\r
+@autostart\r
+def cocount(target, start = 0):\r
+ """\r
+ >>> cc = cocount(printer_sink("%s"))\r
+ >>> cc.send("a")\r
+ 0\r
+ >>> cc.send(None)\r
+ 1\r
+ >>> cc.send([])\r
+ 2\r
+ >>> cc.send(0)\r
+ 3\r
+ """\r
+ for i in itertools.count(start):\r
+ item = yield\r
+ target.send(i)\r
+\r
+\r
+@autostart\r
+def coenumerate(target, start = 0):\r
+ """\r
+ >>> ce = coenumerate(printer_sink("%r"))\r
+ >>> ce.send("a")\r
+ (0, 'a')\r
+ >>> ce.send(None)\r
+ (1, None)\r
+ >>> ce.send([])\r
+ (2, [])\r
+ >>> ce.send(0)\r
+ (3, 0)\r
+ """\r
+ for i in itertools.count(start):\r
+ item = yield\r
+ decoratedItem = i, item\r
+ target.send(decoratedItem)\r
+\r
+\r
+@autostart\r
+def corepeat(target, elem):\r
+ """\r
+ >>> cr = corepeat(printer_sink("%s"), "Hello World")\r
+ >>> cr.send("a")\r
+ Hello World\r
+ >>> cr.send(None)\r
+ Hello World\r
+ >>> cr.send([])\r
+ Hello World\r
+ >>> cr.send(0)\r
+ Hello World\r
+ """\r
+ while True:\r
+ item = yield\r
+ target.send(elem)\r
+\r
+\r
+@autostart\r
+def cointercept(target, elems):\r
+ """\r
+ >>> cr = cointercept(printer_sink("%s"), [1, 2, 3, 4])\r
+ >>> cr.send("a")\r
+ 1\r
+ >>> cr.send(None)\r
+ 2\r
+ >>> cr.send([])\r
+ 3\r
+ >>> cr.send(0)\r
+ 4\r
+ >>> cr.send("Bye")\r
+ Traceback (most recent call last):\r
+ File "/usr/lib/python2.5/doctest.py", line 1228, in __run\r
+ compileflags, 1) in test.globs\r
+ File "<doctest __main__.cointercept[5]>", line 1, in <module>\r
+ cr.send("Bye")\r
+ StopIteration\r
+ """\r
+ item = yield\r
+ for elem in elems:\r
+ target.send(elem)\r
+ item = yield\r
+\r
+\r
+@autostart\r
+def codropwhile(target, pred):\r
+ """\r
+ >>> cdw = codropwhile(printer_sink("%s"), lambda x: x)\r
+ >>> cdw.send([0, 1, 2])\r
+ >>> cdw.send(1)\r
+ >>> cdw.send(True)\r
+ >>> cdw.send(False)\r
+ >>> cdw.send([0, 1, 2])\r
+ [0, 1, 2]\r
+ >>> cdw.send(1)\r
+ 1\r
+ >>> cdw.send(True)\r
+ True\r
+ """\r
+ while True:\r
+ item = yield\r
+ if not pred(item):\r
+ break\r
+\r
+ while True:\r
+ item = yield\r
+ target.send(item)\r
+\r
+\r
+@autostart\r
+def cotakewhile(target, pred):\r
+ """\r
+ >>> ctw = cotakewhile(printer_sink("%s"), lambda x: x)\r
+ >>> ctw.send([0, 1, 2])\r
+ [0, 1, 2]\r
+ >>> ctw.send(1)\r
+ 1\r
+ >>> ctw.send(True)\r
+ True\r
+ >>> ctw.send(False)\r
+ >>> ctw.send([0, 1, 2])\r
+ >>> ctw.send(1)\r
+ >>> ctw.send(True)\r
+ """\r
+ while True:\r
+ item = yield\r
+ if not pred(item):\r
+ break\r
+ target.send(item)\r
+\r
+ while True:\r
+ item = yield\r
+\r
+\r
+@autostart\r
+def coslice(target, lower, upper):\r
+ """\r
+ >>> cs = coslice(printer_sink("%r"), 3, 5)\r
+ >>> cs.send("0")\r
+ >>> cs.send("1")\r
+ >>> cs.send("2")\r
+ >>> cs.send("3")\r
+ '3'\r
+ >>> cs.send("4")\r
+ '4'\r
+ >>> cs.send("5")\r
+ >>> cs.send("6")\r
+ """\r
+ for i in xrange(lower):\r
+ item = yield\r
+ for i in xrange(upper - lower):\r
+ item = yield\r
+ target.send(item)\r
+ while True:\r
+ item = yield\r
+\r
+\r
+@autostart\r
+def cochain(targets):\r
+ """\r
+ >>> cr = cointercept(printer_sink("good %s"), [1, 2, 3, 4])\r
+ >>> cc = cochain([cr, printer_sink("end %s")])\r
+ >>> cc.send("a")\r
+ good 1\r
+ >>> cc.send(None)\r
+ good 2\r
+ >>> cc.send([])\r
+ good 3\r
+ >>> cc.send(0)\r
+ good 4\r
+ >>> cc.send("Bye")\r
+ end Bye\r
+ """\r
+ behind = []\r
+ for target in targets:\r
+ try:\r
+ while behind:\r
+ item = behind.pop()\r
+ target.send(item)\r
+ while True:\r
+ item = yield\r
+ target.send(item)\r
+ except StopIteration:\r
+ behind.append(item)\r
+\r
+\r
+@autostart\r
+def queue_sink(queue):\r
+ """\r
+ >>> q = Queue.Queue()\r
+ >>> qs = queue_sink(q)\r
+ >>> qs.send("Hello")\r
+ >>> qs.send("World")\r
+ >>> qs.throw(RuntimeError, "Goodbye")\r
+ >>> qs.send("Meh")\r
+ >>> qs.close()\r
+ >>> print [i for i in _flush_queue(q)]\r
+ [(None, 'Hello'), (None, 'World'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]\r
+ """\r
+ while True:\r
+ try:\r
+ item = yield\r
+ queue.put((None, item))\r
+ except StandardError, e:\r
+ queue.put((e.__class__, e.message))\r
+ except GeneratorExit:\r
+ queue.put((GeneratorExit, None))\r
+ raise\r
+\r
+\r
+def decode_item(item, target):\r
+ if item[0] is None:\r
+ target.send(item[1])\r
+ return False\r
+ elif item[0] is GeneratorExit:\r
+ target.close()\r
+ return True\r
+ else:\r
+ target.throw(item[0], item[1])\r
+ return False\r
+\r
+\r
+def queue_source(queue, target):\r
+ """\r
+ >>> q = Queue.Queue()\r
+ >>> for i in [\r
+ ... (None, 'Hello'),\r
+ ... (None, 'World'),\r
+ ... (GeneratorExit, None),\r
+ ... ]:\r
+ ... q.put(i)\r
+ >>> qs = queue_source(q, printer_sink())\r
+ Hello\r
+ World\r
+ """\r
+ isDone = False\r
+ while not isDone:\r
+ item = queue.get()\r
+ isDone = decode_item(item, target)\r
+\r
+\r
+def threaded_stage(target, thread_factory = threading.Thread):\r
+ messages = Queue.Queue()\r
+\r
+ run_source = functools.partial(queue_source, messages, target)\r
+ thread_factory(target=run_source).start()\r
+\r
+ # Sink running in current thread\r
+ return functools.partial(queue_sink, messages)\r
+\r
+\r
+@autostart\r
+def pickle_sink(f):\r
+ while True:\r
+ try:\r
+ item = yield\r
+ pickle.dump((None, item), f)\r
+ except StandardError, e:\r
+ pickle.dump((e.__class__, e.message), f)\r
+ except GeneratorExit:\r
+ pickle.dump((GeneratorExit, ), f)\r
+ raise\r
+ except StopIteration:\r
+ f.close()\r
+ return\r
+\r
+\r
+def pickle_source(f, target):\r
+ try:\r
+ isDone = False\r
+ while not isDone:\r
+ item = pickle.load(f)\r
+ isDone = decode_item(item, target)\r
+ except EOFError:\r
+ target.close()\r
+\r
+\r
+class EventHandler(object, xml.sax.ContentHandler):\r
+\r
+ START = "start"\r
+ TEXT = "text"\r
+ END = "end"\r
+\r
+ def __init__(self, target):\r
+ object.__init__(self)\r
+ xml.sax.ContentHandler.__init__(self)\r
+ self._target = target\r
+\r
+ def startElement(self, name, attrs):\r
+ self._target.send((self.START, (name, attrs._attrs)))\r
+\r
+ def characters(self, text):\r
+ self._target.send((self.TEXT, text))\r
+\r
+ def endElement(self, name):\r
+ self._target.send((self.END, name))\r
+\r
+\r
+def expat_parse(f, target):\r
+ parser = xml.parsers.expat.ParserCreate()\r
+ parser.buffer_size = 65536\r
+ parser.buffer_text = True\r
+ parser.returns_unicode = False\r
+ parser.StartElementHandler = lambda name, attrs: target.send(('start', (name, attrs)))\r
+ parser.EndElementHandler = lambda name: target.send(('end', name))\r
+ parser.CharacterDataHandler = lambda data: target.send(('text', data))\r
+ parser.ParseFile(f)\r
+\r
+\r
+if __name__ == "__main__":\r
+ import doctest\r
+ doctest.testmod()\r
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import time
+import functools
+import threading
+import Queue
+import logging
+
+import gobject
+
+import algorithms
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+def make_idler(func):
+ """
+ Decorator that makes a generator-function into a function that will continue execution on next call
+ """
+ a = []
+
+ @functools.wraps(func)
+ def decorated_func(*args, **kwds):
+ if not a:
+ a.append(func(*args, **kwds))
+ try:
+ a[0].next()
+ return True
+ except StopIteration:
+ del a[:]
+ return False
+
+ return decorated_func
+
+
+def async(func):
+ """
+ Make a function mainloop friendly. the function will be called at the
+ next mainloop idle state.
+
+ >>> import misc
+ >>> misc.validate_decorator(async)
+ """
+
+ @functools.wraps(func)
+ def new_function(*args, **kwargs):
+
+ def async_function():
+ func(*args, **kwargs)
+ return False
+
+ gobject.idle_add(async_function)
+
+ return new_function
+
+
+class Async(object):
+
+ def __init__(self, func, once = True):
+ self.__func = func
+ self.__idleId = None
+ self.__once = once
+
+ def start(self):
+ assert self.__idleId is None
+ if self.__once:
+ self.__idleId = gobject.idle_add(self._on_once)
+ else:
+ self.__idleId = gobject.idle_add(self.__func)
+
+ def is_running(self):
+ return self.__idleId is not None
+
+ def cancel(self):
+ if self.__idleId is not None:
+ gobject.source_remove(self.__idleId)
+ self.__idleId = None
+
+ def __call__(self):
+ return self.start()
+
+ @misc.log_exception(_moduleLogger)
+ def _on_once(self):
+ self.cancel()
+ try:
+ self.__func()
+ except Exception:
+ pass
+ return False
+
+
+class Timeout(object):
+
+ def __init__(self, func, once = True):
+ self.__func = func
+ self.__timeoutId = None
+ self.__once = once
+
+ def start(self, **kwds):
+ assert self.__timeoutId is None
+
+ callback = self._on_once if self.__once else self.__func
+
+ assert len(kwds) == 1
+ timeoutInSeconds = kwds["seconds"]
+ assert 0 <= timeoutInSeconds
+
+ if timeoutInSeconds == 0:
+ self.__timeoutId = gobject.idle_add(callback)
+ else:
+ self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback)
+
+ def is_running(self):
+ return self.__timeoutId is not None
+
+ def cancel(self):
+ if self.__timeoutId is not None:
+ gobject.source_remove(self.__timeoutId)
+ self.__timeoutId = None
+
+ def __call__(self, **kwds):
+ return self.start(**kwds)
+
+ @misc.log_exception(_moduleLogger)
+ def _on_once(self):
+ self.cancel()
+ try:
+ self.__func()
+ except Exception:
+ pass
+ return False
+
+
+_QUEUE_EMPTY = object()
+
+
+class FutureThread(object):
+
+ def __init__(self):
+ self.__workQueue = Queue.Queue()
+ self.__thread = threading.Thread(
+ name = type(self).__name__,
+ target = self.__consume_queue,
+ )
+ self.__isRunning = True
+
+ def start(self):
+ self.__thread.start()
+
+ def stop(self):
+ self.__isRunning = False
+ for _ in algorithms.itr_available(self.__workQueue):
+ pass # eat up queue to cut down dumb work
+ self.__workQueue.put(_QUEUE_EMPTY)
+
+ def clear_tasks(self):
+ for _ in algorithms.itr_available(self.__workQueue):
+ pass # eat up queue to cut down dumb work
+
+ def add_task(self, func, args, kwds, on_success, on_error):
+ task = func, args, kwds, on_success, on_error
+ self.__workQueue.put(task)
+
+ @misc.log_exception(_moduleLogger)
+ def __trampoline_callback(self, on_success, on_error, isError, result):
+ if not self.__isRunning:
+ if isError:
+ _moduleLogger.error("Masking: %s" % (result, ))
+ isError = True
+ result = StopIteration("Cancelling all callbacks")
+ callback = on_success if not isError else on_error
+ try:
+ callback(result)
+ except Exception:
+ _moduleLogger.exception("Callback errored")
+ return False
+
+ @misc.log_exception(_moduleLogger)
+ def __consume_queue(self):
+ while True:
+ task = self.__workQueue.get()
+ if task is _QUEUE_EMPTY:
+ break
+ func, args, kwds, on_success, on_error = task
+
+ try:
+ result = func(*args, **kwds)
+ isError = False
+ except Exception, e:
+ _moduleLogger.error("Error, passing it back to the main thread")
+ result = e
+ isError = True
+ self.__workQueue.task_done()
+
+ gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result)
+ _moduleLogger.debug("Shutting down worker thread")
+
+
+class AutoSignal(object):
+
+ def __init__(self, toplevel):
+ self.__disconnectPool = []
+ toplevel.connect("destroy", self.__on_destroy)
+
+ def connect_auto(self, widget, *args):
+ id = widget.connect(*args)
+ self.__disconnectPool.append((widget, id))
+
+ @misc.log_exception(_moduleLogger)
+ def __on_destroy(self, widget):
+ _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool)))
+ for widget, id in self.__disconnectPool:
+ widget.disconnect(id)
+ del self.__disconnectPool[:]
+
+
+def throttled(minDelay, queue):
+ """
+ Throttle the calls to a function by queueing all the calls that happen
+ before the minimum delay
+
+ >>> import misc
+ >>> import Queue
+ >>> misc.validate_decorator(throttled(0, Queue.Queue()))
+ """
+
+ def actual_decorator(func):
+
+ lastCallTime = [None]
+
+ def process_queue():
+ if 0 < len(queue):
+ func, args, kwargs = queue.pop(0)
+ lastCallTime[0] = time.time() * 1000
+ func(*args, **kwargs)
+ return False
+
+ @functools.wraps(func)
+ def new_function(*args, **kwargs):
+ now = time.time() * 1000
+ if (
+ lastCallTime[0] is None or
+ (now - lastCallTime >= minDelay)
+ ):
+ lastCallTime[0] = now
+ func(*args, **kwargs)
+ else:
+ queue.append((func, args, kwargs))
+ lastCallDelta = now - lastCallTime[0]
+ processQueueTimeout = int(minDelay * len(queue) - lastCallDelta)
+ gobject.timeout_add(processQueueTimeout, process_queue)
+
+ return new_function
+
+ return actual_decorator
+
+
+def _old_timeout_add_seconds(timeout, callback):
+ return gobject.timeout_add(timeout * 1000, callback)
+
+
+def _timeout_add_seconds(timeout, callback):
+ return gobject.timeout_add_seconds(timeout, callback)
+
+
+try:
+ gobject.timeout_add_seconds
+ timeout_add_seconds = _timeout_add_seconds
+except AttributeError:
+ timeout_add_seconds = _old_timeout_add_seconds
--- /dev/null
+#!/usr/bin/env python
+
+
+from __future__ import with_statement
+
+import os
+import pickle
+import contextlib
+import itertools
+import codecs
+from xml.sax import saxutils
+import csv
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+
+@contextlib.contextmanager
+def change_directory(directory):
+ previousDirectory = os.getcwd()
+ os.chdir(directory)
+ currentDirectory = os.getcwd()
+
+ try:
+ yield previousDirectory, currentDirectory
+ finally:
+ os.chdir(previousDirectory)
+
+
+@contextlib.contextmanager
+def pickled(filename):
+ """
+ Here is an example usage:
+ with pickled("foo.db") as p:
+ p("users", list).append(["srid", "passwd", 23])
+ """
+
+ if os.path.isfile(filename):
+ data = pickle.load(open(filename))
+ else:
+ data = {}
+
+ def getter(item, factory):
+ if item in data:
+ return data[item]
+ else:
+ data[item] = factory()
+ return data[item]
+
+ yield getter
+
+ pickle.dump(data, open(filename, "w"))
+
+
+@contextlib.contextmanager
+def redirect(object_, attr, value):
+ """
+ >>> import sys
+ ... with redirect(sys, 'stdout', open('stdout', 'w')):
+ ... print "hello"
+ ...
+ >>> print "we're back"
+ we're back
+ """
+ orig = getattr(object_, attr)
+ setattr(object_, attr, value)
+ try:
+ yield
+ finally:
+ setattr(object_, attr, orig)
+
+
+def pathsplit(path):
+ """
+ >>> pathsplit("/a/b/c")
+ ['', 'a', 'b', 'c']
+ >>> pathsplit("./plugins/builtins.ini")
+ ['.', 'plugins', 'builtins.ini']
+ """
+ pathParts = path.split(os.path.sep)
+ return pathParts
+
+
+def commonpath(l1, l2, common=None):
+ """
+ >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1'))
+ (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1'])
+ >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini"))
+ (['.', 'plugins'], [''], ['builtins.ini'])
+ >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins"))
+ (['.', 'plugins'], ['builtins'], [])
+ """
+ if common is None:
+ common = []
+
+ if l1 == l2:
+ return l1, [], []
+
+ for i, (leftDir, rightDir) in enumerate(zip(l1, l2)):
+ if leftDir != rightDir:
+ return l1[0:i], l1[i:], l2[i:]
+ else:
+ if leftDir == rightDir:
+ i += 1
+ return l1[0:i], l1[i:], l2[i:]
+
+
+def relpath(p1, p2):
+ """
+ >>> relpath('/', '/')
+ './'
+ >>> relpath('/a/b/c/d', '/')
+ '../../../../'
+ >>> relpath('/a/b/c/d', '/a/b/c1/d1')
+ '../../c1/d1'
+ >>> relpath('/a/b/c/d', '/a/b/c1/d1/')
+ '../../c1/d1'
+ >>> relpath("./plugins/builtins", "./plugins")
+ '../'
+ >>> relpath("./plugins/", "./plugins/builtins.ini")
+ 'builtins.ini'
+ """
+ sourcePath = os.path.normpath(p1)
+ destPath = os.path.normpath(p2)
+
+ (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath))
+ if len(sourceOnly) or len(destOnly):
+ relParts = itertools.chain(
+ (('..' + os.sep) * len(sourceOnly), ),
+ destOnly,
+ )
+ return os.path.join(*relParts)
+ else:
+ return "."+os.sep
+
+
+class UTF8Recoder(object):
+ """
+ Iterator that reads an encoded stream and reencodes the input to UTF-8
+ """
+ def __init__(self, f, encoding):
+ self.reader = codecs.getreader(encoding)(f)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.reader.next().encode("utf-8")
+
+
+class UnicodeReader(object):
+ """
+ A CSV reader which will iterate over lines in the CSV file "f",
+ which is encoded in the given encoding.
+ """
+
+ def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+ f = UTF8Recoder(f, encoding)
+ self.reader = csv.reader(f, dialect=dialect, **kwds)
+
+ def next(self):
+ row = self.reader.next()
+ return [unicode(s, "utf-8") for s in row]
+
+ def __iter__(self):
+ return self
+
+class UnicodeWriter(object):
+ """
+ A CSV writer which will write rows to CSV file "f",
+ which is encoded in the given encoding.
+ """
+
+ def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+ # Redirect output to a queue
+ self.queue = StringIO.StringIO()
+ self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
+ self.stream = f
+ self.encoder = codecs.getincrementalencoder(encoding)()
+
+ def writerow(self, row):
+ self.writer.writerow([s.encode("utf-8") for s in row])
+ # Fetch UTF-8 output from the queue ...
+ data = self.queue.getvalue()
+ data = data.decode("utf-8")
+ # ... and reencode it into the target encoding
+ data = self.encoder.encode(data)
+ # write to the target stream
+ self.stream.write(data)
+ # empty queue
+ self.queue.truncate(0)
+
+ def writerows(self, rows):
+ for row in rows:
+ self.writerow(row)
+
+
+def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
+ # csv.py doesn't do Unicode; encode temporarily as UTF-8:
+ csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
+ dialect=dialect, **kwargs)
+ for row in csv_reader:
+ # decode UTF-8 back to Unicode, cell by cell:
+ yield [unicode(cell, 'utf-8') for cell in row]
+
+
+def utf_8_encoder(unicode_csv_data):
+ for line in unicode_csv_data:
+ yield line.encode('utf-8')
+
+
+_UNESCAPE_ENTITIES = {
+ """: '"',
+ " ": " ",
+ "'": "'",
+}
+
+
+_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys()))
+del _ESCAPE_ENTITIES[" "]
+
+
+def unescape(text):
+ plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+ return plain
+
+
+def escape(text):
+ fancy = saxutils.escape(text, _ESCAPE_ENTITIES)
+ return fancy
--- /dev/null
+#!/usr/bin/env python
+
+
+import os
+import logging
+
+try:
+ from xdg import BaseDirectory as _BaseDirectory
+ BaseDirectory = _BaseDirectory
+except ImportError:
+ BaseDirectory = None
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_libc = None
+
+
+def set_process_name(name):
+ try: # change process name for killall
+ global _libc
+ if _libc is None:
+ import ctypes
+ _libc = ctypes.CDLL('libc.so.6')
+ _libc.prctl(15, name, 0, 0, 0)
+ except Exception, e:
+ _moduleLogger.warning('Unable to set processName: %s" % e')
+
+
+def get_new_resource(resourceType, resource, name):
+ if BaseDirectory is not None:
+ if resourceType == "data":
+ base = BaseDirectory.xdg_data_home
+ if base == "/usr/share/mime":
+ # Ugly hack because somehow Maemo 4.1 seems to be set to this
+ base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+ elif resourceType == "config":
+ base = BaseDirectory.xdg_config_home
+ elif resourceType == "cache":
+ base = BaseDirectory.xdg_cache_home
+ else:
+ raise RuntimeError("Unknown type: "+resourceType)
+ else:
+ base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+
+ filePath = os.path.join(base, resource, name)
+ dirPath = os.path.dirname(filePath)
+ if not os.path.exists(dirPath):
+ # Looking before I leap to not mask errors
+ os.makedirs(dirPath)
+
+ return filePath
+
+
+def get_existing_resource(resourceType, resource, name):
+ if BaseDirectory is not None:
+ if resourceType == "data":
+ base = BaseDirectory.xdg_data_home
+ elif resourceType == "config":
+ base = BaseDirectory.xdg_config_home
+ elif resourceType == "cache":
+ base = BaseDirectory.xdg_cache_home
+ else:
+ raise RuntimeError("Unknown type: "+resourceType)
+ else:
+ base = None
+
+ if base is not None:
+ finalPath = os.path.join(base, name)
+ if os.path.exists(finalPath):
+ return finalPath
+
+ altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+ finalPath = os.path.join(altBase, name)
+ if os.path.exists(finalPath):
+ return finalPath
+ else:
+ raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), ))
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import re
+import cPickle
+
+import functools
+import contextlib
+import inspect
+
+import optparse
+import traceback
+import warnings
+import string
+
+
+class AnyData(object):
+
+ pass
+
+
+_indentationLevel = [0]
+
+
+def log_call(logger):
+
+ def log_call_decorator(func):
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwds):
+ logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, ))
+ _indentationLevel[0] += 1
+ try:
+ return func(*args, **kwds)
+ finally:
+ _indentationLevel[0] -= 1
+ logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, ))
+
+ return wrapper
+
+ return log_call_decorator
+
+
+def log_exception(logger):
+
+ def log_exception_decorator(func):
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwds):
+ try:
+ return func(*args, **kwds)
+ except Exception:
+ logger.exception(func.__name__)
+ raise
+
+ return wrapper
+
+ return log_exception_decorator
+
+
+def printfmt(template):
+ """
+ This hides having to create the Template object and call substitute/safe_substitute on it. For example:
+
+ >>> num = 10
+ >>> word = "spam"
+ >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
+ I would like to order 10 units of spam, please
+ """
+ frame = inspect.stack()[-1][0]
+ try:
+ print string.Template(template).safe_substitute(frame.f_locals)
+ finally:
+ del frame
+
+
+def is_special(name):
+ return name.startswith("__") and name.endswith("__")
+
+
+def is_private(name):
+ return name.startswith("_") and not is_special(name)
+
+
+def privatize(clsName, attributeName):
+ """
+ At runtime, make an attributeName private
+
+ Example:
+ >>> class Test(object):
+ ... pass
+ ...
+ >>> try:
+ ... dir(Test).index("_Test__me")
+ ... print dir(Test)
+ ... except:
+ ... print "Not Found"
+ Not Found
+ >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
+ >>> try:
+ ... dir(Test).index("_Test__me")
+ ... print "Found"
+ ... except:
+ ... print dir(Test)
+ 0
+ Found
+ >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+ Hello World
+ >>>
+ >>> is_private(privatize(Test.__name__, "me"))
+ True
+ >>> is_special(privatize(Test.__name__, "me"))
+ False
+ """
+ return "".join(["_", clsName, "__", attributeName])
+
+
+def obfuscate(clsName, attributeName):
+ """
+ At runtime, turn a private name into the obfuscated form
+
+ Example:
+ >>> class Test(object):
+ ... __me = "Hello World"
+ ...
+ >>> try:
+ ... dir(Test).index("_Test__me")
+ ... print "Found"
+ ... except:
+ ... print dir(Test)
+ 0
+ Found
+ >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+ Hello World
+ >>> is_private(obfuscate(Test.__name__, "__me"))
+ True
+ >>> is_special(obfuscate(Test.__name__, "__me"))
+ False
+ """
+ return "".join(["_", clsName, attributeName])
+
+
+class PAOptionParser(optparse.OptionParser, object):
+ """
+ >>> if __name__ == '__main__':
+ ... #parser = PAOptionParser("My usage str")
+ ... parser = PAOptionParser()
+ ... parser.add_posarg("Foo", help="Foo usage")
+ ... parser.add_posarg("Bar", dest="bar_dest")
+ ... parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
+ ... parser.add_option('--stocksym', dest='symbol')
+ ... values, args = parser.parse_args()
+ ... print values, args
+ ...
+
+ python mycp.py -h
+ python mycp.py
+ python mycp.py foo
+ python mycp.py foo bar
+
+ python mycp.py foo bar lava
+ Usage: pa.py <Foo> <Bar> <Language> [options]
+
+ Positional Arguments:
+ Foo: Foo usage
+ Bar:
+ Language:
+
+ pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other'
+ """
+
+ def __init__(self, *args, **kw):
+ self.posargs = []
+ super(PAOptionParser, self).__init__(*args, **kw)
+
+ def add_posarg(self, *args, **kw):
+ pa_help = kw.get("help", "")
+ kw["help"] = optparse.SUPPRESS_HELP
+ o = self.add_option("--%s" % args[0], *args[1:], **kw)
+ self.posargs.append((args[0], pa_help))
+
+ def get_usage(self, *args, **kwargs):
+ params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs]))
+ self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params
+ return super(PAOptionParser, self).get_usage(*args, **kwargs)
+
+ def parse_args(self, *args, **kwargs):
+ args = sys.argv[1:]
+ args0 = []
+ for p, v in zip(self.posargs, args):
+ args0.append("--%s" % p[0])
+ args0.append(v)
+ args = args0 + args
+ options, args = super(PAOptionParser, self).parse_args(args, **kwargs)
+ if len(args) < len(self.posargs):
+ msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):])
+ self.error(msg)
+ return options, args
+
+
+def explicitly(name, stackadd=0):
+ """
+ This is an alias for adding to '__all__'. Less error-prone than using
+ __all__ itself, since setting __all__ directly is prone to stomping on
+ things implicitly exported via L{alias}.
+
+ @note Taken from PyExport (which could turn out pretty cool):
+ @li @a http://codebrowse.launchpad.net/~glyph/
+ @li @a http://glyf.livejournal.com/74356.html
+ """
+ packageVars = sys._getframe(1+stackadd).f_locals
+ globalAll = packageVars.setdefault('__all__', [])
+ globalAll.append(name)
+
+
+def public(thunk):
+ """
+ This is a decorator, for convenience. Rather than typing the name of your
+ function twice, you can decorate a function with this.
+
+ To be real, @public would need to work on methods as well, which gets into
+ supporting types...
+
+ @note Taken from PyExport (which could turn out pretty cool):
+ @li @a http://codebrowse.launchpad.net/~glyph/
+ @li @a http://glyf.livejournal.com/74356.html
+ """
+ explicitly(thunk.__name__, 1)
+ return thunk
+
+
+def _append_docstring(obj, message):
+ if obj.__doc__ is None:
+ obj.__doc__ = message
+ else:
+ obj.__doc__ += message
+
+
+def validate_decorator(decorator):
+
+ def simple(x):
+ return x
+
+ f = simple
+ f.__name__ = "name"
+ f.__doc__ = "doc"
+ f.__dict__["member"] = True
+
+ g = decorator(f)
+
+ if f.__name__ != g.__name__:
+ print f.__name__, "!=", g.__name__
+
+ if g.__doc__ is None:
+ print decorator.__name__, "has no doc string"
+ elif not g.__doc__.startswith(f.__doc__):
+ print g.__doc__, "didn't start with", f.__doc__
+
+ if not ("member" in g.__dict__ and g.__dict__["member"]):
+ print "'member' not in ", g.__dict__
+
+
+def deprecated_api(func):
+ """
+ This is a decorator which can be used to mark functions
+ as deprecated. It will result in a warning being emitted
+ when the function is used.
+
+ >>> validate_decorator(deprecated_api)
+ """
+
+ @functools.wraps(func)
+ def newFunc(*args, **kwargs):
+ warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning)
+ return func(*args, **kwargs)
+
+ _append_docstring(newFunc, "\n@deprecated")
+ return newFunc
+
+
+def unstable_api(func):
+ """
+ This is a decorator which can be used to mark functions
+ as deprecated. It will result in a warning being emitted
+ when the function is used.
+
+ >>> validate_decorator(unstable_api)
+ """
+
+ @functools.wraps(func)
+ def newFunc(*args, **kwargs):
+ warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning)
+ return func(*args, **kwargs)
+ _append_docstring(newFunc, "\n@unstable")
+ return newFunc
+
+
+def enabled(func):
+ """
+ This decorator doesn't add any behavior
+
+ >>> validate_decorator(enabled)
+ """
+ return func
+
+
+def disabled(func):
+ """
+ This decorator disables the provided function, and does nothing
+
+ >>> validate_decorator(disabled)
+ """
+
+ @functools.wraps(func)
+ def emptyFunc(*args, **kargs):
+ pass
+ _append_docstring(emptyFunc, "\n@note Temporarily Disabled")
+ return emptyFunc
+
+
+def metadata(document=True, **kwds):
+ """
+ >>> validate_decorator(metadata(author="Ed"))
+ """
+
+ def decorate(func):
+ for k, v in kwds.iteritems():
+ setattr(func, k, v)
+ if document:
+ _append_docstring(func, "\n@"+k+" "+v)
+ return func
+ return decorate
+
+
+def prop(func):
+ """Function decorator for defining property attributes
+
+ The decorated function is expected to return a dictionary
+ containing one or more of the following pairs:
+ fget - function for getting attribute value
+ fset - function for setting attribute value
+ fdel - function for deleting attribute
+ This can be conveniently constructed by the locals() builtin
+ function; see:
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
+ @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html
+
+ Example:
+ >>> #Due to transformation from function to property, does not need to be validated
+ >>> #validate_decorator(prop)
+ >>> class MyExampleClass(object):
+ ... @prop
+ ... def foo():
+ ... "The foo property attribute's doc-string"
+ ... def fget(self):
+ ... print "GET"
+ ... return self._foo
+ ... def fset(self, value):
+ ... print "SET"
+ ... self._foo = value
+ ... return locals()
+ ...
+ >>> me = MyExampleClass()
+ >>> me.foo = 10
+ SET
+ >>> print me.foo
+ GET
+ 10
+ """
+ return property(doc=func.__doc__, **func())
+
+
+def print_handler(e):
+ """
+ @see ExpHandler
+ """
+ print "%s: %s" % (type(e).__name__, e)
+
+
+def print_ignore(e):
+ """
+ @see ExpHandler
+ """
+ print 'Ignoring %s exception: %s' % (type(e).__name__, e)
+
+
+def print_traceback(e):
+ """
+ @see ExpHandler
+ """
+ #print sys.exc_info()
+ traceback.print_exc(file=sys.stdout)
+
+
+def ExpHandler(handler = print_handler, *exceptions):
+ """
+ An exception handling idiom using decorators
+ Examples
+ Specify exceptions in order, first one is handled first
+ last one last.
+
+ >>> validate_decorator(ExpHandler())
+ >>> @ExpHandler(print_ignore, ZeroDivisionError)
+ ... @ExpHandler(None, AttributeError, ValueError)
+ ... def f1():
+ ... 1/0
+ >>> @ExpHandler(print_traceback, ZeroDivisionError)
+ ... def f2():
+ ... 1/0
+ >>> @ExpHandler()
+ ... def f3(*pargs):
+ ... l = pargs
+ ... return l[10]
+ >>> @ExpHandler(print_traceback, ZeroDivisionError)
+ ... def f4():
+ ... return 1
+ >>>
+ >>>
+ >>> f1()
+ Ignoring ZeroDivisionError exception: integer division or modulo by zero
+ >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ ZeroDivisionError: integer division or modulo by zero
+ >>> f3()
+ IndexError: tuple index out of range
+ >>> f4()
+ 1
+ """
+
+ def wrapper(f):
+ localExceptions = exceptions
+ if not localExceptions:
+ localExceptions = [Exception]
+ t = [(ex, handler) for ex in localExceptions]
+ t.reverse()
+
+ def newfunc(t, *args, **kwargs):
+ ex, handler = t[0]
+ try:
+ if len(t) == 1:
+ return f(*args, **kwargs)
+ else:
+ #Recurse for embedded try/excepts
+ dec_func = functools.partial(newfunc, t[1:])
+ dec_func = functools.update_wrapper(dec_func, f)
+ return dec_func(*args, **kwargs)
+ except ex, e:
+ return handler(e)
+
+ dec_func = functools.partial(newfunc, t)
+ dec_func = functools.update_wrapper(dec_func, f)
+ return dec_func
+ return wrapper
+
+
+def into_debugger(func):
+ """
+ >>> validate_decorator(into_debugger)
+ """
+
+ @functools.wraps(func)
+ def newFunc(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except:
+ import pdb
+ pdb.post_mortem()
+
+ return newFunc
+
+
+class bindclass(object):
+ """
+ >>> validate_decorator(bindclass)
+ >>> class Foo(BoundObject):
+ ... @bindclass
+ ... def foo(this_class, self):
+ ... return this_class, self
+ ...
+ >>> class Bar(Foo):
+ ... @bindclass
+ ... def bar(this_class, self):
+ ... return this_class, self
+ ...
+ >>> f = Foo()
+ >>> b = Bar()
+ >>>
+ >>> f.foo() # doctest: +ELLIPSIS
+ (<class '...Foo'>, <...Foo object at ...>)
+ >>> b.foo() # doctest: +ELLIPSIS
+ (<class '...Foo'>, <...Bar object at ...>)
+ >>> b.bar() # doctest: +ELLIPSIS
+ (<class '...Bar'>, <...Bar object at ...>)
+ """
+
+ def __init__(self, f):
+ self.f = f
+ self.__name__ = f.__name__
+ self.__doc__ = f.__doc__
+ self.__dict__.update(f.__dict__)
+ self.m = None
+
+ def bind(self, cls, attr):
+
+ def bound_m(*args, **kwargs):
+ return self.f(cls, *args, **kwargs)
+ bound_m.__name__ = attr
+ self.m = bound_m
+
+ def __get__(self, obj, objtype=None):
+ return self.m.__get__(obj, objtype)
+
+
+class ClassBindingSupport(type):
+ "@see bindclass"
+
+ def __init__(mcs, name, bases, attrs):
+ type.__init__(mcs, name, bases, attrs)
+ for attr, val in attrs.iteritems():
+ if isinstance(val, bindclass):
+ val.bind(mcs, attr)
+
+
+class BoundObject(object):
+ "@see bindclass"
+ __metaclass__ = ClassBindingSupport
+
+
+def bindfunction(f):
+ """
+ >>> validate_decorator(bindfunction)
+ >>> @bindfunction
+ ... def factorial(thisfunction, n):
+ ... # Within this function the name 'thisfunction' refers to the factorial
+ ... # function(with only one argument), even after 'factorial' is bound
+ ... # to another object
+ ... if n > 0:
+ ... return n * thisfunction(n - 1)
+ ... else:
+ ... return 1
+ ...
+ >>> factorial(3)
+ 6
+ """
+
+ @functools.wraps(f)
+ def bound_f(*args, **kwargs):
+ return f(bound_f, *args, **kwargs)
+ return bound_f
+
+
+class Memoize(object):
+ """
+ Memoize(fn) - an instance which acts like fn but memoizes its arguments
+ Will only work on functions with non-mutable arguments
+ @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
+
+ >>> validate_decorator(Memoize)
+ """
+
+ def __init__(self, fn):
+ self.fn = fn
+ self.__name__ = fn.__name__
+ self.__doc__ = fn.__doc__
+ self.__dict__.update(fn.__dict__)
+ self.memo = {}
+
+ def __call__(self, *args):
+ if args not in self.memo:
+ self.memo[args] = self.fn(*args)
+ return self.memo[args]
+
+
+class MemoizeMutable(object):
+ """Memoize(fn) - an instance which acts like fn but memoizes its arguments
+ Will work on functions with mutable arguments(slower than Memoize)
+ @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
+
+ >>> validate_decorator(MemoizeMutable)
+ """
+
+ def __init__(self, fn):
+ self.fn = fn
+ self.__name__ = fn.__name__
+ self.__doc__ = fn.__doc__
+ self.__dict__.update(fn.__dict__)
+ self.memo = {}
+
+ def __call__(self, *args, **kw):
+ text = cPickle.dumps((args, kw))
+ if text not in self.memo:
+ self.memo[text] = self.fn(*args, **kw)
+ return self.memo[text]
+
+
+callTraceIndentationLevel = 0
+
+
+def call_trace(f):
+ """
+ Synchronization decorator.
+
+ >>> validate_decorator(call_trace)
+ >>> @call_trace
+ ... def a(a, b, c):
+ ... pass
+ >>> a(1, 2, c=3)
+ Entering a((1, 2), {'c': 3})
+ Exiting a((1, 2), {'c': 3})
+ """
+
+ @functools.wraps(f)
+ def verboseTrace(*args, **kw):
+ global callTraceIndentationLevel
+
+ print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+ callTraceIndentationLevel += 1
+ try:
+ result = f(*args, **kw)
+ except:
+ callTraceIndentationLevel -= 1
+ print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+ raise
+ callTraceIndentationLevel -= 1
+ print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+ return result
+
+ @functools.wraps(f)
+ def smallTrace(*args, **kw):
+ global callTraceIndentationLevel
+
+ print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+ callTraceIndentationLevel += 1
+ try:
+ result = f(*args, **kw)
+ except:
+ callTraceIndentationLevel -= 1
+ print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+ raise
+ callTraceIndentationLevel -= 1
+ print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+ return result
+
+ #return smallTrace
+ return verboseTrace
+
+
+@contextlib.contextmanager
+def nested_break():
+ """
+ >>> with nested_break() as mylabel:
+ ... for i in xrange(3):
+ ... print "Outer", i
+ ... for j in xrange(3):
+ ... if i == 2: raise mylabel
+ ... if j == 2: break
+ ... print "Inner", j
+ ... print "more processing"
+ Outer 0
+ Inner 0
+ Inner 1
+ Outer 1
+ Inner 0
+ Inner 1
+ Outer 2
+ """
+
+ class NestedBreakException(Exception):
+ pass
+
+ try:
+ yield NestedBreakException
+ except NestedBreakException:
+ pass
+
+
+@contextlib.contextmanager
+def lexical_scope(*args):
+ """
+ @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586
+ Example:
+ >>> b = 0
+ >>> with lexical_scope(1) as (a):
+ ... print a
+ ...
+ 1
+ >>> with lexical_scope(1,2,3) as (a,b,c):
+ ... print a,b,c
+ ...
+ 1 2 3
+ >>> with lexical_scope():
+ ... d = 10
+ ... def foo():
+ ... pass
+ ...
+ >>> print b
+ 2
+ """
+
+ frame = inspect.currentframe().f_back.f_back
+ saved = frame.f_locals.keys()
+ try:
+ if not args:
+ yield
+ elif len(args) == 1:
+ yield args[0]
+ else:
+ yield args
+ finally:
+ f_locals = frame.f_locals
+ for key in (x for x in f_locals.keys() if x not in saved):
+ del f_locals[key]
+ del frame
+
+
+def normalize_number(prettynumber):
+ """
+ function to take a phone number and strip out all non-numeric
+ characters
+
+ >>> normalize_number("+012-(345)-678-90")
+ '+01234567890'
+ >>> normalize_number("1-(345)-678-9000")
+ '+13456789000'
+ >>> normalize_number("+1-(345)-678-9000")
+ '+13456789000'
+ """
+ uglynumber = re.sub('[^0-9+]', '', prettynumber)
+ if uglynumber.startswith("+"):
+ pass
+ elif uglynumber.startswith("1"):
+ uglynumber = "+"+uglynumber
+ elif 10 <= len(uglynumber):
+ assert uglynumber[0] not in ("+", "1"), "Number format confusing"
+ uglynumber = "+1"+uglynumber
+ else:
+ pass
+
+ return uglynumber
+
+
+_VALIDATE_RE = re.compile("^\+?[0-9]{10,}$")
+
+
+def is_valid_number(number):
+ """
+ @returns If This number be called ( syntax validation only )
+ """
+ return _VALIDATE_RE.match(number) is not None
+
+
+def make_ugly(prettynumber):
+ """
+ function to take a phone number and strip out all non-numeric
+ characters
+
+ >>> make_ugly("+012-(345)-678-90")
+ '+01234567890'
+ """
+ return normalize_number(prettynumber)
+
+
+def _make_pretty_with_areacode(phonenumber):
+ prettynumber = "(%s)" % (phonenumber[0:3], )
+ if 3 < len(phonenumber):
+ prettynumber += " %s" % (phonenumber[3:6], )
+ if 6 < len(phonenumber):
+ prettynumber += "-%s" % (phonenumber[6:], )
+ return prettynumber
+
+
+def _make_pretty_local(phonenumber):
+ prettynumber = "%s" % (phonenumber[0:3], )
+ if 3 < len(phonenumber):
+ prettynumber += "-%s" % (phonenumber[3:], )
+ return prettynumber
+
+
+def _make_pretty_international(phonenumber):
+ prettynumber = phonenumber
+ if phonenumber.startswith("1"):
+ prettynumber = "1 "
+ prettynumber += _make_pretty_with_areacode(phonenumber[1:])
+ return prettynumber
+
+
+def make_pretty(phonenumber):
+ """
+ Function to take a phone number and return the pretty version
+ pretty numbers:
+ if phonenumber begins with 0:
+ ...-(...)-...-....
+ if phonenumber begins with 1: ( for gizmo callback numbers )
+ 1 (...)-...-....
+ if phonenumber is 13 digits:
+ (...)-...-....
+ if phonenumber is 10 digits:
+ ...-....
+ >>> make_pretty("12")
+ '12'
+ >>> make_pretty("1234567")
+ '123-4567'
+ >>> make_pretty("2345678901")
+ '+1 (234) 567-8901'
+ >>> make_pretty("12345678901")
+ '+1 (234) 567-8901'
+ >>> make_pretty("01234567890")
+ '+012 (345) 678-90'
+ >>> make_pretty("+01234567890")
+ '+012 (345) 678-90'
+ >>> make_pretty("+12")
+ '+1 (2)'
+ >>> make_pretty("+123")
+ '+1 (23)'
+ >>> make_pretty("+1234")
+ '+1 (234)'
+ """
+ if phonenumber is None or phonenumber == "":
+ return ""
+
+ phonenumber = normalize_number(phonenumber)
+
+ if phonenumber == "":
+ return ""
+ elif phonenumber[0] == "+":
+ prettynumber = _make_pretty_international(phonenumber[1:])
+ if not prettynumber.startswith("+"):
+ prettynumber = "+"+prettynumber
+ elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
+ prettynumber = _make_pretty_international(phonenumber)
+ elif 7 < len(phonenumber):
+ prettynumber = _make_pretty_with_areacode(phonenumber)
+ elif 3 < len(phonenumber):
+ prettynumber = _make_pretty_local(phonenumber)
+ else:
+ prettynumber = phonenumber
+ return prettynumber.strip()
+
+
+def similar_ugly_numbers(lhs, rhs):
+ return (
+ lhs == rhs or
+ lhs[1:] == rhs and lhs.startswith("1") or
+ lhs[2:] == rhs and lhs.startswith("+1") or
+ lhs == rhs[1:] and rhs.startswith("1") or
+ lhs == rhs[2:] and rhs.startswith("+1")
+ )
+
+
+def abbrev_relative_date(date):
+ """
+ >>> abbrev_relative_date("42 hours ago")
+ '42 h'
+ >>> abbrev_relative_date("2 days ago")
+ '2 d'
+ >>> abbrev_relative_date("4 weeks ago")
+ '4 w'
+ """
+ parts = date.split(" ")
+ return "%s %s" % (parts[0], parts[1][0])
+
+
+def parse_version(versionText):
+ """
+ >>> parse_version("0.5.2")
+ [0, 5, 2]
+ """
+ return [
+ int(number)
+ for number in versionText.split(".")
+ ]
+
+
+def compare_versions(leftParsedVersion, rightParsedVersion):
+ """
+ >>> compare_versions([0, 1, 2], [0, 1, 2])
+ 0
+ >>> compare_versions([0, 1, 2], [0, 1, 3])
+ -1
+ >>> compare_versions([0, 1, 2], [0, 2, 2])
+ -1
+ >>> compare_versions([0, 1, 2], [1, 1, 2])
+ -1
+ >>> compare_versions([0, 1, 3], [0, 1, 2])
+ 1
+ >>> compare_versions([0, 2, 2], [0, 1, 2])
+ 1
+ >>> compare_versions([1, 1, 2], [0, 1, 2])
+ 1
+ """
+ for left, right in zip(leftParsedVersion, rightParsedVersion):
+ if left < right:
+ return -1
+ elif right < left:
+ return 1
+ else:
+ return 0
--- /dev/null
+#!/usr/bin/env python
+import new
+
+# Make the environment more like Python 3.0
+__metaclass__ = type
+from itertools import izip as zip
+import textwrap
+import inspect
+
+
+__all__ = [
+ "AnyType",
+ "overloaded"
+]
+
+
+AnyType = object
+
+
+class overloaded:
+ """
+ Dynamically overloaded functions.
+
+ This is an implementation of (dynamically, or run-time) overloaded
+ functions; also known as generic functions or multi-methods.
+
+ The dispatch algorithm uses the types of all argument for dispatch,
+ similar to (compile-time) overloaded functions or methods in C++ and
+ Java.
+
+ Most of the complexity in the algorithm comes from the need to support
+ subclasses in call signatures. For example, if an function is
+ registered for a signature (T1, T2), then a call with a signature (S1,
+ S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass
+ of T2, and there are no other more specific matches (see below).
+
+ If there are multiple matches and one of those doesn't *dominate* all
+ others, the match is deemed ambiguous and an exception is raised. A
+ subtlety here: if, after removing the dominated matches, there are
+ still multiple matches left, but they all map to the same function,
+ then the match is not deemed ambiguous and that function is used.
+ Read the method find_func() below for details.
+
+ @note Python 2.5 is required due to the use of predicates any() and all().
+ @note only supports positional arguments
+
+ @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514
+
+ >>> import misc
+ >>> misc.validate_decorator (overloaded)
+ >>>
+ >>>
+ >>>
+ >>>
+ >>> #################
+ >>> #Basics, with reusing names and without
+ >>> @overloaded
+ ... def foo(x):
+ ... "prints x"
+ ... print x
+ ...
+ >>> @foo.register(int)
+ ... def foo(x):
+ ... "prints the hex representation of x"
+ ... print hex(x)
+ ...
+ >>> from types import DictType
+ >>> @foo.register(DictType)
+ ... def foo_dict(x):
+ ... "prints the keys of x"
+ ... print [k for k in x.iterkeys()]
+ ...
+ >>> #combines all of the doc strings to help keep track of the specializations
+ >>> foo.__doc__ # doctest: +ELLIPSIS
+ "prints x\\n\\n...overloading.foo (<type 'int'>):\\n\\tprints the hex representation of x\\n\\n...overloading.foo_dict (<type 'dict'>):\\n\\tprints the keys of x"
+ >>> foo ("text")
+ text
+ >>> foo (10) #calling the specialized foo
+ 0xa
+ >>> foo ({3:5, 6:7}) #calling the specialization foo_dict
+ [3, 6]
+ >>> foo_dict ({3:5, 6:7}) #with using a unique name, you still have the option of calling the function directly
+ [3, 6]
+ >>>
+ >>>
+ >>>
+ >>>
+ >>> #################
+ >>> #Multiple arguments, accessing the default, and function finding
+ >>> @overloaded
+ ... def two_arg (x, y):
+ ... print x,y
+ ...
+ >>> @two_arg.register(int, int)
+ ... def two_arg_int_int (x, y):
+ ... print hex(x), hex(y)
+ ...
+ >>> @two_arg.register(float, int)
+ ... def two_arg_float_int (x, y):
+ ... print x, hex(y)
+ ...
+ >>> @two_arg.register(int, float)
+ ... def two_arg_int_float (x, y):
+ ... print hex(x), y
+ ...
+ >>> two_arg.__doc__ # doctest: +ELLIPSIS
+ "...overloading.two_arg_int_int (<type 'int'>, <type 'int'>):\\n\\n...overloading.two_arg_float_int (<type 'float'>, <type 'int'>):\\n\\n...overloading.two_arg_int_float (<type 'int'>, <type 'float'>):"
+ >>> two_arg(9, 10)
+ 0x9 0xa
+ >>> two_arg(9.0, 10)
+ 9.0 0xa
+ >>> two_arg(15, 16.0)
+ 0xf 16.0
+ >>> two_arg.default_func(9, 10)
+ 9 10
+ >>> two_arg.find_func ((int, float)) == two_arg_int_float
+ True
+ >>> (int, float) in two_arg
+ True
+ >>> (str, int) in two_arg
+ False
+ >>>
+ >>>
+ >>>
+ >>> #################
+ >>> #wildcard
+ >>> @two_arg.register(AnyType, str)
+ ... def two_arg_any_str (x, y):
+ ... print x, y.lower()
+ ...
+ >>> two_arg("Hello", "World")
+ Hello world
+ >>> two_arg(500, "World")
+ 500 world
+ """
+
+ def __init__(self, default_func):
+ # Decorator to declare new overloaded function.
+ self.registry = {}
+ self.cache = {}
+ self.default_func = default_func
+ self.__name__ = self.default_func.__name__
+ self.__doc__ = self.default_func.__doc__
+ self.__dict__.update (self.default_func.__dict__)
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ return new.instancemethod(self, obj)
+
+ def register(self, *types):
+ """
+ Decorator to register an implementation for a specific set of types.
+
+ .register(t1, t2)(f) is equivalent to .register_func((t1, t2), f).
+ """
+
+ def helper(func):
+ self.register_func(types, func)
+
+ originalDoc = self.__doc__ if self.__doc__ is not None else ""
+ typeNames = ", ".join ([str(type) for type in types])
+ typeNames = "".join ([func.__module__+".", func.__name__, " (", typeNames, "):"])
+ overloadedDoc = ""
+ if func.__doc__ is not None:
+ overloadedDoc = textwrap.fill (func.__doc__, width=60, initial_indent="\t", subsequent_indent="\t")
+ self.__doc__ = "\n".join ([originalDoc, "", typeNames, overloadedDoc]).strip()
+
+ new_func = func
+
+ #Masking the function, so we want to take on its traits
+ if func.__name__ == self.__name__:
+ self.__dict__.update (func.__dict__)
+ new_func = self
+ return new_func
+
+ return helper
+
+ def register_func(self, types, func):
+ """Helper to register an implementation."""
+ self.registry[tuple(types)] = func
+ self.cache = {} # Clear the cache (later we can optimize this).
+
+ def __call__(self, *args):
+ """Call the overloaded function."""
+ types = tuple(map(type, args))
+ func = self.cache.get(types)
+ if func is None:
+ self.cache[types] = func = self.find_func(types)
+ return func(*args)
+
+ def __contains__ (self, types):
+ return self.find_func(types) is not self.default_func
+
+ def find_func(self, types):
+ """Find the appropriate overloaded function; don't call it.
+
+ @note This won't work for old-style classes or classes without __mro__
+ """
+ func = self.registry.get(types)
+ if func is not None:
+ # Easy case -- direct hit in registry.
+ return func
+
+ # Phillip Eby suggests to use issubclass() instead of __mro__.
+ # There are advantages and disadvantages.
+
+ # I can't help myself -- this is going to be intense functional code.
+ # Find all possible candidate signatures.
+ mros = tuple(inspect.getmro(t) for t in types)
+ n = len(mros)
+ candidates = [sig for sig in self.registry
+ if len(sig) == n and
+ all(t in mro for t, mro in zip(sig, mros))]
+
+ if not candidates:
+ # No match at all -- use the default function.
+ return self.default_func
+ elif len(candidates) == 1:
+ # Unique match -- that's an easy case.
+ return self.registry[candidates[0]]
+
+ # More than one match -- weed out the subordinate ones.
+
+ def dominates(dom, sub,
+ orders=tuple(dict((t, i) for i, t in enumerate(mro))
+ for mro in mros)):
+ # Predicate to decide whether dom strictly dominates sub.
+ # Strict domination is defined as domination without equality.
+ # The arguments dom and sub are type tuples of equal length.
+ # The orders argument is a precomputed auxiliary data structure
+ # giving dicts of ordering information corresponding to the
+ # positions in the type tuples.
+ # A type d dominates a type s iff order[d] <= order[s].
+ # A type tuple (d1, d2, ...) dominates a type tuple of equal length
+ # (s1, s2, ...) iff d1 dominates s1, d2 dominates s2, etc.
+ if dom is sub:
+ return False
+ return all(order[d] <= order[s] for d, s, order in zip(dom, sub, orders))
+
+ # I suppose I could inline dominates() but it wouldn't get any clearer.
+ candidates = [cand
+ for cand in candidates
+ if not any(dominates(dom, cand) for dom in candidates)]
+ if len(candidates) == 1:
+ # There's exactly one candidate left.
+ return self.registry[candidates[0]]
+
+ # Perhaps these multiple candidates all have the same implementation?
+ funcs = set(self.registry[cand] for cand in candidates)
+ if len(funcs) == 1:
+ return funcs.pop()
+
+ # No, the situation is irreducibly ambiguous.
+ raise TypeError("ambigous call; types=%r; candidates=%r" %
+ (types, candidates))
--- /dev/null
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QThread44(QtCore.QThread):
+ """
+ This is to imitate QThread in Qt 4.4+ for when running on older version
+ See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
+ (On Lucid I have Qt 4.7 and this is still an issue)
+ """
+
+ def __init__(self, parent = None):
+ QtCore.QThread.__init__(self, parent)
+
+ def run(self):
+ self.exec_()
+
+
+class _WorkerThread(QtCore.QObject):
+
+ _taskComplete = qt_compat.Signal(object)
+
+ def __init__(self, futureThread):
+ QtCore.QObject.__init__(self)
+ self._futureThread = futureThread
+ self._futureThread._addTask.connect(self._on_task_added)
+ self._taskComplete.connect(self._futureThread._on_task_complete)
+
+ @qt_compat.Slot(object)
+ def _on_task_added(self, task):
+ self.__on_task_added(task)
+
+ @misc.log_exception(_moduleLogger)
+ def __on_task_added(self, task):
+ if not self._futureThread._isRunning:
+ _moduleLogger.error("Dropping task")
+
+ func, args, kwds, on_success, on_error = task
+
+ try:
+ result = func(*args, **kwds)
+ isError = False
+ except Exception, e:
+ _moduleLogger.error("Error, passing it back to the main thread")
+ result = e
+ isError = True
+
+ taskResult = on_success, on_error, isError, result
+ self._taskComplete.emit(taskResult)
+
+
+class FutureThread(QtCore.QObject):
+
+ _addTask = qt_compat.Signal(object)
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._thread = QThread44()
+ self._isRunning = False
+ self._worker = _WorkerThread(self)
+ self._worker.moveToThread(self._thread)
+
+ def start(self):
+ self._thread.start()
+ self._isRunning = True
+
+ def stop(self):
+ self._isRunning = False
+ self._thread.quit()
+
+ def add_task(self, func, args, kwds, on_success, on_error):
+ assert self._isRunning, "Task queue not started"
+ task = func, args, kwds, on_success, on_error
+ self._addTask.emit(task)
+
+ @qt_compat.Slot(object)
+ def _on_task_complete(self, taskResult):
+ self.__on_task_complete(taskResult)
+
+ @misc.log_exception(_moduleLogger)
+ def __on_task_complete(self, taskResult):
+ on_success, on_error, isError, result = taskResult
+ if not self._isRunning:
+ if isError:
+ _moduleLogger.error("Masking: %s" % (result, ))
+ isError = True
+ result = StopIteration("Cancelling all callbacks")
+ callback = on_success if not isError else on_error
+ try:
+ callback(result)
+ except Exception:
+ _moduleLogger.exception("Callback errored")
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+#try:
+# import PySide.QtCore as _QtCore
+# QtCore = _QtCore
+# USES_PYSIDE = True
+#except ImportError:
+if True:
+ import sip
+ sip.setapi('QString', 2)
+ sip.setapi('QVariant', 2)
+ import PyQt4.QtCore as _QtCore
+ QtCore = _QtCore
+ USES_PYSIDE = False
+
+
+def _pyside_import_module(moduleName):
+ pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
+ return getattr(pyside, moduleName)
+
+
+def _pyqt4_import_module(moduleName):
+ pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
+ return getattr(pyside, moduleName)
+
+
+if USES_PYSIDE:
+ import_module = _pyside_import_module
+
+ Signal = QtCore.Signal
+ Slot = QtCore.Slot
+ Property = QtCore.Property
+else:
+ import_module = _pyqt4_import_module
+
+ Signal = QtCore.pyqtSignal
+ Slot = QtCore.pyqtSlot
+ Property = QtCore.pyqtProperty
+
+
+if __name__ == "__main__":
+ pass
+
--- /dev/null
+#!/usr/bin/env python
+
+import math
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_TWOPI = 2 * math.pi
+
+
+def _radius_at(center, pos):
+ delta = pos - center
+ xDelta = delta.x()
+ yDelta = delta.y()
+
+ radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+ return radius
+
+
+def _angle_at(center, pos):
+ delta = pos - center
+ xDelta = delta.x()
+ yDelta = delta.y()
+
+ radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+ angle = math.acos(xDelta / radius)
+ if 0 <= yDelta:
+ angle = _TWOPI - angle
+
+ return angle
+
+
+class QActionPieItem(object):
+
+ def __init__(self, action, weight = 1):
+ self._action = action
+ self._weight = weight
+
+ def action(self):
+ return self._action
+
+ def setWeight(self, weight):
+ self._weight = weight
+
+ def weight(self):
+ return self._weight
+
+ def setEnabled(self, enabled = True):
+ self._action.setEnabled(enabled)
+
+ def isEnabled(self):
+ return self._action.isEnabled()
+
+
+class PieFiling(object):
+
+ INNER_RADIUS_DEFAULT = 64
+ OUTER_RADIUS_DEFAULT = 192
+
+ SELECTION_CENTER = -1
+ SELECTION_NONE = -2
+
+ NULL_CENTER = QActionPieItem(QtGui.QAction(None))
+
+ def __init__(self):
+ self._innerRadius = self.INNER_RADIUS_DEFAULT
+ self._outerRadius = self.OUTER_RADIUS_DEFAULT
+ self._children = []
+ self._center = self.NULL_CENTER
+
+ self._cacheIndexToAngle = {}
+ self._cacheTotalWeight = 0
+
+ def insertItem(self, item, index = -1):
+ self._children.insert(index, item)
+ self._invalidate_cache()
+
+ def removeItemAt(self, index):
+ item = self._children.pop(index)
+ self._invalidate_cache()
+
+ def set_center(self, item):
+ if item is None:
+ item = self.NULL_CENTER
+ self._center = item
+
+ def center(self):
+ return self._center
+
+ def clear(self):
+ del self._children[:]
+ self._center = self.NULL_CENTER
+ self._invalidate_cache()
+
+ def itemAt(self, index):
+ return self._children[index]
+
+ def indexAt(self, center, point):
+ return self._angle_to_index(_angle_at(center, point))
+
+ def innerRadius(self):
+ return self._innerRadius
+
+ def setInnerRadius(self, radius):
+ self._innerRadius = radius
+
+ def outerRadius(self):
+ return self._outerRadius
+
+ def setOuterRadius(self, radius):
+ self._outerRadius = radius
+
+ def __iter__(self):
+ return iter(self._children)
+
+ def __len__(self):
+ return len(self._children)
+
+ def __getitem__(self, index):
+ return self._children[index]
+
+ def _invalidate_cache(self):
+ self._cacheIndexToAngle.clear()
+ self._cacheTotalWeight = sum(child.weight() for child in self._children)
+ if self._cacheTotalWeight == 0:
+ self._cacheTotalWeight = 1
+
+ def _index_to_angle(self, index, isShifted):
+ key = index, isShifted
+ if key in self._cacheIndexToAngle:
+ return self._cacheIndexToAngle[key]
+ index = index % len(self._children)
+
+ baseAngle = _TWOPI / self._cacheTotalWeight
+
+ angle = math.pi / 2
+ if isShifted:
+ if self._children:
+ angle -= (self._children[0].weight() * baseAngle) / 2
+ else:
+ angle -= baseAngle / 2
+ while angle < 0:
+ angle += _TWOPI
+
+ for i, child in enumerate(self._children):
+ if index < i:
+ break
+ angle += child.weight() * baseAngle
+ while _TWOPI < angle:
+ angle -= _TWOPI
+
+ self._cacheIndexToAngle[key] = angle
+ return angle
+
+ def _angle_to_index(self, angle):
+ numChildren = len(self._children)
+ if numChildren == 0:
+ return self.SELECTION_CENTER
+
+ baseAngle = _TWOPI / self._cacheTotalWeight
+
+ iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
+ while iterAngle < 0:
+ iterAngle += _TWOPI
+
+ oldIterAngle = iterAngle
+ for index, child in enumerate(self._children):
+ iterAngle += child.weight() * baseAngle
+ if oldIterAngle < angle and angle <= iterAngle:
+ return index - 1 if index != 0 else numChildren - 1
+ elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
+ return index - 1 if index != 0 else numChildren - 1
+ oldIterAngle = iterAngle
+
+
+class PieArtist(object):
+
+ ICON_SIZE_DEFAULT = 48
+
+ SHAPE_CIRCLE = "circle"
+ SHAPE_SQUARE = "square"
+ DEFAULT_SHAPE = SHAPE_SQUARE
+
+ BACKGROUND_FILL = "fill"
+ BACKGROUND_NOFILL = "no fill"
+
+ def __init__(self, filing, background = BACKGROUND_FILL):
+ self._filing = filing
+
+ self._cachedOuterRadius = self._filing.outerRadius()
+ self._cachedInnerRadius = self._filing.innerRadius()
+ canvasSize = self._cachedOuterRadius * 2 + 1
+ self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
+ self._mask = None
+ self._backgroundState = background
+ self.palette = None
+
+ def pieSize(self):
+ diameter = self._filing.outerRadius() * 2 + 1
+ return QtCore.QSize(diameter, diameter)
+
+ def centerSize(self):
+ painter = QtGui.QPainter(self._canvas)
+ text = self._filing.center().action().text()
+ fontMetrics = painter.fontMetrics()
+ if text:
+ textBoundingRect = fontMetrics.boundingRect(text)
+ else:
+ textBoundingRect = QtCore.QRect()
+ textWidth = textBoundingRect.width()
+ textHeight = textBoundingRect.height()
+
+ return QtCore.QSize(
+ textWidth + self.ICON_SIZE_DEFAULT,
+ max(textHeight, self.ICON_SIZE_DEFAULT),
+ )
+
+ def show(self, palette):
+ self.palette = palette
+
+ if (
+ self._cachedOuterRadius != self._filing.outerRadius() or
+ self._cachedInnerRadius != self._filing.innerRadius()
+ ):
+ self._cachedOuterRadius = self._filing.outerRadius()
+ self._cachedInnerRadius = self._filing.innerRadius()
+ self._canvas = self._canvas.scaled(self.pieSize())
+
+ if self._mask is None:
+ self._mask = QtGui.QBitmap(self._canvas.size())
+ self._mask.fill(QtCore.Qt.color0)
+ self._generate_mask(self._mask)
+ self._canvas.setMask(self._mask)
+ return self._mask
+
+ def hide(self):
+ self.palette = None
+
+ def paint(self, selectionIndex):
+ painter = QtGui.QPainter(self._canvas)
+ painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
+
+ self.paintPainter(selectionIndex, painter)
+
+ return self._canvas
+
+ def paintPainter(self, selectionIndex, painter):
+ adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1)
+
+ numChildren = len(self._filing)
+ if numChildren == 0:
+ self._paint_center_background(painter, adjustmentRect, selectionIndex)
+ self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
+ return self._canvas
+ else:
+ for i in xrange(len(self._filing)):
+ self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
+
+ self._paint_center_background(painter, adjustmentRect, selectionIndex)
+ self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
+
+ for i in xrange(len(self._filing)):
+ self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex)
+
+ def _generate_mask(self, mask):
+ """
+ Specifies on the mask the shape of the pie menu
+ """
+ painter = QtGui.QPainter(mask)
+ painter.setPen(QtCore.Qt.color1)
+ painter.setBrush(QtCore.Qt.color1)
+ if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+ painter.drawRect(mask.rect())
+ elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+ painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
+ else:
+ raise NotImplementedError(self.DEFAULT_SHAPE)
+
+ def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
+ if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+ currentWidth = adjustmentRect.width()
+ newWidth = math.sqrt(2) * currentWidth
+ dx = (newWidth - currentWidth) / 2
+ adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
+ elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+ pass
+ else:
+ raise NotImplementedError(self.DEFAULT_SHAPE)
+
+ if self._backgroundState == self.BACKGROUND_NOFILL:
+ painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
+ painter.setPen(self.palette.highlight().color())
+ else:
+ if i == selectionIndex and self._filing[i].isEnabled():
+ painter.setBrush(self.palette.highlight())
+ painter.setPen(self.palette.highlight().color())
+ else:
+ painter.setBrush(self.palette.window())
+ painter.setPen(self.palette.window().color())
+
+ a = self._filing._index_to_angle(i, True)
+ b = self._filing._index_to_angle(i + 1, True)
+ if b < a:
+ b += _TWOPI
+ size = b - a
+ if size < 0:
+ size += _TWOPI
+
+ startAngleInDeg = (a * 360 * 16) / _TWOPI
+ sizeInDeg = (size * 360 * 16) / _TWOPI
+ painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
+
+ def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex):
+ child = self._filing[i]
+
+ a = self._filing._index_to_angle(i, True)
+ b = self._filing._index_to_angle(i + 1, True)
+ if b < a:
+ b += _TWOPI
+ middleAngle = (a + b) / 2
+ averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
+
+ sliceX = averageRadius * math.cos(middleAngle)
+ sliceY = - averageRadius * math.sin(middleAngle)
+
+ piePos = adjustmentRect.center()
+ pieX = piePos.x()
+ pieY = piePos.y()
+ self._paint_label(
+ painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
+ )
+
+ def _paint_label(self, painter, action, isSelected, x, y):
+ text = action.text()
+ fontMetrics = painter.fontMetrics()
+ if text:
+ textBoundingRect = fontMetrics.boundingRect(text)
+ else:
+ textBoundingRect = QtCore.QRect()
+ textWidth = textBoundingRect.width()
+ textHeight = textBoundingRect.height()
+
+ icon = action.icon().pixmap(
+ QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
+ QtGui.QIcon.Normal,
+ QtGui.QIcon.On,
+ )
+ iconWidth = icon.width()
+ iconHeight = icon.width()
+ averageWidth = (iconWidth + textWidth)/2
+ if not icon.isNull():
+ iconRect = QtCore.QRect(
+ x - averageWidth,
+ y - iconHeight/2,
+ iconWidth,
+ iconHeight,
+ )
+
+ painter.drawPixmap(iconRect, icon)
+
+ if text:
+ if isSelected:
+ if action.isEnabled():
+ pen = self.palette.highlightedText()
+ brush = self.palette.highlight()
+ else:
+ pen = self.palette.mid()
+ brush = self.palette.window()
+ else:
+ if action.isEnabled():
+ pen = self.palette.windowText()
+ else:
+ pen = self.palette.mid()
+ brush = self.palette.window()
+
+ leftX = x - averageWidth + iconWidth
+ topY = y + textHeight/2
+ painter.setPen(pen.color())
+ painter.setBrush(brush)
+ painter.drawText(leftX, topY, text)
+
+ def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
+ if self._backgroundState == self.BACKGROUND_NOFILL:
+ return
+ if len(self._filing) == 0:
+ if self._backgroundState == self.BACKGROUND_NOFILL:
+ painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
+ else:
+ if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+ painter.setBrush(self.palette.highlight())
+ else:
+ painter.setBrush(self.palette.window())
+ painter.setPen(self.palette.mid().color())
+
+ painter.drawRect(adjustmentRect)
+ else:
+ dark = self.palette.mid().color()
+ light = self.palette.light().color()
+ if self._backgroundState == self.BACKGROUND_NOFILL:
+ background = QtGui.QBrush(QtCore.Qt.transparent)
+ else:
+ if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+ background = self.palette.highlight().color()
+ else:
+ background = self.palette.window().color()
+
+ innerRadius = self._cachedInnerRadius
+ adjustmentCenterPos = adjustmentRect.center()
+ innerRect = QtCore.QRect(
+ adjustmentCenterPos.x() - innerRadius,
+ adjustmentCenterPos.y() - innerRadius,
+ innerRadius * 2 + 1,
+ innerRadius * 2 + 1,
+ )
+
+ painter.setPen(QtCore.Qt.NoPen)
+ painter.setBrush(background)
+ painter.drawPie(innerRect, 0, 360 * 16)
+
+ if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+ pass
+ elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+ painter.setPen(QtGui.QPen(dark, 1))
+ painter.setBrush(QtCore.Qt.NoBrush)
+ painter.drawEllipse(adjustmentRect)
+ else:
+ raise NotImplementedError(self.DEFAULT_SHAPE)
+
+ def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex):
+ centerPos = adjustmentRect.center()
+ pieX = centerPos.x()
+ pieY = centerPos.y()
+
+ x = pieX
+ y = pieY
+
+ self._paint_label(
+ painter,
+ self._filing.center().action(),
+ selectionIndex == PieFiling.SELECTION_CENTER,
+ x, y
+ )
+
+
+class QPieDisplay(QtGui.QWidget):
+
+ def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
+ QtGui.QWidget.__init__(self, parent, flags)
+ self._filing = filing
+ self._artist = PieArtist(self._filing)
+ self._selectionIndex = PieFiling.SELECTION_NONE
+
+ def popup(self, pos):
+ self._update_selection(pos)
+ self.show()
+
+ def sizeHint(self):
+ return self._artist.pieSize()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def showEvent(self, showEvent):
+ mask = self._artist.show(self.palette())
+ self.setMask(mask)
+
+ QtGui.QWidget.showEvent(self, showEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def hideEvent(self, hideEvent):
+ self._artist.hide()
+ self._selectionIndex = PieFiling.SELECTION_NONE
+ QtGui.QWidget.hideEvent(self, hideEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def paintEvent(self, paintEvent):
+ canvas = self._artist.paint(self._selectionIndex)
+ offset = (self.size() - canvas.size()) / 2
+
+ screen = QtGui.QPainter(self)
+ screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
+
+ QtGui.QWidget.paintEvent(self, paintEvent)
+
+ def selectAt(self, index):
+ oldIndex = self._selectionIndex
+ self._selectionIndex = index
+ if self.isVisible():
+ self.update()
+
+
+class QPieButton(QtGui.QWidget):
+
+ activated = qt_compat.Signal(int)
+ highlighted = qt_compat.Signal(int)
+ canceled = qt_compat.Signal()
+ aboutToShow = qt_compat.Signal()
+ aboutToHide = qt_compat.Signal()
+
+ BUTTON_RADIUS = 24
+ DELAY = 250
+
+ def __init__(self, buttonSlice, parent = None, buttonSlices = None):
+ # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
+ # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
+ QtGui.QWidget.__init__(self, parent)
+ self._cachedCenterPosition = self.rect().center()
+
+ self._filing = PieFiling()
+ self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
+ self._selectionIndex = PieFiling.SELECTION_NONE
+
+ self._buttonFiling = PieFiling()
+ self._buttonFiling.set_center(buttonSlice)
+ if buttonSlices is not None:
+ for slice in buttonSlices:
+ self._buttonFiling.insertItem(slice)
+ self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
+ self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL)
+ self._poppedUp = False
+ self._pressed = False
+
+ self._delayPopupTimer = QtCore.QTimer()
+ self._delayPopupTimer.setInterval(self.DELAY)
+ self._delayPopupTimer.setSingleShot(True)
+ self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
+ self._popupLocation = None
+
+ self._mousePosition = None
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+ self.setSizePolicy(
+ QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ )
+ )
+
+ def insertItem(self, item, index = -1):
+ self._filing.insertItem(item, index)
+
+ def removeItemAt(self, index):
+ self._filing.removeItemAt(index)
+
+ def set_center(self, item):
+ self._filing.set_center(item)
+
+ def set_button(self, item):
+ self.update()
+
+ def clear(self):
+ self._filing.clear()
+
+ def itemAt(self, index):
+ return self._filing.itemAt(index)
+
+ def indexAt(self, point):
+ return self._filing.indexAt(self._cachedCenterPosition, point)
+
+ def innerRadius(self):
+ return self._filing.innerRadius()
+
+ def setInnerRadius(self, radius):
+ self._filing.setInnerRadius(radius)
+
+ def outerRadius(self):
+ return self._filing.outerRadius()
+
+ def setOuterRadius(self, radius):
+ self._filing.setOuterRadius(radius)
+
+ def buttonRadius(self):
+ return self._buttonFiling.outerRadius()
+
+ def setButtonRadius(self, radius):
+ self._buttonFiling.setOuterRadius(radius)
+ self._buttonFiling.setInnerRadius(radius / 2)
+ self._buttonArtist.show(self.palette())
+
+ def minimumSizeHint(self):
+ return self._buttonArtist.centerSize()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mousePressEvent(self, mouseEvent):
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ self._mousePosition = lastMousePos
+ self._update_selection(self._cachedCenterPosition)
+
+ self.highlighted.emit(self._selectionIndex)
+
+ self._display.selectAt(self._selectionIndex)
+ self._pressed = True
+ self.update()
+ self._popupLocation = mouseEvent.globalPos()
+ self._delayPopupTimer.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_popup(self):
+ assert self._popupLocation is not None, "Widget location abuse"
+ self._popup_child(self._popupLocation)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mouseMoveEvent(self, mouseEvent):
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ if self._mousePosition is None:
+ # Absolute
+ self._update_selection(lastMousePos)
+ else:
+ # Relative
+ self._update_selection(
+ self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+ ignoreOuter = True,
+ )
+
+ if lastSelection != self._selectionIndex:
+ self.highlighted.emit(self._selectionIndex)
+ self._display.selectAt(self._selectionIndex)
+
+ if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
+ self._on_delayed_popup()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mouseReleaseEvent(self, mouseEvent):
+ self._delayPopupTimer.stop()
+ self._popupLocation = None
+
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ if self._mousePosition is None:
+ # Absolute
+ self._update_selection(lastMousePos)
+ else:
+ # Relative
+ self._update_selection(
+ self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+ ignoreOuter = True,
+ )
+ self._mousePosition = None
+
+ self._activate_at(self._selectionIndex)
+ self._pressed = False
+ self.update()
+ self._hide_child()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def keyPressEvent(self, keyEvent):
+ if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+ self._popup_child(QtGui.QCursor.pos())
+ if self._selectionIndex != len(self._filing) - 1:
+ nextSelection = self._selectionIndex + 1
+ else:
+ nextSelection = 0
+ self._select_at(nextSelection)
+ self._display.selectAt(self._selectionIndex)
+ elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+ self._popup_child(QtGui.QCursor.pos())
+ if 0 < self._selectionIndex:
+ nextSelection = self._selectionIndex - 1
+ else:
+ nextSelection = len(self._filing) - 1
+ self._select_at(nextSelection)
+ self._display.selectAt(self._selectionIndex)
+ elif keyEvent.key() in [QtCore.Qt.Key_Space]:
+ self._popup_child(QtGui.QCursor.pos())
+ self._select_at(PieFiling.SELECTION_CENTER)
+ self._display.selectAt(self._selectionIndex)
+ elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+ self._delayPopupTimer.stop()
+ self._popupLocation = None
+ self._activate_at(self._selectionIndex)
+ self._hide_child()
+ elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+ self._delayPopupTimer.stop()
+ self._popupLocation = None
+ self._activate_at(PieFiling.SELECTION_NONE)
+ self._hide_child()
+ else:
+ QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def resizeEvent(self, resizeEvent):
+ self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
+ QtGui.QWidget.resizeEvent(self, resizeEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def showEvent(self, showEvent):
+ self._buttonArtist.show(self.palette())
+ self._cachedCenterPosition = self.rect().center()
+
+ QtGui.QWidget.showEvent(self, showEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def hideEvent(self, hideEvent):
+ self._display.hide()
+ self._select_at(PieFiling.SELECTION_NONE)
+ QtGui.QWidget.hideEvent(self, hideEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def paintEvent(self, paintEvent):
+ self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
+ if self._poppedUp:
+ selectionIndex = PieFiling.SELECTION_CENTER
+ else:
+ selectionIndex = PieFiling.SELECTION_NONE
+
+ screen = QtGui.QStylePainter(self)
+ screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
+ option = QtGui.QStyleOptionButton()
+ option.initFrom(self)
+ option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
+
+ screen.drawControl(QtGui.QStyle.CE_PushButton, option)
+ self._buttonArtist.paintPainter(selectionIndex, screen)
+
+ QtGui.QWidget.paintEvent(self, paintEvent)
+
+ def __iter__(self):
+ return iter(self._filing)
+
+ def __len__(self):
+ return len(self._filing)
+
+ def _popup_child(self, position):
+ self._poppedUp = True
+ self.aboutToShow.emit()
+
+ self._delayPopupTimer.stop()
+ self._popupLocation = None
+
+ position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
+ self._display.move(position)
+ self._display.show()
+
+ self.update()
+
+ def _hide_child(self):
+ self._poppedUp = False
+ self.aboutToHide.emit()
+ self._display.hide()
+ self.update()
+
+ def _select_at(self, index):
+ self._selectionIndex = index
+
+ def _update_selection(self, lastMousePos, ignoreOuter = False):
+ radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+ if radius < self._filing.innerRadius():
+ self._select_at(PieFiling.SELECTION_CENTER)
+ elif radius <= self._filing.outerRadius() or ignoreOuter:
+ self._select_at(self.indexAt(lastMousePos))
+ else:
+ self._select_at(PieFiling.SELECTION_NONE)
+
+ def _activate_at(self, index):
+ if index == PieFiling.SELECTION_NONE:
+ self.canceled.emit()
+ return
+ elif index == PieFiling.SELECTION_CENTER:
+ child = self._filing.center()
+ else:
+ child = self.itemAt(index)
+
+ if child.action().isEnabled():
+ child.action().trigger()
+ self.activated.emit(index)
+ else:
+ self.canceled.emit()
+
+
+class QPieMenu(QtGui.QWidget):
+
+ activated = qt_compat.Signal(int)
+ highlighted = qt_compat.Signal(int)
+ canceled = qt_compat.Signal()
+ aboutToShow = qt_compat.Signal()
+ aboutToHide = qt_compat.Signal()
+
+ def __init__(self, parent = None):
+ QtGui.QWidget.__init__(self, parent)
+ self._cachedCenterPosition = self.rect().center()
+
+ self._filing = PieFiling()
+ self._artist = PieArtist(self._filing)
+ self._selectionIndex = PieFiling.SELECTION_NONE
+
+ self._mousePosition = ()
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+ def popup(self, pos):
+ self._update_selection(pos)
+ self.show()
+
+ def insertItem(self, item, index = -1):
+ self._filing.insertItem(item, index)
+ self.update()
+
+ def removeItemAt(self, index):
+ self._filing.removeItemAt(index)
+ self.update()
+
+ def set_center(self, item):
+ self._filing.set_center(item)
+ self.update()
+
+ def clear(self):
+ self._filing.clear()
+ self.update()
+
+ def itemAt(self, index):
+ return self._filing.itemAt(index)
+
+ def indexAt(self, point):
+ return self._filing.indexAt(self._cachedCenterPosition, point)
+
+ def innerRadius(self):
+ return self._filing.innerRadius()
+
+ def setInnerRadius(self, radius):
+ self._filing.setInnerRadius(radius)
+ self.update()
+
+ def outerRadius(self):
+ return self._filing.outerRadius()
+
+ def setOuterRadius(self, radius):
+ self._filing.setOuterRadius(radius)
+ self.update()
+
+ def sizeHint(self):
+ return self._artist.pieSize()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mousePressEvent(self, mouseEvent):
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ self._update_selection(lastMousePos)
+ self._mousePosition = lastMousePos
+
+ if lastSelection != self._selectionIndex:
+ self.highlighted.emit(self._selectionIndex)
+ self.update()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mouseMoveEvent(self, mouseEvent):
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ self._update_selection(lastMousePos)
+
+ if lastSelection != self._selectionIndex:
+ self.highlighted.emit(self._selectionIndex)
+ self.update()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def mouseReleaseEvent(self, mouseEvent):
+ lastSelection = self._selectionIndex
+
+ lastMousePos = mouseEvent.pos()
+ self._update_selection(lastMousePos)
+ self._mousePosition = ()
+
+ self._activate_at(self._selectionIndex)
+ self.update()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def keyPressEvent(self, keyEvent):
+ if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+ if self._selectionIndex != len(self._filing) - 1:
+ nextSelection = self._selectionIndex + 1
+ else:
+ nextSelection = 0
+ self._select_at(nextSelection)
+ self.update()
+ elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+ if 0 < self._selectionIndex:
+ nextSelection = self._selectionIndex - 1
+ else:
+ nextSelection = len(self._filing) - 1
+ self._select_at(nextSelection)
+ self.update()
+ elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+ self._activate_at(self._selectionIndex)
+ elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+ self._activate_at(PieFiling.SELECTION_NONE)
+ else:
+ QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def showEvent(self, showEvent):
+ self.aboutToShow.emit()
+ self._cachedCenterPosition = self.rect().center()
+
+ mask = self._artist.show(self.palette())
+ self.setMask(mask)
+
+ lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
+ self._update_selection(lastMousePos)
+
+ QtGui.QWidget.showEvent(self, showEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def hideEvent(self, hideEvent):
+ self._artist.hide()
+ self._selectionIndex = PieFiling.SELECTION_NONE
+ QtGui.QWidget.hideEvent(self, hideEvent)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def paintEvent(self, paintEvent):
+ canvas = self._artist.paint(self._selectionIndex)
+
+ screen = QtGui.QPainter(self)
+ screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+ QtGui.QWidget.paintEvent(self, paintEvent)
+
+ def __iter__(self):
+ return iter(self._filing)
+
+ def __len__(self):
+ return len(self._filing)
+
+ def _select_at(self, index):
+ self._selectionIndex = index
+
+ def _update_selection(self, lastMousePos):
+ radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+ if radius < self._filing.innerRadius():
+ self._selectionIndex = PieFiling.SELECTION_CENTER
+ elif radius <= self._filing.outerRadius():
+ self._select_at(self.indexAt(lastMousePos))
+ else:
+ self._selectionIndex = PieFiling.SELECTION_NONE
+
+ def _activate_at(self, index):
+ if index == PieFiling.SELECTION_NONE:
+ self.canceled.emit()
+ self.aboutToHide.emit()
+ self.hide()
+ return
+ elif index == PieFiling.SELECTION_CENTER:
+ child = self._filing.center()
+ else:
+ child = self.itemAt(index)
+
+ if child.isEnabled():
+ child.action().trigger()
+ self.activated.emit(index)
+ else:
+ self.canceled.emit()
+ self.aboutToHide.emit()
+ self.hide()
+
+
+def init_pies():
+ PieFiling.NULL_CENTER.setEnabled(False)
+
+
+def _print(msg):
+ print msg
+
+
+def _on_about_to_hide(app):
+ app.exit()
+
+
+if __name__ == "__main__":
+ app = QtGui.QApplication([])
+ init_pies()
+
+ if False:
+ pie = QPieMenu()
+ pie.show()
+
+ if False:
+ singleAction = QtGui.QAction(None)
+ singleAction.setText("Boo")
+ singleItem = QActionPieItem(singleAction)
+ spie = QPieMenu()
+ spie.insertItem(singleItem)
+ spie.show()
+
+ if False:
+ oneAction = QtGui.QAction(None)
+ oneAction.setText("Chew")
+ oneItem = QActionPieItem(oneAction)
+ twoAction = QtGui.QAction(None)
+ twoAction.setText("Foo")
+ twoItem = QActionPieItem(twoAction)
+ iconTextAction = QtGui.QAction(None)
+ iconTextAction.setText("Icon")
+ iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+ iconTextItem = QActionPieItem(iconTextAction)
+ mpie = QPieMenu()
+ mpie.insertItem(oneItem)
+ mpie.insertItem(twoItem)
+ mpie.insertItem(oneItem)
+ mpie.insertItem(iconTextItem)
+ mpie.show()
+
+ if True:
+ oneAction = QtGui.QAction(None)
+ oneAction.setText("Chew")
+ oneAction.triggered.connect(lambda: _print("Chew"))
+ oneItem = QActionPieItem(oneAction)
+ twoAction = QtGui.QAction(None)
+ twoAction.setText("Foo")
+ twoAction.triggered.connect(lambda: _print("Foo"))
+ twoItem = QActionPieItem(twoAction)
+ iconAction = QtGui.QAction(None)
+ iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+ iconAction.triggered.connect(lambda: _print("Icon"))
+ iconItem = QActionPieItem(iconAction)
+ iconTextAction = QtGui.QAction(None)
+ iconTextAction.setText("Icon")
+ iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+ iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+ iconTextItem = QActionPieItem(iconTextAction)
+ mpie = QPieMenu()
+ mpie.set_center(iconItem)
+ mpie.insertItem(oneItem)
+ mpie.insertItem(twoItem)
+ mpie.insertItem(oneItem)
+ mpie.insertItem(iconTextItem)
+ mpie.show()
+ mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+ mpie.canceled.connect(lambda: _print("Canceled"))
+
+ if False:
+ oneAction = QtGui.QAction(None)
+ oneAction.setText("Chew")
+ oneAction.triggered.connect(lambda: _print("Chew"))
+ oneItem = QActionPieItem(oneAction)
+ twoAction = QtGui.QAction(None)
+ twoAction.setText("Foo")
+ twoAction.triggered.connect(lambda: _print("Foo"))
+ twoItem = QActionPieItem(twoAction)
+ iconAction = QtGui.QAction(None)
+ iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+ iconAction.triggered.connect(lambda: _print("Icon"))
+ iconItem = QActionPieItem(iconAction)
+ iconTextAction = QtGui.QAction(None)
+ iconTextAction.setText("Icon")
+ iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+ iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+ iconTextItem = QActionPieItem(iconTextAction)
+ pieFiling = PieFiling()
+ pieFiling.set_center(iconItem)
+ pieFiling.insertItem(oneItem)
+ pieFiling.insertItem(twoItem)
+ pieFiling.insertItem(oneItem)
+ pieFiling.insertItem(iconTextItem)
+ mpie = QPieDisplay(pieFiling)
+ mpie.show()
+
+ if False:
+ oneAction = QtGui.QAction(None)
+ oneAction.setText("Chew")
+ oneAction.triggered.connect(lambda: _print("Chew"))
+ oneItem = QActionPieItem(oneAction)
+ twoAction = QtGui.QAction(None)
+ twoAction.setText("Foo")
+ twoAction.triggered.connect(lambda: _print("Foo"))
+ twoItem = QActionPieItem(twoAction)
+ iconAction = QtGui.QAction(None)
+ iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+ iconAction.triggered.connect(lambda: _print("Icon"))
+ iconItem = QActionPieItem(iconAction)
+ iconTextAction = QtGui.QAction(None)
+ iconTextAction.setText("Icon")
+ iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+ iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+ iconTextItem = QActionPieItem(iconTextAction)
+ mpie = QPieButton(iconItem)
+ mpie.set_center(iconItem)
+ mpie.insertItem(oneItem)
+ mpie.insertItem(twoItem)
+ mpie.insertItem(oneItem)
+ mpie.insertItem(iconTextItem)
+ mpie.show()
+ mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+ mpie.canceled.connect(lambda: _print("Canceled"))
+
+ app.exec_()
--- /dev/null
+#!/usr/bin/env python
+
+
+from __future__ import division
+
+import os
+import warnings
+
+import qt_compat
+QtGui = qt_compat.import_module("QtGui")
+
+import qtpie
+
+
+class PieKeyboard(object):
+
+ SLICE_CENTER = -1
+ SLICE_NORTH = 0
+ SLICE_NORTH_WEST = 1
+ SLICE_WEST = 2
+ SLICE_SOUTH_WEST = 3
+ SLICE_SOUTH = 4
+ SLICE_SOUTH_EAST = 5
+ SLICE_EAST = 6
+ SLICE_NORTH_EAST = 7
+
+ MAX_ANGULAR_SLICES = 8
+
+ SLICE_DIRECTIONS = [
+ SLICE_CENTER,
+ SLICE_NORTH,
+ SLICE_NORTH_WEST,
+ SLICE_WEST,
+ SLICE_SOUTH_WEST,
+ SLICE_SOUTH,
+ SLICE_SOUTH_EAST,
+ SLICE_EAST,
+ SLICE_NORTH_EAST,
+ ]
+
+ SLICE_DIRECTION_NAMES = [
+ "CENTER",
+ "NORTH",
+ "NORTH_WEST",
+ "WEST",
+ "SOUTH_WEST",
+ "SOUTH",
+ "SOUTH_EAST",
+ "EAST",
+ "NORTH_EAST",
+ ]
+
+ def __init__(self):
+ self._layout = QtGui.QGridLayout()
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self.__cells = {}
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def add_pie(self, row, column, pieButton):
+ assert len(pieButton) == 8
+ self._layout.addWidget(pieButton, row, column)
+ self.__cells[(row, column)] = pieButton
+
+ def get_pie(self, row, column):
+ return self.__cells[(row, column)]
+
+
+class KeyboardModifier(object):
+
+ def __init__(self, name):
+ self.name = name
+ self.lock = False
+ self.once = False
+
+ @property
+ def isActive(self):
+ return self.lock or self.once
+
+ def on_toggle_lock(self, *args, **kwds):
+ self.lock = not self.lock
+
+ def on_toggle_once(self, *args, **kwds):
+ self.once = not self.once
+
+ def reset_once(self):
+ self.once = False
+
+
+def parse_keyboard_data(text):
+ return eval(text)
+
+
+def _enumerate_pie_slices(pieData, iconPaths):
+ for direction, directionName in zip(
+ PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES
+ ):
+ if directionName in pieData:
+ sliceData = pieData[directionName]
+
+ action = QtGui.QAction(None)
+ try:
+ action.setText(sliceData["text"])
+ except KeyError:
+ pass
+ try:
+ relativeIconPath = sliceData["path"]
+ except KeyError:
+ pass
+ else:
+ for iconPath in iconPaths:
+ absIconPath = os.path.join(iconPath, relativeIconPath)
+ if os.path.exists(absIconPath):
+ action.setIcon(QtGui.QIcon(absIconPath))
+ break
+ pieItem = qtpie.QActionPieItem(action)
+ actionToken = sliceData["action"]
+ else:
+ pieItem = qtpie.PieFiling.NULL_CENTER
+ actionToken = ""
+ yield direction, pieItem, actionToken
+
+
+def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths):
+ for (row, column), pieData in dataTree.iteritems():
+ pieItems = list(_enumerate_pie_slices(pieData, iconPaths))
+ assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0]
+ _, center, centerAction = pieItems.pop(0)
+
+ pieButton = qtpie.QPieButton(center)
+ pieButton.set_center(center)
+ keyboardHandler.map_slice_action(center, centerAction)
+ for direction, pieItem, action in pieItems:
+ pieButton.insertItem(pieItem)
+ keyboardHandler.map_slice_action(pieItem, action)
+ keyboard.add_pie(row, column, pieButton)
+
+
+class KeyboardHandler(object):
+
+ def __init__(self, keyhandler):
+ self.__keyhandler = keyhandler
+ self.__commandHandlers = {}
+ self.__modifiers = {}
+ self.__sliceActions = {}
+
+ self.register_modifier("Shift")
+ self.register_modifier("Super")
+ self.register_modifier("Control")
+ self.register_modifier("Alt")
+
+ def register_command_handler(self, command, handler):
+ # @todo Look into hooking these up directly to the pie actions
+ self.__commandHandlers["[%s]" % command] = handler
+
+ def unregister_command_handler(self, command):
+ # @todo Look into hooking these up directly to the pie actions
+ del self.__commandHandlers["[%s]" % command]
+
+ def register_modifier(self, modifierName):
+ mod = KeyboardModifier(modifierName)
+ self.register_command_handler(modifierName, mod.on_toggle_lock)
+ self.__modifiers["<%s>" % modifierName] = mod
+
+ def unregister_modifier(self, modifierName):
+ self.unregister_command_handler(modifierName)
+ del self.__modifiers["<%s>" % modifierName]
+
+ def map_slice_action(self, slice, action):
+ callback = lambda direction: self(direction, action)
+ slice.action().triggered.connect(callback)
+ self.__sliceActions[slice] = (action, callback)
+
+ def __call__(self, direction, action):
+ activeModifiers = [
+ mod.name
+ for mod in self.__modifiers.itervalues()
+ if mod.isActive
+ ]
+
+ needResetOnce = False
+ if action.startswith("[") and action.endswith("]"):
+ commandName = action[1:-1]
+ if action in self.__commandHandlers:
+ self.__commandHandlers[action](commandName, activeModifiers)
+ needResetOnce = True
+ else:
+ warnings.warn("Unknown command: [%s]" % commandName)
+ elif action.startswith("<") and action.endswith(">"):
+ modName = action[1:-1]
+ for mod in self.__modifiers.itervalues():
+ if mod.name == modName:
+ mod.on_toggle_once()
+ break
+ else:
+ warnings.warn("Unknown modifier: <%s>" % modName)
+ else:
+ self.__keyhandler(action, activeModifiers)
+ needResetOnce = True
+
+ if needResetOnce:
+ for mod in self.__modifiers.itervalues():
+ mod.reset_once()
--- /dev/null
+import sys
+import contextlib
+import datetime
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def notify_error(log):
+ try:
+ yield
+ except:
+ log.push_exception()
+
+
+@contextlib.contextmanager
+def notify_busy(log, message):
+ log.push_busy(message)
+ try:
+ yield
+ finally:
+ log.pop(message)
+
+
+class ErrorMessage(object):
+
+ LEVEL_ERROR = 0
+ LEVEL_BUSY = 1
+ LEVEL_INFO = 2
+
+ def __init__(self, message, level):
+ self._message = message
+ self._level = level
+ self._time = datetime.datetime.now()
+
+ @property
+ def level(self):
+ return self._level
+
+ @property
+ def message(self):
+ return self._message
+
+ def __repr__(self):
+ return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
+
+
+class QErrorLog(QtCore.QObject):
+
+ messagePushed = qt_compat.Signal()
+ messagePopped = qt_compat.Signal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._messages = []
+
+ def push_busy(self, message):
+ _moduleLogger.info("Entering state: %s" % message)
+ self._push_message(message, ErrorMessage.LEVEL_BUSY)
+
+ def push_message(self, message):
+ self._push_message(message, ErrorMessage.LEVEL_INFO)
+
+ def push_error(self, message):
+ self._push_message(message, ErrorMessage.LEVEL_ERROR)
+
+ def push_exception(self):
+ userMessage = str(sys.exc_info()[1])
+ _moduleLogger.exception(userMessage)
+ self.push_error(userMessage)
+
+ def pop(self, message = None):
+ if message is None:
+ del self._messages[0]
+ else:
+ _moduleLogger.info("Exiting state: %s" % message)
+ messageIndex = [
+ i
+ for (i, error) in enumerate(self._messages)
+ if error.message == message
+ ]
+ # Might be removed out of order
+ if messageIndex:
+ del self._messages[messageIndex[0]]
+ self.messagePopped.emit()
+
+ def peek_message(self):
+ return self._messages[0]
+
+ def _push_message(self, message, level):
+ self._messages.append(ErrorMessage(message, level))
+ # Sort is defined as stable, so this should be fine
+ self._messages.sort(key=lambda x: x.level)
+ self.messagePushed.emit()
+
+ def __len__(self):
+ return len(self._messages)
+
+
+class ErrorDisplay(object):
+
+ _SENTINEL_ICON = QtGui.QIcon()
+
+ def __init__(self, errorLog):
+ self._errorLog = errorLog
+ self._errorLog.messagePushed.connect(self._on_message_pushed)
+ self._errorLog.messagePopped.connect(self._on_message_popped)
+
+ self._icons = None
+ self._severityLabel = QtGui.QLabel()
+ self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+ self._message = QtGui.QLabel()
+ self._message.setText("Boo")
+ self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ self._message.setWordWrap(True)
+
+ self._closeLabel = None
+
+ self._controlLayout = QtGui.QHBoxLayout()
+ self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
+ self._controlLayout.addWidget(self._message, 1000)
+
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._controlLayout)
+ self._widget.hide()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def _show_error(self):
+ if self._icons is None:
+ self._icons = {
+ ErrorMessage.LEVEL_BUSY:
+ get_theme_icon(
+ #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
+ ("view-refresh", "general_refresh", "gtk-refresh", )
+ ).pixmap(32, 32),
+ ErrorMessage.LEVEL_INFO:
+ get_theme_icon(
+ ("dialog-information", "general_notes", "gtk-info")
+ ).pixmap(32, 32),
+ ErrorMessage.LEVEL_ERROR:
+ get_theme_icon(
+ ("dialog-error", "app_install_error", "gtk-dialog-error")
+ ).pixmap(32, 32),
+ }
+ if self._closeLabel is None:
+ closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+ if closeIcon is not self._SENTINEL_ICON:
+ self._closeLabel = QtGui.QPushButton(closeIcon, "")
+ else:
+ self._closeLabel = QtGui.QPushButton("X")
+ self._closeLabel.clicked.connect(self._on_close)
+ self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
+ error = self._errorLog.peek_message()
+ self._message.setText(error.message)
+ self._severityLabel.setPixmap(self._icons[error.level])
+ self._widget.show()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc.log_exception(_moduleLogger)
+ def _on_close(self, checked = False):
+ self._errorLog.pop()
+
+ @qt_compat.Slot()
+ @misc.log_exception(_moduleLogger)
+ def _on_message_pushed(self):
+ self._show_error()
+
+ @qt_compat.Slot()
+ @misc.log_exception(_moduleLogger)
+ def _on_message_popped(self):
+ if len(self._errorLog) == 0:
+ self._message.setText("")
+ self._widget.hide()
+ else:
+ self._show_error()
+
+
+class QHtmlDelegate(QtGui.QStyledItemDelegate):
+
+ UNDEFINED_SIZE = -1
+
+ def __init__(self, *args, **kwd):
+ QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
+ self._width = self.UNDEFINED_SIZE
+
+ def paint(self, painter, option, index):
+ newOption = QtGui.QStyleOptionViewItemV4(option)
+ self.initStyleOption(newOption, index)
+ if newOption.widget is not None:
+ style = newOption.widget.style()
+ else:
+ style = QtGui.QApplication.style()
+
+ doc = QtGui.QTextDocument()
+ doc.setHtml(newOption.text)
+ doc.setTextWidth(newOption.rect.width())
+
+ newOption.text = ""
+ style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
+
+ ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
+ if newOption.state & QtGui.QStyle.State_Selected:
+ ctx.palette.setColor(
+ QtGui.QPalette.Text,
+ newOption.palette.color(
+ QtGui.QPalette.Active,
+ QtGui.QPalette.HighlightedText
+ )
+ )
+ else:
+ ctx.palette.setColor(
+ QtGui.QPalette.Text,
+ newOption.palette.color(
+ QtGui.QPalette.Active,
+ QtGui.QPalette.Text
+ )
+ )
+
+ textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
+ painter.save()
+ painter.translate(textRect.topLeft())
+ painter.setClipRect(textRect.translated(-textRect.topLeft()))
+ doc.documentLayout().draw(painter, ctx)
+ painter.restore()
+
+ def setWidth(self, width, model):
+ if self._width == width:
+ return
+ self._width = width
+ for c in xrange(model.rowCount()):
+ cItem = model.item(c, 0)
+ for r in xrange(model.rowCount()):
+ rItem = cItem.child(r, 0)
+ rIndex = model.indexFromItem(rItem)
+ self.sizeHintChanged.emit(rIndex)
+ return
+
+ def sizeHint(self, option, index):
+ newOption = QtGui.QStyleOptionViewItemV4(option)
+ self.initStyleOption(newOption, index)
+
+ doc = QtGui.QTextDocument()
+ doc.setHtml(newOption.text)
+ if self._width != self.UNDEFINED_SIZE:
+ width = self._width
+ else:
+ width = newOption.rect.width()
+ doc.setTextWidth(width)
+ size = QtCore.QSize(doc.idealWidth(), doc.size().height())
+ return size
+
+
+class QSignalingMainWindow(QtGui.QMainWindow):
+
+ closed = qt_compat.Signal()
+ hidden = qt_compat.Signal()
+ shown = qt_compat.Signal()
+ resized = qt_compat.Signal()
+
+ def __init__(self, *args, **kwd):
+ QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
+
+ def closeEvent(self, event):
+ val = QtGui.QMainWindow.closeEvent(self, event)
+ self.closed.emit()
+ return val
+
+ def hideEvent(self, event):
+ val = QtGui.QMainWindow.hideEvent(self, event)
+ self.hidden.emit()
+ return val
+
+ def showEvent(self, event):
+ val = QtGui.QMainWindow.showEvent(self, event)
+ self.shown.emit()
+ return val
+
+ def resizeEvent(self, event):
+ val = QtGui.QMainWindow.resizeEvent(self, event)
+ self.resized.emit()
+ return val
+
+def set_current_index(selector, itemText, default = 0):
+ for i in xrange(selector.count()):
+ if selector.itemText(i) == itemText:
+ selector.setCurrentIndex(i)
+ break
+ else:
+ itemText.setCurrentIndex(default)
+
+
+def _null_set_stackable(window, isStackable):
+ pass
+
+
+def _maemo_set_stackable(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+ QtCore.Qt.WA_Maemo5StackedWindow
+ set_stackable = _maemo_set_stackable
+except AttributeError:
+ set_stackable = _null_set_stackable
+
+
+def _null_set_autorient(window, doAutoOrient):
+ pass
+
+
+def _maemo_set_autorient(window, doAutoOrient):
+ window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
+
+
+try:
+ QtCore.Qt.WA_Maemo5AutoOrientation
+ set_autorient = _maemo_set_autorient
+except AttributeError:
+ set_autorient = _null_set_autorient
+
+
+def screen_orientation():
+ geom = QtGui.QApplication.desktop().screenGeometry()
+ if geom.width() <= geom.height():
+ return QtCore.Qt.Vertical
+ else:
+ return QtCore.Qt.Horizontal
+
+
+def _null_set_window_orientation(window, orientation):
+ pass
+
+
+def _maemo_set_window_orientation(window, orientation):
+ if orientation == QtCore.Qt.Vertical:
+ window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+ window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
+ elif orientation == QtCore.Qt.Horizontal:
+ window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
+ window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+ elif orientation is None:
+ window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+ window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+ else:
+ raise RuntimeError("Unknown orientation: %r" % orientation)
+
+
+try:
+ QtCore.Qt.WA_Maemo5LandscapeOrientation
+ QtCore.Qt.WA_Maemo5PortraitOrientation
+ set_window_orientation = _maemo_set_window_orientation
+except AttributeError:
+ set_window_orientation = _null_set_window_orientation
+
+
+def _null_show_progress_indicator(window, isStackable):
+ pass
+
+
+def _maemo_show_progress_indicator(window, isStackable):
+ window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
+
+
+try:
+ QtCore.Qt.WA_Maemo5ShowProgressIndicator
+ show_progress_indicator = _maemo_show_progress_indicator
+except AttributeError:
+ show_progress_indicator = _null_show_progress_indicator
+
+
+def _null_mark_numbers_preferred(widget):
+ pass
+
+
+def _newqt_mark_numbers_preferred(widget):
+ widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
+
+
+try:
+ QtCore.Qt.ImhPreferNumbers
+ mark_numbers_preferred = _newqt_mark_numbers_preferred
+except AttributeError:
+ mark_numbers_preferred = _null_mark_numbers_preferred
+
+
+def _null_get_theme_icon(iconNames, fallback = None):
+ icon = fallback if fallback is not None else QtGui.QIcon()
+ return icon
+
+
+def _newqt_get_theme_icon(iconNames, fallback = None):
+ for iconName in iconNames:
+ if QtGui.QIcon.hasThemeIcon(iconName):
+ icon = QtGui.QIcon.fromTheme(iconName)
+ break
+ else:
+ icon = fallback if fallback is not None else QtGui.QIcon()
+ return icon
+
+
+try:
+ QtGui.QIcon.fromTheme
+ get_theme_icon = _newqt_get_theme_icon
+except AttributeError:
+ get_theme_icon = _null_get_theme_icon
+
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class ApplicationWrapper(object):
+
+ DEFAULT_ORIENTATION = "Default"
+ AUTO_ORIENTATION = "Auto"
+ LANDSCAPE_ORIENTATION = "Landscape"
+ PORTRAIT_ORIENTATION = "Portrait"
+
+ def __init__(self, qapp, constants):
+ self._constants = constants
+ self._qapp = qapp
+ self._clipboard = QtGui.QApplication.clipboard()
+
+ self._errorLog = qui_utils.QErrorLog()
+ self._mainWindow = None
+
+ self._fullscreenAction = QtGui.QAction(None)
+ self._fullscreenAction.setText("Fullscreen")
+ self._fullscreenAction.setCheckable(True)
+ self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
+ self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
+
+ self._orientation = self.DEFAULT_ORIENTATION
+ self._orientationAction = QtGui.QAction(None)
+ self._orientationAction.setText("Next Orientation")
+ self._orientationAction.setCheckable(True)
+ self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
+ self._orientationAction.triggered.connect(self._on_next_orientation)
+
+ self._logAction = QtGui.QAction(None)
+ self._logAction.setText("Log")
+ self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
+ self._logAction.triggered.connect(self._on_log)
+
+ self._quitAction = QtGui.QAction(None)
+ self._quitAction.setText("Quit")
+ self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
+ self._quitAction.triggered.connect(self._on_quit)
+
+ self._aboutAction = QtGui.QAction(None)
+ self._aboutAction.setText("About")
+ self._aboutAction.triggered.connect(self._on_about)
+
+ self._qapp.lastWindowClosed.connect(self._on_app_quit)
+ self._mainWindow = self._new_main_window()
+ self._mainWindow.window.destroyed.connect(self._on_child_close)
+
+ self.load_settings()
+
+ self._mainWindow.show()
+ self._idleDelay = QtCore.QTimer()
+ self._idleDelay.setSingleShot(True)
+ self._idleDelay.setInterval(0)
+ self._idleDelay.timeout.connect(self._on_delayed_start)
+ self._idleDelay.start()
+
+ def load_settings(self):
+ raise NotImplementedError("Booh")
+
+ def save_settings(self):
+ raise NotImplementedError("Booh")
+
+ def _new_main_window(self):
+ raise NotImplementedError("Booh")
+
+ @property
+ def qapp(self):
+ return self._qapp
+
+ @property
+ def constants(self):
+ return self._constants
+
+ @property
+ def errorLog(self):
+ return self._errorLog
+
+ @property
+ def fullscreenAction(self):
+ return self._fullscreenAction
+
+ @property
+ def orientationAction(self):
+ return self._orientationAction
+
+ @property
+ def orientation(self):
+ return self._orientation
+
+ @property
+ def logAction(self):
+ return self._logAction
+
+ @property
+ def aboutAction(self):
+ return self._aboutAction
+
+ @property
+ def quitAction(self):
+ return self._quitAction
+
+ def set_orientation(self, orientation):
+ self._orientation = orientation
+ self._mainWindow.update_orientation(self._orientation)
+
+ @classmethod
+ def _next_orientation(cls, current):
+ return {
+ cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
+ cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
+ cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
+ cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
+ }[current]
+
+ def _close_windows(self):
+ if self._mainWindow is not None:
+ self.save_settings()
+ self._mainWindow.window.destroyed.disconnect(self._on_child_close)
+ self._mainWindow.close()
+ self._mainWindow = None
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_start(self):
+ self._mainWindow.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_app_quit(self, checked = False):
+ if self._mainWindow is not None:
+ self.save_settings()
+ self._mainWindow.destroy()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_child_close(self, obj = None):
+ if self._mainWindow is not None:
+ self.save_settings()
+ self._mainWindow = None
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_toggle_fullscreen(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._mainWindow.set_fullscreen(checked)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_next_orientation(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ self.set_orientation(self._next_orientation(self._orientation))
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_about(self, checked = True):
+ raise NotImplementedError("Booh")
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_log(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ with open(self._constants._user_logpath_, "r") as f:
+ logLines = f.xreadlines()
+ log = "".join(logLines)
+ self._clipboard.setText(log)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_quit(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._close_windows()
+
+
+class WindowWrapper(object):
+
+ def __init__(self, parent, app):
+ self._app = app
+
+ self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog)
+
+ self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight)
+ self._layout.setContentsMargins(0, 0, 0, 0)
+
+ self._superLayout = QtGui.QVBoxLayout()
+ self._superLayout.addWidget(self._errorDisplay.toplevel)
+ self._superLayout.setContentsMargins(0, 0, 0, 0)
+ self._superLayout.addLayout(self._layout)
+
+ centralWidget = QtGui.QWidget()
+ centralWidget.setLayout(self._superLayout)
+ centralWidget.setContentsMargins(0, 0, 0, 0)
+
+ self._window = qui_utils.QSignalingMainWindow(parent)
+ self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
+ qui_utils.set_stackable(self._window, True)
+ self._window.setCentralWidget(centralWidget)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ self._window.addAction(self._closeWindowAction)
+ self._window.addAction(self._app.quitAction)
+ self._window.addAction(self._app.fullscreenAction)
+ self._window.addAction(self._app.orientationAction)
+ self._window.addAction(self._app.logAction)
+
+ @property
+ def window(self):
+ return self._window
+
+ @property
+ def windowOrientation(self):
+ geom = self._window.size()
+ if geom.width() <= geom.height():
+ return QtCore.Qt.Vertical
+ else:
+ return QtCore.Qt.Horizontal
+
+ @property
+ def idealWindowOrientation(self):
+ if self._app.orientation == self._app.AUTO_ORIENTATION:
+ windowOrientation = self.windowOrientation
+ elif self._app.orientation == self._app.DEFAULT_ORIENTATION:
+ windowOrientation = qui_utils.screen_orientation()
+ elif self._app.orientation == self._app.LANDSCAPE_ORIENTATION:
+ windowOrientation = QtCore.Qt.Horizontal
+ elif self._app.orientation == self._app.PORTRAIT_ORIENTATION:
+ windowOrientation = QtCore.Qt.Vertical
+ else:
+ raise RuntimeError("Bad! No %r for you" % self._app.orientation)
+ return windowOrientation
+
+ def walk_children(self):
+ return ()
+
+ def start(self):
+ pass
+
+ def close(self):
+ for child in self.walk_children():
+ child.window.destroyed.disconnect(self._on_child_close)
+ child.close()
+ self._window.close()
+
+ def destroy(self):
+ pass
+
+ def show(self):
+ self._window.show()
+ for child in self.walk_children():
+ child.show()
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+
+ def hide(self):
+ for child in self.walk_children():
+ child.hide()
+ self._window.hide()
+
+ def set_fullscreen(self, isFullscreen):
+ if self._window.isVisible():
+ if isFullscreen:
+ self._window.showFullScreen()
+ else:
+ self._window.showNormal()
+ for child in self.walk_children():
+ child.set_fullscreen(isFullscreen)
+
+ def update_orientation(self, orientation):
+ if orientation == self._app.DEFAULT_ORIENTATION:
+ qui_utils.set_autorient(self.window, False)
+ qui_utils.set_window_orientation(self.window, None)
+ elif orientation == self._app.AUTO_ORIENTATION:
+ qui_utils.set_autorient(self.window, True)
+ qui_utils.set_window_orientation(self.window, None)
+ elif orientation == self._app.LANDSCAPE_ORIENTATION:
+ qui_utils.set_autorient(self.window, False)
+ qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+ elif orientation == self._app.PORTRAIT_ORIENTATION:
+ qui_utils.set_autorient(self.window, False)
+ qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
+ else:
+ raise RuntimeError("Unknown orientation: %r" % orientation)
+ for child in self.walk_children():
+ child.update_orientation(orientation)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_child_close(self, obj = None):
+ raise NotImplementedError("Booh")
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self.close()
+
+
+class AutoFreezeWindowFeature(object):
+
+ def __init__(self, app, window):
+ self._app = app
+ self._window = window
+ self._app.qapp.focusChanged.connect(self._on_focus_changed)
+ if self._app.qapp.focusWidget() is not None:
+ self._window.setUpdatesEnabled(True)
+ else:
+ self._window.setUpdatesEnabled(False)
+
+ def close(self):
+ self._app.qapp.focusChanged.disconnect(self._on_focus_changed)
+ self._window.setUpdatesEnabled(True)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_focus_changed(self, oldWindow, newWindow):
+ with qui_utils.notify_error(self._app.errorLog):
+ if oldWindow is None and newWindow is not None:
+ self._window.setUpdatesEnabled(True)
+ elif oldWindow is not None and newWindow is None:
+ self._window.setUpdatesEnabled(False)
--- /dev/null
+from datetime import tzinfo, timedelta, datetime
+
+ZERO = timedelta(0)
+HOUR = timedelta(hours=1)
+
+
+def first_sunday_on_or_after(dt):
+ days_to_go = 6 - dt.weekday()
+ if days_to_go:
+ dt += timedelta(days_to_go)
+ return dt
+
+
+# US DST Rules
+#
+# This is a simplified (i.e., wrong for a few cases) set of rules for US
+# DST start and end times. For a complete and up-to-date set of DST rules
+# and timezone definitions, visit the Olson Database (or try pytz):
+# http://www.twinsun.com/tz/tz-link.htm
+# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
+#
+# In the US, since 2007, DST starts at 2am (standard time) on the second
+# Sunday in March, which is the first Sunday on or after Mar 8.
+DSTSTART_2007 = datetime(1, 3, 8, 2)
+# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 1)
+# From 1987 to 2006, DST used to start at 2am (standard time) on the first
+# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday of October, which is the first Sunday on or after Oct 25.
+DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
+DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+# From 1967 to 1986, DST used to start at 2am (standard time) on the last
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
+# 1am standard time) on the last Sunday of October, which is the first Sunday
+# on or after Oct 25.
+DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
+DSTEND_1967_1986 = DSTEND_1987_2006
+
+
+class USTimeZone(tzinfo):
+
+ def __init__(self, hours, reprname, stdname, dstname):
+ self.stdoffset = timedelta(hours=hours)
+ self.reprname = reprname
+ self.stdname = stdname
+ self.dstname = dstname
+
+ def __repr__(self):
+ return self.reprname
+
+ def tzname(self, dt):
+ if self.dst(dt):
+ return self.dstname
+ else:
+ return self.stdname
+
+ def utcoffset(self, dt):
+ return self.stdoffset + self.dst(dt)
+
+ def dst(self, dt):
+ if dt is None or dt.tzinfo is None:
+ # An exception may be sensible here, in one or both cases.
+ # It depends on how you want to treat them. The default
+ # fromutc() implementation (called by the default astimezone()
+ # implementation) passes a datetime with dt.tzinfo is self.
+ return ZERO
+ assert dt.tzinfo is self
+
+ # Find start and end times for US DST. For years before 1967, return
+ # ZERO for no DST.
+ if 2006 < dt.year:
+ dststart, dstend = DSTSTART_2007, DSTEND_2007
+ elif 1986 < dt.year < 2007:
+ dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+ elif 1966 < dt.year < 1987:
+ dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+ else:
+ return ZERO
+
+ start = first_sunday_on_or_after(dststart.replace(year=dt.year))
+ end = first_sunday_on_or_after(dstend.replace(year=dt.year))
+
+ # Can't compare naive to aware objects, so strip the timezone from
+ # dt first.
+ if start <= dt.replace(tzinfo=None) < end:
+ return HOUR
+ else:
+ return ZERO
+
+
+Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
+Central = USTimeZone(-6, "Central", "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
--- /dev/null
+#!/usr/bin/env python
+
+import logging
+
+import dbus
+import telepathy
+
+import util.go_utils as gobject_utils
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
+
+
+class WasMissedCall(object):
+
+ def __init__(self, bus, conn, chan, on_success, on_error):
+ self.__on_success = on_success
+ self.__on_error = on_error
+
+ self._requested = None
+ self._didMembersChange = False
+ self._didClose = False
+ self._didReport = False
+
+ self._onTimeout = gobject_utils.Timeout(self._on_timeout)
+ self._onTimeout.start(seconds=60)
+
+ chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal(
+ "MembersChanged",
+ self._on_members_changed,
+ )
+
+ chan[telepathy.interfaces.CHANNEL].connect_to_signal(
+ "Closed",
+ self._on_closed,
+ )
+
+ chan[DBUS_PROPERTIES].GetAll(
+ telepathy.interfaces.CHANNEL_INTERFACE,
+ reply_handler = self._on_got_all,
+ error_handler = self._on_error,
+ )
+
+ def cancel(self):
+ self._report_error("by request")
+
+ def _report_missed_if_ready(self):
+ if self._didReport:
+ pass
+ elif self._requested is not None and (self._didMembersChange or self._didClose):
+ if self._requested:
+ self._report_error("wrong direction")
+ elif self._didClose:
+ self._report_success()
+ else:
+ self._report_error("members added")
+ else:
+ if self._didClose:
+ self._report_error("closed too early")
+
+ def _report_success(self):
+ assert not self._didReport, "Double reporting a missed call"
+ self._didReport = True
+ self._onTimeout.cancel()
+ self.__on_success(self)
+
+ def _report_error(self, reason):
+ assert not self._didReport, "Double reporting a missed call"
+ self._didReport = True
+ self._onTimeout.cancel()
+ self.__on_error(self, reason)
+
+ @misc.log_exception(_moduleLogger)
+ def _on_got_all(self, properties):
+ self._requested = properties["Requested"]
+ self._report_missed_if_ready()
+
+ @misc.log_exception(_moduleLogger)
+ def _on_members_changed(self, message, added, removed, lp, rp, actor, reason):
+ if added:
+ self._didMembersChange = True
+ self._report_missed_if_ready()
+
+ @misc.log_exception(_moduleLogger)
+ def _on_closed(self):
+ self._didClose = True
+ self._report_missed_if_ready()
+
+ @misc.log_exception(_moduleLogger)
+ def _on_error(self, *args):
+ self._report_error(args)
+
+ @misc.log_exception(_moduleLogger)
+ def _on_timeout(self):
+ self._report_error("timeout")
+ return False
+
+
+class NewChannelSignaller(object):
+
+ def __init__(self, on_new_channel):
+ self._sessionBus = dbus.SessionBus()
+ self._on_user_new_channel = on_new_channel
+
+ def start(self):
+ self._sessionBus.add_signal_receiver(
+ self._on_new_channel,
+ "NewChannel",
+ "org.freedesktop.Telepathy.Connection",
+ None,
+ None
+ )
+
+ def stop(self):
+ self._sessionBus.remove_signal_receiver(
+ self._on_new_channel,
+ "NewChannel",
+ "org.freedesktop.Telepathy.Connection",
+ None,
+ None
+ )
+
+ @misc.log_exception(_moduleLogger)
+ def _on_new_channel(
+ self, channelObjectPath, channelType, handleType, handle, supressHandler
+ ):
+ connObjectPath = channel_path_to_conn_path(channelObjectPath)
+ serviceName = path_to_service_name(channelObjectPath)
+ try:
+ self._on_user_new_channel(
+ self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType
+ )
+ except Exception:
+ _moduleLogger.exception("Blocking exception from being passed up")
+
+
+class EnableSystemContactIntegration(object):
+
+ ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager"
+ ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager"
+ ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query"
+ ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat"
+ ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile"
+ DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
+
+ def __init__(self, profileName):
+ self._bus = dbus.SessionBus()
+ self._profileName = profileName
+
+ def start(self):
+ self._accountManager = self._bus.get_object(
+ self.ACCOUNT_MGR_NAME,
+ self.ACCOUNT_MGR_PATH,
+ )
+ self._accountManagerQuery = dbus.Interface(
+ self._accountManager,
+ dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY,
+ )
+
+ self._accountManagerQuery.FindAccounts(
+ {
+ self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName,
+ },
+ reply_handler = self._on_found_accounts_reply,
+ error_handler = self._on_error,
+ )
+
+ @misc.log_exception(_moduleLogger)
+ def _on_found_accounts_reply(self, accountObjectPaths):
+ for accountObjectPath in accountObjectPaths:
+ print accountObjectPath
+ account = self._bus.get_object(
+ self.ACCOUNT_MGR_NAME,
+ accountObjectPath,
+ )
+ accountProperties = dbus.Interface(
+ account,
+ self.DBUS_PROPERTIES,
+ )
+ accountProperties.Set(
+ self.ACCOUNT_IFACE_COMPAT,
+ "SecondaryVCardFields",
+ ["TEL"],
+ reply_handler = self._on_field_set,
+ error_handler = self._on_error,
+ )
+
+ @misc.log_exception(_moduleLogger)
+ def _on_field_set(self):
+ _moduleLogger.info("SecondaryVCardFields Set")
+
+ @misc.log_exception(_moduleLogger)
+ def _on_error(self, error):
+ _moduleLogger.error("%r" % (error, ))
+
+
+def channel_path_to_conn_path(channelObjectPath):
+ """
+ >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+ '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME'
+ """
+ return channelObjectPath.rsplit("/", 1)[0]
+
+
+def path_to_service_name(path):
+ """
+ >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+ 'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME'
+ """
+ return ".".join(path[1:].split("/")[0:7])
+
+
+def cm_from_path(path):
+ """
+ >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+ 'theonering'
+ """
+ return path[1:].split("/")[4]
--- /dev/null
+dialcentral/
\ No newline at end of file
+++ /dev/null
-#!/usr/bin/env python
+++ /dev/null
-#!/usr/bin/env python
-
-import os
-import time
-import datetime
-import ConfigParser
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-import dbus
-
-
-_FREMANTLE_ALARM = "Fremantle"
-_DIABLO_ALARM = "Diablo"
-_NO_ALARM = "None"
-
-
-try:
- import alarm
- ALARM_TYPE = _FREMANTLE_ALARM
-except (ImportError, OSError):
- try:
- import osso.alarmd as alarmd
- ALARM_TYPE = _DIABLO_ALARM
- except (ImportError, OSError):
- ALARM_TYPE = _NO_ALARM
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-def _get_start_time(recurrence):
- now = datetime.datetime.now()
- startTimeMinute = now.minute + max(recurrence, 5) # being safe
- startTimeHour = now.hour + int(startTimeMinute / 60)
- startTimeMinute = startTimeMinute % 59
- now.replace(minute=startTimeMinute)
- timestamp = int(time.mktime(now.timetuple()))
- return timestamp
-
-
-def _create_recurrence_mask(recurrence, base):
- """
- >>> bin(_create_recurrence_mask(60, 60))
- '0b1'
- >>> bin(_create_recurrence_mask(30, 60))
- '0b1000000000000000000000000000001'
- >>> bin(_create_recurrence_mask(2, 60))
- '0b10101010101010101010101010101010101010101010101010101010101'
- >>> bin(_create_recurrence_mask(1, 60))
- '0b111111111111111111111111111111111111111111111111111111111111'
- """
- mask = 0
- for i in xrange(base / recurrence):
- mask |= 1 << (recurrence * i)
- return mask
-
-
-def _unpack_minutes(recurrence):
- """
- >>> _unpack_minutes(0)
- (0, 0, 0)
- >>> _unpack_minutes(1)
- (0, 0, 1)
- >>> _unpack_minutes(59)
- (0, 0, 59)
- >>> _unpack_minutes(60)
- (0, 1, 0)
- >>> _unpack_minutes(129)
- (0, 2, 9)
- >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
- (5, 3, 2)
- >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
- (5, 3, 2)
- """
- minutesInAnHour = 60
- minutesInDay = 24 * minutesInAnHour
- minutesInAWeek = minutesInDay * 7
-
- days = recurrence / minutesInDay
- daysOfWeek = days % 7
- recurrence -= days * minutesInDay
- hours = recurrence / minutesInAnHour
- recurrence -= hours * minutesInAnHour
- mins = recurrence % minutesInAnHour
- recurrence -= mins
- assert recurrence == 0, "Recurrence %d" % recurrence
- return daysOfWeek, hours, mins
-
-
-class _FremantleAlarmHandler(object):
-
- _INVALID_COOKIE = -1
- _REPEAT_FOREVER = -1
- _TITLE = "Dialcentral Notifications"
- _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
-
- def __init__(self):
- self._recurrence = 5
-
- self._alarmCookie = self._INVALID_COOKIE
- self._launcher = self._LAUNCHER
-
- def load_settings(self, config, sectionName):
- try:
- self._recurrence = config.getint(sectionName, "recurrence")
- self._alarmCookie = config.getint(sectionName, "alarmCookie")
- launcher = config.get(sectionName, "notifier")
- if launcher:
- self._launcher = launcher
- except ConfigParser.NoOptionError:
- pass
- except ConfigParser.NoSectionError:
- pass
-
- def save_settings(self, config, sectionName):
- try:
- config.set(sectionName, "recurrence", str(self._recurrence))
- config.set(sectionName, "alarmCookie", str(self._alarmCookie))
- launcher = self._launcher if self._launcher != self._LAUNCHER else ""
- config.set(sectionName, "notifier", launcher)
- except ConfigParser.NoOptionError:
- pass
- except ConfigParser.NoSectionError:
- pass
-
- def apply_settings(self, enabled, recurrence):
- if recurrence != self._recurrence or enabled != self.isEnabled:
- if self.isEnabled:
- self._clear_alarm()
- if enabled:
- self._set_alarm(recurrence)
- self._recurrence = int(recurrence)
-
- @property
- def recurrence(self):
- return self._recurrence
-
- @property
- def isEnabled(self):
- return self._alarmCookie != self._INVALID_COOKIE
-
- def _set_alarm(self, recurrenceMins):
- assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
- alarmTime = _get_start_time(recurrenceMins)
-
- event = alarm.Event()
- event.appid = self._TITLE
- event.alarm_time = alarmTime
- event.recurrences_left = self._REPEAT_FOREVER
-
- action = event.add_actions(1)[0]
- action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
- action.command = self._launcher
-
- recurrence = event.add_recurrences(1)[0]
- recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
- recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
- recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
- recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
- recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
- recurrence.special |= alarm.RECUR_SPECIAL_NONE
-
- assert event.is_sane()
- self._alarmCookie = alarm.add_event(event)
-
- def _clear_alarm(self):
- if self._alarmCookie == self._INVALID_COOKIE:
- return
- alarm.delete_event(self._alarmCookie)
- self._alarmCookie = self._INVALID_COOKIE
-
-
-class _DiabloAlarmHandler(object):
-
- _INVALID_COOKIE = -1
- _TITLE = "Dialcentral Notifications"
- _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
- _REPEAT_FOREVER = -1
-
- def __init__(self):
- self._recurrence = 5
-
- bus = dbus.SystemBus()
- self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
- self._alarmCookie = self._INVALID_COOKIE
- self._launcher = self._LAUNCHER
-
- def load_settings(self, config, sectionName):
- try:
- self._recurrence = config.getint(sectionName, "recurrence")
- self._alarmCookie = config.getint(sectionName, "alarmCookie")
- launcher = config.get(sectionName, "notifier")
- if launcher:
- self._launcher = launcher
- except ConfigParser.NoOptionError:
- pass
- except ConfigParser.NoSectionError:
- pass
-
- def save_settings(self, config, sectionName):
- config.set(sectionName, "recurrence", str(self._recurrence))
- config.set(sectionName, "alarmCookie", str(self._alarmCookie))
- launcher = self._launcher if self._launcher != self._LAUNCHER else ""
- config.set(sectionName, "notifier", launcher)
-
- def apply_settings(self, enabled, recurrence):
- if recurrence != self._recurrence or enabled != self.isEnabled:
- if self.isEnabled:
- self._clear_alarm()
- if enabled:
- self._set_alarm(recurrence)
- self._recurrence = int(recurrence)
-
- @property
- def recurrence(self):
- return self._recurrence
-
- @property
- def isEnabled(self):
- return self._alarmCookie != self._INVALID_COOKIE
-
- def _set_alarm(self, recurrence):
- assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
- alarmTime = _get_start_time(recurrence)
-
- #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
- _DEFAULT_FLAGS = (
- alarmd.ALARM_EVENT_NO_DIALOG |
- alarmd.ALARM_EVENT_NO_SNOOZE |
- alarmd.ALARM_EVENT_CONNECTED
- )
- action = []
- action.extend(['flags', _DEFAULT_FLAGS])
- action.extend(['title', self._TITLE])
- action.extend(['path', self._launcher])
- action.extend([
- 'arguments',
- dbus.Array(
- [alarmTime, int(27)],
- signature=dbus.Signature('v')
- )
- ]) #int(27) used in place of alarm_index
-
- event = []
- event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
- event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus
- event.append(dbus.UInt32(len(action) / 2))
- event.extend(action)
- event.extend(['time', dbus.Int64(alarmTime)])
- event.extend(['recurr_interval', dbus.UInt32(recurrence)])
- event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
-
- self._alarmCookie = self._alarmdDBus.add_event(*event);
-
- def _clear_alarm(self):
- if self._alarmCookie == self._INVALID_COOKIE:
- return
- deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
- self._alarmCookie = self._INVALID_COOKIE
- assert deleteResult != -1, "Deleting of alarm event failed"
-
-
-class _ApplicationAlarmHandler(object):
-
- _REPEAT_FOREVER = -1
- _MIN_TO_MS_FACTORY = 1000 * 60
-
- def __init__(self):
- self._timer = QtCore.QTimer()
- self._timer.setSingleShot(False)
- self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
-
- def load_settings(self, config, sectionName):
- try:
- self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
- except ConfigParser.NoOptionError:
- pass
- except ConfigParser.NoSectionError:
- pass
- self._timer.start()
-
- def save_settings(self, config, sectionName):
- config.set(sectionName, "recurrence", str(self.recurrence))
-
- def apply_settings(self, enabled, recurrence):
- self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
- if enabled:
- self._timer.start()
- else:
- self._timer.stop()
-
- @property
- def notifySignal(self):
- return self._timer.timeout
-
- @property
- def recurrence(self):
- return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
-
- @property
- def isEnabled(self):
- return self._timer.isActive()
-
-
-class _NoneAlarmHandler(object):
-
- def __init__(self):
- self._enabled = False
- self._recurrence = 5
-
- def load_settings(self, config, sectionName):
- try:
- self._recurrence = config.getint(sectionName, "recurrence")
- self._enabled = True
- except ConfigParser.NoOptionError:
- pass
- except ConfigParser.NoSectionError:
- pass
-
- def save_settings(self, config, sectionName):
- config.set(sectionName, "recurrence", str(self.recurrence))
-
- def apply_settings(self, enabled, recurrence):
- self._enabled = enabled
-
- @property
- def recurrence(self):
- return self._recurrence
-
- @property
- def isEnabled(self):
- return self._enabled
-
-
-_BACKGROUND_ALARM_FACTORY = {
- _FREMANTLE_ALARM: _FremantleAlarmHandler,
- _DIABLO_ALARM: _DiabloAlarmHandler,
- _NO_ALARM: None,
-}[ALARM_TYPE]
-
-
-class AlarmHandler(object):
-
- ALARM_NONE = "No Alert"
- ALARM_BACKGROUND = "Background Alert"
- ALARM_APPLICATION = "Application Alert"
- ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
-
- ALARM_FACTORY = {
- ALARM_NONE: _NoneAlarmHandler,
- ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
- ALARM_APPLICATION: _ApplicationAlarmHandler,
- }
-
- def __init__(self):
- self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
- self._currentAlarmType = self.ALARM_NONE
-
- def load_settings(self, config, sectionName):
- try:
- self._currentAlarmType = config.get(sectionName, "alarm")
- except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
- _moduleLogger.exception("Falling back to old style")
- self._currentAlarmType = self.ALARM_BACKGROUND
- if self._currentAlarmType not in self.ALARM_TYPES:
- self._currentAlarmType = self.ALARM_NONE
-
- self._init_alarm(self._currentAlarmType)
- if self._currentAlarmType in self._alarms:
- self._alarms[self._currentAlarmType].load_settings(config, sectionName)
- if not self._alarms[self._currentAlarmType].isEnabled:
- _moduleLogger.info("Config file lied, not actually enabled")
- self._currentAlarmType = self.ALARM_NONE
- else:
- _moduleLogger.info("Background alerts not supported")
- self._currentAlarmType = self.ALARM_NONE
-
- def save_settings(self, config, sectionName):
- config.set(sectionName, "alarm", self._currentAlarmType)
- self._alarms[self._currentAlarmType].save_settings(config, sectionName)
-
- def apply_settings(self, t, recurrence):
- self._init_alarm(t)
- newHandler = self._alarms[t]
- oldHandler = self._alarms[self._currentAlarmType]
- if newHandler != oldHandler:
- oldHandler.apply_settings(False, 0)
- newHandler.apply_settings(True, recurrence)
- self._currentAlarmType = t
-
- @property
- def alarmType(self):
- return self._currentAlarmType
-
- @property
- def backgroundNotificationsSupported(self):
- return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
-
- @property
- def applicationNotifySignal(self):
- self._init_alarm(self.ALARM_APPLICATION)
- return self._alarms[self.ALARM_APPLICATION].notifySignal
-
- @property
- def recurrence(self):
- return self._alarms[self._currentAlarmType].recurrence
-
- @property
- def isEnabled(self):
- return self._currentAlarmType != self.ALARM_NONE
-
- def _init_alarm(self, t):
- if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
- self._alarms[t] = self.ALARM_FACTORY[t]()
-
-
-def main():
- logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
- logging.basicConfig(level=logging.DEBUG, format=logFormat)
- import constants
- try:
- import optparse
- except ImportError:
- return
-
- parser = optparse.OptionParser()
- parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
- parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
- parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
- parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
- (commandOptions, commandArgs) = parser.parse_args()
-
- alarmHandler = AlarmHandler()
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- alarmHandler.load_settings(config, "alarm")
-
- if commandOptions.display:
- print "Alarm (%s) is %s for every %d minutes" % (
- alarmHandler._alarmCookie,
- "enabled" if alarmHandler.isEnabled else "disabled",
- alarmHandler.recurrence,
- )
- else:
- isEnabled = commandOptions.enabled
- recurrence = commandOptions.recurrence
- alarmHandler.apply_settings(isEnabled, recurrence)
-
- alarmHandler.save_settings(config, "alarm")
- configFile = open(constants._user_settings_, "wb")
- try:
- config.write(configFile)
- finally:
- configFile.close()
-
-
-if __name__ == "__main__":
- main()
+++ /dev/null
-#!/usr/bin/env python
-
-import os
-import filecmp
-import ConfigParser
-import pprint
-import logging
-import logging.handlers
-
-import constants
-from backends.gvoice import gvoice
-
-
-def get_missed(backend):
- missedPage = backend._browser.download(backend._XML_MISSED_URL)
- missedJson = backend._grab_json(missedPage)
- return missedJson
-
-
-def get_voicemail(backend):
- voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL)
- voicemailJson = backend._grab_json(voicemailPage)
- return voicemailJson
-
-
-def get_sms(backend):
- smsPage = backend._browser.download(backend._XML_SMS_URL)
- smsJson = backend._grab_json(smsPage)
- return smsJson
-
-
-def remove_reltime(data):
- for messageData in data["messages"].itervalues():
- for badPart in [
- "relTime",
- "relativeStartTime",
- "time",
- "star",
- "isArchived",
- "isRead",
- "isSpam",
- "isTrash",
- "labels",
- ]:
- if badPart in messageData:
- del messageData[badPart]
- for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
- if globalBad in data:
- del data[globalBad]
-
-
-def is_type_changed(backend, type, get_material):
- jsonMaterial = get_material(backend)
- unreadCount = jsonMaterial["unreadCounts"][type]
-
- previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
- currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
-
- try:
- os.remove(previousSnapshotPath)
- except OSError, e:
- # check if failed purely because the old file didn't exist, which is fine
- if e.errno != 2:
- raise
- try:
- os.rename(currentSnapshotPath, previousSnapshotPath)
- previousExists = True
- except OSError, e:
- # check if failed purely because the new old file didn't exist, which is fine
- if e.errno != 2:
- raise
- previousExists = False
-
- remove_reltime(jsonMaterial)
- textMaterial = pprint.pformat(jsonMaterial)
- currentSnapshot = file(currentSnapshotPath, "w")
- try:
- currentSnapshot.write(textMaterial)
- finally:
- currentSnapshot.close()
-
- if unreadCount == 0 or not previousExists:
- return False
-
- seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
- return not seemEqual
-
-
-def create_backend(config):
- gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
- backend = gvoice.GVoiceBackend(gvCookiePath)
-
- loggedIn = False
-
- if not loggedIn:
- loggedIn = backend.refresh_account_info() is not None
-
- if not loggedIn:
- import base64
- try:
- blobs = (
- config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
- for i in xrange(2)
- )
- creds = (
- base64.b64decode(blob)
- for blob in blobs
- )
- username, password = tuple(creds)
- loggedIn = backend.login(username, password) is not None
- except ConfigParser.NoOptionError, e:
- pass
- except ConfigParser.NoSectionError, e:
- pass
-
- assert loggedIn
- return backend
-
-
-def is_changed(config, backend):
- try:
- notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
- notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
- notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
- except ConfigParser.NoOptionError, e:
- notifyOnMissed = False
- notifyOnVoicemail = False
- notifyOnSms = False
- except ConfigParser.NoSectionError, e:
- notifyOnMissed = False
- notifyOnVoicemail = False
- notifyOnSms = False
- logging.debug(
- "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms)
- )
-
- notifySources = []
- if notifyOnMissed:
- notifySources.append(("missed", get_missed))
- if notifyOnVoicemail:
- notifySources.append(("voicemail", get_voicemail))
- if notifyOnSms:
- notifySources.append(("sms", get_sms))
-
- notifyUser = False
- for type, get_material in notifySources:
- if is_type_changed(backend, type, get_material):
- notifyUser = True
- return notifyUser
-
-
-def notify_on_change():
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- backend = create_backend(config)
- notifyUser = is_changed(config, backend)
-
- if notifyUser:
- logging.info("Changed")
- import led_handler
- led = led_handler.LedHandler()
- led.on()
- else:
- logging.info("No Change")
-
-
-if __name__ == "__main__":
- logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
- logging.basicConfig(level=logging.DEBUG, format=logFormat)
- rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
- rotating.setFormatter(logging.Formatter(logFormat))
- root = logging.getLogger()
- root.addHandler(rotating)
- logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
- logging.info("OS: %s" % (os.uname()[0], ))
- logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
- logging.info("Hostname: %s" % os.uname()[1])
- try:
- notify_on_change()
- except:
- logging.exception("Error")
- raise
+++ /dev/null
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's Grand Central service.
-Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Filesystem backend for contact support
-"""
-
-from __future__ import with_statement
-
-import os
-import csv
-
-
-def try_unicode(s):
- try:
- return s.decode("UTF-8")
- except UnicodeDecodeError:
- return s
-
-
-class CsvAddressBook(object):
- """
- Currently supported file format
- @li Has the first line as a header
- @li Escapes with quotes
- @li Comma as delimiter
- @li Column 0 is name, column 1 is number
- """
-
- def __init__(self, name, csvPath):
- self._name = name
- self._csvPath = csvPath
- self._contacts = {}
-
- @property
- def name(self):
- return self._name
-
- def update_account(self, force = True):
- if not force or not self._contacts:
- return
- self._contacts = dict(
- self._read_csv(self._csvPath)
- )
-
- def get_contacts(self):
- """
- @returns Iterable of (contact id, contact name)
- """
- if not self._contacts:
- self._contacts = dict(
- self._read_csv(self._csvPath)
- )
- return self._contacts
-
- def _read_csv(self, csvPath):
- try:
- f = open(csvPath, "rU")
- csvReader = iter(csv.reader(f))
- except IOError, e:
- if e.errno == 2:
- return
- raise
-
- header = csvReader.next()
- nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
-
- yieldCount = 0
- for row in csvReader:
- contactDetails = []
- for (phoneType, phoneColumn) in phoneColumns:
- try:
- if len(row[phoneColumn]) == 0:
- continue
- contactDetails.append({
- "phoneType": try_unicode(phoneType),
- "phoneNumber": row[phoneColumn],
- })
- except IndexError:
- pass
- if 0 < len(contactDetails):
- nameParts = (row[i].strip() for i in nameColumns)
- nameParts = (part for part in nameParts if part)
- fullName = " ".join(nameParts).strip()
- if not fullName:
- for fallbackColumn in nameFallbacks:
- if row[fallbackColumn].strip():
- fullName = row[fallbackColumn].strip()
- break
- else:
- fullName = "Unknown"
- fullName = try_unicode(fullName)
- yield str(yieldCount), {
- "contactId": "%s-%d" % (self._name, yieldCount),
- "name": fullName,
- "numbers": contactDetails,
- }
- yieldCount += 1
-
- @classmethod
- def _guess_columns(cls, row):
- firstMiddleLast = [-1, -1, -1]
- names = []
- nameFallbacks = []
- phones = []
- for i, item in enumerate(row):
- lowerItem = item.lower()
- if 0 <= lowerItem.find("name"):
- names.append((item, i))
-
- if 0 <= lowerItem.find("couple"):
- names.insert(0, (item, i))
-
- if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"):
- firstMiddleLast[0] = i
- elif 0 <= lowerItem.find("middle"):
- firstMiddleLast[1] = i
- elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"):
- firstMiddleLast[2] = i
- elif 0 <= lowerItem.find("phone"):
- phones.append((item, i))
- elif 0 <= lowerItem.find("mobile"):
- phones.append((item, i))
- elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"):
- nameFallbacks.append(i)
- if len(names) == 0:
- names.append(("Name", 0))
- if len(phones) == 0:
- phones.append(("Phone", 1))
-
- nameColumns = [i for i in firstMiddleLast if 0 <= i]
- if len(nameColumns) < 2:
- del nameColumns[:]
- nameColumns.append(names[0][1])
-
- return nameColumns, nameFallbacks, phones
-
-
-class FilesystemAddressBookFactory(object):
-
- FILETYPE_SUPPORT = {
- "csv": CsvAddressBook,
- }
-
- def __init__(self, path):
- self._path = path
-
- def get_addressbooks(self):
- for root, dirs, filenames in os.walk(self._path):
- for filename in filenames:
- try:
- name, ext = filename.rsplit(".", 1)
- except ValueError:
- continue
-
- try:
- cls = self.FILETYPE_SUPPORT[ext]
- except KeyError:
- continue
- yield cls(name, os.path.join(root, filename))
+++ /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 itertools
-import logging
-
-from gvoice import gvoice
-
-from util import io as io_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class GVDialer(object):
-
- MESSAGE_TEXTS = "Text"
- MESSAGE_VOICEMAILS = "Voicemail"
- MESSAGE_ALL = "All"
-
- HISTORY_RECEIVED = "Received"
- HISTORY_MISSED = "Missed"
- HISTORY_PLACED = "Placed"
- HISTORY_ALL = "All"
-
- def __init__(self, cookieFile = None):
- self._gvoice = gvoice.GVoiceBackend(cookieFile)
- self._texts = []
- self._voicemails = []
- self._received = []
- self._missed = []
- self._placed = []
-
- def is_quick_login_possible(self):
- """
- @returns True then refresh_account_info might be enough to login, else full login is required
- """
- return self._gvoice.is_quick_login_possible()
-
- def refresh_account_info(self):
- return self._gvoice.refresh_account_info()
-
- def login(self, username, password):
- """
- Attempt to login to GoogleVoice
- @returns Whether login was successful or not
- """
- return self._gvoice.login(username, password)
-
- def logout(self):
- self._texts = []
- self._voicemails = []
- self._received = []
- self._missed = []
- self._placed = []
- return self._gvoice.logout()
-
- def persist(self):
- return self._gvoice.persist()
-
- def is_dnd(self):
- return self._gvoice.is_dnd()
-
- def set_dnd(self, doNotDisturb):
- return self._gvoice.set_dnd(doNotDisturb)
-
- def call(self, outgoingNumber):
- """
- This is the main function responsible for initating the callback
- """
- return self._gvoice.call(outgoingNumber)
-
- def cancel(self, outgoingNumber=None):
- """
- Cancels a call matching outgoing and forwarding numbers (if given).
- Will raise an error if no matching call is being placed
- """
- return self._gvoice.cancel(outgoingNumber)
-
- def send_sms(self, phoneNumbers, message):
- self._gvoice.send_sms(phoneNumbers, message)
-
- def search(self, query):
- """
- Search your Google Voice Account history for calls, voicemails, and sms
- Returns ``Folder`` instance containting matching messages
- """
- return self._gvoice.search(query)
-
- def get_feed(self, feed):
- return self._gvoice.get_feed(feed)
-
- def download(self, messageId, targetPath):
- """
- Download a voicemail or recorded call MP3 matching the given ``msg``
- which can either be a ``Message`` instance, or a SHA1 identifier.
- Message hashes can be found in ``self.voicemail().messages`` for example.
- Returns location of saved file.
- """
- self._gvoice.download(messageId, targetPath)
-
- def is_valid_syntax(self, number):
- """
- @returns If This number be called ( syntax validation only )
- """
- return self._gvoice.is_valid_syntax(number)
-
- def get_account_number(self):
- """
- @returns The GoogleVoice phone number
- """
- return self._gvoice.get_account_number()
-
- def get_callback_numbers(self):
- """
- @returns a dictionary mapping call back numbers to descriptions
- @note These results are cached for 30 minutes.
- """
- return self._gvoice.get_callback_numbers()
-
- def set_callback_number(self, callbacknumber):
- """
- Set the number that GoogleVoice calls
- @param callbacknumber should be a proper 10 digit number
- """
- return self._gvoice.set_callback_number(callbacknumber)
-
- def get_callback_number(self):
- """
- @returns Current callback number or None
- """
- return self._gvoice.get_callback_number()
-
- def get_call_history(self, historyType):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- """
- history = list(self._get_call_history(historyType))
- history.sort(key=lambda item: item["time"])
- return history
-
- def _get_call_history(self, historyType):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- """
- if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
- self._received = list(self._gvoice.get_received_calls())
- for item in self._received:
- item["action"] = self.HISTORY_RECEIVED
- if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
- self._missed = list(self._gvoice.get_missed_calls())
- for item in self._missed:
- item["action"] = self.HISTORY_MISSED
- if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
- self._placed = list(self._gvoice.get_placed_calls())
- for item in self._placed:
- item["action"] = self.HISTORY_PLACED
- received = self._received
- missed = self._missed
- placed = self._placed
- for item in received:
- yield item
- for item in missed:
- yield item
- for item in placed:
- yield item
-
- def get_messages(self, messageType):
- messages = list(self._get_messages(messageType))
- messages.sort(key=lambda message: message["time"])
- return messages
-
- def _get_messages(self, messageType):
- if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
- self._voicemails = list(self._gvoice.get_voicemails())
- if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
- self._texts = list(self._gvoice.get_texts())
- voicemails = self._voicemails
- smss = self._texts
-
- conversations = itertools.chain(voicemails, smss)
- for conversation in conversations:
- messages = conversation.messages
- messageParts = [
- (message.whoFrom, self._format_message(message), message.when)
- for message in messages
- ]
-
- messageDetails = {
- "id": conversation.id,
- "contactId": conversation.contactId,
- "name": conversation.name,
- "time": conversation.time,
- "relTime": conversation.relTime,
- "prettyNumber": conversation.prettyNumber,
- "number": conversation.number,
- "location": conversation.location,
- "messageParts": messageParts,
- "type": conversation.type,
- "isRead": conversation.isRead,
- "isTrash": conversation.isTrash,
- "isSpam": conversation.isSpam,
- "isArchived": conversation.isArchived,
- }
- yield messageDetails
-
- def clear_caches(self):
- pass
-
- def get_addressbooks(self):
- """
- @returns Iterable of (Address Book Factory, Book Id, Book Name)
- """
- yield self, "", ""
-
- def open_addressbook(self, bookId):
- return self
-
- @staticmethod
- def contact_source_short_name(contactId):
- return "GV"
-
- @staticmethod
- def factory_name():
- return "Google Voice"
-
- def _format_message(self, message):
- messagePartFormat = {
- "med1": "<i>%s</i>",
- "med2": "%s",
- "high": "<b>%s</b>",
- }
- return " ".join(
- messagePartFormat[text.accuracy] % io_utils.escape(text.text)
- for text in message.body
- )
-
-
-def sort_messages(allMessages):
- sortableAllMessages = [
- (message["time"], message)
- for message in allMessages
- ]
- sortableAllMessages.sort(reverse=True)
- return (
- message
- for (exactTime, message) in sortableAllMessages
- )
-
-
-def decorate_recent(recentCallData):
- """
- @returns (personsName, phoneNumber, date, action)
- """
- contactId = recentCallData["contactId"]
- if recentCallData["name"]:
- header = recentCallData["name"]
- elif recentCallData["prettyNumber"]:
- header = recentCallData["prettyNumber"]
- elif recentCallData["location"]:
- header = recentCallData["location"]
- else:
- header = "Unknown"
-
- number = recentCallData["number"]
- relTime = recentCallData["relTime"]
- action = recentCallData["action"]
- return contactId, header, number, relTime, action
-
-
-def decorate_message(messageData):
- contactId = messageData["contactId"]
- exactTime = messageData["time"]
- if messageData["name"]:
- header = messageData["name"]
- elif messageData["prettyNumber"]:
- header = messageData["prettyNumber"]
- else:
- header = "Unknown"
- number = messageData["number"]
- relativeTime = messageData["relTime"]
-
- messageParts = list(messageData["messageParts"])
- if len(messageParts) == 0:
- messages = ("No Transcription", )
- elif len(messageParts) == 1:
- messages = (messageParts[0][1], )
- else:
- messages = [
- "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
- for messagePart in messageParts
- ]
-
- decoratedResults = contactId, header, number, relativeTime, messages
- return decoratedResults
+++ /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(25)
-
-
-def add_proxy(protocol, url, port):
- proxyInfo = "%s:%s" % (url, port)
- proxy = urllib2.ProxyHandler(
- {protocol: proxyInfo}
- )
- opener = urllib2.build_opener(proxy)
- urllib2.install_opener(opener)
-
-
-class MozillaEmulator(object):
-
- USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
- #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
-
- def __init__(self, trycount = 1):
- """Create a new MozillaEmulator object.
-
- @param trycount: The download() method will retry the operation if it
- fails. You can specify -1 for infinite retrying. A value of 0 means no
- retrying. A value of 1 means one retry. etc."""
- self.debug = False
- self.trycount = trycount
- self._cookies = cookielib.LWPCookieJar()
- self._loadedFromCookies = False
- self._storeCookies = False
-
- def load_cookies(self, path):
- assert not self._loadedFromCookies, "Load cookies only once"
- if path is None:
- return
-
- self._cookies.filename = path
- try:
- self._cookies.load()
- except cookielib.LoadError:
- _moduleLogger.exception("Bad cookie file")
- except IOError:
- _moduleLogger.exception("No cookie file")
- except Exception, e:
- _moduleLogger.exception("Unknown error with cookies")
- else:
- self._loadedFromCookies = True
- self._storeCookies = True
-
- return self._loadedFromCookies
-
- def save_cookies(self):
- if self._storeCookies:
- self._cookies.save()
-
- def clear_cookies(self):
- if self._storeCookies:
- self._cookies.clear()
-
- def download(self, url,
- postdata = None, extraheaders = None, forbidRedirect = False,
- trycount = None, only_head = False,
- ):
- """Download an URL with GET or POST methods.
-
- @param postdata: It can be a string that will be POST-ed to the URL.
- When None is given, the method will be GET instead.
- @param extraheaders: You can add/modify HTTP headers with a dict here.
- @param forbidRedirect: Set this flag if you do not want to handle
- HTTP 301 and 302 redirects.
- @param trycount: Specify the maximum number of retries here.
- 0 means no retry on error. Using -1 means infinite retring.
- None means the default value (that is self.trycount).
- @param only_head: Create the openerdirector and return it. In other
- words, this will not retrieve any content except HTTP headers.
-
- @return: The raw HTML page data
- """
- _moduleLogger.debug("Performing download of %s" % url)
-
- if extraheaders is None:
- extraheaders = {}
- if trycount is None:
- trycount = self.trycount
- cnt = 0
-
- while True:
- try:
- req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect)
- openerdirector = u.open(req)
- if self.debug:
- _moduleLogger.info("%r - %r" % (req.get_method(), url))
- _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg))
- _moduleLogger.info("%r" % (openerdirector.headers))
- self._cookies.extract_cookies(openerdirector, req)
- if only_head:
- return openerdirector
-
- return self._read(openerdirector, trycount)
- except urllib2.URLError, e:
- _moduleLogger.debug("%s: %s" % (e, url))
- cnt += 1
- if (-1 < trycount) and (trycount < cnt):
- raise
-
- # Retry :-)
- _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
-
- def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
- if extraheaders is None:
- extraheaders = {}
-
- txheaders = {
- 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
- 'Accept-Language': 'en,en-us;q=0.5',
- 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
- 'User-Agent': self.USER_AGENT,
- }
- for key, value in extraheaders.iteritems():
- txheaders[key] = value
- req = urllib2.Request(url, postdata, txheaders)
- self._cookies.add_cookie_header(req)
- if forbidRedirect:
- redirector = HTTPNoRedirector()
- #_moduleLogger.info("Redirection disabled")
- else:
- redirector = urllib2.HTTPRedirectHandler()
- #_moduleLogger.info("Redirection enabled")
-
- http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
- https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
-
- u = urllib2.build_opener(
- http_handler,
- https_handler,
- urllib2.HTTPCookieProcessor(self._cookies),
- redirector
- )
- if not postdata is None:
- req.add_data(postdata)
- return (req, u)
-
- def _read(self, openerdirector, trycount):
- chunks = []
-
- chunk = openerdirector.read()
- chunks.append(chunk)
- #while chunk and cnt < trycount:
- # time.sleep(1)
- # cnt += 1
- # chunk = openerdirector.read()
- # chunks.append(chunk)
-
- data = "".join(chunks)
-
- if "Content-Length" in openerdirector.info():
- assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
- openerdirector.info()["Content-Length"],
- len(data),
- )
-
- return data
-
-
-class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
- """This is a custom http redirect handler that FORBIDS redirection."""
-
- def http_error_302(self, req, fp, code, msg, headers):
- e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
- if e.code in (301, 302):
- if 'location' in headers:
- newurl = headers.getheaders('location')[0]
- elif 'uri' in headers:
- newurl = headers.getheaders('uri')[0]
- e.newurl = newurl
- _moduleLogger.info("New url: %s" % e.newurl)
- raise e
+++ /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._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._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 refresh_account_info might be enough to login, else full login is required
- """
- return self._loadedFromCookies or 0.0 < self._lastAuthed
-
- def refresh_account_info(self):
- try:
- page = self._get_page(self._JSON_CONTACTS_URL)
- accountData = self._grab_account_info(page)
- except Exception, e:
- _moduleLogger.exception(str(e))
- return None
-
- self._browser.save_cookies()
- self._lastAuthed = time.time()
- return accountData
-
- def _get_token(self):
- tokenPage = self._get_page(self._tokenURL)
-
- galxTokens = self._galxRe.search(tokenPage)
- if galxTokens is not None:
- galxToken = galxTokens.group(1)
- else:
- galxToken = ""
- _moduleLogger.debug("Could not grab GALX token")
- return galxToken
-
- def _login(self, username, password, token):
- loginData = {
- 'Email' : username,
- 'Passwd' : password,
- 'service': "grandcentral",
- "ltmpl": "mobile",
- "btmpl": "mobile",
- "PersistentCookie": "yes",
- "GALX": token,
- "continue": self._JSON_CONTACTS_URL,
- }
-
- loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
- return loginSuccessOrFailurePage
-
- def login(self, username, password):
- """
- Attempt to login to GoogleVoice
- @returns Whether login was successful or not
- @blocks
- """
- self.logout()
- galxToken = self._get_token()
- loginSuccessOrFailurePage = self._login(username, password, galxToken)
-
- try:
- accountData = self._grab_account_info(loginSuccessOrFailurePage)
- except Exception, e:
- # Retry in case the redirect failed
- # luckily refresh_account_info does everything we need for a retry
- accountData = self.refresh_account_info()
- if accountData is None:
- _moduleLogger.exception(str(e))
- return None
- _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
-
- self._browser.save_cookies()
- self._lastAuthed = time.time()
- return accountData
-
- def persist(self):
- self._browser.save_cookies()
-
- def shutdown(self):
- self._browser.save_cookies()
- self._token = None
- self._lastAuthed = 0.0
-
- def logout(self):
- self._browser.clear_cookies()
- self._browser.save_cookies()
- self._token = None
- self._lastAuthed = 0.0
- self._callbackNumbers = {}
-
- def is_dnd(self):
- """
- @blocks
- """
- isDndPage = self._get_page(self._isDndURL)
-
- dndGroup = self._isDndRe.search(isDndPage)
- if dndGroup is None:
- return False
- dndStatus = dndGroup.group(1)
- isDnd = True if dndStatus.strip().lower() == "true" else False
- return isDnd
-
- def set_dnd(self, doNotDisturb):
- """
- @blocks
- """
- dndPostData = {
- "doNotDisturb": 1 if doNotDisturb else 0,
- }
-
- dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
-
- def call(self, outgoingNumber):
- """
- This is the main function responsible for initating the callback
- @blocks
- """
- outgoingNumber = self._send_validation(outgoingNumber)
- subscriberNumber = None
- phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
-
- callData = {
- 'outgoingNumber': outgoingNumber,
- 'forwardingNumber': self._callbackNumber,
- 'subscriberNumber': subscriberNumber or 'undefined',
- 'phoneType': str(phoneType),
- 'remember': '1',
- }
- _moduleLogger.info("%r" % callData)
-
- page = self._get_page_with_token(
- self._callUrl,
- callData,
- )
- self._parse_with_validation(page)
- return True
-
- def cancel(self, outgoingNumber=None):
- """
- Cancels a call matching outgoing and forwarding numbers (if given).
- Will raise an error if no matching call is being placed
- @blocks
- """
- page = self._get_page_with_token(
- self._callCancelURL,
- {
- 'outgoingNumber': outgoingNumber or 'undefined',
- 'forwardingNumber': self._callbackNumber or 'undefined',
- 'cancelType': 'C2C',
- },
- )
- self._parse_with_validation(page)
-
- def send_sms(self, phoneNumbers, message):
- """
- @blocks
- """
- validatedPhoneNumbers = [
- self._send_validation(phoneNumber)
- for phoneNumber in phoneNumbers
- ]
- flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
- page = self._get_page_with_token(
- self._sendSmsURL,
- {
- 'phoneNumber': flattenedPhoneNumbers,
- 'text': unicode(message).encode("utf-8"),
- },
- )
- self._parse_with_validation(page)
-
- def search(self, query):
- """
- Search your Google Voice Account history for calls, voicemails, and sms
- Returns ``Folder`` instance containting matching messages
- @blocks
- """
- page = self._get_page(
- self._XML_SEARCH_URL,
- {"q": query},
- )
- json, html = extract_payload(page)
- return json
-
- def get_feed(self, feed):
- """
- @blocks
- """
- actualFeed = "_XML_%s_URL" % feed.upper()
- feedUrl = getattr(self, actualFeed)
-
- page = self._get_page(feedUrl)
- json, html = extract_payload(page)
-
- return json
-
- def recording_url(self, messageId):
- url = self._downloadVoicemailURL+messageId
- return url
-
- def download(self, messageId, targetPath):
- """
- Download a voicemail or recorded call MP3 matching the given ``msg``
- which can either be a ``Message`` instance, or a SHA1 identifier.
- Message hashes can be found in ``self.voicemail().messages`` for example.
- @returns location of saved file.
- @blocks
- """
- page = self._get_page(self.recording_url(messageId))
- with open(targetPath, 'wb') as fo:
- fo.write(page)
-
- def is_valid_syntax(self, number):
- """
- @returns If This number be called ( syntax validation only )
- """
- return self._validateRe.match(number) is not None
-
- def get_account_number(self):
- """
- @returns The GoogleVoice phone number
- """
- return self._accountNum
-
- def get_callback_numbers(self):
- """
- @returns a dictionary mapping call back numbers to descriptions
- @note These results are cached for 30 minutes.
- """
- return self._callbackNumbers
-
- def set_callback_number(self, callbacknumber):
- """
- Set the number that GoogleVoice calls
- @param callbacknumber should be a proper 10 digit number
- """
- self._callbackNumber = callbacknumber
- _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
- return True
-
- def get_callback_number(self):
- """
- @returns Current callback number or None
- """
- return self._callbackNumber
-
- def get_received_calls(self):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- @blocks
- """
- return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
-
- def get_missed_calls(self):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- @blocks
- """
- return self._parse_recent(self._get_page(self._XML_MISSED_URL))
-
- def get_placed_calls(self):
- """
- @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
- @blocks
- """
- return self._parse_recent(self._get_page(self._XML_PLACED_URL))
-
- def get_csv_contacts(self):
- data = {
- "groupToExport": "mine",
- "exportType": "ALL",
- "out": "OUTLOOK_CSV",
- }
- encodedData = urllib.urlencode(data)
- contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
- return contacts
-
- def get_voicemails(self):
- """
- @blocks
- """
- voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
- voicemailHtml = self._grab_html(voicemailPage)
- voicemailJson = self._grab_json(voicemailPage)
- if voicemailJson is None:
- return ()
- parsedVoicemail = self._parse_voicemail(voicemailHtml)
- voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
- return voicemails
-
- def get_texts(self):
- """
- @blocks
- """
- smsPage = self._get_page(self._XML_SMS_URL)
- smsHtml = self._grab_html(smsPage)
- smsJson = self._grab_json(smsPage)
- if smsJson is None:
- return ()
- parsedSms = self._parse_sms(smsHtml)
- smss = self._merge_conversation_sources(parsedSms, smsJson)
- return smss
-
- def get_unread_counts(self):
- countPage = self._get_page(self._JSON_SMS_COUNT_URL)
- counts = parse_json(countPage)
- counts = counts["unreadCounts"]
- return counts
-
- def mark_message(self, messageId, asRead):
- """
- @blocks
- """
- postData = {
- "read": 1 if asRead else 0,
- "id": messageId,
- }
-
- markPage = self._get_page(self._markAsReadURL, postData)
-
- def archive_message(self, messageId):
- """
- @blocks
- """
- postData = {
- "id": messageId,
- }
-
- markPage = self._get_page(self._archiveMessageURL, postData)
-
- def _grab_json(self, flatXml):
- xmlTree = ElementTree.fromstring(flatXml)
- jsonElement = xmlTree.getchildren()[0]
- flatJson = jsonElement.text
- jsonTree = parse_json(flatJson)
- return jsonTree
-
- def _grab_html(self, flatXml):
- xmlTree = ElementTree.fromstring(flatXml)
- htmlElement = xmlTree.getchildren()[1]
- flatHtml = htmlElement.text
- return flatHtml
-
- def _grab_account_info(self, page):
- accountData = parse_json(page)
- self._token = accountData["r"]
- self._accountNum = accountData["number"]["raw"]
- for callback in accountData["phones"].itervalues():
- self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
- if len(self._callbackNumbers) == 0:
- _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
- return accountData
-
- def _send_validation(self, number):
- if not self.is_valid_syntax(number):
- raise ValueError('Number is not valid: "%s"' % number)
- return number
-
- def _parse_recent(self, recentPage):
- allRecentHtml = self._grab_html(recentPage)
- allRecentData = self._parse_history(allRecentHtml)
- for recentCallData in allRecentData:
- yield recentCallData
-
- def _parse_history(self, historyHtml):
- splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
- for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
- exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
- exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
- exactTime = google_strptime(exactTime)
- relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
- relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
- locationGroup = self._voicemailLocationRegex.search(messageHtml)
- location = locationGroup.group(1).strip() if locationGroup else ""
-
- nameGroup = self._voicemailNameRegex.search(messageHtml)
- name = nameGroup.group(1).strip() if nameGroup else ""
- numberGroup = self._voicemailNumberRegex.search(messageHtml)
- number = numberGroup.group(1).strip() if numberGroup else ""
- prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
- prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
- contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
- contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
- yield {
- "id": messageId.strip(),
- "contactId": contactId,
- "name": unescape(name),
- "time": exactTime,
- "relTime": relativeTime,
- "prettyNumber": prettyNumber,
- "number": number,
- "location": unescape(location),
- }
-
- @staticmethod
- def _interpret_voicemail_regex(group):
- quality, content, number = group.group(2), group.group(3), group.group(4)
- text = MessageText()
- if quality is not None and content is not None:
- text.accuracy = quality
- text.text = unescape(content)
- return text
- elif number is not None:
- text.accuracy = MessageText.ACCURACY_HIGH
- text.text = number
- return text
-
- def _parse_voicemail(self, voicemailHtml):
- splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
- for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
- conv = Conversation()
- conv.type = Conversation.TYPE_VOICEMAIL
- conv.id = messageId.strip()
-
- exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
- exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
- conv.time = google_strptime(exactTimeText)
- relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
- conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
- locationGroup = self._voicemailLocationRegex.search(messageHtml)
- conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
-
- nameGroup = self._voicemailNameRegex.search(messageHtml)
- conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
- numberGroup = self._voicemailNumberRegex.search(messageHtml)
- conv.number = numberGroup.group(1).strip() if numberGroup else ""
- prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
- conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
- contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
- conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
- messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
- messageParts = [
- self._interpret_voicemail_regex(group)
- for group in messageGroups
- ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
- message = Message()
- message.body = messageParts
- message.whoFrom = conv.name
- try:
- message.when = conv.time.strftime("%I:%M %p")
- except ValueError:
- _moduleLogger.exception("Confusing time provided: %r" % conv.time)
- message.when = "Unknown"
- conv.messages = (message, )
-
- yield conv
-
- @staticmethod
- def _interpret_sms_message_parts(fromPart, textPart, timePart):
- text = MessageText()
- text.accuracy = MessageText.ACCURACY_MEDIUM
- text.text = unescape(textPart)
-
- message = Message()
- message.body = (text, )
- message.whoFrom = fromPart
- message.when = timePart
-
- return message
-
- def _parse_sms(self, smsHtml):
- splitSms = self._seperateVoicemailsRegex.split(smsHtml)
- for messageId, messageHtml in itergroup(splitSms[1:], 2):
- conv = Conversation()
- conv.type = Conversation.TYPE_SMS
- conv.id = messageId.strip()
-
- exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
- exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
- conv.time = google_strptime(exactTimeText)
- relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
- conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
- conv.location = ""
-
- nameGroup = self._voicemailNameRegex.search(messageHtml)
- conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
- numberGroup = self._voicemailNumberRegex.search(messageHtml)
- conv.number = numberGroup.group(1).strip() if numberGroup else ""
- prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
- conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
- contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
- conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
- fromGroups = self._smsFromRegex.finditer(messageHtml)
- fromParts = (group.group(1).strip() for group in fromGroups)
- textGroups = self._smsTextRegex.finditer(messageHtml)
- textParts = (group.group(1).strip() for group in textGroups)
- timeGroups = self._smsTimeRegex.finditer(messageHtml)
- timeParts = (group.group(1).strip() for group in timeGroups)
-
- messageParts = itertools.izip(fromParts, textParts, timeParts)
- messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
- conv.messages = messages
-
- yield conv
-
- @staticmethod
- def _merge_conversation_sources(parsedMessages, json):
- for message in parsedMessages:
- jsonItem = json["messages"][message.id]
- message.isRead = jsonItem["isRead"]
- message.isSpam = jsonItem["isSpam"]
- message.isTrash = jsonItem["isTrash"]
- message.isArchived = "inbox" not in jsonItem["labels"]
- yield message
-
- def _get_page(self, url, data = None, refererUrl = None):
- headers = {}
- if refererUrl is not None:
- headers["Referer"] = refererUrl
-
- encodedData = urllib.urlencode(data) if data is not None else None
-
- try:
- page = self._browser.download(url, encodedData, None, headers)
- except urllib2.URLError, e:
- _moduleLogger.error("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % url)
-
- return page
-
- def _get_page_with_token(self, url, data = None, refererUrl = None):
- if data is None:
- data = {}
- data['_rnr_se'] = self._token
-
- page = self._get_page(url, data, refererUrl)
-
- return page
-
- def _parse_with_validation(self, page):
- json = parse_json(page)
- self._validate_response(json)
- return json
-
- def _validate_response(self, response):
- """
- Validates that the JSON response is A-OK
- """
- try:
- assert response is not None, "Response not provided"
- assert 'ok' in response, "Response lacks status"
- assert response['ok'], "Response not good"
- except AssertionError:
- try:
- if response["data"]["code"] == 20:
- raise RuntimeError(
-"""Ambiguous error 20 returned by Google Voice.
-Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
- except KeyError:
- pass
- raise RuntimeError('There was a problem with GV: %s' % response)
-
-
-_UNESCAPE_ENTITIES = {
- """: '"',
- " ": " ",
- "'": "'",
-}
-
-
-def unescape(text):
- plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
- return plain
-
-
-def google_strptime(time):
- """
- Hack: Google always returns the time in the same locale. Sadly if the
- local system's locale is different, there isn't a way to perfectly handle
- the time. So instead we handle implement some time formatting
- """
- abbrevTime = time[:-3]
- parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
- if time.endswith("PM"):
- parsedTime += datetime.timedelta(hours=12)
- return parsedTime
-
-
-def itergroup(iterator, count, padValue = None):
- """
- Iterate in groups of 'count' values. If there
- aren't enough values, the last result is padded with
- None.
-
- >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
- ... print tuple(val)
- (1, 2, 3)
- (4, 5, 6)
- >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
- ... print list(val)
- [1, 2, 3]
- [4, 5, 6]
- >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
- ... print tuple(val)
- (1, 2, 3)
- (4, 5, 6)
- (7, None, None)
- >>> for val in itergroup("123456", 3):
- ... print tuple(val)
- ('1', '2', '3')
- ('4', '5', '6')
- >>> for val in itergroup("123456", 3):
- ... print repr("".join(val))
- '123'
- '456'
- """
- paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
- nIterators = (paddedIterator, ) * count
- return itertools.izip(*nIterators)
-
-
-def safe_eval(s):
- _TRUE_REGEX = re.compile("true")
- _FALSE_REGEX = re.compile("false")
- _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
- s = _TRUE_REGEX.sub("True", s)
- s = _FALSE_REGEX.sub("False", s)
- s = _COMMENT_REGEX.sub("#", s)
- try:
- results = eval(s, {}, {})
- except SyntaxError:
- _moduleLogger.exception("Oops")
- results = None
- return results
-
-
-def _fake_parse_json(flattened):
- return safe_eval(flattened)
-
-
-def _actual_parse_json(flattened):
- return simplejson.loads(flattened)
-
-
-if simplejson is None:
- parse_json = _fake_parse_json
-else:
- parse_json = _actual_parse_json
-
-
-def extract_payload(flatXml):
- xmlTree = ElementTree.fromstring(flatXml)
-
- jsonElement = xmlTree.getchildren()[0]
- flatJson = jsonElement.text
- jsonTree = parse_json(flatJson)
-
- htmlElement = xmlTree.getchildren()[1]
- flatHtml = htmlElement.text
-
- return jsonTree, flatHtml
-
-
-def guess_phone_type(number):
- if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
- return GVoiceBackend.PHONE_TYPE_GIZMO
- else:
- return GVoiceBackend.PHONE_TYPE_MOBILE
-
-
-def get_sane_callback(backend):
- """
- Try to set a sane default callback number on these preferences
- 1) 1747 numbers ( Gizmo )
- 2) anything with gizmo in the name
- 3) anything with computer in the name
- 4) the first value
- """
- numbers = backend.get_callback_numbers()
-
- priorityOrderedCriteria = [
- ("\+1747", None),
- ("1747", None),
- ("747", None),
- (None, "gizmo"),
- (None, "computer"),
- (None, "sip"),
- (None, None),
- ]
-
- for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
- numberMatcher = None
- descriptionMatcher = None
- if numberCriteria is not None:
- numberMatcher = re.compile(numberCriteria)
- elif descriptionCriteria is not None:
- descriptionMatcher = re.compile(descriptionCriteria, re.I)
-
- for number, description in numbers.iteritems():
- if numberMatcher is not None and numberMatcher.match(number) is None:
- continue
- if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
- continue
- return number
-
-
-def set_sane_callback(backend):
- """
- Try to set a sane default callback number on these preferences
- 1) 1747 numbers ( Gizmo )
- 2) anything with gizmo in the name
- 3) anything with computer in the name
- 4) the first value
- """
- number = get_sane_callback(backend)
- backend.set_callback_number(number)
-
-
-def _is_not_special(name):
- return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
-
-
-def to_dict(obj):
- members = inspect.getmembers(obj)
- return dict((name, value) for (name, value) in members if _is_not_special(name))
-
-
-def grab_debug_info(username, password):
- cookieFile = os.path.join(".", "raw_cookies.txt")
- try:
- os.remove(cookieFile)
- except OSError:
- pass
-
- backend = GVoiceBackend(cookieFile)
- browser = backend._browser
-
- _TEST_WEBPAGES = [
- ("token", backend._tokenURL),
- ("login", backend._loginURL),
- ("isdnd", backend._isDndURL),
- ("account", backend._XML_ACCOUNT_URL),
- ("html_contacts", backend._XML_CONTACTS_URL),
- ("contacts", backend._JSON_CONTACTS_URL),
- ("csv", backend._CSV_CONTACTS_URL),
-
- ("voicemail", backend._XML_VOICEMAIL_URL),
- ("html_sms", backend._XML_SMS_URL),
- ("sms", backend._JSON_SMS_URL),
- ("count", backend._JSON_SMS_COUNT_URL),
-
- ("recent", backend._XML_RECENT_URL),
- ("placed", backend._XML_PLACED_URL),
- ("recieved", backend._XML_RECEIVED_URL),
- ("missed", backend._XML_MISSED_URL),
- ]
-
- # Get Pages
- print "Grabbing pre-login pages"
- for name, url in _TEST_WEBPAGES:
- try:
- page = browser.download(url)
- except StandardError, e:
- print e.message
- continue
- print "\tWriting to file"
- with open("not_loggedin_%s.txt" % name, "w") as f:
- f.write(page)
-
- # Login
- print "Attempting login"
- galxToken = backend._get_token()
- loginSuccessOrFailurePage = backend._login(username, password, galxToken)
- with open("loggingin.txt", "w") as f:
- print "\tWriting to file"
- f.write(loginSuccessOrFailurePage)
- try:
- backend._grab_account_info(loginSuccessOrFailurePage)
- except Exception:
- # Retry in case the redirect failed
- # luckily refresh_account_info does everything we need for a retry
- loggedIn = backend.refresh_account_info() is not None
- if not loggedIn:
- raise
-
- # Get Pages
- print "Grabbing post-login pages"
- for name, url in _TEST_WEBPAGES:
- try:
- page = browser.download(url)
- except StandardError, e:
- print str(e)
- continue
- print "\tWriting to file"
- with open("loggedin_%s.txt" % name, "w") as f:
- f.write(page)
-
- # Cookies
- browser.save_cookies()
- print "\tWriting cookies to file"
- with open("cookies.txt", "w") as f:
- f.writelines(
- "%s: %s\n" % (c.name, c.value)
- for c in browser._cookies
- )
-
-
-def grab_voicemails(username, password):
- cookieFile = os.path.join(".", "raw_cookies.txt")
- try:
- os.remove(cookieFile)
- except OSError:
- pass
-
- backend = GVoiceBackend(cookieFile)
- backend.login(username, password)
- voicemails = list(backend.get_voicemails())
- for voicemail in voicemails:
- print voicemail.id
- backend.download(voicemail.id, ".")
-
-
-def main():
- import sys
- logging.basicConfig(level=logging.DEBUG)
- args = sys.argv
- if 3 <= len(args):
- username = args[1]
- password = args[2]
-
- grab_debug_info(username, password)
- grab_voicemails(username, password)
-
-
-if __name__ == "__main__":
- main()
+++ /dev/null
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's Grand Central service.
-Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-"""
-
-
-class NullAddressBook(object):
-
- @property
- def name(self):
- return "None"
-
- def update_account(self, force = True):
- pass
-
- def get_contacts(self):
- return {}
-
-
-class NullAddressBookFactory(object):
-
- def get_addressbooks(self):
- yield NullAddressBook()
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-if qt_compat.USES_PYSIDE:
- try:
- import QtMobility.Contacts as _QtContacts
- QtContacts = _QtContacts
- except ImportError:
- QtContacts = None
-else:
- QtContacts = None
-
-import null_backend
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class QtContactsAddressBook(object):
-
- def __init__(self, name, uri):
- self._name = name
- self._uri = uri
- self._manager = QtContacts.QContactManager.fromUri(uri)
- self._contacts = None
-
- @property
- def name(self):
- return self._name
-
- @property
- def error(self):
- return self._manager.error()
-
- def update_account(self, force = True):
- if not force and self._contacts is not None:
- return
- self._contacts = dict(self._get_contacts())
-
- def get_contacts(self):
- if self._contacts is None:
- self._contacts = dict(self._get_contacts())
- return self._contacts
-
- def _get_contacts(self):
- contacts = self._manager.contacts()
- for contact in contacts:
- contactId = contact.localId()
- contactName = contact.displayLabel()
- phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
- phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
- contactDetails = phones
- if 0 < len(contactDetails):
- yield str(contactId), {
- "contactId": str(contactId),
- "name": contactName,
- "numbers": contactDetails,
- }
-
-
-class _QtContactsAddressBookFactory(object):
-
- def __init__(self):
- self._availableManagers = {}
-
- availableMgrs = QtContacts.QContactManager.availableManagers()
- availableMgrs.remove("invalid")
- for managerName in availableMgrs:
- params = {}
- managerUri = QtContacts.QContactManager.buildUri(managerName, params)
- self._availableManagers[managerName] = managerUri
-
- def get_addressbooks(self):
- for name, uri in self._availableManagers.iteritems():
- book = QtContactsAddressBook(name, uri)
- if book.error:
- _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
- else:
- yield book
-
-
-class _EmptyAddressBookFactory(object):
-
- def get_addressbooks(self):
- if False:
- yield None
-
-
-if QtContacts is not None:
- QtContactsAddressBookFactory = _QtContactsAddressBookFactory
-else:
- QtContactsAddressBookFactory = _EmptyAddressBookFactory
- _moduleLogger.info("QtContacts support not available")
-
-
-if __name__ == "__main__":
- factory = QtContactsAddressBookFactory()
- books = factory.get_addressbooks()
- for book in books:
- print book.name
- print book.get_contacts()
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-import dbus
-try:
- import telepathy as _telepathy
- import util.tp_utils as telepathy_utils
- telepathy = _telepathy
-except ImportError:
- telepathy = None
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class _FakeSignaller(object):
-
- def start(self):
- pass
-
- def stop(self):
- pass
-
-
-class _MissedCallWatcher(QtCore.QObject):
-
- callMissed = qt_compat.Signal()
-
- def __init__(self):
- QtCore.QObject.__init__(self)
- self._isStarted = False
- self._isSupported = True
-
- self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
- self._outstandingRequests = []
-
- @property
- def isSupported(self):
- return self._isSupported
-
- @property
- def isStarted(self):
- return self._isStarted
-
- def start(self):
- if self._isStarted:
- _moduleLogger.info("voicemail monitor already started")
- return
- try:
- self._newChannelSignaller.start()
- except RuntimeError:
- _moduleLogger.exception("Missed call detection not supported")
- self._newChannelSignaller = _FakeSignaller()
- self._isSupported = False
- self._isStarted = True
-
- def stop(self):
- if not self._isStarted:
- _moduleLogger.info("voicemail monitor stopped without starting")
- return
- _moduleLogger.info("Stopping voicemail refresh")
- self._newChannelSignaller.stop()
-
- # I don't want to trust whether the cancel happens within the current
- # callback or not which could be the deciding factor between invalid
- # iterators or infinite loops
- localRequests = [r for r in self._outstandingRequests]
- for request in localRequests:
- localRequests.cancel()
-
- self._isStarted = False
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
- if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
- return
-
- conn = telepathy.client.Connection(serviceName, connObjectPath)
- try:
- chan = telepathy.client.Channel(serviceName, channelObjectPath)
- except dbus.exceptions.UnknownMethodException:
- _moduleLogger.exception("Client might not have implemented a deprecated method")
- return
- missDetection = telepathy_utils.WasMissedCall(
- bus, conn, chan, self._on_missed_call, self._on_error_for_missed
- )
- self._outstandingRequests.append(missDetection)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_missed_call(self, missDetection):
- _moduleLogger.info("Missed a call")
- self.callMissed.emit()
- self._outstandingRequests.remove(missDetection)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_error_for_missed(self, missDetection, reason):
- _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
- self._outstandingRequests.remove(missDetection)
-
-
-class _DummyMissedCallWatcher(QtCore.QObject):
-
- callMissed = qt_compat.Signal()
-
- def __init__(self):
- QtCore.QObject.__init__(self)
- self._isStarted = False
-
- @property
- def isSupported(self):
- return False
-
- @property
- def isStarted(self):
- return self._isStarted
-
- def start(self):
- self._isStarted = True
-
- def stop(self):
- if not self._isStarted:
- _moduleLogger.info("voicemail monitor stopped without starting")
- return
- _moduleLogger.info("Stopping voicemail refresh")
- self._isStarted = False
-
-
-if telepathy is not None:
- MissedCallWatcher = _MissedCallWatcher
-else:
- MissedCallWatcher = _DummyMissedCallWatcher
-
-
-if __name__ == "__main__":
- pass
-
+++ /dev/null
-import os
-
-__pretty_app_name__ = "DialCentral"
-__app_name__ = "dialcentral"
-__version__ = "1.3.6"
-__build__ = 0
-__app_magic__ = 0xdeadbeef
-_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
-_user_settings_ = "%s/settings.ini" % _data_path_
-_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_
-_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
-_notifier_logpath_ = "%s/notifier.log" % _data_path_
-IS_MAEMO = True
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-
-import sys
-
-
-sys.path.append("/opt/dialcentral/lib")
-
-
-import dialcentral_qt
-
-
-if __name__ == "__main__":
- 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
-import logging.handlers
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import constants
-import alarm_handler
-from util import qtpie
-from util import qwrappers
-from util import qui_utils
-from util import misc as misc_utils
-
-import session
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Dialcentral(qwrappers.ApplicationWrapper):
-
- _DATA_PATHS = [
- os.path.join(os.path.dirname(__file__), "../share"),
- os.path.join(os.path.dirname(__file__), "../data"),
- ]
-
- def __init__(self, app):
- self._dataPath = None
- self._aboutDialog = None
- self.notifyOnMissed = False
- self.notifyOnVoicemail = False
- self.notifyOnSms = False
-
- self._streamHandler = None
- self._ledHandler = None
- self._alarmHandler = alarm_handler.AlarmHandler()
-
- qwrappers.ApplicationWrapper.__init__(self, app, constants)
-
- def load_settings(self):
- try:
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- except IOError, e:
- _moduleLogger.info("No settings")
- return
- except ValueError:
- _moduleLogger.info("Settings were corrupt")
- return
- except ConfigParser.MissingSectionHeaderError:
- _moduleLogger.info("Settings were corrupt")
- return
- except Exception:
- _moduleLogger.exception("Unknown loading error")
-
- self._mainWindow.load_settings(config)
-
- def save_settings(self):
- _moduleLogger.info("Saving settings")
- config = ConfigParser.SafeConfigParser()
-
- self._mainWindow.save_settings(config)
-
- with open(constants._user_settings_, "wb") as configFile:
- config.write(configFile)
-
- def get_icon(self, name):
- if self._dataPath is None:
- for path in self._DATA_PATHS:
- if os.path.exists(os.path.join(path, name)):
- self._dataPath = path
- break
- if self._dataPath is not None:
- icon = QtGui.QIcon(os.path.join(self._dataPath, name))
- return icon
- else:
- return None
-
- def get_resource(self, name):
- if self._dataPath is None:
- for path in self._DATA_PATHS:
- if os.path.exists(os.path.join(path, name)):
- self._dataPath = path
- break
- if self._dataPath is not None:
- return os.path.join(self._dataPath, name)
- else:
- return None
-
- def _close_windows(self):
- qwrappers.ApplicationWrapper._close_windows(self)
- if self._aboutDialog is not None:
- self._aboutDialog.close()
-
- @property
- def fsContactsPath(self):
- return os.path.join(constants._data_path_, "contacts")
-
- @property
- def streamHandler(self):
- if self._streamHandler is None:
- import stream_handler
- self._streamHandler = stream_handler.StreamHandler()
- return self._streamHandler
-
- @property
- def alarmHandler(self):
- return self._alarmHandler
-
- @property
- def ledHandler(self):
- if self._ledHandler is None:
- import led_handler
- self._ledHandler = led_handler.LedHandler()
- return self._ledHandler
-
- def _new_main_window(self):
- return MainWindow(None, self)
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_about(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- if self._aboutDialog is None:
- import dialogs
- self._aboutDialog = dialogs.AboutDialog(self)
- response = self._aboutDialog.run(self._mainWindow.window)
-
-
-class DelayedWidget(object):
-
- def __init__(self, app, settingsNames):
- self._layout = QtGui.QVBoxLayout()
- self._layout.setContentsMargins(0, 0, 0, 0)
- self._widget = QtGui.QWidget()
- self._widget.setContentsMargins(0, 0, 0, 0)
- self._widget.setLayout(self._layout)
- self._settings = dict((name, "") for name in settingsNames)
-
- self._child = None
- self._isEnabled = True
-
- @property
- def toplevel(self):
- return self._widget
-
- def has_child(self):
- return self._child is not None
-
- def set_child(self, child):
- if self._child is not None:
- self._layout.removeWidget(self._child.toplevel)
- self._child = child
- if self._child is not None:
- self._layout.addWidget(self._child.toplevel)
-
- self._child.set_settings(self._settings)
-
- if self._isEnabled:
- self._child.enable()
- else:
- self._child.disable()
-
- @property
- def child(self):
- return self._child
-
- def enable(self):
- self._isEnabled = True
- if self._child is not None:
- self._child.enable()
-
- def disable(self):
- self._isEnabled = False
- if self._child is not None:
- self._child.disable()
-
- def clear(self):
- if self._child is not None:
- self._child.clear()
-
- def refresh(self, force=True):
- if self._child is not None:
- self._child.refresh(force)
-
- def get_settings(self):
- if self._child is not None:
- return self._child.get_settings()
- else:
- return self._settings
-
- def set_settings(self, settings):
- if self._child is not None:
- self._child.set_settings(settings)
- else:
- self._settings = settings
-
-
-def _tab_factory(tab, app, session, errorLog):
- import gv_views
- return gv_views.__dict__[tab](app, session, errorLog)
-
-
-class MainWindow(qwrappers.WindowWrapper):
-
- KEYPAD_TAB = 0
- RECENT_TAB = 1
- MESSAGES_TAB = 2
- CONTACTS_TAB = 3
- MAX_TABS = 4
-
- _TAB_TITLES = [
- "Dialpad",
- "History",
- "Messages",
- "Contacts",
- ]
- assert len(_TAB_TITLES) == MAX_TABS
-
- _TAB_ICONS = [
- "dialpad.png",
- "history.png",
- "messages.png",
- "contacts.png",
- ]
- assert len(_TAB_ICONS) == MAX_TABS
-
- _TAB_CLASS = [
- functools.partial(_tab_factory, "Dialpad"),
- functools.partial(_tab_factory, "History"),
- functools.partial(_tab_factory, "Messages"),
- functools.partial(_tab_factory, "Contacts"),
- ]
- assert len(_TAB_CLASS) == MAX_TABS
-
- # Hack to allow delay importing/loading of tabs
- _TAB_SETTINGS_NAMES = [
- (),
- ("filter", ),
- ("status", "type"),
- ("selectedAddressbook", ),
- ]
- assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
-
- def __init__(self, parent, app):
- qwrappers.WindowWrapper.__init__(self, parent, app)
- self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
- self._window.resized.connect(self._on_window_resized)
- self._errorLog = self._app.errorLog
-
- self._session = session.Session(self._errorLog, constants._data_path_)
- self._session.error.connect(self._on_session_error)
- self._session.loggedIn.connect(self._on_login)
- self._session.loggedOut.connect(self._on_logout)
- self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
- self._session.newMessages.connect(self._on_new_message_alert)
- self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
- self._voicemailRefreshDelay = QtCore.QTimer()
- self._voicemailRefreshDelay.setInterval(30 * 1000)
- self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
- self._voicemailRefreshDelay.setSingleShot(True)
- self._callHandler = None
- self._updateVoicemailOnMissedCall = False
-
- self._defaultCredentials = "", ""
- self._curentCredentials = "", ""
- self._currentTab = 0
-
- self._credentialsDialog = None
- self._smsEntryDialog = None
- self._accountDialog = None
-
- self._tabsContents = [
- DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
- for i in xrange(self.MAX_TABS)
- ]
- for tab in self._tabsContents:
- tab.disable()
-
- self._tabWidget = QtGui.QTabWidget()
- if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
- else:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
- defaultTabIconSize = self._tabWidget.iconSize()
- defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height()
- for tabIndex, (tabTitle, tabIcon) in enumerate(
- zip(self._TAB_TITLES, self._TAB_ICONS)
- ):
- icon = self._app.get_icon(tabIcon)
- if constants.IS_MAEMO and icon is not None:
- tabTitle = ""
-
- if icon is None:
- self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
- else:
- iconSize = icon.availableSizes()[0]
- defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width())
- defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height())
- self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
- defaultTabIconWidth = max(defaultTabIconWidth, 32)
- defaultTabIconHeight = max(defaultTabIconHeight, 32)
- self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight))
- self._tabWidget.currentChanged.connect(self._on_tab_changed)
- self._tabWidget.setContentsMargins(0, 0, 0, 0)
-
- self._layout.addWidget(self._tabWidget)
-
- self._loginAction = QtGui.QAction(None)
- self._loginAction.setText("Login")
- self._loginAction.triggered.connect(self._on_login_requested)
-
- self._importAction = QtGui.QAction(None)
- self._importAction.setText("Import")
- self._importAction.triggered.connect(self._on_import)
-
- self._accountAction = QtGui.QAction(None)
- self._accountAction.setText("Account")
- self._accountAction.triggered.connect(self._on_account)
-
- self._refreshConnectionAction = QtGui.QAction(None)
- self._refreshConnectionAction.setText("Refresh Connection")
- self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
- self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
-
- self._refreshTabAction = QtGui.QAction(None)
- self._refreshTabAction.setText("Refresh Tab")
- self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
- self._refreshTabAction.triggered.connect(self._on_refresh)
-
- fileMenu = self._window.menuBar().addMenu("&File")
- fileMenu.addAction(self._loginAction)
- fileMenu.addAction(self._refreshTabAction)
- fileMenu.addAction(self._refreshConnectionAction)
-
- toolsMenu = self._window.menuBar().addMenu("&Tools")
- toolsMenu.addAction(self._accountAction)
- toolsMenu.addAction(self._importAction)
- toolsMenu.addAction(self._app.aboutAction)
-
- self._initialize_tab(self._tabWidget.currentIndex())
- self.set_fullscreen(self._app.fullscreenAction.isChecked())
- self.update_orientation(self._app.orientation)
-
- def _init_call_handler(self):
- if self._callHandler is not None:
- return
- import call_handler
- self._callHandler = call_handler.MissedCallWatcher()
- self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
-
- def set_default_credentials(self, username, password):
- self._defaultCredentials = username, password
-
- def get_default_credentials(self):
- return self._defaultCredentials
-
- def walk_children(self):
- if self._smsEntryDialog is not None:
- return (self._smsEntryDialog, )
- else:
- return ()
-
- def start(self):
- qwrappers.WindowWrapper.start(self)
- assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up"
- if self._defaultCredentials != ("", ""):
- username, password = self._defaultCredentials[0], self._defaultCredentials[1]
- self._curentCredentials = username, password
- self._session.login(username, password)
- else:
- self._prompt_for_login()
-
- def close(self):
- for diag in (
- self._credentialsDialog,
- self._accountDialog,
- ):
- if diag is not None:
- diag.close()
- for child in self.walk_children():
- child.window.destroyed.disconnect(self._on_child_close)
- child.window.closed.disconnect(self._on_child_close)
- child.close()
- self._window.close()
-
- def destroy(self):
- qwrappers.WindowWrapper.destroy(self)
- if self._session.state != self._session.LOGGEDOUT_STATE:
- self._session.logout()
-
- def get_current_tab(self):
- return self._currentTab
-
- def set_current_tab(self, tabIndex):
- self._tabWidget.setCurrentIndex(tabIndex)
-
- def load_settings(self, config):
- blobs = "", ""
- isFullscreen = False
- orientation = self._app.orientation
- tabIndex = 0
- try:
- blobs = [
- config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
- for i in xrange(len(self.get_default_credentials()))
- ]
- isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
- tabIndex = config.getint(constants.__pretty_app_name__, "tab")
- orientation = config.get(constants.__pretty_app_name__, "orientation")
- except ConfigParser.NoOptionError, e:
- _moduleLogger.info(
- "Settings file %s is missing option %s" % (
- constants._user_settings_,
- e.option,
- ),
- )
- except ConfigParser.NoSectionError, e:
- _moduleLogger.info(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- except Exception:
- _moduleLogger.exception("Unknown loading error")
-
- try:
- self._app.alarmHandler.load_settings(config, "alarm")
- self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
- self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
- self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
- self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
- except ConfigParser.NoOptionError, e:
- _moduleLogger.info(
- "Settings file %s is missing option %s" % (
- constants._user_settings_,
- e.option,
- ),
- )
- except ConfigParser.NoSectionError, e:
- _moduleLogger.info(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- except Exception:
- _moduleLogger.exception("Unknown loading error")
-
- creds = (
- base64.b64decode(blob)
- for blob in blobs
- )
- self.set_default_credentials(*creds)
- self._app.fullscreenAction.setChecked(isFullscreen)
- self._app.set_orientation(orientation)
- self.set_current_tab(tabIndex)
-
- backendId = 2 # For backwards compatibility
- for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
- sectionName = "%s - %s" % (backendId, tabTitle)
- settings = self._tabsContents[tabIndex].get_settings()
- for settingName in settings.iterkeys():
- try:
- settingValue = config.get(sectionName, settingName)
- except ConfigParser.NoOptionError, e:
- _moduleLogger.info(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- return
- except ConfigParser.NoSectionError, e:
- _moduleLogger.info(
- "Settings file %s is missing section %s" % (
- constants._user_settings_,
- e.section,
- ),
- )
- return
- except Exception:
- _moduleLogger.exception("Unknown loading error")
- return
- settings[settingName] = settingValue
- self._tabsContents[tabIndex].set_settings(settings)
-
- def save_settings(self, config):
- config.add_section(constants.__pretty_app_name__)
- config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
- config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
- config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
- for i, value in enumerate(self.get_default_credentials()):
- blob = base64.b64encode(value)
- config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
-
- config.add_section("alarm")
- self._app.alarmHandler.save_settings(config, "alarm")
- config.add_section("2 - Account Info")
- config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
- config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
- config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
- config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
-
- backendId = 2 # For backwards compatibility
- for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
- sectionName = "%s - %s" % (backendId, tabTitle)
- config.add_section(sectionName)
- tabSettings = self._tabsContents[tabIndex].get_settings()
- for settingName, settingValue in tabSettings.iteritems():
- config.set(sectionName, settingName, settingValue)
-
- def update_orientation(self, orientation):
- qwrappers.WindowWrapper.update_orientation(self, orientation)
- windowOrientation = self.idealWindowOrientation
- if windowOrientation == QtCore.Qt.Horizontal:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
- else:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-
- def _initialize_tab(self, index):
- assert index < self.MAX_TABS, "Invalid tab"
- if not self._tabsContents[index].has_child():
- tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
- self._tabsContents[index].set_child(tab)
- self._tabsContents[index].refresh(force=False)
-
- def _prompt_for_login(self):
- if self._credentialsDialog is None:
- import dialogs
- self._credentialsDialog = dialogs.CredentialsDialog(self._app)
- credentials = self._credentialsDialog.run(
- self._defaultCredentials[0], self._defaultCredentials[1], self.window
- )
- if credentials is None:
- return
- username, password = credentials
- self._curentCredentials = username, password
- self._session.login(username, password)
-
- def _show_account_dialog(self):
- if self._accountDialog is None:
- import dialogs
- self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
- self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
- self._accountDialog.settingsApproved.connect(self._on_settings_approved)
-
- if self._callHandler is not None and not self._callHandler.isSupported:
- self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
- elif self._updateVoicemailOnMissedCall:
- self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
- else:
- self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
- self._accountDialog.notifications = self._app.alarmHandler.alarmType
- self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
- self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
- self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
- self._accountDialog.notifyOnSms = self._app.notifyOnSms
- self._accountDialog.set_callbacks(
- self._session.get_callback_numbers(), self._session.get_callback_number()
- )
- accountNumberToDisplay = self._session.get_account_number()
- if not accountNumberToDisplay:
- accountNumberToDisplay = "Not Available (%s)" % self._session.state
- self._accountDialog.set_account_number(accountNumberToDisplay)
- self._accountDialog.orientation = self._app.orientation
-
- self._accountDialog.run()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_settings_approved(self):
- if self._accountDialog.doClear:
- self._session.logout_and_clear()
- self._defaultCredentials = "", ""
- self._curentCredentials = "", ""
- for tab in self._tabsContents:
- tab.disable()
- else:
- callbackNumber = self._accountDialog.selectedCallback
- self._session.set_callback_number(callbackNumber)
-
- if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED:
- pass
- elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
- self._updateVoicemailOnMissedCall = True
- self._init_call_handler()
- self._callHandler.start()
- else:
- self._updateVoicemailOnMissedCall = False
- if self._callHandler is not None:
- self._callHandler.stop()
- if (
- self._accountDialog.notifyOnMissed or
- self._accountDialog.notifyOnVoicemail or
- self._accountDialog.notifyOnSms
- ):
- notifications = self._accountDialog.notifications
- else:
- notifications = self._accountDialog.ALARM_NONE
- self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
-
- self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
- self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
- self._app.notifyOnSms = self._accountDialog.notifyOnSms
- self._app.set_orientation(self._accountDialog.orientation)
- self._app.save_settings()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_window_resized(self):
- with qui_utils.notify_error(self._app.errorLog):
- windowOrientation = self.idealWindowOrientation
- if windowOrientation == QtCore.Qt.Horizontal:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
- else:
- self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_new_message_alert(self):
- with qui_utils.notify_error(self._errorLog):
- if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
- if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
- self._errorLog.push_message("New messages")
- else:
- self._app.ledHandler.on()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_call_missed(self):
- with qui_utils.notify_error(self._errorLog):
- self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
-
- @qt_compat.Slot(str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_session_error(self, message):
- with qui_utils.notify_error(self._errorLog):
- self._errorLog.push_error(message)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_login(self):
- with qui_utils.notify_error(self._errorLog):
- changedAccounts = self._defaultCredentials != self._curentCredentials
- noCallback = not self._session.get_callback_number()
- if changedAccounts or noCallback:
- self._show_account_dialog()
-
- self._defaultCredentials = self._curentCredentials
-
- for tab in self._tabsContents:
- tab.enable()
- self._initialize_tab(self._currentTab)
- if self._updateVoicemailOnMissedCall:
- self._init_call_handler()
- self._callHandler.start()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_logout(self):
- with qui_utils.notify_error(self._errorLog):
- for tab in self._tabsContents:
- tab.disable()
- if self._callHandler is not None:
- self._callHandler.stop()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_app_alert(self):
- with qui_utils.notify_error(self._errorLog):
- if self._session.state == self._session.LOGGEDIN_STATE:
- messageType = {
- (True, True): self._session.MESSAGE_ALL,
- (True, False): self._session.MESSAGE_TEXTS,
- (False, True): self._session.MESSAGE_VOICEMAILS,
- }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
- self._session.update_messages(messageType, force=True)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_recipients_changed(self):
- with qui_utils.notify_error(self._errorLog):
- if self._session.draft.get_num_contacts() == 0:
- return
-
- if self._smsEntryDialog is None:
- import dialogs
- self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
- self._smsEntryDialog.window.destroyed.connect(self._on_child_close)
- self._smsEntryDialog.window.closed.connect(self._on_child_close)
- self._smsEntryDialog.window.show()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_child_close(self, obj = None):
- self._smsEntryDialog = None
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_login_requested(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- self._prompt_for_login()
-
- @qt_compat.Slot(int)
- @misc_utils.log_exception(_moduleLogger)
- def _on_tab_changed(self, index):
- with qui_utils.notify_error(self._errorLog):
- self._currentTab = index
- self._initialize_tab(index)
- if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
- self._app.ledHandler.off()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- self._tabsContents[self._currentTab].refresh(force=True)
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh_connection(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- self._session.refresh_connection()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_import(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
- csvName = unicode(csvName)
- if not csvName:
- return
- import shutil
- shutil.copy2(csvName, self._app.fsContactsPath)
- if self._tabsContents[self.CONTACTS_TAB].has_child:
- self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_account(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings"
- self._show_account_dialog()
-
-
-def run():
- try:
- os.makedirs(constants._data_path_)
- except OSError, e:
- if e.errno != 17:
- raise
-
- logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
- logging.basicConfig(level=logging.DEBUG, format=logFormat)
- rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
- rotating.setFormatter(logging.Formatter(logFormat))
- root = logging.getLogger()
- root.addHandler(rotating)
- _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
- _moduleLogger.info("OS: %s" % (os.uname()[0], ))
- _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
- _moduleLogger.info("Hostname: %s" % os.uname()[1])
-
- try:
- import gobject
- gobject.threads_init()
- except ImportError:
- _moduleLogger.info("GObject support not available")
- try:
- import dbus
- try:
- from dbus.mainloop.qt import DBusQtMainLoop
- DBusQtMainLoop(set_as_default=True)
- _moduleLogger.info("Using Qt mainloop")
- except ImportError:
- try:
- from dbus.mainloop.glib import DBusGMainLoop
- DBusGMainLoop(set_as_default=True)
- _moduleLogger.info("Using GObject mainloop")
- except ImportError:
- _moduleLogger.info("Mainloop not available")
- except ImportError:
- _moduleLogger.info("DBus support not available")
-
- app = QtGui.QApplication([])
- handle = Dialcentral(app)
- qtpie.init_pies()
- return app.exec_()
-
-
-if __name__ == "__main__":
- import sys
-
- val = run()
- sys.exit(val)
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import functools
-import copy
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import constants
-from util import qwrappers
-from util import qui_utils
-from util import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class CredentialsDialog(object):
-
- def __init__(self, app):
- self._app = app
- self._usernameField = QtGui.QLineEdit()
- self._passwordField = QtGui.QLineEdit()
- self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
-
- self._credLayout = QtGui.QGridLayout()
- self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
- self._credLayout.addWidget(self._usernameField, 0, 1)
- self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
- self._credLayout.addWidget(self._passwordField, 1, 1)
-
- self._loginButton = QtGui.QPushButton("&Login")
- self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
- self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addLayout(self._credLayout)
- self._layout.addWidget(self._buttonLayout)
-
- self._dialog = QtGui.QDialog()
- self._dialog.setWindowTitle("Login")
- self._dialog.setLayout(self._layout)
- self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
- self._buttonLayout.accepted.connect(self._dialog.accept)
- self._buttonLayout.rejected.connect(self._dialog.reject)
-
- self._closeWindowAction = QtGui.QAction(None)
- self._closeWindowAction.setText("Close")
- self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
- self._closeWindowAction.triggered.connect(self._on_close_window)
-
- self._dialog.addAction(self._closeWindowAction)
- self._dialog.addAction(app.quitAction)
- self._dialog.addAction(app.fullscreenAction)
-
- def run(self, defaultUsername, defaultPassword, parent=None):
- self._dialog.setParent(parent, QtCore.Qt.Dialog)
- try:
- self._usernameField.setText(defaultUsername)
- self._passwordField.setText(defaultPassword)
-
- response = self._dialog.exec_()
- if response == QtGui.QDialog.Accepted:
- return str(self._usernameField.text()), str(self._passwordField.text())
- elif response == QtGui.QDialog.Rejected:
- return None
- else:
- _moduleLogger.error("Unknown response")
- return None
- finally:
- self._dialog.setParent(None, QtCore.Qt.Dialog)
-
- def close(self):
- try:
- self._dialog.reject()
- except RuntimeError:
- _moduleLogger.exception("Oh well")
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_close_window(self, checked = True):
- with qui_utils.notify_error(self._app.errorLog):
- self._dialog.reject()
-
-
-class AboutDialog(object):
-
- def __init__(self, app):
- self._app = app
- self._title = QtGui.QLabel(
- "<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._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addWidget(self._title)
- self._layout.addWidget(self._copyright)
- self._layout.addWidget(self._link)
- self._layout.addWidget(self._buttonLayout)
-
- self._dialog = QtGui.QDialog()
- self._dialog.setWindowTitle("About")
- self._dialog.setLayout(self._layout)
- self._buttonLayout.rejected.connect(self._dialog.reject)
-
- self._closeWindowAction = QtGui.QAction(None)
- self._closeWindowAction.setText("Close")
- self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
- self._closeWindowAction.triggered.connect(self._on_close_window)
-
- self._dialog.addAction(self._closeWindowAction)
- self._dialog.addAction(app.quitAction)
- self._dialog.addAction(app.fullscreenAction)
-
- def run(self, parent=None):
- self._dialog.setParent(parent, QtCore.Qt.Dialog)
-
- response = self._dialog.exec_()
- return response
-
- def close(self):
- try:
- self._dialog.reject()
- except RuntimeError:
- _moduleLogger.exception("Oh well")
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_close_window(self, checked = True):
- with qui_utils.notify_error(self._app.errorLog):
- self._dialog.reject()
-
-
-class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper):
-
- # @bug Can't enter custom callback numbers
-
- _RECURRENCE_CHOICES = [
- (1, "1 minute"),
- (2, "2 minutes"),
- (3, "3 minutes"),
- (5, "5 minutes"),
- (8, "8 minutes"),
- (10, "10 minutes"),
- (15, "15 minutes"),
- (30, "30 minutes"),
- (45, "45 minutes"),
- (60, "1 hour"),
- (3*60, "3 hours"),
- (6*60, "6 hours"),
- (12*60, "12 hours"),
- ]
-
- ALARM_NONE = "No Alert"
- ALARM_BACKGROUND = "Background Alert"
- ALARM_APPLICATION = "Application Alert"
-
- VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
- VOICEMAIL_CHECK_DISABLED = "Disabled"
- VOICEMAIL_CHECK_ENABLED = "Enabled"
-
- settingsApproved = qt_compat.Signal()
-
- def __init__(self, parent, app, errorLog):
- QtCore.QObject.__init__(self)
- qwrappers.WindowWrapper.__init__(self, parent, app)
- self._app = app
- self._doClear = False
-
- self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
- self._notificationSelecter = QtGui.QComboBox()
- self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
- self._notificationTimeSelector = QtGui.QComboBox()
- #self._notificationTimeSelector.setEditable(True)
- self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
- for _, label in self._RECURRENCE_CHOICES:
- self._notificationTimeSelector.addItem(label)
- self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
- self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
- self._smsNotificationButton = QtGui.QCheckBox("SMS")
- self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
- self._clearButton = QtGui.QPushButton("Clear Account")
- self._clearButton.clicked.connect(self._on_clear)
- self._callbackSelector = QtGui.QComboBox()
- #self._callbackSelector.setEditable(True)
- self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
- self._orientationSelector = QtGui.QComboBox()
- for orientationMode in [
- self._app.DEFAULT_ORIENTATION,
- self._app.AUTO_ORIENTATION,
- self._app.LANDSCAPE_ORIENTATION,
- self._app.PORTRAIT_ORIENTATION,
- ]:
- self._orientationSelector.addItem(orientationMode)
-
- self._update_notification_state()
-
- self._credLayout = QtGui.QGridLayout()
- self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
- self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
- self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
- self._credLayout.addWidget(self._callbackSelector, 1, 1)
- self._credLayout.addWidget(self._notificationSelecter, 2, 0)
- self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
- self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
- self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
- self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
- self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
- self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
- self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
- self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0)
- self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
- self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0)
- self._credLayout.addWidget(self._orientationSelector, 7, 1)
- self._credLayout.addWidget(QtGui.QLabel(""), 8, 0)
- self._credLayout.addWidget(QtGui.QLabel(""), 9, 0)
- self._credLayout.addWidget(self._clearButton, 9, 1)
-
- self._credWidget = QtGui.QWidget()
- self._credWidget.setLayout(self._credLayout)
- self._credWidget.setContentsMargins(0, 0, 0, 0)
- self._scrollSettings = QtGui.QScrollArea()
- self._scrollSettings.setWidget(self._credWidget)
- self._scrollSettings.setWidgetResizable(True)
- self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
- self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
- self._applyButton = QtGui.QPushButton("&Apply")
- self._applyButton.clicked.connect(self._on_settings_apply)
- self._cancelButton = QtGui.QPushButton("&Cancel")
- self._cancelButton.clicked.connect(self._on_settings_cancel)
- self._buttonLayout = QtGui.QHBoxLayout()
- self._buttonLayout.addStretch()
- self._buttonLayout.addWidget(self._cancelButton)
- self._buttonLayout.addStretch()
- self._buttonLayout.addWidget(self._applyButton)
- self._buttonLayout.addStretch()
-
- self._layout.addWidget(self._scrollSettings)
- self._layout.addLayout(self._buttonLayout)
- self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
-
- self._window.setWindowTitle("Account")
- self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
-
- @property
- def doClear(self):
- return self._doClear
-
- def setIfNotificationsSupported(self, isSupported):
- if isSupported:
- self._notificationSelecter.clear()
- self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
- self._notificationTimeSelector.setEnabled(False)
- self._missedCallsNotificationButton.setEnabled(False)
- self._voicemailNotificationButton.setEnabled(False)
- self._smsNotificationButton.setEnabled(False)
- else:
- self._notificationSelecter.clear()
- self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
- self._notificationTimeSelector.setEnabled(False)
- self._missedCallsNotificationButton.setEnabled(False)
- self._voicemailNotificationButton.setEnabled(False)
- self._smsNotificationButton.setEnabled(False)
-
- def set_account_number(self, num):
- self._accountNumberLabel.setText(num)
-
- orientation = property(
- lambda self: str(self._orientationSelector.currentText()),
- lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode),
- )
-
- def _set_voicemail_on_missed(self, status):
- if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
- self._voicemailOnMissedButton.setChecked(False)
- self._voicemailOnMissedButton.hide()
- elif status == self.VOICEMAIL_CHECK_DISABLED:
- self._voicemailOnMissedButton.setChecked(False)
- self._voicemailOnMissedButton.show()
- elif status == self.VOICEMAIL_CHECK_ENABLED:
- self._voicemailOnMissedButton.setChecked(True)
- self._voicemailOnMissedButton.show()
- else:
- raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
-
- def _get_voicemail_on_missed(self):
- if not self._voicemailOnMissedButton.isVisible():
- return self.VOICEMAIL_CHECK_NOT_SUPPORTED
- elif self._voicemailOnMissedButton.isChecked():
- return self.VOICEMAIL_CHECK_ENABLED
- else:
- return self.VOICEMAIL_CHECK_DISABLED
-
- updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
-
- notifications = property(
- lambda self: str(self._notificationSelecter.currentText()),
- lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled),
- )
-
- notifyOnMissed = property(
- lambda self: self._missedCallsNotificationButton.isChecked(),
- lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
- )
-
- notifyOnVoicemail = property(
- lambda self: self._voicemailNotificationButton.isChecked(),
- lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
- )
-
- notifyOnSms = property(
- lambda self: self._smsNotificationButton.isChecked(),
- lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
- )
-
- def _get_notification_time(self):
- index = self._notificationTimeSelector.currentIndex()
- minutes = self._RECURRENCE_CHOICES[index][0]
- return minutes
-
- def _set_notification_time(self, minutes):
- for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
- if time == minutes:
- self._notificationTimeSelector.setCurrentIndex(i)
- break
- else:
- self._notificationTimeSelector.setCurrentIndex(0)
-
- notificationTime = property(_get_notification_time, _set_notification_time)
-
- @property
- def selectedCallback(self):
- index = self._callbackSelector.currentIndex()
- data = str(self._callbackSelector.itemData(index))
- return data
-
- def set_callbacks(self, choices, default):
- self._callbackSelector.clear()
-
- self._callbackSelector.addItem("Not Set", "")
-
- uglyDefault = misc_utils.make_ugly(default)
- if not uglyDefault:
- uglyDefault = default
- for number, description in choices.iteritems():
- prettyNumber = misc_utils.make_pretty(number)
- uglyNumber = misc_utils.make_ugly(number)
- if not uglyNumber:
- prettyNumber = number
- uglyNumber = number
-
- self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
- if uglyNumber == uglyDefault:
- self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
-
- def run(self):
- self._doClear = False
- self._window.show()
-
- def close(self):
- try:
- self._window.hide()
- except RuntimeError:
- _moduleLogger.exception("Oh well")
-
- def _update_notification_state(self):
- currentText = str(self._notificationSelecter.currentText())
- if currentText == self.ALARM_BACKGROUND:
- self._notificationTimeSelector.setEnabled(True)
-
- self._missedCallsNotificationButton.setEnabled(True)
- self._voicemailNotificationButton.setEnabled(True)
- self._smsNotificationButton.setEnabled(True)
- elif currentText == self.ALARM_APPLICATION:
- self._notificationTimeSelector.setEnabled(True)
-
- self._missedCallsNotificationButton.setEnabled(False)
- self._voicemailNotificationButton.setEnabled(True)
- self._smsNotificationButton.setEnabled(True)
-
- self._missedCallsNotificationButton.setChecked(False)
- else:
- self._notificationTimeSelector.setEnabled(False)
-
- self._missedCallsNotificationButton.setEnabled(False)
- self._voicemailNotificationButton.setEnabled(False)
- self._smsNotificationButton.setEnabled(False)
-
- self._missedCallsNotificationButton.setChecked(False)
- self._voicemailNotificationButton.setChecked(False)
- self._smsNotificationButton.setChecked(False)
-
- @qt_compat.Slot(int)
- @misc_utils.log_exception(_moduleLogger)
- def _on_notification_change(self, index):
- with qui_utils.notify_error(self._app.errorLog):
- self._update_notification_state()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_settings_cancel(self, checked = False):
- with qui_utils.notify_error(self._app.errorLog):
- self.hide()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- def _on_settings_apply(self, checked = False):
- self.__on_settings_apply(checked)
-
- @misc_utils.log_exception(_moduleLogger)
- def __on_settings_apply(self, checked = False):
- with qui_utils.notify_error(self._app.errorLog):
- self.settingsApproved.emit()
- self.hide()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_clear(self, checked = False):
- with qui_utils.notify_error(self._app.errorLog):
- self._doClear = True
- self.settingsApproved.emit()
- self.hide()
-
-
-class ContactList(object):
-
- _SENTINEL_ICON = QtGui.QIcon()
-
- def __init__(self, app, session):
- self._app = app
- self._session = session
- self._targetLayout = QtGui.QVBoxLayout()
- self._targetList = QtGui.QWidget()
- self._targetList.setLayout(self._targetLayout)
- self._uiItems = []
- self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
-
- @property
- def toplevel(self):
- return self._targetList
-
- def setVisible(self, isVisible):
- self._targetList.setVisible(isVisible)
-
- def update(self):
- cids = list(self._session.draft.get_contacts())
- amountCommon = min(len(cids), len(self._uiItems))
-
- # Run through everything in common
- for i in xrange(0, amountCommon):
- cid = cids[i]
- uiItem = self._uiItems[i]
- title = self._session.draft.get_title(cid)
- description = self._session.draft.get_description(cid)
- numbers = self._session.draft.get_numbers(cid)
- uiItem["cid"] = cid
- uiItem["title"] = title
- uiItem["description"] = description
- uiItem["numbers"] = numbers
- uiItem["label"].setText(title)
- self._populate_number_selector(uiItem["selector"], cid, i, numbers)
- uiItem["rowWidget"].setVisible(True)
-
- # More contacts than ui items
- for i in xrange(amountCommon, len(cids)):
- cid = cids[i]
- title = self._session.draft.get_title(cid)
- description = self._session.draft.get_description(cid)
- numbers = self._session.draft.get_numbers(cid)
-
- titleLabel = QtGui.QLabel(title)
- titleLabel.setWordWrap(True)
- numberSelector = QtGui.QComboBox()
- self._populate_number_selector(numberSelector, cid, i, numbers)
-
- callback = functools.partial(
- self._on_change_number,
- i
- )
- callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
- numberSelector.activated.connect(
- qt_compat.Slot(int)(callback)
- )
-
- if self._closeIcon is self._SENTINEL_ICON:
- deleteButton = QtGui.QPushButton("Delete")
- else:
- deleteButton = QtGui.QPushButton(self._closeIcon, "")
- deleteButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.PushButton,
- ))
- callback = functools.partial(
- self._on_remove_contact,
- i
- )
- callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
- deleteButton.clicked.connect(callback)
-
- rowLayout = QtGui.QHBoxLayout()
- rowLayout.addWidget(titleLabel, 1000)
- rowLayout.addWidget(numberSelector, 0)
- rowLayout.addWidget(deleteButton, 0)
- rowWidget = QtGui.QWidget()
- rowWidget.setLayout(rowLayout)
- self._targetLayout.addWidget(rowWidget)
-
- uiItem = {}
- uiItem["cid"] = cid
- uiItem["title"] = title
- uiItem["description"] = description
- uiItem["numbers"] = numbers
- uiItem["label"] = titleLabel
- uiItem["selector"] = numberSelector
- uiItem["rowWidget"] = rowWidget
- self._uiItems.append(uiItem)
- amountCommon = i+1
-
- # More UI items than contacts
- for i in xrange(amountCommon, len(self._uiItems)):
- uiItem = self._uiItems[i]
- uiItem["rowWidget"].setVisible(False)
- amountCommon = i+1
-
- def _populate_number_selector(self, selector, cid, cidIndex, numbers):
- selector.clear()
-
- selectedNumber = self._session.draft.get_selected_number(cid)
- if len(numbers) == 1:
- # If no alt numbers available, check the address book
- numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
- else:
- defaultIndex = _index_number(numbers, selectedNumber)
-
- for number, description in numbers:
- if description:
- label = "%s - %s" % (number, description)
- else:
- label = number
- selector.addItem(label)
- selector.setVisible(True)
- if 1 < len(numbers):
- selector.setEnabled(True)
- selector.setCurrentIndex(defaultIndex)
- else:
- selector.setEnabled(False)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_change_number(self, cidIndex, index):
- with qui_utils.notify_error(self._app.errorLog):
- # Exception thrown when the first item is removed
- try:
- cid = self._uiItems[cidIndex]["cid"]
- numbers = self._session.draft.get_numbers(cid)
- except IndexError:
- _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
- return
- except KeyError:
- _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
- return
- number = numbers[index][0]
- self._session.draft.set_selected_number(cid, number)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_remove_contact(self, index, toggled):
- with qui_utils.notify_error(self._app.errorLog):
- self._session.draft.remove_contact(self._uiItems[index]["cid"])
-
-
-class VoicemailPlayer(object):
-
- def __init__(self, app, session, errorLog):
- self._app = app
- self._session = session
- self._errorLog = errorLog
- self._token = None
- self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
- self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
-
- self._playButton = QtGui.QPushButton("Play")
- self._playButton.clicked.connect(self._on_voicemail_play)
- self._pauseButton = QtGui.QPushButton("Pause")
- self._pauseButton.clicked.connect(self._on_voicemail_pause)
- self._pauseButton.hide()
- self._resumeButton = QtGui.QPushButton("Resume")
- self._resumeButton.clicked.connect(self._on_voicemail_resume)
- self._resumeButton.hide()
- self._stopButton = QtGui.QPushButton("Stop")
- self._stopButton.clicked.connect(self._on_voicemail_stop)
- self._stopButton.hide()
-
- self._downloadButton = QtGui.QPushButton("Download Voicemail")
- self._downloadButton.clicked.connect(self._on_voicemail_download)
- self._downloadLayout = QtGui.QHBoxLayout()
- self._downloadLayout.addWidget(self._downloadButton)
- self._downloadWidget = QtGui.QWidget()
- self._downloadWidget.setLayout(self._downloadLayout)
-
- self._playLabel = QtGui.QLabel("Voicemail")
- self._saveButton = QtGui.QPushButton("Save")
- self._saveButton.clicked.connect(self._on_voicemail_save)
- self._playerLayout = QtGui.QHBoxLayout()
- self._playerLayout.addWidget(self._playLabel)
- self._playerLayout.addWidget(self._playButton)
- self._playerLayout.addWidget(self._pauseButton)
- self._playerLayout.addWidget(self._resumeButton)
- self._playerLayout.addWidget(self._stopButton)
- self._playerLayout.addWidget(self._saveButton)
- self._playerWidget = QtGui.QWidget()
- self._playerWidget.setLayout(self._playerLayout)
-
- self._visibleWidget = None
- self._layout = QtGui.QHBoxLayout()
- self._layout.setContentsMargins(0, 0, 0, 0)
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
- self._update_state()
-
- @property
- def toplevel(self):
- return self._widget
-
- def destroy(self):
- self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
- self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
- self._invalidate_token()
-
- def _invalidate_token(self):
- if self._token is not None:
- self._token.invalidate()
- self._token.error.disconnect(self._on_play_error)
- self._token.stateChange.connect(self._on_play_state)
- self._token.invalidated.connect(self._on_play_invalidated)
-
- def _show_download(self, messageId):
- if self._visibleWidget is self._downloadWidget:
- return
- self._hide()
- self._layout.addWidget(self._downloadWidget)
- self._visibleWidget = self._downloadWidget
- self._visibleWidget.show()
-
- def _show_player(self, messageId):
- if self._visibleWidget is self._playerWidget:
- return
- self._hide()
- self._layout.addWidget(self._playerWidget)
- self._visibleWidget = self._playerWidget
- self._visibleWidget.show()
-
- def _hide(self):
- if self._visibleWidget is None:
- return
- self._visibleWidget.hide()
- self._layout.removeWidget(self._visibleWidget)
- self._visibleWidget = None
-
- def _update_play_state(self):
- if self._token is not None and self._token.isValid:
- self._playButton.setText("Stop")
- else:
- self._playButton.setText("Play")
-
- def _update_state(self):
- if self._session.draft.get_num_contacts() != 1:
- self._hide()
- return
-
- (cid, ) = self._session.draft.get_contacts()
- messageId = self._session.draft.get_message_id(cid)
- if messageId is None:
- self._hide()
- return
-
- if self._session.is_available(messageId):
- self._show_player(messageId)
- else:
- self._show_download(messageId)
- if self._token is not None:
- self._token.invalidate()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_save(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
- targetPath = unicode(targetPath)
- if not targetPath:
- return
-
- (cid, ) = self._session.draft.get_contacts()
- messageId = self._session.draft.get_message_id(cid)
- sourcePath = self._session.voicemail_path(messageId)
- import shutil
- shutil.copy2(sourcePath, targetPath)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_play_error(self, error):
- with qui_utils.notify_error(self._app.errorLog):
- self._app.errorLog.push_error(error)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_play_invalidated(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._playButton.show()
- self._pauseButton.hide()
- self._resumeButton.hide()
- self._stopButton.hide()
- self._invalidate_token()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_play_state(self, state):
- with qui_utils.notify_error(self._app.errorLog):
- if state == self._token.STATE_PLAY:
- self._playButton.hide()
- self._pauseButton.show()
- self._resumeButton.hide()
- self._stopButton.show()
- elif state == self._token.STATE_PAUSE:
- self._playButton.hide()
- self._pauseButton.hide()
- self._resumeButton.show()
- self._stopButton.show()
- elif state == self._token.STATE_STOP:
- self._playButton.show()
- self._pauseButton.hide()
- self._resumeButton.hide()
- self._stopButton.hide()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_play(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- (cid, ) = self._session.draft.get_contacts()
- messageId = self._session.draft.get_message_id(cid)
- sourcePath = self._session.voicemail_path(messageId)
-
- self._invalidate_token()
- uri = "file://%s" % sourcePath
- self._token = self._app.streamHandler.set_file(uri)
- self._token.stateChange.connect(self._on_play_state)
- self._token.invalidated.connect(self._on_play_invalidated)
- self._token.error.connect(self._on_play_error)
- self._token.play()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_pause(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- self._token.pause()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_resume(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- self._token.play()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_stop(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- self._token.stop()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_download(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- (cid, ) = self._session.draft.get_contacts()
- messageId = self._session.draft.get_message_id(cid)
- self._session.download_voicemail(messageId)
- self._hide()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_recipients_changed(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._update_state()
-
- @qt_compat.Slot(str, str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_voicemail_downloaded(self, messageId, filepath):
- with qui_utils.notify_error(self._app.errorLog):
- self._update_state()
-
-
-class SMSEntryWindow(qwrappers.WindowWrapper):
-
- MAX_CHAR = 160
- # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
-
- def __init__(self, parent, app, session, errorLog):
- qwrappers.WindowWrapper.__init__(self, parent, app)
- self._session = session
- self._session.messagesUpdated.connect(self._on_refresh_history)
- self._session.historyUpdated.connect(self._on_refresh_history)
- self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
-
- self._session.draft.sendingMessage.connect(self._on_op_started)
- self._session.draft.calling.connect(self._on_op_started)
- self._session.draft.calling.connect(self._on_calling_started)
- self._session.draft.cancelling.connect(self._on_op_started)
-
- self._session.draft.sentMessage.connect(self._on_op_finished)
- self._session.draft.called.connect(self._on_op_finished)
- self._session.draft.cancelled.connect(self._on_op_finished)
- self._session.draft.error.connect(self._on_op_error)
-
- self._errorLog = errorLog
-
- self._targetList = ContactList(self._app, self._session)
- self._history = QtGui.QLabel()
- self._history.setTextFormat(QtCore.Qt.RichText)
- self._history.setWordWrap(True)
- self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
- self._smsEntry = QtGui.QTextEdit()
- self._smsEntry.textChanged.connect(self._on_letter_count_changed)
-
- self._entryLayout = QtGui.QVBoxLayout()
- self._entryLayout.addWidget(self._targetList.toplevel)
- self._entryLayout.addWidget(self._history)
- self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
- self._entryLayout.addWidget(self._smsEntry)
- self._entryLayout.setContentsMargins(0, 0, 0, 0)
- self._entryWidget = QtGui.QWidget()
- self._entryWidget.setLayout(self._entryLayout)
- self._entryWidget.setContentsMargins(0, 0, 0, 0)
- self._scrollEntry = QtGui.QScrollArea()
- self._scrollEntry.setWidget(self._entryWidget)
- self._scrollEntry.setWidgetResizable(True)
- self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
- self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
- self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
- self._characterCountLabel = QtGui.QLabel("")
- self._singleNumberSelector = QtGui.QComboBox()
- self._cids = []
- self._singleNumberSelector.activated.connect(self._on_single_change_number)
- self._smsButton = QtGui.QPushButton("SMS")
- self._smsButton.clicked.connect(self._on_sms_clicked)
- self._smsButton.setEnabled(False)
- self._dialButton = QtGui.QPushButton("Dial")
- self._dialButton.clicked.connect(self._on_call_clicked)
- self._cancelButton = QtGui.QPushButton("Cancel Call")
- self._cancelButton.clicked.connect(self._on_cancel_clicked)
- self._cancelButton.setVisible(False)
-
- self._buttonLayout = QtGui.QHBoxLayout()
- self._buttonLayout.addWidget(self._characterCountLabel)
- self._buttonLayout.addStretch()
- self._buttonLayout.addWidget(self._singleNumberSelector)
- self._buttonLayout.addStretch()
- self._buttonLayout.addWidget(self._smsButton)
- self._buttonLayout.addWidget(self._dialButton)
- self._buttonLayout.addWidget(self._cancelButton)
-
- self._layout.addWidget(self._errorDisplay.toplevel)
- self._layout.addWidget(self._scrollEntry)
- self._layout.addLayout(self._buttonLayout)
- self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
-
- self._window.setWindowTitle("Contact")
- self._window.closed.connect(self._on_close_window)
- self._window.hidden.connect(self._on_close_window)
- self._window.resized.connect(self._on_window_resized)
-
- self._scrollTimer = QtCore.QTimer()
- self._scrollTimer.setInterval(100)
- self._scrollTimer.setSingleShot(True)
- self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
-
- self._smsEntry.setPlainText(self._session.draft.message)
- self._update_letter_count()
- self._update_target_fields()
- self.set_fullscreen(self._app.fullscreenAction.isChecked())
- self.update_orientation(self._app.orientation)
-
- def close(self):
- if self._window is None:
- # Already closed
- return
- window = self._window
- try:
- message = unicode(self._smsEntry.toPlainText())
- self._session.draft.message = message
- self.hide()
- except AttributeError:
- _moduleLogger.exception("Oh well")
- except RuntimeError:
- _moduleLogger.exception("Oh well")
-
- def destroy(self):
- self._session.messagesUpdated.disconnect(self._on_refresh_history)
- self._session.historyUpdated.disconnect(self._on_refresh_history)
- self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
- self._session.draft.sendingMessage.disconnect(self._on_op_started)
- self._session.draft.calling.disconnect(self._on_op_started)
- self._session.draft.calling.disconnect(self._on_calling_started)
- self._session.draft.cancelling.disconnect(self._on_op_started)
- self._session.draft.sentMessage.disconnect(self._on_op_finished)
- self._session.draft.called.disconnect(self._on_op_finished)
- self._session.draft.cancelled.disconnect(self._on_op_finished)
- self._session.draft.error.disconnect(self._on_op_error)
- self._voicemailPlayer.destroy()
- window = self._window
- self._window = None
- try:
- window.close()
- window.destroy()
- except AttributeError:
- _moduleLogger.exception("Oh well")
- except RuntimeError:
- _moduleLogger.exception("Oh well")
-
- def update_orientation(self, orientation):
- qwrappers.WindowWrapper.update_orientation(self, orientation)
- self._scroll_to_bottom()
-
- def _update_letter_count(self):
- count = len(self._smsEntry.toPlainText())
- numTexts, numCharInText = divmod(count, self.MAX_CHAR)
- numTexts += 1
- numCharsLeftInText = self.MAX_CHAR - numCharInText
- self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
-
- def _update_button_state(self):
- self._cancelButton.setEnabled(True)
- if self._session.draft.get_num_contacts() == 0:
- self._dialButton.setEnabled(False)
- self._smsButton.setEnabled(False)
- elif self._session.draft.get_num_contacts() == 1:
- count = len(self._smsEntry.toPlainText())
- if count == 0:
- self._dialButton.setEnabled(True)
- self._smsButton.setEnabled(False)
- else:
- self._dialButton.setEnabled(False)
- self._smsButton.setEnabled(True)
- else:
- self._dialButton.setEnabled(False)
- count = len(self._smsEntry.toPlainText())
- if count == 0:
- self._smsButton.setEnabled(False)
- else:
- self._smsButton.setEnabled(True)
-
- def _update_history(self, cid):
- draftContactsCount = self._session.draft.get_num_contacts()
- if draftContactsCount != 1:
- self._history.setVisible(False)
- else:
- description = self._session.draft.get_description(cid)
-
- self._targetList.setVisible(False)
- if description:
- self._history.setText(description)
- self._history.setVisible(True)
- else:
- self._history.setText("")
- self._history.setVisible(False)
-
- def _update_target_fields(self):
- draftContactsCount = self._session.draft.get_num_contacts()
- if draftContactsCount == 0:
- self.hide()
- del self._cids[:]
- elif draftContactsCount == 1:
- (cid, ) = self._session.draft.get_contacts()
- title = self._session.draft.get_title(cid)
- numbers = self._session.draft.get_numbers(cid)
-
- self._targetList.setVisible(False)
- self._update_history(cid)
- self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
- self._cids = [cid]
-
- self._scroll_to_bottom()
- self._window.setWindowTitle(title)
- self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
- self.show()
- self._window.raise_()
- else:
- self._targetList.setVisible(True)
- self._targetList.update()
- self._history.setText("")
- self._history.setVisible(False)
- self._singleNumberSelector.setVisible(False)
-
- self._scroll_to_bottom()
- self._window.setWindowTitle("Contacts")
- self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
- self.show()
- self._window.raise_()
-
- def _populate_number_selector(self, selector, cid, cidIndex, numbers):
- selector.clear()
-
- selectedNumber = self._session.draft.get_selected_number(cid)
- if len(numbers) == 1:
- # If no alt numbers available, check the address book
- numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
- else:
- defaultIndex = _index_number(numbers, selectedNumber)
-
- for number, description in numbers:
- if description:
- label = "%s - %s" % (number, description)
- else:
- label = number
- selector.addItem(label)
- selector.setVisible(True)
- if 1 < len(numbers):
- selector.setEnabled(True)
- selector.setCurrentIndex(defaultIndex)
- else:
- selector.setEnabled(False)
-
- def _scroll_to_bottom(self):
- self._scrollTimer.start()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_delayed_scroll_to_bottom(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._scrollEntry.ensureWidgetVisible(self._smsEntry)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_sms_clicked(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- message = unicode(self._smsEntry.toPlainText())
- self._session.draft.message = message
- self._session.draft.send()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_call_clicked(self, arg):
- with qui_utils.notify_error(self._app.errorLog):
- message = unicode(self._smsEntry.toPlainText())
- self._session.draft.message = message
- self._session.draft.call()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_cancel_clicked(self, message):
- with qui_utils.notify_error(self._app.errorLog):
- self._session.draft.cancel()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_single_change_number(self, index):
- with qui_utils.notify_error(self._app.errorLog):
- # Exception thrown when the first item is removed
- cid = self._cids[0]
- try:
- numbers = self._session.draft.get_numbers(cid)
- except KeyError:
- _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
- return
- number = numbers[index][0]
- self._session.draft.set_selected_number(cid, number)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh_history(self):
- with qui_utils.notify_error(self._app.errorLog):
- draftContactsCount = self._session.draft.get_num_contacts()
- if draftContactsCount != 1:
- # Changing contact count will automatically refresh it
- return
- (cid, ) = self._session.draft.get_contacts()
- self._update_history(cid)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_recipients_changed(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._update_target_fields()
- self._update_button_state()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_op_started(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._smsEntry.setReadOnly(True)
- self._smsButton.setVisible(False)
- self._dialButton.setVisible(False)
- self.show()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_calling_started(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._cancelButton.setVisible(True)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_op_finished(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._smsEntry.setPlainText("")
- self._smsEntry.setReadOnly(False)
- self._cancelButton.setVisible(False)
- self._smsButton.setVisible(True)
- self._dialButton.setVisible(True)
- self.close()
- self.destroy()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_op_error(self, message):
- with qui_utils.notify_error(self._app.errorLog):
- self._smsEntry.setReadOnly(False)
- self._cancelButton.setVisible(False)
- self._smsButton.setVisible(True)
- self._dialButton.setVisible(True)
-
- self._errorLog.push_error(message)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_letter_count_changed(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._update_letter_count()
- self._update_button_state()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_window_resized(self):
- with qui_utils.notify_error(self._app.errorLog):
- self._scroll_to_bottom()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_close_window(self, checked = True):
- with qui_utils.notify_error(self._app.errorLog):
- self.close()
-
-
-def _index_number(numbers, default):
- uglyDefault = misc_utils.make_ugly(default)
- uglyContactNumbers = list(
- misc_utils.make_ugly(contactNumber)
- for (contactNumber, _) in numbers
- )
- defaultMatches = [
- misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
- for contactNumber in uglyContactNumbers
- ]
- try:
- defaultIndex = defaultMatches.index(True)
- except ValueError:
- defaultIndex = -1
- _moduleLogger.warn(
- "Could not find contact number %s among %r" % (
- default, numbers
- )
- )
- return defaultIndex
-
-
-def _get_contact_numbers(session, contactId, number, description):
- contactPhoneNumbers = []
- if contactId and contactId != "0":
- try:
- contactDetails = copy.deepcopy(session.get_contacts()[contactId])
- contactPhoneNumbers = contactDetails["numbers"]
- except KeyError:
- contactPhoneNumbers = []
- contactPhoneNumbers = [
- (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
- for contactPhoneNumber in contactPhoneNumbers
- ]
- defaultIndex = _index_number(contactPhoneNumbers, number)
-
- if not contactPhoneNumbers or defaultIndex == -1:
- contactPhoneNumbers += [(number, description)]
- defaultIndex = 0
-
- return contactPhoneNumbers, defaultIndex
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import sys
-import datetime
-import ConfigParser
-
-
-sys.path.insert(0,"/usr/lib/dialcentral/")
-
-
-import constants
-import alarm_notify
-
-
-def notify_on_change():
- with open(constants._notifier_logpath_, "a") as file:
- file.write("Notification: %r\n" % (datetime.datetime.now(), ))
-
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- backend = alarm_notify.create_backend(config)
- notifyUser = alarm_notify.is_changed(config, backend)
-
- if notifyUser:
- file.write("\tChange occurred\n")
-
-
-if __name__ == "__main__":
- notify_on_change()
+++ /dev/null
-#!/usr/bin/env python
-
-import os
-import sys
-import ConfigParser
-import logging
-
-
-sys.path.insert(0,"/usr/lib/dialcentral/")
-
-
-import constants
-import alarm_notify
-
-
-def notify_on_change():
- config = ConfigParser.SafeConfigParser()
- config.read(constants._user_settings_)
- backend = alarm_notify.create_backend(config)
- notifyUser = alarm_notify.is_changed(config, backend)
-
- config = ConfigParser.SafeConfigParser()
- config.read(constants._custom_notifier_settings_)
- soundFile = config.get("Sound Notifier", "soundfile")
- soundFile = "/usr/lib/gv-notifier/alert.mp3"
-
- if notifyUser:
- import subprocess
- import led_handler
- logging.info("Changed, playing %s" % soundFile)
- led = led_handler.LedHandler()
- led.on()
- soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True)
- else:
- logging.info("No Change")
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
- logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__))
- logging.info("OS: %s" % (os.uname()[0], ))
- logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
- logging.info("Hostname: %s" % os.uname()[1])
- try:
- notify_on_change()
- except:
- logging.exception("Error")
- raise
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import datetime
-import string
-import itertools
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-from util import qtpie
-from util import qui_utils
-from util import misc as misc_utils
-
-import backends.null_backend as null_backend
-import backends.file_backend as file_backend
-import backends.qt_backend as qt_backend
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_SENTINEL_ICON = QtGui.QIcon()
-
-
-class Dialpad(object):
-
- def __init__(self, app, session, errorLog):
- self._app = app
- self._session = session
- self._errorLog = errorLog
-
- self._plus = QtGui.QPushButton("+")
- self._plus.clicked.connect(lambda: self._on_keypress("+"))
- self._entry = QtGui.QLineEdit()
-
- backAction = QtGui.QAction(None)
- backAction.setText("Back")
- backAction.triggered.connect(self._on_backspace)
- backPieItem = qtpie.QActionPieItem(backAction)
- clearAction = QtGui.QAction(None)
- clearAction.setText("Clear")
- clearAction.triggered.connect(self._on_clear_text)
- clearPieItem = qtpie.QActionPieItem(clearAction)
- backSlices = [
- qtpie.PieFiling.NULL_CENTER,
- clearPieItem,
- qtpie.PieFiling.NULL_CENTER,
- qtpie.PieFiling.NULL_CENTER,
- ]
- self._back = qtpie.QPieButton(backPieItem)
- self._back.set_center(backPieItem)
- for slice in backSlices:
- self._back.insertItem(slice)
-
- self._entryLayout = QtGui.QHBoxLayout()
- self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
- self._entryLayout.addWidget(self._entry, 1000)
- self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
-
- smsIcon = self._app.get_icon("messages.png")
- self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
- self._smsButton.clicked.connect(self._on_sms_clicked)
- self._smsButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.PushButton,
- ))
- callIcon = self._app.get_icon("dialpad.png")
- self._callButton = QtGui.QPushButton(callIcon, "Call")
- self._callButton.clicked.connect(self._on_call_clicked)
- self._callButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.PushButton,
- ))
-
- self._padLayout = QtGui.QGridLayout()
- rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
- columns = [0, 1, 2] * 3
- keys = [
- ("1", ""),
- ("2", "ABC"),
- ("3", "DEF"),
- ("4", "GHI"),
- ("5", "JKL"),
- ("6", "MNO"),
- ("7", "PQRS"),
- ("8", "TUV"),
- ("9", "WXYZ"),
- ]
- for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
- self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
- self._zerothButton = QtGui.QPushButton("0")
- self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
- self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.PushButton,
- ))
- self._padLayout.addWidget(self._smsButton, 3, 0)
- self._padLayout.addWidget(self._zerothButton)
- self._padLayout.addWidget(self._callButton, 3, 2)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addLayout(self._entryLayout, 0)
- self._layout.addLayout(self._padLayout, 1000000)
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
-
- @property
- def toplevel(self):
- return self._widget
-
- def enable(self):
- self._smsButton.setEnabled(True)
- self._callButton.setEnabled(True)
-
- def disable(self):
- self._smsButton.setEnabled(False)
- self._callButton.setEnabled(False)
-
- def get_settings(self):
- return {}
-
- def set_settings(self, settings):
- pass
-
- def clear(self):
- pass
-
- def refresh(self, force = True):
- pass
-
- def _generate_key_button(self, center, letters):
- button = QtGui.QPushButton("%s\n%s" % (center, letters))
- button.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.PushButton,
- ))
- button.clicked.connect(lambda: self._on_keypress(center))
- return button
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_keypress(self, key):
- with qui_utils.notify_error(self._errorLog):
- self._entry.insert(key)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_backspace(self, toggled = False):
- with qui_utils.notify_error(self._errorLog):
- self._entry.backspace()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_clear_text(self, toggled = False):
- with qui_utils.notify_error(self._errorLog):
- self._entry.clear()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_sms_clicked(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- number = misc_utils.make_ugly(str(self._entry.text()))
- self._entry.clear()
-
- contactId = number
- title = misc_utils.make_pretty(number)
- description = misc_utils.make_pretty(number)
- numbersWithDescriptions = [(number, "")]
- self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc_utils.log_exception(_moduleLogger)
- def _on_call_clicked(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- number = misc_utils.make_ugly(str(self._entry.text()))
- self._entry.clear()
-
- contactId = number
- title = misc_utils.make_pretty(number)
- description = misc_utils.make_pretty(number)
- numbersWithDescriptions = [(number, "")]
- self._session.draft.clear()
- self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
- self._session.draft.call()
-
-
-class TimeCategories(object):
-
- _NOW_SECTION = 0
- _TODAY_SECTION = 1
- _WEEK_SECTION = 2
- _MONTH_SECTION = 3
- _REST_SECTION = 4
- _MAX_SECTIONS = 5
-
- _NO_ELAPSED = datetime.timedelta(hours=1)
- _WEEK_ELAPSED = datetime.timedelta(weeks=1)
- _MONTH_ELAPSED = datetime.timedelta(days=30)
-
- def __init__(self, parentItem):
- self._timeItems = [
- QtGui.QStandardItem(description)
- for (i, description) in zip(
- xrange(self._MAX_SECTIONS),
- ["Now", "Today", "Week", "Month", "Past"],
- )
- ]
- for item in self._timeItems:
- item.setEditable(False)
- item.setCheckable(False)
- row = (item, )
- parentItem.appendRow(row)
-
- self._today = datetime.datetime(1900, 1, 1)
-
- self.prepare_for_update(self._today)
-
- def prepare_for_update(self, newToday):
- self._today = newToday
- for item in self._timeItems:
- item.removeRows(0, item.rowCount())
- try:
- hour = self._today.strftime("%X")
- day = self._today.strftime("%x")
- except ValueError:
- _moduleLogger.exception("Can't format times")
- hour = "Now"
- day = "Today"
- self._timeItems[self._NOW_SECTION].setText(hour)
- self._timeItems[self._TODAY_SECTION].setText(day)
-
- def add_row(self, rowDate, row):
- elapsedTime = self._today - rowDate
- todayTuple = self._today.timetuple()
- rowTuple = rowDate.timetuple()
- if elapsedTime < self._NO_ELAPSED:
- section = self._NOW_SECTION
- elif todayTuple[0:3] == rowTuple[0:3]:
- section = self._TODAY_SECTION
- elif elapsedTime < self._WEEK_ELAPSED:
- section = self._WEEK_SECTION
- elif elapsedTime < self._MONTH_ELAPSED:
- section = self._MONTH_SECTION
- else:
- section = self._REST_SECTION
- self._timeItems[section].appendRow(row)
-
- def get_item(self, timeIndex, rowIndex, column):
- timeItem = self._timeItems[timeIndex]
- item = timeItem.child(rowIndex, column)
- return item
-
-
-class History(object):
-
- DETAILS_IDX = 0
- FROM_IDX = 1
- MAX_IDX = 2
-
- HISTORY_RECEIVED = "Received"
- HISTORY_MISSED = "Missed"
- HISTORY_PLACED = "Placed"
- HISTORY_ALL = "All"
-
- HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
- HISTORY_COLUMNS = ["", "From"]
- assert len(HISTORY_COLUMNS) == MAX_IDX
-
- def __init__(self, app, session, errorLog):
- self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
- self._app = app
- self._session = session
- self._session.historyUpdated.connect(self._on_history_updated)
- self._errorLog = errorLog
-
- self._typeSelection = QtGui.QComboBox()
- self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
- self._typeSelection.setCurrentIndex(
- self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
- )
- self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
- refreshIcon = qui_utils.get_theme_icon(
- ("view-refresh", "general_refresh", "gtk-refresh", ),
- _SENTINEL_ICON
- )
- if refreshIcon is not _SENTINEL_ICON:
- self._refreshButton = QtGui.QPushButton(refreshIcon, "")
- else:
- self._refreshButton = QtGui.QPushButton("Refresh")
- self._refreshButton.clicked.connect(self._on_refresh_clicked)
- self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.PushButton,
- ))
- self._managerLayout = QtGui.QHBoxLayout()
- self._managerLayout.addWidget(self._typeSelection, 1000)
- self._managerLayout.addWidget(self._refreshButton, 0)
-
- self._itemStore = QtGui.QStandardItemModel()
- self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
- self._categoryManager = TimeCategories(self._itemStore)
-
- self._itemView = QtGui.QTreeView()
- self._itemView.setModel(self._itemStore)
- self._itemView.setUniformRowHeights(True)
- self._itemView.setRootIsDecorated(False)
- self._itemView.setIndentation(0)
- self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
- self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
- self._itemView.setHeaderHidden(True)
- self._itemView.setItemsExpandable(False)
- self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
- self._itemView.activated.connect(self._on_row_activated)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addLayout(self._managerLayout)
- self._layout.addWidget(self._itemView)
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
-
- self._actionIcon = {
- "Placed": self._app.get_icon("placed.png"),
- "Missed": self._app.get_icon("missed.png"),
- "Received": self._app.get_icon("received.png"),
- }
-
- self._populate_items()
-
- @property
- def toplevel(self):
- return self._widget
-
- def enable(self):
- self._itemView.setEnabled(True)
-
- def disable(self):
- self._itemView.setEnabled(False)
-
- def get_settings(self):
- return {
- "filter": self._selectedFilter,
- }
-
- def set_settings(self, settings):
- selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
- if selectedFilter in self.HISTORY_ITEM_TYPES:
- self._selectedFilter = selectedFilter
- self._typeSelection.setCurrentIndex(
- self.HISTORY_ITEM_TYPES.index(selectedFilter)
- )
-
- def clear(self):
- self._itemView.clear()
-
- def refresh(self, force=True):
- self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-
- if self._selectedFilter == self.HISTORY_RECEIVED:
- self._session.update_history(self._session.HISTORY_RECEIVED, force)
- elif self._selectedFilter == self.HISTORY_MISSED:
- self._session.update_history(self._session.HISTORY_MISSED, force)
- elif self._selectedFilter == self.HISTORY_PLACED:
- self._session.update_history(self._session.HISTORY_PLACED, force)
- elif self._selectedFilter == self.HISTORY_ALL:
- self._session.update_history(self._session.HISTORY_ALL, force)
- else:
- assert False, "How did we get here?"
-
- if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
- self._app.ledHandler.off()
-
- def _populate_items(self):
- self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
-
- history = self._session.get_history()
- history.sort(key=lambda item: item["time"], reverse=True)
- for event in history:
- if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
- continue
-
- relTime = event["relTime"]
- action = event["action"]
- number = event["number"]
- prettyNumber = misc_utils.make_pretty(number)
- if prettyNumber.startswith("+1 "):
- prettyNumber = prettyNumber[len("+1 "):]
- name = event["name"]
- if not name or name == number:
- name = event["location"]
- if not name:
- name = "Unknown"
-
- detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime))
- detailsFont = detailsItem.font()
- detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 5))
- detailsItem.setFont(detailsFont)
- nameItem = QtGui.QStandardItem(name)
- nameFont = nameItem.font()
- nameFont.setPointSize(nameFont.pointSize() + 4)
- nameItem.setFont(nameFont)
- row = detailsItem, nameItem
- for item in row:
- item.setEditable(False)
- item.setCheckable(False)
- row[self.DETAILS_IDX].setData(event)
- self._categoryManager.add_row(event["time"], row)
- self._itemView.expandAll()
-
- @qt_compat.Slot(str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_filter_changed(self, newItem):
- with qui_utils.notify_error(self._errorLog):
- self._selectedFilter = str(newItem)
- self._populate_items()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_history_updated(self):
- with qui_utils.notify_error(self._errorLog):
- self._populate_items()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh_clicked(self, arg = None):
- with qui_utils.notify_error(self._errorLog):
- self.refresh(force=True)
-
- @qt_compat.Slot(QtCore.QModelIndex)
- @misc_utils.log_exception(_moduleLogger)
- def _on_row_activated(self, index):
- with qui_utils.notify_error(self._errorLog):
- timeIndex = index.parent()
- if not timeIndex.isValid():
- return
- timeRow = timeIndex.row()
- row = index.row()
- detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
- fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
- contactDetails = detailsItem.data()
-
- title = unicode(fromItem.text())
- number = str(contactDetails["number"])
- contactId = number # ids don't seem too unique so using numbers
-
- descriptionRows = []
- for t in xrange(self._itemStore.rowCount()):
- randomTimeItem = self._itemStore.item(t, 0)
- for i in xrange(randomTimeItem.rowCount()):
- iItem = randomTimeItem.child(i, 0)
- iContactDetails = iItem.data()
- iNumber = str(iContactDetails["number"])
- if number != iNumber:
- continue
- relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
- action = str(iContactDetails["action"])
- number = str(iContactDetails["number"])
- prettyNumber = misc_utils.make_pretty(number)
- rowItems = relTime, action, prettyNumber
- descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
- description = "<table>%s</table>" % "".join(descriptionRows)
- numbersWithDescriptions = [(str(contactDetails["number"]), "")]
- self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
-
-
-class Messages(object):
-
- NO_MESSAGES = "None"
- VOICEMAIL_MESSAGES = "Voicemail"
- TEXT_MESSAGES = "SMS"
- ALL_TYPES = "All Messages"
- MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
-
- UNREAD_STATUS = "Unread"
- UNARCHIVED_STATUS = "Inbox"
- ALL_STATUS = "Any"
- MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
-
- _MIN_MESSAGES_SHOWN = 1
-
- def __init__(self, app, session, errorLog):
- self._selectedTypeFilter = self.ALL_TYPES
- self._selectedStatusFilter = self.ALL_STATUS
- self._app = app
- self._session = session
- self._session.messagesUpdated.connect(self._on_messages_updated)
- self._errorLog = errorLog
-
- self._typeSelection = QtGui.QComboBox()
- self._typeSelection.addItems(self.MESSAGE_TYPES)
- self._typeSelection.setCurrentIndex(
- self.MESSAGE_TYPES.index(self._selectedTypeFilter)
- )
- self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
-
- self._statusSelection = QtGui.QComboBox()
- self._statusSelection.addItems(self.MESSAGE_STATUSES)
- self._statusSelection.setCurrentIndex(
- self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
- )
- self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
-
- refreshIcon = qui_utils.get_theme_icon(
- ("view-refresh", "general_refresh", "gtk-refresh", ),
- _SENTINEL_ICON
- )
- if refreshIcon is not _SENTINEL_ICON:
- self._refreshButton = QtGui.QPushButton(refreshIcon, "")
- else:
- self._refreshButton = QtGui.QPushButton("Refresh")
- self._refreshButton.clicked.connect(self._on_refresh_clicked)
- self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.PushButton,
- ))
-
- self._selectionLayout = QtGui.QHBoxLayout()
- self._selectionLayout.addWidget(self._typeSelection, 1000)
- self._selectionLayout.addWidget(self._statusSelection, 1000)
- self._selectionLayout.addWidget(self._refreshButton, 0)
-
- self._itemStore = QtGui.QStandardItemModel()
- self._itemStore.setHorizontalHeaderLabels(["Messages"])
- self._categoryManager = TimeCategories(self._itemStore)
-
- self._htmlDelegate = qui_utils.QHtmlDelegate()
- self._itemView = QtGui.QTreeView()
- self._itemView.setModel(self._itemStore)
- self._itemView.setUniformRowHeights(False)
- self._itemView.setRootIsDecorated(False)
- self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
- self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
- self._itemView.setHeaderHidden(True)
- self._itemView.setItemsExpandable(False)
- self._itemView.setItemDelegate(self._htmlDelegate)
- self._itemView.activated.connect(self._on_row_activated)
- self._itemView.header().sectionResized.connect(self._on_column_resized)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addLayout(self._selectionLayout)
- self._layout.addWidget(self._itemView)
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
-
- self._populate_items()
-
- @property
- def toplevel(self):
- return self._widget
-
- def enable(self):
- self._itemView.setEnabled(True)
-
- def disable(self):
- self._itemView.setEnabled(False)
-
- def get_settings(self):
- return {
- "type": self._selectedTypeFilter,
- "status": self._selectedStatusFilter,
- }
-
- def set_settings(self, settings):
- selectedType = settings.get("type", self.ALL_TYPES)
- if selectedType in self.MESSAGE_TYPES:
- self._selectedTypeFilter = selectedType
- self._typeSelection.setCurrentIndex(
- self.MESSAGE_TYPES.index(self._selectedTypeFilter)
- )
-
- selectedStatus = settings.get("status", self.ALL_STATUS)
- if selectedStatus in self.MESSAGE_STATUSES:
- self._selectedStatusFilter = selectedStatus
- self._statusSelection.setCurrentIndex(
- self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
- )
-
- def clear(self):
- self._itemView.clear()
-
- def refresh(self, force=True):
- self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-
- if self._selectedTypeFilter == self.NO_MESSAGES:
- pass
- elif self._selectedTypeFilter == self.TEXT_MESSAGES:
- self._session.update_messages(self._session.MESSAGE_TEXTS, force)
- elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES:
- self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force)
- elif self._selectedTypeFilter == self.ALL_TYPES:
- self._session.update_messages(self._session.MESSAGE_ALL, force)
- else:
- assert False, "How did we get here?"
-
- if (self._app.notifyOnSms or self._app.notifyOnVoicemail) and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
- self._app.ledHandler.off()
-
- def _populate_items(self):
- self._categoryManager.prepare_for_update(self._session.get_when_messages_updated())
-
- rawMessages = self._session.get_messages()
- rawMessages.sort(key=lambda item: item["time"], reverse=True)
- for item in rawMessages:
- isUnarchived = not item["isArchived"]
- isUnread = not item["isRead"]
- visibleStatus = {
- self.UNREAD_STATUS: isUnarchived and isUnread,
- self.UNARCHIVED_STATUS: isUnarchived,
- self.ALL_STATUS: True,
- }[self._selectedStatusFilter]
- visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
- if not (visibleType and visibleStatus):
- continue
-
- relTime = misc_utils.abbrev_relative_date(item["relTime"])
- number = item["number"]
- prettyNumber = misc_utils.make_pretty(number)
- name = item["name"]
- if not name or name == number:
- name = item["location"]
- if not name:
- name = "Unknown"
-
- messageParts = list(item["messageParts"])
- if len(messageParts) == 0:
- messages = ("No Transcription", )
- elif len(messageParts) == 1:
- if messageParts[0][1]:
- messages = (messageParts[0][1], )
- else:
- messages = ("No Transcription", )
- else:
- messages = [
- "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
- for messagePart in messageParts
- ]
-
- firstMessage = "<b>%s<br/>%s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
-
- expandedMessages = [firstMessage]
- expandedMessages.extend(messages)
- if self._MIN_MESSAGES_SHOWN < 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:
- 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()
-
- @qt_compat.Slot(str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_type_filter_changed(self, newItem):
- with qui_utils.notify_error(self._errorLog):
- self._selectedTypeFilter = str(newItem)
- self._populate_items()
-
- @qt_compat.Slot(str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_status_filter_changed(self, newItem):
- with qui_utils.notify_error(self._errorLog):
- self._selectedStatusFilter = str(newItem)
- self._populate_items()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh_clicked(self, arg = None):
- with qui_utils.notify_error(self._errorLog):
- self.refresh(force=True)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_messages_updated(self):
- with qui_utils.notify_error(self._errorLog):
- self._populate_items()
-
- @qt_compat.Slot(QtCore.QModelIndex)
- @misc_utils.log_exception(_moduleLogger)
- def _on_row_activated(self, index):
- with qui_utils.notify_error(self._errorLog):
- timeIndex = index.parent()
- if not timeIndex.isValid():
- return
- timeRow = timeIndex.row()
- row = index.row()
- item = self._categoryManager.get_item(timeRow, row, 0)
- contactDetails = item.data()
-
- name = unicode(contactDetails["name"])
- number = str(contactDetails["number"])
- if not name or name == number:
- name = unicode(contactDetails["location"])
- if not name:
- name = "Unknown"
-
- if str(contactDetails["type"]) == "Voicemail":
- messageId = str(contactDetails["id"])
- else:
- messageId = None
- contactId = str(contactDetails["contactId"])
- title = name
- description = unicode(contactDetails["expandedMessages"])
- numbersWithDescriptions = [(number, "")]
- self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions)
-
- @qt_compat.Slot(QtCore.QModelIndex)
- @misc_utils.log_exception(_moduleLogger)
- def _on_column_resized(self, index, oldSize, newSize):
- self._htmlDelegate.setWidth(newSize, self._itemStore)
-
-
-class Contacts(object):
-
- # @todo Provide some sort of letter jump
-
- def __init__(self, app, session, errorLog):
- self._app = app
- self._session = session
- self._session.accountUpdated.connect(self._on_contacts_updated)
- self._errorLog = errorLog
- self._addressBookFactories = [
- null_backend.NullAddressBookFactory(),
- file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
- qt_backend.QtContactsAddressBookFactory(),
- ]
- self._addressBooks = []
-
- self._listSelection = QtGui.QComboBox()
- self._listSelection.addItems([])
- self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
- self._activeList = "None"
- refreshIcon = qui_utils.get_theme_icon(
- ("view-refresh", "general_refresh", "gtk-refresh", ),
- _SENTINEL_ICON
- )
- if refreshIcon is not _SENTINEL_ICON:
- self._refreshButton = QtGui.QPushButton(refreshIcon, "")
- else:
- self._refreshButton = QtGui.QPushButton("Refresh")
- self._refreshButton.clicked.connect(self._on_refresh_clicked)
- self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.Minimum,
- QtGui.QSizePolicy.PushButton,
- ))
- self._managerLayout = QtGui.QHBoxLayout()
- self._managerLayout.addWidget(self._listSelection, 1000)
- self._managerLayout.addWidget(self._refreshButton, 0)
-
- self._itemStore = QtGui.QStandardItemModel()
- self._itemStore.setHorizontalHeaderLabels(["Contacts"])
- self._alphaItem = {}
-
- self._itemView = QtGui.QTreeView()
- self._itemView.setModel(self._itemStore)
- self._itemView.setUniformRowHeights(True)
- self._itemView.setRootIsDecorated(False)
- self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
- self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
- self._itemView.setHeaderHidden(True)
- self._itemView.setItemsExpandable(False)
- self._itemView.activated.connect(self._on_row_activated)
-
- self._layout = QtGui.QVBoxLayout()
- self._layout.addLayout(self._managerLayout)
- self._layout.addWidget(self._itemView)
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
-
- self.update_addressbooks()
- self._populate_items()
-
- @property
- def toplevel(self):
- return self._widget
-
- def enable(self):
- self._itemView.setEnabled(True)
-
- def disable(self):
- self._itemView.setEnabled(False)
-
- def get_settings(self):
- return {
- "selectedAddressbook": self._activeList,
- }
-
- def set_settings(self, settings):
- currentItem = settings.get("selectedAddressbook", "None")
- bookNames = [book["name"] for book in self._addressBooks]
- try:
- newIndex = bookNames.index(currentItem)
- except ValueError:
- # Switch over to None for the user
- newIndex = 0
- self._listSelection.setCurrentIndex(newIndex)
- self._activeList = currentItem
-
- def clear(self):
- self._itemView.clear()
-
- def refresh(self, force=True):
- self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
- self._backend.update_account(force)
-
- @property
- def _backend(self):
- return self._addressBooks[self._listSelection.currentIndex()]["book"]
-
- def update_addressbooks(self):
- self._addressBooks = [
- {"book": book, "name": book.name}
- for factory in self._addressBookFactories
- for book in factory.get_addressbooks()
- ]
- self._addressBooks.append(
- {
- "book": self._session,
- "name": "Google Voice",
- }
- )
-
- currentItem = str(self._listSelection.currentText())
- self._activeList = currentItem
- if currentItem == "":
- # Not loaded yet
- currentItem = "None"
- self._listSelection.clear()
- bookNames = [book["name"] for book in self._addressBooks]
- try:
- newIndex = bookNames.index(currentItem)
- except ValueError:
- # Switch over to None for the user
- newIndex = 0
- self._itemStore.clear()
- _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
- self._listSelection.addItems(bookNames)
- self._listSelection.setCurrentIndex(newIndex)
-
- def _populate_items(self):
- self._itemStore.clear()
- self._alphaItem = dict(
- (letter, QtGui.QStandardItem(letter))
- for letter in self._prefixes()
- )
- for letter in self._prefixes():
- item = self._alphaItem[letter]
- item.setEditable(False)
- item.setCheckable(False)
- row = (item, )
- self._itemStore.appendRow(row)
-
- for item in self._get_contacts():
- name = item["name"]
- if not name:
- name = "Unknown"
- numbers = item["numbers"]
-
- nameItem = QtGui.QStandardItem(name)
- nameItem.setEditable(False)
- nameItem.setCheckable(False)
- nameItem.setData(item)
- nameItemFont = nameItem.font()
- nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5))
- nameItem.setFont(nameItemFont)
-
- row = (nameItem, )
- rowKey = name[0].upper()
- rowKey = rowKey if rowKey in self._alphaItem else "#"
- self._alphaItem[rowKey].appendRow(row)
- self._itemView.expandAll()
-
- def _prefixes(self):
- return itertools.chain(string.ascii_uppercase, ("#", ))
-
- def _jump_to_prefix(self, letter):
- i = list(self._prefixes()).index(letter)
- rootIndex = self._itemView.rootIndex()
- currentIndex = self._itemView.model().index(i, 0, rootIndex)
- self._itemView.scrollTo(currentIndex)
- self._itemView.setItemSelected(self._itemView.topLevelItem(i), True)
-
- def _get_contacts(self):
- contacts = list(self._backend.get_contacts().itervalues())
- contacts.sort(key=lambda contact: contact["name"].lower())
- return contacts
-
- @qt_compat.Slot(str)
- @misc_utils.log_exception(_moduleLogger)
- def _on_filter_changed(self, newItem):
- with qui_utils.notify_error(self._errorLog):
- self._activeList = str(newItem)
- self.refresh(force=False)
- self._populate_items()
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_refresh_clicked(self, arg = None):
- with qui_utils.notify_error(self._errorLog):
- self.refresh(force=True)
-
- @qt_compat.Slot()
- @misc_utils.log_exception(_moduleLogger)
- def _on_contacts_updated(self):
- with qui_utils.notify_error(self._errorLog):
- self._populate_items()
-
- @qt_compat.Slot(QtCore.QModelIndex)
- @misc_utils.log_exception(_moduleLogger)
- def _on_row_activated(self, index):
- with qui_utils.notify_error(self._errorLog):
- letterIndex = index.parent()
- if not letterIndex.isValid():
- return
- letterRow = letterIndex.row()
- letter = list(self._prefixes())[letterRow]
- letterItem = self._alphaItem[letter]
- rowIndex = index.row()
- item = letterItem.child(rowIndex, 0)
- contactDetails = item.data()
-
- name = unicode(contactDetails["name"])
- if not name:
- name = unicode(contactDetails["location"])
- if not name:
- name = "Unknown"
-
- contactId = str(contactDetails["contactId"])
- numbers = contactDetails["numbers"]
- numbers = [
- dict(
- (str(k), str(v))
- for (k, v) in number.iteritems()
- )
- for number in numbers
- ]
- numbersWithDescriptions = [
- (
- number["phoneNumber"],
- self._choose_phonetype(number),
- )
- for number in numbers
- ]
- title = name
- description = name
- self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
-
- @staticmethod
- def _choose_phonetype(numberDetails):
- if "phoneTypeName" in numberDetails:
- return numberDetails["phoneTypeName"]
- elif "phoneType" in numberDetails:
- return numberDetails["phoneType"]
- else:
- return ""
+++ /dev/null
-#!/usr/bin/env python
-
-import dbus
-
-
-class _NokiaLedHandler(object):
-
- def __init__(self):
- self._bus = dbus.SystemBus()
- self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
- self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
-
- self._ledPattern = "PatternCommunicationChat"
-
- def on(self):
- self._mceRequest.req_led_pattern_activate(self._ledPattern)
-
- def off(self):
- self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
-
-
-class _NoLedHandler(object):
-
- def __init__(self):
- pass
-
- def on(self):
- pass
-
- def off(self):
- pass
-
-
-class LedHandler(object):
-
- def __init__(self):
- self._actual = None
- self._isReal = False
-
- def on(self):
- self._lazy_init()
- self._actual.on()
-
- def off(self):
- self._lazy_init()
- self._actual.off()
-
- @property
- def isReal(self):
- self._lazy_init()
- self._isReal
-
- def _lazy_init(self):
- if self._actual is not None:
- return
- try:
- self._actual = _NokiaLedHandler()
- self._isReal = True
- except dbus.DBusException:
- self._actual = _NoLedHandler()
- self._isReal = False
-
-
-if __name__ == "__main__":
- leds = _NokiaLedHandler()
- leds.off()
+++ /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
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-
-from util import qore_utils
-from util import qui_utils
-from util import concurrent
-from util import misc as misc_utils
-
-import constants
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class _DraftContact(object):
-
- def __init__(self, messageId, title, description, numbersWithDescriptions):
- self.messageId = messageId
- self.title = title
- self.description = description
- self.numbers = numbersWithDescriptions
- self.selectedNumber = numbersWithDescriptions[0][0]
-
-
-class Draft(QtCore.QObject):
-
- sendingMessage = qt_compat.Signal()
- sentMessage = qt_compat.Signal()
- calling = qt_compat.Signal()
- called = qt_compat.Signal()
- cancelling = qt_compat.Signal()
- cancelled = qt_compat.Signal()
- error = qt_compat.Signal(str)
-
- recipientsChanged = qt_compat.Signal()
-
- def __init__(self, asyncQueue, backend, errorLog):
- QtCore.QObject.__init__(self)
- self._errorLog = errorLog
- self._contacts = {}
- self._asyncQueue = asyncQueue
- self._backend = backend
- self._busyReason = None
- self._message = ""
-
- def send(self):
- assert 0 < len(self._contacts), "No contacts selected"
- assert 0 < len(self._message), "No message to send"
- numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
- le = self._asyncQueue.add_async(self._send)
- le.start(numbers, self._message)
-
- def call(self):
- assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
- assert len(self._message) == 0, "Cannot send message with call"
- (contact, ) = self._contacts.itervalues()
- number = misc_utils.make_ugly(contact.selectedNumber)
- le = self._asyncQueue.add_async(self._call)
- le.start(number)
-
- def cancel(self):
- le = self._asyncQueue.add_async(self._cancel)
- le.start()
-
- def _get_message(self):
- return self._message
-
- def _set_message(self, message):
- self._message = message
-
- message = property(_get_message, _set_message)
-
- def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
- if self._busyReason is not None:
- raise RuntimeError("Please wait for %r" % self._busyReason)
- # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
- contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
- self._contacts[contactId] = contactDetails
- self.recipientsChanged.emit()
-
- def remove_contact(self, contactId):
- if self._busyReason is not None:
- raise RuntimeError("Please wait for %r" % self._busyReason)
- assert contactId in self._contacts, "Contact missing"
- del self._contacts[contactId]
- self.recipientsChanged.emit()
-
- def get_contacts(self):
- return self._contacts.iterkeys()
-
- def get_num_contacts(self):
- return len(self._contacts)
-
- def get_message_id(self, cid):
- return self._contacts[cid].messageId
-
- def get_title(self, cid):
- return self._contacts[cid].title
-
- def get_description(self, cid):
- return self._contacts[cid].description
-
- def get_numbers(self, cid):
- return self._contacts[cid].numbers
-
- def get_selected_number(self, cid):
- return self._contacts[cid].selectedNumber
-
- def set_selected_number(self, cid, number):
- # @note I'm lazy, this isn't firing any kind of signal since only one
- # controller right now and that is the viewer
- assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
- self._contacts[cid].selectedNumber = number
-
- def clear(self):
- if self._busyReason is not None:
- raise RuntimeError("Please wait for %r" % self._busyReason)
- self._clear()
-
- def _clear(self):
- oldContacts = self._contacts
- self._contacts = {}
- self._message = ""
- if oldContacts:
- self.recipientsChanged.emit()
-
- @contextlib.contextmanager
- def _busy(self, message):
- if self._busyReason is not None:
- raise RuntimeError("Already busy doing %r" % self._busyReason)
- try:
- self._busyReason = message
- yield
- finally:
- self._busyReason = None
-
- def _send(self, numbers, text):
- self.sendingMessage.emit()
- try:
- with self._busy("Sending Text"):
- with qui_utils.notify_busy(self._errorLog, "Sending Text"):
- yield (
- self._backend[0].send_sms,
- (numbers, text),
- {},
- )
- self.sentMessage.emit()
- self._clear()
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
-
- def _call(self, number):
- self.calling.emit()
- try:
- with self._busy("Calling"):
- with qui_utils.notify_busy(self._errorLog, "Calling"):
- yield (
- self._backend[0].call,
- (number, ),
- {},
- )
- self.called.emit()
- self._clear()
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
-
- def _cancel(self):
- self.cancelling.emit()
- try:
- with qui_utils.notify_busy(self._errorLog, "Cancelling"):
- yield (
- self._backend[0].cancel,
- (),
- {},
- )
- self.cancelled.emit()
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
-
-
-class Session(QtCore.QObject):
-
- # @todo Somehow add support for csv contacts
- # @BUG When loading without caches, downloads messages twice
-
- stateChange = qt_compat.Signal(str)
- loggedOut = qt_compat.Signal()
- loggedIn = qt_compat.Signal()
- callbackNumberChanged = qt_compat.Signal(str)
-
- accountUpdated = qt_compat.Signal()
- messagesUpdated = qt_compat.Signal()
- newMessages = qt_compat.Signal()
- historyUpdated = qt_compat.Signal()
- dndStateChange = qt_compat.Signal(bool)
- voicemailAvailable = qt_compat.Signal(str, str)
-
- error = qt_compat.Signal(str)
-
- LOGGEDOUT_STATE = "logged out"
- LOGGINGIN_STATE = "logging in"
- LOGGEDIN_STATE = "logged in"
-
- MESSAGE_TEXTS = "Text"
- MESSAGE_VOICEMAILS = "Voicemail"
- MESSAGE_ALL = "All"
-
- HISTORY_RECEIVED = "Received"
- HISTORY_MISSED = "Missed"
- HISTORY_PLACED = "Placed"
- HISTORY_ALL = "All"
-
- _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
-
- _LOGGEDOUT_TIME = -1
- _LOGGINGIN_TIME = 0
-
- def __init__(self, errorLog, cachePath):
- QtCore.QObject.__init__(self)
- self._errorLog = errorLog
- self._pool = qore_utils.FutureThread()
- self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
- self._backend = []
- self._loggedInTime = self._LOGGEDOUT_TIME
- self._loginOps = []
- self._cachePath = cachePath
- self._voicemailCachePath = None
- self._username = None
- self._password = None
- self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
- self._delayedRelogin = QtCore.QTimer()
- self._delayedRelogin.setInterval(0)
- self._delayedRelogin.setSingleShot(True)
- self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
-
- self._contacts = {}
- self._accountUpdateTime = datetime.datetime(1971, 1, 1)
- self._messages = []
- self._cleanMessages = []
- self._messageUpdateTime = datetime.datetime(1971, 1, 1)
- self._history = []
- self._historyUpdateTime = datetime.datetime(1971, 1, 1)
- self._dnd = False
- self._callback = ""
-
- @property
- def state(self):
- return {
- self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
- self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
- }.get(self._loggedInTime, self.LOGGEDIN_STATE)
-
- @property
- def draft(self):
- return self._draft
-
- def login(self, username, password):
- assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
- assert username != "", "No username specified"
- if self._cachePath is not None:
- cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
- else:
- cookiePath = None
-
- if self._username != username or not self._backend:
- from backends import gv_backend
- del self._backend[:]
- self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
-
- self._pool.start()
- le = self._asyncQueue.add_async(self._login)
- le.start(username, password)
-
- def logout(self):
- assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
- _moduleLogger.info("Logging out")
- self._pool.stop()
- self._loggedInTime = self._LOGGEDOUT_TIME
- self._backend[0].persist()
- self._save_to_cache()
- self._clear_voicemail_cache()
- self.stateChange.emit(self.LOGGEDOUT_STATE)
- self.loggedOut.emit()
-
- def clear(self):
- assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
- self._backend[0].logout()
- del self._backend[0]
- self._clear_cache()
- self._draft.clear()
-
- def logout_and_clear(self):
- assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
- _moduleLogger.info("Logging out and clearing the account")
- self._pool.stop()
- self._loggedInTime = self._LOGGEDOUT_TIME
- self.clear()
- self.stateChange.emit(self.LOGGEDOUT_STATE)
- self.loggedOut.emit()
-
- def update_account(self, force = True):
- if not force and self._contacts:
- return
- le = self._asyncQueue.add_async(self._update_account), (), {}
- self._perform_op_while_loggedin(le)
-
- def refresh_connection(self):
- le = self._asyncQueue.add_async(self._refresh_authentication)
- le.start()
-
- def get_contacts(self):
- return self._contacts
-
- def get_when_contacts_updated(self):
- return self._accountUpdateTime
-
- def update_messages(self, messageType, force = True):
- if not force and self._messages:
- return
- le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
- self._perform_op_while_loggedin(le)
-
- def get_messages(self):
- return self._messages
-
- def get_when_messages_updated(self):
- return self._messageUpdateTime
-
- def update_history(self, historyType, force = True):
- if not force and self._history:
- return
- le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
- self._perform_op_while_loggedin(le)
-
- def get_history(self):
- return self._history
-
- def get_when_history_updated(self):
- return self._historyUpdateTime
-
- def update_dnd(self):
- le = self._asyncQueue.add_async(self._update_dnd), (), {}
- self._perform_op_while_loggedin(le)
-
- def set_dnd(self, dnd):
- le = self._asyncQueue.add_async(self._set_dnd)
- le.start(dnd)
-
- def is_available(self, messageId):
- actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
- return os.path.exists(actualPath)
-
- def voicemail_path(self, messageId):
- actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
- if not os.path.exists(actualPath):
- raise RuntimeError("Voicemail not available")
- return actualPath
-
- def download_voicemail(self, messageId):
- le = self._asyncQueue.add_async(self._download_voicemail)
- le.start(messageId)
-
- def _set_dnd(self, dnd):
- oldDnd = self._dnd
- try:
- assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
- with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
- yield (
- self._backend[0].set_dnd,
- (dnd, ),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- self._dnd = dnd
- if oldDnd != self._dnd:
- self.dndStateChange.emit(self._dnd)
-
- def get_dnd(self):
- return self._dnd
-
- def get_account_number(self):
- if self.state != self.LOGGEDIN_STATE:
- return ""
- return self._backend[0].get_account_number()
-
- def get_callback_numbers(self):
- if self.state != self.LOGGEDIN_STATE:
- return {}
- return self._backend[0].get_callback_numbers()
-
- def get_callback_number(self):
- return self._callback
-
- def set_callback_number(self, callback):
- le = self._asyncQueue.add_async(self._set_callback_number)
- le.start(callback)
-
- def _set_callback_number(self, callback):
- oldCallback = self._callback
- try:
- assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
- yield (
- self._backend[0].set_callback_number,
- (callback, ),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- self._callback = callback
- if oldCallback != self._callback:
- self.callbackNumberChanged.emit(self._callback)
-
- def _login(self, username, password):
- with qui_utils.notify_busy(self._errorLog, "Logging In"):
- self._loggedInTime = self._LOGGINGIN_TIME
- self.stateChange.emit(self.LOGGINGIN_STATE)
- finalState = self.LOGGEDOUT_STATE
- accountData = None
- try:
- if accountData is None and self._backend[0].is_quick_login_possible():
- accountData = yield (
- self._backend[0].refresh_account_info,
- (),
- {},
- )
- if accountData is not None:
- _moduleLogger.info("Logged in through cookies")
- else:
- # Force a clearing of the cookies
- yield (
- self._backend[0].logout,
- (),
- {},
- )
-
- if accountData is None:
- accountData = yield (
- self._backend[0].login,
- (username, password),
- {},
- )
- if accountData is not None:
- _moduleLogger.info("Logged in through credentials")
-
- if accountData is not None:
- self._loggedInTime = int(time.time())
- oldUsername = self._username
- self._username = username
- self._password = password
- finalState = self.LOGGEDIN_STATE
- if oldUsername != self._username:
- needOps = not self._load()
- else:
- needOps = True
-
- self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
- try:
- os.makedirs(self._voicemailCachePath)
- except OSError, e:
- if e.errno != 17:
- raise
-
- self.loggedIn.emit()
- self.stateChange.emit(finalState)
- finalState = None # Mark it as already set
- self._process_account_data(accountData)
-
- if needOps:
- loginOps = self._loginOps[:]
- else:
- loginOps = []
- del self._loginOps[:]
- for asyncOp, args, kwds in loginOps:
- asyncOp.start(*args, **kwds)
- else:
- self._loggedInTime = self._LOGGEDOUT_TIME
- self.error.emit("Error logging in")
- except Exception, e:
- _moduleLogger.exception("Booh")
- self._loggedInTime = self._LOGGEDOUT_TIME
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- finally:
- if finalState is not None:
- self.stateChange.emit(finalState)
- if accountData is not None and self._callback:
- self.set_callback_number(self._callback)
-
- def _update_account(self):
- try:
- with qui_utils.notify_busy(self._errorLog, "Updating Account"):
- accountData = yield (
- self._backend[0].refresh_account_info,
- (),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- self._loggedInTime = int(time.time())
- self._process_account_data(accountData)
-
- def _refresh_authentication(self):
- try:
- with qui_utils.notify_busy(self._errorLog, "Updating Account"):
- accountData = yield (
- self._backend[0].refresh_account_info,
- (),
- {},
- )
- accountData = None
- except Exception, e:
- _moduleLogger.exception("Passing to user")
- self.error.emit(str(e))
- # refresh_account_info does not normally throw, so it is fine if we
- # just quit early because something seriously wrong is going on
- return
-
- if accountData is not None:
- self._loggedInTime = int(time.time())
- self._process_account_data(accountData)
- else:
- self._delayedRelogin.start()
-
- def _load(self):
- updateMessages = len(self._messages) != 0
- updateHistory = len(self._history) != 0
- oldDnd = self._dnd
- oldCallback = self._callback
-
- self._messages = []
- self._cleanMessages = []
- self._history = []
- self._dnd = False
- self._callback = ""
-
- loadedFromCache = self._load_from_cache()
- if loadedFromCache:
- updateMessages = True
- updateHistory = True
-
- if updateMessages:
- self.messagesUpdated.emit()
- if updateHistory:
- self.historyUpdated.emit()
- if oldDnd != self._dnd:
- self.dndStateChange.emit(self._dnd)
- if oldCallback != self._callback:
- self.callbackNumberChanged.emit(self._callback)
-
- return loadedFromCache
-
- def _load_from_cache(self):
- if self._cachePath is None:
- return False
- cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
-
- try:
- with open(cachePath, "rb") as f:
- dumpedData = pickle.load(f)
- except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
- _moduleLogger.exception("Pickle fun loading")
- return False
- except:
- _moduleLogger.exception("Weirdness loading")
- return False
-
- try:
- version, build = dumpedData[0:2]
- except ValueError:
- _moduleLogger.exception("Upgrade/downgrade fun")
- return False
- except:
- _moduleLogger.exception("Weirdlings")
- return False
-
- if misc_utils.compare_versions(
- self._OLDEST_COMPATIBLE_FORMAT_VERSION,
- misc_utils.parse_version(version),
- ) <= 0:
- try:
- (
- version, build,
- messages, messageUpdateTime,
- history, historyUpdateTime,
- dnd, callback
- ) = dumpedData
- except ValueError:
- _moduleLogger.exception("Upgrade/downgrade fun")
- return False
- except:
- _moduleLogger.exception("Weirdlings")
- return False
-
- _moduleLogger.info("Loaded cache")
- self._messages = messages
- self._alert_on_messages(self._messages)
- self._messageUpdateTime = messageUpdateTime
- self._history = history
- self._historyUpdateTime = historyUpdateTime
- self._dnd = dnd
- self._callback = callback
- return True
- else:
- _moduleLogger.debug(
- "Skipping cache due to version mismatch (%s-%s)" % (
- version, build
- )
- )
- return False
-
- def _save_to_cache(self):
- _moduleLogger.info("Saving cache")
- if self._cachePath is None:
- return
- cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
-
- try:
- dataToDump = (
- constants.__version__, constants.__build__,
- self._messages, self._messageUpdateTime,
- self._history, self._historyUpdateTime,
- self._dnd, self._callback
- )
- with open(cachePath, "wb") as f:
- pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
- _moduleLogger.info("Cache saved")
- except (pickle.PickleError, IOError):
- _moduleLogger.exception("While saving")
-
- def _clear_cache(self):
- updateMessages = len(self._messages) != 0
- updateHistory = len(self._history) != 0
- oldDnd = self._dnd
- oldCallback = self._callback
-
- self._messages = []
- self._messageUpdateTime = datetime.datetime(1971, 1, 1)
- self._history = []
- self._historyUpdateTime = datetime.datetime(1971, 1, 1)
- self._dnd = False
- self._callback = ""
-
- if updateMessages:
- self.messagesUpdated.emit()
- if updateHistory:
- self.historyUpdated.emit()
- if oldDnd != self._dnd:
- self.dndStateChange.emit(self._dnd)
- if oldCallback != self._callback:
- self.callbackNumberChanged.emit(self._callback)
-
- self._save_to_cache()
- self._clear_voicemail_cache()
-
- def _clear_voicemail_cache(self):
- import shutil
- shutil.rmtree(self._voicemailCachePath, True)
-
- def _update_messages(self, messageType):
- try:
- assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
- with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
- self._messages = yield (
- self._backend[0].get_messages,
- (messageType, ),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- self._messageUpdateTime = datetime.datetime.now()
- self.messagesUpdated.emit()
- self._alert_on_messages(self._messages)
-
- def _update_history(self, historyType):
- try:
- assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
- with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType):
- self._history = yield (
- self._backend[0].get_call_history,
- (historyType, ),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- self._historyUpdateTime = datetime.datetime.now()
- self.historyUpdated.emit()
-
- def _update_dnd(self):
- with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
- oldDnd = self._dnd
- try:
- assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
- self._dnd = yield (
- self._backend[0].is_dnd,
- (),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Reporting error to user")
- self.error.emit(str(e))
- return
- if oldDnd != self._dnd:
- self.dndStateChange(self._dnd)
-
- def _download_voicemail(self, messageId):
- actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
- targetPath = "%s.%s.part" % (actualPath, time.time())
- if os.path.exists(actualPath):
- self.voicemailAvailable.emit(messageId, actualPath)
- return
- with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
- try:
- yield (
- self._backend[0].download,
- (messageId, targetPath),
- {},
- )
- except Exception, e:
- _moduleLogger.exception("Passing to user")
- self.error.emit(str(e))
- return
-
- if os.path.exists(actualPath):
- try:
- os.remove(targetPath)
- except:
- _moduleLogger.exception("Ignoring file problems with cache")
- self.voicemailAvailable.emit(messageId, actualPath)
- return
- else:
- os.rename(targetPath, actualPath)
- self.voicemailAvailable.emit(messageId, actualPath)
-
- def _perform_op_while_loggedin(self, op):
- if self.state == self.LOGGEDIN_STATE:
- op, args, kwds = op
- op.start(*args, **kwds)
- else:
- self._push_login_op(op)
-
- def _push_login_op(self, asyncOp):
- assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
- if asyncOp in self._loginOps:
- _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
- return
- self._loginOps.append(asyncOp)
-
- def _process_account_data(self, accountData):
- self._contacts = dict(
- (contactId, contactDetails)
- for contactId, contactDetails in accountData["contacts"].iteritems()
- # A zero contact id is the catch all for unknown contacts
- if contactId != "0"
- )
-
- self._accountUpdateTime = datetime.datetime.now()
- self.accountUpdated.emit()
-
- def _alert_on_messages(self, messages):
- cleanNewMessages = list(self._clean_messages(messages))
- cleanNewMessages.sort(key=lambda m: m["contactId"])
- if self._cleanMessages:
- if self._cleanMessages != cleanNewMessages:
- self.newMessages.emit()
- self._cleanMessages = cleanNewMessages
-
- def _clean_messages(self, messages):
- for message in messages:
- cleaned = dict(
- kv
- for kv in message.iteritems()
- if kv[0] not in
- [
- "relTime",
- "time",
- "isArchived",
- "isRead",
- "isSpam",
- "isTrash",
- ]
- )
-
- # Don't let outbound messages cause alerts, especially if the package has only outbound
- cleaned["messageParts"] = [
- tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
- ]
- if not cleaned["messageParts"]:
- continue
-
- yield cleaned
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_delayed_relogin(self):
- try:
- username = self._username
- password = self._password
- self.logout()
- self.login(username, password)
- except Exception, e:
- _moduleLogger.exception("Passing to user")
- self.error.emit(str(e))
- return
+++ /dev/null
-import logging
-
-import gobject
-import gst
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(gobject.GObject):
-
- # @bug Advertising state changes a bit early, should watch for GStreamer state change
-
- STATE_PLAY = "play"
- STATE_PAUSE = "pause"
- STATE_STOP = "stop"
-
- __gsignals__ = {
- 'state-change' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_STRING, ),
- ),
- 'eof' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_STRING, ),
- ),
- 'error' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
- ),
- }
-
- def __init__(self):
- gobject.GObject.__init__(self)
- #Fields
- self._uri = ""
- self._elapsed = 0
- self._duration = 0
-
- #Set up GStreamer
- self._player = gst.element_factory_make("playbin2", "player")
- bus = self._player.get_bus()
- bus.add_signal_watch()
- bus.connect("message", self._on_message)
-
- #Constants
- self._timeFormat = gst.Format(gst.FORMAT_TIME)
- self._seekFlag = gst.SEEK_FLAG_FLUSH
-
- @property
- def playing(self):
- return self.state == self.STATE_PLAY
-
- @property
- def has_file(self):
- return 0 < len(self._uri)
-
- @property
- def state(self):
- state = self._player.get_state()[1]
- return self._translate_state(state)
-
- def set_file(self, uri):
- if self._uri != uri:
- self._invalidate_cache()
- if self.state != self.STATE_STOP:
- self.stop()
-
- self._uri = uri
- self._player.set_property("uri", uri)
-
- def play(self):
- if self.state == self.STATE_PLAY:
- _moduleLogger.info("Already play")
- return
- _moduleLogger.info("Play")
- self._player.set_state(gst.STATE_PLAYING)
- self.emit("state-change", self.STATE_PLAY)
-
- def pause(self):
- if self.state == self.STATE_PAUSE:
- _moduleLogger.info("Already pause")
- return
- _moduleLogger.info("Pause")
- self._player.set_state(gst.STATE_PAUSED)
- self.emit("state-change", self.STATE_PAUSE)
-
- def stop(self):
- if self.state == self.STATE_STOP:
- _moduleLogger.info("Already stop")
- return
- self._player.set_state(gst.STATE_NULL)
- _moduleLogger.info("Stopped")
- self.emit("state-change", self.STATE_STOP)
-
- @property
- def elapsed(self):
- try:
- self._elapsed = self._player.query_position(self._timeFormat, None)[0]
- except:
- pass
- return self._elapsed
-
- @property
- def duration(self):
- try:
- self._duration = self._player.query_duration(self._timeFormat, None)[0]
- except:
- _moduleLogger.exception("Query failed")
- return self._duration
-
- def seek_time(self, ns):
- self._elapsed = ns
- self._player.seek_simple(self._timeFormat, self._seekFlag, ns)
-
- def _invalidate_cache(self):
- self._elapsed = 0
- self._duration = 0
-
- def _translate_state(self, gstState):
- return {
- gst.STATE_NULL: self.STATE_STOP,
- gst.STATE_PAUSED: self.STATE_PAUSE,
- gst.STATE_PLAYING: self.STATE_PLAY,
- }.get(gstState, self.STATE_STOP)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_message(self, bus, message):
- t = message.type
- if t == gst.MESSAGE_EOS:
- self._player.set_state(gst.STATE_NULL)
- self.emit("eof", self._uri)
- elif t == gst.MESSAGE_ERROR:
- self._player.set_state(gst.STATE_NULL)
- err, debug = message.parse_error()
- _moduleLogger.error("Error: %s, (%s)" % (err, debug))
- self.emit("error", err, debug)
-
-
-gobject.type_register(Stream)
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-
-import util.misc as misc_utils
-try:
- import stream_gst
- stream = stream_gst
-except ImportError:
- try:
- import stream_osso
- stream = stream_osso
- except ImportError:
- import stream_null
- stream = stream_null
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class StreamToken(QtCore.QObject):
-
- stateChange = qt_compat.Signal(str)
- invalidated = qt_compat.Signal()
- error = qt_compat.Signal(str)
-
- STATE_PLAY = stream.Stream.STATE_PLAY
- STATE_PAUSE = stream.Stream.STATE_PAUSE
- STATE_STOP = stream.Stream.STATE_STOP
-
- def __init__(self, stream):
- QtCore.QObject.__init__(self)
- self._stream = stream
- self._stream.connect("state-change", self._on_stream_state)
- self._stream.connect("eof", self._on_stream_eof)
- self._stream.connect("error", self._on_stream_error)
-
- @property
- def state(self):
- if self.isValid:
- return self._stream.state
- else:
- return self.STATE_STOP
-
- @property
- def isValid(self):
- return self._stream is not None
-
- def play(self):
- self._stream.play()
-
- def pause(self):
- self._stream.pause()
-
- def stop(self):
- self._stream.stop()
-
- def invalidate(self):
- if self._stream is None:
- return
- _moduleLogger.info("Playback token invalidated")
- self._stream = None
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_stream_state(self, s, state):
- if not self.isValid:
- return
- if state == self.STATE_STOP:
- self.invalidate()
- self.stateChange.emit(state)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_stream_eof(self, s, uri):
- if not self.isValid:
- return
- self.invalidate()
- self.stateChange.emit(self.STATE_STOP)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_stream_error(self, s, error, debug):
- if not self.isValid:
- return
- _moduleLogger.info("Error %s %s" % (error, debug))
- self.error.emit(str(error))
-
-
-class StreamHandler(QtCore.QObject):
-
- def __init__(self):
- QtCore.QObject.__init__(self)
- self._stream = stream.Stream()
- self._token = StreamToken(self._stream)
-
- def set_file(self, path):
- self._token.invalidate()
- self._token = StreamToken(self._stream)
- self._stream.set_file(path)
- return self._token
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_stream_state(self, s, state):
- _moduleLogger.info("State change %r" % state)
-
-
-if __name__ == "__main__":
- pass
-
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(object):
-
- STATE_PLAY = "play"
- STATE_PAUSE = "pause"
- STATE_STOP = "stop"
-
- def __init__(self):
- pass
-
- def connect(self, signalName, slot):
- pass
-
- @property
- def playing(self):
- return False
-
- @property
- def has_file(self):
- return False
-
- @property
- def state(self):
- return self.STATE_STOP
-
- def set_file(self, uri):
- pass
-
- def play(self):
- pass
-
- def pause(self):
- pass
-
- def stop(self):
- pass
-
- @property
- def elapsed(self):
- return 0
-
- @property
- def duration(self):
- return 0
-
- def seek_time(self, ns):
- pass
-
-
-if __name__ == "__main__":
- pass
-
+++ /dev/null
-import logging
-
-import gobject
-import dbus
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(gobject.GObject):
-
- STATE_PLAY = "play"
- STATE_PAUSE = "pause"
- STATE_STOP = "stop"
-
- __gsignals__ = {
- 'state-change' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_STRING, ),
- ),
- 'eof' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_STRING, ),
- ),
- 'error' : (
- gobject.SIGNAL_RUN_LAST,
- gobject.TYPE_NONE,
- (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
- ),
- }
-
- _SERVICE_NAME = "com.nokia.osso_media_server"
- _OBJECT_PATH = "/com/nokia/osso_media_server"
- _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
-
- def __init__(self):
- gobject.GObject.__init__(self)
- #Fields
- self._state = self.STATE_STOP
- self._nextState = self.STATE_STOP
- self._uri = ""
- self._elapsed = 0
- self._duration = 0
-
- session_bus = dbus.SessionBus()
-
- # Get the osso-media-player proxy object
- oms_object = session_bus.get_object(
- self._SERVICE_NAME,
- self._OBJECT_PATH,
- introspect=False,
- follow_name_owner_changes=True,
- )
- # Use the audio interface
- oms_audio_interface = dbus.Interface(
- oms_object,
- self._AUDIO_INTERFACE_NAME,
- )
- self._audioProxy = oms_audio_interface
-
- self._audioProxy.connect_to_signal("state_changed", self._on_state_changed)
- self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream)
-
- error_signals = [
- "no_media_selected",
- "file_not_found",
- "type_not_found",
- "unsupported_type",
- "gstreamer",
- "dsp",
- "device_unavailable",
- "corrupted_file",
- "out_of_memory",
- "audio_codec_not_supported",
- ]
- for error in error_signals:
- self._audioProxy.connect_to_signal(error, self._on_error)
-
- @property
- def playing(self):
- return self.state == self.STATE_PLAY
-
- @property
- def has_file(self):
- return 0 < len(self._uri)
-
- @property
- def state(self):
- return self._state
-
- def set_file(self, uri):
- if self._uri != uri:
- self._invalidate_cache()
- if self.state != self.STATE_STOP:
- self.stop()
-
- self._uri = uri
- self._audioProxy.set_media_location(self._uri)
-
- def play(self):
- if self._nextState == self.STATE_PLAY:
- _moduleLogger.info("Already play")
- return
- _moduleLogger.info("Play")
- self._audioProxy.play()
- self._nextState = self.STATE_PLAY
- #self.emit("state-change", self.STATE_PLAY)
-
- def pause(self):
- if self._nextState == self.STATE_PAUSE:
- _moduleLogger.info("Already pause")
- return
- _moduleLogger.info("Pause")
- self._audioProxy.pause()
- self._nextState = self.STATE_PAUSE
- #self.emit("state-change", self.STATE_PLAY)
-
- def stop(self):
- if self._nextState == self.STATE_STOP:
- _moduleLogger.info("Already stop")
- return
- self._audioProxy.stop()
- _moduleLogger.info("Stopped")
- self._nextState = self.STATE_STOP
- #self.emit("state-change", self.STATE_STOP)
-
- @property
- def elapsed(self):
- pos_info = self._audioProxy.get_position()
- if isinstance(pos_info, tuple):
- self._elapsed, self._duration = pos_info
- return self._elapsed
-
- @property
- def duration(self):
- pos_info = self._audioProxy.get_position()
- if isinstance(pos_info, tuple):
- self._elapsed, self._duration = pos_info
- return self._duration
-
- def seek_time(self, ns):
- _moduleLogger.debug("Seeking to: %s", ns)
- self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) )
-
- def _invalidate_cache(self):
- self._elapsed = 0
- self._duration = 0
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_error(self, *args):
- err, debug = "", repr(args)
- _moduleLogger.error("Error: %s, (%s)" % (err, debug))
- self.emit("error", err, debug)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_end_of_stream(self, *args):
- self._state = self.STATE_STOP
- self._nextState = self.STATE_STOP
- self.emit("eof", self._uri)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_state_changed(self, state):
- _moduleLogger.info("State: %s", state)
- state = {
- "playing": self.STATE_PLAY,
- "paused": self.STATE_PAUSE,
- "stopped": self.STATE_STOP,
- }[state]
- if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP:
- # They seem to want to advertise stop right as the stream is starting, breaking the owner of this
- return
- self._state = state
- self._nextState = state
- self.emit("state-change", state)
-
-
-gobject.type_register(Stream)
+++ /dev/null
-#!/usr/bin/env python
+++ /dev/null
-#!/usr/bin/env python
-
-"""
-@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448
-"""
-
-import itertools
-import functools
-import datetime
-import types
-import array
-import random
-
-
-def ordered_itr(collection):
- """
- >>> [v for v in ordered_itr({"a": 1, "b": 2})]
- [('a', 1), ('b', 2)]
- >>> [v for v in ordered_itr([3, 1, 10, -20])]
- [-20, 1, 3, 10]
- """
- if isinstance(collection, types.DictType):
- keys = list(collection.iterkeys())
- keys.sort()
- for key in keys:
- yield key, collection[key]
- else:
- values = list(collection)
- values.sort()
- for value in values:
- yield value
-
-
-def itercat(*iterators):
- """
- Concatenate several iterators into one.
-
- >>> [v for v in itercat([1, 2, 3], [4, 1, 3])]
- [1, 2, 3, 4, 1, 3]
- """
- for i in iterators:
- for x in i:
- yield x
-
-
-def product(*args, **kwds):
- # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
- # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
- pools = map(tuple, args) * kwds.get('repeat', 1)
- result = [[]]
- for pool in pools:
- result = [x+[y] for x in result for y in pool]
- for prod in result:
- yield tuple(prod)
-
-
-def iterwhile(func, iterator):
- """
- Iterate for as long as func(value) returns true.
- >>> through = lambda b: b
- >>> [v for v in iterwhile(through, [True, True, False])]
- [True, True]
- """
- iterator = iter(iterator)
- while 1:
- next = iterator.next()
- if not func(next):
- raise StopIteration
- yield next
-
-
-def iterfirst(iterator, count=1):
- """
- Iterate through 'count' first values.
-
- >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)]
- [1, 2, 3]
- """
- iterator = iter(iterator)
- for i in xrange(count):
- yield iterator.next()
-
-
-def iterstep(iterator, n):
- """
- Iterate every nth value.
-
- >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)]
- [1, 2, 3, 4, 5]
- >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)]
- [1, 3, 5]
- >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)]
- [1, 4]
- """
- iterator = iter(iterator)
- while True:
- yield iterator.next()
- # skip n-1 values
- for dummy in xrange(n-1):
- iterator.next()
-
-
-def itergroup(iterator, count, padValue = None):
- """
- Iterate in groups of 'count' values. If there
- aren't enough values, the last result is padded with
- None.
-
- >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
- ... print tuple(val)
- (1, 2, 3)
- (4, 5, 6)
- >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
- ... print list(val)
- [1, 2, 3]
- [4, 5, 6]
- >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
- ... print tuple(val)
- (1, 2, 3)
- (4, 5, 6)
- (7, None, None)
- >>> for val in itergroup("123456", 3):
- ... print tuple(val)
- ('1', '2', '3')
- ('4', '5', '6')
- >>> for val in itergroup("123456", 3):
- ... print repr("".join(val))
- '123'
- '456'
- """
- paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
- nIterators = (paddedIterator, ) * count
- return itertools.izip(*nIterators)
-
-
-def xzip(*iterators):
- """Iterative version of builtin 'zip'."""
- iterators = itertools.imap(iter, iterators)
- while 1:
- yield tuple([x.next() for x in iterators])
-
-
-def xmap(func, *iterators):
- """Iterative version of builtin 'map'."""
- iterators = itertools.imap(iter, iterators)
- values_left = [1]
-
- def values():
- # Emulate map behaviour, i.e. shorter
- # sequences are padded with None when
- # they run out of values.
- values_left[0] = 0
- for i in range(len(iterators)):
- iterator = iterators[i]
- if iterator is None:
- yield None
- else:
- try:
- yield iterator.next()
- values_left[0] = 1
- except StopIteration:
- iterators[i] = None
- yield None
- while 1:
- args = tuple(values())
- if not values_left[0]:
- raise StopIteration
- yield func(*args)
-
-
-def xfilter(func, iterator):
- """Iterative version of builtin 'filter'."""
- iterator = iter(iterator)
- while 1:
- next = iterator.next()
- if func(next):
- yield next
-
-
-def xreduce(func, iterator, default=None):
- """Iterative version of builtin 'reduce'."""
- iterator = iter(iterator)
- try:
- prev = iterator.next()
- except StopIteration:
- return default
- single = 1
- for next in iterator:
- single = 0
- prev = func(prev, next)
- if single:
- return func(prev, default)
- return prev
-
-
-def daterange(begin, end, delta = datetime.timedelta(1)):
- """
- Form a range of dates and iterate over them.
-
- Arguments:
- begin -- a date (or datetime) object; the beginning of the range.
- end -- a date (or datetime) object; the end of the range.
- delta -- (optional) a datetime.timedelta object; how much to step each iteration.
- Default step is 1 day.
-
- Usage:
- """
- if not isinstance(delta, datetime.timedelta):
- delta = datetime.timedelta(delta)
-
- ZERO = datetime.timedelta(0)
-
- if begin < end:
- if delta <= ZERO:
- raise StopIteration
- test = end.__gt__
- else:
- if delta >= ZERO:
- raise StopIteration
- test = end.__lt__
-
- while test(begin):
- yield begin
- begin += delta
-
-
-class LazyList(object):
- """
- A Sequence whose values are computed lazily by an iterator.
-
- Module for the creation and use of iterator-based lazy lists.
- this module defines a class LazyList which can be used to represent sequences
- of values generated lazily. One can also create recursively defined lazy lists
- that generate their values based on ones previously generated.
-
- Backport to python 2.5 by Michael Pust
- """
-
- __author__ = 'Dan Spitz'
-
- def __init__(self, iterable):
- self._exhausted = False
- self._iterator = iter(iterable)
- self._data = []
-
- def __len__(self):
- """Get the length of a LazyList's computed data."""
- return len(self._data)
-
- def __getitem__(self, i):
- """Get an item from a LazyList.
- i should be a positive integer or a slice object."""
- if isinstance(i, int):
- #index has not yet been yielded by iterator (or iterator exhausted
- #before reaching that index)
- if i >= len(self):
- self.exhaust(i)
- elif i < 0:
- raise ValueError('cannot index LazyList with negative number')
- return self._data[i]
-
- #LazyList slices are iterators over a portion of the list.
- elif isinstance(i, slice):
- start, stop, step = i.start, i.stop, i.step
- if any(x is not None and x < 0 for x in (start, stop, step)):
- raise ValueError('cannot index or step through a LazyList with'
- 'a negative number')
- #set start and step to their integer defaults if they are None.
- if start is None:
- start = 0
- if step is None:
- step = 1
-
- def LazyListIterator():
- count = start
- predicate = (
- (lambda: True)
- if stop is None
- else (lambda: count < stop)
- )
- while predicate():
- try:
- yield self[count]
- #slices can go out of actual index range without raising an
- #error
- except IndexError:
- break
- count += step
- return LazyListIterator()
-
- raise TypeError('i must be an integer or slice')
-
- def __iter__(self):
- """return an iterator over each value in the sequence,
- whether it has been computed yet or not."""
- return self[:]
-
- def computed(self):
- """Return an iterator over the values in a LazyList that have
- already been computed."""
- return self[:len(self)]
-
- def exhaust(self, index = None):
- """Exhaust the iterator generating this LazyList's values.
- if index is None, this will exhaust the iterator completely.
- Otherwise, it will iterate over the iterator until either the list
- has a value for index or the iterator is exhausted.
- """
- if self._exhausted:
- return
- if index is None:
- ind_range = itertools.count(len(self))
- else:
- ind_range = range(len(self), index + 1)
-
- for ind in ind_range:
- try:
- self._data.append(self._iterator.next())
- except StopIteration: #iterator is fully exhausted
- self._exhausted = True
- break
-
-
-class RecursiveLazyList(LazyList):
-
- def __init__(self, prod, *args, **kwds):
- super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds))
-
-
-class RecursiveLazyListFactory:
-
- def __init__(self, producer):
- self._gen = producer
-
- def __call__(self, *a, **kw):
- return RecursiveLazyList(self._gen, *a, **kw)
-
-
-def lazylist(gen):
- """
- Decorator for creating a RecursiveLazyList subclass.
- This should decorate a generator function taking the LazyList object as its
- first argument which yields the contents of the list in order.
-
- >>> #fibonnacci sequence in a lazy list.
- >>> @lazylist
- ... def fibgen(lst):
- ... yield 0
- ... yield 1
- ... for a, b in itertools.izip(lst, lst[1:]):
- ... yield a + b
- ...
- >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence
- >>> fibs = fibgen()
- >>>
- >>> #prime numbers in a lazy list.
- >>> @lazylist
- ... def primegen(lst):
- ... yield 2
- ... for candidate in itertools.count(3): #start at next number after 2
- ... #if candidate is not divisible by any smaller prime numbers,
- ... #it is a prime.
- ... if all(candidate % p for p in lst.computed()):
- ... yield candidate
- ...
- >>> #same for primes- treat it like an infinitely long list containing all prime numbers.
- >>> primes = primegen()
- >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2]
- 0 1 1 2 3 5
- >>> print list(fibs[:10]), list(primes[:10])
- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
- """
- return RecursiveLazyListFactory(gen)
-
-
-def map_func(f):
- """
- >>> import misc
- >>> misc.validate_decorator(map_func)
- """
-
- @functools.wraps(f)
- def wrapper(*args):
- result = itertools.imap(f, args)
- return result
- return wrapper
-
-
-def reduce_func(function):
- """
- >>> import misc
- >>> misc.validate_decorator(reduce_func(lambda x: x))
- """
-
- def decorator(f):
-
- @functools.wraps(f)
- def wrapper(*args):
- result = reduce(function, f(args))
- return result
- return wrapper
- return decorator
-
-
-def any_(iterable):
- """
- @note Python Version <2.5
-
- >>> any_([True, True])
- True
- >>> any_([True, False])
- True
- >>> any_([False, False])
- False
- """
-
- for element in iterable:
- if element:
- return True
- return False
-
-
-def all_(iterable):
- """
- @note Python Version <2.5
-
- >>> all_([True, True])
- True
- >>> all_([True, False])
- False
- >>> all_([False, False])
- False
- """
-
- for element in iterable:
- if not element:
- return False
- return True
-
-
-def for_every(pred, seq):
- """
- for_every takes a one argument predicate function and a sequence.
- @param pred The predicate function should return true or false.
- @returns true if every element in seq returns true for predicate, else returns false.
-
- >>> for_every (lambda c: c > 5,(6,7,8,9))
- True
-
- @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
- """
-
- for i in seq:
- if not pred(i):
- return False
- return True
-
-
-def there_exists(pred, seq):
- """
- there_exists takes a one argument predicate function and a sequence.
- @param pred The predicate function should return true or false.
- @returns true if any element in seq returns true for predicate, else returns false.
-
- >>> there_exists (lambda c: c > 5,(6,7,8,9))
- True
-
- @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
- """
-
- for i in seq:
- if pred(i):
- return True
- return False
-
-
-def func_repeat(quantity, func, *args, **kwd):
- """
- Meant to be in connection with "reduce"
- """
- for i in xrange(quantity):
- yield func(*args, **kwd)
-
-
-def function_map(preds, item):
- """
- Meant to be in connection with "reduce"
- """
- results = (pred(item) for pred in preds)
-
- return results
-
-
-def functional_if(combiner, preds, item):
- """
- Combines the result of a list of predicates applied to item according to combiner
-
- @see any, every for example combiners
- """
- pass_bool = lambda b: b
-
- bool_results = function_map(preds, item)
- return combiner(pass_bool, bool_results)
-
-
-def pushback_itr(itr):
- """
- >>> list(pushback_itr(xrange(5)))
- [0, 1, 2, 3, 4]
- >>>
- >>> first = True
- >>> itr = pushback_itr(xrange(5))
- >>> for i in itr:
- ... print i
- ... if first and i == 2:
- ... first = False
- ... print itr.send(i)
- 0
- 1
- 2
- None
- 2
- 3
- 4
- >>>
- >>> first = True
- >>> itr = pushback_itr(xrange(5))
- >>> for i in itr:
- ... print i
- ... if first and i == 2:
- ... first = False
- ... print itr.send(i)
- ... print itr.send(i)
- 0
- 1
- 2
- None
- None
- 2
- 2
- 3
- 4
- >>>
- >>> itr = pushback_itr(xrange(5))
- >>> print itr.next()
- 0
- >>> print itr.next()
- 1
- >>> print itr.send(10)
- None
- >>> print itr.next()
- 10
- >>> print itr.next()
- 2
- >>> print itr.send(20)
- None
- >>> print itr.send(30)
- None
- >>> print itr.send(40)
- None
- >>> print itr.next()
- 40
- >>> print itr.next()
- 30
- >>> print itr.send(50)
- None
- >>> print itr.next()
- 50
- >>> print itr.next()
- 20
- >>> print itr.next()
- 3
- >>> print itr.next()
- 4
- """
- for item in itr:
- maybePushedBack = yield item
- queue = []
- while queue or maybePushedBack is not None:
- if maybePushedBack is not None:
- queue.append(maybePushedBack)
- maybePushedBack = yield None
- else:
- item = queue.pop()
- maybePushedBack = yield item
-
-
-def itr_available(queue, initiallyBlock = False):
- if initiallyBlock:
- yield queue.get()
- while not queue.empty():
- yield queue.get_nowait()
-
-
-class BloomFilter(object):
- """
- http://en.wikipedia.org/wiki/Bloom_filter
- Sources:
- http://code.activestate.com/recipes/577684-bloom-filter/
- http://code.activestate.com/recipes/577686-bloom-filter/
-
- >>> from random import sample
- >>> from string import ascii_letters
- >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut
- ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas
- ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota
- ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey
- ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon
- ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah
- ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split()
- >>> bf = BloomFilter(num_bits=1000, num_probes=14)
- >>> for state in states:
- ... bf.add(state)
- >>> numStatesFound = sum(state in bf for state in states)
- >>> numStatesFound, len(states)
- (50, 50)
- >>> trials = 100
- >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials))
- >>> numGarbageFound, trials
- (0, 100)
- """
-
- def __init__(self, num_bits, num_probes):
- num_words = (num_bits + 31) // 32
- self._arr = array.array('B', [0]) * num_words
- self._num_probes = num_probes
-
- def add(self, key):
- for i, mask in self._get_probes(key):
- self._arr[i] |= mask
-
- def union(self, bfilter):
- if self._match_template(bfilter):
- for i, b in enumerate(bfilter._arr):
- self._arr[i] |= b
- else:
- # Union b/w two unrelated bloom filter raises this
- raise ValueError("Mismatched bloom filters")
-
- def intersection(self, bfilter):
- if self._match_template(bfilter):
- for i, b in enumerate(bfilter._arr):
- self._arr[i] &= b
- else:
- # Intersection b/w two unrelated bloom filter raises this
- raise ValueError("Mismatched bloom filters")
-
- def __contains__(self, key):
- return all(self._arr[i] & mask for i, mask in self._get_probes(key))
-
- def _match_template(self, bfilter):
- return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes
-
- def _get_probes(self, key):
- hasher = random.Random(key).randrange
- for _ in range(self._num_probes):
- array_index = hasher(len(self._arr))
- bit_index = hasher(32)
- yield array_index, 1 << bit_index
-
-
-if __name__ == "__main__":
- import doctest
- print doctest.testmod()
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import os
-import errno
-import time
-import functools
-import contextlib
-import logging
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class AsyncTaskQueue(object):
-
- def __init__(self, taskPool):
- self._asyncs = []
- self._taskPool = taskPool
-
- def add_async(self, func):
- self.flush()
- a = AsyncGeneratorTask(self._taskPool, func)
- self._asyncs.append(a)
- return a
-
- def flush(self):
- self._asyncs = [a for a in self._asyncs if not a.isDone]
-
-
-class AsyncGeneratorTask(object):
-
- def __init__(self, pool, func):
- self._pool = pool
- self._func = func
- self._run = None
- self._isDone = False
-
- @property
- def isDone(self):
- return self._isDone
-
- def start(self, *args, **kwds):
- assert self._run is None, "Task already started"
- self._run = self._func(*args, **kwds)
- trampoline, args, kwds = self._run.send(None) # priming the function
- self._pool.add_task(
- trampoline,
- args,
- kwds,
- self.on_success,
- self.on_error,
- )
-
- @misc.log_exception(_moduleLogger)
- def on_success(self, result):
- _moduleLogger.debug("Processing success for: %r", self._func)
- try:
- trampoline, args, kwds = self._run.send(result)
- except StopIteration, e:
- self._isDone = True
- else:
- self._pool.add_task(
- trampoline,
- args,
- kwds,
- self.on_success,
- self.on_error,
- )
-
- @misc.log_exception(_moduleLogger)
- def on_error(self, error):
- _moduleLogger.debug("Processing error for: %r", self._func)
- try:
- trampoline, args, kwds = self._run.throw(error)
- except StopIteration, e:
- self._isDone = True
- else:
- self._pool.add_task(
- trampoline,
- args,
- kwds,
- self.on_success,
- self.on_error,
- )
-
- def __repr__(self):
- return "<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):
- """
- Synchronization decorator.
-
- >>> import misc
- >>> misc.validate_decorator(synchronized(object()))
- """
-
- def wrap(f):
-
- @functools.wraps(f)
- def newFunction(*args, **kw):
- lock.acquire()
- try:
- return f(*args, **kw)
- finally:
- lock.release()
- return newFunction
- return wrap
-
-
-@contextlib.contextmanager
-def qlock(queue, gblock = True, gtimeout = None, pblock = True, ptimeout = None):
- """
- Locking with a queue, good for when you want to lock an item passed around
-
- >>> import Queue
- >>> item = 5
- >>> lock = Queue.Queue()
- >>> lock.put(item)
- >>> with qlock(lock) as i:
- ... print i
- 5
- """
- item = queue.get(gblock, gtimeout)
- try:
- yield item
- finally:
- queue.put(item, pblock, ptimeout)
-
-
-@contextlib.contextmanager
-def flock(path, timeout=-1):
- WAIT_FOREVER = -1
- DELAY = 0.1
- timeSpent = 0
-
- acquired = False
-
- while timeSpent <= timeout or timeout == WAIT_FOREVER:
- try:
- fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
- acquired = True
- break
- except OSError, e:
- if e.errno != errno.EEXIST:
- raise
- time.sleep(DELAY)
- timeSpent += DELAY
-
- assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
-
- try:
- yield fd
- finally:
- os.unlink(path)
+++ /dev/null
-#!/usr/bin/env python\r
-\r
-"""\r
-Uses for generators\r
-* Pull pipelining (iterators)\r
-* Push pipelining (coroutines)\r
-* State machines (coroutines)\r
-* "Cooperative multitasking" (coroutines)\r
-* Algorithm -> Object transform for cohesiveness (for example context managers) (coroutines)\r
-\r
-Design considerations\r
-* When should a stage pass on exceptions or have it thrown within it?\r
-* When should a stage pass on GeneratorExits?\r
-* Is there a way to either turn a push generator into a iterator or to use\r
- comprehensions syntax for push generators (I doubt it)\r
-* When should the stage try and send data in both directions\r
-* Since pull generators (generators), push generators (coroutines), subroutines, and coroutines are all coroutines, maybe we should rename the push generators to not confuse them, like signals/slots? and then refer to two-way generators as coroutines\r
-** If so, make s* and co* implementation of functions\r
-"""\r
-\r
-import threading\r
-import Queue\r
-import pickle\r
-import functools\r
-import itertools\r
-import xml.sax\r
-import xml.parsers.expat\r
-\r
-\r
-def autostart(func):\r
- """\r
- >>> @autostart\r
- ... def grep_sink(pattern):\r
- ... print "Looking for %s" % pattern\r
- ... while True:\r
- ... line = yield\r
- ... if pattern in line:\r
- ... print line,\r
- >>> g = grep_sink("python")\r
- Looking for python\r
- >>> g.send("Yeah but no but yeah but no")\r
- >>> g.send("A series of tubes")\r
- >>> g.send("python generators rock!")\r
- python generators rock!\r
- >>> g.close()\r
- """\r
-\r
- @functools.wraps(func)\r
- def start(*args, **kwargs):\r
- cr = func(*args, **kwargs)\r
- cr.next()\r
- return cr\r
-\r
- return start\r
-\r
-\r
-@autostart\r
-def printer_sink(format = "%s"):\r
- """\r
- >>> pr = printer_sink("%r")\r
- >>> pr.send("Hello")\r
- 'Hello'\r
- >>> pr.send("5")\r
- '5'\r
- >>> pr.send(5)\r
- 5\r
- >>> p = printer_sink()\r
- >>> p.send("Hello")\r
- Hello\r
- >>> p.send("World")\r
- World\r
- >>> # p.throw(RuntimeError, "Goodbye")\r
- >>> # p.send("Meh")\r
- >>> # p.close()\r
- """\r
- while True:\r
- item = yield\r
- print format % (item, )\r
-\r
-\r
-@autostart\r
-def null_sink():\r
- """\r
- Good for uses like with cochain to pick up any slack\r
- """\r
- while True:\r
- item = yield\r
-\r
-\r
-def itr_source(itr, target):\r
- """\r
- >>> itr_source(xrange(2), printer_sink())\r
- 0\r
- 1\r
- """\r
- for item in itr:\r
- target.send(item)\r
-\r
-\r
-@autostart\r
-def cofilter(predicate, target):\r
- """\r
- >>> p = printer_sink()\r
- >>> cf = cofilter(None, p)\r
- >>> cf.send("")\r
- >>> cf.send("Hello")\r
- Hello\r
- >>> cf.send([])\r
- >>> cf.send([1, 2])\r
- [1, 2]\r
- >>> cf.send(False)\r
- >>> cf.send(True)\r
- True\r
- >>> cf.send(0)\r
- >>> cf.send(1)\r
- 1\r
- >>> # cf.throw(RuntimeError, "Goodbye")\r
- >>> # cf.send(False)\r
- >>> # cf.send(True)\r
- >>> # cf.close()\r
- """\r
- if predicate is None:\r
- predicate = bool\r
-\r
- while True:\r
- try:\r
- item = yield\r
- if predicate(item):\r
- target.send(item)\r
- except StandardError, e:\r
- target.throw(e.__class__, e.message)\r
-\r
-\r
-@autostart\r
-def comap(function, target):\r
- """\r
- >>> p = printer_sink()\r
- >>> cm = comap(lambda x: x+1, p)\r
- >>> cm.send(0)\r
- 1\r
- >>> cm.send(1.0)\r
- 2.0\r
- >>> cm.send(-2)\r
- -1\r
- >>> # cm.throw(RuntimeError, "Goodbye")\r
- >>> # cm.send(0)\r
- >>> # cm.send(1.0)\r
- >>> # cm.close()\r
- """\r
- while True:\r
- try:\r
- item = yield\r
- mappedItem = function(item)\r
- target.send(mappedItem)\r
- except StandardError, e:\r
- target.throw(e.__class__, e.message)\r
-\r
-\r
-def func_sink(function):\r
- return comap(function, null_sink())\r
-\r
-\r
-def expand_positional(function):\r
-\r
- @functools.wraps(function)\r
- def expander(item):\r
- return function(*item)\r
-\r
- return expander\r
-\r
-\r
-@autostart\r
-def append_sink(l):\r
- """\r
- >>> l = []\r
- >>> apps = append_sink(l)\r
- >>> apps.send(1)\r
- >>> apps.send(2)\r
- >>> apps.send(3)\r
- >>> print l\r
- [1, 2, 3]\r
- """\r
- while True:\r
- item = yield\r
- l.append(item)\r
-\r
-\r
-@autostart\r
-def last_n_sink(l, n = 1):\r
- """\r
- >>> l = []\r
- >>> lns = last_n_sink(l)\r
- >>> lns.send(1)\r
- >>> lns.send(2)\r
- >>> lns.send(3)\r
- >>> print l\r
- [3]\r
- """\r
- del l[:]\r
- while True:\r
- item = yield\r
- extraCount = len(l) - n + 1\r
- if 0 < extraCount:\r
- del l[0:extraCount]\r
- l.append(item)\r
-\r
-\r
-@autostart\r
-def coreduce(target, function, initializer = None):\r
- """\r
- >>> reduceResult = []\r
- >>> lns = last_n_sink(reduceResult)\r
- >>> cr = coreduce(lns, lambda x, y: x + y, 0)\r
- >>> cr.send(1)\r
- >>> cr.send(2)\r
- >>> cr.send(3)\r
- >>> print reduceResult\r
- [6]\r
- >>> cr = coreduce(lns, lambda x, y: x + y)\r
- >>> cr.send(1)\r
- >>> cr.send(2)\r
- >>> cr.send(3)\r
- >>> print reduceResult\r
- [6]\r
- """\r
- isFirst = True\r
- cumulativeRef = initializer\r
- while True:\r
- item = yield\r
- if isFirst and initializer is None:\r
- cumulativeRef = item\r
- else:\r
- cumulativeRef = function(cumulativeRef, item)\r
- target.send(cumulativeRef)\r
- isFirst = False\r
-\r
-\r
-@autostart\r
-def cotee(targets):\r
- """\r
- Takes a sequence of coroutines and sends the received items to all of them\r
-\r
- >>> ct = cotee((printer_sink("1 %s"), printer_sink("2 %s")))\r
- >>> ct.send("Hello")\r
- 1 Hello\r
- 2 Hello\r
- >>> ct.send("World")\r
- 1 World\r
- 2 World\r
- >>> # ct.throw(RuntimeError, "Goodbye")\r
- >>> # ct.send("Meh")\r
- >>> # ct.close()\r
- """\r
- while True:\r
- try:\r
- item = yield\r
- for target in targets:\r
- target.send(item)\r
- except StandardError, e:\r
- for target in targets:\r
- target.throw(e.__class__, e.message)\r
-\r
-\r
-class CoTee(object):\r
- """\r
- >>> ct = CoTee()\r
- >>> ct.register_sink(printer_sink("1 %s"))\r
- >>> ct.register_sink(printer_sink("2 %s"))\r
- >>> ct.stage.send("Hello")\r
- 1 Hello\r
- 2 Hello\r
- >>> ct.stage.send("World")\r
- 1 World\r
- 2 World\r
- >>> ct.register_sink(printer_sink("3 %s"))\r
- >>> ct.stage.send("Foo")\r
- 1 Foo\r
- 2 Foo\r
- 3 Foo\r
- >>> # ct.stage.throw(RuntimeError, "Goodbye")\r
- >>> # ct.stage.send("Meh")\r
- >>> # ct.stage.close()\r
- """\r
-\r
- def __init__(self):\r
- self.stage = self._stage()\r
- self._targets = []\r
-\r
- def register_sink(self, sink):\r
- self._targets.append(sink)\r
-\r
- def unregister_sink(self, sink):\r
- self._targets.remove(sink)\r
-\r
- def restart(self):\r
- self.stage = self._stage()\r
-\r
- @autostart\r
- def _stage(self):\r
- while True:\r
- try:\r
- item = yield\r
- for target in self._targets:\r
- target.send(item)\r
- except StandardError, e:\r
- for target in self._targets:\r
- target.throw(e.__class__, e.message)\r
-\r
-\r
-def _flush_queue(queue):\r
- while not queue.empty():\r
- yield queue.get()\r
-\r
-\r
-@autostart\r
-def cocount(target, start = 0):\r
- """\r
- >>> cc = cocount(printer_sink("%s"))\r
- >>> cc.send("a")\r
- 0\r
- >>> cc.send(None)\r
- 1\r
- >>> cc.send([])\r
- 2\r
- >>> cc.send(0)\r
- 3\r
- """\r
- for i in itertools.count(start):\r
- item = yield\r
- target.send(i)\r
-\r
-\r
-@autostart\r
-def coenumerate(target, start = 0):\r
- """\r
- >>> ce = coenumerate(printer_sink("%r"))\r
- >>> ce.send("a")\r
- (0, 'a')\r
- >>> ce.send(None)\r
- (1, None)\r
- >>> ce.send([])\r
- (2, [])\r
- >>> ce.send(0)\r
- (3, 0)\r
- """\r
- for i in itertools.count(start):\r
- item = yield\r
- decoratedItem = i, item\r
- target.send(decoratedItem)\r
-\r
-\r
-@autostart\r
-def corepeat(target, elem):\r
- """\r
- >>> cr = corepeat(printer_sink("%s"), "Hello World")\r
- >>> cr.send("a")\r
- Hello World\r
- >>> cr.send(None)\r
- Hello World\r
- >>> cr.send([])\r
- Hello World\r
- >>> cr.send(0)\r
- Hello World\r
- """\r
- while True:\r
- item = yield\r
- target.send(elem)\r
-\r
-\r
-@autostart\r
-def cointercept(target, elems):\r
- """\r
- >>> cr = cointercept(printer_sink("%s"), [1, 2, 3, 4])\r
- >>> cr.send("a")\r
- 1\r
- >>> cr.send(None)\r
- 2\r
- >>> cr.send([])\r
- 3\r
- >>> cr.send(0)\r
- 4\r
- >>> cr.send("Bye")\r
- Traceback (most recent call last):\r
- File "/usr/lib/python2.5/doctest.py", line 1228, in __run\r
- compileflags, 1) in test.globs\r
- File "<doctest __main__.cointercept[5]>", line 1, in <module>\r
- cr.send("Bye")\r
- StopIteration\r
- """\r
- item = yield\r
- for elem in elems:\r
- target.send(elem)\r
- item = yield\r
-\r
-\r
-@autostart\r
-def codropwhile(target, pred):\r
- """\r
- >>> cdw = codropwhile(printer_sink("%s"), lambda x: x)\r
- >>> cdw.send([0, 1, 2])\r
- >>> cdw.send(1)\r
- >>> cdw.send(True)\r
- >>> cdw.send(False)\r
- >>> cdw.send([0, 1, 2])\r
- [0, 1, 2]\r
- >>> cdw.send(1)\r
- 1\r
- >>> cdw.send(True)\r
- True\r
- """\r
- while True:\r
- item = yield\r
- if not pred(item):\r
- break\r
-\r
- while True:\r
- item = yield\r
- target.send(item)\r
-\r
-\r
-@autostart\r
-def cotakewhile(target, pred):\r
- """\r
- >>> ctw = cotakewhile(printer_sink("%s"), lambda x: x)\r
- >>> ctw.send([0, 1, 2])\r
- [0, 1, 2]\r
- >>> ctw.send(1)\r
- 1\r
- >>> ctw.send(True)\r
- True\r
- >>> ctw.send(False)\r
- >>> ctw.send([0, 1, 2])\r
- >>> ctw.send(1)\r
- >>> ctw.send(True)\r
- """\r
- while True:\r
- item = yield\r
- if not pred(item):\r
- break\r
- target.send(item)\r
-\r
- while True:\r
- item = yield\r
-\r
-\r
-@autostart\r
-def coslice(target, lower, upper):\r
- """\r
- >>> cs = coslice(printer_sink("%r"), 3, 5)\r
- >>> cs.send("0")\r
- >>> cs.send("1")\r
- >>> cs.send("2")\r
- >>> cs.send("3")\r
- '3'\r
- >>> cs.send("4")\r
- '4'\r
- >>> cs.send("5")\r
- >>> cs.send("6")\r
- """\r
- for i in xrange(lower):\r
- item = yield\r
- for i in xrange(upper - lower):\r
- item = yield\r
- target.send(item)\r
- while True:\r
- item = yield\r
-\r
-\r
-@autostart\r
-def cochain(targets):\r
- """\r
- >>> cr = cointercept(printer_sink("good %s"), [1, 2, 3, 4])\r
- >>> cc = cochain([cr, printer_sink("end %s")])\r
- >>> cc.send("a")\r
- good 1\r
- >>> cc.send(None)\r
- good 2\r
- >>> cc.send([])\r
- good 3\r
- >>> cc.send(0)\r
- good 4\r
- >>> cc.send("Bye")\r
- end Bye\r
- """\r
- behind = []\r
- for target in targets:\r
- try:\r
- while behind:\r
- item = behind.pop()\r
- target.send(item)\r
- while True:\r
- item = yield\r
- target.send(item)\r
- except StopIteration:\r
- behind.append(item)\r
-\r
-\r
-@autostart\r
-def queue_sink(queue):\r
- """\r
- >>> q = Queue.Queue()\r
- >>> qs = queue_sink(q)\r
- >>> qs.send("Hello")\r
- >>> qs.send("World")\r
- >>> qs.throw(RuntimeError, "Goodbye")\r
- >>> qs.send("Meh")\r
- >>> qs.close()\r
- >>> print [i for i in _flush_queue(q)]\r
- [(None, 'Hello'), (None, 'World'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]\r
- """\r
- while True:\r
- try:\r
- item = yield\r
- queue.put((None, item))\r
- except StandardError, e:\r
- queue.put((e.__class__, e.message))\r
- except GeneratorExit:\r
- queue.put((GeneratorExit, None))\r
- raise\r
-\r
-\r
-def decode_item(item, target):\r
- if item[0] is None:\r
- target.send(item[1])\r
- return False\r
- elif item[0] is GeneratorExit:\r
- target.close()\r
- return True\r
- else:\r
- target.throw(item[0], item[1])\r
- return False\r
-\r
-\r
-def queue_source(queue, target):\r
- """\r
- >>> q = Queue.Queue()\r
- >>> for i in [\r
- ... (None, 'Hello'),\r
- ... (None, 'World'),\r
- ... (GeneratorExit, None),\r
- ... ]:\r
- ... q.put(i)\r
- >>> qs = queue_source(q, printer_sink())\r
- Hello\r
- World\r
- """\r
- isDone = False\r
- while not isDone:\r
- item = queue.get()\r
- isDone = decode_item(item, target)\r
-\r
-\r
-def threaded_stage(target, thread_factory = threading.Thread):\r
- messages = Queue.Queue()\r
-\r
- run_source = functools.partial(queue_source, messages, target)\r
- thread_factory(target=run_source).start()\r
-\r
- # Sink running in current thread\r
- return functools.partial(queue_sink, messages)\r
-\r
-\r
-@autostart\r
-def pickle_sink(f):\r
- while True:\r
- try:\r
- item = yield\r
- pickle.dump((None, item), f)\r
- except StandardError, e:\r
- pickle.dump((e.__class__, e.message), f)\r
- except GeneratorExit:\r
- pickle.dump((GeneratorExit, ), f)\r
- raise\r
- except StopIteration:\r
- f.close()\r
- return\r
-\r
-\r
-def pickle_source(f, target):\r
- try:\r
- isDone = False\r
- while not isDone:\r
- item = pickle.load(f)\r
- isDone = decode_item(item, target)\r
- except EOFError:\r
- target.close()\r
-\r
-\r
-class EventHandler(object, xml.sax.ContentHandler):\r
-\r
- START = "start"\r
- TEXT = "text"\r
- END = "end"\r
-\r
- def __init__(self, target):\r
- object.__init__(self)\r
- xml.sax.ContentHandler.__init__(self)\r
- self._target = target\r
-\r
- def startElement(self, name, attrs):\r
- self._target.send((self.START, (name, attrs._attrs)))\r
-\r
- def characters(self, text):\r
- self._target.send((self.TEXT, text))\r
-\r
- def endElement(self, name):\r
- self._target.send((self.END, name))\r
-\r
-\r
-def expat_parse(f, target):\r
- parser = xml.parsers.expat.ParserCreate()\r
- parser.buffer_size = 65536\r
- parser.buffer_text = True\r
- parser.returns_unicode = False\r
- parser.StartElementHandler = lambda name, attrs: target.send(('start', (name, attrs)))\r
- parser.EndElementHandler = lambda name: target.send(('end', name))\r
- parser.CharacterDataHandler = lambda data: target.send(('text', data))\r
- parser.ParseFile(f)\r
-\r
-\r
-if __name__ == "__main__":\r
- import doctest\r
- doctest.testmod()\r
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import time
-import functools
-import threading
-import Queue
-import logging
-
-import gobject
-
-import algorithms
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-def make_idler(func):
- """
- Decorator that makes a generator-function into a function that will continue execution on next call
- """
- a = []
-
- @functools.wraps(func)
- def decorated_func(*args, **kwds):
- if not a:
- a.append(func(*args, **kwds))
- try:
- a[0].next()
- return True
- except StopIteration:
- del a[:]
- return False
-
- return decorated_func
-
-
-def async(func):
- """
- Make a function mainloop friendly. the function will be called at the
- next mainloop idle state.
-
- >>> import misc
- >>> misc.validate_decorator(async)
- """
-
- @functools.wraps(func)
- def new_function(*args, **kwargs):
-
- def async_function():
- func(*args, **kwargs)
- return False
-
- gobject.idle_add(async_function)
-
- return new_function
-
-
-class Async(object):
-
- def __init__(self, func, once = True):
- self.__func = func
- self.__idleId = None
- self.__once = once
-
- def start(self):
- assert self.__idleId is None
- if self.__once:
- self.__idleId = gobject.idle_add(self._on_once)
- else:
- self.__idleId = gobject.idle_add(self.__func)
-
- def is_running(self):
- return self.__idleId is not None
-
- def cancel(self):
- if self.__idleId is not None:
- gobject.source_remove(self.__idleId)
- self.__idleId = None
-
- def __call__(self):
- return self.start()
-
- @misc.log_exception(_moduleLogger)
- def _on_once(self):
- self.cancel()
- try:
- self.__func()
- except Exception:
- pass
- return False
-
-
-class Timeout(object):
-
- def __init__(self, func, once = True):
- self.__func = func
- self.__timeoutId = None
- self.__once = once
-
- def start(self, **kwds):
- assert self.__timeoutId is None
-
- callback = self._on_once if self.__once else self.__func
-
- assert len(kwds) == 1
- timeoutInSeconds = kwds["seconds"]
- assert 0 <= timeoutInSeconds
-
- if timeoutInSeconds == 0:
- self.__timeoutId = gobject.idle_add(callback)
- else:
- self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback)
-
- def is_running(self):
- return self.__timeoutId is not None
-
- def cancel(self):
- if self.__timeoutId is not None:
- gobject.source_remove(self.__timeoutId)
- self.__timeoutId = None
-
- def __call__(self, **kwds):
- return self.start(**kwds)
-
- @misc.log_exception(_moduleLogger)
- def _on_once(self):
- self.cancel()
- try:
- self.__func()
- except Exception:
- pass
- return False
-
-
-_QUEUE_EMPTY = object()
-
-
-class FutureThread(object):
-
- def __init__(self):
- self.__workQueue = Queue.Queue()
- self.__thread = threading.Thread(
- name = type(self).__name__,
- target = self.__consume_queue,
- )
- self.__isRunning = True
-
- def start(self):
- self.__thread.start()
-
- def stop(self):
- self.__isRunning = False
- for _ in algorithms.itr_available(self.__workQueue):
- pass # eat up queue to cut down dumb work
- self.__workQueue.put(_QUEUE_EMPTY)
-
- def clear_tasks(self):
- for _ in algorithms.itr_available(self.__workQueue):
- pass # eat up queue to cut down dumb work
-
- def add_task(self, func, args, kwds, on_success, on_error):
- task = func, args, kwds, on_success, on_error
- self.__workQueue.put(task)
-
- @misc.log_exception(_moduleLogger)
- def __trampoline_callback(self, on_success, on_error, isError, result):
- if not self.__isRunning:
- if isError:
- _moduleLogger.error("Masking: %s" % (result, ))
- isError = True
- result = StopIteration("Cancelling all callbacks")
- callback = on_success if not isError else on_error
- try:
- callback(result)
- except Exception:
- _moduleLogger.exception("Callback errored")
- return False
-
- @misc.log_exception(_moduleLogger)
- def __consume_queue(self):
- while True:
- task = self.__workQueue.get()
- if task is _QUEUE_EMPTY:
- break
- func, args, kwds, on_success, on_error = task
-
- try:
- result = func(*args, **kwds)
- isError = False
- except Exception, e:
- _moduleLogger.error("Error, passing it back to the main thread")
- result = e
- isError = True
- self.__workQueue.task_done()
-
- gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result)
- _moduleLogger.debug("Shutting down worker thread")
-
-
-class AutoSignal(object):
-
- def __init__(self, toplevel):
- self.__disconnectPool = []
- toplevel.connect("destroy", self.__on_destroy)
-
- def connect_auto(self, widget, *args):
- id = widget.connect(*args)
- self.__disconnectPool.append((widget, id))
-
- @misc.log_exception(_moduleLogger)
- def __on_destroy(self, widget):
- _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool)))
- for widget, id in self.__disconnectPool:
- widget.disconnect(id)
- del self.__disconnectPool[:]
-
-
-def throttled(minDelay, queue):
- """
- Throttle the calls to a function by queueing all the calls that happen
- before the minimum delay
-
- >>> import misc
- >>> import Queue
- >>> misc.validate_decorator(throttled(0, Queue.Queue()))
- """
-
- def actual_decorator(func):
-
- lastCallTime = [None]
-
- def process_queue():
- if 0 < len(queue):
- func, args, kwargs = queue.pop(0)
- lastCallTime[0] = time.time() * 1000
- func(*args, **kwargs)
- return False
-
- @functools.wraps(func)
- def new_function(*args, **kwargs):
- now = time.time() * 1000
- if (
- lastCallTime[0] is None or
- (now - lastCallTime >= minDelay)
- ):
- lastCallTime[0] = now
- func(*args, **kwargs)
- else:
- queue.append((func, args, kwargs))
- lastCallDelta = now - lastCallTime[0]
- processQueueTimeout = int(minDelay * len(queue) - lastCallDelta)
- gobject.timeout_add(processQueueTimeout, process_queue)
-
- return new_function
-
- return actual_decorator
-
-
-def _old_timeout_add_seconds(timeout, callback):
- return gobject.timeout_add(timeout * 1000, callback)
-
-
-def _timeout_add_seconds(timeout, callback):
- return gobject.timeout_add_seconds(timeout, callback)
-
-
-try:
- gobject.timeout_add_seconds
- timeout_add_seconds = _timeout_add_seconds
-except AttributeError:
- timeout_add_seconds = _old_timeout_add_seconds
+++ /dev/null
-#!/usr/bin/env python
-
-
-from __future__ import with_statement
-
-import os
-import pickle
-import contextlib
-import itertools
-import codecs
-from xml.sax import saxutils
-import csv
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-
-
-@contextlib.contextmanager
-def change_directory(directory):
- previousDirectory = os.getcwd()
- os.chdir(directory)
- currentDirectory = os.getcwd()
-
- try:
- yield previousDirectory, currentDirectory
- finally:
- os.chdir(previousDirectory)
-
-
-@contextlib.contextmanager
-def pickled(filename):
- """
- Here is an example usage:
- with pickled("foo.db") as p:
- p("users", list).append(["srid", "passwd", 23])
- """
-
- if os.path.isfile(filename):
- data = pickle.load(open(filename))
- else:
- data = {}
-
- def getter(item, factory):
- if item in data:
- return data[item]
- else:
- data[item] = factory()
- return data[item]
-
- yield getter
-
- pickle.dump(data, open(filename, "w"))
-
-
-@contextlib.contextmanager
-def redirect(object_, attr, value):
- """
- >>> import sys
- ... with redirect(sys, 'stdout', open('stdout', 'w')):
- ... print "hello"
- ...
- >>> print "we're back"
- we're back
- """
- orig = getattr(object_, attr)
- setattr(object_, attr, value)
- try:
- yield
- finally:
- setattr(object_, attr, orig)
-
-
-def pathsplit(path):
- """
- >>> pathsplit("/a/b/c")
- ['', 'a', 'b', 'c']
- >>> pathsplit("./plugins/builtins.ini")
- ['.', 'plugins', 'builtins.ini']
- """
- pathParts = path.split(os.path.sep)
- return pathParts
-
-
-def commonpath(l1, l2, common=None):
- """
- >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1'))
- (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1'])
- >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini"))
- (['.', 'plugins'], [''], ['builtins.ini'])
- >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins"))
- (['.', 'plugins'], ['builtins'], [])
- """
- if common is None:
- common = []
-
- if l1 == l2:
- return l1, [], []
-
- for i, (leftDir, rightDir) in enumerate(zip(l1, l2)):
- if leftDir != rightDir:
- return l1[0:i], l1[i:], l2[i:]
- else:
- if leftDir == rightDir:
- i += 1
- return l1[0:i], l1[i:], l2[i:]
-
-
-def relpath(p1, p2):
- """
- >>> relpath('/', '/')
- './'
- >>> relpath('/a/b/c/d', '/')
- '../../../../'
- >>> relpath('/a/b/c/d', '/a/b/c1/d1')
- '../../c1/d1'
- >>> relpath('/a/b/c/d', '/a/b/c1/d1/')
- '../../c1/d1'
- >>> relpath("./plugins/builtins", "./plugins")
- '../'
- >>> relpath("./plugins/", "./plugins/builtins.ini")
- 'builtins.ini'
- """
- sourcePath = os.path.normpath(p1)
- destPath = os.path.normpath(p2)
-
- (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath))
- if len(sourceOnly) or len(destOnly):
- relParts = itertools.chain(
- (('..' + os.sep) * len(sourceOnly), ),
- destOnly,
- )
- return os.path.join(*relParts)
- else:
- return "."+os.sep
-
-
-class UTF8Recoder(object):
- """
- Iterator that reads an encoded stream and reencodes the input to UTF-8
- """
- def __init__(self, f, encoding):
- self.reader = codecs.getreader(encoding)(f)
-
- def __iter__(self):
- return self
-
- def next(self):
- return self.reader.next().encode("utf-8")
-
-
-class UnicodeReader(object):
- """
- A CSV reader which will iterate over lines in the CSV file "f",
- which is encoded in the given encoding.
- """
-
- def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
- f = UTF8Recoder(f, encoding)
- self.reader = csv.reader(f, dialect=dialect, **kwds)
-
- def next(self):
- row = self.reader.next()
- return [unicode(s, "utf-8") for s in row]
-
- def __iter__(self):
- return self
-
-class UnicodeWriter(object):
- """
- A CSV writer which will write rows to CSV file "f",
- which is encoded in the given encoding.
- """
-
- def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
- # Redirect output to a queue
- self.queue = StringIO.StringIO()
- self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
- self.stream = f
- self.encoder = codecs.getincrementalencoder(encoding)()
-
- def writerow(self, row):
- self.writer.writerow([s.encode("utf-8") for s in row])
- # Fetch UTF-8 output from the queue ...
- data = self.queue.getvalue()
- data = data.decode("utf-8")
- # ... and reencode it into the target encoding
- data = self.encoder.encode(data)
- # write to the target stream
- self.stream.write(data)
- # empty queue
- self.queue.truncate(0)
-
- def writerows(self, rows):
- for row in rows:
- self.writerow(row)
-
-
-def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
- # csv.py doesn't do Unicode; encode temporarily as UTF-8:
- csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
- dialect=dialect, **kwargs)
- for row in csv_reader:
- # decode UTF-8 back to Unicode, cell by cell:
- yield [unicode(cell, 'utf-8') for cell in row]
-
-
-def utf_8_encoder(unicode_csv_data):
- for line in unicode_csv_data:
- yield line.encode('utf-8')
-
-
-_UNESCAPE_ENTITIES = {
- """: '"',
- " ": " ",
- "'": "'",
-}
-
-
-_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys()))
-del _ESCAPE_ENTITIES[" "]
-
-
-def unescape(text):
- plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
- return plain
-
-
-def escape(text):
- fancy = saxutils.escape(text, _ESCAPE_ENTITIES)
- return fancy
+++ /dev/null
-#!/usr/bin/env python
-
-
-import os
-import logging
-
-try:
- from xdg import BaseDirectory as _BaseDirectory
- BaseDirectory = _BaseDirectory
-except ImportError:
- BaseDirectory = None
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_libc = None
-
-
-def set_process_name(name):
- try: # change process name for killall
- global _libc
- if _libc is None:
- import ctypes
- _libc = ctypes.CDLL('libc.so.6')
- _libc.prctl(15, name, 0, 0, 0)
- except Exception, e:
- _moduleLogger.warning('Unable to set processName: %s" % e')
-
-
-def get_new_resource(resourceType, resource, name):
- if BaseDirectory is not None:
- if resourceType == "data":
- base = BaseDirectory.xdg_data_home
- if base == "/usr/share/mime":
- # Ugly hack because somehow Maemo 4.1 seems to be set to this
- base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
- elif resourceType == "config":
- base = BaseDirectory.xdg_config_home
- elif resourceType == "cache":
- base = BaseDirectory.xdg_cache_home
- else:
- raise RuntimeError("Unknown type: "+resourceType)
- else:
- base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
-
- filePath = os.path.join(base, resource, name)
- dirPath = os.path.dirname(filePath)
- if not os.path.exists(dirPath):
- # Looking before I leap to not mask errors
- os.makedirs(dirPath)
-
- return filePath
-
-
-def get_existing_resource(resourceType, resource, name):
- if BaseDirectory is not None:
- if resourceType == "data":
- base = BaseDirectory.xdg_data_home
- elif resourceType == "config":
- base = BaseDirectory.xdg_config_home
- elif resourceType == "cache":
- base = BaseDirectory.xdg_cache_home
- else:
- raise RuntimeError("Unknown type: "+resourceType)
- else:
- base = None
-
- if base is not None:
- finalPath = os.path.join(base, name)
- if os.path.exists(finalPath):
- return finalPath
-
- altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource)
- finalPath = os.path.join(altBase, name)
- if os.path.exists(finalPath):
- return finalPath
- else:
- raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), ))
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import sys
-import re
-import cPickle
-
-import functools
-import contextlib
-import inspect
-
-import optparse
-import traceback
-import warnings
-import string
-
-
-class AnyData(object):
-
- pass
-
-
-_indentationLevel = [0]
-
-
-def log_call(logger):
-
- def log_call_decorator(func):
-
- @functools.wraps(func)
- def wrapper(*args, **kwds):
- logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, ))
- _indentationLevel[0] += 1
- try:
- return func(*args, **kwds)
- finally:
- _indentationLevel[0] -= 1
- logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, ))
-
- return wrapper
-
- return log_call_decorator
-
-
-def log_exception(logger):
-
- def log_exception_decorator(func):
-
- @functools.wraps(func)
- def wrapper(*args, **kwds):
- try:
- return func(*args, **kwds)
- except Exception:
- logger.exception(func.__name__)
- raise
-
- return wrapper
-
- return log_exception_decorator
-
-
-def printfmt(template):
- """
- This hides having to create the Template object and call substitute/safe_substitute on it. For example:
-
- >>> num = 10
- >>> word = "spam"
- >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
- I would like to order 10 units of spam, please
- """
- frame = inspect.stack()[-1][0]
- try:
- print string.Template(template).safe_substitute(frame.f_locals)
- finally:
- del frame
-
-
-def is_special(name):
- return name.startswith("__") and name.endswith("__")
-
-
-def is_private(name):
- return name.startswith("_") and not is_special(name)
-
-
-def privatize(clsName, attributeName):
- """
- At runtime, make an attributeName private
-
- Example:
- >>> class Test(object):
- ... pass
- ...
- >>> try:
- ... dir(Test).index("_Test__me")
- ... print dir(Test)
- ... except:
- ... print "Not Found"
- Not Found
- >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
- >>> try:
- ... dir(Test).index("_Test__me")
- ... print "Found"
- ... except:
- ... print dir(Test)
- 0
- Found
- >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
- Hello World
- >>>
- >>> is_private(privatize(Test.__name__, "me"))
- True
- >>> is_special(privatize(Test.__name__, "me"))
- False
- """
- return "".join(["_", clsName, "__", attributeName])
-
-
-def obfuscate(clsName, attributeName):
- """
- At runtime, turn a private name into the obfuscated form
-
- Example:
- >>> class Test(object):
- ... __me = "Hello World"
- ...
- >>> try:
- ... dir(Test).index("_Test__me")
- ... print "Found"
- ... except:
- ... print dir(Test)
- 0
- Found
- >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
- Hello World
- >>> is_private(obfuscate(Test.__name__, "__me"))
- True
- >>> is_special(obfuscate(Test.__name__, "__me"))
- False
- """
- return "".join(["_", clsName, attributeName])
-
-
-class PAOptionParser(optparse.OptionParser, object):
- """
- >>> if __name__ == '__main__':
- ... #parser = PAOptionParser("My usage str")
- ... parser = PAOptionParser()
- ... parser.add_posarg("Foo", help="Foo usage")
- ... parser.add_posarg("Bar", dest="bar_dest")
- ... parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
- ... parser.add_option('--stocksym', dest='symbol')
- ... values, args = parser.parse_args()
- ... print values, args
- ...
-
- python mycp.py -h
- python mycp.py
- python mycp.py foo
- python mycp.py foo bar
-
- python mycp.py foo bar lava
- Usage: pa.py <Foo> <Bar> <Language> [options]
-
- Positional Arguments:
- Foo: Foo usage
- Bar:
- Language:
-
- pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other'
- """
-
- def __init__(self, *args, **kw):
- self.posargs = []
- super(PAOptionParser, self).__init__(*args, **kw)
-
- def add_posarg(self, *args, **kw):
- pa_help = kw.get("help", "")
- kw["help"] = optparse.SUPPRESS_HELP
- o = self.add_option("--%s" % args[0], *args[1:], **kw)
- self.posargs.append((args[0], pa_help))
-
- def get_usage(self, *args, **kwargs):
- params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs]))
- self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params
- return super(PAOptionParser, self).get_usage(*args, **kwargs)
-
- def parse_args(self, *args, **kwargs):
- args = sys.argv[1:]
- args0 = []
- for p, v in zip(self.posargs, args):
- args0.append("--%s" % p[0])
- args0.append(v)
- args = args0 + args
- options, args = super(PAOptionParser, self).parse_args(args, **kwargs)
- if len(args) < len(self.posargs):
- msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):])
- self.error(msg)
- return options, args
-
-
-def explicitly(name, stackadd=0):
- """
- This is an alias for adding to '__all__'. Less error-prone than using
- __all__ itself, since setting __all__ directly is prone to stomping on
- things implicitly exported via L{alias}.
-
- @note Taken from PyExport (which could turn out pretty cool):
- @li @a http://codebrowse.launchpad.net/~glyph/
- @li @a http://glyf.livejournal.com/74356.html
- """
- packageVars = sys._getframe(1+stackadd).f_locals
- globalAll = packageVars.setdefault('__all__', [])
- globalAll.append(name)
-
-
-def public(thunk):
- """
- This is a decorator, for convenience. Rather than typing the name of your
- function twice, you can decorate a function with this.
-
- To be real, @public would need to work on methods as well, which gets into
- supporting types...
-
- @note Taken from PyExport (which could turn out pretty cool):
- @li @a http://codebrowse.launchpad.net/~glyph/
- @li @a http://glyf.livejournal.com/74356.html
- """
- explicitly(thunk.__name__, 1)
- return thunk
-
-
-def _append_docstring(obj, message):
- if obj.__doc__ is None:
- obj.__doc__ = message
- else:
- obj.__doc__ += message
-
-
-def validate_decorator(decorator):
-
- def simple(x):
- return x
-
- f = simple
- f.__name__ = "name"
- f.__doc__ = "doc"
- f.__dict__["member"] = True
-
- g = decorator(f)
-
- if f.__name__ != g.__name__:
- print f.__name__, "!=", g.__name__
-
- if g.__doc__ is None:
- print decorator.__name__, "has no doc string"
- elif not g.__doc__.startswith(f.__doc__):
- print g.__doc__, "didn't start with", f.__doc__
-
- if not ("member" in g.__dict__ and g.__dict__["member"]):
- print "'member' not in ", g.__dict__
-
-
-def deprecated_api(func):
- """
- This is a decorator which can be used to mark functions
- as deprecated. It will result in a warning being emitted
- when the function is used.
-
- >>> validate_decorator(deprecated_api)
- """
-
- @functools.wraps(func)
- def newFunc(*args, **kwargs):
- warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning)
- return func(*args, **kwargs)
-
- _append_docstring(newFunc, "\n@deprecated")
- return newFunc
-
-
-def unstable_api(func):
- """
- This is a decorator which can be used to mark functions
- as deprecated. It will result in a warning being emitted
- when the function is used.
-
- >>> validate_decorator(unstable_api)
- """
-
- @functools.wraps(func)
- def newFunc(*args, **kwargs):
- warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning)
- return func(*args, **kwargs)
- _append_docstring(newFunc, "\n@unstable")
- return newFunc
-
-
-def enabled(func):
- """
- This decorator doesn't add any behavior
-
- >>> validate_decorator(enabled)
- """
- return func
-
-
-def disabled(func):
- """
- This decorator disables the provided function, and does nothing
-
- >>> validate_decorator(disabled)
- """
-
- @functools.wraps(func)
- def emptyFunc(*args, **kargs):
- pass
- _append_docstring(emptyFunc, "\n@note Temporarily Disabled")
- return emptyFunc
-
-
-def metadata(document=True, **kwds):
- """
- >>> validate_decorator(metadata(author="Ed"))
- """
-
- def decorate(func):
- for k, v in kwds.iteritems():
- setattr(func, k, v)
- if document:
- _append_docstring(func, "\n@"+k+" "+v)
- return func
- return decorate
-
-
-def prop(func):
- """Function decorator for defining property attributes
-
- The decorated function is expected to return a dictionary
- containing one or more of the following pairs:
- fget - function for getting attribute value
- fset - function for setting attribute value
- fdel - function for deleting attribute
- This can be conveniently constructed by the locals() builtin
- function; see:
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
- @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html
-
- Example:
- >>> #Due to transformation from function to property, does not need to be validated
- >>> #validate_decorator(prop)
- >>> class MyExampleClass(object):
- ... @prop
- ... def foo():
- ... "The foo property attribute's doc-string"
- ... def fget(self):
- ... print "GET"
- ... return self._foo
- ... def fset(self, value):
- ... print "SET"
- ... self._foo = value
- ... return locals()
- ...
- >>> me = MyExampleClass()
- >>> me.foo = 10
- SET
- >>> print me.foo
- GET
- 10
- """
- return property(doc=func.__doc__, **func())
-
-
-def print_handler(e):
- """
- @see ExpHandler
- """
- print "%s: %s" % (type(e).__name__, e)
-
-
-def print_ignore(e):
- """
- @see ExpHandler
- """
- print 'Ignoring %s exception: %s' % (type(e).__name__, e)
-
-
-def print_traceback(e):
- """
- @see ExpHandler
- """
- #print sys.exc_info()
- traceback.print_exc(file=sys.stdout)
-
-
-def ExpHandler(handler = print_handler, *exceptions):
- """
- An exception handling idiom using decorators
- Examples
- Specify exceptions in order, first one is handled first
- last one last.
-
- >>> validate_decorator(ExpHandler())
- >>> @ExpHandler(print_ignore, ZeroDivisionError)
- ... @ExpHandler(None, AttributeError, ValueError)
- ... def f1():
- ... 1/0
- >>> @ExpHandler(print_traceback, ZeroDivisionError)
- ... def f2():
- ... 1/0
- >>> @ExpHandler()
- ... def f3(*pargs):
- ... l = pargs
- ... return l[10]
- >>> @ExpHandler(print_traceback, ZeroDivisionError)
- ... def f4():
- ... return 1
- >>>
- >>>
- >>> f1()
- Ignoring ZeroDivisionError exception: integer division or modulo by zero
- >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
- Traceback (most recent call last):
- ...
- ZeroDivisionError: integer division or modulo by zero
- >>> f3()
- IndexError: tuple index out of range
- >>> f4()
- 1
- """
-
- def wrapper(f):
- localExceptions = exceptions
- if not localExceptions:
- localExceptions = [Exception]
- t = [(ex, handler) for ex in localExceptions]
- t.reverse()
-
- def newfunc(t, *args, **kwargs):
- ex, handler = t[0]
- try:
- if len(t) == 1:
- return f(*args, **kwargs)
- else:
- #Recurse for embedded try/excepts
- dec_func = functools.partial(newfunc, t[1:])
- dec_func = functools.update_wrapper(dec_func, f)
- return dec_func(*args, **kwargs)
- except ex, e:
- return handler(e)
-
- dec_func = functools.partial(newfunc, t)
- dec_func = functools.update_wrapper(dec_func, f)
- return dec_func
- return wrapper
-
-
-def into_debugger(func):
- """
- >>> validate_decorator(into_debugger)
- """
-
- @functools.wraps(func)
- def newFunc(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except:
- import pdb
- pdb.post_mortem()
-
- return newFunc
-
-
-class bindclass(object):
- """
- >>> validate_decorator(bindclass)
- >>> class Foo(BoundObject):
- ... @bindclass
- ... def foo(this_class, self):
- ... return this_class, self
- ...
- >>> class Bar(Foo):
- ... @bindclass
- ... def bar(this_class, self):
- ... return this_class, self
- ...
- >>> f = Foo()
- >>> b = Bar()
- >>>
- >>> f.foo() # doctest: +ELLIPSIS
- (<class '...Foo'>, <...Foo object at ...>)
- >>> b.foo() # doctest: +ELLIPSIS
- (<class '...Foo'>, <...Bar object at ...>)
- >>> b.bar() # doctest: +ELLIPSIS
- (<class '...Bar'>, <...Bar object at ...>)
- """
-
- def __init__(self, f):
- self.f = f
- self.__name__ = f.__name__
- self.__doc__ = f.__doc__
- self.__dict__.update(f.__dict__)
- self.m = None
-
- def bind(self, cls, attr):
-
- def bound_m(*args, **kwargs):
- return self.f(cls, *args, **kwargs)
- bound_m.__name__ = attr
- self.m = bound_m
-
- def __get__(self, obj, objtype=None):
- return self.m.__get__(obj, objtype)
-
-
-class ClassBindingSupport(type):
- "@see bindclass"
-
- def __init__(mcs, name, bases, attrs):
- type.__init__(mcs, name, bases, attrs)
- for attr, val in attrs.iteritems():
- if isinstance(val, bindclass):
- val.bind(mcs, attr)
-
-
-class BoundObject(object):
- "@see bindclass"
- __metaclass__ = ClassBindingSupport
-
-
-def bindfunction(f):
- """
- >>> validate_decorator(bindfunction)
- >>> @bindfunction
- ... def factorial(thisfunction, n):
- ... # Within this function the name 'thisfunction' refers to the factorial
- ... # function(with only one argument), even after 'factorial' is bound
- ... # to another object
- ... if n > 0:
- ... return n * thisfunction(n - 1)
- ... else:
- ... return 1
- ...
- >>> factorial(3)
- 6
- """
-
- @functools.wraps(f)
- def bound_f(*args, **kwargs):
- return f(bound_f, *args, **kwargs)
- return bound_f
-
-
-class Memoize(object):
- """
- Memoize(fn) - an instance which acts like fn but memoizes its arguments
- Will only work on functions with non-mutable arguments
- @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
-
- >>> validate_decorator(Memoize)
- """
-
- def __init__(self, fn):
- self.fn = fn
- self.__name__ = fn.__name__
- self.__doc__ = fn.__doc__
- self.__dict__.update(fn.__dict__)
- self.memo = {}
-
- def __call__(self, *args):
- if args not in self.memo:
- self.memo[args] = self.fn(*args)
- return self.memo[args]
-
-
-class MemoizeMutable(object):
- """Memoize(fn) - an instance which acts like fn but memoizes its arguments
- Will work on functions with mutable arguments(slower than Memoize)
- @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
-
- >>> validate_decorator(MemoizeMutable)
- """
-
- def __init__(self, fn):
- self.fn = fn
- self.__name__ = fn.__name__
- self.__doc__ = fn.__doc__
- self.__dict__.update(fn.__dict__)
- self.memo = {}
-
- def __call__(self, *args, **kw):
- text = cPickle.dumps((args, kw))
- if text not in self.memo:
- self.memo[text] = self.fn(*args, **kw)
- return self.memo[text]
-
-
-callTraceIndentationLevel = 0
-
-
-def call_trace(f):
- """
- Synchronization decorator.
-
- >>> validate_decorator(call_trace)
- >>> @call_trace
- ... def a(a, b, c):
- ... pass
- >>> a(1, 2, c=3)
- Entering a((1, 2), {'c': 3})
- Exiting a((1, 2), {'c': 3})
- """
-
- @functools.wraps(f)
- def verboseTrace(*args, **kw):
- global callTraceIndentationLevel
-
- print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
- callTraceIndentationLevel += 1
- try:
- result = f(*args, **kw)
- except:
- callTraceIndentationLevel -= 1
- print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
- raise
- callTraceIndentationLevel -= 1
- print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
- return result
-
- @functools.wraps(f)
- def smallTrace(*args, **kw):
- global callTraceIndentationLevel
-
- print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__)
- callTraceIndentationLevel += 1
- try:
- result = f(*args, **kw)
- except:
- callTraceIndentationLevel -= 1
- print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__)
- raise
- callTraceIndentationLevel -= 1
- print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__)
- return result
-
- #return smallTrace
- return verboseTrace
-
-
-@contextlib.contextmanager
-def nested_break():
- """
- >>> with nested_break() as mylabel:
- ... for i in xrange(3):
- ... print "Outer", i
- ... for j in xrange(3):
- ... if i == 2: raise mylabel
- ... if j == 2: break
- ... print "Inner", j
- ... print "more processing"
- Outer 0
- Inner 0
- Inner 1
- Outer 1
- Inner 0
- Inner 1
- Outer 2
- """
-
- class NestedBreakException(Exception):
- pass
-
- try:
- yield NestedBreakException
- except NestedBreakException:
- pass
-
-
-@contextlib.contextmanager
-def lexical_scope(*args):
- """
- @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586
- Example:
- >>> b = 0
- >>> with lexical_scope(1) as (a):
- ... print a
- ...
- 1
- >>> with lexical_scope(1,2,3) as (a,b,c):
- ... print a,b,c
- ...
- 1 2 3
- >>> with lexical_scope():
- ... d = 10
- ... def foo():
- ... pass
- ...
- >>> print b
- 2
- """
-
- frame = inspect.currentframe().f_back.f_back
- saved = frame.f_locals.keys()
- try:
- if not args:
- yield
- elif len(args) == 1:
- yield args[0]
- else:
- yield args
- finally:
- f_locals = frame.f_locals
- for key in (x for x in f_locals.keys() if x not in saved):
- del f_locals[key]
- del frame
-
-
-def normalize_number(prettynumber):
- """
- function to take a phone number and strip out all non-numeric
- characters
-
- >>> normalize_number("+012-(345)-678-90")
- '+01234567890'
- >>> normalize_number("1-(345)-678-9000")
- '+13456789000'
- >>> normalize_number("+1-(345)-678-9000")
- '+13456789000'
- """
- uglynumber = re.sub('[^0-9+]', '', prettynumber)
- if uglynumber.startswith("+"):
- pass
- elif uglynumber.startswith("1"):
- uglynumber = "+"+uglynumber
- elif 10 <= len(uglynumber):
- assert uglynumber[0] not in ("+", "1"), "Number format confusing"
- uglynumber = "+1"+uglynumber
- else:
- pass
-
- return uglynumber
-
-
-_VALIDATE_RE = re.compile("^\+?[0-9]{10,}$")
-
-
-def is_valid_number(number):
- """
- @returns If This number be called ( syntax validation only )
- """
- return _VALIDATE_RE.match(number) is not None
-
-
-def make_ugly(prettynumber):
- """
- function to take a phone number and strip out all non-numeric
- characters
-
- >>> make_ugly("+012-(345)-678-90")
- '+01234567890'
- """
- return normalize_number(prettynumber)
-
-
-def _make_pretty_with_areacode(phonenumber):
- prettynumber = "(%s)" % (phonenumber[0:3], )
- if 3 < len(phonenumber):
- prettynumber += " %s" % (phonenumber[3:6], )
- if 6 < len(phonenumber):
- prettynumber += "-%s" % (phonenumber[6:], )
- return prettynumber
-
-
-def _make_pretty_local(phonenumber):
- prettynumber = "%s" % (phonenumber[0:3], )
- if 3 < len(phonenumber):
- prettynumber += "-%s" % (phonenumber[3:], )
- return prettynumber
-
-
-def _make_pretty_international(phonenumber):
- prettynumber = phonenumber
- if phonenumber.startswith("1"):
- prettynumber = "1 "
- prettynumber += _make_pretty_with_areacode(phonenumber[1:])
- return prettynumber
-
-
-def make_pretty(phonenumber):
- """
- Function to take a phone number and return the pretty version
- pretty numbers:
- if phonenumber begins with 0:
- ...-(...)-...-....
- if phonenumber begins with 1: ( for gizmo callback numbers )
- 1 (...)-...-....
- if phonenumber is 13 digits:
- (...)-...-....
- if phonenumber is 10 digits:
- ...-....
- >>> make_pretty("12")
- '12'
- >>> make_pretty("1234567")
- '123-4567'
- >>> make_pretty("2345678901")
- '+1 (234) 567-8901'
- >>> make_pretty("12345678901")
- '+1 (234) 567-8901'
- >>> make_pretty("01234567890")
- '+012 (345) 678-90'
- >>> make_pretty("+01234567890")
- '+012 (345) 678-90'
- >>> make_pretty("+12")
- '+1 (2)'
- >>> make_pretty("+123")
- '+1 (23)'
- >>> make_pretty("+1234")
- '+1 (234)'
- """
- if phonenumber is None or phonenumber == "":
- return ""
-
- phonenumber = normalize_number(phonenumber)
-
- if phonenumber == "":
- return ""
- elif phonenumber[0] == "+":
- prettynumber = _make_pretty_international(phonenumber[1:])
- if not prettynumber.startswith("+"):
- prettynumber = "+"+prettynumber
- elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
- prettynumber = _make_pretty_international(phonenumber)
- elif 7 < len(phonenumber):
- prettynumber = _make_pretty_with_areacode(phonenumber)
- elif 3 < len(phonenumber):
- prettynumber = _make_pretty_local(phonenumber)
- else:
- prettynumber = phonenumber
- return prettynumber.strip()
-
-
-def similar_ugly_numbers(lhs, rhs):
- return (
- lhs == rhs or
- lhs[1:] == rhs and lhs.startswith("1") or
- lhs[2:] == rhs and lhs.startswith("+1") or
- lhs == rhs[1:] and rhs.startswith("1") or
- lhs == rhs[2:] and rhs.startswith("+1")
- )
-
-
-def abbrev_relative_date(date):
- """
- >>> abbrev_relative_date("42 hours ago")
- '42 h'
- >>> abbrev_relative_date("2 days ago")
- '2 d'
- >>> abbrev_relative_date("4 weeks ago")
- '4 w'
- """
- parts = date.split(" ")
- return "%s %s" % (parts[0], parts[1][0])
-
-
-def parse_version(versionText):
- """
- >>> parse_version("0.5.2")
- [0, 5, 2]
- """
- return [
- int(number)
- for number in versionText.split(".")
- ]
-
-
-def compare_versions(leftParsedVersion, rightParsedVersion):
- """
- >>> compare_versions([0, 1, 2], [0, 1, 2])
- 0
- >>> compare_versions([0, 1, 2], [0, 1, 3])
- -1
- >>> compare_versions([0, 1, 2], [0, 2, 2])
- -1
- >>> compare_versions([0, 1, 2], [1, 1, 2])
- -1
- >>> compare_versions([0, 1, 3], [0, 1, 2])
- 1
- >>> compare_versions([0, 2, 2], [0, 1, 2])
- 1
- >>> compare_versions([1, 1, 2], [0, 1, 2])
- 1
- """
- for left, right in zip(leftParsedVersion, rightParsedVersion):
- if left < right:
- return -1
- elif right < left:
- return 1
- else:
- return 0
+++ /dev/null
-#!/usr/bin/env python
-import new
-
-# Make the environment more like Python 3.0
-__metaclass__ = type
-from itertools import izip as zip
-import textwrap
-import inspect
-
-
-__all__ = [
- "AnyType",
- "overloaded"
-]
-
-
-AnyType = object
-
-
-class overloaded:
- """
- Dynamically overloaded functions.
-
- This is an implementation of (dynamically, or run-time) overloaded
- functions; also known as generic functions or multi-methods.
-
- The dispatch algorithm uses the types of all argument for dispatch,
- similar to (compile-time) overloaded functions or methods in C++ and
- Java.
-
- Most of the complexity in the algorithm comes from the need to support
- subclasses in call signatures. For example, if an function is
- registered for a signature (T1, T2), then a call with a signature (S1,
- S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass
- of T2, and there are no other more specific matches (see below).
-
- If there are multiple matches and one of those doesn't *dominate* all
- others, the match is deemed ambiguous and an exception is raised. A
- subtlety here: if, after removing the dominated matches, there are
- still multiple matches left, but they all map to the same function,
- then the match is not deemed ambiguous and that function is used.
- Read the method find_func() below for details.
-
- @note Python 2.5 is required due to the use of predicates any() and all().
- @note only supports positional arguments
-
- @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514
-
- >>> import misc
- >>> misc.validate_decorator (overloaded)
- >>>
- >>>
- >>>
- >>>
- >>> #################
- >>> #Basics, with reusing names and without
- >>> @overloaded
- ... def foo(x):
- ... "prints x"
- ... print x
- ...
- >>> @foo.register(int)
- ... def foo(x):
- ... "prints the hex representation of x"
- ... print hex(x)
- ...
- >>> from types import DictType
- >>> @foo.register(DictType)
- ... def foo_dict(x):
- ... "prints the keys of x"
- ... print [k for k in x.iterkeys()]
- ...
- >>> #combines all of the doc strings to help keep track of the specializations
- >>> foo.__doc__ # doctest: +ELLIPSIS
- "prints x\\n\\n...overloading.foo (<type 'int'>):\\n\\tprints the hex representation of x\\n\\n...overloading.foo_dict (<type 'dict'>):\\n\\tprints the keys of x"
- >>> foo ("text")
- text
- >>> foo (10) #calling the specialized foo
- 0xa
- >>> foo ({3:5, 6:7}) #calling the specialization foo_dict
- [3, 6]
- >>> foo_dict ({3:5, 6:7}) #with using a unique name, you still have the option of calling the function directly
- [3, 6]
- >>>
- >>>
- >>>
- >>>
- >>> #################
- >>> #Multiple arguments, accessing the default, and function finding
- >>> @overloaded
- ... def two_arg (x, y):
- ... print x,y
- ...
- >>> @two_arg.register(int, int)
- ... def two_arg_int_int (x, y):
- ... print hex(x), hex(y)
- ...
- >>> @two_arg.register(float, int)
- ... def two_arg_float_int (x, y):
- ... print x, hex(y)
- ...
- >>> @two_arg.register(int, float)
- ... def two_arg_int_float (x, y):
- ... print hex(x), y
- ...
- >>> two_arg.__doc__ # doctest: +ELLIPSIS
- "...overloading.two_arg_int_int (<type 'int'>, <type 'int'>):\\n\\n...overloading.two_arg_float_int (<type 'float'>, <type 'int'>):\\n\\n...overloading.two_arg_int_float (<type 'int'>, <type 'float'>):"
- >>> two_arg(9, 10)
- 0x9 0xa
- >>> two_arg(9.0, 10)
- 9.0 0xa
- >>> two_arg(15, 16.0)
- 0xf 16.0
- >>> two_arg.default_func(9, 10)
- 9 10
- >>> two_arg.find_func ((int, float)) == two_arg_int_float
- True
- >>> (int, float) in two_arg
- True
- >>> (str, int) in two_arg
- False
- >>>
- >>>
- >>>
- >>> #################
- >>> #wildcard
- >>> @two_arg.register(AnyType, str)
- ... def two_arg_any_str (x, y):
- ... print x, y.lower()
- ...
- >>> two_arg("Hello", "World")
- Hello world
- >>> two_arg(500, "World")
- 500 world
- """
-
- def __init__(self, default_func):
- # Decorator to declare new overloaded function.
- self.registry = {}
- self.cache = {}
- self.default_func = default_func
- self.__name__ = self.default_func.__name__
- self.__doc__ = self.default_func.__doc__
- self.__dict__.update (self.default_func.__dict__)
-
- def __get__(self, obj, type=None):
- if obj is None:
- return self
- return new.instancemethod(self, obj)
-
- def register(self, *types):
- """
- Decorator to register an implementation for a specific set of types.
-
- .register(t1, t2)(f) is equivalent to .register_func((t1, t2), f).
- """
-
- def helper(func):
- self.register_func(types, func)
-
- originalDoc = self.__doc__ if self.__doc__ is not None else ""
- typeNames = ", ".join ([str(type) for type in types])
- typeNames = "".join ([func.__module__+".", func.__name__, " (", typeNames, "):"])
- overloadedDoc = ""
- if func.__doc__ is not None:
- overloadedDoc = textwrap.fill (func.__doc__, width=60, initial_indent="\t", subsequent_indent="\t")
- self.__doc__ = "\n".join ([originalDoc, "", typeNames, overloadedDoc]).strip()
-
- new_func = func
-
- #Masking the function, so we want to take on its traits
- if func.__name__ == self.__name__:
- self.__dict__.update (func.__dict__)
- new_func = self
- return new_func
-
- return helper
-
- def register_func(self, types, func):
- """Helper to register an implementation."""
- self.registry[tuple(types)] = func
- self.cache = {} # Clear the cache (later we can optimize this).
-
- def __call__(self, *args):
- """Call the overloaded function."""
- types = tuple(map(type, args))
- func = self.cache.get(types)
- if func is None:
- self.cache[types] = func = self.find_func(types)
- return func(*args)
-
- def __contains__ (self, types):
- return self.find_func(types) is not self.default_func
-
- def find_func(self, types):
- """Find the appropriate overloaded function; don't call it.
-
- @note This won't work for old-style classes or classes without __mro__
- """
- func = self.registry.get(types)
- if func is not None:
- # Easy case -- direct hit in registry.
- return func
-
- # Phillip Eby suggests to use issubclass() instead of __mro__.
- # There are advantages and disadvantages.
-
- # I can't help myself -- this is going to be intense functional code.
- # Find all possible candidate signatures.
- mros = tuple(inspect.getmro(t) for t in types)
- n = len(mros)
- candidates = [sig for sig in self.registry
- if len(sig) == n and
- all(t in mro for t, mro in zip(sig, mros))]
-
- if not candidates:
- # No match at all -- use the default function.
- return self.default_func
- elif len(candidates) == 1:
- # Unique match -- that's an easy case.
- return self.registry[candidates[0]]
-
- # More than one match -- weed out the subordinate ones.
-
- def dominates(dom, sub,
- orders=tuple(dict((t, i) for i, t in enumerate(mro))
- for mro in mros)):
- # Predicate to decide whether dom strictly dominates sub.
- # Strict domination is defined as domination without equality.
- # The arguments dom and sub are type tuples of equal length.
- # The orders argument is a precomputed auxiliary data structure
- # giving dicts of ordering information corresponding to the
- # positions in the type tuples.
- # A type d dominates a type s iff order[d] <= order[s].
- # A type tuple (d1, d2, ...) dominates a type tuple of equal length
- # (s1, s2, ...) iff d1 dominates s1, d2 dominates s2, etc.
- if dom is sub:
- return False
- return all(order[d] <= order[s] for d, s, order in zip(dom, sub, orders))
-
- # I suppose I could inline dominates() but it wouldn't get any clearer.
- candidates = [cand
- for cand in candidates
- if not any(dominates(dom, cand) for dom in candidates)]
- if len(candidates) == 1:
- # There's exactly one candidate left.
- return self.registry[candidates[0]]
-
- # Perhaps these multiple candidates all have the same implementation?
- funcs = set(self.registry[cand] for cand in candidates)
- if len(funcs) == 1:
- return funcs.pop()
-
- # No, the situation is irreducibly ambiguous.
- raise TypeError("ambigous call; types=%r; candidates=%r" %
- (types, candidates))
+++ /dev/null
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class QThread44(QtCore.QThread):
- """
- This is to imitate QThread in Qt 4.4+ for when running on older version
- See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
- (On Lucid I have Qt 4.7 and this is still an issue)
- """
-
- def __init__(self, parent = None):
- QtCore.QThread.__init__(self, parent)
-
- def run(self):
- self.exec_()
-
-
-class _WorkerThread(QtCore.QObject):
-
- _taskComplete = qt_compat.Signal(object)
-
- def __init__(self, futureThread):
- QtCore.QObject.__init__(self)
- self._futureThread = futureThread
- self._futureThread._addTask.connect(self._on_task_added)
- self._taskComplete.connect(self._futureThread._on_task_complete)
-
- @qt_compat.Slot(object)
- def _on_task_added(self, task):
- self.__on_task_added(task)
-
- @misc.log_exception(_moduleLogger)
- def __on_task_added(self, task):
- if not self._futureThread._isRunning:
- _moduleLogger.error("Dropping task")
-
- func, args, kwds, on_success, on_error = task
-
- try:
- result = func(*args, **kwds)
- isError = False
- except Exception, e:
- _moduleLogger.error("Error, passing it back to the main thread")
- result = e
- isError = True
-
- taskResult = on_success, on_error, isError, result
- self._taskComplete.emit(taskResult)
-
-
-class FutureThread(QtCore.QObject):
-
- _addTask = qt_compat.Signal(object)
-
- def __init__(self):
- QtCore.QObject.__init__(self)
- self._thread = QThread44()
- self._isRunning = False
- self._worker = _WorkerThread(self)
- self._worker.moveToThread(self._thread)
-
- def start(self):
- self._thread.start()
- self._isRunning = True
-
- def stop(self):
- self._isRunning = False
- self._thread.quit()
-
- def add_task(self, func, args, kwds, on_success, on_error):
- assert self._isRunning, "Task queue not started"
- task = func, args, kwds, on_success, on_error
- self._addTask.emit(task)
-
- @qt_compat.Slot(object)
- def _on_task_complete(self, taskResult):
- self.__on_task_complete(taskResult)
-
- @misc.log_exception(_moduleLogger)
- def __on_task_complete(self, taskResult):
- on_success, on_error, isError, result = taskResult
- if not self._isRunning:
- if isError:
- _moduleLogger.error("Masking: %s" % (result, ))
- isError = True
- result = StopIteration("Cancelling all callbacks")
- callback = on_success if not isError else on_error
- try:
- callback(result)
- except Exception:
- _moduleLogger.exception("Callback errored")
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-#try:
-# import PySide.QtCore as _QtCore
-# QtCore = _QtCore
-# USES_PYSIDE = True
-#except ImportError:
-if True:
- import sip
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
- import PyQt4.QtCore as _QtCore
- QtCore = _QtCore
- USES_PYSIDE = False
-
-
-def _pyside_import_module(moduleName):
- pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
- return getattr(pyside, moduleName)
-
-
-def _pyqt4_import_module(moduleName):
- pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
- return getattr(pyside, moduleName)
-
-
-if USES_PYSIDE:
- import_module = _pyside_import_module
-
- Signal = QtCore.Signal
- Slot = QtCore.Slot
- Property = QtCore.Property
-else:
- import_module = _pyqt4_import_module
-
- Signal = QtCore.pyqtSignal
- Slot = QtCore.pyqtSlot
- Property = QtCore.pyqtProperty
-
-
-if __name__ == "__main__":
- pass
-
+++ /dev/null
-#!/usr/bin/env python
-
-import math
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_TWOPI = 2 * math.pi
-
-
-def _radius_at(center, pos):
- delta = pos - center
- xDelta = delta.x()
- yDelta = delta.y()
-
- radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
- return radius
-
-
-def _angle_at(center, pos):
- delta = pos - center
- xDelta = delta.x()
- yDelta = delta.y()
-
- radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
- angle = math.acos(xDelta / radius)
- if 0 <= yDelta:
- angle = _TWOPI - angle
-
- return angle
-
-
-class QActionPieItem(object):
-
- def __init__(self, action, weight = 1):
- self._action = action
- self._weight = weight
-
- def action(self):
- return self._action
-
- def setWeight(self, weight):
- self._weight = weight
-
- def weight(self):
- return self._weight
-
- def setEnabled(self, enabled = True):
- self._action.setEnabled(enabled)
-
- def isEnabled(self):
- return self._action.isEnabled()
-
-
-class PieFiling(object):
-
- INNER_RADIUS_DEFAULT = 64
- OUTER_RADIUS_DEFAULT = 192
-
- SELECTION_CENTER = -1
- SELECTION_NONE = -2
-
- NULL_CENTER = QActionPieItem(QtGui.QAction(None))
-
- def __init__(self):
- self._innerRadius = self.INNER_RADIUS_DEFAULT
- self._outerRadius = self.OUTER_RADIUS_DEFAULT
- self._children = []
- self._center = self.NULL_CENTER
-
- self._cacheIndexToAngle = {}
- self._cacheTotalWeight = 0
-
- def insertItem(self, item, index = -1):
- self._children.insert(index, item)
- self._invalidate_cache()
-
- def removeItemAt(self, index):
- item = self._children.pop(index)
- self._invalidate_cache()
-
- def set_center(self, item):
- if item is None:
- item = self.NULL_CENTER
- self._center = item
-
- def center(self):
- return self._center
-
- def clear(self):
- del self._children[:]
- self._center = self.NULL_CENTER
- self._invalidate_cache()
-
- def itemAt(self, index):
- return self._children[index]
-
- def indexAt(self, center, point):
- return self._angle_to_index(_angle_at(center, point))
-
- def innerRadius(self):
- return self._innerRadius
-
- def setInnerRadius(self, radius):
- self._innerRadius = radius
-
- def outerRadius(self):
- return self._outerRadius
-
- def setOuterRadius(self, radius):
- self._outerRadius = radius
-
- def __iter__(self):
- return iter(self._children)
-
- def __len__(self):
- return len(self._children)
-
- def __getitem__(self, index):
- return self._children[index]
-
- def _invalidate_cache(self):
- self._cacheIndexToAngle.clear()
- self._cacheTotalWeight = sum(child.weight() for child in self._children)
- if self._cacheTotalWeight == 0:
- self._cacheTotalWeight = 1
-
- def _index_to_angle(self, index, isShifted):
- key = index, isShifted
- if key in self._cacheIndexToAngle:
- return self._cacheIndexToAngle[key]
- index = index % len(self._children)
-
- baseAngle = _TWOPI / self._cacheTotalWeight
-
- angle = math.pi / 2
- if isShifted:
- if self._children:
- angle -= (self._children[0].weight() * baseAngle) / 2
- else:
- angle -= baseAngle / 2
- while angle < 0:
- angle += _TWOPI
-
- for i, child in enumerate(self._children):
- if index < i:
- break
- angle += child.weight() * baseAngle
- while _TWOPI < angle:
- angle -= _TWOPI
-
- self._cacheIndexToAngle[key] = angle
- return angle
-
- def _angle_to_index(self, angle):
- numChildren = len(self._children)
- if numChildren == 0:
- return self.SELECTION_CENTER
-
- baseAngle = _TWOPI / self._cacheTotalWeight
-
- iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
- while iterAngle < 0:
- iterAngle += _TWOPI
-
- oldIterAngle = iterAngle
- for index, child in enumerate(self._children):
- iterAngle += child.weight() * baseAngle
- if oldIterAngle < angle and angle <= iterAngle:
- return index - 1 if index != 0 else numChildren - 1
- elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
- return index - 1 if index != 0 else numChildren - 1
- oldIterAngle = iterAngle
-
-
-class PieArtist(object):
-
- ICON_SIZE_DEFAULT = 48
-
- SHAPE_CIRCLE = "circle"
- SHAPE_SQUARE = "square"
- DEFAULT_SHAPE = SHAPE_SQUARE
-
- BACKGROUND_FILL = "fill"
- BACKGROUND_NOFILL = "no fill"
-
- def __init__(self, filing, background = BACKGROUND_FILL):
- self._filing = filing
-
- self._cachedOuterRadius = self._filing.outerRadius()
- self._cachedInnerRadius = self._filing.innerRadius()
- canvasSize = self._cachedOuterRadius * 2 + 1
- self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
- self._mask = None
- self._backgroundState = background
- self.palette = None
-
- def pieSize(self):
- diameter = self._filing.outerRadius() * 2 + 1
- return QtCore.QSize(diameter, diameter)
-
- def centerSize(self):
- painter = QtGui.QPainter(self._canvas)
- text = self._filing.center().action().text()
- fontMetrics = painter.fontMetrics()
- if text:
- textBoundingRect = fontMetrics.boundingRect(text)
- else:
- textBoundingRect = QtCore.QRect()
- textWidth = textBoundingRect.width()
- textHeight = textBoundingRect.height()
-
- return QtCore.QSize(
- textWidth + self.ICON_SIZE_DEFAULT,
- max(textHeight, self.ICON_SIZE_DEFAULT),
- )
-
- def show(self, palette):
- self.palette = palette
-
- if (
- self._cachedOuterRadius != self._filing.outerRadius() or
- self._cachedInnerRadius != self._filing.innerRadius()
- ):
- self._cachedOuterRadius = self._filing.outerRadius()
- self._cachedInnerRadius = self._filing.innerRadius()
- self._canvas = self._canvas.scaled(self.pieSize())
-
- if self._mask is None:
- self._mask = QtGui.QBitmap(self._canvas.size())
- self._mask.fill(QtCore.Qt.color0)
- self._generate_mask(self._mask)
- self._canvas.setMask(self._mask)
- return self._mask
-
- def hide(self):
- self.palette = None
-
- def paint(self, selectionIndex):
- painter = QtGui.QPainter(self._canvas)
- painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
-
- self.paintPainter(selectionIndex, painter)
-
- return self._canvas
-
- def paintPainter(self, selectionIndex, painter):
- adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1)
-
- numChildren = len(self._filing)
- if numChildren == 0:
- self._paint_center_background(painter, adjustmentRect, selectionIndex)
- self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
- return self._canvas
- else:
- for i in xrange(len(self._filing)):
- self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
-
- self._paint_center_background(painter, adjustmentRect, selectionIndex)
- self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
-
- for i in xrange(len(self._filing)):
- self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex)
-
- def _generate_mask(self, mask):
- """
- Specifies on the mask the shape of the pie menu
- """
- painter = QtGui.QPainter(mask)
- painter.setPen(QtCore.Qt.color1)
- painter.setBrush(QtCore.Qt.color1)
- if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
- painter.drawRect(mask.rect())
- elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
- painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
- else:
- raise NotImplementedError(self.DEFAULT_SHAPE)
-
- def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
- if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
- currentWidth = adjustmentRect.width()
- newWidth = math.sqrt(2) * currentWidth
- dx = (newWidth - currentWidth) / 2
- adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
- elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
- pass
- else:
- raise NotImplementedError(self.DEFAULT_SHAPE)
-
- if self._backgroundState == self.BACKGROUND_NOFILL:
- painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
- painter.setPen(self.palette.highlight().color())
- else:
- if i == selectionIndex and self._filing[i].isEnabled():
- painter.setBrush(self.palette.highlight())
- painter.setPen(self.palette.highlight().color())
- else:
- painter.setBrush(self.palette.window())
- painter.setPen(self.palette.window().color())
-
- a = self._filing._index_to_angle(i, True)
- b = self._filing._index_to_angle(i + 1, True)
- if b < a:
- b += _TWOPI
- size = b - a
- if size < 0:
- size += _TWOPI
-
- startAngleInDeg = (a * 360 * 16) / _TWOPI
- sizeInDeg = (size * 360 * 16) / _TWOPI
- painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
-
- def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex):
- child = self._filing[i]
-
- a = self._filing._index_to_angle(i, True)
- b = self._filing._index_to_angle(i + 1, True)
- if b < a:
- b += _TWOPI
- middleAngle = (a + b) / 2
- averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
-
- sliceX = averageRadius * math.cos(middleAngle)
- sliceY = - averageRadius * math.sin(middleAngle)
-
- piePos = adjustmentRect.center()
- pieX = piePos.x()
- pieY = piePos.y()
- self._paint_label(
- painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
- )
-
- def _paint_label(self, painter, action, isSelected, x, y):
- text = action.text()
- fontMetrics = painter.fontMetrics()
- if text:
- textBoundingRect = fontMetrics.boundingRect(text)
- else:
- textBoundingRect = QtCore.QRect()
- textWidth = textBoundingRect.width()
- textHeight = textBoundingRect.height()
-
- icon = action.icon().pixmap(
- QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
- QtGui.QIcon.Normal,
- QtGui.QIcon.On,
- )
- iconWidth = icon.width()
- iconHeight = icon.width()
- averageWidth = (iconWidth + textWidth)/2
- if not icon.isNull():
- iconRect = QtCore.QRect(
- x - averageWidth,
- y - iconHeight/2,
- iconWidth,
- iconHeight,
- )
-
- painter.drawPixmap(iconRect, icon)
-
- if text:
- if isSelected:
- if action.isEnabled():
- pen = self.palette.highlightedText()
- brush = self.palette.highlight()
- else:
- pen = self.palette.mid()
- brush = self.palette.window()
- else:
- if action.isEnabled():
- pen = self.palette.windowText()
- else:
- pen = self.palette.mid()
- brush = self.palette.window()
-
- leftX = x - averageWidth + iconWidth
- topY = y + textHeight/2
- painter.setPen(pen.color())
- painter.setBrush(brush)
- painter.drawText(leftX, topY, text)
-
- def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
- if self._backgroundState == self.BACKGROUND_NOFILL:
- return
- if len(self._filing) == 0:
- if self._backgroundState == self.BACKGROUND_NOFILL:
- painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
- else:
- if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
- painter.setBrush(self.palette.highlight())
- else:
- painter.setBrush(self.palette.window())
- painter.setPen(self.palette.mid().color())
-
- painter.drawRect(adjustmentRect)
- else:
- dark = self.palette.mid().color()
- light = self.palette.light().color()
- if self._backgroundState == self.BACKGROUND_NOFILL:
- background = QtGui.QBrush(QtCore.Qt.transparent)
- else:
- if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
- background = self.palette.highlight().color()
- else:
- background = self.palette.window().color()
-
- innerRadius = self._cachedInnerRadius
- adjustmentCenterPos = adjustmentRect.center()
- innerRect = QtCore.QRect(
- adjustmentCenterPos.x() - innerRadius,
- adjustmentCenterPos.y() - innerRadius,
- innerRadius * 2 + 1,
- innerRadius * 2 + 1,
- )
-
- painter.setPen(QtCore.Qt.NoPen)
- painter.setBrush(background)
- painter.drawPie(innerRect, 0, 360 * 16)
-
- if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
- pass
- elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
- painter.setPen(QtGui.QPen(dark, 1))
- painter.setBrush(QtCore.Qt.NoBrush)
- painter.drawEllipse(adjustmentRect)
- else:
- raise NotImplementedError(self.DEFAULT_SHAPE)
-
- def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex):
- centerPos = adjustmentRect.center()
- pieX = centerPos.x()
- pieY = centerPos.y()
-
- x = pieX
- y = pieY
-
- self._paint_label(
- painter,
- self._filing.center().action(),
- selectionIndex == PieFiling.SELECTION_CENTER,
- x, y
- )
-
-
-class QPieDisplay(QtGui.QWidget):
-
- def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
- QtGui.QWidget.__init__(self, parent, flags)
- self._filing = filing
- self._artist = PieArtist(self._filing)
- self._selectionIndex = PieFiling.SELECTION_NONE
-
- def popup(self, pos):
- self._update_selection(pos)
- self.show()
-
- def sizeHint(self):
- return self._artist.pieSize()
-
- @misc_utils.log_exception(_moduleLogger)
- def showEvent(self, showEvent):
- mask = self._artist.show(self.palette())
- self.setMask(mask)
-
- QtGui.QWidget.showEvent(self, showEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def hideEvent(self, hideEvent):
- self._artist.hide()
- self._selectionIndex = PieFiling.SELECTION_NONE
- QtGui.QWidget.hideEvent(self, hideEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def paintEvent(self, paintEvent):
- canvas = self._artist.paint(self._selectionIndex)
- offset = (self.size() - canvas.size()) / 2
-
- screen = QtGui.QPainter(self)
- screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
-
- QtGui.QWidget.paintEvent(self, paintEvent)
-
- def selectAt(self, index):
- oldIndex = self._selectionIndex
- self._selectionIndex = index
- if self.isVisible():
- self.update()
-
-
-class QPieButton(QtGui.QWidget):
-
- activated = qt_compat.Signal(int)
- highlighted = qt_compat.Signal(int)
- canceled = qt_compat.Signal()
- aboutToShow = qt_compat.Signal()
- aboutToHide = qt_compat.Signal()
-
- BUTTON_RADIUS = 24
- DELAY = 250
-
- def __init__(self, buttonSlice, parent = None, buttonSlices = None):
- # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
- # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
- QtGui.QWidget.__init__(self, parent)
- self._cachedCenterPosition = self.rect().center()
-
- self._filing = PieFiling()
- self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
- self._selectionIndex = PieFiling.SELECTION_NONE
-
- self._buttonFiling = PieFiling()
- self._buttonFiling.set_center(buttonSlice)
- if buttonSlices is not None:
- for slice in buttonSlices:
- self._buttonFiling.insertItem(slice)
- self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
- self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL)
- self._poppedUp = False
- self._pressed = False
-
- self._delayPopupTimer = QtCore.QTimer()
- self._delayPopupTimer.setInterval(self.DELAY)
- self._delayPopupTimer.setSingleShot(True)
- self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
- self._popupLocation = None
-
- self._mousePosition = None
- self.setFocusPolicy(QtCore.Qt.StrongFocus)
- self.setSizePolicy(
- QtGui.QSizePolicy(
- QtGui.QSizePolicy.MinimumExpanding,
- QtGui.QSizePolicy.MinimumExpanding,
- )
- )
-
- def insertItem(self, item, index = -1):
- self._filing.insertItem(item, index)
-
- def removeItemAt(self, index):
- self._filing.removeItemAt(index)
-
- def set_center(self, item):
- self._filing.set_center(item)
-
- def set_button(self, item):
- self.update()
-
- def clear(self):
- self._filing.clear()
-
- def itemAt(self, index):
- return self._filing.itemAt(index)
-
- def indexAt(self, point):
- return self._filing.indexAt(self._cachedCenterPosition, point)
-
- def innerRadius(self):
- return self._filing.innerRadius()
-
- def setInnerRadius(self, radius):
- self._filing.setInnerRadius(radius)
-
- def outerRadius(self):
- return self._filing.outerRadius()
-
- def setOuterRadius(self, radius):
- self._filing.setOuterRadius(radius)
-
- def buttonRadius(self):
- return self._buttonFiling.outerRadius()
-
- def setButtonRadius(self, radius):
- self._buttonFiling.setOuterRadius(radius)
- self._buttonFiling.setInnerRadius(radius / 2)
- self._buttonArtist.show(self.palette())
-
- def minimumSizeHint(self):
- return self._buttonArtist.centerSize()
-
- @misc_utils.log_exception(_moduleLogger)
- def mousePressEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- self._mousePosition = lastMousePos
- self._update_selection(self._cachedCenterPosition)
-
- self.highlighted.emit(self._selectionIndex)
-
- self._display.selectAt(self._selectionIndex)
- self._pressed = True
- self.update()
- self._popupLocation = mouseEvent.globalPos()
- self._delayPopupTimer.start()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_delayed_popup(self):
- assert self._popupLocation is not None, "Widget location abuse"
- self._popup_child(self._popupLocation)
-
- @misc_utils.log_exception(_moduleLogger)
- def mouseMoveEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- if self._mousePosition is None:
- # Absolute
- self._update_selection(lastMousePos)
- else:
- # Relative
- self._update_selection(
- self._cachedCenterPosition + (lastMousePos - self._mousePosition),
- ignoreOuter = True,
- )
-
- if lastSelection != self._selectionIndex:
- self.highlighted.emit(self._selectionIndex)
- self._display.selectAt(self._selectionIndex)
-
- if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
- self._on_delayed_popup()
-
- @misc_utils.log_exception(_moduleLogger)
- def mouseReleaseEvent(self, mouseEvent):
- self._delayPopupTimer.stop()
- self._popupLocation = None
-
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- if self._mousePosition is None:
- # Absolute
- self._update_selection(lastMousePos)
- else:
- # Relative
- self._update_selection(
- self._cachedCenterPosition + (lastMousePos - self._mousePosition),
- ignoreOuter = True,
- )
- self._mousePosition = None
-
- self._activate_at(self._selectionIndex)
- self._pressed = False
- self.update()
- self._hide_child()
-
- @misc_utils.log_exception(_moduleLogger)
- def keyPressEvent(self, keyEvent):
- if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
- self._popup_child(QtGui.QCursor.pos())
- if self._selectionIndex != len(self._filing) - 1:
- nextSelection = self._selectionIndex + 1
- else:
- nextSelection = 0
- self._select_at(nextSelection)
- self._display.selectAt(self._selectionIndex)
- elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
- self._popup_child(QtGui.QCursor.pos())
- if 0 < self._selectionIndex:
- nextSelection = self._selectionIndex - 1
- else:
- nextSelection = len(self._filing) - 1
- self._select_at(nextSelection)
- self._display.selectAt(self._selectionIndex)
- elif keyEvent.key() in [QtCore.Qt.Key_Space]:
- self._popup_child(QtGui.QCursor.pos())
- self._select_at(PieFiling.SELECTION_CENTER)
- self._display.selectAt(self._selectionIndex)
- elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
- self._delayPopupTimer.stop()
- self._popupLocation = None
- self._activate_at(self._selectionIndex)
- self._hide_child()
- elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
- self._delayPopupTimer.stop()
- self._popupLocation = None
- self._activate_at(PieFiling.SELECTION_NONE)
- self._hide_child()
- else:
- QtGui.QWidget.keyPressEvent(self, keyEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def resizeEvent(self, resizeEvent):
- self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
- QtGui.QWidget.resizeEvent(self, resizeEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def showEvent(self, showEvent):
- self._buttonArtist.show(self.palette())
- self._cachedCenterPosition = self.rect().center()
-
- QtGui.QWidget.showEvent(self, showEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def hideEvent(self, hideEvent):
- self._display.hide()
- self._select_at(PieFiling.SELECTION_NONE)
- QtGui.QWidget.hideEvent(self, hideEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def paintEvent(self, paintEvent):
- self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
- if self._poppedUp:
- selectionIndex = PieFiling.SELECTION_CENTER
- else:
- selectionIndex = PieFiling.SELECTION_NONE
-
- screen = QtGui.QStylePainter(self)
- screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
- option = QtGui.QStyleOptionButton()
- option.initFrom(self)
- option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
-
- screen.drawControl(QtGui.QStyle.CE_PushButton, option)
- self._buttonArtist.paintPainter(selectionIndex, screen)
-
- QtGui.QWidget.paintEvent(self, paintEvent)
-
- def __iter__(self):
- return iter(self._filing)
-
- def __len__(self):
- return len(self._filing)
-
- def _popup_child(self, position):
- self._poppedUp = True
- self.aboutToShow.emit()
-
- self._delayPopupTimer.stop()
- self._popupLocation = None
-
- position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
- self._display.move(position)
- self._display.show()
-
- self.update()
-
- def _hide_child(self):
- self._poppedUp = False
- self.aboutToHide.emit()
- self._display.hide()
- self.update()
-
- def _select_at(self, index):
- self._selectionIndex = index
-
- def _update_selection(self, lastMousePos, ignoreOuter = False):
- radius = _radius_at(self._cachedCenterPosition, lastMousePos)
- if radius < self._filing.innerRadius():
- self._select_at(PieFiling.SELECTION_CENTER)
- elif radius <= self._filing.outerRadius() or ignoreOuter:
- self._select_at(self.indexAt(lastMousePos))
- else:
- self._select_at(PieFiling.SELECTION_NONE)
-
- def _activate_at(self, index):
- if index == PieFiling.SELECTION_NONE:
- self.canceled.emit()
- return
- elif index == PieFiling.SELECTION_CENTER:
- child = self._filing.center()
- else:
- child = self.itemAt(index)
-
- if child.action().isEnabled():
- child.action().trigger()
- self.activated.emit(index)
- else:
- self.canceled.emit()
-
-
-class QPieMenu(QtGui.QWidget):
-
- activated = qt_compat.Signal(int)
- highlighted = qt_compat.Signal(int)
- canceled = qt_compat.Signal()
- aboutToShow = qt_compat.Signal()
- aboutToHide = qt_compat.Signal()
-
- def __init__(self, parent = None):
- QtGui.QWidget.__init__(self, parent)
- self._cachedCenterPosition = self.rect().center()
-
- self._filing = PieFiling()
- self._artist = PieArtist(self._filing)
- self._selectionIndex = PieFiling.SELECTION_NONE
-
- self._mousePosition = ()
- self.setFocusPolicy(QtCore.Qt.StrongFocus)
-
- def popup(self, pos):
- self._update_selection(pos)
- self.show()
-
- def insertItem(self, item, index = -1):
- self._filing.insertItem(item, index)
- self.update()
-
- def removeItemAt(self, index):
- self._filing.removeItemAt(index)
- self.update()
-
- def set_center(self, item):
- self._filing.set_center(item)
- self.update()
-
- def clear(self):
- self._filing.clear()
- self.update()
-
- def itemAt(self, index):
- return self._filing.itemAt(index)
-
- def indexAt(self, point):
- return self._filing.indexAt(self._cachedCenterPosition, point)
-
- def innerRadius(self):
- return self._filing.innerRadius()
-
- def setInnerRadius(self, radius):
- self._filing.setInnerRadius(radius)
- self.update()
-
- def outerRadius(self):
- return self._filing.outerRadius()
-
- def setOuterRadius(self, radius):
- self._filing.setOuterRadius(radius)
- self.update()
-
- def sizeHint(self):
- return self._artist.pieSize()
-
- @misc_utils.log_exception(_moduleLogger)
- def mousePressEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
- self._mousePosition = lastMousePos
-
- if lastSelection != self._selectionIndex:
- self.highlighted.emit(self._selectionIndex)
- self.update()
-
- @misc_utils.log_exception(_moduleLogger)
- def mouseMoveEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
-
- if lastSelection != self._selectionIndex:
- self.highlighted.emit(self._selectionIndex)
- self.update()
-
- @misc_utils.log_exception(_moduleLogger)
- def mouseReleaseEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
-
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
- self._mousePosition = ()
-
- self._activate_at(self._selectionIndex)
- self.update()
-
- @misc_utils.log_exception(_moduleLogger)
- def keyPressEvent(self, keyEvent):
- if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
- if self._selectionIndex != len(self._filing) - 1:
- nextSelection = self._selectionIndex + 1
- else:
- nextSelection = 0
- self._select_at(nextSelection)
- self.update()
- elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
- if 0 < self._selectionIndex:
- nextSelection = self._selectionIndex - 1
- else:
- nextSelection = len(self._filing) - 1
- self._select_at(nextSelection)
- self.update()
- elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
- self._activate_at(self._selectionIndex)
- elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
- self._activate_at(PieFiling.SELECTION_NONE)
- else:
- QtGui.QWidget.keyPressEvent(self, keyEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def showEvent(self, showEvent):
- self.aboutToShow.emit()
- self._cachedCenterPosition = self.rect().center()
-
- mask = self._artist.show(self.palette())
- self.setMask(mask)
-
- lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
- self._update_selection(lastMousePos)
-
- QtGui.QWidget.showEvent(self, showEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def hideEvent(self, hideEvent):
- self._artist.hide()
- self._selectionIndex = PieFiling.SELECTION_NONE
- QtGui.QWidget.hideEvent(self, hideEvent)
-
- @misc_utils.log_exception(_moduleLogger)
- def paintEvent(self, paintEvent):
- canvas = self._artist.paint(self._selectionIndex)
-
- screen = QtGui.QPainter(self)
- screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
-
- QtGui.QWidget.paintEvent(self, paintEvent)
-
- def __iter__(self):
- return iter(self._filing)
-
- def __len__(self):
- return len(self._filing)
-
- def _select_at(self, index):
- self._selectionIndex = index
-
- def _update_selection(self, lastMousePos):
- radius = _radius_at(self._cachedCenterPosition, lastMousePos)
- if radius < self._filing.innerRadius():
- self._selectionIndex = PieFiling.SELECTION_CENTER
- elif radius <= self._filing.outerRadius():
- self._select_at(self.indexAt(lastMousePos))
- else:
- self._selectionIndex = PieFiling.SELECTION_NONE
-
- def _activate_at(self, index):
- if index == PieFiling.SELECTION_NONE:
- self.canceled.emit()
- self.aboutToHide.emit()
- self.hide()
- return
- elif index == PieFiling.SELECTION_CENTER:
- child = self._filing.center()
- else:
- child = self.itemAt(index)
-
- if child.isEnabled():
- child.action().trigger()
- self.activated.emit(index)
- else:
- self.canceled.emit()
- self.aboutToHide.emit()
- self.hide()
-
-
-def init_pies():
- PieFiling.NULL_CENTER.setEnabled(False)
-
-
-def _print(msg):
- print msg
-
-
-def _on_about_to_hide(app):
- app.exit()
-
-
-if __name__ == "__main__":
- app = QtGui.QApplication([])
- init_pies()
-
- if False:
- pie = QPieMenu()
- pie.show()
-
- if False:
- singleAction = QtGui.QAction(None)
- singleAction.setText("Boo")
- singleItem = QActionPieItem(singleAction)
- spie = QPieMenu()
- spie.insertItem(singleItem)
- spie.show()
-
- if False:
- oneAction = QtGui.QAction(None)
- oneAction.setText("Chew")
- oneItem = QActionPieItem(oneAction)
- twoAction = QtGui.QAction(None)
- twoAction.setText("Foo")
- twoItem = QActionPieItem(twoAction)
- iconTextAction = QtGui.QAction(None)
- iconTextAction.setText("Icon")
- iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
- iconTextItem = QActionPieItem(iconTextAction)
- mpie = QPieMenu()
- mpie.insertItem(oneItem)
- mpie.insertItem(twoItem)
- mpie.insertItem(oneItem)
- mpie.insertItem(iconTextItem)
- mpie.show()
-
- if True:
- oneAction = QtGui.QAction(None)
- oneAction.setText("Chew")
- oneAction.triggered.connect(lambda: _print("Chew"))
- oneItem = QActionPieItem(oneAction)
- twoAction = QtGui.QAction(None)
- twoAction.setText("Foo")
- twoAction.triggered.connect(lambda: _print("Foo"))
- twoItem = QActionPieItem(twoAction)
- iconAction = QtGui.QAction(None)
- iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
- iconAction.triggered.connect(lambda: _print("Icon"))
- iconItem = QActionPieItem(iconAction)
- iconTextAction = QtGui.QAction(None)
- iconTextAction.setText("Icon")
- iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
- iconTextAction.triggered.connect(lambda: _print("Icon and text"))
- iconTextItem = QActionPieItem(iconTextAction)
- mpie = QPieMenu()
- mpie.set_center(iconItem)
- mpie.insertItem(oneItem)
- mpie.insertItem(twoItem)
- mpie.insertItem(oneItem)
- mpie.insertItem(iconTextItem)
- mpie.show()
- mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
- mpie.canceled.connect(lambda: _print("Canceled"))
-
- if False:
- oneAction = QtGui.QAction(None)
- oneAction.setText("Chew")
- oneAction.triggered.connect(lambda: _print("Chew"))
- oneItem = QActionPieItem(oneAction)
- twoAction = QtGui.QAction(None)
- twoAction.setText("Foo")
- twoAction.triggered.connect(lambda: _print("Foo"))
- twoItem = QActionPieItem(twoAction)
- iconAction = QtGui.QAction(None)
- iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
- iconAction.triggered.connect(lambda: _print("Icon"))
- iconItem = QActionPieItem(iconAction)
- iconTextAction = QtGui.QAction(None)
- iconTextAction.setText("Icon")
- iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
- iconTextAction.triggered.connect(lambda: _print("Icon and text"))
- iconTextItem = QActionPieItem(iconTextAction)
- pieFiling = PieFiling()
- pieFiling.set_center(iconItem)
- pieFiling.insertItem(oneItem)
- pieFiling.insertItem(twoItem)
- pieFiling.insertItem(oneItem)
- pieFiling.insertItem(iconTextItem)
- mpie = QPieDisplay(pieFiling)
- mpie.show()
-
- if False:
- oneAction = QtGui.QAction(None)
- oneAction.setText("Chew")
- oneAction.triggered.connect(lambda: _print("Chew"))
- oneItem = QActionPieItem(oneAction)
- twoAction = QtGui.QAction(None)
- twoAction.setText("Foo")
- twoAction.triggered.connect(lambda: _print("Foo"))
- twoItem = QActionPieItem(twoAction)
- iconAction = QtGui.QAction(None)
- iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
- iconAction.triggered.connect(lambda: _print("Icon"))
- iconItem = QActionPieItem(iconAction)
- iconTextAction = QtGui.QAction(None)
- iconTextAction.setText("Icon")
- iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
- iconTextAction.triggered.connect(lambda: _print("Icon and text"))
- iconTextItem = QActionPieItem(iconTextAction)
- mpie = QPieButton(iconItem)
- mpie.set_center(iconItem)
- mpie.insertItem(oneItem)
- mpie.insertItem(twoItem)
- mpie.insertItem(oneItem)
- mpie.insertItem(iconTextItem)
- mpie.show()
- mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
- mpie.canceled.connect(lambda: _print("Canceled"))
-
- app.exec_()
+++ /dev/null
-#!/usr/bin/env python
-
-
-from __future__ import division
-
-import os
-import warnings
-
-import qt_compat
-QtGui = qt_compat.import_module("QtGui")
-
-import qtpie
-
-
-class PieKeyboard(object):
-
- SLICE_CENTER = -1
- SLICE_NORTH = 0
- SLICE_NORTH_WEST = 1
- SLICE_WEST = 2
- SLICE_SOUTH_WEST = 3
- SLICE_SOUTH = 4
- SLICE_SOUTH_EAST = 5
- SLICE_EAST = 6
- SLICE_NORTH_EAST = 7
-
- MAX_ANGULAR_SLICES = 8
-
- SLICE_DIRECTIONS = [
- SLICE_CENTER,
- SLICE_NORTH,
- SLICE_NORTH_WEST,
- SLICE_WEST,
- SLICE_SOUTH_WEST,
- SLICE_SOUTH,
- SLICE_SOUTH_EAST,
- SLICE_EAST,
- SLICE_NORTH_EAST,
- ]
-
- SLICE_DIRECTION_NAMES = [
- "CENTER",
- "NORTH",
- "NORTH_WEST",
- "WEST",
- "SOUTH_WEST",
- "SOUTH",
- "SOUTH_EAST",
- "EAST",
- "NORTH_EAST",
- ]
-
- def __init__(self):
- self._layout = QtGui.QGridLayout()
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._layout)
-
- self.__cells = {}
-
- @property
- def toplevel(self):
- return self._widget
-
- def add_pie(self, row, column, pieButton):
- assert len(pieButton) == 8
- self._layout.addWidget(pieButton, row, column)
- self.__cells[(row, column)] = pieButton
-
- def get_pie(self, row, column):
- return self.__cells[(row, column)]
-
-
-class KeyboardModifier(object):
-
- def __init__(self, name):
- self.name = name
- self.lock = False
- self.once = False
-
- @property
- def isActive(self):
- return self.lock or self.once
-
- def on_toggle_lock(self, *args, **kwds):
- self.lock = not self.lock
-
- def on_toggle_once(self, *args, **kwds):
- self.once = not self.once
-
- def reset_once(self):
- self.once = False
-
-
-def parse_keyboard_data(text):
- return eval(text)
-
-
-def _enumerate_pie_slices(pieData, iconPaths):
- for direction, directionName in zip(
- PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES
- ):
- if directionName in pieData:
- sliceData = pieData[directionName]
-
- action = QtGui.QAction(None)
- try:
- action.setText(sliceData["text"])
- except KeyError:
- pass
- try:
- relativeIconPath = sliceData["path"]
- except KeyError:
- pass
- else:
- for iconPath in iconPaths:
- absIconPath = os.path.join(iconPath, relativeIconPath)
- if os.path.exists(absIconPath):
- action.setIcon(QtGui.QIcon(absIconPath))
- break
- pieItem = qtpie.QActionPieItem(action)
- actionToken = sliceData["action"]
- else:
- pieItem = qtpie.PieFiling.NULL_CENTER
- actionToken = ""
- yield direction, pieItem, actionToken
-
-
-def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths):
- for (row, column), pieData in dataTree.iteritems():
- pieItems = list(_enumerate_pie_slices(pieData, iconPaths))
- assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0]
- _, center, centerAction = pieItems.pop(0)
-
- pieButton = qtpie.QPieButton(center)
- pieButton.set_center(center)
- keyboardHandler.map_slice_action(center, centerAction)
- for direction, pieItem, action in pieItems:
- pieButton.insertItem(pieItem)
- keyboardHandler.map_slice_action(pieItem, action)
- keyboard.add_pie(row, column, pieButton)
-
-
-class KeyboardHandler(object):
-
- def __init__(self, keyhandler):
- self.__keyhandler = keyhandler
- self.__commandHandlers = {}
- self.__modifiers = {}
- self.__sliceActions = {}
-
- self.register_modifier("Shift")
- self.register_modifier("Super")
- self.register_modifier("Control")
- self.register_modifier("Alt")
-
- def register_command_handler(self, command, handler):
- # @todo Look into hooking these up directly to the pie actions
- self.__commandHandlers["[%s]" % command] = handler
-
- def unregister_command_handler(self, command):
- # @todo Look into hooking these up directly to the pie actions
- del self.__commandHandlers["[%s]" % command]
-
- def register_modifier(self, modifierName):
- mod = KeyboardModifier(modifierName)
- self.register_command_handler(modifierName, mod.on_toggle_lock)
- self.__modifiers["<%s>" % modifierName] = mod
-
- def unregister_modifier(self, modifierName):
- self.unregister_command_handler(modifierName)
- del self.__modifiers["<%s>" % modifierName]
-
- def map_slice_action(self, slice, action):
- callback = lambda direction: self(direction, action)
- slice.action().triggered.connect(callback)
- self.__sliceActions[slice] = (action, callback)
-
- def __call__(self, direction, action):
- activeModifiers = [
- mod.name
- for mod in self.__modifiers.itervalues()
- if mod.isActive
- ]
-
- needResetOnce = False
- if action.startswith("[") and action.endswith("]"):
- commandName = action[1:-1]
- if action in self.__commandHandlers:
- self.__commandHandlers[action](commandName, activeModifiers)
- needResetOnce = True
- else:
- warnings.warn("Unknown command: [%s]" % commandName)
- elif action.startswith("<") and action.endswith(">"):
- modName = action[1:-1]
- for mod in self.__modifiers.itervalues():
- if mod.name == modName:
- mod.on_toggle_once()
- break
- else:
- warnings.warn("Unknown modifier: <%s>" % modName)
- else:
- self.__keyhandler(action, activeModifiers)
- needResetOnce = True
-
- if needResetOnce:
- for mod in self.__modifiers.itervalues():
- mod.reset_once()
+++ /dev/null
-import sys
-import contextlib
-import datetime
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-@contextlib.contextmanager
-def notify_error(log):
- try:
- yield
- except:
- log.push_exception()
-
-
-@contextlib.contextmanager
-def notify_busy(log, message):
- log.push_busy(message)
- try:
- yield
- finally:
- log.pop(message)
-
-
-class ErrorMessage(object):
-
- LEVEL_ERROR = 0
- LEVEL_BUSY = 1
- LEVEL_INFO = 2
-
- def __init__(self, message, level):
- self._message = message
- self._level = level
- self._time = datetime.datetime.now()
-
- @property
- def level(self):
- return self._level
-
- @property
- def message(self):
- return self._message
-
- def __repr__(self):
- return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
-
-
-class QErrorLog(QtCore.QObject):
-
- messagePushed = qt_compat.Signal()
- messagePopped = qt_compat.Signal()
-
- def __init__(self):
- QtCore.QObject.__init__(self)
- self._messages = []
-
- def push_busy(self, message):
- _moduleLogger.info("Entering state: %s" % message)
- self._push_message(message, ErrorMessage.LEVEL_BUSY)
-
- def push_message(self, message):
- self._push_message(message, ErrorMessage.LEVEL_INFO)
-
- def push_error(self, message):
- self._push_message(message, ErrorMessage.LEVEL_ERROR)
-
- def push_exception(self):
- userMessage = str(sys.exc_info()[1])
- _moduleLogger.exception(userMessage)
- self.push_error(userMessage)
-
- def pop(self, message = None):
- if message is None:
- del self._messages[0]
- else:
- _moduleLogger.info("Exiting state: %s" % message)
- messageIndex = [
- i
- for (i, error) in enumerate(self._messages)
- if error.message == message
- ]
- # Might be removed out of order
- if messageIndex:
- del self._messages[messageIndex[0]]
- self.messagePopped.emit()
-
- def peek_message(self):
- return self._messages[0]
-
- def _push_message(self, message, level):
- self._messages.append(ErrorMessage(message, level))
- # Sort is defined as stable, so this should be fine
- self._messages.sort(key=lambda x: x.level)
- self.messagePushed.emit()
-
- def __len__(self):
- return len(self._messages)
-
-
-class ErrorDisplay(object):
-
- _SENTINEL_ICON = QtGui.QIcon()
-
- def __init__(self, errorLog):
- self._errorLog = errorLog
- self._errorLog.messagePushed.connect(self._on_message_pushed)
- self._errorLog.messagePopped.connect(self._on_message_popped)
-
- self._icons = None
- self._severityLabel = QtGui.QLabel()
- self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-
- self._message = QtGui.QLabel()
- self._message.setText("Boo")
- self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
- self._message.setWordWrap(True)
-
- self._closeLabel = None
-
- self._controlLayout = QtGui.QHBoxLayout()
- self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
- self._controlLayout.addWidget(self._message, 1000)
-
- self._widget = QtGui.QWidget()
- self._widget.setLayout(self._controlLayout)
- self._widget.hide()
-
- @property
- def toplevel(self):
- return self._widget
-
- def _show_error(self):
- if self._icons is None:
- self._icons = {
- ErrorMessage.LEVEL_BUSY:
- get_theme_icon(
- #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
- ("view-refresh", "general_refresh", "gtk-refresh", )
- ).pixmap(32, 32),
- ErrorMessage.LEVEL_INFO:
- get_theme_icon(
- ("dialog-information", "general_notes", "gtk-info")
- ).pixmap(32, 32),
- ErrorMessage.LEVEL_ERROR:
- get_theme_icon(
- ("dialog-error", "app_install_error", "gtk-dialog-error")
- ).pixmap(32, 32),
- }
- if self._closeLabel is None:
- closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
- if closeIcon is not self._SENTINEL_ICON:
- self._closeLabel = QtGui.QPushButton(closeIcon, "")
- else:
- self._closeLabel = QtGui.QPushButton("X")
- self._closeLabel.clicked.connect(self._on_close)
- self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
- error = self._errorLog.peek_message()
- self._message.setText(error.message)
- self._severityLabel.setPixmap(self._icons[error.level])
- self._widget.show()
-
- @qt_compat.Slot()
- @qt_compat.Slot(bool)
- @misc.log_exception(_moduleLogger)
- def _on_close(self, checked = False):
- self._errorLog.pop()
-
- @qt_compat.Slot()
- @misc.log_exception(_moduleLogger)
- def _on_message_pushed(self):
- self._show_error()
-
- @qt_compat.Slot()
- @misc.log_exception(_moduleLogger)
- def _on_message_popped(self):
- if len(self._errorLog) == 0:
- self._message.setText("")
- self._widget.hide()
- else:
- self._show_error()
-
-
-class QHtmlDelegate(QtGui.QStyledItemDelegate):
-
- UNDEFINED_SIZE = -1
-
- def __init__(self, *args, **kwd):
- QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
- self._width = self.UNDEFINED_SIZE
-
- def paint(self, painter, option, index):
- newOption = QtGui.QStyleOptionViewItemV4(option)
- self.initStyleOption(newOption, index)
- if newOption.widget is not None:
- style = newOption.widget.style()
- else:
- style = QtGui.QApplication.style()
-
- doc = QtGui.QTextDocument()
- doc.setHtml(newOption.text)
- doc.setTextWidth(newOption.rect.width())
-
- newOption.text = ""
- style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
-
- ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
- if newOption.state & QtGui.QStyle.State_Selected:
- ctx.palette.setColor(
- QtGui.QPalette.Text,
- newOption.palette.color(
- QtGui.QPalette.Active,
- QtGui.QPalette.HighlightedText
- )
- )
- else:
- ctx.palette.setColor(
- QtGui.QPalette.Text,
- newOption.palette.color(
- QtGui.QPalette.Active,
- QtGui.QPalette.Text
- )
- )
-
- textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
- painter.save()
- painter.translate(textRect.topLeft())
- painter.setClipRect(textRect.translated(-textRect.topLeft()))
- doc.documentLayout().draw(painter, ctx)
- painter.restore()
-
- def setWidth(self, width, model):
- if self._width == width:
- return
- self._width = width
- for c in xrange(model.rowCount()):
- cItem = model.item(c, 0)
- for r in xrange(model.rowCount()):
- rItem = cItem.child(r, 0)
- rIndex = model.indexFromItem(rItem)
- self.sizeHintChanged.emit(rIndex)
- return
-
- def sizeHint(self, option, index):
- newOption = QtGui.QStyleOptionViewItemV4(option)
- self.initStyleOption(newOption, index)
-
- doc = QtGui.QTextDocument()
- doc.setHtml(newOption.text)
- if self._width != self.UNDEFINED_SIZE:
- width = self._width
- else:
- width = newOption.rect.width()
- doc.setTextWidth(width)
- size = QtCore.QSize(doc.idealWidth(), doc.size().height())
- return size
-
-
-class QSignalingMainWindow(QtGui.QMainWindow):
-
- closed = qt_compat.Signal()
- hidden = qt_compat.Signal()
- shown = qt_compat.Signal()
- resized = qt_compat.Signal()
-
- def __init__(self, *args, **kwd):
- QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
-
- def closeEvent(self, event):
- val = QtGui.QMainWindow.closeEvent(self, event)
- self.closed.emit()
- return val
-
- def hideEvent(self, event):
- val = QtGui.QMainWindow.hideEvent(self, event)
- self.hidden.emit()
- return val
-
- def showEvent(self, event):
- val = QtGui.QMainWindow.showEvent(self, event)
- self.shown.emit()
- return val
-
- def resizeEvent(self, event):
- val = QtGui.QMainWindow.resizeEvent(self, event)
- self.resized.emit()
- return val
-
-def set_current_index(selector, itemText, default = 0):
- for i in xrange(selector.count()):
- if selector.itemText(i) == itemText:
- selector.setCurrentIndex(i)
- break
- else:
- itemText.setCurrentIndex(default)
-
-
-def _null_set_stackable(window, isStackable):
- pass
-
-
-def _maemo_set_stackable(window, isStackable):
- window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
-
-
-try:
- QtCore.Qt.WA_Maemo5StackedWindow
- set_stackable = _maemo_set_stackable
-except AttributeError:
- set_stackable = _null_set_stackable
-
-
-def _null_set_autorient(window, doAutoOrient):
- pass
-
-
-def _maemo_set_autorient(window, doAutoOrient):
- window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
-
-
-try:
- QtCore.Qt.WA_Maemo5AutoOrientation
- set_autorient = _maemo_set_autorient
-except AttributeError:
- set_autorient = _null_set_autorient
-
-
-def screen_orientation():
- geom = QtGui.QApplication.desktop().screenGeometry()
- if geom.width() <= geom.height():
- return QtCore.Qt.Vertical
- else:
- return QtCore.Qt.Horizontal
-
-
-def _null_set_window_orientation(window, orientation):
- pass
-
-
-def _maemo_set_window_orientation(window, orientation):
- if orientation == QtCore.Qt.Vertical:
- window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
- window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
- elif orientation == QtCore.Qt.Horizontal:
- window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
- window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
- elif orientation is None:
- window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
- window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
- else:
- raise RuntimeError("Unknown orientation: %r" % orientation)
-
-
-try:
- QtCore.Qt.WA_Maemo5LandscapeOrientation
- QtCore.Qt.WA_Maemo5PortraitOrientation
- set_window_orientation = _maemo_set_window_orientation
-except AttributeError:
- set_window_orientation = _null_set_window_orientation
-
-
-def _null_show_progress_indicator(window, isStackable):
- pass
-
-
-def _maemo_show_progress_indicator(window, isStackable):
- window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
-
-
-try:
- QtCore.Qt.WA_Maemo5ShowProgressIndicator
- show_progress_indicator = _maemo_show_progress_indicator
-except AttributeError:
- show_progress_indicator = _null_show_progress_indicator
-
-
-def _null_mark_numbers_preferred(widget):
- pass
-
-
-def _newqt_mark_numbers_preferred(widget):
- widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
-
-
-try:
- QtCore.Qt.ImhPreferNumbers
- mark_numbers_preferred = _newqt_mark_numbers_preferred
-except AttributeError:
- mark_numbers_preferred = _null_mark_numbers_preferred
-
-
-def _null_get_theme_icon(iconNames, fallback = None):
- icon = fallback if fallback is not None else QtGui.QIcon()
- return icon
-
-
-def _newqt_get_theme_icon(iconNames, fallback = None):
- for iconName in iconNames:
- if QtGui.QIcon.hasThemeIcon(iconName):
- icon = QtGui.QIcon.fromTheme(iconName)
- break
- else:
- icon = fallback if fallback is not None else QtGui.QIcon()
- return icon
-
-
-try:
- QtGui.QIcon.fromTheme
- get_theme_icon = _newqt_get_theme_icon
-except AttributeError:
- get_theme_icon = _null_get_theme_icon
-
+++ /dev/null
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-from util import qui_utils
-from util import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class ApplicationWrapper(object):
-
- DEFAULT_ORIENTATION = "Default"
- AUTO_ORIENTATION = "Auto"
- LANDSCAPE_ORIENTATION = "Landscape"
- PORTRAIT_ORIENTATION = "Portrait"
-
- def __init__(self, qapp, constants):
- self._constants = constants
- self._qapp = qapp
- self._clipboard = QtGui.QApplication.clipboard()
-
- self._errorLog = qui_utils.QErrorLog()
- self._mainWindow = None
-
- self._fullscreenAction = QtGui.QAction(None)
- self._fullscreenAction.setText("Fullscreen")
- self._fullscreenAction.setCheckable(True)
- self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
- self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
-
- self._orientation = self.DEFAULT_ORIENTATION
- self._orientationAction = QtGui.QAction(None)
- self._orientationAction.setText("Next Orientation")
- self._orientationAction.setCheckable(True)
- self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
- self._orientationAction.triggered.connect(self._on_next_orientation)
-
- self._logAction = QtGui.QAction(None)
- self._logAction.setText("Log")
- self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
- self._logAction.triggered.connect(self._on_log)
-
- self._quitAction = QtGui.QAction(None)
- self._quitAction.setText("Quit")
- self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
- self._quitAction.triggered.connect(self._on_quit)
-
- self._aboutAction = QtGui.QAction(None)
- self._aboutAction.setText("About")
- self._aboutAction.triggered.connect(self._on_about)
-
- self._qapp.lastWindowClosed.connect(self._on_app_quit)
- self._mainWindow = self._new_main_window()
- self._mainWindow.window.destroyed.connect(self._on_child_close)
-
- self.load_settings()
-
- self._mainWindow.show()
- self._idleDelay = QtCore.QTimer()
- self._idleDelay.setSingleShot(True)
- self._idleDelay.setInterval(0)
- self._idleDelay.timeout.connect(self._on_delayed_start)
- self._idleDelay.start()
-
- def load_settings(self):
- raise NotImplementedError("Booh")
-
- def save_settings(self):
- raise NotImplementedError("Booh")
-
- def _new_main_window(self):
- raise NotImplementedError("Booh")
-
- @property
- def qapp(self):
- return self._qapp
-
- @property
- def constants(self):
- return self._constants
-
- @property
- def errorLog(self):
- return self._errorLog
-
- @property
- def fullscreenAction(self):
- return self._fullscreenAction
-
- @property
- def orientationAction(self):
- return self._orientationAction
-
- @property
- def orientation(self):
- return self._orientation
-
- @property
- def logAction(self):
- return self._logAction
-
- @property
- def aboutAction(self):
- return self._aboutAction
-
- @property
- def quitAction(self):
- return self._quitAction
-
- def set_orientation(self, orientation):
- self._orientation = orientation
- self._mainWindow.update_orientation(self._orientation)
-
- @classmethod
- def _next_orientation(cls, current):
- return {
- cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
- cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
- cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
- cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
- }[current]
-
- def _close_windows(self):
- if self._mainWindow is not None:
- self.save_settings()
- self._mainWindow.window.destroyed.disconnect(self._on_child_close)
- self._mainWindow.close()
- self._mainWindow = None
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_delayed_start(self):
- self._mainWindow.start()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_app_quit(self, checked = False):
- if self._mainWindow is not None:
- self.save_settings()
- self._mainWindow.destroy()
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_child_close(self, obj = None):
- if self._mainWindow is not None:
- self.save_settings()
- self._mainWindow = None
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_toggle_fullscreen(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- self._mainWindow.set_fullscreen(checked)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_next_orientation(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- self.set_orientation(self._next_orientation(self._orientation))
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_about(self, checked = True):
- raise NotImplementedError("Booh")
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_log(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- with open(self._constants._user_logpath_, "r") as f:
- logLines = f.xreadlines()
- log = "".join(logLines)
- self._clipboard.setText(log)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_quit(self, checked = False):
- with qui_utils.notify_error(self._errorLog):
- self._close_windows()
-
-
-class WindowWrapper(object):
-
- def __init__(self, parent, app):
- self._app = app
-
- self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog)
-
- self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight)
- self._layout.setContentsMargins(0, 0, 0, 0)
-
- self._superLayout = QtGui.QVBoxLayout()
- self._superLayout.addWidget(self._errorDisplay.toplevel)
- self._superLayout.setContentsMargins(0, 0, 0, 0)
- self._superLayout.addLayout(self._layout)
-
- centralWidget = QtGui.QWidget()
- centralWidget.setLayout(self._superLayout)
- centralWidget.setContentsMargins(0, 0, 0, 0)
-
- self._window = qui_utils.QSignalingMainWindow(parent)
- self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
- qui_utils.set_stackable(self._window, True)
- self._window.setCentralWidget(centralWidget)
-
- self._closeWindowAction = QtGui.QAction(None)
- self._closeWindowAction.setText("Close")
- self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
- self._closeWindowAction.triggered.connect(self._on_close_window)
-
- self._window.addAction(self._closeWindowAction)
- self._window.addAction(self._app.quitAction)
- self._window.addAction(self._app.fullscreenAction)
- self._window.addAction(self._app.orientationAction)
- self._window.addAction(self._app.logAction)
-
- @property
- def window(self):
- return self._window
-
- @property
- def windowOrientation(self):
- geom = self._window.size()
- if geom.width() <= geom.height():
- return QtCore.Qt.Vertical
- else:
- return QtCore.Qt.Horizontal
-
- @property
- def idealWindowOrientation(self):
- if self._app.orientation == self._app.AUTO_ORIENTATION:
- windowOrientation = self.windowOrientation
- elif self._app.orientation == self._app.DEFAULT_ORIENTATION:
- windowOrientation = qui_utils.screen_orientation()
- elif self._app.orientation == self._app.LANDSCAPE_ORIENTATION:
- windowOrientation = QtCore.Qt.Horizontal
- elif self._app.orientation == self._app.PORTRAIT_ORIENTATION:
- windowOrientation = QtCore.Qt.Vertical
- else:
- raise RuntimeError("Bad! No %r for you" % self._app.orientation)
- return windowOrientation
-
- def walk_children(self):
- return ()
-
- def start(self):
- pass
-
- def close(self):
- for child in self.walk_children():
- child.window.destroyed.disconnect(self._on_child_close)
- child.close()
- self._window.close()
-
- def destroy(self):
- pass
-
- def show(self):
- self._window.show()
- for child in self.walk_children():
- child.show()
- self.set_fullscreen(self._app.fullscreenAction.isChecked())
-
- def hide(self):
- for child in self.walk_children():
- child.hide()
- self._window.hide()
-
- def set_fullscreen(self, isFullscreen):
- if self._window.isVisible():
- if isFullscreen:
- self._window.showFullScreen()
- else:
- self._window.showNormal()
- for child in self.walk_children():
- child.set_fullscreen(isFullscreen)
-
- def update_orientation(self, orientation):
- if orientation == self._app.DEFAULT_ORIENTATION:
- qui_utils.set_autorient(self.window, False)
- qui_utils.set_window_orientation(self.window, None)
- elif orientation == self._app.AUTO_ORIENTATION:
- qui_utils.set_autorient(self.window, True)
- qui_utils.set_window_orientation(self.window, None)
- elif orientation == self._app.LANDSCAPE_ORIENTATION:
- qui_utils.set_autorient(self.window, False)
- qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
- elif orientation == self._app.PORTRAIT_ORIENTATION:
- qui_utils.set_autorient(self.window, False)
- qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
- else:
- raise RuntimeError("Unknown orientation: %r" % orientation)
- for child in self.walk_children():
- child.update_orientation(orientation)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_child_close(self, obj = None):
- raise NotImplementedError("Booh")
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_close_window(self, checked = True):
- with qui_utils.notify_error(self._errorLog):
- self.close()
-
-
-class AutoFreezeWindowFeature(object):
-
- def __init__(self, app, window):
- self._app = app
- self._window = window
- self._app.qapp.focusChanged.connect(self._on_focus_changed)
- if self._app.qapp.focusWidget() is not None:
- self._window.setUpdatesEnabled(True)
- else:
- self._window.setUpdatesEnabled(False)
-
- def close(self):
- self._app.qapp.focusChanged.disconnect(self._on_focus_changed)
- self._window.setUpdatesEnabled(True)
-
- @misc_utils.log_exception(_moduleLogger)
- def _on_focus_changed(self, oldWindow, newWindow):
- with qui_utils.notify_error(self._app.errorLog):
- if oldWindow is None and newWindow is not None:
- self._window.setUpdatesEnabled(True)
- elif oldWindow is not None and newWindow is None:
- self._window.setUpdatesEnabled(False)
+++ /dev/null
-from datetime import tzinfo, timedelta, datetime
-
-ZERO = timedelta(0)
-HOUR = timedelta(hours=1)
-
-
-def first_sunday_on_or_after(dt):
- days_to_go = 6 - dt.weekday()
- if days_to_go:
- dt += timedelta(days_to_go)
- return dt
-
-
-# US DST Rules
-#
-# This is a simplified (i.e., wrong for a few cases) set of rules for US
-# DST start and end times. For a complete and up-to-date set of DST rules
-# and timezone definitions, visit the Olson Database (or try pytz):
-# http://www.twinsun.com/tz/tz-link.htm
-# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
-#
-# In the US, since 2007, DST starts at 2am (standard time) on the second
-# Sunday in March, which is the first Sunday on or after Mar 8.
-DSTSTART_2007 = datetime(1, 3, 8, 2)
-# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
-DSTEND_2007 = datetime(1, 11, 1, 1)
-# From 1987 to 2006, DST used to start at 2am (standard time) on the first
-# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
-# Sunday of October, which is the first Sunday on or after Oct 25.
-DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
-DSTEND_1987_2006 = datetime(1, 10, 25, 1)
-# From 1967 to 1986, DST used to start at 2am (standard time) on the last
-# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
-# 1am standard time) on the last Sunday of October, which is the first Sunday
-# on or after Oct 25.
-DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
-DSTEND_1967_1986 = DSTEND_1987_2006
-
-
-class USTimeZone(tzinfo):
-
- def __init__(self, hours, reprname, stdname, dstname):
- self.stdoffset = timedelta(hours=hours)
- self.reprname = reprname
- self.stdname = stdname
- self.dstname = dstname
-
- def __repr__(self):
- return self.reprname
-
- def tzname(self, dt):
- if self.dst(dt):
- return self.dstname
- else:
- return self.stdname
-
- def utcoffset(self, dt):
- return self.stdoffset + self.dst(dt)
-
- def dst(self, dt):
- if dt is None or dt.tzinfo is None:
- # An exception may be sensible here, in one or both cases.
- # It depends on how you want to treat them. The default
- # fromutc() implementation (called by the default astimezone()
- # implementation) passes a datetime with dt.tzinfo is self.
- return ZERO
- assert dt.tzinfo is self
-
- # Find start and end times for US DST. For years before 1967, return
- # ZERO for no DST.
- if 2006 < dt.year:
- dststart, dstend = DSTSTART_2007, DSTEND_2007
- elif 1986 < dt.year < 2007:
- dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
- elif 1966 < dt.year < 1987:
- dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
- else:
- return ZERO
-
- start = first_sunday_on_or_after(dststart.replace(year=dt.year))
- end = first_sunday_on_or_after(dstend.replace(year=dt.year))
-
- # Can't compare naive to aware objects, so strip the timezone from
- # dt first.
- if start <= dt.replace(tzinfo=None) < end:
- return HOUR
- else:
- return ZERO
-
-
-Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
-Central = USTimeZone(-6, "Central", "CST", "CDT")
-Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
-Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
+++ /dev/null
-#!/usr/bin/env python
-
-import logging
-
-import dbus
-import telepathy
-
-import util.go_utils as gobject_utils
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
-
-
-class WasMissedCall(object):
-
- def __init__(self, bus, conn, chan, on_success, on_error):
- self.__on_success = on_success
- self.__on_error = on_error
-
- self._requested = None
- self._didMembersChange = False
- self._didClose = False
- self._didReport = False
-
- self._onTimeout = gobject_utils.Timeout(self._on_timeout)
- self._onTimeout.start(seconds=60)
-
- chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal(
- "MembersChanged",
- self._on_members_changed,
- )
-
- chan[telepathy.interfaces.CHANNEL].connect_to_signal(
- "Closed",
- self._on_closed,
- )
-
- chan[DBUS_PROPERTIES].GetAll(
- telepathy.interfaces.CHANNEL_INTERFACE,
- reply_handler = self._on_got_all,
- error_handler = self._on_error,
- )
-
- def cancel(self):
- self._report_error("by request")
-
- def _report_missed_if_ready(self):
- if self._didReport:
- pass
- elif self._requested is not None and (self._didMembersChange or self._didClose):
- if self._requested:
- self._report_error("wrong direction")
- elif self._didClose:
- self._report_success()
- else:
- self._report_error("members added")
- else:
- if self._didClose:
- self._report_error("closed too early")
-
- def _report_success(self):
- assert not self._didReport, "Double reporting a missed call"
- self._didReport = True
- self._onTimeout.cancel()
- self.__on_success(self)
-
- def _report_error(self, reason):
- assert not self._didReport, "Double reporting a missed call"
- self._didReport = True
- self._onTimeout.cancel()
- self.__on_error(self, reason)
-
- @misc.log_exception(_moduleLogger)
- def _on_got_all(self, properties):
- self._requested = properties["Requested"]
- self._report_missed_if_ready()
-
- @misc.log_exception(_moduleLogger)
- def _on_members_changed(self, message, added, removed, lp, rp, actor, reason):
- if added:
- self._didMembersChange = True
- self._report_missed_if_ready()
-
- @misc.log_exception(_moduleLogger)
- def _on_closed(self):
- self._didClose = True
- self._report_missed_if_ready()
-
- @misc.log_exception(_moduleLogger)
- def _on_error(self, *args):
- self._report_error(args)
-
- @misc.log_exception(_moduleLogger)
- def _on_timeout(self):
- self._report_error("timeout")
- return False
-
-
-class NewChannelSignaller(object):
-
- def __init__(self, on_new_channel):
- self._sessionBus = dbus.SessionBus()
- self._on_user_new_channel = on_new_channel
-
- def start(self):
- self._sessionBus.add_signal_receiver(
- self._on_new_channel,
- "NewChannel",
- "org.freedesktop.Telepathy.Connection",
- None,
- None
- )
-
- def stop(self):
- self._sessionBus.remove_signal_receiver(
- self._on_new_channel,
- "NewChannel",
- "org.freedesktop.Telepathy.Connection",
- None,
- None
- )
-
- @misc.log_exception(_moduleLogger)
- def _on_new_channel(
- self, channelObjectPath, channelType, handleType, handle, supressHandler
- ):
- connObjectPath = channel_path_to_conn_path(channelObjectPath)
- serviceName = path_to_service_name(channelObjectPath)
- try:
- self._on_user_new_channel(
- self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType
- )
- except Exception:
- _moduleLogger.exception("Blocking exception from being passed up")
-
-
-class EnableSystemContactIntegration(object):
-
- ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager"
- ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager"
- ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query"
- ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat"
- ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile"
- DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
-
- def __init__(self, profileName):
- self._bus = dbus.SessionBus()
- self._profileName = profileName
-
- def start(self):
- self._accountManager = self._bus.get_object(
- self.ACCOUNT_MGR_NAME,
- self.ACCOUNT_MGR_PATH,
- )
- self._accountManagerQuery = dbus.Interface(
- self._accountManager,
- dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY,
- )
-
- self._accountManagerQuery.FindAccounts(
- {
- self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName,
- },
- reply_handler = self._on_found_accounts_reply,
- error_handler = self._on_error,
- )
-
- @misc.log_exception(_moduleLogger)
- def _on_found_accounts_reply(self, accountObjectPaths):
- for accountObjectPath in accountObjectPaths:
- print accountObjectPath
- account = self._bus.get_object(
- self.ACCOUNT_MGR_NAME,
- accountObjectPath,
- )
- accountProperties = dbus.Interface(
- account,
- self.DBUS_PROPERTIES,
- )
- accountProperties.Set(
- self.ACCOUNT_IFACE_COMPAT,
- "SecondaryVCardFields",
- ["TEL"],
- reply_handler = self._on_field_set,
- error_handler = self._on_error,
- )
-
- @misc.log_exception(_moduleLogger)
- def _on_field_set(self):
- _moduleLogger.info("SecondaryVCardFields Set")
-
- @misc.log_exception(_moduleLogger)
- def _on_error(self, error):
- _moduleLogger.error("%r" % (error, ))
-
-
-def channel_path_to_conn_path(channelObjectPath):
- """
- >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
- '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME'
- """
- return channelObjectPath.rsplit("/", 1)[0]
-
-
-def path_to_service_name(path):
- """
- >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
- 'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME'
- """
- return ".".join(path[1:].split("/")[0:7])
-
-
-def cm_from_path(path):
- """
- >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
- 'theonering'
- """
- return path[1:].split("/")[4]
+++ /dev/null
-[Desktop Entry]
-Encoding=UTF-8
-Version=1.0
-Type=Application
-Name=DialCentral
-Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py
-Icon=dialcentral
-Categories=Network;InstantMessaging;Qt;