BROKEN: Moved everything
authorEd Page <eopage@byu.net>
Wed, 10 Aug 2011 00:10:18 +0000 (19:10 -0500)
committerEd Page <eopage@byu.net>
Wed, 10 Aug 2011 00:10:18 +0000 (19:10 -0500)
115 files changed:
DialCentral [new file with mode: 0755]
README [deleted file]
data/LICENSE [deleted file]
data/app/LICENSE [new file with mode: 0644]
data/app/bell.flac [new file with mode: 0644]
data/app/bell.wav [new file with mode: 0644]
data/app/contacts.png [new file with mode: 0644]
data/app/dialpad.png [new file with mode: 0644]
data/app/history.png [new file with mode: 0644]
data/app/messages.png [new file with mode: 0644]
data/app/missed.png [new file with mode: 0644]
data/app/placed.png [new file with mode: 0644]
data/app/received.png [new file with mode: 0644]
data/bell.flac [deleted file]
data/bell.wav [deleted file]
data/contacts.png [deleted file]
data/dialcentral-base.svg [new file with mode: 0644]
data/dialcentral.colors [new file with mode: 0644]
data/dialcentral.png [new file with mode: 0644]
data/dialcentral.svg [new file with mode: 0644]
data/dialpad.png [deleted file]
data/history.png [deleted file]
data/messages.png [deleted file]
data/missed.png [deleted file]
data/placed.png [deleted file]
data/received.png [deleted file]
data/template.desktop [new file with mode: 0644]
dialcentral/__init__.py [new file with mode: 0644]
dialcentral/alarm_handler.py [new file with mode: 0644]
dialcentral/alarm_notify.py [new file with mode: 0755]
dialcentral/backends/__init__.py [new file with mode: 0644]
dialcentral/backends/file_backend.py [new file with mode: 0644]
dialcentral/backends/gv_backend.py [new file with mode: 0644]
dialcentral/backends/gvoice/__init__.py [new file with mode: 0644]
dialcentral/backends/gvoice/browser_emu.py [new file with mode: 0644]
dialcentral/backends/gvoice/gvoice.py [new file with mode: 0755]
dialcentral/backends/null_backend.py [new file with mode: 0644]
dialcentral/backends/qt_backend.py [new file with mode: 0644]
dialcentral/call_handler.py [new file with mode: 0644]
dialcentral/constants.py [new file with mode: 0644]
dialcentral/dialcentral_qt.py [new file with mode: 0755]
dialcentral/dialogs.py [new file with mode: 0644]
dialcentral/examples/log_notifier.py [new file with mode: 0644]
dialcentral/examples/sound_notifier.py [new file with mode: 0644]
dialcentral/gv_views.py [new file with mode: 0644]
dialcentral/led_handler.py [new file with mode: 0755]
dialcentral/session.py [new file with mode: 0644]
dialcentral/stream_gst.py [new file with mode: 0644]
dialcentral/stream_handler.py [new file with mode: 0644]
dialcentral/stream_null.py [new file with mode: 0644]
dialcentral/stream_osso.py [new file with mode: 0644]
dialcentral/util/__init__.py [new file with mode: 0644]
dialcentral/util/algorithms.py [new file with mode: 0644]
dialcentral/util/concurrent.py [new file with mode: 0644]
dialcentral/util/coroutines.py [new file with mode: 0755]
dialcentral/util/go_utils.py [new file with mode: 0644]
dialcentral/util/io.py [new file with mode: 0644]
dialcentral/util/linux.py [new file with mode: 0644]
dialcentral/util/misc.py [new file with mode: 0644]
dialcentral/util/overloading.py [new file with mode: 0644]
dialcentral/util/qore_utils.py [new file with mode: 0644]
dialcentral/util/qt_compat.py [new file with mode: 0644]
dialcentral/util/qtpie.py [new file with mode: 0755]
dialcentral/util/qtpieboard.py [new file with mode: 0755]
dialcentral/util/qui_utils.py [new file with mode: 0644]
dialcentral/util/qwrappers.py [new file with mode: 0644]
dialcentral/util/time_utils.py [new file with mode: 0644]
dialcentral/util/tp_utils.py [new file with mode: 0644]
src [new symlink]
src/__init__.py [deleted file]
src/alarm_handler.py [deleted file]
src/alarm_notify.py [deleted file]
src/backends/__init__.py [deleted file]
src/backends/file_backend.py [deleted file]
src/backends/gv_backend.py [deleted file]
src/backends/gvoice/__init__.py [deleted file]
src/backends/gvoice/browser_emu.py [deleted file]
src/backends/gvoice/gvoice.py [deleted file]
src/backends/null_backend.py [deleted file]
src/backends/qt_backend.py [deleted file]
src/call_handler.py [deleted file]
src/constants.py [deleted file]
src/dialcentral.py [deleted file]
src/dialcentral_qt.py [deleted file]
src/dialogs.py [deleted file]
src/examples/log_notifier.py [deleted file]
src/examples/sound_notifier.py [deleted file]
src/gv_views.py [deleted file]
src/led_handler.py [deleted file]
src/session.py [deleted file]
src/stream_gst.py [deleted file]
src/stream_handler.py [deleted file]
src/stream_null.py [deleted file]
src/stream_osso.py [deleted file]
src/util/__init__.py [deleted file]
src/util/algorithms.py [deleted file]
src/util/concurrent.py [deleted file]
src/util/coroutines.py [deleted file]
src/util/go_utils.py [deleted file]
src/util/io.py [deleted file]
src/util/linux.py [deleted file]
src/util/misc.py [deleted file]
src/util/overloading.py [deleted file]
src/util/qore_utils.py [deleted file]
src/util/qt_compat.py [deleted file]
src/util/qtpie.py [deleted file]
src/util/qtpieboard.py [deleted file]
src/util/qui_utils.py [deleted file]
src/util/qwrappers.py [deleted file]
src/util/time_utils.py [deleted file]
src/util/tp_utils.py [deleted file]
support/dialcentral.desktop [deleted file]
support/icons/hicolor/26x26/hildon/dialcentral.png [deleted file]
support/icons/hicolor/64x64/hildon/dialcentral.png [deleted file]
support/icons/hicolor/scalable/hildon/dialcentral.png [deleted file]

diff --git a/DialCentral b/DialCentral
new file mode 100755 (executable)
index 0000000..a20d4fe
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import sys
+
+
+sys.path.append("/opt/dialcentral/lib")
+
+
+import dialcentral_qt
+
+
+if __name__ == "__main__":
+       dialcentral_qt.run()
diff --git a/README b/README
deleted file mode 100644 (file)
index b87ce9c..0000000
--- a/README
+++ /dev/null
@@ -1,37 +0,0 @@
-Building a package
-===================
-Run
-       make PLATFORM=... package
-which will create a "./pkg-.../..." heirarchy.  Move this structure to somewhere on the tablet, then run pypackager. 
-
-Supported PLATFORMs include
-       desktop
-       os2007
-       os2008
-
-SDK Enviroment
-===================
-
-Native
-
-Follow install instructions
-       Ubuntu: http://www.linuxuk.org/node/38
-Install Nokia stuff (for each target)
-       fakeroot apt-get install maemo-explicit
-
-Userful commands
-Login
-       /scratchbox/login
-Change targets
-       sb-conf select DIABLO_ARMEL
-       sb-conf select DIABLO_X86
-Fixing it
-       fakeroot apt-get -f install
-
-Starting scratchbox
-       Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite
-       scratchbox
-       export DISPLAY=:2
-       af-sb-init.sh start
-Then running a command in the "Maemo" terminal will launch it in the Xephyr session
-       Tip: run with "run-standalone.sh" for niceness?
diff --git a/data/LICENSE b/data/LICENSE
deleted file mode 100644 (file)
index fb44a62..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-http://www.gentleface.com/free_icon_set.html
-The Creative Commons Attribution-NonCommercial -- FREE
-http://creativecommons.org/licenses/by-nc-nd/3.0/
-
-Sound:
-http://www.freesound.org/samplesViewSingle.php?id=2166
-http://creativecommons.org/licenses/sampling+/1.0/
-
-placed.png, received.png, placed.png
-Free for commercial use
-http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
diff --git a/data/app/LICENSE b/data/app/LICENSE
new file mode 100644 (file)
index 0000000..fb44a62
--- /dev/null
@@ -0,0 +1,11 @@
+http://www.gentleface.com/free_icon_set.html
+The Creative Commons Attribution-NonCommercial -- FREE
+http://creativecommons.org/licenses/by-nc-nd/3.0/
+
+Sound:
+http://www.freesound.org/samplesViewSingle.php?id=2166
+http://creativecommons.org/licenses/sampling+/1.0/
+
+placed.png, received.png, placed.png
+Free for commercial use
+http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
diff --git a/data/app/bell.flac b/data/app/bell.flac
new file mode 100644 (file)
index 0000000..419420e
Binary files /dev/null and b/data/app/bell.flac differ
diff --git a/data/app/bell.wav b/data/app/bell.wav
new file mode 100644 (file)
index 0000000..6b7fc1b
Binary files /dev/null and b/data/app/bell.wav differ
diff --git a/data/app/contacts.png b/data/app/contacts.png
new file mode 100644 (file)
index 0000000..aa1a7ce
Binary files /dev/null and b/data/app/contacts.png differ
diff --git a/data/app/dialpad.png b/data/app/dialpad.png
new file mode 100644 (file)
index 0000000..b54013b
Binary files /dev/null and b/data/app/dialpad.png differ
diff --git a/data/app/history.png b/data/app/history.png
new file mode 100644 (file)
index 0000000..887989a
Binary files /dev/null and b/data/app/history.png differ
diff --git a/data/app/messages.png b/data/app/messages.png
new file mode 100644 (file)
index 0000000..e117918
Binary files /dev/null and b/data/app/messages.png differ
diff --git a/data/app/missed.png b/data/app/missed.png
new file mode 100644 (file)
index 0000000..34f71c4
Binary files /dev/null and b/data/app/missed.png differ
diff --git a/data/app/placed.png b/data/app/placed.png
new file mode 100644 (file)
index 0000000..329771d
Binary files /dev/null and b/data/app/placed.png differ
diff --git a/data/app/received.png b/data/app/received.png
new file mode 100644 (file)
index 0000000..2b45263
Binary files /dev/null and b/data/app/received.png differ
diff --git a/data/bell.flac b/data/bell.flac
deleted file mode 100644 (file)
index 419420e..0000000
Binary files a/data/bell.flac and /dev/null differ
diff --git a/data/bell.wav b/data/bell.wav
deleted file mode 100644 (file)
index 6b7fc1b..0000000
Binary files a/data/bell.wav and /dev/null differ
diff --git a/data/contacts.png b/data/contacts.png
deleted file mode 100644 (file)
index aa1a7ce..0000000
Binary files a/data/contacts.png and /dev/null differ
diff --git a/data/dialcentral-base.svg b/data/dialcentral-base.svg
new file mode 100644 (file)
index 0000000..aa35390
--- /dev/null
@@ -0,0 +1,148 @@
+<?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>
diff --git a/data/dialcentral.colors b/data/dialcentral.colors
new file mode 100644 (file)
index 0000000..c6b9fa9
--- /dev/null
@@ -0,0 +1,3 @@
+Your icon's dominant color is #72ab40
+A suggested disabled color is #d1ffa9
+A suggested pressed color is #57743e
diff --git a/data/dialcentral.png b/data/dialcentral.png
new file mode 100644 (file)
index 0000000..5f8b811
Binary files /dev/null and b/data/dialcentral.png differ
diff --git a/data/dialcentral.svg b/data/dialcentral.svg
new file mode 100644 (file)
index 0000000..e0d45a8
--- /dev/null
@@ -0,0 +1,56 @@
+<?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>
diff --git a/data/dialpad.png b/data/dialpad.png
deleted file mode 100644 (file)
index b54013b..0000000
Binary files a/data/dialpad.png and /dev/null differ
diff --git a/data/history.png b/data/history.png
deleted file mode 100644 (file)
index 887989a..0000000
Binary files a/data/history.png and /dev/null differ
diff --git a/data/messages.png b/data/messages.png
deleted file mode 100644 (file)
index e117918..0000000
Binary files a/data/messages.png and /dev/null differ
diff --git a/data/missed.png b/data/missed.png
deleted file mode 100644 (file)
index 34f71c4..0000000
Binary files a/data/missed.png and /dev/null differ
diff --git a/data/placed.png b/data/placed.png
deleted file mode 100644 (file)
index 329771d..0000000
Binary files a/data/placed.png and /dev/null differ
diff --git a/data/received.png b/data/received.png
deleted file mode 100644 (file)
index 2b45263..0000000
Binary files a/data/received.png and /dev/null differ
diff --git a/data/template.desktop b/data/template.desktop
new file mode 100644 (file)
index 0000000..3b446d7
--- /dev/null
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=DialCentral
+Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py
+Icon=dialcentral
+Categories=Network;InstantMessaging;Qt;
diff --git a/dialcentral/__init__.py b/dialcentral/__init__.py
new file mode 100644 (file)
index 0000000..4265cc3
--- /dev/null
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/dialcentral/alarm_handler.py b/dialcentral/alarm_handler.py
new file mode 100644 (file)
index 0000000..a79f992
--- /dev/null
@@ -0,0 +1,460 @@
+#!/usr/bin/env python
+
+import os
+import time
+import datetime
+import ConfigParser
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+
+
+_FREMANTLE_ALARM = "Fremantle"
+_DIABLO_ALARM = "Diablo"
+_NO_ALARM = "None"
+
+
+try:
+       import alarm
+       ALARM_TYPE = _FREMANTLE_ALARM
+except (ImportError, OSError):
+       try:
+               import osso.alarmd as alarmd
+               ALARM_TYPE = _DIABLO_ALARM
+       except (ImportError, OSError):
+               ALARM_TYPE = _NO_ALARM
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+def _get_start_time(recurrence):
+       now = datetime.datetime.now()
+       startTimeMinute = now.minute + max(recurrence, 5) # being safe
+       startTimeHour = now.hour + int(startTimeMinute / 60)
+       startTimeMinute = startTimeMinute % 59
+       now.replace(minute=startTimeMinute)
+       timestamp = int(time.mktime(now.timetuple()))
+       return timestamp
+
+
+def _create_recurrence_mask(recurrence, base):
+       """
+       >>> bin(_create_recurrence_mask(60, 60))
+       '0b1'
+       >>> bin(_create_recurrence_mask(30, 60))
+       '0b1000000000000000000000000000001'
+       >>> bin(_create_recurrence_mask(2, 60))
+       '0b10101010101010101010101010101010101010101010101010101010101'
+       >>> bin(_create_recurrence_mask(1, 60))
+       '0b111111111111111111111111111111111111111111111111111111111111'
+       """
+       mask = 0
+       for i in xrange(base / recurrence):
+               mask |= 1 << (recurrence * i)
+       return mask
+
+
+def _unpack_minutes(recurrence):
+       """
+       >>> _unpack_minutes(0)
+       (0, 0, 0)
+       >>> _unpack_minutes(1)
+       (0, 0, 1)
+       >>> _unpack_minutes(59)
+       (0, 0, 59)
+       >>> _unpack_minutes(60)
+       (0, 1, 0)
+       >>> _unpack_minutes(129)
+       (0, 2, 9)
+       >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
+       (5, 3, 2)
+       >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
+       (5, 3, 2)
+       """
+       minutesInAnHour = 60
+       minutesInDay = 24 * minutesInAnHour
+       minutesInAWeek = minutesInDay * 7
+
+       days = recurrence / minutesInDay
+       daysOfWeek = days % 7
+       recurrence -= days * minutesInDay
+       hours = recurrence / minutesInAnHour
+       recurrence -= hours * minutesInAnHour
+       mins = recurrence % minutesInAnHour
+       recurrence -= mins
+       assert recurrence == 0, "Recurrence %d" % recurrence
+       return daysOfWeek, hours, mins
+
+
+class _FremantleAlarmHandler(object):
+
+       _INVALID_COOKIE = -1
+       _REPEAT_FOREVER = -1
+       _TITLE = "Dialcentral Notifications"
+       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+
+       def __init__(self):
+               self._recurrence = 5
+
+               self._alarmCookie = self._INVALID_COOKIE
+               self._launcher = self._LAUNCHER
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._recurrence = config.getint(sectionName, "recurrence")
+                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
+                       launcher = config.get(sectionName, "notifier")
+                       if launcher:
+                               self._launcher = launcher
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+
+       def save_settings(self, config, sectionName):
+               try:
+                       config.set(sectionName, "recurrence", str(self._recurrence))
+                       config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+                       launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+                       config.set(sectionName, "notifier", launcher)
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+
+       def apply_settings(self, enabled, recurrence):
+               if recurrence != self._recurrence or enabled != self.isEnabled:
+                       if self.isEnabled:
+                               self._clear_alarm()
+                       if enabled:
+                               self._set_alarm(recurrence)
+               self._recurrence = int(recurrence)
+
+       @property
+       def recurrence(self):
+               return self._recurrence
+
+       @property
+       def isEnabled(self):
+               return self._alarmCookie != self._INVALID_COOKIE
+
+       def _set_alarm(self, recurrenceMins):
+               assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
+               alarmTime = _get_start_time(recurrenceMins)
+
+               event = alarm.Event()
+               event.appid = self._TITLE
+               event.alarm_time = alarmTime
+               event.recurrences_left = self._REPEAT_FOREVER
+
+               action = event.add_actions(1)[0]
+               action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
+               action.command = self._launcher
+
+               recurrence = event.add_recurrences(1)[0]
+               recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
+               recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
+               recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
+               recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
+               recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
+               recurrence.special |= alarm.RECUR_SPECIAL_NONE
+
+               assert event.is_sane()
+               self._alarmCookie = alarm.add_event(event)
+
+       def _clear_alarm(self):
+               if self._alarmCookie == self._INVALID_COOKIE:
+                       return
+               alarm.delete_event(self._alarmCookie)
+               self._alarmCookie = self._INVALID_COOKIE
+
+
+class _DiabloAlarmHandler(object):
+
+       _INVALID_COOKIE = -1
+       _TITLE = "Dialcentral Notifications"
+       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+       _REPEAT_FOREVER = -1
+
+       def __init__(self):
+               self._recurrence = 5
+
+               bus = dbus.SystemBus()
+               self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
+               self._alarmCookie = self._INVALID_COOKIE
+               self._launcher = self._LAUNCHER
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._recurrence = config.getint(sectionName, "recurrence")
+                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
+                       launcher = config.get(sectionName, "notifier")
+                       if launcher:
+                               self._launcher = launcher
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self._recurrence))
+               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+               launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+               config.set(sectionName, "notifier", launcher)
+
+       def apply_settings(self, enabled, recurrence):
+               if recurrence != self._recurrence or enabled != self.isEnabled:
+                       if self.isEnabled:
+                               self._clear_alarm()
+                       if enabled:
+                               self._set_alarm(recurrence)
+               self._recurrence = int(recurrence)
+
+       @property
+       def recurrence(self):
+               return self._recurrence
+
+       @property
+       def isEnabled(self):
+               return self._alarmCookie != self._INVALID_COOKIE
+
+       def _set_alarm(self, recurrence):
+               assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
+               alarmTime = _get_start_time(recurrence)
+
+               #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
+               _DEFAULT_FLAGS = (
+                       alarmd.ALARM_EVENT_NO_DIALOG |
+                       alarmd.ALARM_EVENT_NO_SNOOZE |
+                       alarmd.ALARM_EVENT_CONNECTED
+               )
+               action = []
+               action.extend(['flags', _DEFAULT_FLAGS])
+               action.extend(['title', self._TITLE])
+               action.extend(['path', self._launcher])
+               action.extend([
+                       'arguments',
+                       dbus.Array(
+                               [alarmTime, int(27)],
+                               signature=dbus.Signature('v')
+                       )
+               ])  #int(27) used in place of alarm_index
+
+               event = []
+               event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
+               event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')])  #use AlarmdActionExec instead of AlarmdActionDbus
+               event.append(dbus.UInt32(len(action) / 2))
+               event.extend(action)
+               event.extend(['time', dbus.Int64(alarmTime)])
+               event.extend(['recurr_interval', dbus.UInt32(recurrence)])
+               event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
+
+               self._alarmCookie = self._alarmdDBus.add_event(*event);
+
+       def _clear_alarm(self):
+               if self._alarmCookie == self._INVALID_COOKIE:
+                       return
+               deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
+               self._alarmCookie = self._INVALID_COOKIE
+               assert deleteResult != -1, "Deleting of alarm event failed"
+
+
+class _ApplicationAlarmHandler(object):
+
+       _REPEAT_FOREVER = -1
+       _MIN_TO_MS_FACTORY = 1000 * 60
+
+       def __init__(self):
+               self._timer = QtCore.QTimer()
+               self._timer.setSingleShot(False)
+               self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+               self._timer.start()
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self.recurrence))
+
+       def apply_settings(self, enabled, recurrence):
+               self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
+               if enabled:
+                       self._timer.start()
+               else:
+                       self._timer.stop()
+
+       @property
+       def notifySignal(self):
+               return self._timer.timeout
+
+       @property
+       def recurrence(self):
+               return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
+
+       @property
+       def isEnabled(self):
+               return self._timer.isActive()
+
+
+class _NoneAlarmHandler(object):
+
+       def __init__(self):
+               self._enabled = False
+               self._recurrence = 5
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._recurrence = config.getint(sectionName, "recurrence")
+                       self._enabled = True
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self.recurrence))
+
+       def apply_settings(self, enabled, recurrence):
+               self._enabled = enabled
+
+       @property
+       def recurrence(self):
+               return self._recurrence
+
+       @property
+       def isEnabled(self):
+               return self._enabled
+
+
+_BACKGROUND_ALARM_FACTORY = {
+       _FREMANTLE_ALARM: _FremantleAlarmHandler,
+       _DIABLO_ALARM: _DiabloAlarmHandler,
+       _NO_ALARM: None,
+}[ALARM_TYPE]
+
+
+class AlarmHandler(object):
+
+       ALARM_NONE = "No Alert"
+       ALARM_BACKGROUND = "Background Alert"
+       ALARM_APPLICATION = "Application Alert"
+       ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
+
+       ALARM_FACTORY = {
+               ALARM_NONE: _NoneAlarmHandler,
+               ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
+               ALARM_APPLICATION: _ApplicationAlarmHandler,
+       }
+
+       def __init__(self):
+               self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
+               self._currentAlarmType = self.ALARM_NONE
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._currentAlarmType = config.get(sectionName, "alarm")
+               except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+                       _moduleLogger.exception("Falling back to old style")
+                       self._currentAlarmType = self.ALARM_BACKGROUND
+               if self._currentAlarmType not in self.ALARM_TYPES:
+                       self._currentAlarmType = self.ALARM_NONE
+
+               self._init_alarm(self._currentAlarmType)
+               if self._currentAlarmType in self._alarms:
+                       self._alarms[self._currentAlarmType].load_settings(config, sectionName)
+                       if not self._alarms[self._currentAlarmType].isEnabled:
+                               _moduleLogger.info("Config file lied, not actually enabled")
+                               self._currentAlarmType = self.ALARM_NONE
+               else:
+                       _moduleLogger.info("Background alerts not supported")
+                       self._currentAlarmType = self.ALARM_NONE
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "alarm", self._currentAlarmType)
+               self._alarms[self._currentAlarmType].save_settings(config, sectionName)
+
+       def apply_settings(self, t, recurrence):
+               self._init_alarm(t)
+               newHandler = self._alarms[t]
+               oldHandler = self._alarms[self._currentAlarmType]
+               if newHandler != oldHandler:
+                       oldHandler.apply_settings(False, 0)
+               newHandler.apply_settings(True, recurrence)
+               self._currentAlarmType = t
+
+       @property
+       def alarmType(self):
+               return self._currentAlarmType
+
+       @property
+       def backgroundNotificationsSupported(self):
+               return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
+
+       @property
+       def applicationNotifySignal(self):
+               self._init_alarm(self.ALARM_APPLICATION)
+               return self._alarms[self.ALARM_APPLICATION].notifySignal
+
+       @property
+       def recurrence(self):
+               return self._alarms[self._currentAlarmType].recurrence
+
+       @property
+       def isEnabled(self):
+               return self._currentAlarmType != self.ALARM_NONE
+
+       def _init_alarm(self, t):
+               if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
+                       self._alarms[t] = self.ALARM_FACTORY[t]()
+
+
+def main():
+       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+       logging.basicConfig(level=logging.DEBUG, format=logFormat)
+       import constants
+       try:
+               import optparse
+       except ImportError:
+               return
+
+       parser = optparse.OptionParser()
+       parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
+       parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
+       (commandOptions, commandArgs) = parser.parse_args()
+
+       alarmHandler = AlarmHandler()
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       alarmHandler.load_settings(config, "alarm")
+
+       if commandOptions.display:
+               print "Alarm (%s) is %s for every %d minutes" % (
+                       alarmHandler._alarmCookie,
+                       "enabled" if alarmHandler.isEnabled else "disabled",
+                       alarmHandler.recurrence,
+               )
+       else:
+               isEnabled = commandOptions.enabled
+               recurrence = commandOptions.recurrence
+               alarmHandler.apply_settings(isEnabled, recurrence)
+
+               alarmHandler.save_settings(config, "alarm")
+               configFile = open(constants._user_settings_, "wb")
+               try:
+                       config.write(configFile)
+               finally:
+                       configFile.close()
+
+
+if __name__ == "__main__":
+       main()
diff --git a/dialcentral/alarm_notify.py b/dialcentral/alarm_notify.py
new file mode 100755 (executable)
index 0000000..bc6240e
--- /dev/null
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+
+import os
+import filecmp
+import ConfigParser
+import pprint
+import logging
+import logging.handlers
+
+import constants
+from backends.gvoice import gvoice
+
+
+def get_missed(backend):
+       missedPage = backend._browser.download(backend._XML_MISSED_URL)
+       missedJson = backend._grab_json(missedPage)
+       return missedJson
+
+
+def get_voicemail(backend):
+       voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL)
+       voicemailJson = backend._grab_json(voicemailPage)
+       return voicemailJson
+
+
+def get_sms(backend):
+       smsPage = backend._browser.download(backend._XML_SMS_URL)
+       smsJson = backend._grab_json(smsPage)
+       return smsJson
+
+
+def remove_reltime(data):
+       for messageData in data["messages"].itervalues():
+               for badPart in [
+                       "relTime",
+                       "relativeStartTime",
+                       "time",
+                       "star",
+                       "isArchived",
+                       "isRead",
+                       "isSpam",
+                       "isTrash",
+                       "labels",
+               ]:
+                       if badPart in messageData:
+                               del messageData[badPart]
+       for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
+               if globalBad in data:
+                       del data[globalBad]
+
+
+def is_type_changed(backend, type, get_material):
+       jsonMaterial = get_material(backend)
+       unreadCount = jsonMaterial["unreadCounts"][type]
+
+       previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
+       currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
+
+       try:
+               os.remove(previousSnapshotPath)
+       except OSError, e:
+               # check if failed purely because the old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+       try:
+               os.rename(currentSnapshotPath, previousSnapshotPath)
+               previousExists = True
+       except OSError, e:
+               # check if failed purely because the new old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+               previousExists = False
+
+       remove_reltime(jsonMaterial)
+       textMaterial = pprint.pformat(jsonMaterial)
+       currentSnapshot = file(currentSnapshotPath, "w")
+       try:
+               currentSnapshot.write(textMaterial)
+       finally:
+               currentSnapshot.close()
+
+       if unreadCount == 0 or not previousExists:
+               return False
+
+       seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
+       return not seemEqual
+
+
+def create_backend(config):
+       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+       backend = gvoice.GVoiceBackend(gvCookiePath)
+
+       loggedIn = False
+
+       if not loggedIn:
+               loggedIn = backend.refresh_account_info() is not None
+
+       if not loggedIn:
+               import base64
+               try:
+                       blobs = (
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(2)
+                       )
+                       creds = (
+                               base64.b64decode(blob)
+                               for blob in blobs
+                       )
+                       username, password = tuple(creds)
+                       loggedIn = backend.login(username, password) is not None
+               except ConfigParser.NoOptionError, e:
+                       pass
+               except ConfigParser.NoSectionError, e:
+                       pass
+
+       assert loggedIn
+       return backend
+
+
+def is_changed(config, backend):
+       try:
+               notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+               notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+               notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+       except ConfigParser.NoOptionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+       except ConfigParser.NoSectionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+       logging.debug(
+               "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms)
+       )
+
+       notifySources = []
+       if notifyOnMissed:
+               notifySources.append(("missed", get_missed))
+       if notifyOnVoicemail:
+               notifySources.append(("voicemail", get_voicemail))
+       if notifyOnSms:
+               notifySources.append(("sms", get_sms))
+
+       notifyUser = False
+       for type, get_material in notifySources:
+               if is_type_changed(backend, type, get_material):
+                       notifyUser = True
+       return notifyUser
+
+
+def notify_on_change():
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       backend = create_backend(config)
+       notifyUser = is_changed(config, backend)
+
+       if notifyUser:
+               logging.info("Changed")
+               import led_handler
+               led = led_handler.LedHandler()
+               led.on()
+       else:
+               logging.info("No Change")
+
+
+if __name__ == "__main__":
+       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+       logging.basicConfig(level=logging.DEBUG, format=logFormat)
+       rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
+       rotating.setFormatter(logging.Formatter(logFormat))
+       root = logging.getLogger()
+       root.addHandler(rotating)
+       logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
+       logging.info("OS: %s" % (os.uname()[0], ))
+       logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+       logging.info("Hostname: %s" % os.uname()[1])
+       try:
+               notify_on_change()
+       except:
+               logging.exception("Error")
+               raise
diff --git a/dialcentral/backends/__init__.py b/dialcentral/backends/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dialcentral/backends/file_backend.py b/dialcentral/backends/file_backend.py
new file mode 100644 (file)
index 0000000..9f8927a
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Filesystem backend for contact support
+"""
+
+from __future__ import with_statement
+
+import os
+import csv
+
+
+def try_unicode(s):
+       try:
+               return s.decode("UTF-8")
+       except UnicodeDecodeError:
+               return s
+
+
+class CsvAddressBook(object):
+       """
+       Currently supported file format
+       @li Has the first line as a header
+       @li Escapes with quotes
+       @li Comma as delimiter
+       @li Column 0 is name, column 1 is number
+       """
+
+       def __init__(self, name, csvPath):
+               self._name = name
+               self._csvPath = csvPath
+               self._contacts = {}
+
+       @property
+       def name(self):
+               return self._name
+
+       def update_account(self, force = True):
+               if not force or not self._contacts:
+                       return
+               self._contacts = dict(
+                       self._read_csv(self._csvPath)
+               )
+
+       def get_contacts(self):
+               """
+               @returns Iterable of (contact id, contact name)
+               """
+               if not self._contacts:
+                       self._contacts = dict(
+                               self._read_csv(self._csvPath)
+                       )
+               return self._contacts
+
+       def _read_csv(self, csvPath):
+               try:
+                       f = open(csvPath, "rU")
+                       csvReader = iter(csv.reader(f))
+               except IOError, e:
+                       if e.errno == 2:
+                               return
+                       raise
+
+               header = csvReader.next()
+               nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
+
+               yieldCount = 0
+               for row in csvReader:
+                       contactDetails = []
+                       for (phoneType, phoneColumn) in phoneColumns:
+                               try:
+                                       if len(row[phoneColumn]) == 0:
+                                               continue
+                                       contactDetails.append({
+                                               "phoneType": try_unicode(phoneType),
+                                               "phoneNumber": row[phoneColumn],
+                                       })
+                               except IndexError:
+                                       pass
+                       if 0 < len(contactDetails):
+                               nameParts = (row[i].strip() for i in nameColumns)
+                               nameParts = (part for part in nameParts if part)
+                               fullName = " ".join(nameParts).strip()
+                               if not fullName:
+                                       for fallbackColumn in nameFallbacks:
+                                               if row[fallbackColumn].strip():
+                                                       fullName = row[fallbackColumn].strip()
+                                                       break
+                                       else:
+                                               fullName = "Unknown"
+                               fullName = try_unicode(fullName)
+                               yield str(yieldCount), {
+                                       "contactId": "%s-%d" % (self._name, yieldCount),
+                                       "name": fullName,
+                                       "numbers": contactDetails,
+                               }
+                               yieldCount += 1
+
+       @classmethod
+       def _guess_columns(cls, row):
+               firstMiddleLast = [-1, -1, -1]
+               names = []
+               nameFallbacks = []
+               phones = []
+               for i, item in enumerate(row):
+                       lowerItem = item.lower()
+                       if 0 <= lowerItem.find("name"):
+                               names.append((item, i))
+
+                               if 0 <= lowerItem.find("couple"):
+                                       names.insert(0, (item, i))
+
+                               if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"):
+                                       firstMiddleLast[0] = i
+                               elif 0 <= lowerItem.find("middle"):
+                                       firstMiddleLast[1] = i
+                               elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"):
+                                       firstMiddleLast[2] = i
+                       elif 0 <= lowerItem.find("phone"):
+                               phones.append((item, i))
+                       elif 0 <= lowerItem.find("mobile"):
+                               phones.append((item, i))
+                       elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"):
+                               nameFallbacks.append(i)
+               if len(names) == 0:
+                       names.append(("Name", 0))
+               if len(phones) == 0:
+                       phones.append(("Phone", 1))
+
+               nameColumns = [i for i in firstMiddleLast if 0 <= i]
+               if len(nameColumns) < 2:
+                       del nameColumns[:]
+                       nameColumns.append(names[0][1])
+
+               return nameColumns, nameFallbacks, phones
+
+
+class FilesystemAddressBookFactory(object):
+
+       FILETYPE_SUPPORT = {
+               "csv": CsvAddressBook,
+       }
+
+       def __init__(self, path):
+               self._path = path
+
+       def get_addressbooks(self):
+               for root, dirs, filenames in os.walk(self._path):
+                       for filename in filenames:
+                               try:
+                                       name, ext = filename.rsplit(".", 1)
+                               except ValueError:
+                                       continue
+
+                               try:
+                                       cls = self.FILETYPE_SUPPORT[ext]
+                               except KeyError:
+                                       continue
+                               yield cls(name, os.path.join(root, filename))
diff --git a/dialcentral/backends/gv_backend.py b/dialcentral/backends/gv_backend.py
new file mode 100644 (file)
index 0000000..17bbc90
--- /dev/null
@@ -0,0 +1,321 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Google Voice backend code
+
+Resources
+       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+       http://posttopic.com/topic/google-voice-add-on-development
+"""
+
+from __future__ import with_statement
+
+import itertools
+import logging
+
+from gvoice import gvoice
+
+from util import io as io_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class GVDialer(object):
+
+       MESSAGE_TEXTS = "Text"
+       MESSAGE_VOICEMAILS = "Voicemail"
+       MESSAGE_ALL = "All"
+
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
+       def __init__(self, cookieFile = None):
+               self._gvoice = gvoice.GVoiceBackend(cookieFile)
+               self._texts = []
+               self._voicemails = []
+               self._received = []
+               self._missed = []
+               self._placed = []
+
+       def is_quick_login_possible(self):
+               """
+               @returns True then refresh_account_info might be enough to login, else full login is required
+               """
+               return self._gvoice.is_quick_login_possible()
+
+       def refresh_account_info(self):
+               return self._gvoice.refresh_account_info()
+
+       def login(self, username, password):
+               """
+               Attempt to login to GoogleVoice
+               @returns Whether login was successful or not
+               """
+               return self._gvoice.login(username, password)
+
+       def logout(self):
+               self._texts = []
+               self._voicemails = []
+               self._received = []
+               self._missed = []
+               self._placed = []
+               return self._gvoice.logout()
+
+       def persist(self):
+               return self._gvoice.persist()
+
+       def is_dnd(self):
+               return self._gvoice.is_dnd()
+
+       def set_dnd(self, doNotDisturb):
+               return self._gvoice.set_dnd(doNotDisturb)
+
+       def call(self, outgoingNumber):
+               """
+               This is the main function responsible for initating the callback
+               """
+               return self._gvoice.call(outgoingNumber)
+
+       def cancel(self, outgoingNumber=None):
+               """
+               Cancels a call matching outgoing and forwarding numbers (if given). 
+               Will raise an error if no matching call is being placed
+               """
+               return self._gvoice.cancel(outgoingNumber)
+
+       def send_sms(self, phoneNumbers, message):
+               self._gvoice.send_sms(phoneNumbers, message)
+
+       def search(self, query):
+               """
+               Search your Google Voice Account history for calls, voicemails, and sms
+               Returns ``Folder`` instance containting matching messages
+               """
+               return self._gvoice.search(query)
+
+       def get_feed(self, feed):
+               return self._gvoice.get_feed(feed)
+
+       def download(self, messageId, targetPath):
+               """
+               Download a voicemail or recorded call MP3 matching the given ``msg``
+               which can either be a ``Message`` instance, or a SHA1 identifier. 
+               Message hashes can be found in ``self.voicemail().messages`` for example. 
+               Returns location of saved file.
+               """
+               self._gvoice.download(messageId, targetPath)
+
+       def is_valid_syntax(self, number):
+               """
+               @returns If This number be called ( syntax validation only )
+               """
+               return self._gvoice.is_valid_syntax(number)
+
+       def get_account_number(self):
+               """
+               @returns The GoogleVoice phone number
+               """
+               return self._gvoice.get_account_number()
+
+       def get_callback_numbers(self):
+               """
+               @returns a dictionary mapping call back numbers to descriptions
+               @note These results are cached for 30 minutes.
+               """
+               return self._gvoice.get_callback_numbers()
+
+       def set_callback_number(self, callbacknumber):
+               """
+               Set the number that GoogleVoice calls
+               @param callbacknumber should be a proper 10 digit number
+               """
+               return self._gvoice.set_callback_number(callbacknumber)
+
+       def get_callback_number(self):
+               """
+               @returns Current callback number or None
+               """
+               return self._gvoice.get_callback_number()
+
+       def get_call_history(self, historyType):
+               """
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+               """
+               history = list(self._get_call_history(historyType))
+               history.sort(key=lambda item: item["time"])
+               return history
+
+       def _get_call_history(self, historyType):
+               """
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+               """
+               if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
+                       self._received = list(self._gvoice.get_received_calls())
+                       for item in self._received:
+                               item["action"] = self.HISTORY_RECEIVED
+               if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
+                       self._missed = list(self._gvoice.get_missed_calls())
+                       for item in self._missed:
+                               item["action"] = self.HISTORY_MISSED
+               if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
+                       self._placed = list(self._gvoice.get_placed_calls())
+                       for item in self._placed:
+                               item["action"] = self.HISTORY_PLACED
+               received = self._received
+               missed = self._missed
+               placed = self._placed
+               for item in received:
+                       yield item
+               for item in missed:
+                       yield item
+               for item in placed:
+                       yield item
+
+       def get_messages(self, messageType):
+               messages = list(self._get_messages(messageType))
+               messages.sort(key=lambda message: message["time"])
+               return messages
+
+       def _get_messages(self, messageType):
+               if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
+                       self._voicemails = list(self._gvoice.get_voicemails())
+               if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
+                       self._texts = list(self._gvoice.get_texts())
+               voicemails = self._voicemails
+               smss = self._texts
+
+               conversations = itertools.chain(voicemails, smss)
+               for conversation in conversations:
+                       messages = conversation.messages
+                       messageParts = [
+                               (message.whoFrom, self._format_message(message), message.when)
+                               for message in messages
+                       ]
+
+                       messageDetails = {
+                               "id": conversation.id,
+                               "contactId": conversation.contactId,
+                               "name": conversation.name,
+                               "time": conversation.time,
+                               "relTime": conversation.relTime,
+                               "prettyNumber": conversation.prettyNumber,
+                               "number": conversation.number,
+                               "location": conversation.location,
+                               "messageParts": messageParts,
+                               "type": conversation.type,
+                               "isRead": conversation.isRead,
+                               "isTrash": conversation.isTrash,
+                               "isSpam": conversation.isSpam,
+                               "isArchived": conversation.isArchived,
+                       }
+                       yield messageDetails
+
+       def clear_caches(self):
+               pass
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               yield self, "", ""
+
+       def open_addressbook(self, bookId):
+               return self
+
+       @staticmethod
+       def contact_source_short_name(contactId):
+               return "GV"
+
+       @staticmethod
+       def factory_name():
+               return "Google Voice"
+
+       def _format_message(self, message):
+               messagePartFormat = {
+                       "med1": "<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
diff --git a/dialcentral/backends/gvoice/__init__.py b/dialcentral/backends/gvoice/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dialcentral/backends/gvoice/browser_emu.py b/dialcentral/backends/gvoice/browser_emu.py
new file mode 100644 (file)
index 0000000..4fef6e8
--- /dev/null
@@ -0,0 +1,210 @@
+"""
+@author:         Laszlo Nagy
+@copyright:   (c) 2005 by Szoftver Messias Bt.
+@licence:       BSD style
+
+Objects of the MozillaEmulator class can emulate a browser that is capable of:
+
+       - cookie management
+       - configurable user agent string
+       - GET and POST
+       - multipart POST (send files)
+       - receive content into file
+
+I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it:
+
+       1. Use firefox
+       2. Install and open the livehttpheaders plugin
+       3. Use the website manually with firefox
+       4. Check the GET and POST requests in the livehttpheaders capture window
+       5. Create an instance of the above class and send the same GET and POST requests to the server.
+
+Optional steps:
+
+       - You can change user agent string in the build_opened method
+       - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files
+"""
+
+import urllib2
+import cookielib
+import logging
+
+import socket
+
+
+_moduleLogger = logging.getLogger(__name__)
+socket.setdefaulttimeout(25)
+
+
+def add_proxy(protocol, url, port):
+       proxyInfo = "%s:%s" % (url, port)
+       proxy = urllib2.ProxyHandler(
+               {protocol: proxyInfo}
+       )
+       opener = urllib2.build_opener(proxy)
+       urllib2.install_opener(opener)
+
+
+class MozillaEmulator(object):
+
+       USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
+       #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
+
+       def __init__(self, trycount = 1):
+               """Create a new MozillaEmulator object.
+
+               @param trycount: The download() method will retry the operation if it
+               fails. You can specify -1 for infinite retrying.  A value of 0 means no
+               retrying. A value of 1 means one retry. etc."""
+               self.debug = False
+               self.trycount = trycount
+               self._cookies = cookielib.LWPCookieJar()
+               self._loadedFromCookies = False
+               self._storeCookies = False
+
+       def load_cookies(self, path):
+               assert not self._loadedFromCookies, "Load cookies only once"
+               if path is None:
+                       return
+
+               self._cookies.filename = path
+               try:
+                       self._cookies.load()
+               except cookielib.LoadError:
+                       _moduleLogger.exception("Bad cookie file")
+               except IOError:
+                       _moduleLogger.exception("No cookie file")
+               except Exception, e:
+                       _moduleLogger.exception("Unknown error with cookies")
+               else:
+                       self._loadedFromCookies = True
+               self._storeCookies = True
+
+               return self._loadedFromCookies
+
+       def save_cookies(self):
+               if self._storeCookies:
+                       self._cookies.save()
+
+       def clear_cookies(self):
+               if self._storeCookies:
+                       self._cookies.clear()
+
+       def download(self, url,
+                       postdata = None, extraheaders = None, forbidRedirect = False,
+                       trycount = None, only_head = False,
+               ):
+               """Download an URL with GET or POST methods.
+
+               @param postdata: It can be a string that will be POST-ed to the URL.
+                       When None is given, the method will be GET instead.
+               @param extraheaders: You can add/modify HTTP headers with a dict here.
+               @param forbidRedirect: Set this flag if you do not want to handle
+                       HTTP 301 and 302 redirects.
+               @param trycount: Specify the maximum number of retries here.
+                       0 means no retry on error. Using -1 means infinite retring.
+                       None means the default value (that is self.trycount).
+               @param only_head: Create the openerdirector and return it. In other
+                       words, this will not retrieve any content except HTTP headers.
+
+               @return: The raw HTML page data
+               """
+               _moduleLogger.debug("Performing download of %s" % url)
+
+               if extraheaders is None:
+                       extraheaders = {}
+               if trycount is None:
+                       trycount = self.trycount
+               cnt = 0
+
+               while True:
+                       try:
+                               req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect)
+                               openerdirector = u.open(req)
+                               if self.debug:
+                                       _moduleLogger.info("%r - %r" % (req.get_method(), url))
+                                       _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg))
+                                       _moduleLogger.info("%r" % (openerdirector.headers))
+                               self._cookies.extract_cookies(openerdirector, req)
+                               if only_head:
+                                       return openerdirector
+
+                               return self._read(openerdirector, trycount)
+                       except urllib2.URLError, e:
+                               _moduleLogger.debug("%s: %s" % (e, url))
+                               cnt += 1
+                               if (-1 < trycount) and (trycount < cnt):
+                                       raise
+
+                       # Retry :-)
+                       _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
+
+       def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
+               if extraheaders is None:
+                       extraheaders = {}
+
+               txheaders = {
+                       'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
+                       'Accept-Language': 'en,en-us;q=0.5',
+                       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+                       'User-Agent': self.USER_AGENT,
+               }
+               for key, value in extraheaders.iteritems():
+                       txheaders[key] = value
+               req = urllib2.Request(url, postdata, txheaders)
+               self._cookies.add_cookie_header(req)
+               if forbidRedirect:
+                       redirector = HTTPNoRedirector()
+                       #_moduleLogger.info("Redirection disabled")
+               else:
+                       redirector = urllib2.HTTPRedirectHandler()
+                       #_moduleLogger.info("Redirection enabled")
+
+               http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
+               https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
+
+               u = urllib2.build_opener(
+                       http_handler,
+                       https_handler,
+                       urllib2.HTTPCookieProcessor(self._cookies),
+                       redirector
+               )
+               if not postdata is None:
+                       req.add_data(postdata)
+               return (req, u)
+
+       def _read(self, openerdirector, trycount):
+               chunks = []
+
+               chunk = openerdirector.read()
+               chunks.append(chunk)
+               #while chunk and cnt < trycount:
+               #       time.sleep(1)
+               #       cnt += 1
+               #       chunk = openerdirector.read()
+               #       chunks.append(chunk)
+
+               data = "".join(chunks)
+
+               if "Content-Length" in openerdirector.info():
+                       assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
+                               openerdirector.info()["Content-Length"],
+                               len(data),
+                       )
+
+               return data
+
+
+class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
+       """This is a custom http redirect handler that FORBIDS redirection."""
+
+       def http_error_302(self, req, fp, code, msg, headers):
+               e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
+               if e.code in (301, 302):
+                       if 'location' in headers:
+                               newurl = headers.getheaders('location')[0]
+                       elif 'uri' in headers:
+                               newurl = headers.getheaders('uri')[0]
+                       e.newurl = newurl
+               _moduleLogger.info("New url: %s" % e.newurl)
+               raise e
diff --git a/dialcentral/backends/gvoice/gvoice.py b/dialcentral/backends/gvoice/gvoice.py
new file mode 100755 (executable)
index 0000000..b0825ef
--- /dev/null
@@ -0,0 +1,1050 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Google Voice backend code
+
+Resources
+       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+       http://posttopic.com/topic/google-voice-add-on-development
+"""
+
+from __future__ import with_statement
+
+import os
+import re
+import urllib
+import urllib2
+import time
+import datetime
+import itertools
+import logging
+import inspect
+
+from xml.sax import saxutils
+from xml.etree import ElementTree
+
+try:
+       import simplejson as _simplejson
+       simplejson = _simplejson
+except ImportError:
+       simplejson = None
+
+import browser_emu
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class NetworkError(RuntimeError):
+       pass
+
+
+class MessageText(object):
+
+       ACCURACY_LOW = "med1"
+       ACCURACY_MEDIUM = "med2"
+       ACCURACY_HIGH = "high"
+
+       def __init__(self):
+               self.accuracy = None
+               self.text = None
+
+       def __str__(self):
+               return self.text
+
+       def to_dict(self):
+               return to_dict(self)
+
+       def __eq__(self, other):
+               return self.accuracy == other.accuracy and self.text == other.text
+
+
+class Message(object):
+
+       def __init__(self):
+               self.whoFrom = None
+               self.body = None
+               self.when = None
+
+       def __str__(self):
+               return "%s (%s): %s" % (
+                       self.whoFrom,
+                       self.when,
+                       "".join(unicode(part) for part in self.body)
+               )
+
+       def to_dict(self):
+               selfDict = to_dict(self)
+               selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
+               return selfDict
+
+       def __eq__(self, other):
+               return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
+
+
+class Conversation(object):
+
+       TYPE_VOICEMAIL = "Voicemail"
+       TYPE_SMS = "SMS"
+
+       def __init__(self):
+               self.type = None
+               self.id = None
+               self.contactId = None
+               self.name = None
+               self.location = None
+               self.prettyNumber = None
+               self.number = None
+
+               self.time = None
+               self.relTime = None
+               self.messages = None
+               self.isRead = None
+               self.isSpam = None
+               self.isTrash = None
+               self.isArchived = None
+
+       def __cmp__(self, other):
+               cmpValue = cmp(self.contactId, other.contactId)
+               if cmpValue != 0:
+                       return cmpValue
+
+               cmpValue = cmp(self.time, other.time)
+               if cmpValue != 0:
+                       return cmpValue
+
+               cmpValue = cmp(self.id, other.id)
+               if cmpValue != 0:
+                       return cmpValue
+
+       def to_dict(self):
+               selfDict = to_dict(self)
+               selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
+               return selfDict
+
+
+class GVoiceBackend(object):
+       """
+       This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
+       the functions include login, setting up a callback number, and initalting a callback
+       """
+
+       PHONE_TYPE_HOME = 1
+       PHONE_TYPE_MOBILE = 2
+       PHONE_TYPE_WORK = 3
+       PHONE_TYPE_GIZMO = 7
+
+       def __init__(self, cookieFile = None):
+               # Important items in this function are the setup of the browser emulation and cookie file
+               self._browser = browser_emu.MozillaEmulator(1)
+               self._loadedFromCookies = self._browser.load_cookies(cookieFile)
+
+               self._token = ""
+               self._accountNum = ""
+               self._lastAuthed = 0.0
+               self._callbackNumber = ""
+               self._callbackNumbers = {}
+
+               # Suprisingly, moving all of these from class to self sped up startup time
+
+               self._validateRe = re.compile("^\+?[0-9]{10,}$")
+
+               self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
+               SECURE_URL_BASE = "https://www.google.com/voice/"
+               SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
+               self._tokenURL = SECURE_URL_BASE + "m"
+               self._callUrl = SECURE_URL_BASE + "call/connect"
+               self._callCancelURL = SECURE_URL_BASE + "call/cancel"
+               self._sendSmsURL = SECURE_URL_BASE + "sms/send"
+
+               self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
+               self._isDndRe = re.compile(r"""<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 = {
+ "&quot;": '"',
+ "&nbsp;": " ",
+ "&#39;": "'",
+}
+
+
+def unescape(text):
+       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+       return plain
+
+
+def google_strptime(time):
+       """
+       Hack: Google always returns the time in the same locale.  Sadly if the
+       local system's locale is different, there isn't a way to perfectly handle
+       the time.  So instead we handle implement some time formatting
+       """
+       abbrevTime = time[:-3]
+       parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
+       if time.endswith("PM"):
+               parsedTime += datetime.timedelta(hours=12)
+       return parsedTime
+
+
+def itergroup(iterator, count, padValue = None):
+       """
+       Iterate in groups of 'count' values. If there
+       aren't enough values, the last result is padded with
+       None.
+
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print list(val)
+       [1, 2, 3]
+       [4, 5, 6]
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       (7, None, None)
+       >>> for val in itergroup("123456", 3):
+       ...     print tuple(val)
+       ('1', '2', '3')
+       ('4', '5', '6')
+       >>> for val in itergroup("123456", 3):
+       ...     print repr("".join(val))
+       '123'
+       '456'
+       """
+       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+       nIterators = (paddedIterator, ) * count
+       return itertools.izip(*nIterators)
+
+
+def safe_eval(s):
+       _TRUE_REGEX = re.compile("true")
+       _FALSE_REGEX = re.compile("false")
+       _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
+       s = _TRUE_REGEX.sub("True", s)
+       s = _FALSE_REGEX.sub("False", s)
+       s = _COMMENT_REGEX.sub("#", s)
+       try:
+               results = eval(s, {}, {})
+       except SyntaxError:
+               _moduleLogger.exception("Oops")
+               results = None
+       return results
+
+
+def _fake_parse_json(flattened):
+       return safe_eval(flattened)
+
+
+def _actual_parse_json(flattened):
+       return simplejson.loads(flattened)
+
+
+if simplejson is None:
+       parse_json = _fake_parse_json
+else:
+       parse_json = _actual_parse_json
+
+
+def extract_payload(flatXml):
+       xmlTree = ElementTree.fromstring(flatXml)
+
+       jsonElement = xmlTree.getchildren()[0]
+       flatJson = jsonElement.text
+       jsonTree = parse_json(flatJson)
+
+       htmlElement = xmlTree.getchildren()[1]
+       flatHtml = htmlElement.text
+
+       return jsonTree, flatHtml
+
+
+def guess_phone_type(number):
+       if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
+               return GVoiceBackend.PHONE_TYPE_GIZMO
+       else:
+               return GVoiceBackend.PHONE_TYPE_MOBILE
+
+
+def get_sane_callback(backend):
+       """
+       Try to set a sane default callback number on these preferences
+       1) 1747 numbers ( Gizmo )
+       2) anything with gizmo in the name
+       3) anything with computer in the name
+       4) the first value
+       """
+       numbers = backend.get_callback_numbers()
+
+       priorityOrderedCriteria = [
+               ("\+1747", None),
+               ("1747", None),
+               ("747", None),
+               (None, "gizmo"),
+               (None, "computer"),
+               (None, "sip"),
+               (None, None),
+       ]
+
+       for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
+               numberMatcher = None
+               descriptionMatcher = None
+               if numberCriteria is not None:
+                       numberMatcher = re.compile(numberCriteria)
+               elif descriptionCriteria is not None:
+                       descriptionMatcher = re.compile(descriptionCriteria, re.I)
+
+               for number, description in numbers.iteritems():
+                       if numberMatcher is not None and numberMatcher.match(number) is None:
+                               continue
+                       if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
+                               continue
+                       return number
+
+
+def set_sane_callback(backend):
+       """
+       Try to set a sane default callback number on these preferences
+       1) 1747 numbers ( Gizmo )
+       2) anything with gizmo in the name
+       3) anything with computer in the name
+       4) the first value
+       """
+       number = get_sane_callback(backend)
+       backend.set_callback_number(number)
+
+
+def _is_not_special(name):
+       return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
+
+
+def to_dict(obj):
+       members = inspect.getmembers(obj)
+       return dict((name, value) for (name, value) in members if _is_not_special(name))
+
+
+def grab_debug_info(username, password):
+       cookieFile = os.path.join(".", "raw_cookies.txt")
+       try:
+               os.remove(cookieFile)
+       except OSError:
+               pass
+
+       backend = GVoiceBackend(cookieFile)
+       browser = backend._browser
+
+       _TEST_WEBPAGES = [
+               ("token", backend._tokenURL),
+               ("login", backend._loginURL),
+               ("isdnd", backend._isDndURL),
+               ("account", backend._XML_ACCOUNT_URL),
+               ("html_contacts", backend._XML_CONTACTS_URL),
+               ("contacts", backend._JSON_CONTACTS_URL),
+               ("csv", backend._CSV_CONTACTS_URL),
+
+               ("voicemail", backend._XML_VOICEMAIL_URL),
+               ("html_sms", backend._XML_SMS_URL),
+               ("sms", backend._JSON_SMS_URL),
+               ("count", backend._JSON_SMS_COUNT_URL),
+
+               ("recent", backend._XML_RECENT_URL),
+               ("placed", backend._XML_PLACED_URL),
+               ("recieved", backend._XML_RECEIVED_URL),
+               ("missed", backend._XML_MISSED_URL),
+       ]
+
+       # Get Pages
+       print "Grabbing pre-login pages"
+       for name, url in _TEST_WEBPAGES:
+               try:
+                       page = browser.download(url)
+               except StandardError, e:
+                       print e.message
+                       continue
+               print "\tWriting to file"
+               with open("not_loggedin_%s.txt" % name, "w") as f:
+                       f.write(page)
+
+       # Login
+       print "Attempting login"
+       galxToken = backend._get_token()
+       loginSuccessOrFailurePage = backend._login(username, password, galxToken)
+       with open("loggingin.txt", "w") as f:
+               print "\tWriting to file"
+               f.write(loginSuccessOrFailurePage)
+       try:
+               backend._grab_account_info(loginSuccessOrFailurePage)
+       except Exception:
+               # Retry in case the redirect failed
+               # luckily refresh_account_info does everything we need for a retry
+               loggedIn = backend.refresh_account_info() is not None
+               if not loggedIn:
+                       raise
+
+       # Get Pages
+       print "Grabbing post-login pages"
+       for name, url in _TEST_WEBPAGES:
+               try:
+                       page = browser.download(url)
+               except StandardError, e:
+                       print str(e)
+                       continue
+               print "\tWriting to file"
+               with open("loggedin_%s.txt" % name, "w") as f:
+                       f.write(page)
+
+       # Cookies
+       browser.save_cookies()
+       print "\tWriting cookies to file"
+       with open("cookies.txt", "w") as f:
+               f.writelines(
+                       "%s: %s\n" % (c.name, c.value)
+                       for c in browser._cookies
+               )
+
+
+def grab_voicemails(username, password):
+       cookieFile = os.path.join(".", "raw_cookies.txt")
+       try:
+               os.remove(cookieFile)
+       except OSError:
+               pass
+
+       backend = GVoiceBackend(cookieFile)
+       backend.login(username, password)
+       voicemails = list(backend.get_voicemails())
+       for voicemail in voicemails:
+               print voicemail.id
+               backend.download(voicemail.id, ".")
+
+
+def main():
+       import sys
+       logging.basicConfig(level=logging.DEBUG)
+       args = sys.argv
+       if 3 <= len(args):
+               username = args[1]
+               password = args[2]
+
+       grab_debug_info(username, password)
+       grab_voicemails(username, password)
+
+
+if __name__ == "__main__":
+       main()
diff --git a/dialcentral/backends/null_backend.py b/dialcentral/backends/null_backend.py
new file mode 100644 (file)
index 0000000..ebaa932
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+"""
+
+
+class NullAddressBook(object):
+
+       @property
+       def name(self):
+               return "None"
+
+       def update_account(self, force = True):
+               pass
+
+       def get_contacts(self):
+               return {}
+
+
+class NullAddressBookFactory(object):
+
+       def get_addressbooks(self):
+               yield NullAddressBook()
diff --git a/dialcentral/backends/qt_backend.py b/dialcentral/backends/qt_backend.py
new file mode 100644 (file)
index 0000000..88e52fa
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+if qt_compat.USES_PYSIDE:
+       try:
+               import QtMobility.Contacts as _QtContacts
+               QtContacts = _QtContacts
+       except ImportError:
+               QtContacts = None
+else:
+       QtContacts = None
+
+import null_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QtContactsAddressBook(object):
+
+       def __init__(self, name, uri):
+               self._name = name
+               self._uri = uri
+               self._manager = QtContacts.QContactManager.fromUri(uri)
+               self._contacts = None
+
+       @property
+       def name(self):
+               return self._name
+
+       @property
+       def error(self):
+               return self._manager.error()
+
+       def update_account(self, force = True):
+               if not force and self._contacts is not None:
+                       return
+               self._contacts = dict(self._get_contacts())
+
+       def get_contacts(self):
+               if self._contacts is None:
+                       self._contacts = dict(self._get_contacts())
+               return self._contacts
+
+       def _get_contacts(self):
+               contacts = self._manager.contacts()
+               for contact in contacts:
+                       contactId = contact.localId()
+                       contactName = contact.displayLabel()
+                       phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
+                       phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
+                       contactDetails = phones
+                       if 0 < len(contactDetails):
+                               yield str(contactId), {
+                                       "contactId": str(contactId),
+                                       "name": contactName,
+                                       "numbers": contactDetails,
+                               }
+
+
+class _QtContactsAddressBookFactory(object):
+
+       def __init__(self):
+               self._availableManagers = {}
+
+               availableMgrs = QtContacts.QContactManager.availableManagers()
+               availableMgrs.remove("invalid")
+               for managerName in availableMgrs:
+                       params = {}
+                       managerUri = QtContacts.QContactManager.buildUri(managerName, params)
+                       self._availableManagers[managerName] =  managerUri
+
+       def get_addressbooks(self):
+               for name, uri in self._availableManagers.iteritems():
+                       book = QtContactsAddressBook(name, uri)
+                       if book.error:
+                               _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
+                       else:
+                               yield book
+
+
+class _EmptyAddressBookFactory(object):
+
+       def get_addressbooks(self):
+               if False:
+                       yield None
+
+
+if QtContacts is not None:
+       QtContactsAddressBookFactory = _QtContactsAddressBookFactory
+else:
+       QtContactsAddressBookFactory = _EmptyAddressBookFactory
+       _moduleLogger.info("QtContacts support not available")
+
+
+if __name__ == "__main__":
+       factory = QtContactsAddressBookFactory()
+       books = factory.get_addressbooks()
+       for book in books:
+               print book.name
+               print book.get_contacts()
diff --git a/dialcentral/call_handler.py b/dialcentral/call_handler.py
new file mode 100644 (file)
index 0000000..9b9c47d
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+try:
+       import telepathy as _telepathy
+       import util.tp_utils as telepathy_utils
+       telepathy = _telepathy
+except ImportError:
+       telepathy = None
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _FakeSignaller(object):
+
+       def start(self):
+               pass
+
+       def stop(self):
+               pass
+
+
+class _MissedCallWatcher(QtCore.QObject):
+
+       callMissed = qt_compat.Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._isStarted = False
+               self._isSupported = True
+
+               self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
+               self._outstandingRequests = []
+
+       @property
+       def isSupported(self):
+               return self._isSupported
+
+       @property
+       def isStarted(self):
+               return self._isStarted
+
+       def start(self):
+               if self._isStarted:
+                       _moduleLogger.info("voicemail monitor already started")
+                       return
+               try:
+                       self._newChannelSignaller.start()
+               except RuntimeError:
+                       _moduleLogger.exception("Missed call detection not supported")
+                       self._newChannelSignaller = _FakeSignaller()
+                       self._isSupported = False
+               self._isStarted = True
+
+       def stop(self):
+               if not self._isStarted:
+                       _moduleLogger.info("voicemail monitor stopped without starting")
+                       return
+               _moduleLogger.info("Stopping voicemail refresh")
+               self._newChannelSignaller.stop()
+
+               # I don't want to trust whether the cancel happens within the current
+               # callback or not which could be the deciding factor between invalid
+               # iterators or infinite loops
+               localRequests = [r for r in self._outstandingRequests]
+               for request in localRequests:
+                       localRequests.cancel()
+
+               self._isStarted = False
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
+               if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
+                       return
+
+               conn = telepathy.client.Connection(serviceName, connObjectPath)
+               try:
+                       chan = telepathy.client.Channel(serviceName, channelObjectPath)
+               except dbus.exceptions.UnknownMethodException:
+                       _moduleLogger.exception("Client might not have implemented a deprecated method")
+                       return
+               missDetection = telepathy_utils.WasMissedCall(
+                       bus, conn, chan, self._on_missed_call, self._on_error_for_missed
+               )
+               self._outstandingRequests.append(missDetection)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_missed_call(self, missDetection):
+               _moduleLogger.info("Missed a call")
+               self.callMissed.emit()
+               self._outstandingRequests.remove(missDetection)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error_for_missed(self, missDetection, reason):
+               _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
+               self._outstandingRequests.remove(missDetection)
+
+
+class _DummyMissedCallWatcher(QtCore.QObject):
+
+       callMissed = qt_compat.Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._isStarted = False
+
+       @property
+       def isSupported(self):
+               return False
+
+       @property
+       def isStarted(self):
+               return self._isStarted
+
+       def start(self):
+               self._isStarted = True
+
+       def stop(self):
+               if not self._isStarted:
+                       _moduleLogger.info("voicemail monitor stopped without starting")
+                       return
+               _moduleLogger.info("Stopping voicemail refresh")
+               self._isStarted = False
+
+
+if telepathy is not None:
+       MissedCallWatcher = _MissedCallWatcher
+else:
+       MissedCallWatcher = _DummyMissedCallWatcher
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/dialcentral/constants.py b/dialcentral/constants.py
new file mode 100644 (file)
index 0000000..b9d3c79
--- /dev/null
@@ -0,0 +1,13 @@
+import os
+
+__pretty_app_name__ = "DialCentral"
+__app_name__ = "dialcentral"
+__version__ = "1.3.6"
+__build__ = 0
+__app_magic__ = 0xdeadbeef
+_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
+_user_settings_ = "%s/settings.ini" % _data_path_
+_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_
+_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
+_notifier_logpath_ = "%s/notifier.log" % _data_path_
+IS_MAEMO = True
diff --git a/dialcentral/dialcentral_qt.py b/dialcentral/dialcentral_qt.py
new file mode 100755 (executable)
index 0000000..a464ad6
--- /dev/null
@@ -0,0 +1,812 @@
+#!/usr/bin/env python
+# -*- coding: UTF8 -*-
+
+from __future__ import with_statement
+
+import os
+import base64
+import ConfigParser
+import functools
+import logging
+import logging.handlers
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+import alarm_handler
+from util import qtpie
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+import session
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Dialcentral(qwrappers.ApplicationWrapper):
+
+       _DATA_PATHS = [
+               os.path.join(os.path.dirname(__file__), "../share"),
+               os.path.join(os.path.dirname(__file__), "../data"),
+       ]
+
+       def __init__(self, app):
+               self._dataPath = None
+               self._aboutDialog = None
+               self.notifyOnMissed = False
+               self.notifyOnVoicemail = False
+               self.notifyOnSms = False
+
+               self._streamHandler = None
+               self._ledHandler = None
+               self._alarmHandler = alarm_handler.AlarmHandler()
+
+               qwrappers.ApplicationWrapper.__init__(self, app, constants)
+
+       def load_settings(self):
+               try:
+                       config = ConfigParser.SafeConfigParser()
+                       config.read(constants._user_settings_)
+               except IOError, e:
+                       _moduleLogger.info("No settings")
+                       return
+               except ValueError:
+                       _moduleLogger.info("Settings were corrupt")
+                       return
+               except ConfigParser.MissingSectionHeaderError:
+                       _moduleLogger.info("Settings were corrupt")
+                       return
+               except Exception:
+                       _moduleLogger.exception("Unknown loading error")
+
+               self._mainWindow.load_settings(config)
+
+       def save_settings(self):
+               _moduleLogger.info("Saving settings")
+               config = ConfigParser.SafeConfigParser()
+
+               self._mainWindow.save_settings(config)
+
+               with open(constants._user_settings_, "wb") as configFile:
+                       config.write(configFile)
+
+       def get_icon(self, name):
+               if self._dataPath is None:
+                       for path in self._DATA_PATHS:
+                               if os.path.exists(os.path.join(path, name)):
+                                       self._dataPath = path
+                                       break
+               if self._dataPath is not None:
+                       icon = QtGui.QIcon(os.path.join(self._dataPath, name))
+                       return icon
+               else:
+                       return None
+
+       def get_resource(self, name):
+               if self._dataPath is None:
+                       for path in self._DATA_PATHS:
+                               if os.path.exists(os.path.join(path, name)):
+                                       self._dataPath = path
+                                       break
+               if self._dataPath is not None:
+                       return os.path.join(self._dataPath, name)
+               else:
+                       return None
+
+       def _close_windows(self):
+               qwrappers.ApplicationWrapper._close_windows(self)
+               if self._aboutDialog  is not None:
+                       self._aboutDialog.close()
+
+       @property
+       def fsContactsPath(self):
+               return os.path.join(constants._data_path_, "contacts")
+
+       @property
+       def streamHandler(self):
+               if self._streamHandler is None:
+                       import stream_handler
+                       self._streamHandler = stream_handler.StreamHandler()
+               return self._streamHandler
+
+       @property
+       def alarmHandler(self):
+               return self._alarmHandler
+
+       @property
+       def ledHandler(self):
+               if self._ledHandler is None:
+                       import led_handler
+                       self._ledHandler = led_handler.LedHandler()
+               return self._ledHandler
+
+       def _new_main_window(self):
+               return MainWindow(None, self)
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_about(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._aboutDialog is None:
+                               import dialogs
+                               self._aboutDialog = dialogs.AboutDialog(self)
+                       response = self._aboutDialog.run(self._mainWindow.window)
+
+
+class DelayedWidget(object):
+
+       def __init__(self, app, settingsNames):
+               self._layout = QtGui.QVBoxLayout()
+               self._layout.setContentsMargins(0, 0, 0, 0)
+               self._widget = QtGui.QWidget()
+               self._widget.setContentsMargins(0, 0, 0, 0)
+               self._widget.setLayout(self._layout)
+               self._settings = dict((name, "") for name in settingsNames)
+
+               self._child = None
+               self._isEnabled = True
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def has_child(self):
+               return self._child is not None
+
+       def set_child(self, child):
+               if self._child is not None:
+                       self._layout.removeWidget(self._child.toplevel)
+               self._child = child
+               if self._child is not None:
+                       self._layout.addWidget(self._child.toplevel)
+
+               self._child.set_settings(self._settings)
+
+               if self._isEnabled:
+                       self._child.enable()
+               else:
+                       self._child.disable()
+
+       @property
+       def child(self):
+               return self._child
+
+       def enable(self):
+               self._isEnabled = True
+               if self._child is not None:
+                       self._child.enable()
+
+       def disable(self):
+               self._isEnabled = False
+               if self._child is not None:
+                       self._child.disable()
+
+       def clear(self):
+               if self._child is not None:
+                       self._child.clear()
+
+       def refresh(self, force=True):
+               if self._child is not None:
+                       self._child.refresh(force)
+
+       def get_settings(self):
+               if self._child is not None:
+                       return self._child.get_settings()
+               else:
+                       return self._settings
+
+       def set_settings(self, settings):
+               if self._child is not None:
+                       self._child.set_settings(settings)
+               else:
+                       self._settings = settings
+
+
+def _tab_factory(tab, app, session, errorLog):
+       import gv_views
+       return gv_views.__dict__[tab](app, session, errorLog)
+
+
+class MainWindow(qwrappers.WindowWrapper):
+
+       KEYPAD_TAB = 0
+       RECENT_TAB = 1
+       MESSAGES_TAB = 2
+       CONTACTS_TAB = 3
+       MAX_TABS = 4
+
+       _TAB_TITLES = [
+               "Dialpad",
+               "History",
+               "Messages",
+               "Contacts",
+       ]
+       assert len(_TAB_TITLES) == MAX_TABS
+
+       _TAB_ICONS = [
+               "dialpad.png",
+               "history.png",
+               "messages.png",
+               "contacts.png",
+       ]
+       assert len(_TAB_ICONS) == MAX_TABS
+
+       _TAB_CLASS = [
+               functools.partial(_tab_factory, "Dialpad"),
+               functools.partial(_tab_factory, "History"),
+               functools.partial(_tab_factory, "Messages"),
+               functools.partial(_tab_factory, "Contacts"),
+       ]
+       assert len(_TAB_CLASS) == MAX_TABS
+
+       # Hack to allow delay importing/loading of tabs
+       _TAB_SETTINGS_NAMES = [
+               (),
+               ("filter", ),
+               ("status", "type"),
+               ("selectedAddressbook", ),
+       ]
+       assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
+
+       def __init__(self, parent, app):
+               qwrappers.WindowWrapper.__init__(self, parent, app)
+               self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
+               self._window.resized.connect(self._on_window_resized)
+               self._errorLog = self._app.errorLog
+
+               self._session = session.Session(self._errorLog, constants._data_path_)
+               self._session.error.connect(self._on_session_error)
+               self._session.loggedIn.connect(self._on_login)
+               self._session.loggedOut.connect(self._on_logout)
+               self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+               self._session.newMessages.connect(self._on_new_message_alert)
+               self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
+               self._voicemailRefreshDelay = QtCore.QTimer()
+               self._voicemailRefreshDelay.setInterval(30 * 1000)
+               self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
+               self._voicemailRefreshDelay.setSingleShot(True)
+               self._callHandler = None
+               self._updateVoicemailOnMissedCall = False
+
+               self._defaultCredentials = "", ""
+               self._curentCredentials = "", ""
+               self._currentTab = 0
+
+               self._credentialsDialog = None
+               self._smsEntryDialog = None
+               self._accountDialog = None
+
+               self._tabsContents = [
+                       DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
+                       for i in xrange(self.MAX_TABS)
+               ]
+               for tab in self._tabsContents:
+                       tab.disable()
+
+               self._tabWidget = QtGui.QTabWidget()
+               if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
+                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+               else:
+                       self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+               defaultTabIconSize = self._tabWidget.iconSize()
+               defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height()
+               for tabIndex, (tabTitle, tabIcon) in enumerate(
+                       zip(self._TAB_TITLES, self._TAB_ICONS)
+               ):
+                       icon = self._app.get_icon(tabIcon)
+                       if constants.IS_MAEMO and icon is not None:
+                               tabTitle = ""
+
+                       if icon is None:
+                               self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
+                       else:
+                               iconSize = icon.availableSizes()[0]
+                               defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width())
+                               defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height())
+                               self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
+               defaultTabIconWidth = max(defaultTabIconWidth, 32)
+               defaultTabIconHeight = max(defaultTabIconHeight, 32)
+               self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight))
+               self._tabWidget.currentChanged.connect(self._on_tab_changed)
+               self._tabWidget.setContentsMargins(0, 0, 0, 0)
+
+               self._layout.addWidget(self._tabWidget)
+
+               self._loginAction = QtGui.QAction(None)
+               self._loginAction.setText("Login")
+               self._loginAction.triggered.connect(self._on_login_requested)
+
+               self._importAction = QtGui.QAction(None)
+               self._importAction.setText("Import")
+               self._importAction.triggered.connect(self._on_import)
+
+               self._accountAction = QtGui.QAction(None)
+               self._accountAction.setText("Account")
+               self._accountAction.triggered.connect(self._on_account)
+
+               self._refreshConnectionAction = QtGui.QAction(None)
+               self._refreshConnectionAction.setText("Refresh Connection")
+               self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
+               self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
+
+               self._refreshTabAction = QtGui.QAction(None)
+               self._refreshTabAction.setText("Refresh Tab")
+               self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
+               self._refreshTabAction.triggered.connect(self._on_refresh)
+
+               fileMenu = self._window.menuBar().addMenu("&File")
+               fileMenu.addAction(self._loginAction)
+               fileMenu.addAction(self._refreshTabAction)
+               fileMenu.addAction(self._refreshConnectionAction)
+
+               toolsMenu = self._window.menuBar().addMenu("&Tools")
+               toolsMenu.addAction(self._accountAction)
+               toolsMenu.addAction(self._importAction)
+               toolsMenu.addAction(self._app.aboutAction)
+
+               self._initialize_tab(self._tabWidget.currentIndex())
+               self.set_fullscreen(self._app.fullscreenAction.isChecked())
+               self.update_orientation(self._app.orientation)
+
+       def _init_call_handler(self):
+               if self._callHandler is not None:
+                       return
+               import call_handler
+               self._callHandler = call_handler.MissedCallWatcher()
+               self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
+
+       def set_default_credentials(self, username, password):
+               self._defaultCredentials = username, password
+
+       def get_default_credentials(self):
+               return self._defaultCredentials
+
+       def walk_children(self):
+               if self._smsEntryDialog is not None:
+                       return (self._smsEntryDialog, )
+               else:
+                       return ()
+
+       def start(self):
+               qwrappers.WindowWrapper.start(self)
+               assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up"
+               if self._defaultCredentials != ("", ""):
+                       username, password = self._defaultCredentials[0], self._defaultCredentials[1]
+                       self._curentCredentials = username, password
+                       self._session.login(username, password)
+               else:
+                       self._prompt_for_login()
+
+       def close(self):
+               for diag in (
+                       self._credentialsDialog,
+                       self._accountDialog,
+               ):
+                       if diag is not None:
+                               diag.close()
+               for child in self.walk_children():
+                       child.window.destroyed.disconnect(self._on_child_close)
+                       child.window.closed.disconnect(self._on_child_close)
+                       child.close()
+               self._window.close()
+
+       def destroy(self):
+               qwrappers.WindowWrapper.destroy(self)
+               if self._session.state != self._session.LOGGEDOUT_STATE:
+                       self._session.logout()
+
+       def get_current_tab(self):
+               return self._currentTab
+
+       def set_current_tab(self, tabIndex):
+               self._tabWidget.setCurrentIndex(tabIndex)
+
+       def load_settings(self, config):
+               blobs = "", ""
+               isFullscreen = False
+               orientation = self._app.orientation
+               tabIndex = 0
+               try:
+                       blobs = [
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(len(self.get_default_credentials()))
+                       ]
+                       isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
+                       tabIndex = config.getint(constants.__pretty_app_name__, "tab")
+                       orientation = config.get(constants.__pretty_app_name__, "orientation")
+               except ConfigParser.NoOptionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing option %s" % (
+                                       constants._user_settings_,
+                                       e.option,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except Exception:
+                       _moduleLogger.exception("Unknown loading error")
+
+               try:
+                       self._app.alarmHandler.load_settings(config, "alarm")
+                       self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+                       self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+                       self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+                       self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
+               except ConfigParser.NoOptionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing option %s" % (
+                                       constants._user_settings_,
+                                       e.option,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except Exception:
+                       _moduleLogger.exception("Unknown loading error")
+
+               creds = (
+                       base64.b64decode(blob)
+                       for blob in blobs
+               )
+               self.set_default_credentials(*creds)
+               self._app.fullscreenAction.setChecked(isFullscreen)
+               self._app.set_orientation(orientation)
+               self.set_current_tab(tabIndex)
+
+               backendId = 2 # For backwards compatibility
+               for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+                       sectionName = "%s - %s" % (backendId, tabTitle)
+                       settings = self._tabsContents[tabIndex].get_settings()
+                       for settingName in settings.iterkeys():
+                               try:
+                                       settingValue = config.get(sectionName, settingName)
+                               except ConfigParser.NoOptionError, e:
+                                       _moduleLogger.info(
+                                               "Settings file %s is missing section %s" % (
+                                                       constants._user_settings_,
+                                                       e.section,
+                                               ),
+                                       )
+                                       return
+                               except ConfigParser.NoSectionError, e:
+                                       _moduleLogger.info(
+                                               "Settings file %s is missing section %s" % (
+                                                       constants._user_settings_,
+                                                       e.section,
+                                               ),
+                                       )
+                                       return
+                               except Exception:
+                                       _moduleLogger.exception("Unknown loading error")
+                                       return
+                               settings[settingName] = settingValue
+                       self._tabsContents[tabIndex].set_settings(settings)
+
+       def save_settings(self, config):
+               config.add_section(constants.__pretty_app_name__)
+               config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
+               config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
+               config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
+               for i, value in enumerate(self.get_default_credentials()):
+                       blob = base64.b64encode(value)
+                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+
+               config.add_section("alarm")
+               self._app.alarmHandler.save_settings(config, "alarm")
+               config.add_section("2 - Account Info")
+               config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
+               config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
+               config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
+               config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
+
+               backendId = 2 # For backwards compatibility
+               for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+                       sectionName = "%s - %s" % (backendId, tabTitle)
+                       config.add_section(sectionName)
+                       tabSettings = self._tabsContents[tabIndex].get_settings()
+                       for settingName, settingValue in tabSettings.iteritems():
+                               config.set(sectionName, settingName, settingValue)
+
+       def update_orientation(self, orientation):
+               qwrappers.WindowWrapper.update_orientation(self, orientation)
+               windowOrientation = self.idealWindowOrientation
+               if windowOrientation == QtCore.Qt.Horizontal:
+                       self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+               else:
+                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+       def _initialize_tab(self, index):
+               assert index < self.MAX_TABS, "Invalid tab"
+               if not self._tabsContents[index].has_child():
+                       tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
+                       self._tabsContents[index].set_child(tab)
+               self._tabsContents[index].refresh(force=False)
+
+       def _prompt_for_login(self):
+               if self._credentialsDialog is None:
+                       import dialogs
+                       self._credentialsDialog = dialogs.CredentialsDialog(self._app)
+               credentials = self._credentialsDialog.run(
+                       self._defaultCredentials[0], self._defaultCredentials[1], self.window
+               )
+               if credentials is None:
+                       return
+               username, password = credentials
+               self._curentCredentials = username, password
+               self._session.login(username, password)
+
+       def _show_account_dialog(self):
+               if self._accountDialog is None:
+                       import dialogs
+                       self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
+                       self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
+                       self._accountDialog.settingsApproved.connect(self._on_settings_approved)
+
+               if self._callHandler is not None and not self._callHandler.isSupported:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
+               elif self._updateVoicemailOnMissedCall:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
+               else:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
+               self._accountDialog.notifications = self._app.alarmHandler.alarmType
+               self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
+               self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
+               self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
+               self._accountDialog.notifyOnSms = self._app.notifyOnSms
+               self._accountDialog.set_callbacks(
+                       self._session.get_callback_numbers(), self._session.get_callback_number()
+               )
+               accountNumberToDisplay = self._session.get_account_number()
+               if not accountNumberToDisplay:
+                       accountNumberToDisplay = "Not Available (%s)" % self._session.state
+               self._accountDialog.set_account_number(accountNumberToDisplay)
+               self._accountDialog.orientation = self._app.orientation
+
+               self._accountDialog.run()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_settings_approved(self):
+               if self._accountDialog.doClear:
+                       self._session.logout_and_clear()
+                       self._defaultCredentials = "", ""
+                       self._curentCredentials = "", ""
+                       for tab in self._tabsContents:
+                               tab.disable()
+               else:
+                       callbackNumber = self._accountDialog.selectedCallback
+                       self._session.set_callback_number(callbackNumber)
+
+               if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED:
+                       pass
+               elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
+                       self._updateVoicemailOnMissedCall = True
+                       self._init_call_handler()
+                       self._callHandler.start()
+               else:
+                       self._updateVoicemailOnMissedCall = False
+                       if self._callHandler is not None:
+                               self._callHandler.stop()
+               if (
+                       self._accountDialog.notifyOnMissed or
+                       self._accountDialog.notifyOnVoicemail or
+                       self._accountDialog.notifyOnSms
+               ):
+                       notifications = self._accountDialog.notifications
+               else:
+                       notifications = self._accountDialog.ALARM_NONE
+               self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
+
+               self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
+               self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
+               self._app.notifyOnSms = self._accountDialog.notifyOnSms
+               self._app.set_orientation(self._accountDialog.orientation)
+               self._app.save_settings()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_window_resized(self):
+               with qui_utils.notify_error(self._app.errorLog):
+                       windowOrientation = self.idealWindowOrientation
+                       if windowOrientation == QtCore.Qt.Horizontal:
+                               self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+                       else:
+                               self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_new_message_alert(self):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+                               if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
+                                       self._errorLog.push_message("New messages")
+                               else:
+                                       self._app.ledHandler.on()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_call_missed(self):
+               with qui_utils.notify_error(self._errorLog):
+                       self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
+
+       @qt_compat.Slot(str)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_session_error(self, message):
+               with qui_utils.notify_error(self._errorLog):
+                       self._errorLog.push_error(message)
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_login(self):
+               with qui_utils.notify_error(self._errorLog):
+                       changedAccounts = self._defaultCredentials != self._curentCredentials
+                       noCallback = not self._session.get_callback_number()
+                       if changedAccounts or noCallback:
+                               self._show_account_dialog()
+
+                       self._defaultCredentials = self._curentCredentials
+
+                       for tab in self._tabsContents:
+                               tab.enable()
+                       self._initialize_tab(self._currentTab)
+                       if self._updateVoicemailOnMissedCall:
+                               self._init_call_handler()
+                               self._callHandler.start()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_logout(self):
+               with qui_utils.notify_error(self._errorLog):
+                       for tab in self._tabsContents:
+                               tab.disable()
+                       if self._callHandler is not None:
+                               self._callHandler.stop()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_app_alert(self):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._session.state == self._session.LOGGEDIN_STATE:
+                               messageType = {
+                                       (True, True): self._session.MESSAGE_ALL,
+                                       (True, False): self._session.MESSAGE_TEXTS,
+                                       (False, True): self._session.MESSAGE_VOICEMAILS,
+                               }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
+                               self._session.update_messages(messageType, force=True)
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_recipients_changed(self):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._session.draft.get_num_contacts() == 0:
+                               return
+
+                       if self._smsEntryDialog is None:
+                               import dialogs
+                               self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
+                               self._smsEntryDialog.window.destroyed.connect(self._on_child_close)
+                               self._smsEntryDialog.window.closed.connect(self._on_child_close)
+                               self._smsEntryDialog.window.show()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_close(self, obj = None):
+               self._smsEntryDialog = None
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_login_requested(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       self._prompt_for_login()
+
+       @qt_compat.Slot(int)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_tab_changed(self, index):
+               with qui_utils.notify_error(self._errorLog):
+                       self._currentTab = index
+                       self._initialize_tab(index)
+                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+                               self._app.ledHandler.off()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_refresh(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       self._tabsContents[self._currentTab].refresh(force=True)
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_refresh_connection(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       self._session.refresh_connection()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_import(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
+                       csvName = unicode(csvName)
+                       if not csvName:
+                               return
+                       import shutil
+                       shutil.copy2(csvName, self._app.fsContactsPath)
+                       if self._tabsContents[self.CONTACTS_TAB].has_child:
+                               self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_account(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings"
+                       self._show_account_dialog()
+
+
+def run():
+       try:
+               os.makedirs(constants._data_path_)
+       except OSError, e:
+               if e.errno != 17:
+                       raise
+
+       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+       logging.basicConfig(level=logging.DEBUG, format=logFormat)
+       rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
+       rotating.setFormatter(logging.Formatter(logFormat))
+       root = logging.getLogger()
+       root.addHandler(rotating)
+       _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
+       _moduleLogger.info("OS: %s" % (os.uname()[0], ))
+       _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+       _moduleLogger.info("Hostname: %s" % os.uname()[1])
+
+       try:
+               import gobject
+               gobject.threads_init()
+       except ImportError:
+               _moduleLogger.info("GObject support not available")
+       try:
+               import dbus
+               try:
+                       from dbus.mainloop.qt import DBusQtMainLoop
+                       DBusQtMainLoop(set_as_default=True)
+                       _moduleLogger.info("Using Qt mainloop")
+               except ImportError:
+                       try:
+                               from dbus.mainloop.glib import DBusGMainLoop
+                               DBusGMainLoop(set_as_default=True)
+                               _moduleLogger.info("Using GObject mainloop")
+                       except ImportError:
+                               _moduleLogger.info("Mainloop not available")
+       except ImportError:
+               _moduleLogger.info("DBus support not available")
+
+       app = QtGui.QApplication([])
+       handle = Dialcentral(app)
+       qtpie.init_pies()
+       return app.exec_()
+
+
+if __name__ == "__main__":
+       import sys
+
+       val = run()
+       sys.exit(val)
diff --git a/dialcentral/dialogs.py b/dialcentral/dialogs.py
new file mode 100644 (file)
index 0000000..8fbf328
--- /dev/null
@@ -0,0 +1,1192 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import functools
+import copy
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class CredentialsDialog(object):
+
+       def __init__(self, app):
+               self._app = app
+               self._usernameField = QtGui.QLineEdit()
+               self._passwordField = QtGui.QLineEdit()
+               self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
+
+               self._credLayout = QtGui.QGridLayout()
+               self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
+               self._credLayout.addWidget(self._usernameField, 0, 1)
+               self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
+               self._credLayout.addWidget(self._passwordField, 1, 1)
+
+               self._loginButton = QtGui.QPushButton("&Login")
+               self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
+               self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
+
+               self._layout = QtGui.QVBoxLayout()
+               self._layout.addLayout(self._credLayout)
+               self._layout.addWidget(self._buttonLayout)
+
+               self._dialog = QtGui.QDialog()
+               self._dialog.setWindowTitle("Login")
+               self._dialog.setLayout(self._layout)
+               self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
+               self._buttonLayout.accepted.connect(self._dialog.accept)
+               self._buttonLayout.rejected.connect(self._dialog.reject)
+
+               self._closeWindowAction = QtGui.QAction(None)
+               self._closeWindowAction.setText("Close")
+               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+               self._closeWindowAction.triggered.connect(self._on_close_window)
+
+               self._dialog.addAction(self._closeWindowAction)
+               self._dialog.addAction(app.quitAction)
+               self._dialog.addAction(app.fullscreenAction)
+
+       def run(self, defaultUsername, defaultPassword, parent=None):
+               self._dialog.setParent(parent, QtCore.Qt.Dialog)
+               try:
+                       self._usernameField.setText(defaultUsername)
+                       self._passwordField.setText(defaultPassword)
+
+                       response = self._dialog.exec_()
+                       if response == QtGui.QDialog.Accepted:
+                               return str(self._usernameField.text()), str(self._passwordField.text())
+                       elif response == QtGui.QDialog.Rejected:
+                               return None
+                       else:
+                               _moduleLogger.error("Unknown response")
+                               return None
+               finally:
+                       self._dialog.setParent(None, QtCore.Qt.Dialog)
+
+       def close(self):
+               try:
+                       self._dialog.reject()
+               except RuntimeError:
+                       _moduleLogger.exception("Oh well")
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_close_window(self, checked = True):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._dialog.reject()
+
+
+class AboutDialog(object):
+
+       def __init__(self, app):
+               self._app = app
+               self._title = QtGui.QLabel(
+                       "<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
diff --git a/dialcentral/examples/log_notifier.py b/dialcentral/examples/log_notifier.py
new file mode 100644 (file)
index 0000000..541ac18
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import datetime
+import ConfigParser
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+       with open(constants._notifier_logpath_, "a") as file:
+               file.write("Notification: %r\n" % (datetime.datetime.now(), ))
+
+               config = ConfigParser.SafeConfigParser()
+               config.read(constants._user_settings_)
+               backend = alarm_notify.create_backend(config)
+               notifyUser = alarm_notify.is_changed(config, backend)
+
+               if notifyUser:
+                       file.write("\tChange occurred\n")
+
+
+if __name__ == "__main__":
+       notify_on_change()
diff --git a/dialcentral/examples/sound_notifier.py b/dialcentral/examples/sound_notifier.py
new file mode 100644 (file)
index 0000000..c31e413
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import ConfigParser
+import logging
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       backend = alarm_notify.create_backend(config)
+       notifyUser = alarm_notify.is_changed(config, backend)
+
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._custom_notifier_settings_)
+       soundFile = config.get("Sound Notifier", "soundfile")
+       soundFile = "/usr/lib/gv-notifier/alert.mp3"
+
+       if notifyUser:
+               import subprocess
+               import led_handler
+               logging.info("Changed, playing %s" % soundFile)
+               led = led_handler.LedHandler()
+               led.on()
+               soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True)
+       else:
+               logging.info("No Change")
+
+
+if __name__ == "__main__":
+       logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
+       logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__))
+       logging.info("OS: %s" % (os.uname()[0], ))
+       logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+       logging.info("Hostname: %s" % os.uname()[1])
+       try:
+               notify_on_change()
+       except:
+               logging.exception("Error")
+               raise
diff --git a/dialcentral/gv_views.py b/dialcentral/gv_views.py
new file mode 100644 (file)
index 0000000..2bd0663
--- /dev/null
@@ -0,0 +1,977 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import datetime
+import string
+import itertools
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+from util import qtpie
+from util import qui_utils
+from util import misc as misc_utils
+
+import backends.null_backend as null_backend
+import backends.file_backend as file_backend
+import backends.qt_backend as qt_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_SENTINEL_ICON = QtGui.QIcon()
+
+
+class Dialpad(object):
+
+       def __init__(self, app, session, errorLog):
+               self._app = app
+               self._session = session
+               self._errorLog = errorLog
+
+               self._plus = QtGui.QPushButton("+")
+               self._plus.clicked.connect(lambda: self._on_keypress("+"))
+               self._entry = QtGui.QLineEdit()
+
+               backAction = QtGui.QAction(None)
+               backAction.setText("Back")
+               backAction.triggered.connect(self._on_backspace)
+               backPieItem = qtpie.QActionPieItem(backAction)
+               clearAction = QtGui.QAction(None)
+               clearAction.setText("Clear")
+               clearAction.triggered.connect(self._on_clear_text)
+               clearPieItem = qtpie.QActionPieItem(clearAction)
+               backSlices = [
+                       qtpie.PieFiling.NULL_CENTER,
+                       clearPieItem,
+                       qtpie.PieFiling.NULL_CENTER,
+                       qtpie.PieFiling.NULL_CENTER,
+               ]
+               self._back = qtpie.QPieButton(backPieItem)
+               self._back.set_center(backPieItem)
+               for slice in backSlices:
+                       self._back.insertItem(slice)
+
+               self._entryLayout = QtGui.QHBoxLayout()
+               self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
+               self._entryLayout.addWidget(self._entry, 1000)
+               self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
+
+               smsIcon = self._app.get_icon("messages.png")
+               self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
+               self._smsButton.clicked.connect(self._on_sms_clicked)
+               self._smsButton.setSizePolicy(QtGui.QSizePolicy(
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.PushButton,
+               ))
+               callIcon = self._app.get_icon("dialpad.png")
+               self._callButton = QtGui.QPushButton(callIcon, "Call")
+               self._callButton.clicked.connect(self._on_call_clicked)
+               self._callButton.setSizePolicy(QtGui.QSizePolicy(
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.PushButton,
+               ))
+
+               self._padLayout = QtGui.QGridLayout()
+               rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
+               columns = [0, 1, 2] * 3
+               keys = [
+                       ("1", ""),
+                       ("2", "ABC"),
+                       ("3", "DEF"),
+                       ("4", "GHI"),
+                       ("5", "JKL"),
+                       ("6", "MNO"),
+                       ("7", "PQRS"),
+                       ("8", "TUV"),
+                       ("9", "WXYZ"),
+               ]
+               for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
+                       self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
+               self._zerothButton = QtGui.QPushButton("0")
+               self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
+               self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.PushButton,
+               ))
+               self._padLayout.addWidget(self._smsButton, 3, 0)
+               self._padLayout.addWidget(self._zerothButton)
+               self._padLayout.addWidget(self._callButton, 3, 2)
+
+               self._layout = QtGui.QVBoxLayout()
+               self._layout.addLayout(self._entryLayout, 0)
+               self._layout.addLayout(self._padLayout, 1000000)
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._layout)
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def enable(self):
+               self._smsButton.setEnabled(True)
+               self._callButton.setEnabled(True)
+
+       def disable(self):
+               self._smsButton.setEnabled(False)
+               self._callButton.setEnabled(False)
+
+       def get_settings(self):
+               return {}
+
+       def set_settings(self, settings):
+               pass
+
+       def clear(self):
+               pass
+
+       def refresh(self, force = True):
+               pass
+
+       def _generate_key_button(self, center, letters):
+               button = QtGui.QPushButton("%s\n%s" % (center, letters))
+               button.setSizePolicy(QtGui.QSizePolicy(
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.MinimumExpanding,
+                       QtGui.QSizePolicy.PushButton,
+               ))
+               button.clicked.connect(lambda: self._on_keypress(center))
+               return button
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_keypress(self, key):
+               with qui_utils.notify_error(self._errorLog):
+                       self._entry.insert(key)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_backspace(self, toggled = False):
+               with qui_utils.notify_error(self._errorLog):
+                       self._entry.backspace()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_clear_text(self, toggled = False):
+               with qui_utils.notify_error(self._errorLog):
+                       self._entry.clear()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_sms_clicked(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       number = misc_utils.make_ugly(str(self._entry.text()))
+                       self._entry.clear()
+
+                       contactId = number
+                       title = misc_utils.make_pretty(number)
+                       description = misc_utils.make_pretty(number)
+                       numbersWithDescriptions = [(number, "")]
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_call_clicked(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       number = misc_utils.make_ugly(str(self._entry.text()))
+                       self._entry.clear()
+
+                       contactId = number
+                       title = misc_utils.make_pretty(number)
+                       description = misc_utils.make_pretty(number)
+                       numbersWithDescriptions = [(number, "")]
+                       self._session.draft.clear()
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+                       self._session.draft.call()
+
+
+class TimeCategories(object):
+
+       _NOW_SECTION = 0
+       _TODAY_SECTION = 1
+       _WEEK_SECTION = 2
+       _MONTH_SECTION = 3
+       _REST_SECTION = 4
+       _MAX_SECTIONS = 5
+
+       _NO_ELAPSED = datetime.timedelta(hours=1)
+       _WEEK_ELAPSED = datetime.timedelta(weeks=1)
+       _MONTH_ELAPSED = datetime.timedelta(days=30)
+
+       def __init__(self, parentItem):
+               self._timeItems = [
+                       QtGui.QStandardItem(description)
+                       for (i, description) in zip(
+                               xrange(self._MAX_SECTIONS),
+                               ["Now", "Today", "Week", "Month", "Past"],
+                       )
+               ]
+               for item in self._timeItems:
+                       item.setEditable(False)
+                       item.setCheckable(False)
+                       row = (item, )
+                       parentItem.appendRow(row)
+
+               self._today = datetime.datetime(1900, 1, 1)
+
+               self.prepare_for_update(self._today)
+
+       def prepare_for_update(self, newToday):
+               self._today = newToday
+               for item in self._timeItems:
+                       item.removeRows(0, item.rowCount())
+               try:
+                       hour = self._today.strftime("%X")
+                       day = self._today.strftime("%x")
+               except ValueError:
+                       _moduleLogger.exception("Can't format times")
+                       hour = "Now"
+                       day = "Today"
+               self._timeItems[self._NOW_SECTION].setText(hour)
+               self._timeItems[self._TODAY_SECTION].setText(day)
+
+       def add_row(self, rowDate, row):
+               elapsedTime = self._today - rowDate
+               todayTuple = self._today.timetuple()
+               rowTuple = rowDate.timetuple()
+               if elapsedTime < self._NO_ELAPSED:
+                       section = self._NOW_SECTION
+               elif todayTuple[0:3] == rowTuple[0:3]:
+                       section = self._TODAY_SECTION
+               elif elapsedTime < self._WEEK_ELAPSED:
+                       section = self._WEEK_SECTION
+               elif elapsedTime < self._MONTH_ELAPSED:
+                       section = self._MONTH_SECTION
+               else:
+                       section = self._REST_SECTION
+               self._timeItems[section].appendRow(row)
+
+       def get_item(self, timeIndex, rowIndex, column):
+               timeItem = self._timeItems[timeIndex]
+               item = timeItem.child(rowIndex, column)
+               return item
+
+
+class History(object):
+
+       DETAILS_IDX = 0
+       FROM_IDX = 1
+       MAX_IDX = 2
+
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
+       HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
+       HISTORY_COLUMNS = ["", "From"]
+       assert len(HISTORY_COLUMNS) == MAX_IDX
+
+       def __init__(self, app, session, errorLog):
+               self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
+               self._app = app
+               self._session = session
+               self._session.historyUpdated.connect(self._on_history_updated)
+               self._errorLog = errorLog
+
+               self._typeSelection = QtGui.QComboBox()
+               self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
+               self._typeSelection.setCurrentIndex(
+                       self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
+               )
+               self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+               refreshIcon = qui_utils.get_theme_icon(
+                       ("view-refresh", "general_refresh", "gtk-refresh", ),
+                       _SENTINEL_ICON
+               )
+               if refreshIcon is not _SENTINEL_ICON:
+                       self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               else:
+                       self._refreshButton = QtGui.QPushButton("Refresh")
+               self._refreshButton.clicked.connect(self._on_refresh_clicked)
+               self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+                       QtGui.QSizePolicy.Minimum,
+                       QtGui.QSizePolicy.Minimum,
+                       QtGui.QSizePolicy.PushButton,
+               ))
+               self._managerLayout = QtGui.QHBoxLayout()
+               self._managerLayout.addWidget(self._typeSelection, 1000)
+               self._managerLayout.addWidget(self._refreshButton, 0)
+
+               self._itemStore = QtGui.QStandardItemModel()
+               self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
+               self._categoryManager = TimeCategories(self._itemStore)
+
+               self._itemView = QtGui.QTreeView()
+               self._itemView.setModel(self._itemStore)
+               self._itemView.setUniformRowHeights(True)
+               self._itemView.setRootIsDecorated(False)
+               self._itemView.setIndentation(0)
+               self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+               self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+               self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+               self._itemView.setHeaderHidden(True)
+               self._itemView.setItemsExpandable(False)
+               self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
+               self._itemView.activated.connect(self._on_row_activated)
+
+               self._layout = QtGui.QVBoxLayout()
+               self._layout.addLayout(self._managerLayout)
+               self._layout.addWidget(self._itemView)
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._layout)
+
+               self._actionIcon = {
+                       "Placed": self._app.get_icon("placed.png"),
+                       "Missed": self._app.get_icon("missed.png"),
+                       "Received": self._app.get_icon("received.png"),
+               }
+
+               self._populate_items()
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def enable(self):
+               self._itemView.setEnabled(True)
+
+       def disable(self):
+               self._itemView.setEnabled(False)
+
+       def get_settings(self):
+               return {
+                       "filter": self._selectedFilter,
+               }
+
+       def set_settings(self, settings):
+               selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
+               if selectedFilter in self.HISTORY_ITEM_TYPES:
+                       self._selectedFilter = selectedFilter
+                       self._typeSelection.setCurrentIndex(
+                               self.HISTORY_ITEM_TYPES.index(selectedFilter)
+                       )
+
+       def clear(self):
+               self._itemView.clear()
+
+       def refresh(self, force=True):
+               self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+
+               if self._selectedFilter == self.HISTORY_RECEIVED:
+                       self._session.update_history(self._session.HISTORY_RECEIVED, force)
+               elif self._selectedFilter == self.HISTORY_MISSED:
+                       self._session.update_history(self._session.HISTORY_MISSED, force)
+               elif self._selectedFilter == self.HISTORY_PLACED:
+                       self._session.update_history(self._session.HISTORY_PLACED, force)
+               elif self._selectedFilter == self.HISTORY_ALL:
+                       self._session.update_history(self._session.HISTORY_ALL, force)
+               else:
+                       assert False, "How did we get here?"
+
+               if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
+                       self._app.ledHandler.off()
+
+       def _populate_items(self):
+               self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
+
+               history = self._session.get_history()
+               history.sort(key=lambda item: item["time"], reverse=True)
+               for event in history:
+                       if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
+                               continue
+
+                       relTime = event["relTime"]
+                       action = event["action"]
+                       number = event["number"]
+                       prettyNumber = misc_utils.make_pretty(number)
+                       if prettyNumber.startswith("+1 "):
+                               prettyNumber = prettyNumber[len("+1 "):]
+                       name = event["name"]
+                       if not name or name == number:
+                               name = event["location"]
+                       if not name:
+                               name = "Unknown"
+
+                       detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime))
+                       detailsFont = detailsItem.font()
+                       detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 5))
+                       detailsItem.setFont(detailsFont)
+                       nameItem = QtGui.QStandardItem(name)
+                       nameFont = nameItem.font()
+                       nameFont.setPointSize(nameFont.pointSize() + 4)
+                       nameItem.setFont(nameFont)
+                       row = detailsItem, nameItem
+                       for item in row:
+                               item.setEditable(False)
+                               item.setCheckable(False)
+                       row[self.DETAILS_IDX].setData(event)
+                       self._categoryManager.add_row(event["time"], row)
+               self._itemView.expandAll()
+
+       @qt_compat.Slot(str)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_filter_changed(self, newItem):
+               with qui_utils.notify_error(self._errorLog):
+                       self._selectedFilter = str(newItem)
+                       self._populate_items()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_history_updated(self):
+               with qui_utils.notify_error(self._errorLog):
+                       self._populate_items()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_refresh_clicked(self, arg = None):
+               with qui_utils.notify_error(self._errorLog):
+                       self.refresh(force=True)
+
+       @qt_compat.Slot(QtCore.QModelIndex)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_row_activated(self, index):
+               with qui_utils.notify_error(self._errorLog):
+                       timeIndex = index.parent()
+                       if not timeIndex.isValid():
+                               return
+                       timeRow = timeIndex.row()
+                       row = index.row()
+                       detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
+                       fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
+                       contactDetails = detailsItem.data()
+
+                       title = unicode(fromItem.text())
+                       number = str(contactDetails["number"])
+                       contactId = number # ids don't seem too unique so using numbers
+
+                       descriptionRows = []
+                       for t in xrange(self._itemStore.rowCount()):
+                               randomTimeItem = self._itemStore.item(t, 0)
+                               for i in xrange(randomTimeItem.rowCount()):
+                                       iItem = randomTimeItem.child(i, 0)
+                                       iContactDetails = iItem.data()
+                                       iNumber = str(iContactDetails["number"])
+                                       if number != iNumber:
+                                               continue
+                                       relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
+                                       action = str(iContactDetails["action"])
+                                       number = str(iContactDetails["number"])
+                                       prettyNumber = misc_utils.make_pretty(number)
+                                       rowItems = relTime, action, prettyNumber
+                                       descriptionRows.append("<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 ""
diff --git a/dialcentral/led_handler.py b/dialcentral/led_handler.py
new file mode 100755 (executable)
index 0000000..0914105
--- /dev/null
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+import dbus
+
+
+class _NokiaLedHandler(object):
+
+       def __init__(self):
+               self._bus = dbus.SystemBus()
+               self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
+               self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
+
+               self._ledPattern = "PatternCommunicationChat"
+
+       def on(self):
+               self._mceRequest.req_led_pattern_activate(self._ledPattern)
+
+       def off(self):
+               self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
+
+
+class _NoLedHandler(object):
+
+       def __init__(self):
+               pass
+
+       def on(self):
+               pass
+
+       def off(self):
+               pass
+
+
+class LedHandler(object):
+
+       def __init__(self):
+               self._actual = None
+               self._isReal = False
+
+       def on(self):
+               self._lazy_init()
+               self._actual.on()
+
+       def off(self):
+               self._lazy_init()
+               self._actual.off()
+
+       @property
+       def isReal(self):
+               self._lazy_init()
+               self._isReal
+
+       def _lazy_init(self):
+               if self._actual is not None:
+                       return
+               try:
+                       self._actual = _NokiaLedHandler()
+                       self._isReal = True
+               except dbus.DBusException:
+                       self._actual = _NoLedHandler()
+                       self._isReal = False
+
+
+if __name__ == "__main__":
+       leds = _NokiaLedHandler()
+       leds.off()
diff --git a/dialcentral/session.py b/dialcentral/session.py
new file mode 100644 (file)
index 0000000..dbdc3e4
--- /dev/null
@@ -0,0 +1,830 @@
+from __future__ import with_statement
+
+import os
+import time
+import datetime
+import contextlib
+import logging
+
+try:
+       import cPickle
+       pickle = cPickle
+except ImportError:
+       import pickle
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+from util import qore_utils
+from util import qui_utils
+from util import concurrent
+from util import misc as misc_utils
+
+import constants
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _DraftContact(object):
+
+       def __init__(self, messageId, title, description, numbersWithDescriptions):
+               self.messageId = messageId
+               self.title = title
+               self.description = description
+               self.numbers = numbersWithDescriptions
+               self.selectedNumber = numbersWithDescriptions[0][0]
+
+
+class Draft(QtCore.QObject):
+
+       sendingMessage = qt_compat.Signal()
+       sentMessage = qt_compat.Signal()
+       calling = qt_compat.Signal()
+       called = qt_compat.Signal()
+       cancelling = qt_compat.Signal()
+       cancelled = qt_compat.Signal()
+       error = qt_compat.Signal(str)
+
+       recipientsChanged = qt_compat.Signal()
+
+       def __init__(self, asyncQueue, backend, errorLog):
+               QtCore.QObject.__init__(self)
+               self._errorLog = errorLog
+               self._contacts = {}
+               self._asyncQueue = asyncQueue
+               self._backend = backend
+               self._busyReason = None
+               self._message = ""
+
+       def send(self):
+               assert 0 < len(self._contacts), "No contacts selected"
+               assert 0 < len(self._message), "No message to send"
+               numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
+               le = self._asyncQueue.add_async(self._send)
+               le.start(numbers, self._message)
+
+       def call(self):
+               assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
+               assert len(self._message) == 0, "Cannot send message with call"
+               (contact, ) = self._contacts.itervalues()
+               number = misc_utils.make_ugly(contact.selectedNumber)
+               le = self._asyncQueue.add_async(self._call)
+               le.start(number)
+
+       def cancel(self):
+               le = self._asyncQueue.add_async(self._cancel)
+               le.start()
+
+       def _get_message(self):
+               return self._message
+
+       def _set_message(self, message):
+               self._message = message
+
+       message = property(_get_message, _set_message)
+
+       def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
+               if self._busyReason is not None:
+                       raise RuntimeError("Please wait for %r" % self._busyReason)
+               # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
+               contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
+               self._contacts[contactId] = contactDetails
+               self.recipientsChanged.emit()
+
+       def remove_contact(self, contactId):
+               if self._busyReason is not None:
+                       raise RuntimeError("Please wait for %r" % self._busyReason)
+               assert contactId in self._contacts, "Contact missing"
+               del self._contacts[contactId]
+               self.recipientsChanged.emit()
+
+       def get_contacts(self):
+               return self._contacts.iterkeys()
+
+       def get_num_contacts(self):
+               return len(self._contacts)
+
+       def get_message_id(self, cid):
+               return self._contacts[cid].messageId
+
+       def get_title(self, cid):
+               return self._contacts[cid].title
+
+       def get_description(self, cid):
+               return self._contacts[cid].description
+
+       def get_numbers(self, cid):
+               return self._contacts[cid].numbers
+
+       def get_selected_number(self, cid):
+               return self._contacts[cid].selectedNumber
+
+       def set_selected_number(self, cid, number):
+               # @note I'm lazy, this isn't firing any kind of signal since only one
+               # controller right now and that is the viewer
+               assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
+               self._contacts[cid].selectedNumber = number
+
+       def clear(self):
+               if self._busyReason is not None:
+                       raise RuntimeError("Please wait for %r" % self._busyReason)
+               self._clear()
+
+       def _clear(self):
+               oldContacts = self._contacts
+               self._contacts = {}
+               self._message = ""
+               if oldContacts:
+                       self.recipientsChanged.emit()
+
+       @contextlib.contextmanager
+       def _busy(self, message):
+               if self._busyReason is not None:
+                       raise RuntimeError("Already busy doing %r" % self._busyReason)
+               try:
+                       self._busyReason = message
+                       yield
+               finally:
+                       self._busyReason = None
+
+       def _send(self, numbers, text):
+               self.sendingMessage.emit()
+               try:
+                       with self._busy("Sending Text"):
+                               with qui_utils.notify_busy(self._errorLog, "Sending Text"):
+                                       yield (
+                                               self._backend[0].send_sms,
+                                               (numbers, text),
+                                               {},
+                                       )
+                               self.sentMessage.emit()
+                               self._clear()
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+
+       def _call(self, number):
+               self.calling.emit()
+               try:
+                       with self._busy("Calling"):
+                               with qui_utils.notify_busy(self._errorLog, "Calling"):
+                                       yield (
+                                               self._backend[0].call,
+                                               (number, ),
+                                               {},
+                                       )
+                               self.called.emit()
+                               self._clear()
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+
+       def _cancel(self):
+               self.cancelling.emit()
+               try:
+                       with qui_utils.notify_busy(self._errorLog, "Cancelling"):
+                               yield (
+                                       self._backend[0].cancel,
+                                       (),
+                                       {},
+                               )
+                       self.cancelled.emit()
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+
+
+class Session(QtCore.QObject):
+
+       # @todo Somehow add support for csv contacts
+       # @BUG When loading without caches, downloads messages twice
+
+       stateChange = qt_compat.Signal(str)
+       loggedOut = qt_compat.Signal()
+       loggedIn = qt_compat.Signal()
+       callbackNumberChanged = qt_compat.Signal(str)
+
+       accountUpdated = qt_compat.Signal()
+       messagesUpdated = qt_compat.Signal()
+       newMessages = qt_compat.Signal()
+       historyUpdated = qt_compat.Signal()
+       dndStateChange = qt_compat.Signal(bool)
+       voicemailAvailable = qt_compat.Signal(str, str)
+
+       error = qt_compat.Signal(str)
+
+       LOGGEDOUT_STATE = "logged out"
+       LOGGINGIN_STATE = "logging in"
+       LOGGEDIN_STATE = "logged in"
+
+       MESSAGE_TEXTS = "Text"
+       MESSAGE_VOICEMAILS = "Voicemail"
+       MESSAGE_ALL = "All"
+
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
+       _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
+
+       _LOGGEDOUT_TIME = -1
+       _LOGGINGIN_TIME = 0
+
+       def __init__(self, errorLog, cachePath):
+               QtCore.QObject.__init__(self)
+               self._errorLog = errorLog
+               self._pool = qore_utils.FutureThread()
+               self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
+               self._backend = []
+               self._loggedInTime = self._LOGGEDOUT_TIME
+               self._loginOps = []
+               self._cachePath = cachePath
+               self._voicemailCachePath = None
+               self._username = None
+               self._password = None
+               self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
+               self._delayedRelogin = QtCore.QTimer()
+               self._delayedRelogin.setInterval(0)
+               self._delayedRelogin.setSingleShot(True)
+               self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
+
+               self._contacts = {}
+               self._accountUpdateTime = datetime.datetime(1971, 1, 1)
+               self._messages = []
+               self._cleanMessages = []
+               self._messageUpdateTime = datetime.datetime(1971, 1, 1)
+               self._history = []
+               self._historyUpdateTime = datetime.datetime(1971, 1, 1)
+               self._dnd = False
+               self._callback = ""
+
+       @property
+       def state(self):
+               return {
+                       self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
+                       self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
+               }.get(self._loggedInTime, self.LOGGEDIN_STATE)
+
+       @property
+       def draft(self):
+               return self._draft
+
+       def login(self, username, password):
+               assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
+               assert username != "", "No username specified"
+               if self._cachePath is not None:
+                       cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
+               else:
+                       cookiePath = None
+
+               if self._username != username or not self._backend:
+                       from backends import gv_backend
+                       del self._backend[:]
+                       self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
+
+               self._pool.start()
+               le = self._asyncQueue.add_async(self._login)
+               le.start(username, password)
+
+       def logout(self):
+               assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+               _moduleLogger.info("Logging out")
+               self._pool.stop()
+               self._loggedInTime = self._LOGGEDOUT_TIME
+               self._backend[0].persist()
+               self._save_to_cache()
+               self._clear_voicemail_cache()
+               self.stateChange.emit(self.LOGGEDOUT_STATE)
+               self.loggedOut.emit()
+
+       def clear(self):
+               assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
+               self._backend[0].logout()
+               del self._backend[0]
+               self._clear_cache()
+               self._draft.clear()
+
+       def logout_and_clear(self):
+               assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+               _moduleLogger.info("Logging out and clearing the account")
+               self._pool.stop()
+               self._loggedInTime = self._LOGGEDOUT_TIME
+               self.clear()
+               self.stateChange.emit(self.LOGGEDOUT_STATE)
+               self.loggedOut.emit()
+
+       def update_account(self, force = True):
+               if not force and self._contacts:
+                       return
+               le = self._asyncQueue.add_async(self._update_account), (), {}
+               self._perform_op_while_loggedin(le)
+
+       def refresh_connection(self):
+               le = self._asyncQueue.add_async(self._refresh_authentication)
+               le.start()
+
+       def get_contacts(self):
+               return self._contacts
+
+       def get_when_contacts_updated(self):
+               return self._accountUpdateTime
+
+       def update_messages(self, messageType, force = True):
+               if not force and self._messages:
+                       return
+               le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
+               self._perform_op_while_loggedin(le)
+
+       def get_messages(self):
+               return self._messages
+
+       def get_when_messages_updated(self):
+               return self._messageUpdateTime
+
+       def update_history(self, historyType, force = True):
+               if not force and self._history:
+                       return
+               le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
+               self._perform_op_while_loggedin(le)
+
+       def get_history(self):
+               return self._history
+
+       def get_when_history_updated(self):
+               return self._historyUpdateTime
+
+       def update_dnd(self):
+               le = self._asyncQueue.add_async(self._update_dnd), (), {}
+               self._perform_op_while_loggedin(le)
+
+       def set_dnd(self, dnd):
+               le = self._asyncQueue.add_async(self._set_dnd)
+               le.start(dnd)
+
+       def is_available(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               return os.path.exists(actualPath)
+
+       def voicemail_path(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               if not os.path.exists(actualPath):
+                       raise RuntimeError("Voicemail not available")
+               return actualPath
+
+       def download_voicemail(self, messageId):
+               le = self._asyncQueue.add_async(self._download_voicemail)
+               le.start(messageId)
+
+       def _set_dnd(self, dnd):
+               oldDnd = self._dnd
+               try:
+                       assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
+                       with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
+                               yield (
+                                       self._backend[0].set_dnd,
+                                       (dnd, ),
+                                       {},
+                               )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._dnd = dnd
+               if oldDnd != self._dnd:
+                       self.dndStateChange.emit(self._dnd)
+
+       def get_dnd(self):
+               return self._dnd
+
+       def get_account_number(self):
+               if self.state != self.LOGGEDIN_STATE:
+                       return ""
+               return self._backend[0].get_account_number()
+
+       def get_callback_numbers(self):
+               if self.state != self.LOGGEDIN_STATE:
+                       return {}
+               return self._backend[0].get_callback_numbers()
+
+       def get_callback_number(self):
+               return self._callback
+
+       def set_callback_number(self, callback):
+               le = self._asyncQueue.add_async(self._set_callback_number)
+               le.start(callback)
+
+       def _set_callback_number(self, callback):
+               oldCallback = self._callback
+               try:
+                       assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
+                       yield (
+                               self._backend[0].set_callback_number,
+                               (callback, ),
+                               {},
+                       )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._callback = callback
+               if oldCallback != self._callback:
+                       self.callbackNumberChanged.emit(self._callback)
+
+       def _login(self, username, password):
+               with qui_utils.notify_busy(self._errorLog, "Logging In"):
+                       self._loggedInTime = self._LOGGINGIN_TIME
+                       self.stateChange.emit(self.LOGGINGIN_STATE)
+                       finalState = self.LOGGEDOUT_STATE
+                       accountData = None
+                       try:
+                               if accountData is None and self._backend[0].is_quick_login_possible():
+                                       accountData = yield (
+                                               self._backend[0].refresh_account_info,
+                                               (),
+                                               {},
+                                       )
+                                       if accountData is not None:
+                                               _moduleLogger.info("Logged in through cookies")
+                                       else:
+                                               # Force a clearing of the cookies
+                                               yield (
+                                                       self._backend[0].logout,
+                                                       (),
+                                                       {},
+                                               )
+
+                               if accountData is None:
+                                       accountData = yield (
+                                               self._backend[0].login,
+                                               (username, password),
+                                               {},
+                                       )
+                                       if accountData is not None:
+                                               _moduleLogger.info("Logged in through credentials")
+
+                               if accountData is not None:
+                                       self._loggedInTime = int(time.time())
+                                       oldUsername = self._username
+                                       self._username = username
+                                       self._password = password
+                                       finalState = self.LOGGEDIN_STATE
+                                       if oldUsername != self._username:
+                                               needOps = not self._load()
+                                       else:
+                                               needOps = True
+
+                                       self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
+                                       try:
+                                               os.makedirs(self._voicemailCachePath)
+                                       except OSError, e:
+                                               if e.errno != 17:
+                                                       raise
+
+                                       self.loggedIn.emit()
+                                       self.stateChange.emit(finalState)
+                                       finalState = None # Mark it as already set
+                                       self._process_account_data(accountData)
+
+                                       if needOps:
+                                               loginOps = self._loginOps[:]
+                                       else:
+                                               loginOps = []
+                                       del self._loginOps[:]
+                                       for asyncOp, args, kwds in loginOps:
+                                               asyncOp.start(*args, **kwds)
+                               else:
+                                       self._loggedInTime = self._LOGGEDOUT_TIME
+                                       self.error.emit("Error logging in")
+                       except Exception, e:
+                               _moduleLogger.exception("Booh")
+                               self._loggedInTime = self._LOGGEDOUT_TIME
+                               _moduleLogger.exception("Reporting error to user")
+                               self.error.emit(str(e))
+                       finally:
+                               if finalState is not None:
+                                       self.stateChange.emit(finalState)
+                       if accountData is not None and self._callback:
+                               self.set_callback_number(self._callback)
+
+       def _update_account(self):
+               try:
+                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+                               accountData = yield (
+                                       self._backend[0].refresh_account_info,
+                                       (),
+                                       {},
+                               )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._loggedInTime = int(time.time())
+               self._process_account_data(accountData)
+
+       def _refresh_authentication(self):
+               try:
+                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+                               accountData = yield (
+                                       self._backend[0].refresh_account_info,
+                                       (),
+                                       {},
+                               )
+                               accountData = None
+               except Exception, e:
+                       _moduleLogger.exception("Passing to user")
+                       self.error.emit(str(e))
+                       # refresh_account_info does not normally throw, so it is fine if we
+                       # just quit early because something seriously wrong is going on
+                       return
+
+               if accountData is not None:
+                       self._loggedInTime = int(time.time())
+                       self._process_account_data(accountData)
+               else:
+                       self._delayedRelogin.start()
+
+       def _load(self):
+               updateMessages = len(self._messages) != 0
+               updateHistory = len(self._history) != 0
+               oldDnd = self._dnd
+               oldCallback = self._callback
+
+               self._messages = []
+               self._cleanMessages = []
+               self._history = []
+               self._dnd = False
+               self._callback = ""
+
+               loadedFromCache = self._load_from_cache()
+               if loadedFromCache:
+                       updateMessages = True
+                       updateHistory = True
+
+               if updateMessages:
+                       self.messagesUpdated.emit()
+               if updateHistory:
+                       self.historyUpdated.emit()
+               if oldDnd != self._dnd:
+                       self.dndStateChange.emit(self._dnd)
+               if oldCallback != self._callback:
+                       self.callbackNumberChanged.emit(self._callback)
+
+               return loadedFromCache
+
+       def _load_from_cache(self):
+               if self._cachePath is None:
+                       return False
+               cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
+
+               try:
+                       with open(cachePath, "rb") as f:
+                               dumpedData = pickle.load(f)
+               except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
+                       _moduleLogger.exception("Pickle fun loading")
+                       return False
+               except:
+                       _moduleLogger.exception("Weirdness loading")
+                       return False
+
+               try:
+                       version, build = dumpedData[0:2]
+               except ValueError:
+                       _moduleLogger.exception("Upgrade/downgrade fun")
+                       return False
+               except:
+                       _moduleLogger.exception("Weirdlings")
+                       return False
+
+               if misc_utils.compare_versions(
+                       self._OLDEST_COMPATIBLE_FORMAT_VERSION,
+                       misc_utils.parse_version(version),
+               ) <= 0:
+                       try:
+                               (
+                                       version, build,
+                                       messages, messageUpdateTime,
+                                       history, historyUpdateTime,
+                                       dnd, callback
+                               ) = dumpedData
+                       except ValueError:
+                               _moduleLogger.exception("Upgrade/downgrade fun")
+                               return False
+                       except:
+                               _moduleLogger.exception("Weirdlings")
+                               return False
+
+                       _moduleLogger.info("Loaded cache")
+                       self._messages = messages
+                       self._alert_on_messages(self._messages)
+                       self._messageUpdateTime = messageUpdateTime
+                       self._history = history
+                       self._historyUpdateTime = historyUpdateTime
+                       self._dnd = dnd
+                       self._callback = callback
+                       return True
+               else:
+                       _moduleLogger.debug(
+                               "Skipping cache due to version mismatch (%s-%s)" % (
+                                       version, build
+                               )
+                       )
+                       return False
+
+       def _save_to_cache(self):
+               _moduleLogger.info("Saving cache")
+               if self._cachePath is None:
+                       return
+               cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
+
+               try:
+                       dataToDump = (
+                               constants.__version__, constants.__build__,
+                               self._messages, self._messageUpdateTime,
+                               self._history, self._historyUpdateTime,
+                               self._dnd, self._callback
+                       )
+                       with open(cachePath, "wb") as f:
+                               pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
+                       _moduleLogger.info("Cache saved")
+               except (pickle.PickleError, IOError):
+                       _moduleLogger.exception("While saving")
+
+       def _clear_cache(self):
+               updateMessages = len(self._messages) != 0
+               updateHistory = len(self._history) != 0
+               oldDnd = self._dnd
+               oldCallback = self._callback
+
+               self._messages = []
+               self._messageUpdateTime = datetime.datetime(1971, 1, 1)
+               self._history = []
+               self._historyUpdateTime = datetime.datetime(1971, 1, 1)
+               self._dnd = False
+               self._callback = ""
+
+               if updateMessages:
+                       self.messagesUpdated.emit()
+               if updateHistory:
+                       self.historyUpdated.emit()
+               if oldDnd != self._dnd:
+                       self.dndStateChange.emit(self._dnd)
+               if oldCallback != self._callback:
+                       self.callbackNumberChanged.emit(self._callback)
+
+               self._save_to_cache()
+               self._clear_voicemail_cache()
+
+       def _clear_voicemail_cache(self):
+               import shutil
+               shutil.rmtree(self._voicemailCachePath, True)
+
+       def _update_messages(self, messageType):
+               try:
+                       assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
+                       with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
+                               self._messages = yield (
+                                       self._backend[0].get_messages,
+                                       (messageType, ),
+                                       {},
+                               )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._messageUpdateTime = datetime.datetime.now()
+               self.messagesUpdated.emit()
+               self._alert_on_messages(self._messages)
+
+       def _update_history(self, historyType):
+               try:
+                       assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
+                       with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType):
+                               self._history = yield (
+                                       self._backend[0].get_call_history,
+                                       (historyType, ),
+                                       {},
+                               )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._historyUpdateTime = datetime.datetime.now()
+               self.historyUpdated.emit()
+
+       def _update_dnd(self):
+               with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
+                       oldDnd = self._dnd
+                       try:
+                               assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
+                               self._dnd = yield (
+                                       self._backend[0].is_dnd,
+                                       (),
+                                       {},
+                               )
+                       except Exception, e:
+                               _moduleLogger.exception("Reporting error to user")
+                               self.error.emit(str(e))
+                               return
+                       if oldDnd != self._dnd:
+                               self.dndStateChange(self._dnd)
+
+       def _download_voicemail(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               targetPath = "%s.%s.part" % (actualPath, time.time())
+               if os.path.exists(actualPath):
+                       self.voicemailAvailable.emit(messageId, actualPath)
+                       return
+               with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
+                       try:
+                               yield (
+                                       self._backend[0].download,
+                                       (messageId, targetPath),
+                                       {},
+                               )
+                       except Exception, e:
+                               _moduleLogger.exception("Passing to user")
+                               self.error.emit(str(e))
+                               return
+
+               if os.path.exists(actualPath):
+                       try:
+                               os.remove(targetPath)
+                       except:
+                               _moduleLogger.exception("Ignoring file problems with cache")
+                       self.voicemailAvailable.emit(messageId, actualPath)
+                       return
+               else:
+                       os.rename(targetPath, actualPath)
+                       self.voicemailAvailable.emit(messageId, actualPath)
+
+       def _perform_op_while_loggedin(self, op):
+               if self.state == self.LOGGEDIN_STATE:
+                       op, args, kwds = op
+                       op.start(*args, **kwds)
+               else:
+                       self._push_login_op(op)
+
+       def _push_login_op(self, asyncOp):
+               assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
+               if asyncOp in self._loginOps:
+                       _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
+                       return
+               self._loginOps.append(asyncOp)
+
+       def _process_account_data(self, accountData):
+               self._contacts = dict(
+                       (contactId, contactDetails)
+                       for contactId, contactDetails in accountData["contacts"].iteritems()
+                       # A zero contact id is the catch all for unknown contacts
+                       if contactId != "0"
+               )
+
+               self._accountUpdateTime = datetime.datetime.now()
+               self.accountUpdated.emit()
+
+       def _alert_on_messages(self, messages):
+               cleanNewMessages = list(self._clean_messages(messages))
+               cleanNewMessages.sort(key=lambda m: m["contactId"])
+               if self._cleanMessages:
+                       if self._cleanMessages != cleanNewMessages:
+                               self.newMessages.emit()
+               self._cleanMessages = cleanNewMessages
+
+       def _clean_messages(self, messages):
+               for message in messages:
+                       cleaned = dict(
+                               kv
+                               for kv in message.iteritems()
+                               if kv[0] not in
+                               [
+                                       "relTime",
+                                       "time",
+                                       "isArchived",
+                                       "isRead",
+                                       "isSpam",
+                                       "isTrash",
+                               ]
+                       )
+
+                       # Don't let outbound messages cause alerts, especially if the package has only outbound
+                       cleaned["messageParts"] = [
+                               tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
+                       ]
+                       if not cleaned["messageParts"]:
+                               continue
+
+                       yield cleaned
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_relogin(self):
+               try:
+                       username = self._username
+                       password = self._password
+                       self.logout()
+                       self.login(username, password)
+               except Exception, e:
+                       _moduleLogger.exception("Passing to user")
+                       self.error.emit(str(e))
+                       return
diff --git a/dialcentral/stream_gst.py b/dialcentral/stream_gst.py
new file mode 100644 (file)
index 0000000..ce97fb6
--- /dev/null
@@ -0,0 +1,145 @@
+import logging
+
+import gobject
+import gst
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+       # @bug Advertising state changes a bit early, should watch for GStreamer state change
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       __gsignals__ = {
+               'state-change' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'eof' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'error' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+               ),
+       }
+
+       def __init__(self):
+               gobject.GObject.__init__(self)
+               #Fields
+               self._uri = ""
+               self._elapsed = 0
+               self._duration = 0
+
+               #Set up GStreamer
+               self._player = gst.element_factory_make("playbin2", "player")
+               bus = self._player.get_bus()
+               bus.add_signal_watch()
+               bus.connect("message", self._on_message)
+
+               #Constants
+               self._timeFormat = gst.Format(gst.FORMAT_TIME)
+               self._seekFlag = gst.SEEK_FLAG_FLUSH
+
+       @property
+       def playing(self):
+               return self.state == self.STATE_PLAY
+
+       @property
+       def has_file(self):
+               return 0 < len(self._uri)
+
+       @property
+       def state(self):
+               state = self._player.get_state()[1]
+               return self._translate_state(state)
+
+       def set_file(self, uri):
+               if self._uri != uri:
+                       self._invalidate_cache()
+               if self.state != self.STATE_STOP:
+                       self.stop()
+
+               self._uri = uri
+               self._player.set_property("uri", uri)
+
+       def play(self):
+               if self.state == self.STATE_PLAY:
+                       _moduleLogger.info("Already play")
+                       return
+               _moduleLogger.info("Play")
+               self._player.set_state(gst.STATE_PLAYING)
+               self.emit("state-change", self.STATE_PLAY)
+
+       def pause(self):
+               if self.state == self.STATE_PAUSE:
+                       _moduleLogger.info("Already pause")
+                       return
+               _moduleLogger.info("Pause")
+               self._player.set_state(gst.STATE_PAUSED)
+               self.emit("state-change", self.STATE_PAUSE)
+
+       def stop(self):
+               if self.state == self.STATE_STOP:
+                       _moduleLogger.info("Already stop")
+                       return
+               self._player.set_state(gst.STATE_NULL)
+               _moduleLogger.info("Stopped")
+               self.emit("state-change", self.STATE_STOP)
+
+       @property
+       def elapsed(self):
+               try:
+                       self._elapsed = self._player.query_position(self._timeFormat, None)[0]
+               except:
+                       pass
+               return self._elapsed
+
+       @property
+       def duration(self):
+               try:
+                       self._duration = self._player.query_duration(self._timeFormat, None)[0]
+               except:
+                       _moduleLogger.exception("Query failed")
+               return self._duration
+
+       def seek_time(self, ns):
+               self._elapsed = ns
+               self._player.seek_simple(self._timeFormat, self._seekFlag, ns)
+
+       def _invalidate_cache(self):
+               self._elapsed = 0
+               self._duration = 0
+
+       def _translate_state(self, gstState):
+               return {
+                       gst.STATE_NULL: self.STATE_STOP,
+                       gst.STATE_PAUSED: self.STATE_PAUSE,
+                       gst.STATE_PLAYING: self.STATE_PLAY,
+               }.get(gstState, self.STATE_STOP)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_message(self, bus, message):
+               t = message.type
+               if t == gst.MESSAGE_EOS:
+                       self._player.set_state(gst.STATE_NULL)
+                       self.emit("eof", self._uri)
+               elif t == gst.MESSAGE_ERROR:
+                       self._player.set_state(gst.STATE_NULL)
+                       err, debug = message.parse_error()
+                       _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+                       self.emit("error", err, debug)
+
+
+gobject.type_register(Stream)
diff --git a/dialcentral/stream_handler.py b/dialcentral/stream_handler.py
new file mode 100644 (file)
index 0000000..3c0c9e3
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+import util.misc as misc_utils
+try:
+       import stream_gst
+       stream = stream_gst
+except ImportError:
+       try:
+               import stream_osso
+               stream = stream_osso
+       except ImportError:
+               import stream_null
+               stream = stream_null
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class StreamToken(QtCore.QObject):
+
+       stateChange = qt_compat.Signal(str)
+       invalidated = qt_compat.Signal()
+       error = qt_compat.Signal(str)
+
+       STATE_PLAY = stream.Stream.STATE_PLAY
+       STATE_PAUSE = stream.Stream.STATE_PAUSE
+       STATE_STOP = stream.Stream.STATE_STOP
+
+       def __init__(self, stream):
+               QtCore.QObject.__init__(self)
+               self._stream = stream
+               self._stream.connect("state-change", self._on_stream_state)
+               self._stream.connect("eof", self._on_stream_eof)
+               self._stream.connect("error", self._on_stream_error)
+
+       @property
+       def state(self):
+               if self.isValid:
+                       return self._stream.state
+               else:
+                       return self.STATE_STOP
+
+       @property
+       def isValid(self):
+               return self._stream is not None
+
+       def play(self):
+               self._stream.play()
+
+       def pause(self):
+               self._stream.pause()
+
+       def stop(self):
+               self._stream.stop()
+
+       def invalidate(self):
+               if self._stream is None:
+                       return
+               _moduleLogger.info("Playback token invalidated")
+               self._stream = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_state(self, s, state):
+               if not self.isValid:
+                       return
+               if state == self.STATE_STOP:
+                       self.invalidate()
+               self.stateChange.emit(state)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_eof(self, s, uri):
+               if not self.isValid:
+                       return
+               self.invalidate()
+               self.stateChange.emit(self.STATE_STOP)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_error(self, s, error, debug):
+               if not self.isValid:
+                       return
+               _moduleLogger.info("Error %s %s" % (error, debug))
+               self.error.emit(str(error))
+
+
+class StreamHandler(QtCore.QObject):
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._stream = stream.Stream()
+               self._token = StreamToken(self._stream)
+
+       def set_file(self, path):
+               self._token.invalidate()
+               self._token = StreamToken(self._stream)
+               self._stream.set_file(path)
+               return self._token
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_state(self, s, state):
+               _moduleLogger.info("State change %r" % state)
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/dialcentral/stream_null.py b/dialcentral/stream_null.py
new file mode 100644 (file)
index 0000000..44fbbed
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(object):
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       def __init__(self):
+               pass
+
+       def connect(self, signalName, slot):
+               pass
+
+       @property
+       def playing(self):
+               return False
+
+       @property
+       def has_file(self):
+               return False
+
+       @property
+       def state(self):
+               return self.STATE_STOP
+
+       def set_file(self, uri):
+               pass
+
+       def play(self):
+               pass
+
+       def pause(self):
+               pass
+
+       def stop(self):
+               pass
+
+       @property
+       def elapsed(self):
+               return 0
+
+       @property
+       def duration(self):
+               return 0
+
+       def seek_time(self, ns):
+               pass
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/dialcentral/stream_osso.py b/dialcentral/stream_osso.py
new file mode 100644 (file)
index 0000000..abc453f
--- /dev/null
@@ -0,0 +1,181 @@
+import logging
+
+import gobject
+import dbus
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       __gsignals__ = {
+               'state-change' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'eof' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'error' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+               ),
+       }
+
+       _SERVICE_NAME = "com.nokia.osso_media_server"
+       _OBJECT_PATH = "/com/nokia/osso_media_server"
+       _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
+
+       def __init__(self):
+               gobject.GObject.__init__(self)
+               #Fields
+               self._state = self.STATE_STOP
+               self._nextState = self.STATE_STOP
+               self._uri = ""
+               self._elapsed = 0
+               self._duration = 0
+
+               session_bus = dbus.SessionBus()
+
+               # Get the osso-media-player proxy object
+               oms_object = session_bus.get_object(
+                       self._SERVICE_NAME,
+                       self._OBJECT_PATH,
+                       introspect=False,
+                       follow_name_owner_changes=True,
+               )
+               # Use the audio interface
+               oms_audio_interface = dbus.Interface(
+                       oms_object,
+                       self._AUDIO_INTERFACE_NAME,
+               )
+               self._audioProxy = oms_audio_interface
+
+               self._audioProxy.connect_to_signal("state_changed", self._on_state_changed)
+               self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream)
+
+               error_signals = [
+                       "no_media_selected",
+                       "file_not_found",
+                       "type_not_found",
+                       "unsupported_type",
+                       "gstreamer",
+                       "dsp",
+                       "device_unavailable",
+                       "corrupted_file",
+                       "out_of_memory",
+                       "audio_codec_not_supported",
+               ]
+               for error in error_signals:
+                       self._audioProxy.connect_to_signal(error, self._on_error)
+
+       @property
+       def playing(self):
+               return self.state == self.STATE_PLAY
+
+       @property
+       def has_file(self):
+               return 0 < len(self._uri)
+
+       @property
+       def state(self):
+               return self._state
+
+       def set_file(self, uri):
+               if self._uri != uri:
+                       self._invalidate_cache()
+               if self.state != self.STATE_STOP:
+                       self.stop()
+
+               self._uri = uri
+               self._audioProxy.set_media_location(self._uri)
+
+       def play(self):
+               if self._nextState == self.STATE_PLAY:
+                       _moduleLogger.info("Already play")
+                       return
+               _moduleLogger.info("Play")
+               self._audioProxy.play()
+               self._nextState = self.STATE_PLAY
+               #self.emit("state-change", self.STATE_PLAY)
+
+       def pause(self):
+               if self._nextState == self.STATE_PAUSE:
+                       _moduleLogger.info("Already pause")
+                       return
+               _moduleLogger.info("Pause")
+               self._audioProxy.pause()
+               self._nextState = self.STATE_PAUSE
+               #self.emit("state-change", self.STATE_PLAY)
+
+       def stop(self):
+               if self._nextState == self.STATE_STOP:
+                       _moduleLogger.info("Already stop")
+                       return
+               self._audioProxy.stop()
+               _moduleLogger.info("Stopped")
+               self._nextState = self.STATE_STOP
+               #self.emit("state-change", self.STATE_STOP)
+
+       @property
+       def elapsed(self):
+               pos_info = self._audioProxy.get_position()
+               if isinstance(pos_info, tuple):
+                       self._elapsed, self._duration = pos_info
+               return self._elapsed
+
+       @property
+       def duration(self):
+               pos_info = self._audioProxy.get_position()
+               if isinstance(pos_info, tuple):
+                       self._elapsed, self._duration = pos_info
+               return self._duration
+
+       def seek_time(self, ns):
+               _moduleLogger.debug("Seeking to: %s", ns)
+               self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) )
+
+       def _invalidate_cache(self):
+               self._elapsed = 0
+               self._duration = 0
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error(self, *args):
+               err, debug = "", repr(args)
+               _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+               self.emit("error", err, debug)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_end_of_stream(self, *args):
+               self._state = self.STATE_STOP
+               self._nextState = self.STATE_STOP
+               self.emit("eof", self._uri)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_state_changed(self, state):
+               _moduleLogger.info("State: %s", state)
+               state = {
+                       "playing": self.STATE_PLAY,
+                       "paused": self.STATE_PAUSE,
+                       "stopped": self.STATE_STOP,
+               }[state]
+               if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP:
+                       # They seem to want to advertise stop right as the stream is starting, breaking the owner of this
+                       return
+               self._state = state
+               self._nextState = state
+               self.emit("state-change", state)
+
+
+gobject.type_register(Stream)
diff --git a/dialcentral/util/__init__.py b/dialcentral/util/__init__.py
new file mode 100644 (file)
index 0000000..4265cc3
--- /dev/null
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/dialcentral/util/algorithms.py b/dialcentral/util/algorithms.py
new file mode 100644 (file)
index 0000000..e94fb61
--- /dev/null
@@ -0,0 +1,664 @@
+#!/usr/bin/env python
+
+"""
+@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448
+"""
+
+import itertools
+import functools
+import datetime
+import types
+import array
+import random
+
+
+def ordered_itr(collection):
+       """
+       >>> [v for v in ordered_itr({"a": 1, "b": 2})]
+       [('a', 1), ('b', 2)]
+       >>> [v for v in ordered_itr([3, 1, 10, -20])]
+       [-20, 1, 3, 10]
+       """
+       if isinstance(collection, types.DictType):
+               keys = list(collection.iterkeys())
+               keys.sort()
+               for key in keys:
+                       yield key, collection[key]
+       else:
+               values = list(collection)
+               values.sort()
+               for value in values:
+                       yield value
+
+
+def itercat(*iterators):
+       """
+       Concatenate several iterators into one.
+
+       >>> [v for v in itercat([1, 2, 3], [4, 1, 3])]
+       [1, 2, 3, 4, 1, 3]
+       """
+       for i in iterators:
+               for x in i:
+                       yield x
+
+
+def product(*args, **kwds):
+       # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
+       # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
+       pools = map(tuple, args) * kwds.get('repeat', 1)
+       result = [[]]
+       for pool in pools:
+               result = [x+[y] for x in result for y in pool]
+       for prod in result:
+               yield tuple(prod)
+
+
+def iterwhile(func, iterator):
+       """
+       Iterate for as long as func(value) returns true.
+       >>> through = lambda b: b
+       >>> [v for v in iterwhile(through, [True, True, False])]
+       [True, True]
+       """
+       iterator = iter(iterator)
+       while 1:
+               next = iterator.next()
+               if not func(next):
+                       raise StopIteration
+               yield next
+
+
+def iterfirst(iterator, count=1):
+       """
+       Iterate through 'count' first values.
+
+       >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)]
+       [1, 2, 3]
+       """
+       iterator = iter(iterator)
+       for i in xrange(count):
+               yield iterator.next()
+
+
+def iterstep(iterator, n):
+       """
+       Iterate every nth value.
+
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)]
+       [1, 2, 3, 4, 5]
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)]
+       [1, 3, 5]
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)]
+       [1, 4]
+       """
+       iterator = iter(iterator)
+       while True:
+               yield iterator.next()
+               # skip n-1 values
+               for dummy in xrange(n-1):
+                       iterator.next()
+
+
+def itergroup(iterator, count, padValue = None):
+       """
+       Iterate in groups of 'count' values. If there
+       aren't enough values, the last result is padded with
+       None.
+
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print list(val)
+       [1, 2, 3]
+       [4, 5, 6]
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       (7, None, None)
+       >>> for val in itergroup("123456", 3):
+       ...     print tuple(val)
+       ('1', '2', '3')
+       ('4', '5', '6')
+       >>> for val in itergroup("123456", 3):
+       ...     print repr("".join(val))
+       '123'
+       '456'
+       """
+       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+       nIterators = (paddedIterator, ) * count
+       return itertools.izip(*nIterators)
+
+
+def xzip(*iterators):
+       """Iterative version of builtin 'zip'."""
+       iterators = itertools.imap(iter, iterators)
+       while 1:
+               yield tuple([x.next() for x in iterators])
+
+
+def xmap(func, *iterators):
+       """Iterative version of builtin 'map'."""
+       iterators = itertools.imap(iter, iterators)
+       values_left = [1]
+
+       def values():
+               # Emulate map behaviour, i.e. shorter
+               # sequences are padded with None when
+               # they run out of values.
+               values_left[0] = 0
+               for i in range(len(iterators)):
+                       iterator = iterators[i]
+                       if iterator is None:
+                               yield None
+                       else:
+                               try:
+                                       yield iterator.next()
+                                       values_left[0] = 1
+                               except StopIteration:
+                                       iterators[i] = None
+                                       yield None
+       while 1:
+               args = tuple(values())
+               if not values_left[0]:
+                       raise StopIteration
+               yield func(*args)
+
+
+def xfilter(func, iterator):
+       """Iterative version of builtin 'filter'."""
+       iterator = iter(iterator)
+       while 1:
+               next = iterator.next()
+               if func(next):
+                       yield next
+
+
+def xreduce(func, iterator, default=None):
+       """Iterative version of builtin 'reduce'."""
+       iterator = iter(iterator)
+       try:
+               prev = iterator.next()
+       except StopIteration:
+               return default
+       single = 1
+       for next in iterator:
+               single = 0
+               prev = func(prev, next)
+       if single:
+               return func(prev, default)
+       return prev
+
+
+def daterange(begin, end, delta = datetime.timedelta(1)):
+       """
+       Form a range of dates and iterate over them.
+
+       Arguments:
+       begin -- a date (or datetime) object; the beginning of the range.
+       end   -- a date (or datetime) object; the end of the range.
+       delta -- (optional) a datetime.timedelta object; how much to step each iteration.
+                       Default step is 1 day.
+
+       Usage:
+       """
+       if not isinstance(delta, datetime.timedelta):
+               delta = datetime.timedelta(delta)
+
+       ZERO = datetime.timedelta(0)
+
+       if begin < end:
+               if delta <= ZERO:
+                       raise StopIteration
+               test = end.__gt__
+       else:
+               if delta >= ZERO:
+                       raise StopIteration
+               test = end.__lt__
+
+       while test(begin):
+               yield begin
+               begin += delta
+
+
+class LazyList(object):
+       """
+       A Sequence whose values are computed lazily by an iterator.
+
+       Module for the creation and use of iterator-based lazy lists.
+       this module defines a class LazyList which can be used to represent sequences
+       of values generated lazily. One can also create recursively defined lazy lists
+       that generate their values based on ones previously generated.
+
+       Backport to python 2.5 by Michael Pust
+       """
+
+       __author__ = 'Dan Spitz'
+
+       def __init__(self, iterable):
+               self._exhausted = False
+               self._iterator = iter(iterable)
+               self._data = []
+
+       def __len__(self):
+               """Get the length of a LazyList's computed data."""
+               return len(self._data)
+
+       def __getitem__(self, i):
+               """Get an item from a LazyList.
+               i should be a positive integer or a slice object."""
+               if isinstance(i, int):
+                       #index has not yet been yielded by iterator (or iterator exhausted
+                       #before reaching that index)
+                       if i >= len(self):
+                               self.exhaust(i)
+                       elif i < 0:
+                               raise ValueError('cannot index LazyList with negative number')
+                       return self._data[i]
+
+               #LazyList slices are iterators over a portion of the list.
+               elif isinstance(i, slice):
+                       start, stop, step = i.start, i.stop, i.step
+                       if any(x is not None and x < 0 for x in (start, stop, step)):
+                               raise ValueError('cannot index or step through a LazyList with'
+                                                               'a negative number')
+                       #set start and step to their integer defaults if they are None.
+                       if start is None:
+                               start = 0
+                       if step is None:
+                               step = 1
+
+                       def LazyListIterator():
+                               count = start
+                               predicate = (
+                                       (lambda: True)
+                                       if stop is None
+                                       else (lambda: count < stop)
+                               )
+                               while predicate():
+                                       try:
+                                               yield self[count]
+                                       #slices can go out of actual index range without raising an
+                                       #error
+                                       except IndexError:
+                                               break
+                                       count += step
+                       return LazyListIterator()
+
+               raise TypeError('i must be an integer or slice')
+
+       def __iter__(self):
+               """return an iterator over each value in the sequence,
+               whether it has been computed yet or not."""
+               return self[:]
+
+       def computed(self):
+               """Return an iterator over the values in a LazyList that have
+               already been computed."""
+               return self[:len(self)]
+
+       def exhaust(self, index = None):
+               """Exhaust the iterator generating this LazyList's values.
+               if index is None, this will exhaust the iterator completely.
+               Otherwise, it will iterate over the iterator until either the list
+               has a value for index or the iterator is exhausted.
+               """
+               if self._exhausted:
+                       return
+               if index is None:
+                       ind_range = itertools.count(len(self))
+               else:
+                       ind_range = range(len(self), index + 1)
+
+               for ind in ind_range:
+                       try:
+                               self._data.append(self._iterator.next())
+                       except StopIteration: #iterator is fully exhausted
+                               self._exhausted = True
+                               break
+
+
+class RecursiveLazyList(LazyList):
+
+       def __init__(self, prod, *args, **kwds):
+               super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds))
+
+
+class RecursiveLazyListFactory:
+
+       def __init__(self, producer):
+               self._gen = producer
+
+       def __call__(self, *a, **kw):
+               return RecursiveLazyList(self._gen, *a, **kw)
+
+
+def lazylist(gen):
+       """
+       Decorator for creating a RecursiveLazyList subclass.
+       This should decorate a generator function taking the LazyList object as its
+       first argument which yields the contents of the list in order.
+
+       >>> #fibonnacci sequence in a lazy list.
+       >>> @lazylist
+       ... def fibgen(lst):
+       ...     yield 0
+       ...     yield 1
+       ...     for a, b in itertools.izip(lst, lst[1:]):
+       ...             yield a + b
+       ...
+       >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence
+       >>> fibs = fibgen()
+       >>>
+       >>> #prime numbers in a lazy list.
+       >>> @lazylist
+       ... def primegen(lst):
+       ...     yield 2
+       ...     for candidate in itertools.count(3): #start at next number after 2
+       ...             #if candidate is not divisible by any smaller prime numbers,
+       ...             #it is a prime.
+       ...             if all(candidate % p for p in lst.computed()):
+       ...                     yield candidate
+       ...
+       >>> #same for primes- treat it like an infinitely long list containing all prime numbers.
+       >>> primes = primegen()
+       >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2]
+       0 1 1 2 3 5
+       >>> print list(fibs[:10]), list(primes[:10])
+       [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
+       """
+       return RecursiveLazyListFactory(gen)
+
+
+def map_func(f):
+       """
+       >>> import misc
+       >>> misc.validate_decorator(map_func)
+       """
+
+       @functools.wraps(f)
+       def wrapper(*args):
+               result = itertools.imap(f, args)
+               return result
+       return wrapper
+
+
+def reduce_func(function):
+       """
+       >>> import misc
+       >>> misc.validate_decorator(reduce_func(lambda x: x))
+       """
+
+       def decorator(f):
+
+               @functools.wraps(f)
+               def wrapper(*args):
+                       result = reduce(function, f(args))
+                       return result
+               return wrapper
+       return decorator
+
+
+def any_(iterable):
+       """
+       @note Python Version <2.5
+
+       >>> any_([True, True])
+       True
+       >>> any_([True, False])
+       True
+       >>> any_([False, False])
+       False
+       """
+
+       for element in iterable:
+               if element:
+                       return True
+       return False
+
+
+def all_(iterable):
+       """
+       @note Python Version <2.5
+
+       >>> all_([True, True])
+       True
+       >>> all_([True, False])
+       False
+       >>> all_([False, False])
+       False
+       """
+
+       for element in iterable:
+               if not element:
+                       return False
+       return True
+
+
+def for_every(pred, seq):
+       """
+       for_every takes a one argument predicate function and a sequence.
+       @param pred The predicate function should return true or false.
+       @returns true if every element in seq returns true for predicate, else returns false.
+
+       >>> for_every (lambda c: c > 5,(6,7,8,9))
+       True
+
+       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+       """
+
+       for i in seq:
+               if not pred(i):
+                       return False
+       return True
+
+
+def there_exists(pred, seq):
+       """
+       there_exists takes a one argument predicate     function and a sequence.
+       @param pred The predicate function should return true or false.
+       @returns true if any element in seq returns true for predicate, else returns false.
+
+       >>> there_exists (lambda c: c > 5,(6,7,8,9))
+       True
+
+       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+       """
+
+       for i in seq:
+               if pred(i):
+                       return True
+       return False
+
+
+def func_repeat(quantity, func, *args, **kwd):
+       """
+       Meant to be in connection with "reduce"
+       """
+       for i in xrange(quantity):
+               yield func(*args, **kwd)
+
+
+def function_map(preds, item):
+       """
+       Meant to be in connection with "reduce"
+       """
+       results = (pred(item) for pred in preds)
+
+       return results
+
+
+def functional_if(combiner, preds, item):
+       """
+       Combines the result of a list of predicates applied to item according to combiner
+
+       @see any, every for example combiners
+       """
+       pass_bool = lambda b: b
+
+       bool_results = function_map(preds, item)
+       return combiner(pass_bool, bool_results)
+
+
+def pushback_itr(itr):
+       """
+       >>> list(pushback_itr(xrange(5)))
+       [0, 1, 2, 3, 4]
+       >>>
+       >>> first = True
+       >>> itr = pushback_itr(xrange(5))
+       >>> for i in itr:
+       ...     print i
+       ...     if first and i == 2:
+       ...             first = False
+       ...             print itr.send(i)
+       0
+       1
+       2
+       None
+       2
+       3
+       4
+       >>>
+       >>> first = True
+       >>> itr = pushback_itr(xrange(5))
+       >>> for i in itr:
+       ...     print i
+       ...     if first and i == 2:
+       ...             first = False
+       ...             print itr.send(i)
+       ...             print itr.send(i)
+       0
+       1
+       2
+       None
+       None
+       2
+       2
+       3
+       4
+       >>>
+       >>> itr = pushback_itr(xrange(5))
+       >>> print itr.next()
+       0
+       >>> print itr.next()
+       1
+       >>> print itr.send(10)
+       None
+       >>> print itr.next()
+       10
+       >>> print itr.next()
+       2
+       >>> print itr.send(20)
+       None
+       >>> print itr.send(30)
+       None
+       >>> print itr.send(40)
+       None
+       >>> print itr.next()
+       40
+       >>> print itr.next()
+       30
+       >>> print itr.send(50)
+       None
+       >>> print itr.next()
+       50
+       >>> print itr.next()
+       20
+       >>> print itr.next()
+       3
+       >>> print itr.next()
+       4
+       """
+       for item in itr:
+               maybePushedBack = yield item
+               queue = []
+               while queue or maybePushedBack is not None:
+                       if maybePushedBack is not None:
+                               queue.append(maybePushedBack)
+                               maybePushedBack = yield None
+                       else:
+                               item = queue.pop()
+                               maybePushedBack = yield item
+
+
+def itr_available(queue, initiallyBlock = False):
+       if initiallyBlock:
+               yield queue.get()
+       while not queue.empty():
+               yield queue.get_nowait()
+
+
+class BloomFilter(object):
+       """
+       http://en.wikipedia.org/wiki/Bloom_filter
+       Sources:
+       http://code.activestate.com/recipes/577684-bloom-filter/
+       http://code.activestate.com/recipes/577686-bloom-filter/
+
+       >>> from random import sample
+       >>> from string import ascii_letters
+       >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut
+       ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas
+       ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota
+       ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey
+       ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon
+       ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah
+       ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split()
+       >>> bf = BloomFilter(num_bits=1000, num_probes=14)
+       >>> for state in states:
+       ...     bf.add(state)
+       >>> numStatesFound = sum(state in bf for state in states)
+       >>> numStatesFound, len(states)
+       (50, 50)
+       >>> trials = 100
+       >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials))
+       >>> numGarbageFound, trials
+       (0, 100)
+       """
+
+       def __init__(self, num_bits, num_probes):
+               num_words = (num_bits + 31) // 32
+               self._arr = array.array('B', [0]) * num_words
+               self._num_probes = num_probes
+
+       def add(self, key):
+               for i, mask in self._get_probes(key):
+                       self._arr[i] |= mask
+
+       def union(self, bfilter):
+               if self._match_template(bfilter):
+                       for i, b in enumerate(bfilter._arr):
+                               self._arr[i] |= b
+               else:
+                       # Union b/w two unrelated bloom filter raises this
+                       raise ValueError("Mismatched bloom filters")
+
+       def intersection(self, bfilter):
+               if self._match_template(bfilter):
+                       for i, b in enumerate(bfilter._arr):
+                               self._arr[i] &= b
+               else:
+                       # Intersection b/w two unrelated bloom filter raises this
+                       raise ValueError("Mismatched bloom filters")
+
+       def __contains__(self, key):
+               return all(self._arr[i] & mask for i, mask in self._get_probes(key))
+
+       def _match_template(self, bfilter):
+               return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes
+
+       def _get_probes(self, key):
+               hasher = random.Random(key).randrange
+               for _ in range(self._num_probes):
+                       array_index = hasher(len(self._arr))
+                       bit_index = hasher(32)
+                       yield array_index, 1 << bit_index
+
+
+if __name__ == "__main__":
+       import doctest
+       print doctest.testmod()
diff --git a/dialcentral/util/concurrent.py b/dialcentral/util/concurrent.py
new file mode 100644 (file)
index 0000000..f5f6e1d
--- /dev/null
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import os
+import errno
+import time
+import functools
+import contextlib
+import logging
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class AsyncTaskQueue(object):
+
+       def __init__(self, taskPool):
+               self._asyncs = []
+               self._taskPool = taskPool
+
+       def add_async(self, func):
+               self.flush()
+               a = AsyncGeneratorTask(self._taskPool, func)
+               self._asyncs.append(a)
+               return a
+
+       def flush(self):
+               self._asyncs = [a for a in self._asyncs if not a.isDone]
+
+
+class AsyncGeneratorTask(object):
+
+       def __init__(self, pool, func):
+               self._pool = pool
+               self._func = func
+               self._run = None
+               self._isDone = False
+
+       @property
+       def isDone(self):
+               return self._isDone
+
+       def start(self, *args, **kwds):
+               assert self._run is None, "Task already started"
+               self._run = self._func(*args, **kwds)
+               trampoline, args, kwds = self._run.send(None) # priming the function
+               self._pool.add_task(
+                       trampoline,
+                       args,
+                       kwds,
+                       self.on_success,
+                       self.on_error,
+               )
+
+       @misc.log_exception(_moduleLogger)
+       def on_success(self, result):
+               _moduleLogger.debug("Processing success for: %r", self._func)
+               try:
+                       trampoline, args, kwds = self._run.send(result)
+               except StopIteration, e:
+                       self._isDone = True
+               else:
+                       self._pool.add_task(
+                               trampoline,
+                               args,
+                               kwds,
+                               self.on_success,
+                               self.on_error,
+                       )
+
+       @misc.log_exception(_moduleLogger)
+       def on_error(self, error):
+               _moduleLogger.debug("Processing error for: %r", self._func)
+               try:
+                       trampoline, args, kwds = self._run.throw(error)
+               except StopIteration, e:
+                       self._isDone = True
+               else:
+                       self._pool.add_task(
+                               trampoline,
+                               args,
+                               kwds,
+                               self.on_success,
+                               self.on_error,
+                       )
+
+       def __repr__(self):
+               return "<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)
diff --git a/dialcentral/util/coroutines.py b/dialcentral/util/coroutines.py
new file mode 100755 (executable)
index 0000000..b1e539e
--- /dev/null
@@ -0,0 +1,623 @@
+#!/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
diff --git a/dialcentral/util/go_utils.py b/dialcentral/util/go_utils.py
new file mode 100644 (file)
index 0000000..61e731d
--- /dev/null
@@ -0,0 +1,274 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import time
+import functools
+import threading
+import Queue
+import logging
+
+import gobject
+
+import algorithms
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+def make_idler(func):
+       """
+       Decorator that makes a generator-function into a function that will continue execution on next call
+       """
+       a = []
+
+       @functools.wraps(func)
+       def decorated_func(*args, **kwds):
+               if not a:
+                       a.append(func(*args, **kwds))
+               try:
+                       a[0].next()
+                       return True
+               except StopIteration:
+                       del a[:]
+                       return False
+
+       return decorated_func
+
+
+def async(func):
+       """
+       Make a function mainloop friendly. the function will be called at the
+       next mainloop idle state.
+
+       >>> import misc
+       >>> misc.validate_decorator(async)
+       """
+
+       @functools.wraps(func)
+       def new_function(*args, **kwargs):
+
+               def async_function():
+                       func(*args, **kwargs)
+                       return False
+
+               gobject.idle_add(async_function)
+
+       return new_function
+
+
+class Async(object):
+
+       def __init__(self, func, once = True):
+               self.__func = func
+               self.__idleId = None
+               self.__once = once
+
+       def start(self):
+               assert self.__idleId is None
+               if self.__once:
+                       self.__idleId = gobject.idle_add(self._on_once)
+               else:
+                       self.__idleId = gobject.idle_add(self.__func)
+
+       def is_running(self):
+               return self.__idleId is not None
+
+       def cancel(self):
+               if self.__idleId is not None:
+                       gobject.source_remove(self.__idleId)
+                       self.__idleId = None
+
+       def __call__(self):
+               return self.start()
+
+       @misc.log_exception(_moduleLogger)
+       def _on_once(self):
+               self.cancel()
+               try:
+                       self.__func()
+               except Exception:
+                       pass
+               return False
+
+
+class Timeout(object):
+
+       def __init__(self, func, once = True):
+               self.__func = func
+               self.__timeoutId = None
+               self.__once = once
+
+       def start(self, **kwds):
+               assert self.__timeoutId is None
+
+               callback = self._on_once if self.__once else self.__func
+
+               assert len(kwds) == 1
+               timeoutInSeconds = kwds["seconds"]
+               assert 0 <= timeoutInSeconds
+
+               if timeoutInSeconds == 0:
+                       self.__timeoutId = gobject.idle_add(callback)
+               else:
+                       self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback)
+
+       def is_running(self):
+               return self.__timeoutId is not None
+
+       def cancel(self):
+               if self.__timeoutId is not None:
+                       gobject.source_remove(self.__timeoutId)
+                       self.__timeoutId = None
+
+       def __call__(self, **kwds):
+               return self.start(**kwds)
+
+       @misc.log_exception(_moduleLogger)
+       def _on_once(self):
+               self.cancel()
+               try:
+                       self.__func()
+               except Exception:
+                       pass
+               return False
+
+
+_QUEUE_EMPTY = object()
+
+
+class FutureThread(object):
+
+       def __init__(self):
+               self.__workQueue = Queue.Queue()
+               self.__thread = threading.Thread(
+                       name = type(self).__name__,
+                       target = self.__consume_queue,
+               )
+               self.__isRunning = True
+
+       def start(self):
+               self.__thread.start()
+
+       def stop(self):
+               self.__isRunning = False
+               for _ in algorithms.itr_available(self.__workQueue):
+                       pass # eat up queue to cut down dumb work
+               self.__workQueue.put(_QUEUE_EMPTY)
+
+       def clear_tasks(self):
+               for _ in algorithms.itr_available(self.__workQueue):
+                       pass # eat up queue to cut down dumb work
+
+       def add_task(self, func, args, kwds, on_success, on_error):
+               task = func, args, kwds, on_success, on_error
+               self.__workQueue.put(task)
+
+       @misc.log_exception(_moduleLogger)
+       def __trampoline_callback(self, on_success, on_error, isError, result):
+               if not self.__isRunning:
+                       if isError:
+                               _moduleLogger.error("Masking: %s" % (result, ))
+                       isError = True
+                       result = StopIteration("Cancelling all callbacks")
+               callback = on_success if not isError else on_error
+               try:
+                       callback(result)
+               except Exception:
+                       _moduleLogger.exception("Callback errored")
+               return False
+
+       @misc.log_exception(_moduleLogger)
+       def __consume_queue(self):
+               while True:
+                       task = self.__workQueue.get()
+                       if task is _QUEUE_EMPTY:
+                               break
+                       func, args, kwds, on_success, on_error = task
+
+                       try:
+                               result = func(*args, **kwds)
+                               isError = False
+                       except Exception, e:
+                               _moduleLogger.error("Error, passing it back to the main thread")
+                               result = e
+                               isError = True
+                       self.__workQueue.task_done()
+
+                       gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result)
+               _moduleLogger.debug("Shutting down worker thread")
+
+
+class AutoSignal(object):
+
+       def __init__(self, toplevel):
+               self.__disconnectPool = []
+               toplevel.connect("destroy", self.__on_destroy)
+
+       def connect_auto(self, widget, *args):
+               id = widget.connect(*args)
+               self.__disconnectPool.append((widget, id))
+
+       @misc.log_exception(_moduleLogger)
+       def __on_destroy(self, widget):
+               _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool)))
+               for widget, id in self.__disconnectPool:
+                       widget.disconnect(id)
+               del self.__disconnectPool[:]
+
+
+def throttled(minDelay, queue):
+       """
+       Throttle the calls to a function by queueing all the calls that happen
+       before the minimum delay
+
+       >>> import misc
+       >>> import Queue
+       >>> misc.validate_decorator(throttled(0, Queue.Queue()))
+       """
+
+       def actual_decorator(func):
+
+               lastCallTime = [None]
+
+               def process_queue():
+                       if 0 < len(queue):
+                               func, args, kwargs = queue.pop(0)
+                               lastCallTime[0] = time.time() * 1000
+                               func(*args, **kwargs)
+                       return False
+
+               @functools.wraps(func)
+               def new_function(*args, **kwargs):
+                       now = time.time() * 1000
+                       if (
+                               lastCallTime[0] is None or
+                               (now - lastCallTime >= minDelay)
+                       ):
+                               lastCallTime[0] = now
+                               func(*args, **kwargs)
+                       else:
+                               queue.append((func, args, kwargs))
+                               lastCallDelta = now - lastCallTime[0]
+                               processQueueTimeout = int(minDelay * len(queue) - lastCallDelta)
+                               gobject.timeout_add(processQueueTimeout, process_queue)
+
+               return new_function
+
+       return actual_decorator
+
+
+def _old_timeout_add_seconds(timeout, callback):
+       return gobject.timeout_add(timeout * 1000, callback)
+
+
+def _timeout_add_seconds(timeout, callback):
+       return gobject.timeout_add_seconds(timeout, callback)
+
+
+try:
+       gobject.timeout_add_seconds
+       timeout_add_seconds = _timeout_add_seconds
+except AttributeError:
+       timeout_add_seconds = _old_timeout_add_seconds
diff --git a/dialcentral/util/io.py b/dialcentral/util/io.py
new file mode 100644 (file)
index 0000000..4198f4b
--- /dev/null
@@ -0,0 +1,231 @@
+#!/usr/bin/env python
+
+
+from __future__ import with_statement
+
+import os
+import pickle
+import contextlib
+import itertools
+import codecs
+from xml.sax import saxutils
+import csv
+try:
+       import cStringIO as StringIO
+except ImportError:
+       import StringIO
+
+
+@contextlib.contextmanager
+def change_directory(directory):
+       previousDirectory = os.getcwd()
+       os.chdir(directory)
+       currentDirectory = os.getcwd()
+
+       try:
+               yield previousDirectory, currentDirectory
+       finally:
+               os.chdir(previousDirectory)
+
+
+@contextlib.contextmanager
+def pickled(filename):
+       """
+       Here is an example usage:
+       with pickled("foo.db") as p:
+               p("users", list).append(["srid", "passwd", 23])
+       """
+
+       if os.path.isfile(filename):
+               data = pickle.load(open(filename))
+       else:
+               data = {}
+
+       def getter(item, factory):
+               if item in data:
+                       return data[item]
+               else:
+                       data[item] = factory()
+                       return data[item]
+
+       yield getter
+
+       pickle.dump(data, open(filename, "w"))
+
+
+@contextlib.contextmanager
+def redirect(object_, attr, value):
+       """
+       >>> import sys
+       ... with redirect(sys, 'stdout', open('stdout', 'w')):
+       ...     print "hello"
+       ...
+       >>> print "we're back"
+       we're back
+       """
+       orig = getattr(object_, attr)
+       setattr(object_, attr, value)
+       try:
+               yield
+       finally:
+               setattr(object_, attr, orig)
+
+
+def pathsplit(path):
+       """
+       >>> pathsplit("/a/b/c")
+       ['', 'a', 'b', 'c']
+       >>> pathsplit("./plugins/builtins.ini")
+       ['.', 'plugins', 'builtins.ini']
+       """
+       pathParts = path.split(os.path.sep)
+       return pathParts
+
+
+def commonpath(l1, l2, common=None):
+       """
+       >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1'))
+       (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1'])
+       >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini"))
+       (['.', 'plugins'], [''], ['builtins.ini'])
+       >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins"))
+       (['.', 'plugins'], ['builtins'], [])
+       """
+       if common is None:
+               common = []
+
+       if l1 == l2:
+               return l1, [], []
+
+       for i, (leftDir, rightDir) in enumerate(zip(l1, l2)):
+               if leftDir != rightDir:
+                       return l1[0:i], l1[i:], l2[i:]
+       else:
+               if leftDir == rightDir:
+                       i += 1
+               return l1[0:i], l1[i:], l2[i:]
+
+
+def relpath(p1, p2):
+       """
+       >>> relpath('/', '/')
+       './'
+       >>> relpath('/a/b/c/d', '/')
+       '../../../../'
+       >>> relpath('/a/b/c/d', '/a/b/c1/d1')
+       '../../c1/d1'
+       >>> relpath('/a/b/c/d', '/a/b/c1/d1/')
+       '../../c1/d1'
+       >>> relpath("./plugins/builtins", "./plugins")
+       '../'
+       >>> relpath("./plugins/", "./plugins/builtins.ini")
+       'builtins.ini'
+       """
+       sourcePath = os.path.normpath(p1)
+       destPath = os.path.normpath(p2)
+
+       (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath))
+       if len(sourceOnly) or len(destOnly):
+               relParts = itertools.chain(
+                       (('..' + os.sep) * len(sourceOnly), ),
+                       destOnly,
+               )
+               return os.path.join(*relParts)
+       else:
+               return "."+os.sep
+
+
+class UTF8Recoder(object):
+       """
+       Iterator that reads an encoded stream and reencodes the input to UTF-8
+       """
+       def __init__(self, f, encoding):
+               self.reader = codecs.getreader(encoding)(f)
+
+       def __iter__(self):
+               return self
+
+       def next(self):
+               return self.reader.next().encode("utf-8")
+
+
+class UnicodeReader(object):
+       """
+       A CSV reader which will iterate over lines in the CSV file "f",
+       which is encoded in the given encoding.
+       """
+
+       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+               f = UTF8Recoder(f, encoding)
+               self.reader = csv.reader(f, dialect=dialect, **kwds)
+
+       def next(self):
+               row = self.reader.next()
+               return [unicode(s, "utf-8") for s in row]
+
+       def __iter__(self):
+               return self
+
+class UnicodeWriter(object):
+       """
+       A CSV writer which will write rows to CSV file "f",
+       which is encoded in the given encoding.
+       """
+
+       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+               # Redirect output to a queue
+               self.queue = StringIO.StringIO()
+               self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
+               self.stream = f
+               self.encoder = codecs.getincrementalencoder(encoding)()
+
+       def writerow(self, row):
+               self.writer.writerow([s.encode("utf-8") for s in row])
+               # Fetch UTF-8 output from the queue ...
+               data = self.queue.getvalue()
+               data = data.decode("utf-8")
+               # ... and reencode it into the target encoding
+               data = self.encoder.encode(data)
+               # write to the target stream
+               self.stream.write(data)
+               # empty queue
+               self.queue.truncate(0)
+
+       def writerows(self, rows):
+               for row in rows:
+                       self.writerow(row)
+
+
+def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
+       # csv.py doesn't do Unicode; encode temporarily as UTF-8:
+       csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
+                                                       dialect=dialect, **kwargs)
+       for row in csv_reader:
+               # decode UTF-8 back to Unicode, cell by cell:
+               yield [unicode(cell, 'utf-8') for cell in row]
+
+
+def utf_8_encoder(unicode_csv_data):
+       for line in unicode_csv_data:
+               yield line.encode('utf-8')
+
+
+_UNESCAPE_ENTITIES = {
+ "&quot;": '"',
+ "&nbsp;": " ",
+ "&#39;": "'",
+}
+
+
+_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys()))
+del _ESCAPE_ENTITIES[" "]
+
+
+def unescape(text):
+       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+       return plain
+
+
+def escape(text):
+       fancy = saxutils.escape(text, _ESCAPE_ENTITIES)
+       return fancy
diff --git a/dialcentral/util/linux.py b/dialcentral/util/linux.py
new file mode 100644 (file)
index 0000000..4e77445
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+
+
+import os
+import logging
+
+try:
+       from xdg import BaseDirectory as _BaseDirectory
+       BaseDirectory = _BaseDirectory
+except ImportError:
+       BaseDirectory = None
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_libc = None
+
+
+def set_process_name(name):
+       try: # change process name for killall
+               global _libc
+               if _libc is None:
+                       import ctypes
+                       _libc = ctypes.CDLL('libc.so.6')
+               _libc.prctl(15, name, 0, 0, 0)
+       except Exception, e:
+               _moduleLogger.warning('Unable to set processName: %s" % e')
+
+
+def get_new_resource(resourceType, resource, name):
+       if BaseDirectory is not None:
+               if resourceType == "data":
+                       base = BaseDirectory.xdg_data_home
+                       if base == "/usr/share/mime":
+                               # Ugly hack because somehow Maemo 4.1 seems to be set to this
+                               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+               elif resourceType == "config":
+                       base = BaseDirectory.xdg_config_home
+               elif resourceType == "cache":
+                       base = BaseDirectory.xdg_cache_home
+               else:
+                       raise RuntimeError("Unknown type: "+resourceType)
+       else:
+               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+
+       filePath = os.path.join(base, resource, name)
+       dirPath = os.path.dirname(filePath)
+       if not os.path.exists(dirPath):
+               # Looking before I leap to not mask errors
+               os.makedirs(dirPath)
+
+       return filePath
+
+
+def get_existing_resource(resourceType, resource, name):
+       if BaseDirectory is not None:
+               if resourceType == "data":
+                       base = BaseDirectory.xdg_data_home
+               elif resourceType == "config":
+                       base = BaseDirectory.xdg_config_home
+               elif resourceType == "cache":
+                       base = BaseDirectory.xdg_cache_home
+               else:
+                       raise RuntimeError("Unknown type: "+resourceType)
+       else:
+               base = None
+
+       if base is not None:
+               finalPath = os.path.join(base, name)
+               if os.path.exists(finalPath):
+                       return finalPath
+
+       altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+       finalPath = os.path.join(altBase, name)
+       if os.path.exists(finalPath):
+               return finalPath
+       else:
+               raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), ))
diff --git a/dialcentral/util/misc.py b/dialcentral/util/misc.py
new file mode 100644 (file)
index 0000000..9b8d88c
--- /dev/null
@@ -0,0 +1,900 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import re
+import cPickle
+
+import functools
+import contextlib
+import inspect
+
+import optparse
+import traceback
+import warnings
+import string
+
+
+class AnyData(object):
+
+       pass
+
+
+_indentationLevel = [0]
+
+
+def log_call(logger):
+
+       def log_call_decorator(func):
+
+               @functools.wraps(func)
+               def wrapper(*args, **kwds):
+                       logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, ))
+                       _indentationLevel[0] += 1
+                       try:
+                               return func(*args, **kwds)
+                       finally:
+                               _indentationLevel[0] -= 1
+                               logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, ))
+
+               return wrapper
+
+       return log_call_decorator
+
+
+def log_exception(logger):
+
+       def log_exception_decorator(func):
+
+               @functools.wraps(func)
+               def wrapper(*args, **kwds):
+                       try:
+                               return func(*args, **kwds)
+                       except Exception:
+                               logger.exception(func.__name__)
+                               raise
+
+               return wrapper
+
+       return log_exception_decorator
+
+
+def printfmt(template):
+       """
+       This hides having to create the Template object and call substitute/safe_substitute on it. For example:
+
+       >>> num = 10
+       >>> word = "spam"
+       >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
+       I would like to order 10 units of spam, please
+       """
+       frame = inspect.stack()[-1][0]
+       try:
+               print string.Template(template).safe_substitute(frame.f_locals)
+       finally:
+               del frame
+
+
+def is_special(name):
+       return name.startswith("__") and name.endswith("__")
+
+
+def is_private(name):
+       return name.startswith("_") and not is_special(name)
+
+
+def privatize(clsName, attributeName):
+       """
+       At runtime, make an attributeName private
+
+       Example:
+       >>> class Test(object):
+       ...     pass
+       ...
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print dir(Test)
+       ... except:
+       ...     print "Not Found"
+       Not Found
+       >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print "Found"
+       ... except:
+       ...     print dir(Test)
+       0
+       Found
+       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+       Hello World
+       >>>
+       >>> is_private(privatize(Test.__name__, "me"))
+       True
+       >>> is_special(privatize(Test.__name__, "me"))
+       False
+       """
+       return "".join(["_", clsName, "__", attributeName])
+
+
+def obfuscate(clsName, attributeName):
+       """
+       At runtime, turn a private name into the obfuscated form
+
+       Example:
+       >>> class Test(object):
+       ...     __me = "Hello World"
+       ...
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print "Found"
+       ... except:
+       ...     print dir(Test)
+       0
+       Found
+       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+       Hello World
+       >>> is_private(obfuscate(Test.__name__, "__me"))
+       True
+       >>> is_special(obfuscate(Test.__name__, "__me"))
+       False
+       """
+       return "".join(["_", clsName, attributeName])
+
+
+class PAOptionParser(optparse.OptionParser, object):
+       """
+       >>> if __name__ == '__main__':
+       ...     #parser = PAOptionParser("My usage str")
+       ...     parser = PAOptionParser()
+       ...     parser.add_posarg("Foo", help="Foo usage")
+       ...     parser.add_posarg("Bar", dest="bar_dest")
+       ...     parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
+       ...     parser.add_option('--stocksym', dest='symbol')
+       ...     values, args = parser.parse_args()
+       ...     print values, args
+       ...
+
+       python mycp.py  -h
+       python mycp.py
+       python mycp.py  foo
+       python mycp.py  foo bar
+
+       python mycp.py foo bar lava
+       Usage: pa.py <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
diff --git a/dialcentral/util/overloading.py b/dialcentral/util/overloading.py
new file mode 100644 (file)
index 0000000..89cb738
--- /dev/null
@@ -0,0 +1,256 @@
+#!/usr/bin/env python
+import new
+
+# Make the environment more like Python 3.0
+__metaclass__ = type
+from itertools import izip as zip
+import textwrap
+import inspect
+
+
+__all__ = [
+       "AnyType",
+       "overloaded"
+]
+
+
+AnyType = object
+
+
+class overloaded:
+       """
+       Dynamically overloaded functions.
+
+       This is an implementation of (dynamically, or run-time) overloaded
+       functions; also known as generic functions or multi-methods.
+
+       The dispatch algorithm uses the types of all argument for dispatch,
+       similar to (compile-time) overloaded functions or methods in C++ and
+       Java.
+
+       Most of the complexity in the algorithm comes from the need to support
+       subclasses in call signatures.  For example, if an function is
+       registered for a signature (T1, T2), then a call with a signature (S1,
+       S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass
+       of T2, and there are no other more specific matches (see below).
+
+       If there are multiple matches and one of those doesn't *dominate* all
+       others, the match is deemed ambiguous and an exception is raised.  A
+       subtlety here: if, after removing the dominated matches, there are
+       still multiple matches left, but they all map to the same function,
+       then the match is not deemed ambiguous and that function is used.
+       Read the method find_func() below for details.
+
+       @note Python 2.5 is required due to the use of predicates any() and all().
+       @note only supports positional arguments
+
+       @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514
+
+       >>> import misc
+       >>> misc.validate_decorator (overloaded)
+       >>>
+       >>>
+       >>>
+       >>>
+       >>> #################
+       >>> #Basics, with reusing names and without
+       >>> @overloaded
+       ... def foo(x):
+       ...     "prints x"
+       ...     print x
+       ...
+       >>> @foo.register(int)
+       ... def foo(x):
+       ...     "prints the hex representation of x"
+       ...     print hex(x)
+       ...
+       >>> from types import DictType
+       >>> @foo.register(DictType)
+       ... def foo_dict(x):
+       ...     "prints the keys of x"
+       ...     print [k for k in x.iterkeys()]
+       ...
+       >>> #combines all of the doc strings to help keep track of the specializations
+       >>> foo.__doc__  # doctest: +ELLIPSIS
+       "prints x\\n\\n...overloading.foo (<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))
diff --git a/dialcentral/util/qore_utils.py b/dialcentral/util/qore_utils.py
new file mode 100644 (file)
index 0000000..153558d
--- /dev/null
@@ -0,0 +1,99 @@
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QThread44(QtCore.QThread):
+       """
+       This is to imitate QThread in Qt 4.4+ for when running on older version
+       See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
+       (On Lucid I have Qt 4.7 and this is still an issue)
+       """
+
+       def __init__(self, parent = None):
+               QtCore.QThread.__init__(self, parent)
+
+       def run(self):
+               self.exec_()
+
+
+class _WorkerThread(QtCore.QObject):
+
+       _taskComplete  = qt_compat.Signal(object)
+
+       def __init__(self, futureThread):
+               QtCore.QObject.__init__(self)
+               self._futureThread = futureThread
+               self._futureThread._addTask.connect(self._on_task_added)
+               self._taskComplete.connect(self._futureThread._on_task_complete)
+
+       @qt_compat.Slot(object)
+       def _on_task_added(self, task):
+               self.__on_task_added(task)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_added(self, task):
+               if not self._futureThread._isRunning:
+                       _moduleLogger.error("Dropping task")
+
+               func, args, kwds, on_success, on_error = task
+
+               try:
+                       result = func(*args, **kwds)
+                       isError = False
+               except Exception, e:
+                       _moduleLogger.error("Error, passing it back to the main thread")
+                       result = e
+                       isError = True
+
+               taskResult = on_success, on_error, isError, result
+               self._taskComplete.emit(taskResult)
+
+
+class FutureThread(QtCore.QObject):
+
+       _addTask = qt_compat.Signal(object)
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._thread = QThread44()
+               self._isRunning = False
+               self._worker = _WorkerThread(self)
+               self._worker.moveToThread(self._thread)
+
+       def start(self):
+               self._thread.start()
+               self._isRunning = True
+
+       def stop(self):
+               self._isRunning = False
+               self._thread.quit()
+
+       def add_task(self, func, args, kwds, on_success, on_error):
+               assert self._isRunning, "Task queue not started"
+               task = func, args, kwds, on_success, on_error
+               self._addTask.emit(task)
+
+       @qt_compat.Slot(object)
+       def _on_task_complete(self, taskResult):
+               self.__on_task_complete(taskResult)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_complete(self, taskResult):
+               on_success, on_error, isError, result = taskResult
+               if not self._isRunning:
+                       if isError:
+                               _moduleLogger.error("Masking: %s" % (result, ))
+                       isError = True
+                       result = StopIteration("Cancelling all callbacks")
+               callback = on_success if not isError else on_error
+               try:
+                       callback(result)
+               except Exception:
+                       _moduleLogger.exception("Callback errored")
diff --git a/dialcentral/util/qt_compat.py b/dialcentral/util/qt_compat.py
new file mode 100644 (file)
index 0000000..2ab7fa4
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+#try:
+#      import PySide.QtCore as _QtCore
+#      QtCore = _QtCore
+#      USES_PYSIDE = True
+#except ImportError:
+if True:
+       import sip
+       sip.setapi('QString', 2)
+       sip.setapi('QVariant', 2)
+       import PyQt4.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = False
+
+
+def _pyside_import_module(moduleName):
+       pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+def _pyqt4_import_module(moduleName):
+       pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+if USES_PYSIDE:
+       import_module = _pyside_import_module
+
+       Signal = QtCore.Signal
+       Slot = QtCore.Slot
+       Property = QtCore.Property
+else:
+       import_module = _pyqt4_import_module
+
+       Signal = QtCore.pyqtSignal
+       Slot = QtCore.pyqtSlot
+       Property = QtCore.pyqtProperty
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/dialcentral/util/qtpie.py b/dialcentral/util/qtpie.py
new file mode 100755 (executable)
index 0000000..6b77d5d
--- /dev/null
@@ -0,0 +1,1094 @@
+#!/usr/bin/env python
+
+import math
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_TWOPI = 2 * math.pi
+
+
+def _radius_at(center, pos):
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       return radius
+
+
+def _angle_at(center, pos):
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       angle = math.acos(xDelta / radius)
+       if 0 <= yDelta:
+               angle = _TWOPI - angle
+
+       return angle
+
+
+class QActionPieItem(object):
+
+       def __init__(self, action, weight = 1):
+               self._action = action
+               self._weight = weight
+
+       def action(self):
+               return self._action
+
+       def setWeight(self, weight):
+               self._weight = weight
+
+       def weight(self):
+               return self._weight
+
+       def setEnabled(self, enabled = True):
+               self._action.setEnabled(enabled)
+
+       def isEnabled(self):
+               return self._action.isEnabled()
+
+
+class PieFiling(object):
+
+       INNER_RADIUS_DEFAULT = 64
+       OUTER_RADIUS_DEFAULT = 192
+
+       SELECTION_CENTER = -1
+       SELECTION_NONE = -2
+
+       NULL_CENTER = QActionPieItem(QtGui.QAction(None))
+
+       def __init__(self):
+               self._innerRadius = self.INNER_RADIUS_DEFAULT
+               self._outerRadius = self.OUTER_RADIUS_DEFAULT
+               self._children = []
+               self._center = self.NULL_CENTER
+
+               self._cacheIndexToAngle = {}
+               self._cacheTotalWeight = 0
+
+       def insertItem(self, item, index = -1):
+               self._children.insert(index, item)
+               self._invalidate_cache()
+
+       def removeItemAt(self, index):
+               item = self._children.pop(index)
+               self._invalidate_cache()
+
+       def set_center(self, item):
+               if item is None:
+                       item = self.NULL_CENTER
+               self._center = item
+
+       def center(self):
+               return self._center
+
+       def clear(self):
+               del self._children[:]
+               self._center = self.NULL_CENTER
+               self._invalidate_cache()
+
+       def itemAt(self, index):
+               return self._children[index]
+
+       def indexAt(self, center, point):
+               return self._angle_to_index(_angle_at(center, point))
+
+       def innerRadius(self):
+               return self._innerRadius
+
+       def setInnerRadius(self, radius):
+               self._innerRadius = radius
+
+       def outerRadius(self):
+               return self._outerRadius
+
+       def setOuterRadius(self, radius):
+               self._outerRadius = radius
+
+       def __iter__(self):
+               return iter(self._children)
+
+       def __len__(self):
+               return len(self._children)
+
+       def __getitem__(self, index):
+               return self._children[index]
+
+       def _invalidate_cache(self):
+               self._cacheIndexToAngle.clear()
+               self._cacheTotalWeight = sum(child.weight() for child in self._children)
+               if self._cacheTotalWeight == 0:
+                       self._cacheTotalWeight = 1
+
+       def _index_to_angle(self, index, isShifted):
+               key = index, isShifted
+               if key in self._cacheIndexToAngle:
+                       return self._cacheIndexToAngle[key]
+               index = index % len(self._children)
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               angle = math.pi / 2
+               if isShifted:
+                       if self._children:
+                               angle -= (self._children[0].weight() * baseAngle) / 2
+                       else:
+                               angle -= baseAngle / 2
+               while angle < 0:
+                       angle += _TWOPI
+
+               for i, child in enumerate(self._children):
+                       if index < i:
+                               break
+                       angle += child.weight() * baseAngle
+               while _TWOPI < angle:
+                       angle -= _TWOPI
+
+               self._cacheIndexToAngle[key] = angle
+               return angle
+
+       def _angle_to_index(self, angle):
+               numChildren = len(self._children)
+               if numChildren == 0:
+                       return self.SELECTION_CENTER
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
+               while iterAngle < 0:
+                       iterAngle += _TWOPI
+
+               oldIterAngle = iterAngle
+               for index, child in enumerate(self._children):
+                       iterAngle += child.weight() * baseAngle
+                       if oldIterAngle < angle and angle <= iterAngle:
+                               return index - 1 if index != 0 else numChildren - 1
+                       elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
+                               return index - 1 if index != 0 else numChildren - 1
+                       oldIterAngle = iterAngle
+
+
+class PieArtist(object):
+
+       ICON_SIZE_DEFAULT = 48
+
+       SHAPE_CIRCLE = "circle"
+       SHAPE_SQUARE = "square"
+       DEFAULT_SHAPE = SHAPE_SQUARE
+
+       BACKGROUND_FILL = "fill"
+       BACKGROUND_NOFILL = "no fill"
+
+       def __init__(self, filing, background = BACKGROUND_FILL):
+               self._filing = filing
+
+               self._cachedOuterRadius = self._filing.outerRadius()
+               self._cachedInnerRadius = self._filing.innerRadius()
+               canvasSize = self._cachedOuterRadius * 2 + 1
+               self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
+               self._mask = None
+               self._backgroundState = background
+               self.palette = None
+
+       def pieSize(self):
+               diameter = self._filing.outerRadius() * 2 + 1
+               return QtCore.QSize(diameter, diameter)
+
+       def centerSize(self):
+               painter = QtGui.QPainter(self._canvas)
+               text = self._filing.center().action().text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               return QtCore.QSize(
+                       textWidth + self.ICON_SIZE_DEFAULT,
+                       max(textHeight, self.ICON_SIZE_DEFAULT),
+               )
+
+       def show(self, palette):
+               self.palette = palette
+
+               if (
+                       self._cachedOuterRadius != self._filing.outerRadius() or
+                       self._cachedInnerRadius != self._filing.innerRadius()
+               ):
+                       self._cachedOuterRadius = self._filing.outerRadius()
+                       self._cachedInnerRadius = self._filing.innerRadius()
+                       self._canvas = self._canvas.scaled(self.pieSize())
+
+               if self._mask is None:
+                       self._mask = QtGui.QBitmap(self._canvas.size())
+                       self._mask.fill(QtCore.Qt.color0)
+                       self._generate_mask(self._mask)
+                       self._canvas.setMask(self._mask)
+               return self._mask
+
+       def hide(self):
+               self.palette = None
+
+       def paint(self, selectionIndex):
+               painter = QtGui.QPainter(self._canvas)
+               painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
+
+               self.paintPainter(selectionIndex, painter)
+
+               return self._canvas
+
+       def paintPainter(self, selectionIndex, painter):
+               adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1)
+
+               numChildren = len(self._filing)
+               if numChildren == 0:
+                       self._paint_center_background(painter, adjustmentRect, selectionIndex)
+                       self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
+                       return self._canvas
+               else:
+                       for i in xrange(len(self._filing)):
+                               self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
+
+               self._paint_center_background(painter, adjustmentRect, selectionIndex)
+               self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
+
+               for i in xrange(len(self._filing)):
+                       self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex)
+
+       def _generate_mask(self, mask):
+               """
+               Specifies on the mask the shape of the pie menu
+               """
+               painter = QtGui.QPainter(mask)
+               painter.setPen(QtCore.Qt.color1)
+               painter.setBrush(QtCore.Qt.color1)
+               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                       painter.drawRect(mask.rect())
+               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                       painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
+               else:
+                       raise NotImplementedError(self.DEFAULT_SHAPE)
+
+       def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
+               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                       currentWidth = adjustmentRect.width()
+                       newWidth = math.sqrt(2) * currentWidth
+                       dx = (newWidth - currentWidth) / 2
+                       adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
+               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                       pass
+               else:
+                       raise NotImplementedError(self.DEFAULT_SHAPE)
+
+               if self._backgroundState == self.BACKGROUND_NOFILL:
+                       painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
+                       painter.setPen(self.palette.highlight().color())
+               else:
+                       if i == selectionIndex and self._filing[i].isEnabled():
+                               painter.setBrush(self.palette.highlight())
+                               painter.setPen(self.palette.highlight().color())
+                       else:
+                               painter.setBrush(self.palette.window())
+                               painter.setPen(self.palette.window().color())
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               size = b - a
+               if size < 0:
+                       size += _TWOPI
+
+               startAngleInDeg = (a * 360 * 16) / _TWOPI
+               sizeInDeg = (size * 360 * 16) / _TWOPI
+               painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
+
+       def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex):
+               child = self._filing[i]
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               middleAngle = (a + b) / 2
+               averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
+
+               sliceX = averageRadius * math.cos(middleAngle)
+               sliceY = - averageRadius * math.sin(middleAngle)
+
+               piePos = adjustmentRect.center()
+               pieX = piePos.x()
+               pieY = piePos.y()
+               self._paint_label(
+                       painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
+               )
+
+       def _paint_label(self, painter, action, isSelected, x, y):
+               text = action.text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               icon = action.icon().pixmap(
+                       QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
+                       QtGui.QIcon.Normal,
+                       QtGui.QIcon.On,
+               )
+               iconWidth = icon.width()
+               iconHeight = icon.width()
+               averageWidth = (iconWidth + textWidth)/2
+               if not icon.isNull():
+                       iconRect = QtCore.QRect(
+                               x - averageWidth,
+                               y - iconHeight/2,
+                               iconWidth,
+                               iconHeight,
+                       )
+
+                       painter.drawPixmap(iconRect, icon)
+
+               if text:
+                       if isSelected:
+                               if action.isEnabled():
+                                       pen = self.palette.highlightedText()
+                                       brush = self.palette.highlight()
+                               else:
+                                       pen = self.palette.mid()
+                                       brush = self.palette.window()
+                       else:
+                               if action.isEnabled():
+                                       pen = self.palette.windowText()
+                               else:
+                                       pen = self.palette.mid()
+                               brush = self.palette.window()
+
+                       leftX = x - averageWidth + iconWidth
+                       topY = y + textHeight/2
+                       painter.setPen(pen.color())
+                       painter.setBrush(brush)
+                       painter.drawText(leftX, topY, text)
+
+       def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
+               if self._backgroundState == self.BACKGROUND_NOFILL:
+                       return
+               if len(self._filing) == 0:
+                       if self._backgroundState == self.BACKGROUND_NOFILL:
+                               painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
+                       else:
+                               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                                       painter.setBrush(self.palette.highlight())
+                               else:
+                                       painter.setBrush(self.palette.window())
+                       painter.setPen(self.palette.mid().color())
+
+                       painter.drawRect(adjustmentRect)
+               else:
+                       dark = self.palette.mid().color()
+                       light = self.palette.light().color()
+                       if self._backgroundState == self.BACKGROUND_NOFILL:
+                               background = QtGui.QBrush(QtCore.Qt.transparent)
+                       else:
+                               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                                       background = self.palette.highlight().color()
+                               else:
+                                       background = self.palette.window().color()
+
+                       innerRadius = self._cachedInnerRadius
+                       adjustmentCenterPos = adjustmentRect.center()
+                       innerRect = QtCore.QRect(
+                               adjustmentCenterPos.x() - innerRadius,
+                               adjustmentCenterPos.y() - innerRadius,
+                               innerRadius * 2 + 1,
+                               innerRadius * 2 + 1,
+                       )
+
+                       painter.setPen(QtCore.Qt.NoPen)
+                       painter.setBrush(background)
+                       painter.drawPie(innerRect, 0, 360 * 16)
+
+                       if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                               pass
+                       elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                               painter.setPen(QtGui.QPen(dark, 1))
+                               painter.setBrush(QtCore.Qt.NoBrush)
+                               painter.drawEllipse(adjustmentRect)
+                       else:
+                               raise NotImplementedError(self.DEFAULT_SHAPE)
+
+       def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex):
+               centerPos = adjustmentRect.center()
+               pieX = centerPos.x()
+               pieY = centerPos.y()
+
+               x = pieX
+               y = pieY
+
+               self._paint_label(
+                       painter,
+                       self._filing.center().action(),
+                       selectionIndex == PieFiling.SELECTION_CENTER,
+                       x, y
+               )
+
+
+class QPieDisplay(QtGui.QWidget):
+
+       def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
+               QtGui.QWidget.__init__(self, parent, flags)
+               self._filing = filing
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+               offset = (self.size() - canvas.size()) / 2
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def selectAt(self, index):
+               oldIndex = self._selectionIndex
+               self._selectionIndex = index
+               if self.isVisible():
+                       self.update()
+
+
+class QPieButton(QtGui.QWidget):
+
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
+
+       BUTTON_RADIUS = 24
+       DELAY = 250
+
+       def __init__(self, buttonSlice, parent = None, buttonSlices = None):
+               # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
+               # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
+               QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
+
+               self._filing = PieFiling()
+               self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+               self._buttonFiling = PieFiling()
+               self._buttonFiling.set_center(buttonSlice)
+               if buttonSlices is not None:
+                       for slice in buttonSlices:
+                               self._buttonFiling.insertItem(slice)
+               self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
+               self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL)
+               self._poppedUp = False
+               self._pressed = False
+
+               self._delayPopupTimer = QtCore.QTimer()
+               self._delayPopupTimer.setInterval(self.DELAY)
+               self._delayPopupTimer.setSingleShot(True)
+               self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
+               self._popupLocation = None
+
+               self._mousePosition = None
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
+               self.setSizePolicy(
+                       QtGui.QSizePolicy(
+                               QtGui.QSizePolicy.MinimumExpanding,
+                               QtGui.QSizePolicy.MinimumExpanding,
+                       )
+               )
+
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+
+       def set_center(self, item):
+               self._filing.set_center(item)
+
+       def set_button(self, item):
+               self.update()
+
+       def clear(self):
+               self._filing.clear()
+
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
+
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
+
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+
+       def buttonRadius(self):
+               return self._buttonFiling.outerRadius()
+
+       def setButtonRadius(self, radius):
+               self._buttonFiling.setOuterRadius(radius)
+               self._buttonFiling.setInnerRadius(radius / 2)
+               self._buttonArtist.show(self.palette())
+
+       def minimumSizeHint(self):
+               return self._buttonArtist.centerSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._mousePosition = lastMousePos
+               self._update_selection(self._cachedCenterPosition)
+
+               self.highlighted.emit(self._selectionIndex)
+
+               self._display.selectAt(self._selectionIndex)
+               self._pressed = True
+               self.update()
+               self._popupLocation = mouseEvent.globalPos()
+               self._delayPopupTimer.start()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_popup(self):
+               assert self._popupLocation is not None, "Widget location abuse"
+               self._popup_child(self._popupLocation)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
+               else:
+                       # Relative
+                       self._update_selection(
+                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+                               ignoreOuter = True,
+                       )
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self._display.selectAt(self._selectionIndex)
+
+               if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
+                       self._on_delayed_popup()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
+               else:
+                       # Relative
+                       self._update_selection(
+                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+                               ignoreOuter = True,
+                       )
+               self._mousePosition = None
+
+               self._activate_at(self._selectionIndex)
+               self._pressed = False
+               self.update()
+               self._hide_child()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
+                       else:
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Space]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       self._select_at(PieFiling.SELECTION_CENTER)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(self._selectionIndex)
+                       self._hide_child()
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(PieFiling.SELECTION_NONE)
+                       self._hide_child()
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def resizeEvent(self, resizeEvent):
+               self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
+               QtGui.QWidget.resizeEvent(self, resizeEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self._buttonArtist.show(self.palette())
+               self._cachedCenterPosition = self.rect().center()
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._display.hide()
+               self._select_at(PieFiling.SELECTION_NONE)
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
+               if self._poppedUp:
+                       selectionIndex = PieFiling.SELECTION_CENTER
+               else:
+                       selectionIndex = PieFiling.SELECTION_NONE
+
+               screen = QtGui.QStylePainter(self)
+               screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
+               option = QtGui.QStyleOptionButton()
+               option.initFrom(self)
+               option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
+
+               screen.drawControl(QtGui.QStyle.CE_PushButton, option)
+               self._buttonArtist.paintPainter(selectionIndex, screen)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
+       def _popup_child(self, position):
+               self._poppedUp = True
+               self.aboutToShow.emit()
+
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
+               position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
+               self._display.move(position)
+               self._display.show()
+
+               self.update()
+
+       def _hide_child(self):
+               self._poppedUp = False
+               self.aboutToHide.emit()
+               self._display.hide()
+               self.update()
+
+       def _select_at(self, index):
+               self._selectionIndex = index
+
+       def _update_selection(self, lastMousePos, ignoreOuter = False):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._select_at(PieFiling.SELECTION_CENTER)
+               elif radius <= self._filing.outerRadius() or ignoreOuter:
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._select_at(PieFiling.SELECTION_NONE)
+
+       def _activate_at(self, index):
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
+
+               if child.action().isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
+
+
+class QPieMenu(QtGui.QWidget):
+
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
+
+       def __init__(self, parent = None):
+               QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
+
+               self._filing = PieFiling()
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+               self._mousePosition = ()
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
+
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+               self.update()
+
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+               self.update()
+
+       def set_center(self, item):
+               self._filing.set_center(item)
+               self.update()
+
+       def clear(self):
+               self._filing.clear()
+               self.update()
+
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
+
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+               self.update()
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
+
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+               self.update()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = lastMousePos
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = ()
+
+               self._activate_at(self._selectionIndex)
+               self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
+                       else:
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+                       self._activate_at(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._activate_at(PieFiling.SELECTION_NONE)
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self.aboutToShow.emit()
+               self._cachedCenterPosition = self.rect().center()
+
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
+               self._update_selection(lastMousePos)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
+       def _select_at(self, index):
+               self._selectionIndex = index
+
+       def _update_selection(self, lastMousePos):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._selectionIndex = PieFiling.SELECTION_CENTER
+               elif radius <= self._filing.outerRadius():
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._selectionIndex = PieFiling.SELECTION_NONE
+
+       def _activate_at(self, index):
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       self.aboutToHide.emit()
+                       self.hide()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
+
+               if child.isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
+               self.aboutToHide.emit()
+               self.hide()
+
+
+def init_pies():
+       PieFiling.NULL_CENTER.setEnabled(False)
+
+
+def _print(msg):
+       print msg
+
+
+def _on_about_to_hide(app):
+       app.exit()
+
+
+if __name__ == "__main__":
+       app = QtGui.QApplication([])
+       init_pies()
+
+       if False:
+               pie = QPieMenu()
+               pie.show()
+
+       if False:
+               singleAction = QtGui.QAction(None)
+               singleAction.setText("Boo")
+               singleItem = QActionPieItem(singleAction)
+               spie = QPieMenu()
+               spie.insertItem(singleItem)
+               spie.show()
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoItem = QActionPieItem(twoAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieMenu()
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+
+       if True:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieMenu()
+               mpie.set_center(iconItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               pieFiling = PieFiling()
+               pieFiling.set_center(iconItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(twoItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(iconTextItem)
+               mpie = QPieDisplay(pieFiling)
+               mpie.show()
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieButton(iconItem)
+               mpie.set_center(iconItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
+
+       app.exec_()
diff --git a/dialcentral/util/qtpieboard.py b/dialcentral/util/qtpieboard.py
new file mode 100755 (executable)
index 0000000..50ae9ae
--- /dev/null
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+
+
+from __future__ import division
+
+import os
+import warnings
+
+import qt_compat
+QtGui = qt_compat.import_module("QtGui")
+
+import qtpie
+
+
+class PieKeyboard(object):
+
+       SLICE_CENTER = -1
+       SLICE_NORTH = 0
+       SLICE_NORTH_WEST = 1
+       SLICE_WEST = 2
+       SLICE_SOUTH_WEST = 3
+       SLICE_SOUTH = 4
+       SLICE_SOUTH_EAST = 5
+       SLICE_EAST = 6
+       SLICE_NORTH_EAST = 7
+
+       MAX_ANGULAR_SLICES = 8
+
+       SLICE_DIRECTIONS = [
+               SLICE_CENTER,
+               SLICE_NORTH,
+               SLICE_NORTH_WEST,
+               SLICE_WEST,
+               SLICE_SOUTH_WEST,
+               SLICE_SOUTH,
+               SLICE_SOUTH_EAST,
+               SLICE_EAST,
+               SLICE_NORTH_EAST,
+       ]
+
+       SLICE_DIRECTION_NAMES = [
+               "CENTER",
+               "NORTH",
+               "NORTH_WEST",
+               "WEST",
+               "SOUTH_WEST",
+               "SOUTH",
+               "SOUTH_EAST",
+               "EAST",
+               "NORTH_EAST",
+       ]
+
+       def __init__(self):
+               self._layout = QtGui.QGridLayout()
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._layout)
+
+               self.__cells = {}
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def add_pie(self, row, column, pieButton):
+               assert len(pieButton) == 8
+               self._layout.addWidget(pieButton, row, column)
+               self.__cells[(row, column)] = pieButton
+
+       def get_pie(self, row, column):
+               return self.__cells[(row, column)]
+
+
+class KeyboardModifier(object):
+
+       def __init__(self, name):
+               self.name = name
+               self.lock = False
+               self.once = False
+
+       @property
+       def isActive(self):
+               return self.lock or self.once
+
+       def on_toggle_lock(self, *args, **kwds):
+               self.lock = not self.lock
+
+       def on_toggle_once(self, *args, **kwds):
+               self.once = not self.once
+
+       def reset_once(self):
+               self.once = False
+
+
+def parse_keyboard_data(text):
+       return eval(text)
+
+
+def _enumerate_pie_slices(pieData, iconPaths):
+       for direction, directionName in zip(
+               PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES
+       ):
+               if directionName in pieData:
+                       sliceData = pieData[directionName]
+
+                       action = QtGui.QAction(None)
+                       try:
+                               action.setText(sliceData["text"])
+                       except KeyError:
+                               pass
+                       try:
+                               relativeIconPath = sliceData["path"]
+                       except KeyError:
+                               pass
+                       else:
+                               for iconPath in iconPaths:
+                                       absIconPath = os.path.join(iconPath, relativeIconPath)
+                                       if os.path.exists(absIconPath):
+                                               action.setIcon(QtGui.QIcon(absIconPath))
+                                               break
+                       pieItem = qtpie.QActionPieItem(action)
+                       actionToken = sliceData["action"]
+               else:
+                       pieItem = qtpie.PieFiling.NULL_CENTER
+                       actionToken = ""
+               yield direction, pieItem, actionToken
+
+
+def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths):
+       for (row, column), pieData in dataTree.iteritems():
+               pieItems = list(_enumerate_pie_slices(pieData, iconPaths))
+               assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0]
+               _, center, centerAction = pieItems.pop(0)
+
+               pieButton = qtpie.QPieButton(center)
+               pieButton.set_center(center)
+               keyboardHandler.map_slice_action(center, centerAction)
+               for direction, pieItem, action in pieItems:
+                       pieButton.insertItem(pieItem)
+                       keyboardHandler.map_slice_action(pieItem, action)
+               keyboard.add_pie(row, column, pieButton)
+
+
+class KeyboardHandler(object):
+
+       def __init__(self, keyhandler):
+               self.__keyhandler = keyhandler
+               self.__commandHandlers = {}
+               self.__modifiers = {}
+               self.__sliceActions = {}
+
+               self.register_modifier("Shift")
+               self.register_modifier("Super")
+               self.register_modifier("Control")
+               self.register_modifier("Alt")
+
+       def register_command_handler(self, command, handler):
+               # @todo Look into hooking these up directly to the pie actions
+               self.__commandHandlers["[%s]" % command] = handler
+
+       def unregister_command_handler(self, command):
+               # @todo Look into hooking these up directly to the pie actions
+               del self.__commandHandlers["[%s]" % command]
+
+       def register_modifier(self, modifierName):
+               mod = KeyboardModifier(modifierName)
+               self.register_command_handler(modifierName, mod.on_toggle_lock)
+               self.__modifiers["<%s>" % modifierName] = mod
+
+       def unregister_modifier(self, modifierName):
+               self.unregister_command_handler(modifierName)
+               del self.__modifiers["<%s>" % modifierName]
+
+       def map_slice_action(self, slice, action):
+               callback = lambda direction: self(direction, action)
+               slice.action().triggered.connect(callback)
+               self.__sliceActions[slice] = (action, callback)
+
+       def __call__(self, direction, action):
+               activeModifiers = [
+                       mod.name
+                       for mod in self.__modifiers.itervalues()
+                               if mod.isActive
+               ]
+
+               needResetOnce = False
+               if action.startswith("[") and action.endswith("]"):
+                       commandName = action[1:-1]
+                       if action in self.__commandHandlers:
+                               self.__commandHandlers[action](commandName, activeModifiers)
+                               needResetOnce = True
+                       else:
+                               warnings.warn("Unknown command: [%s]" % commandName)
+               elif action.startswith("<") and action.endswith(">"):
+                       modName = action[1:-1]
+                       for mod in self.__modifiers.itervalues():
+                               if mod.name == modName:
+                                       mod.on_toggle_once()
+                                       break
+                       else:
+                               warnings.warn("Unknown modifier: <%s>" % modName)
+               else:
+                       self.__keyhandler(action, activeModifiers)
+                       needResetOnce = True
+
+               if needResetOnce:
+                       for mod in self.__modifiers.itervalues():
+                               mod.reset_once()
diff --git a/dialcentral/util/qui_utils.py b/dialcentral/util/qui_utils.py
new file mode 100644 (file)
index 0000000..11b3453
--- /dev/null
@@ -0,0 +1,419 @@
+import sys
+import contextlib
+import datetime
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def notify_error(log):
+       try:
+               yield
+       except:
+               log.push_exception()
+
+
+@contextlib.contextmanager
+def notify_busy(log, message):
+       log.push_busy(message)
+       try:
+               yield
+       finally:
+               log.pop(message)
+
+
+class ErrorMessage(object):
+
+       LEVEL_ERROR = 0
+       LEVEL_BUSY = 1
+       LEVEL_INFO = 2
+
+       def __init__(self, message, level):
+               self._message = message
+               self._level = level
+               self._time = datetime.datetime.now()
+
+       @property
+       def level(self):
+               return self._level
+
+       @property
+       def message(self):
+               return self._message
+
+       def __repr__(self):
+               return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
+
+
+class QErrorLog(QtCore.QObject):
+
+       messagePushed = qt_compat.Signal()
+       messagePopped = qt_compat.Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._messages = []
+
+       def push_busy(self, message):
+               _moduleLogger.info("Entering state: %s" % message)
+               self._push_message(message, ErrorMessage.LEVEL_BUSY)
+
+       def push_message(self, message):
+               self._push_message(message, ErrorMessage.LEVEL_INFO)
+
+       def push_error(self, message):
+               self._push_message(message, ErrorMessage.LEVEL_ERROR)
+
+       def push_exception(self):
+               userMessage = str(sys.exc_info()[1])
+               _moduleLogger.exception(userMessage)
+               self.push_error(userMessage)
+
+       def pop(self, message = None):
+               if message is None:
+                       del self._messages[0]
+               else:
+                       _moduleLogger.info("Exiting state: %s" % message)
+                       messageIndex = [
+                               i
+                               for (i, error) in enumerate(self._messages)
+                               if error.message == message
+                       ]
+                       # Might be removed out of order
+                       if messageIndex:
+                               del self._messages[messageIndex[0]]
+               self.messagePopped.emit()
+
+       def peek_message(self):
+               return self._messages[0]
+
+       def _push_message(self, message, level):
+               self._messages.append(ErrorMessage(message, level))
+               # Sort is defined as stable, so this should be fine
+               self._messages.sort(key=lambda x: x.level)
+               self.messagePushed.emit()
+
+       def __len__(self):
+               return len(self._messages)
+
+
+class ErrorDisplay(object):
+
+       _SENTINEL_ICON = QtGui.QIcon()
+
+       def __init__(self, errorLog):
+               self._errorLog = errorLog
+               self._errorLog.messagePushed.connect(self._on_message_pushed)
+               self._errorLog.messagePopped.connect(self._on_message_popped)
+
+               self._icons = None
+               self._severityLabel = QtGui.QLabel()
+               self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+               self._message = QtGui.QLabel()
+               self._message.setText("Boo")
+               self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+               self._message.setWordWrap(True)
+
+               self._closeLabel = None
+
+               self._controlLayout = QtGui.QHBoxLayout()
+               self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
+               self._controlLayout.addWidget(self._message, 1000)
+
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._controlLayout)
+               self._widget.hide()
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def _show_error(self):
+               if self._icons is None:
+                       self._icons = {
+                               ErrorMessage.LEVEL_BUSY:
+                                       get_theme_icon(
+                                               #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
+                                               ("view-refresh", "general_refresh", "gtk-refresh", )
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_INFO:
+                                       get_theme_icon(
+                                               ("dialog-information", "general_notes", "gtk-info")
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_ERROR:
+                                       get_theme_icon(
+                                               ("dialog-error", "app_install_error", "gtk-dialog-error")
+                                       ).pixmap(32, 32),
+                       }
+               if self._closeLabel is None:
+                       closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+                       if closeIcon is not self._SENTINEL_ICON:
+                               self._closeLabel = QtGui.QPushButton(closeIcon, "")
+                       else:
+                               self._closeLabel = QtGui.QPushButton("X")
+                       self._closeLabel.clicked.connect(self._on_close)
+                       self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
+               error = self._errorLog.peek_message()
+               self._message.setText(error.message)
+               self._severityLabel.setPixmap(self._icons[error.level])
+               self._widget.show()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc.log_exception(_moduleLogger)
+       def _on_close(self, checked = False):
+               self._errorLog.pop()
+
+       @qt_compat.Slot()
+       @misc.log_exception(_moduleLogger)
+       def _on_message_pushed(self):
+               self._show_error()
+
+       @qt_compat.Slot()
+       @misc.log_exception(_moduleLogger)
+       def _on_message_popped(self):
+               if len(self._errorLog) == 0:
+                       self._message.setText("")
+                       self._widget.hide()
+               else:
+                       self._show_error()
+
+
+class QHtmlDelegate(QtGui.QStyledItemDelegate):
+
+       UNDEFINED_SIZE = -1
+
+       def __init__(self, *args, **kwd):
+               QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
+               self._width = self.UNDEFINED_SIZE
+
+       def paint(self, painter, option, index):
+               newOption = QtGui.QStyleOptionViewItemV4(option)
+               self.initStyleOption(newOption, index)
+               if newOption.widget is not None:
+                       style = newOption.widget.style()
+               else:
+                       style = QtGui.QApplication.style()
+
+               doc = QtGui.QTextDocument()
+               doc.setHtml(newOption.text)
+               doc.setTextWidth(newOption.rect.width())
+
+               newOption.text = ""
+               style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
+
+               ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
+               if newOption.state & QtGui.QStyle.State_Selected:
+                       ctx.palette.setColor(
+                               QtGui.QPalette.Text,
+                               newOption.palette.color(
+                                       QtGui.QPalette.Active,
+                                       QtGui.QPalette.HighlightedText
+                               )
+                       )
+               else:
+                       ctx.palette.setColor(
+                               QtGui.QPalette.Text,
+                               newOption.palette.color(
+                                       QtGui.QPalette.Active,
+                                       QtGui.QPalette.Text
+                               )
+                       )
+
+               textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
+               painter.save()
+               painter.translate(textRect.topLeft())
+               painter.setClipRect(textRect.translated(-textRect.topLeft()))
+               doc.documentLayout().draw(painter, ctx)
+               painter.restore()
+
+       def setWidth(self, width, model):
+               if self._width == width:
+                       return
+               self._width = width
+               for c in xrange(model.rowCount()):
+                       cItem = model.item(c, 0)
+                       for r in xrange(model.rowCount()):
+                               rItem = cItem.child(r, 0)
+                               rIndex = model.indexFromItem(rItem)
+                               self.sizeHintChanged.emit(rIndex)
+                               return
+
+       def sizeHint(self, option, index):
+               newOption = QtGui.QStyleOptionViewItemV4(option)
+               self.initStyleOption(newOption, index)
+
+               doc = QtGui.QTextDocument()
+               doc.setHtml(newOption.text)
+               if self._width != self.UNDEFINED_SIZE:
+                       width = self._width
+               else:
+                       width = newOption.rect.width()
+               doc.setTextWidth(width)
+               size = QtCore.QSize(doc.idealWidth(), doc.size().height())
+               return size
+
+
+class QSignalingMainWindow(QtGui.QMainWindow):
+
+       closed = qt_compat.Signal()
+       hidden = qt_compat.Signal()
+       shown = qt_compat.Signal()
+       resized = qt_compat.Signal()
+
+       def __init__(self, *args, **kwd):
+               QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
+
+       def closeEvent(self, event):
+               val = QtGui.QMainWindow.closeEvent(self, event)
+               self.closed.emit()
+               return val
+
+       def hideEvent(self, event):
+               val = QtGui.QMainWindow.hideEvent(self, event)
+               self.hidden.emit()
+               return val
+
+       def showEvent(self, event):
+               val = QtGui.QMainWindow.showEvent(self, event)
+               self.shown.emit()
+               return val
+
+       def resizeEvent(self, event):
+               val = QtGui.QMainWindow.resizeEvent(self, event)
+               self.resized.emit()
+               return val
+
+def set_current_index(selector, itemText, default = 0):
+       for i in xrange(selector.count()):
+               if selector.itemText(i) == itemText:
+                       selector.setCurrentIndex(i)
+                       break
+       else:
+               itemText.setCurrentIndex(default)
+
+
+def _null_set_stackable(window, isStackable):
+       pass
+
+
+def _maemo_set_stackable(window, isStackable):
+       window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+       QtCore.Qt.WA_Maemo5StackedWindow
+       set_stackable = _maemo_set_stackable
+except AttributeError:
+       set_stackable = _null_set_stackable
+
+
+def _null_set_autorient(window, doAutoOrient):
+       pass
+
+
+def _maemo_set_autorient(window, doAutoOrient):
+       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
+
+
+try:
+       QtCore.Qt.WA_Maemo5AutoOrientation
+       set_autorient = _maemo_set_autorient
+except AttributeError:
+       set_autorient = _null_set_autorient
+
+
+def screen_orientation():
+       geom = QtGui.QApplication.desktop().screenGeometry()
+       if geom.width() <= geom.height():
+               return QtCore.Qt.Vertical
+       else:
+               return QtCore.Qt.Horizontal
+
+
+def _null_set_window_orientation(window, orientation):
+       pass
+
+
+def _maemo_set_window_orientation(window, orientation):
+       if orientation == QtCore.Qt.Vertical:
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
+       elif orientation == QtCore.Qt.Horizontal:
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+       elif orientation is None:
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+       else:
+               raise RuntimeError("Unknown orientation: %r" % orientation)
+
+
+try:
+       QtCore.Qt.WA_Maemo5LandscapeOrientation
+       QtCore.Qt.WA_Maemo5PortraitOrientation
+       set_window_orientation = _maemo_set_window_orientation
+except AttributeError:
+       set_window_orientation = _null_set_window_orientation
+
+
+def _null_show_progress_indicator(window, isStackable):
+       pass
+
+
+def _maemo_show_progress_indicator(window, isStackable):
+       window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
+
+
+try:
+       QtCore.Qt.WA_Maemo5ShowProgressIndicator
+       show_progress_indicator = _maemo_show_progress_indicator
+except AttributeError:
+       show_progress_indicator = _null_show_progress_indicator
+
+
+def _null_mark_numbers_preferred(widget):
+       pass
+
+
+def _newqt_mark_numbers_preferred(widget):
+       widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
+
+
+try:
+       QtCore.Qt.ImhPreferNumbers
+       mark_numbers_preferred = _newqt_mark_numbers_preferred
+except AttributeError:
+       mark_numbers_preferred = _null_mark_numbers_preferred
+
+
+def _null_get_theme_icon(iconNames, fallback = None):
+       icon = fallback if fallback is not None else QtGui.QIcon()
+       return icon
+
+
+def _newqt_get_theme_icon(iconNames, fallback = None):
+       for iconName in iconNames:
+               if QtGui.QIcon.hasThemeIcon(iconName):
+                       icon = QtGui.QIcon.fromTheme(iconName)
+                       break
+       else:
+               icon = fallback if fallback is not None else QtGui.QIcon()
+       return icon
+
+
+try:
+       QtGui.QIcon.fromTheme
+       get_theme_icon = _newqt_get_theme_icon
+except AttributeError:
+       get_theme_icon = _null_get_theme_icon
+
diff --git a/dialcentral/util/qwrappers.py b/dialcentral/util/qwrappers.py
new file mode 100644 (file)
index 0000000..2c50c8a
--- /dev/null
@@ -0,0 +1,328 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class ApplicationWrapper(object):
+
+       DEFAULT_ORIENTATION = "Default"
+       AUTO_ORIENTATION = "Auto"
+       LANDSCAPE_ORIENTATION = "Landscape"
+       PORTRAIT_ORIENTATION = "Portrait"
+
+       def __init__(self, qapp, constants):
+               self._constants = constants
+               self._qapp = qapp
+               self._clipboard = QtGui.QApplication.clipboard()
+
+               self._errorLog = qui_utils.QErrorLog()
+               self._mainWindow = None
+
+               self._fullscreenAction = QtGui.QAction(None)
+               self._fullscreenAction.setText("Fullscreen")
+               self._fullscreenAction.setCheckable(True)
+               self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
+               self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
+
+               self._orientation = self.DEFAULT_ORIENTATION
+               self._orientationAction = QtGui.QAction(None)
+               self._orientationAction.setText("Next Orientation")
+               self._orientationAction.setCheckable(True)
+               self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
+               self._orientationAction.triggered.connect(self._on_next_orientation)
+
+               self._logAction = QtGui.QAction(None)
+               self._logAction.setText("Log")
+               self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
+               self._logAction.triggered.connect(self._on_log)
+
+               self._quitAction = QtGui.QAction(None)
+               self._quitAction.setText("Quit")
+               self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
+               self._quitAction.triggered.connect(self._on_quit)
+
+               self._aboutAction = QtGui.QAction(None)
+               self._aboutAction.setText("About")
+               self._aboutAction.triggered.connect(self._on_about)
+
+               self._qapp.lastWindowClosed.connect(self._on_app_quit)
+               self._mainWindow = self._new_main_window()
+               self._mainWindow.window.destroyed.connect(self._on_child_close)
+
+               self.load_settings()
+
+               self._mainWindow.show()
+               self._idleDelay = QtCore.QTimer()
+               self._idleDelay.setSingleShot(True)
+               self._idleDelay.setInterval(0)
+               self._idleDelay.timeout.connect(self._on_delayed_start)
+               self._idleDelay.start()
+
+       def load_settings(self):
+               raise NotImplementedError("Booh")
+
+       def save_settings(self):
+               raise NotImplementedError("Booh")
+
+       def _new_main_window(self):
+               raise NotImplementedError("Booh")
+
+       @property
+       def qapp(self):
+               return self._qapp
+
+       @property
+       def constants(self):
+               return self._constants
+
+       @property
+       def errorLog(self):
+               return self._errorLog
+
+       @property
+       def fullscreenAction(self):
+               return self._fullscreenAction
+
+       @property
+       def orientationAction(self):
+               return self._orientationAction
+
+       @property
+       def orientation(self):
+               return self._orientation
+
+       @property
+       def logAction(self):
+               return self._logAction
+
+       @property
+       def aboutAction(self):
+               return self._aboutAction
+
+       @property
+       def quitAction(self):
+               return self._quitAction
+
+       def set_orientation(self, orientation):
+               self._orientation = orientation
+               self._mainWindow.update_orientation(self._orientation)
+
+       @classmethod
+       def _next_orientation(cls, current):
+               return {
+                       cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
+                       cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
+                       cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
+                       cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
+               }[current]
+
+       def _close_windows(self):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow.window.destroyed.disconnect(self._on_child_close)
+                       self._mainWindow.close()
+                       self._mainWindow = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_start(self):
+               self._mainWindow.start()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_app_quit(self, checked = False):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow.destroy()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_close(self, obj = None):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_toggle_fullscreen(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       self._mainWindow.set_fullscreen(checked)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_next_orientation(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       self.set_orientation(self._next_orientation(self._orientation))
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_about(self, checked = True):
+               raise NotImplementedError("Booh")
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_log(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       with open(self._constants._user_logpath_, "r") as f:
+                               logLines = f.xreadlines()
+                               log = "".join(logLines)
+                               self._clipboard.setText(log)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_quit(self, checked = False):
+               with qui_utils.notify_error(self._errorLog):
+                       self._close_windows()
+
+
+class WindowWrapper(object):
+
+       def __init__(self, parent, app):
+               self._app = app
+
+               self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog)
+
+               self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight)
+               self._layout.setContentsMargins(0, 0, 0, 0)
+
+               self._superLayout = QtGui.QVBoxLayout()
+               self._superLayout.addWidget(self._errorDisplay.toplevel)
+               self._superLayout.setContentsMargins(0, 0, 0, 0)
+               self._superLayout.addLayout(self._layout)
+
+               centralWidget = QtGui.QWidget()
+               centralWidget.setLayout(self._superLayout)
+               centralWidget.setContentsMargins(0, 0, 0, 0)
+
+               self._window = qui_utils.QSignalingMainWindow(parent)
+               self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
+               qui_utils.set_stackable(self._window, True)
+               self._window.setCentralWidget(centralWidget)
+
+               self._closeWindowAction = QtGui.QAction(None)
+               self._closeWindowAction.setText("Close")
+               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+               self._closeWindowAction.triggered.connect(self._on_close_window)
+
+               self._window.addAction(self._closeWindowAction)
+               self._window.addAction(self._app.quitAction)
+               self._window.addAction(self._app.fullscreenAction)
+               self._window.addAction(self._app.orientationAction)
+               self._window.addAction(self._app.logAction)
+
+       @property
+       def window(self):
+               return self._window
+
+       @property
+       def windowOrientation(self):
+               geom = self._window.size()
+               if geom.width() <= geom.height():
+                       return QtCore.Qt.Vertical
+               else:
+                       return QtCore.Qt.Horizontal
+
+       @property
+       def idealWindowOrientation(self):
+               if self._app.orientation ==  self._app.AUTO_ORIENTATION:
+                       windowOrientation = self.windowOrientation
+               elif self._app.orientation ==  self._app.DEFAULT_ORIENTATION:
+                       windowOrientation = qui_utils.screen_orientation()
+               elif self._app.orientation ==  self._app.LANDSCAPE_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Horizontal
+               elif self._app.orientation ==  self._app.PORTRAIT_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Vertical
+               else:
+                       raise RuntimeError("Bad! No %r for you" % self._app.orientation)
+               return windowOrientation
+
+       def walk_children(self):
+               return ()
+
+       def start(self):
+               pass
+
+       def close(self):
+               for child in self.walk_children():
+                       child.window.destroyed.disconnect(self._on_child_close)
+                       child.close()
+               self._window.close()
+
+       def destroy(self):
+               pass
+
+       def show(self):
+               self._window.show()
+               for child in self.walk_children():
+                       child.show()
+               self.set_fullscreen(self._app.fullscreenAction.isChecked())
+
+       def hide(self):
+               for child in self.walk_children():
+                       child.hide()
+               self._window.hide()
+
+       def set_fullscreen(self, isFullscreen):
+               if self._window.isVisible():
+                       if isFullscreen:
+                               self._window.showFullScreen()
+                       else:
+                               self._window.showNormal()
+               for child in self.walk_children():
+                       child.set_fullscreen(isFullscreen)
+
+       def update_orientation(self, orientation):
+               if orientation == self._app.DEFAULT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.AUTO_ORIENTATION:
+                       qui_utils.set_autorient(self.window, True)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.LANDSCAPE_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+               elif orientation == self._app.PORTRAIT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
+               else:
+                       raise RuntimeError("Unknown orientation: %r" % orientation)
+               for child in self.walk_children():
+                       child.update_orientation(orientation)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_close(self, obj = None):
+               raise NotImplementedError("Booh")
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_close_window(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       self.close()
+
+
+class AutoFreezeWindowFeature(object):
+
+       def __init__(self, app, window):
+               self._app = app
+               self._window = window
+               self._app.qapp.focusChanged.connect(self._on_focus_changed)
+               if self._app.qapp.focusWidget() is not None:
+                       self._window.setUpdatesEnabled(True)
+               else:
+                       self._window.setUpdatesEnabled(False)
+
+       def close(self):
+               self._app.qapp.focusChanged.disconnect(self._on_focus_changed)
+               self._window.setUpdatesEnabled(True)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_focus_changed(self, oldWindow, newWindow):
+               with qui_utils.notify_error(self._app.errorLog):
+                       if oldWindow is None and newWindow is not None:
+                               self._window.setUpdatesEnabled(True)
+                       elif oldWindow is not None and newWindow is None:
+                               self._window.setUpdatesEnabled(False)
diff --git a/dialcentral/util/time_utils.py b/dialcentral/util/time_utils.py
new file mode 100644 (file)
index 0000000..90ec84d
--- /dev/null
@@ -0,0 +1,94 @@
+from datetime import tzinfo, timedelta, datetime
+
+ZERO = timedelta(0)
+HOUR = timedelta(hours=1)
+
+
+def first_sunday_on_or_after(dt):
+       days_to_go = 6 - dt.weekday()
+       if days_to_go:
+               dt += timedelta(days_to_go)
+       return dt
+
+
+# US DST Rules
+#
+# This is a simplified (i.e., wrong for a few cases) set of rules for US
+# DST start and end times. For a complete and up-to-date set of DST rules
+# and timezone definitions, visit the Olson Database (or try pytz):
+# http://www.twinsun.com/tz/tz-link.htm
+# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
+#
+# In the US, since 2007, DST starts at 2am (standard time) on the second
+# Sunday in March, which is the first Sunday on or after Mar 8.
+DSTSTART_2007 = datetime(1, 3, 8, 2)
+# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 1)
+# From 1987 to 2006, DST used to start at 2am (standard time) on the first
+# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday of October, which is the first Sunday on or after Oct 25.
+DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
+DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+# From 1967 to 1986, DST used to start at 2am (standard time) on the last
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
+# 1am standard time) on the last Sunday of October, which is the first Sunday
+# on or after Oct 25.
+DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
+DSTEND_1967_1986 = DSTEND_1987_2006
+
+
+class USTimeZone(tzinfo):
+
+       def __init__(self, hours, reprname, stdname, dstname):
+               self.stdoffset = timedelta(hours=hours)
+               self.reprname = reprname
+               self.stdname = stdname
+               self.dstname = dstname
+
+       def __repr__(self):
+               return self.reprname
+
+       def tzname(self, dt):
+               if self.dst(dt):
+                       return self.dstname
+               else:
+                       return self.stdname
+
+       def utcoffset(self, dt):
+               return self.stdoffset + self.dst(dt)
+
+       def dst(self, dt):
+               if dt is None or dt.tzinfo is None:
+                       # An exception may be sensible here, in one or both cases.
+                       # It depends on how you want to treat them.  The default
+                       # fromutc() implementation (called by the default astimezone()
+                       # implementation) passes a datetime with dt.tzinfo is self.
+                       return ZERO
+               assert dt.tzinfo is self
+
+               # Find start and end times for US DST. For years before 1967, return
+               # ZERO for no DST.
+               if 2006 < dt.year:
+                       dststart, dstend = DSTSTART_2007, DSTEND_2007
+               elif 1986 < dt.year < 2007:
+                       dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+               elif 1966 < dt.year < 1987:
+                       dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+               else:
+                       return ZERO
+
+               start = first_sunday_on_or_after(dststart.replace(year=dt.year))
+               end = first_sunday_on_or_after(dstend.replace(year=dt.year))
+
+               # Can't compare naive to aware objects, so strip the timezone from
+               # dt first.
+               if start <= dt.replace(tzinfo=None) < end:
+                       return HOUR
+               else:
+                       return ZERO
+
+
+Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
+Central  = USTimeZone(-6, "Central",  "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")
diff --git a/dialcentral/util/tp_utils.py b/dialcentral/util/tp_utils.py
new file mode 100644 (file)
index 0000000..7c55c42
--- /dev/null
@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+
+import logging
+
+import dbus
+import telepathy
+
+import util.go_utils as gobject_utils
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
+
+
+class WasMissedCall(object):
+
+       def __init__(self, bus, conn, chan, on_success, on_error):
+               self.__on_success = on_success
+               self.__on_error = on_error
+
+               self._requested = None
+               self._didMembersChange = False
+               self._didClose = False
+               self._didReport = False
+
+               self._onTimeout = gobject_utils.Timeout(self._on_timeout)
+               self._onTimeout.start(seconds=60)
+
+               chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal(
+                       "MembersChanged",
+                       self._on_members_changed,
+               )
+
+               chan[telepathy.interfaces.CHANNEL].connect_to_signal(
+                       "Closed",
+                       self._on_closed,
+               )
+
+               chan[DBUS_PROPERTIES].GetAll(
+                       telepathy.interfaces.CHANNEL_INTERFACE,
+                       reply_handler = self._on_got_all,
+                       error_handler = self._on_error,
+               )
+
+       def cancel(self):
+               self._report_error("by request")
+
+       def _report_missed_if_ready(self):
+               if self._didReport:
+                       pass
+               elif self._requested is not None and (self._didMembersChange or self._didClose):
+                       if self._requested:
+                               self._report_error("wrong direction")
+                       elif self._didClose:
+                               self._report_success()
+                       else:
+                               self._report_error("members added")
+               else:
+                       if self._didClose:
+                               self._report_error("closed too early")
+
+       def _report_success(self):
+               assert not self._didReport, "Double reporting a missed call"
+               self._didReport = True
+               self._onTimeout.cancel()
+               self.__on_success(self)
+
+       def _report_error(self, reason):
+               assert not self._didReport, "Double reporting a missed call"
+               self._didReport = True
+               self._onTimeout.cancel()
+               self.__on_error(self, reason)
+
+       @misc.log_exception(_moduleLogger)
+       def _on_got_all(self, properties):
+               self._requested = properties["Requested"]
+               self._report_missed_if_ready()
+
+       @misc.log_exception(_moduleLogger)
+       def _on_members_changed(self, message, added, removed, lp, rp, actor, reason):
+               if added:
+                       self._didMembersChange = True
+                       self._report_missed_if_ready()
+
+       @misc.log_exception(_moduleLogger)
+       def _on_closed(self):
+               self._didClose = True
+               self._report_missed_if_ready()
+
+       @misc.log_exception(_moduleLogger)
+       def _on_error(self, *args):
+               self._report_error(args)
+
+       @misc.log_exception(_moduleLogger)
+       def _on_timeout(self):
+               self._report_error("timeout")
+               return False
+
+
+class NewChannelSignaller(object):
+
+       def __init__(self, on_new_channel):
+               self._sessionBus = dbus.SessionBus()
+               self._on_user_new_channel = on_new_channel
+
+       def start(self):
+               self._sessionBus.add_signal_receiver(
+                       self._on_new_channel,
+                       "NewChannel",
+                       "org.freedesktop.Telepathy.Connection",
+                       None,
+                       None
+               )
+
+       def stop(self):
+               self._sessionBus.remove_signal_receiver(
+                       self._on_new_channel,
+                       "NewChannel",
+                       "org.freedesktop.Telepathy.Connection",
+                       None,
+                       None
+               )
+
+       @misc.log_exception(_moduleLogger)
+       def _on_new_channel(
+               self, channelObjectPath, channelType, handleType, handle, supressHandler
+       ):
+               connObjectPath = channel_path_to_conn_path(channelObjectPath)
+               serviceName = path_to_service_name(channelObjectPath)
+               try:
+                       self._on_user_new_channel(
+                               self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType
+                       )
+               except Exception:
+                       _moduleLogger.exception("Blocking exception from being passed up")
+
+
+class EnableSystemContactIntegration(object):
+
+       ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager"
+       ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager"
+       ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query"
+       ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat"
+       ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile"
+       DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
+
+       def __init__(self, profileName):
+               self._bus = dbus.SessionBus()
+               self._profileName = profileName
+
+       def start(self):
+               self._accountManager = self._bus.get_object(
+                       self.ACCOUNT_MGR_NAME,
+                       self.ACCOUNT_MGR_PATH,
+               )
+               self._accountManagerQuery = dbus.Interface(
+                       self._accountManager,
+                       dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY,
+               )
+
+               self._accountManagerQuery.FindAccounts(
+                       {
+                               self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName,
+                       },
+                       reply_handler = self._on_found_accounts_reply,
+                       error_handler = self._on_error,
+               )
+
+       @misc.log_exception(_moduleLogger)
+       def _on_found_accounts_reply(self, accountObjectPaths):
+               for accountObjectPath in accountObjectPaths:
+                       print accountObjectPath
+                       account = self._bus.get_object(
+                               self.ACCOUNT_MGR_NAME,
+                               accountObjectPath,
+                       )
+                       accountProperties = dbus.Interface(
+                               account,
+                               self.DBUS_PROPERTIES,
+                       )
+                       accountProperties.Set(
+                               self.ACCOUNT_IFACE_COMPAT,
+                               "SecondaryVCardFields",
+                               ["TEL"],
+                               reply_handler = self._on_field_set,
+                               error_handler = self._on_error,
+                       )
+
+       @misc.log_exception(_moduleLogger)
+       def _on_field_set(self):
+               _moduleLogger.info("SecondaryVCardFields Set")
+
+       @misc.log_exception(_moduleLogger)
+       def _on_error(self, error):
+               _moduleLogger.error("%r" % (error, ))
+
+
+def channel_path_to_conn_path(channelObjectPath):
+       """
+       >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+       '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME'
+       """
+       return channelObjectPath.rsplit("/", 1)[0]
+
+
+def path_to_service_name(path):
+       """
+       >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+       'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME'
+       """
+       return ".".join(path[1:].split("/")[0:7])
+
+
+def cm_from_path(path):
+       """
+       >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
+       'theonering'
+       """
+       return path[1:].split("/")[4]
diff --git a/src b/src
new file mode 120000 (symlink)
index 0000000..14858ed
--- /dev/null
+++ b/src
@@ -0,0 +1 @@
+dialcentral/
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644 (file)
index 4265cc3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-#!/usr/bin/env python
diff --git a/src/alarm_handler.py b/src/alarm_handler.py
deleted file mode 100644 (file)
index a79f992..0000000
+++ /dev/null
@@ -1,460 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import time
-import datetime
-import ConfigParser
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-import dbus
-
-
-_FREMANTLE_ALARM = "Fremantle"
-_DIABLO_ALARM = "Diablo"
-_NO_ALARM = "None"
-
-
-try:
-       import alarm
-       ALARM_TYPE = _FREMANTLE_ALARM
-except (ImportError, OSError):
-       try:
-               import osso.alarmd as alarmd
-               ALARM_TYPE = _DIABLO_ALARM
-       except (ImportError, OSError):
-               ALARM_TYPE = _NO_ALARM
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-def _get_start_time(recurrence):
-       now = datetime.datetime.now()
-       startTimeMinute = now.minute + max(recurrence, 5) # being safe
-       startTimeHour = now.hour + int(startTimeMinute / 60)
-       startTimeMinute = startTimeMinute % 59
-       now.replace(minute=startTimeMinute)
-       timestamp = int(time.mktime(now.timetuple()))
-       return timestamp
-
-
-def _create_recurrence_mask(recurrence, base):
-       """
-       >>> bin(_create_recurrence_mask(60, 60))
-       '0b1'
-       >>> bin(_create_recurrence_mask(30, 60))
-       '0b1000000000000000000000000000001'
-       >>> bin(_create_recurrence_mask(2, 60))
-       '0b10101010101010101010101010101010101010101010101010101010101'
-       >>> bin(_create_recurrence_mask(1, 60))
-       '0b111111111111111111111111111111111111111111111111111111111111'
-       """
-       mask = 0
-       for i in xrange(base / recurrence):
-               mask |= 1 << (recurrence * i)
-       return mask
-
-
-def _unpack_minutes(recurrence):
-       """
-       >>> _unpack_minutes(0)
-       (0, 0, 0)
-       >>> _unpack_minutes(1)
-       (0, 0, 1)
-       >>> _unpack_minutes(59)
-       (0, 0, 59)
-       >>> _unpack_minutes(60)
-       (0, 1, 0)
-       >>> _unpack_minutes(129)
-       (0, 2, 9)
-       >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
-       (5, 3, 2)
-       >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
-       (5, 3, 2)
-       """
-       minutesInAnHour = 60
-       minutesInDay = 24 * minutesInAnHour
-       minutesInAWeek = minutesInDay * 7
-
-       days = recurrence / minutesInDay
-       daysOfWeek = days % 7
-       recurrence -= days * minutesInDay
-       hours = recurrence / minutesInAnHour
-       recurrence -= hours * minutesInAnHour
-       mins = recurrence % minutesInAnHour
-       recurrence -= mins
-       assert recurrence == 0, "Recurrence %d" % recurrence
-       return daysOfWeek, hours, mins
-
-
-class _FremantleAlarmHandler(object):
-
-       _INVALID_COOKIE = -1
-       _REPEAT_FOREVER = -1
-       _TITLE = "Dialcentral Notifications"
-       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
-
-       def __init__(self):
-               self._recurrence = 5
-
-               self._alarmCookie = self._INVALID_COOKIE
-               self._launcher = self._LAUNCHER
-
-       def load_settings(self, config, sectionName):
-               try:
-                       self._recurrence = config.getint(sectionName, "recurrence")
-                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
-                       launcher = config.get(sectionName, "notifier")
-                       if launcher:
-                               self._launcher = launcher
-               except ConfigParser.NoOptionError:
-                       pass
-               except ConfigParser.NoSectionError:
-                       pass
-
-       def save_settings(self, config, sectionName):
-               try:
-                       config.set(sectionName, "recurrence", str(self._recurrence))
-                       config.set(sectionName, "alarmCookie", str(self._alarmCookie))
-                       launcher = self._launcher if self._launcher != self._LAUNCHER else ""
-                       config.set(sectionName, "notifier", launcher)
-               except ConfigParser.NoOptionError:
-                       pass
-               except ConfigParser.NoSectionError:
-                       pass
-
-       def apply_settings(self, enabled, recurrence):
-               if recurrence != self._recurrence or enabled != self.isEnabled:
-                       if self.isEnabled:
-                               self._clear_alarm()
-                       if enabled:
-                               self._set_alarm(recurrence)
-               self._recurrence = int(recurrence)
-
-       @property
-       def recurrence(self):
-               return self._recurrence
-
-       @property
-       def isEnabled(self):
-               return self._alarmCookie != self._INVALID_COOKIE
-
-       def _set_alarm(self, recurrenceMins):
-               assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
-               alarmTime = _get_start_time(recurrenceMins)
-
-               event = alarm.Event()
-               event.appid = self._TITLE
-               event.alarm_time = alarmTime
-               event.recurrences_left = self._REPEAT_FOREVER
-
-               action = event.add_actions(1)[0]
-               action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
-               action.command = self._launcher
-
-               recurrence = event.add_recurrences(1)[0]
-               recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
-               recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
-               recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
-               recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
-               recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
-               recurrence.special |= alarm.RECUR_SPECIAL_NONE
-
-               assert event.is_sane()
-               self._alarmCookie = alarm.add_event(event)
-
-       def _clear_alarm(self):
-               if self._alarmCookie == self._INVALID_COOKIE:
-                       return
-               alarm.delete_event(self._alarmCookie)
-               self._alarmCookie = self._INVALID_COOKIE
-
-
-class _DiabloAlarmHandler(object):
-
-       _INVALID_COOKIE = -1
-       _TITLE = "Dialcentral Notifications"
-       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
-       _REPEAT_FOREVER = -1
-
-       def __init__(self):
-               self._recurrence = 5
-
-               bus = dbus.SystemBus()
-               self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
-               self._alarmCookie = self._INVALID_COOKIE
-               self._launcher = self._LAUNCHER
-
-       def load_settings(self, config, sectionName):
-               try:
-                       self._recurrence = config.getint(sectionName, "recurrence")
-                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
-                       launcher = config.get(sectionName, "notifier")
-                       if launcher:
-                               self._launcher = launcher
-               except ConfigParser.NoOptionError:
-                       pass
-               except ConfigParser.NoSectionError:
-                       pass
-
-       def save_settings(self, config, sectionName):
-               config.set(sectionName, "recurrence", str(self._recurrence))
-               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
-               launcher = self._launcher if self._launcher != self._LAUNCHER else ""
-               config.set(sectionName, "notifier", launcher)
-
-       def apply_settings(self, enabled, recurrence):
-               if recurrence != self._recurrence or enabled != self.isEnabled:
-                       if self.isEnabled:
-                               self._clear_alarm()
-                       if enabled:
-                               self._set_alarm(recurrence)
-               self._recurrence = int(recurrence)
-
-       @property
-       def recurrence(self):
-               return self._recurrence
-
-       @property
-       def isEnabled(self):
-               return self._alarmCookie != self._INVALID_COOKIE
-
-       def _set_alarm(self, recurrence):
-               assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
-               alarmTime = _get_start_time(recurrence)
-
-               #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
-               _DEFAULT_FLAGS = (
-                       alarmd.ALARM_EVENT_NO_DIALOG |
-                       alarmd.ALARM_EVENT_NO_SNOOZE |
-                       alarmd.ALARM_EVENT_CONNECTED
-               )
-               action = []
-               action.extend(['flags', _DEFAULT_FLAGS])
-               action.extend(['title', self._TITLE])
-               action.extend(['path', self._launcher])
-               action.extend([
-                       'arguments',
-                       dbus.Array(
-                               [alarmTime, int(27)],
-                               signature=dbus.Signature('v')
-                       )
-               ])  #int(27) used in place of alarm_index
-
-               event = []
-               event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
-               event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')])  #use AlarmdActionExec instead of AlarmdActionDbus
-               event.append(dbus.UInt32(len(action) / 2))
-               event.extend(action)
-               event.extend(['time', dbus.Int64(alarmTime)])
-               event.extend(['recurr_interval', dbus.UInt32(recurrence)])
-               event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
-
-               self._alarmCookie = self._alarmdDBus.add_event(*event);
-
-       def _clear_alarm(self):
-               if self._alarmCookie == self._INVALID_COOKIE:
-                       return
-               deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
-               self._alarmCookie = self._INVALID_COOKIE
-               assert deleteResult != -1, "Deleting of alarm event failed"
-
-
-class _ApplicationAlarmHandler(object):
-
-       _REPEAT_FOREVER = -1
-       _MIN_TO_MS_FACTORY = 1000 * 60
-
-       def __init__(self):
-               self._timer = QtCore.QTimer()
-               self._timer.setSingleShot(False)
-               self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
-
-       def load_settings(self, config, sectionName):
-               try:
-                       self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
-               except ConfigParser.NoOptionError:
-                       pass
-               except ConfigParser.NoSectionError:
-                       pass
-               self._timer.start()
-
-       def save_settings(self, config, sectionName):
-               config.set(sectionName, "recurrence", str(self.recurrence))
-
-       def apply_settings(self, enabled, recurrence):
-               self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
-               if enabled:
-                       self._timer.start()
-               else:
-                       self._timer.stop()
-
-       @property
-       def notifySignal(self):
-               return self._timer.timeout
-
-       @property
-       def recurrence(self):
-               return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
-
-       @property
-       def isEnabled(self):
-               return self._timer.isActive()
-
-
-class _NoneAlarmHandler(object):
-
-       def __init__(self):
-               self._enabled = False
-               self._recurrence = 5
-
-       def load_settings(self, config, sectionName):
-               try:
-                       self._recurrence = config.getint(sectionName, "recurrence")
-                       self._enabled = True
-               except ConfigParser.NoOptionError:
-                       pass
-               except ConfigParser.NoSectionError:
-                       pass
-
-       def save_settings(self, config, sectionName):
-               config.set(sectionName, "recurrence", str(self.recurrence))
-
-       def apply_settings(self, enabled, recurrence):
-               self._enabled = enabled
-
-       @property
-       def recurrence(self):
-               return self._recurrence
-
-       @property
-       def isEnabled(self):
-               return self._enabled
-
-
-_BACKGROUND_ALARM_FACTORY = {
-       _FREMANTLE_ALARM: _FremantleAlarmHandler,
-       _DIABLO_ALARM: _DiabloAlarmHandler,
-       _NO_ALARM: None,
-}[ALARM_TYPE]
-
-
-class AlarmHandler(object):
-
-       ALARM_NONE = "No Alert"
-       ALARM_BACKGROUND = "Background Alert"
-       ALARM_APPLICATION = "Application Alert"
-       ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
-
-       ALARM_FACTORY = {
-               ALARM_NONE: _NoneAlarmHandler,
-               ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
-               ALARM_APPLICATION: _ApplicationAlarmHandler,
-       }
-
-       def __init__(self):
-               self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
-               self._currentAlarmType = self.ALARM_NONE
-
-       def load_settings(self, config, sectionName):
-               try:
-                       self._currentAlarmType = config.get(sectionName, "alarm")
-               except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
-                       _moduleLogger.exception("Falling back to old style")
-                       self._currentAlarmType = self.ALARM_BACKGROUND
-               if self._currentAlarmType not in self.ALARM_TYPES:
-                       self._currentAlarmType = self.ALARM_NONE
-
-               self._init_alarm(self._currentAlarmType)
-               if self._currentAlarmType in self._alarms:
-                       self._alarms[self._currentAlarmType].load_settings(config, sectionName)
-                       if not self._alarms[self._currentAlarmType].isEnabled:
-                               _moduleLogger.info("Config file lied, not actually enabled")
-                               self._currentAlarmType = self.ALARM_NONE
-               else:
-                       _moduleLogger.info("Background alerts not supported")
-                       self._currentAlarmType = self.ALARM_NONE
-
-       def save_settings(self, config, sectionName):
-               config.set(sectionName, "alarm", self._currentAlarmType)
-               self._alarms[self._currentAlarmType].save_settings(config, sectionName)
-
-       def apply_settings(self, t, recurrence):
-               self._init_alarm(t)
-               newHandler = self._alarms[t]
-               oldHandler = self._alarms[self._currentAlarmType]
-               if newHandler != oldHandler:
-                       oldHandler.apply_settings(False, 0)
-               newHandler.apply_settings(True, recurrence)
-               self._currentAlarmType = t
-
-       @property
-       def alarmType(self):
-               return self._currentAlarmType
-
-       @property
-       def backgroundNotificationsSupported(self):
-               return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
-
-       @property
-       def applicationNotifySignal(self):
-               self._init_alarm(self.ALARM_APPLICATION)
-               return self._alarms[self.ALARM_APPLICATION].notifySignal
-
-       @property
-       def recurrence(self):
-               return self._alarms[self._currentAlarmType].recurrence
-
-       @property
-       def isEnabled(self):
-               return self._currentAlarmType != self.ALARM_NONE
-
-       def _init_alarm(self, t):
-               if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
-                       self._alarms[t] = self.ALARM_FACTORY[t]()
-
-
-def main():
-       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
-       logging.basicConfig(level=logging.DEBUG, format=logFormat)
-       import constants
-       try:
-               import optparse
-       except ImportError:
-               return
-
-       parser = optparse.OptionParser()
-       parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
-       parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
-       parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
-       parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
-       (commandOptions, commandArgs) = parser.parse_args()
-
-       alarmHandler = AlarmHandler()
-       config = ConfigParser.SafeConfigParser()
-       config.read(constants._user_settings_)
-       alarmHandler.load_settings(config, "alarm")
-
-       if commandOptions.display:
-               print "Alarm (%s) is %s for every %d minutes" % (
-                       alarmHandler._alarmCookie,
-                       "enabled" if alarmHandler.isEnabled else "disabled",
-                       alarmHandler.recurrence,
-               )
-       else:
-               isEnabled = commandOptions.enabled
-               recurrence = commandOptions.recurrence
-               alarmHandler.apply_settings(isEnabled, recurrence)
-
-               alarmHandler.save_settings(config, "alarm")
-               configFile = open(constants._user_settings_, "wb")
-               try:
-                       config.write(configFile)
-               finally:
-                       configFile.close()
-
-
-if __name__ == "__main__":
-       main()
diff --git a/src/alarm_notify.py b/src/alarm_notify.py
deleted file mode 100755 (executable)
index bc6240e..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import filecmp
-import ConfigParser
-import pprint
-import logging
-import logging.handlers
-
-import constants
-from backends.gvoice import gvoice
-
-
-def get_missed(backend):
-       missedPage = backend._browser.download(backend._XML_MISSED_URL)
-       missedJson = backend._grab_json(missedPage)
-       return missedJson
-
-
-def get_voicemail(backend):
-       voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL)
-       voicemailJson = backend._grab_json(voicemailPage)
-       return voicemailJson
-
-
-def get_sms(backend):
-       smsPage = backend._browser.download(backend._XML_SMS_URL)
-       smsJson = backend._grab_json(smsPage)
-       return smsJson
-
-
-def remove_reltime(data):
-       for messageData in data["messages"].itervalues():
-               for badPart in [
-                       "relTime",
-                       "relativeStartTime",
-                       "time",
-                       "star",
-                       "isArchived",
-                       "isRead",
-                       "isSpam",
-                       "isTrash",
-                       "labels",
-               ]:
-                       if badPart in messageData:
-                               del messageData[badPart]
-       for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
-               if globalBad in data:
-                       del data[globalBad]
-
-
-def is_type_changed(backend, type, get_material):
-       jsonMaterial = get_material(backend)
-       unreadCount = jsonMaterial["unreadCounts"][type]
-
-       previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
-       currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
-
-       try:
-               os.remove(previousSnapshotPath)
-       except OSError, e:
-               # check if failed purely because the old file didn't exist, which is fine
-               if e.errno != 2:
-                       raise
-       try:
-               os.rename(currentSnapshotPath, previousSnapshotPath)
-               previousExists = True
-       except OSError, e:
-               # check if failed purely because the new old file didn't exist, which is fine
-               if e.errno != 2:
-                       raise
-               previousExists = False
-
-       remove_reltime(jsonMaterial)
-       textMaterial = pprint.pformat(jsonMaterial)
-       currentSnapshot = file(currentSnapshotPath, "w")
-       try:
-               currentSnapshot.write(textMaterial)
-       finally:
-               currentSnapshot.close()
-
-       if unreadCount == 0 or not previousExists:
-               return False
-
-       seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
-       return not seemEqual
-
-
-def create_backend(config):
-       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
-       backend = gvoice.GVoiceBackend(gvCookiePath)
-
-       loggedIn = False
-
-       if not loggedIn:
-               loggedIn = backend.refresh_account_info() is not None
-
-       if not loggedIn:
-               import base64
-               try:
-                       blobs = (
-                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
-                               for i in xrange(2)
-                       )
-                       creds = (
-                               base64.b64decode(blob)
-                               for blob in blobs
-                       )
-                       username, password = tuple(creds)
-                       loggedIn = backend.login(username, password) is not None
-               except ConfigParser.NoOptionError, e:
-                       pass
-               except ConfigParser.NoSectionError, e:
-                       pass
-
-       assert loggedIn
-       return backend
-
-
-def is_changed(config, backend):
-       try:
-               notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
-               notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
-               notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
-       except ConfigParser.NoOptionError, e:
-               notifyOnMissed = False
-               notifyOnVoicemail = False
-               notifyOnSms = False
-       except ConfigParser.NoSectionError, e:
-               notifyOnMissed = False
-               notifyOnVoicemail = False
-               notifyOnSms = False
-       logging.debug(
-               "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms)
-       )
-
-       notifySources = []
-       if notifyOnMissed:
-               notifySources.append(("missed", get_missed))
-       if notifyOnVoicemail:
-               notifySources.append(("voicemail", get_voicemail))
-       if notifyOnSms:
-               notifySources.append(("sms", get_sms))
-
-       notifyUser = False
-       for type, get_material in notifySources:
-               if is_type_changed(backend, type, get_material):
-                       notifyUser = True
-       return notifyUser
-
-
-def notify_on_change():
-       config = ConfigParser.SafeConfigParser()
-       config.read(constants._user_settings_)
-       backend = create_backend(config)
-       notifyUser = is_changed(config, backend)
-
-       if notifyUser:
-               logging.info("Changed")
-               import led_handler
-               led = led_handler.LedHandler()
-               led.on()
-       else:
-               logging.info("No Change")
-
-
-if __name__ == "__main__":
-       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
-       logging.basicConfig(level=logging.DEBUG, format=logFormat)
-       rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
-       rotating.setFormatter(logging.Formatter(logFormat))
-       root = logging.getLogger()
-       root.addHandler(rotating)
-       logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
-       logging.info("OS: %s" % (os.uname()[0], ))
-       logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
-       logging.info("Hostname: %s" % os.uname()[1])
-       try:
-               notify_on_change()
-       except:
-               logging.exception("Error")
-               raise
diff --git a/src/backends/__init__.py b/src/backends/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/backends/file_backend.py b/src/backends/file_backend.py
deleted file mode 100644 (file)
index 9f8927a..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's Grand Central service.
-Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-
-Filesystem backend for contact support
-"""
-
-from __future__ import with_statement
-
-import os
-import csv
-
-
-def try_unicode(s):
-       try:
-               return s.decode("UTF-8")
-       except UnicodeDecodeError:
-               return s
-
-
-class CsvAddressBook(object):
-       """
-       Currently supported file format
-       @li Has the first line as a header
-       @li Escapes with quotes
-       @li Comma as delimiter
-       @li Column 0 is name, column 1 is number
-       """
-
-       def __init__(self, name, csvPath):
-               self._name = name
-               self._csvPath = csvPath
-               self._contacts = {}
-
-       @property
-       def name(self):
-               return self._name
-
-       def update_account(self, force = True):
-               if not force or not self._contacts:
-                       return
-               self._contacts = dict(
-                       self._read_csv(self._csvPath)
-               )
-
-       def get_contacts(self):
-               """
-               @returns Iterable of (contact id, contact name)
-               """
-               if not self._contacts:
-                       self._contacts = dict(
-                               self._read_csv(self._csvPath)
-                       )
-               return self._contacts
-
-       def _read_csv(self, csvPath):
-               try:
-                       f = open(csvPath, "rU")
-                       csvReader = iter(csv.reader(f))
-               except IOError, e:
-                       if e.errno == 2:
-                               return
-                       raise
-
-               header = csvReader.next()
-               nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
-
-               yieldCount = 0
-               for row in csvReader:
-                       contactDetails = []
-                       for (phoneType, phoneColumn) in phoneColumns:
-                               try:
-                                       if len(row[phoneColumn]) == 0:
-                                               continue
-                                       contactDetails.append({
-                                               "phoneType": try_unicode(phoneType),
-                                               "phoneNumber": row[phoneColumn],
-                                       })
-                               except IndexError:
-                                       pass
-                       if 0 < len(contactDetails):
-                               nameParts = (row[i].strip() for i in nameColumns)
-                               nameParts = (part for part in nameParts if part)
-                               fullName = " ".join(nameParts).strip()
-                               if not fullName:
-                                       for fallbackColumn in nameFallbacks:
-                                               if row[fallbackColumn].strip():
-                                                       fullName = row[fallbackColumn].strip()
-                                                       break
-                                       else:
-                                               fullName = "Unknown"
-                               fullName = try_unicode(fullName)
-                               yield str(yieldCount), {
-                                       "contactId": "%s-%d" % (self._name, yieldCount),
-                                       "name": fullName,
-                                       "numbers": contactDetails,
-                               }
-                               yieldCount += 1
-
-       @classmethod
-       def _guess_columns(cls, row):
-               firstMiddleLast = [-1, -1, -1]
-               names = []
-               nameFallbacks = []
-               phones = []
-               for i, item in enumerate(row):
-                       lowerItem = item.lower()
-                       if 0 <= lowerItem.find("name"):
-                               names.append((item, i))
-
-                               if 0 <= lowerItem.find("couple"):
-                                       names.insert(0, (item, i))
-
-                               if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"):
-                                       firstMiddleLast[0] = i
-                               elif 0 <= lowerItem.find("middle"):
-                                       firstMiddleLast[1] = i
-                               elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"):
-                                       firstMiddleLast[2] = i
-                       elif 0 <= lowerItem.find("phone"):
-                               phones.append((item, i))
-                       elif 0 <= lowerItem.find("mobile"):
-                               phones.append((item, i))
-                       elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"):
-                               nameFallbacks.append(i)
-               if len(names) == 0:
-                       names.append(("Name", 0))
-               if len(phones) == 0:
-                       phones.append(("Phone", 1))
-
-               nameColumns = [i for i in firstMiddleLast if 0 <= i]
-               if len(nameColumns) < 2:
-                       del nameColumns[:]
-                       nameColumns.append(names[0][1])
-
-               return nameColumns, nameFallbacks, phones
-
-
-class FilesystemAddressBookFactory(object):
-
-       FILETYPE_SUPPORT = {
-               "csv": CsvAddressBook,
-       }
-
-       def __init__(self, path):
-               self._path = path
-
-       def get_addressbooks(self):
-               for root, dirs, filenames in os.walk(self._path):
-                       for filename in filenames:
-                               try:
-                                       name, ext = filename.rsplit(".", 1)
-                               except ValueError:
-                                       continue
-
-                               try:
-                                       cls = self.FILETYPE_SUPPORT[ext]
-                               except KeyError:
-                                       continue
-                               yield cls(name, os.path.join(root, filename))
diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py
deleted file mode 100644 (file)
index 17bbc90..0000000
+++ /dev/null
@@ -1,321 +0,0 @@
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's GoogleVoice service.
-Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-
-Google Voice backend code
-
-Resources
-       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
-       http://posttopic.com/topic/google-voice-add-on-development
-"""
-
-from __future__ import with_statement
-
-import itertools
-import logging
-
-from gvoice import gvoice
-
-from util import io as io_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class GVDialer(object):
-
-       MESSAGE_TEXTS = "Text"
-       MESSAGE_VOICEMAILS = "Voicemail"
-       MESSAGE_ALL = "All"
-
-       HISTORY_RECEIVED = "Received"
-       HISTORY_MISSED = "Missed"
-       HISTORY_PLACED = "Placed"
-       HISTORY_ALL = "All"
-
-       def __init__(self, cookieFile = None):
-               self._gvoice = gvoice.GVoiceBackend(cookieFile)
-               self._texts = []
-               self._voicemails = []
-               self._received = []
-               self._missed = []
-               self._placed = []
-
-       def is_quick_login_possible(self):
-               """
-               @returns True then refresh_account_info might be enough to login, else full login is required
-               """
-               return self._gvoice.is_quick_login_possible()
-
-       def refresh_account_info(self):
-               return self._gvoice.refresh_account_info()
-
-       def login(self, username, password):
-               """
-               Attempt to login to GoogleVoice
-               @returns Whether login was successful or not
-               """
-               return self._gvoice.login(username, password)
-
-       def logout(self):
-               self._texts = []
-               self._voicemails = []
-               self._received = []
-               self._missed = []
-               self._placed = []
-               return self._gvoice.logout()
-
-       def persist(self):
-               return self._gvoice.persist()
-
-       def is_dnd(self):
-               return self._gvoice.is_dnd()
-
-       def set_dnd(self, doNotDisturb):
-               return self._gvoice.set_dnd(doNotDisturb)
-
-       def call(self, outgoingNumber):
-               """
-               This is the main function responsible for initating the callback
-               """
-               return self._gvoice.call(outgoingNumber)
-
-       def cancel(self, outgoingNumber=None):
-               """
-               Cancels a call matching outgoing and forwarding numbers (if given). 
-               Will raise an error if no matching call is being placed
-               """
-               return self._gvoice.cancel(outgoingNumber)
-
-       def send_sms(self, phoneNumbers, message):
-               self._gvoice.send_sms(phoneNumbers, message)
-
-       def search(self, query):
-               """
-               Search your Google Voice Account history for calls, voicemails, and sms
-               Returns ``Folder`` instance containting matching messages
-               """
-               return self._gvoice.search(query)
-
-       def get_feed(self, feed):
-               return self._gvoice.get_feed(feed)
-
-       def download(self, messageId, targetPath):
-               """
-               Download a voicemail or recorded call MP3 matching the given ``msg``
-               which can either be a ``Message`` instance, or a SHA1 identifier. 
-               Message hashes can be found in ``self.voicemail().messages`` for example. 
-               Returns location of saved file.
-               """
-               self._gvoice.download(messageId, targetPath)
-
-       def is_valid_syntax(self, number):
-               """
-               @returns If This number be called ( syntax validation only )
-               """
-               return self._gvoice.is_valid_syntax(number)
-
-       def get_account_number(self):
-               """
-               @returns The GoogleVoice phone number
-               """
-               return self._gvoice.get_account_number()
-
-       def get_callback_numbers(self):
-               """
-               @returns a dictionary mapping call back numbers to descriptions
-               @note These results are cached for 30 minutes.
-               """
-               return self._gvoice.get_callback_numbers()
-
-       def set_callback_number(self, callbacknumber):
-               """
-               Set the number that GoogleVoice calls
-               @param callbacknumber should be a proper 10 digit number
-               """
-               return self._gvoice.set_callback_number(callbacknumber)
-
-       def get_callback_number(self):
-               """
-               @returns Current callback number or None
-               """
-               return self._gvoice.get_callback_number()
-
-       def get_call_history(self, historyType):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               """
-               history = list(self._get_call_history(historyType))
-               history.sort(key=lambda item: item["time"])
-               return history
-
-       def _get_call_history(self, historyType):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               """
-               if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
-                       self._received = list(self._gvoice.get_received_calls())
-                       for item in self._received:
-                               item["action"] = self.HISTORY_RECEIVED
-               if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
-                       self._missed = list(self._gvoice.get_missed_calls())
-                       for item in self._missed:
-                               item["action"] = self.HISTORY_MISSED
-               if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
-                       self._placed = list(self._gvoice.get_placed_calls())
-                       for item in self._placed:
-                               item["action"] = self.HISTORY_PLACED
-               received = self._received
-               missed = self._missed
-               placed = self._placed
-               for item in received:
-                       yield item
-               for item in missed:
-                       yield item
-               for item in placed:
-                       yield item
-
-       def get_messages(self, messageType):
-               messages = list(self._get_messages(messageType))
-               messages.sort(key=lambda message: message["time"])
-               return messages
-
-       def _get_messages(self, messageType):
-               if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
-                       self._voicemails = list(self._gvoice.get_voicemails())
-               if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
-                       self._texts = list(self._gvoice.get_texts())
-               voicemails = self._voicemails
-               smss = self._texts
-
-               conversations = itertools.chain(voicemails, smss)
-               for conversation in conversations:
-                       messages = conversation.messages
-                       messageParts = [
-                               (message.whoFrom, self._format_message(message), message.when)
-                               for message in messages
-                       ]
-
-                       messageDetails = {
-                               "id": conversation.id,
-                               "contactId": conversation.contactId,
-                               "name": conversation.name,
-                               "time": conversation.time,
-                               "relTime": conversation.relTime,
-                               "prettyNumber": conversation.prettyNumber,
-                               "number": conversation.number,
-                               "location": conversation.location,
-                               "messageParts": messageParts,
-                               "type": conversation.type,
-                               "isRead": conversation.isRead,
-                               "isTrash": conversation.isTrash,
-                               "isSpam": conversation.isSpam,
-                               "isArchived": conversation.isArchived,
-                       }
-                       yield messageDetails
-
-       def clear_caches(self):
-               pass
-
-       def get_addressbooks(self):
-               """
-               @returns Iterable of (Address Book Factory, Book Id, Book Name)
-               """
-               yield self, "", ""
-
-       def open_addressbook(self, bookId):
-               return self
-
-       @staticmethod
-       def contact_source_short_name(contactId):
-               return "GV"
-
-       @staticmethod
-       def factory_name():
-               return "Google Voice"
-
-       def _format_message(self, message):
-               messagePartFormat = {
-                       "med1": "<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
diff --git a/src/backends/gvoice/__init__.py b/src/backends/gvoice/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/backends/gvoice/browser_emu.py b/src/backends/gvoice/browser_emu.py
deleted file mode 100644 (file)
index 4fef6e8..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-"""
-@author:         Laszlo Nagy
-@copyright:   (c) 2005 by Szoftver Messias Bt.
-@licence:       BSD style
-
-Objects of the MozillaEmulator class can emulate a browser that is capable of:
-
-       - cookie management
-       - configurable user agent string
-       - GET and POST
-       - multipart POST (send files)
-       - receive content into file
-
-I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it:
-
-       1. Use firefox
-       2. Install and open the livehttpheaders plugin
-       3. Use the website manually with firefox
-       4. Check the GET and POST requests in the livehttpheaders capture window
-       5. Create an instance of the above class and send the same GET and POST requests to the server.
-
-Optional steps:
-
-       - You can change user agent string in the build_opened method
-       - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files
-"""
-
-import urllib2
-import cookielib
-import logging
-
-import socket
-
-
-_moduleLogger = logging.getLogger(__name__)
-socket.setdefaulttimeout(25)
-
-
-def add_proxy(protocol, url, port):
-       proxyInfo = "%s:%s" % (url, port)
-       proxy = urllib2.ProxyHandler(
-               {protocol: proxyInfo}
-       )
-       opener = urllib2.build_opener(proxy)
-       urllib2.install_opener(opener)
-
-
-class MozillaEmulator(object):
-
-       USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
-       #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
-
-       def __init__(self, trycount = 1):
-               """Create a new MozillaEmulator object.
-
-               @param trycount: The download() method will retry the operation if it
-               fails. You can specify -1 for infinite retrying.  A value of 0 means no
-               retrying. A value of 1 means one retry. etc."""
-               self.debug = False
-               self.trycount = trycount
-               self._cookies = cookielib.LWPCookieJar()
-               self._loadedFromCookies = False
-               self._storeCookies = False
-
-       def load_cookies(self, path):
-               assert not self._loadedFromCookies, "Load cookies only once"
-               if path is None:
-                       return
-
-               self._cookies.filename = path
-               try:
-                       self._cookies.load()
-               except cookielib.LoadError:
-                       _moduleLogger.exception("Bad cookie file")
-               except IOError:
-                       _moduleLogger.exception("No cookie file")
-               except Exception, e:
-                       _moduleLogger.exception("Unknown error with cookies")
-               else:
-                       self._loadedFromCookies = True
-               self._storeCookies = True
-
-               return self._loadedFromCookies
-
-       def save_cookies(self):
-               if self._storeCookies:
-                       self._cookies.save()
-
-       def clear_cookies(self):
-               if self._storeCookies:
-                       self._cookies.clear()
-
-       def download(self, url,
-                       postdata = None, extraheaders = None, forbidRedirect = False,
-                       trycount = None, only_head = False,
-               ):
-               """Download an URL with GET or POST methods.
-
-               @param postdata: It can be a string that will be POST-ed to the URL.
-                       When None is given, the method will be GET instead.
-               @param extraheaders: You can add/modify HTTP headers with a dict here.
-               @param forbidRedirect: Set this flag if you do not want to handle
-                       HTTP 301 and 302 redirects.
-               @param trycount: Specify the maximum number of retries here.
-                       0 means no retry on error. Using -1 means infinite retring.
-                       None means the default value (that is self.trycount).
-               @param only_head: Create the openerdirector and return it. In other
-                       words, this will not retrieve any content except HTTP headers.
-
-               @return: The raw HTML page data
-               """
-               _moduleLogger.debug("Performing download of %s" % url)
-
-               if extraheaders is None:
-                       extraheaders = {}
-               if trycount is None:
-                       trycount = self.trycount
-               cnt = 0
-
-               while True:
-                       try:
-                               req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect)
-                               openerdirector = u.open(req)
-                               if self.debug:
-                                       _moduleLogger.info("%r - %r" % (req.get_method(), url))
-                                       _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg))
-                                       _moduleLogger.info("%r" % (openerdirector.headers))
-                               self._cookies.extract_cookies(openerdirector, req)
-                               if only_head:
-                                       return openerdirector
-
-                               return self._read(openerdirector, trycount)
-                       except urllib2.URLError, e:
-                               _moduleLogger.debug("%s: %s" % (e, url))
-                               cnt += 1
-                               if (-1 < trycount) and (trycount < cnt):
-                                       raise
-
-                       # Retry :-)
-                       _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
-
-       def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
-               if extraheaders is None:
-                       extraheaders = {}
-
-               txheaders = {
-                       'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
-                       'Accept-Language': 'en,en-us;q=0.5',
-                       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
-                       'User-Agent': self.USER_AGENT,
-               }
-               for key, value in extraheaders.iteritems():
-                       txheaders[key] = value
-               req = urllib2.Request(url, postdata, txheaders)
-               self._cookies.add_cookie_header(req)
-               if forbidRedirect:
-                       redirector = HTTPNoRedirector()
-                       #_moduleLogger.info("Redirection disabled")
-               else:
-                       redirector = urllib2.HTTPRedirectHandler()
-                       #_moduleLogger.info("Redirection enabled")
-
-               http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
-               https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
-
-               u = urllib2.build_opener(
-                       http_handler,
-                       https_handler,
-                       urllib2.HTTPCookieProcessor(self._cookies),
-                       redirector
-               )
-               if not postdata is None:
-                       req.add_data(postdata)
-               return (req, u)
-
-       def _read(self, openerdirector, trycount):
-               chunks = []
-
-               chunk = openerdirector.read()
-               chunks.append(chunk)
-               #while chunk and cnt < trycount:
-               #       time.sleep(1)
-               #       cnt += 1
-               #       chunk = openerdirector.read()
-               #       chunks.append(chunk)
-
-               data = "".join(chunks)
-
-               if "Content-Length" in openerdirector.info():
-                       assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
-                               openerdirector.info()["Content-Length"],
-                               len(data),
-                       )
-
-               return data
-
-
-class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
-       """This is a custom http redirect handler that FORBIDS redirection."""
-
-       def http_error_302(self, req, fp, code, msg, headers):
-               e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
-               if e.code in (301, 302):
-                       if 'location' in headers:
-                               newurl = headers.getheaders('location')[0]
-                       elif 'uri' in headers:
-                               newurl = headers.getheaders('uri')[0]
-                       e.newurl = newurl
-               _moduleLogger.info("New url: %s" % e.newurl)
-               raise e
diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py
deleted file mode 100755 (executable)
index b0825ef..0000000
+++ /dev/null
@@ -1,1050 +0,0 @@
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's GoogleVoice service.
-Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-
-Google Voice backend code
-
-Resources
-       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
-       http://posttopic.com/topic/google-voice-add-on-development
-"""
-
-from __future__ import with_statement
-
-import os
-import re
-import urllib
-import urllib2
-import time
-import datetime
-import itertools
-import logging
-import inspect
-
-from xml.sax import saxutils
-from xml.etree import ElementTree
-
-try:
-       import simplejson as _simplejson
-       simplejson = _simplejson
-except ImportError:
-       simplejson = None
-
-import browser_emu
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class NetworkError(RuntimeError):
-       pass
-
-
-class MessageText(object):
-
-       ACCURACY_LOW = "med1"
-       ACCURACY_MEDIUM = "med2"
-       ACCURACY_HIGH = "high"
-
-       def __init__(self):
-               self.accuracy = None
-               self.text = None
-
-       def __str__(self):
-               return self.text
-
-       def to_dict(self):
-               return to_dict(self)
-
-       def __eq__(self, other):
-               return self.accuracy == other.accuracy and self.text == other.text
-
-
-class Message(object):
-
-       def __init__(self):
-               self.whoFrom = None
-               self.body = None
-               self.when = None
-
-       def __str__(self):
-               return "%s (%s): %s" % (
-                       self.whoFrom,
-                       self.when,
-                       "".join(unicode(part) for part in self.body)
-               )
-
-       def to_dict(self):
-               selfDict = to_dict(self)
-               selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
-               return selfDict
-
-       def __eq__(self, other):
-               return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
-
-
-class Conversation(object):
-
-       TYPE_VOICEMAIL = "Voicemail"
-       TYPE_SMS = "SMS"
-
-       def __init__(self):
-               self.type = None
-               self.id = None
-               self.contactId = None
-               self.name = None
-               self.location = None
-               self.prettyNumber = None
-               self.number = None
-
-               self.time = None
-               self.relTime = None
-               self.messages = None
-               self.isRead = None
-               self.isSpam = None
-               self.isTrash = None
-               self.isArchived = None
-
-       def __cmp__(self, other):
-               cmpValue = cmp(self.contactId, other.contactId)
-               if cmpValue != 0:
-                       return cmpValue
-
-               cmpValue = cmp(self.time, other.time)
-               if cmpValue != 0:
-                       return cmpValue
-
-               cmpValue = cmp(self.id, other.id)
-               if cmpValue != 0:
-                       return cmpValue
-
-       def to_dict(self):
-               selfDict = to_dict(self)
-               selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
-               return selfDict
-
-
-class GVoiceBackend(object):
-       """
-       This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
-       the functions include login, setting up a callback number, and initalting a callback
-       """
-
-       PHONE_TYPE_HOME = 1
-       PHONE_TYPE_MOBILE = 2
-       PHONE_TYPE_WORK = 3
-       PHONE_TYPE_GIZMO = 7
-
-       def __init__(self, cookieFile = None):
-               # Important items in this function are the setup of the browser emulation and cookie file
-               self._browser = browser_emu.MozillaEmulator(1)
-               self._loadedFromCookies = self._browser.load_cookies(cookieFile)
-
-               self._token = ""
-               self._accountNum = ""
-               self._lastAuthed = 0.0
-               self._callbackNumber = ""
-               self._callbackNumbers = {}
-
-               # Suprisingly, moving all of these from class to self sped up startup time
-
-               self._validateRe = re.compile("^\+?[0-9]{10,}$")
-
-               self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
-
-               SECURE_URL_BASE = "https://www.google.com/voice/"
-               SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
-               self._tokenURL = SECURE_URL_BASE + "m"
-               self._callUrl = SECURE_URL_BASE + "call/connect"
-               self._callCancelURL = SECURE_URL_BASE + "call/cancel"
-               self._sendSmsURL = SECURE_URL_BASE + "sms/send"
-
-               self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
-               self._isDndRe = re.compile(r"""<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 = {
- "&quot;": '"',
- "&nbsp;": " ",
- "&#39;": "'",
-}
-
-
-def unescape(text):
-       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
-       return plain
-
-
-def google_strptime(time):
-       """
-       Hack: Google always returns the time in the same locale.  Sadly if the
-       local system's locale is different, there isn't a way to perfectly handle
-       the time.  So instead we handle implement some time formatting
-       """
-       abbrevTime = time[:-3]
-       parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
-       if time.endswith("PM"):
-               parsedTime += datetime.timedelta(hours=12)
-       return parsedTime
-
-
-def itergroup(iterator, count, padValue = None):
-       """
-       Iterate in groups of 'count' values. If there
-       aren't enough values, the last result is padded with
-       None.
-
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print list(val)
-       [1, 2, 3]
-       [4, 5, 6]
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       (7, None, None)
-       >>> for val in itergroup("123456", 3):
-       ...     print tuple(val)
-       ('1', '2', '3')
-       ('4', '5', '6')
-       >>> for val in itergroup("123456", 3):
-       ...     print repr("".join(val))
-       '123'
-       '456'
-       """
-       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
-       nIterators = (paddedIterator, ) * count
-       return itertools.izip(*nIterators)
-
-
-def safe_eval(s):
-       _TRUE_REGEX = re.compile("true")
-       _FALSE_REGEX = re.compile("false")
-       _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
-       s = _TRUE_REGEX.sub("True", s)
-       s = _FALSE_REGEX.sub("False", s)
-       s = _COMMENT_REGEX.sub("#", s)
-       try:
-               results = eval(s, {}, {})
-       except SyntaxError:
-               _moduleLogger.exception("Oops")
-               results = None
-       return results
-
-
-def _fake_parse_json(flattened):
-       return safe_eval(flattened)
-
-
-def _actual_parse_json(flattened):
-       return simplejson.loads(flattened)
-
-
-if simplejson is None:
-       parse_json = _fake_parse_json
-else:
-       parse_json = _actual_parse_json
-
-
-def extract_payload(flatXml):
-       xmlTree = ElementTree.fromstring(flatXml)
-
-       jsonElement = xmlTree.getchildren()[0]
-       flatJson = jsonElement.text
-       jsonTree = parse_json(flatJson)
-
-       htmlElement = xmlTree.getchildren()[1]
-       flatHtml = htmlElement.text
-
-       return jsonTree, flatHtml
-
-
-def guess_phone_type(number):
-       if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
-               return GVoiceBackend.PHONE_TYPE_GIZMO
-       else:
-               return GVoiceBackend.PHONE_TYPE_MOBILE
-
-
-def get_sane_callback(backend):
-       """
-       Try to set a sane default callback number on these preferences
-       1) 1747 numbers ( Gizmo )
-       2) anything with gizmo in the name
-       3) anything with computer in the name
-       4) the first value
-       """
-       numbers = backend.get_callback_numbers()
-
-       priorityOrderedCriteria = [
-               ("\+1747", None),
-               ("1747", None),
-               ("747", None),
-               (None, "gizmo"),
-               (None, "computer"),
-               (None, "sip"),
-               (None, None),
-       ]
-
-       for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
-               numberMatcher = None
-               descriptionMatcher = None
-               if numberCriteria is not None:
-                       numberMatcher = re.compile(numberCriteria)
-               elif descriptionCriteria is not None:
-                       descriptionMatcher = re.compile(descriptionCriteria, re.I)
-
-               for number, description in numbers.iteritems():
-                       if numberMatcher is not None and numberMatcher.match(number) is None:
-                               continue
-                       if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
-                               continue
-                       return number
-
-
-def set_sane_callback(backend):
-       """
-       Try to set a sane default callback number on these preferences
-       1) 1747 numbers ( Gizmo )
-       2) anything with gizmo in the name
-       3) anything with computer in the name
-       4) the first value
-       """
-       number = get_sane_callback(backend)
-       backend.set_callback_number(number)
-
-
-def _is_not_special(name):
-       return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
-
-
-def to_dict(obj):
-       members = inspect.getmembers(obj)
-       return dict((name, value) for (name, value) in members if _is_not_special(name))
-
-
-def grab_debug_info(username, password):
-       cookieFile = os.path.join(".", "raw_cookies.txt")
-       try:
-               os.remove(cookieFile)
-       except OSError:
-               pass
-
-       backend = GVoiceBackend(cookieFile)
-       browser = backend._browser
-
-       _TEST_WEBPAGES = [
-               ("token", backend._tokenURL),
-               ("login", backend._loginURL),
-               ("isdnd", backend._isDndURL),
-               ("account", backend._XML_ACCOUNT_URL),
-               ("html_contacts", backend._XML_CONTACTS_URL),
-               ("contacts", backend._JSON_CONTACTS_URL),
-               ("csv", backend._CSV_CONTACTS_URL),
-
-               ("voicemail", backend._XML_VOICEMAIL_URL),
-               ("html_sms", backend._XML_SMS_URL),
-               ("sms", backend._JSON_SMS_URL),
-               ("count", backend._JSON_SMS_COUNT_URL),
-
-               ("recent", backend._XML_RECENT_URL),
-               ("placed", backend._XML_PLACED_URL),
-               ("recieved", backend._XML_RECEIVED_URL),
-               ("missed", backend._XML_MISSED_URL),
-       ]
-
-       # Get Pages
-       print "Grabbing pre-login pages"
-       for name, url in _TEST_WEBPAGES:
-               try:
-                       page = browser.download(url)
-               except StandardError, e:
-                       print e.message
-                       continue
-               print "\tWriting to file"
-               with open("not_loggedin_%s.txt" % name, "w") as f:
-                       f.write(page)
-
-       # Login
-       print "Attempting login"
-       galxToken = backend._get_token()
-       loginSuccessOrFailurePage = backend._login(username, password, galxToken)
-       with open("loggingin.txt", "w") as f:
-               print "\tWriting to file"
-               f.write(loginSuccessOrFailurePage)
-       try:
-               backend._grab_account_info(loginSuccessOrFailurePage)
-       except Exception:
-               # Retry in case the redirect failed
-               # luckily refresh_account_info does everything we need for a retry
-               loggedIn = backend.refresh_account_info() is not None
-               if not loggedIn:
-                       raise
-
-       # Get Pages
-       print "Grabbing post-login pages"
-       for name, url in _TEST_WEBPAGES:
-               try:
-                       page = browser.download(url)
-               except StandardError, e:
-                       print str(e)
-                       continue
-               print "\tWriting to file"
-               with open("loggedin_%s.txt" % name, "w") as f:
-                       f.write(page)
-
-       # Cookies
-       browser.save_cookies()
-       print "\tWriting cookies to file"
-       with open("cookies.txt", "w") as f:
-               f.writelines(
-                       "%s: %s\n" % (c.name, c.value)
-                       for c in browser._cookies
-               )
-
-
-def grab_voicemails(username, password):
-       cookieFile = os.path.join(".", "raw_cookies.txt")
-       try:
-               os.remove(cookieFile)
-       except OSError:
-               pass
-
-       backend = GVoiceBackend(cookieFile)
-       backend.login(username, password)
-       voicemails = list(backend.get_voicemails())
-       for voicemail in voicemails:
-               print voicemail.id
-               backend.download(voicemail.id, ".")
-
-
-def main():
-       import sys
-       logging.basicConfig(level=logging.DEBUG)
-       args = sys.argv
-       if 3 <= len(args):
-               username = args[1]
-               password = args[2]
-
-       grab_debug_info(username, password)
-       grab_voicemails(username, password)
-
-
-if __name__ == "__main__":
-       main()
diff --git a/src/backends/null_backend.py b/src/backends/null_backend.py
deleted file mode 100644 (file)
index ebaa932..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's Grand Central service.
-Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-"""
-
-
-class NullAddressBook(object):
-
-       @property
-       def name(self):
-               return "None"
-
-       def update_account(self, force = True):
-               pass
-
-       def get_contacts(self):
-               return {}
-
-
-class NullAddressBookFactory(object):
-
-       def get_addressbooks(self):
-               yield NullAddressBook()
diff --git a/src/backends/qt_backend.py b/src/backends/qt_backend.py
deleted file mode 100644 (file)
index 88e52fa..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-if qt_compat.USES_PYSIDE:
-       try:
-               import QtMobility.Contacts as _QtContacts
-               QtContacts = _QtContacts
-       except ImportError:
-               QtContacts = None
-else:
-       QtContacts = None
-
-import null_backend
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class QtContactsAddressBook(object):
-
-       def __init__(self, name, uri):
-               self._name = name
-               self._uri = uri
-               self._manager = QtContacts.QContactManager.fromUri(uri)
-               self._contacts = None
-
-       @property
-       def name(self):
-               return self._name
-
-       @property
-       def error(self):
-               return self._manager.error()
-
-       def update_account(self, force = True):
-               if not force and self._contacts is not None:
-                       return
-               self._contacts = dict(self._get_contacts())
-
-       def get_contacts(self):
-               if self._contacts is None:
-                       self._contacts = dict(self._get_contacts())
-               return self._contacts
-
-       def _get_contacts(self):
-               contacts = self._manager.contacts()
-               for contact in contacts:
-                       contactId = contact.localId()
-                       contactName = contact.displayLabel()
-                       phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
-                       phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
-                       contactDetails = phones
-                       if 0 < len(contactDetails):
-                               yield str(contactId), {
-                                       "contactId": str(contactId),
-                                       "name": contactName,
-                                       "numbers": contactDetails,
-                               }
-
-
-class _QtContactsAddressBookFactory(object):
-
-       def __init__(self):
-               self._availableManagers = {}
-
-               availableMgrs = QtContacts.QContactManager.availableManagers()
-               availableMgrs.remove("invalid")
-               for managerName in availableMgrs:
-                       params = {}
-                       managerUri = QtContacts.QContactManager.buildUri(managerName, params)
-                       self._availableManagers[managerName] =  managerUri
-
-       def get_addressbooks(self):
-               for name, uri in self._availableManagers.iteritems():
-                       book = QtContactsAddressBook(name, uri)
-                       if book.error:
-                               _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
-                       else:
-                               yield book
-
-
-class _EmptyAddressBookFactory(object):
-
-       def get_addressbooks(self):
-               if False:
-                       yield None
-
-
-if QtContacts is not None:
-       QtContactsAddressBookFactory = _QtContactsAddressBookFactory
-else:
-       QtContactsAddressBookFactory = _EmptyAddressBookFactory
-       _moduleLogger.info("QtContacts support not available")
-
-
-if __name__ == "__main__":
-       factory = QtContactsAddressBookFactory()
-       books = factory.get_addressbooks()
-       for book in books:
-               print book.name
-               print book.get_contacts()
diff --git a/src/call_handler.py b/src/call_handler.py
deleted file mode 100644 (file)
index 9b9c47d..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-import dbus
-try:
-       import telepathy as _telepathy
-       import util.tp_utils as telepathy_utils
-       telepathy = _telepathy
-except ImportError:
-       telepathy = None
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class _FakeSignaller(object):
-
-       def start(self):
-               pass
-
-       def stop(self):
-               pass
-
-
-class _MissedCallWatcher(QtCore.QObject):
-
-       callMissed = qt_compat.Signal()
-
-       def __init__(self):
-               QtCore.QObject.__init__(self)
-               self._isStarted = False
-               self._isSupported = True
-
-               self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
-               self._outstandingRequests = []
-
-       @property
-       def isSupported(self):
-               return self._isSupported
-
-       @property
-       def isStarted(self):
-               return self._isStarted
-
-       def start(self):
-               if self._isStarted:
-                       _moduleLogger.info("voicemail monitor already started")
-                       return
-               try:
-                       self._newChannelSignaller.start()
-               except RuntimeError:
-                       _moduleLogger.exception("Missed call detection not supported")
-                       self._newChannelSignaller = _FakeSignaller()
-                       self._isSupported = False
-               self._isStarted = True
-
-       def stop(self):
-               if not self._isStarted:
-                       _moduleLogger.info("voicemail monitor stopped without starting")
-                       return
-               _moduleLogger.info("Stopping voicemail refresh")
-               self._newChannelSignaller.stop()
-
-               # I don't want to trust whether the cancel happens within the current
-               # callback or not which could be the deciding factor between invalid
-               # iterators or infinite loops
-               localRequests = [r for r in self._outstandingRequests]
-               for request in localRequests:
-                       localRequests.cancel()
-
-               self._isStarted = False
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
-               if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
-                       return
-
-               conn = telepathy.client.Connection(serviceName, connObjectPath)
-               try:
-                       chan = telepathy.client.Channel(serviceName, channelObjectPath)
-               except dbus.exceptions.UnknownMethodException:
-                       _moduleLogger.exception("Client might not have implemented a deprecated method")
-                       return
-               missDetection = telepathy_utils.WasMissedCall(
-                       bus, conn, chan, self._on_missed_call, self._on_error_for_missed
-               )
-               self._outstandingRequests.append(missDetection)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_missed_call(self, missDetection):
-               _moduleLogger.info("Missed a call")
-               self.callMissed.emit()
-               self._outstandingRequests.remove(missDetection)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_error_for_missed(self, missDetection, reason):
-               _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
-               self._outstandingRequests.remove(missDetection)
-
-
-class _DummyMissedCallWatcher(QtCore.QObject):
-
-       callMissed = qt_compat.Signal()
-
-       def __init__(self):
-               QtCore.QObject.__init__(self)
-               self._isStarted = False
-
-       @property
-       def isSupported(self):
-               return False
-
-       @property
-       def isStarted(self):
-               return self._isStarted
-
-       def start(self):
-               self._isStarted = True
-
-       def stop(self):
-               if not self._isStarted:
-                       _moduleLogger.info("voicemail monitor stopped without starting")
-                       return
-               _moduleLogger.info("Stopping voicemail refresh")
-               self._isStarted = False
-
-
-if telepathy is not None:
-       MissedCallWatcher = _MissedCallWatcher
-else:
-       MissedCallWatcher = _DummyMissedCallWatcher
-
-
-if __name__ == "__main__":
-       pass
-
diff --git a/src/constants.py b/src/constants.py
deleted file mode 100644 (file)
index b9d3c79..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import os
-
-__pretty_app_name__ = "DialCentral"
-__app_name__ = "dialcentral"
-__version__ = "1.3.6"
-__build__ = 0
-__app_magic__ = 0xdeadbeef
-_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
-_user_settings_ = "%s/settings.ini" % _data_path_
-_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_
-_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
-_notifier_logpath_ = "%s/notifier.log" % _data_path_
-IS_MAEMO = True
diff --git a/src/dialcentral.py b/src/dialcentral.py
deleted file mode 100755 (executable)
index a20d4fe..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-
-import sys
-
-
-sys.path.append("/opt/dialcentral/lib")
-
-
-import dialcentral_qt
-
-
-if __name__ == "__main__":
-       dialcentral_qt.run()
diff --git a/src/dialcentral_qt.py b/src/dialcentral_qt.py
deleted file mode 100755 (executable)
index a464ad6..0000000
+++ /dev/null
@@ -1,812 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: UTF8 -*-
-
-from __future__ import with_statement
-
-import os
-import base64
-import ConfigParser
-import functools
-import logging
-import logging.handlers
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import constants
-import alarm_handler
-from util import qtpie
-from util import qwrappers
-from util import qui_utils
-from util import misc as misc_utils
-
-import session
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Dialcentral(qwrappers.ApplicationWrapper):
-
-       _DATA_PATHS = [
-               os.path.join(os.path.dirname(__file__), "../share"),
-               os.path.join(os.path.dirname(__file__), "../data"),
-       ]
-
-       def __init__(self, app):
-               self._dataPath = None
-               self._aboutDialog = None
-               self.notifyOnMissed = False
-               self.notifyOnVoicemail = False
-               self.notifyOnSms = False
-
-               self._streamHandler = None
-               self._ledHandler = None
-               self._alarmHandler = alarm_handler.AlarmHandler()
-
-               qwrappers.ApplicationWrapper.__init__(self, app, constants)
-
-       def load_settings(self):
-               try:
-                       config = ConfigParser.SafeConfigParser()
-                       config.read(constants._user_settings_)
-               except IOError, e:
-                       _moduleLogger.info("No settings")
-                       return
-               except ValueError:
-                       _moduleLogger.info("Settings were corrupt")
-                       return
-               except ConfigParser.MissingSectionHeaderError:
-                       _moduleLogger.info("Settings were corrupt")
-                       return
-               except Exception:
-                       _moduleLogger.exception("Unknown loading error")
-
-               self._mainWindow.load_settings(config)
-
-       def save_settings(self):
-               _moduleLogger.info("Saving settings")
-               config = ConfigParser.SafeConfigParser()
-
-               self._mainWindow.save_settings(config)
-
-               with open(constants._user_settings_, "wb") as configFile:
-                       config.write(configFile)
-
-       def get_icon(self, name):
-               if self._dataPath is None:
-                       for path in self._DATA_PATHS:
-                               if os.path.exists(os.path.join(path, name)):
-                                       self._dataPath = path
-                                       break
-               if self._dataPath is not None:
-                       icon = QtGui.QIcon(os.path.join(self._dataPath, name))
-                       return icon
-               else:
-                       return None
-
-       def get_resource(self, name):
-               if self._dataPath is None:
-                       for path in self._DATA_PATHS:
-                               if os.path.exists(os.path.join(path, name)):
-                                       self._dataPath = path
-                                       break
-               if self._dataPath is not None:
-                       return os.path.join(self._dataPath, name)
-               else:
-                       return None
-
-       def _close_windows(self):
-               qwrappers.ApplicationWrapper._close_windows(self)
-               if self._aboutDialog  is not None:
-                       self._aboutDialog.close()
-
-       @property
-       def fsContactsPath(self):
-               return os.path.join(constants._data_path_, "contacts")
-
-       @property
-       def streamHandler(self):
-               if self._streamHandler is None:
-                       import stream_handler
-                       self._streamHandler = stream_handler.StreamHandler()
-               return self._streamHandler
-
-       @property
-       def alarmHandler(self):
-               return self._alarmHandler
-
-       @property
-       def ledHandler(self):
-               if self._ledHandler is None:
-                       import led_handler
-                       self._ledHandler = led_handler.LedHandler()
-               return self._ledHandler
-
-       def _new_main_window(self):
-               return MainWindow(None, self)
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_about(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       if self._aboutDialog is None:
-                               import dialogs
-                               self._aboutDialog = dialogs.AboutDialog(self)
-                       response = self._aboutDialog.run(self._mainWindow.window)
-
-
-class DelayedWidget(object):
-
-       def __init__(self, app, settingsNames):
-               self._layout = QtGui.QVBoxLayout()
-               self._layout.setContentsMargins(0, 0, 0, 0)
-               self._widget = QtGui.QWidget()
-               self._widget.setContentsMargins(0, 0, 0, 0)
-               self._widget.setLayout(self._layout)
-               self._settings = dict((name, "") for name in settingsNames)
-
-               self._child = None
-               self._isEnabled = True
-
-       @property
-       def toplevel(self):
-               return self._widget
-
-       def has_child(self):
-               return self._child is not None
-
-       def set_child(self, child):
-               if self._child is not None:
-                       self._layout.removeWidget(self._child.toplevel)
-               self._child = child
-               if self._child is not None:
-                       self._layout.addWidget(self._child.toplevel)
-
-               self._child.set_settings(self._settings)
-
-               if self._isEnabled:
-                       self._child.enable()
-               else:
-                       self._child.disable()
-
-       @property
-       def child(self):
-               return self._child
-
-       def enable(self):
-               self._isEnabled = True
-               if self._child is not None:
-                       self._child.enable()
-
-       def disable(self):
-               self._isEnabled = False
-               if self._child is not None:
-                       self._child.disable()
-
-       def clear(self):
-               if self._child is not None:
-                       self._child.clear()
-
-       def refresh(self, force=True):
-               if self._child is not None:
-                       self._child.refresh(force)
-
-       def get_settings(self):
-               if self._child is not None:
-                       return self._child.get_settings()
-               else:
-                       return self._settings
-
-       def set_settings(self, settings):
-               if self._child is not None:
-                       self._child.set_settings(settings)
-               else:
-                       self._settings = settings
-
-
-def _tab_factory(tab, app, session, errorLog):
-       import gv_views
-       return gv_views.__dict__[tab](app, session, errorLog)
-
-
-class MainWindow(qwrappers.WindowWrapper):
-
-       KEYPAD_TAB = 0
-       RECENT_TAB = 1
-       MESSAGES_TAB = 2
-       CONTACTS_TAB = 3
-       MAX_TABS = 4
-
-       _TAB_TITLES = [
-               "Dialpad",
-               "History",
-               "Messages",
-               "Contacts",
-       ]
-       assert len(_TAB_TITLES) == MAX_TABS
-
-       _TAB_ICONS = [
-               "dialpad.png",
-               "history.png",
-               "messages.png",
-               "contacts.png",
-       ]
-       assert len(_TAB_ICONS) == MAX_TABS
-
-       _TAB_CLASS = [
-               functools.partial(_tab_factory, "Dialpad"),
-               functools.partial(_tab_factory, "History"),
-               functools.partial(_tab_factory, "Messages"),
-               functools.partial(_tab_factory, "Contacts"),
-       ]
-       assert len(_TAB_CLASS) == MAX_TABS
-
-       # Hack to allow delay importing/loading of tabs
-       _TAB_SETTINGS_NAMES = [
-               (),
-               ("filter", ),
-               ("status", "type"),
-               ("selectedAddressbook", ),
-       ]
-       assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
-
-       def __init__(self, parent, app):
-               qwrappers.WindowWrapper.__init__(self, parent, app)
-               self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
-               self._window.resized.connect(self._on_window_resized)
-               self._errorLog = self._app.errorLog
-
-               self._session = session.Session(self._errorLog, constants._data_path_)
-               self._session.error.connect(self._on_session_error)
-               self._session.loggedIn.connect(self._on_login)
-               self._session.loggedOut.connect(self._on_logout)
-               self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
-               self._session.newMessages.connect(self._on_new_message_alert)
-               self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
-               self._voicemailRefreshDelay = QtCore.QTimer()
-               self._voicemailRefreshDelay.setInterval(30 * 1000)
-               self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
-               self._voicemailRefreshDelay.setSingleShot(True)
-               self._callHandler = None
-               self._updateVoicemailOnMissedCall = False
-
-               self._defaultCredentials = "", ""
-               self._curentCredentials = "", ""
-               self._currentTab = 0
-
-               self._credentialsDialog = None
-               self._smsEntryDialog = None
-               self._accountDialog = None
-
-               self._tabsContents = [
-                       DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
-                       for i in xrange(self.MAX_TABS)
-               ]
-               for tab in self._tabsContents:
-                       tab.disable()
-
-               self._tabWidget = QtGui.QTabWidget()
-               if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
-                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-               else:
-                       self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
-               defaultTabIconSize = self._tabWidget.iconSize()
-               defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height()
-               for tabIndex, (tabTitle, tabIcon) in enumerate(
-                       zip(self._TAB_TITLES, self._TAB_ICONS)
-               ):
-                       icon = self._app.get_icon(tabIcon)
-                       if constants.IS_MAEMO and icon is not None:
-                               tabTitle = ""
-
-                       if icon is None:
-                               self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
-                       else:
-                               iconSize = icon.availableSizes()[0]
-                               defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width())
-                               defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height())
-                               self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
-               defaultTabIconWidth = max(defaultTabIconWidth, 32)
-               defaultTabIconHeight = max(defaultTabIconHeight, 32)
-               self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight))
-               self._tabWidget.currentChanged.connect(self._on_tab_changed)
-               self._tabWidget.setContentsMargins(0, 0, 0, 0)
-
-               self._layout.addWidget(self._tabWidget)
-
-               self._loginAction = QtGui.QAction(None)
-               self._loginAction.setText("Login")
-               self._loginAction.triggered.connect(self._on_login_requested)
-
-               self._importAction = QtGui.QAction(None)
-               self._importAction.setText("Import")
-               self._importAction.triggered.connect(self._on_import)
-
-               self._accountAction = QtGui.QAction(None)
-               self._accountAction.setText("Account")
-               self._accountAction.triggered.connect(self._on_account)
-
-               self._refreshConnectionAction = QtGui.QAction(None)
-               self._refreshConnectionAction.setText("Refresh Connection")
-               self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
-               self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
-
-               self._refreshTabAction = QtGui.QAction(None)
-               self._refreshTabAction.setText("Refresh Tab")
-               self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
-               self._refreshTabAction.triggered.connect(self._on_refresh)
-
-               fileMenu = self._window.menuBar().addMenu("&File")
-               fileMenu.addAction(self._loginAction)
-               fileMenu.addAction(self._refreshTabAction)
-               fileMenu.addAction(self._refreshConnectionAction)
-
-               toolsMenu = self._window.menuBar().addMenu("&Tools")
-               toolsMenu.addAction(self._accountAction)
-               toolsMenu.addAction(self._importAction)
-               toolsMenu.addAction(self._app.aboutAction)
-
-               self._initialize_tab(self._tabWidget.currentIndex())
-               self.set_fullscreen(self._app.fullscreenAction.isChecked())
-               self.update_orientation(self._app.orientation)
-
-       def _init_call_handler(self):
-               if self._callHandler is not None:
-                       return
-               import call_handler
-               self._callHandler = call_handler.MissedCallWatcher()
-               self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
-
-       def set_default_credentials(self, username, password):
-               self._defaultCredentials = username, password
-
-       def get_default_credentials(self):
-               return self._defaultCredentials
-
-       def walk_children(self):
-               if self._smsEntryDialog is not None:
-                       return (self._smsEntryDialog, )
-               else:
-                       return ()
-
-       def start(self):
-               qwrappers.WindowWrapper.start(self)
-               assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up"
-               if self._defaultCredentials != ("", ""):
-                       username, password = self._defaultCredentials[0], self._defaultCredentials[1]
-                       self._curentCredentials = username, password
-                       self._session.login(username, password)
-               else:
-                       self._prompt_for_login()
-
-       def close(self):
-               for diag in (
-                       self._credentialsDialog,
-                       self._accountDialog,
-               ):
-                       if diag is not None:
-                               diag.close()
-               for child in self.walk_children():
-                       child.window.destroyed.disconnect(self._on_child_close)
-                       child.window.closed.disconnect(self._on_child_close)
-                       child.close()
-               self._window.close()
-
-       def destroy(self):
-               qwrappers.WindowWrapper.destroy(self)
-               if self._session.state != self._session.LOGGEDOUT_STATE:
-                       self._session.logout()
-
-       def get_current_tab(self):
-               return self._currentTab
-
-       def set_current_tab(self, tabIndex):
-               self._tabWidget.setCurrentIndex(tabIndex)
-
-       def load_settings(self, config):
-               blobs = "", ""
-               isFullscreen = False
-               orientation = self._app.orientation
-               tabIndex = 0
-               try:
-                       blobs = [
-                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
-                               for i in xrange(len(self.get_default_credentials()))
-                       ]
-                       isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
-                       tabIndex = config.getint(constants.__pretty_app_name__, "tab")
-                       orientation = config.get(constants.__pretty_app_name__, "orientation")
-               except ConfigParser.NoOptionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing option %s" % (
-                                       constants._user_settings_,
-                                       e.option,
-                               ),
-                       )
-               except ConfigParser.NoSectionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing section %s" % (
-                                       constants._user_settings_,
-                                       e.section,
-                               ),
-                       )
-               except Exception:
-                       _moduleLogger.exception("Unknown loading error")
-
-               try:
-                       self._app.alarmHandler.load_settings(config, "alarm")
-                       self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
-                       self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
-                       self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
-                       self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
-               except ConfigParser.NoOptionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing option %s" % (
-                                       constants._user_settings_,
-                                       e.option,
-                               ),
-                       )
-               except ConfigParser.NoSectionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing section %s" % (
-                                       constants._user_settings_,
-                                       e.section,
-                               ),
-                       )
-               except Exception:
-                       _moduleLogger.exception("Unknown loading error")
-
-               creds = (
-                       base64.b64decode(blob)
-                       for blob in blobs
-               )
-               self.set_default_credentials(*creds)
-               self._app.fullscreenAction.setChecked(isFullscreen)
-               self._app.set_orientation(orientation)
-               self.set_current_tab(tabIndex)
-
-               backendId = 2 # For backwards compatibility
-               for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
-                       sectionName = "%s - %s" % (backendId, tabTitle)
-                       settings = self._tabsContents[tabIndex].get_settings()
-                       for settingName in settings.iterkeys():
-                               try:
-                                       settingValue = config.get(sectionName, settingName)
-                               except ConfigParser.NoOptionError, e:
-                                       _moduleLogger.info(
-                                               "Settings file %s is missing section %s" % (
-                                                       constants._user_settings_,
-                                                       e.section,
-                                               ),
-                                       )
-                                       return
-                               except ConfigParser.NoSectionError, e:
-                                       _moduleLogger.info(
-                                               "Settings file %s is missing section %s" % (
-                                                       constants._user_settings_,
-                                                       e.section,
-                                               ),
-                                       )
-                                       return
-                               except Exception:
-                                       _moduleLogger.exception("Unknown loading error")
-                                       return
-                               settings[settingName] = settingValue
-                       self._tabsContents[tabIndex].set_settings(settings)
-
-       def save_settings(self, config):
-               config.add_section(constants.__pretty_app_name__)
-               config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
-               config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
-               config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
-               for i, value in enumerate(self.get_default_credentials()):
-                       blob = base64.b64encode(value)
-                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
-
-               config.add_section("alarm")
-               self._app.alarmHandler.save_settings(config, "alarm")
-               config.add_section("2 - Account Info")
-               config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
-               config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
-               config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
-               config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
-
-               backendId = 2 # For backwards compatibility
-               for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
-                       sectionName = "%s - %s" % (backendId, tabTitle)
-                       config.add_section(sectionName)
-                       tabSettings = self._tabsContents[tabIndex].get_settings()
-                       for settingName, settingValue in tabSettings.iteritems():
-                               config.set(sectionName, settingName, settingValue)
-
-       def update_orientation(self, orientation):
-               qwrappers.WindowWrapper.update_orientation(self, orientation)
-               windowOrientation = self.idealWindowOrientation
-               if windowOrientation == QtCore.Qt.Horizontal:
-                       self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
-               else:
-                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-
-       def _initialize_tab(self, index):
-               assert index < self.MAX_TABS, "Invalid tab"
-               if not self._tabsContents[index].has_child():
-                       tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
-                       self._tabsContents[index].set_child(tab)
-               self._tabsContents[index].refresh(force=False)
-
-       def _prompt_for_login(self):
-               if self._credentialsDialog is None:
-                       import dialogs
-                       self._credentialsDialog = dialogs.CredentialsDialog(self._app)
-               credentials = self._credentialsDialog.run(
-                       self._defaultCredentials[0], self._defaultCredentials[1], self.window
-               )
-               if credentials is None:
-                       return
-               username, password = credentials
-               self._curentCredentials = username, password
-               self._session.login(username, password)
-
-       def _show_account_dialog(self):
-               if self._accountDialog is None:
-                       import dialogs
-                       self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
-                       self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
-                       self._accountDialog.settingsApproved.connect(self._on_settings_approved)
-
-               if self._callHandler is not None and not self._callHandler.isSupported:
-                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
-               elif self._updateVoicemailOnMissedCall:
-                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
-               else:
-                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
-               self._accountDialog.notifications = self._app.alarmHandler.alarmType
-               self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
-               self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
-               self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
-               self._accountDialog.notifyOnSms = self._app.notifyOnSms
-               self._accountDialog.set_callbacks(
-                       self._session.get_callback_numbers(), self._session.get_callback_number()
-               )
-               accountNumberToDisplay = self._session.get_account_number()
-               if not accountNumberToDisplay:
-                       accountNumberToDisplay = "Not Available (%s)" % self._session.state
-               self._accountDialog.set_account_number(accountNumberToDisplay)
-               self._accountDialog.orientation = self._app.orientation
-
-               self._accountDialog.run()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_settings_approved(self):
-               if self._accountDialog.doClear:
-                       self._session.logout_and_clear()
-                       self._defaultCredentials = "", ""
-                       self._curentCredentials = "", ""
-                       for tab in self._tabsContents:
-                               tab.disable()
-               else:
-                       callbackNumber = self._accountDialog.selectedCallback
-                       self._session.set_callback_number(callbackNumber)
-
-               if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED:
-                       pass
-               elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
-                       self._updateVoicemailOnMissedCall = True
-                       self._init_call_handler()
-                       self._callHandler.start()
-               else:
-                       self._updateVoicemailOnMissedCall = False
-                       if self._callHandler is not None:
-                               self._callHandler.stop()
-               if (
-                       self._accountDialog.notifyOnMissed or
-                       self._accountDialog.notifyOnVoicemail or
-                       self._accountDialog.notifyOnSms
-               ):
-                       notifications = self._accountDialog.notifications
-               else:
-                       notifications = self._accountDialog.ALARM_NONE
-               self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
-
-               self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
-               self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
-               self._app.notifyOnSms = self._accountDialog.notifyOnSms
-               self._app.set_orientation(self._accountDialog.orientation)
-               self._app.save_settings()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_window_resized(self):
-               with qui_utils.notify_error(self._app.errorLog):
-                       windowOrientation = self.idealWindowOrientation
-                       if windowOrientation == QtCore.Qt.Horizontal:
-                               self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
-                       else:
-                               self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_new_message_alert(self):
-               with qui_utils.notify_error(self._errorLog):
-                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
-                               if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
-                                       self._errorLog.push_message("New messages")
-                               else:
-                                       self._app.ledHandler.on()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_call_missed(self):
-               with qui_utils.notify_error(self._errorLog):
-                       self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
-
-       @qt_compat.Slot(str)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_session_error(self, message):
-               with qui_utils.notify_error(self._errorLog):
-                       self._errorLog.push_error(message)
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_login(self):
-               with qui_utils.notify_error(self._errorLog):
-                       changedAccounts = self._defaultCredentials != self._curentCredentials
-                       noCallback = not self._session.get_callback_number()
-                       if changedAccounts or noCallback:
-                               self._show_account_dialog()
-
-                       self._defaultCredentials = self._curentCredentials
-
-                       for tab in self._tabsContents:
-                               tab.enable()
-                       self._initialize_tab(self._currentTab)
-                       if self._updateVoicemailOnMissedCall:
-                               self._init_call_handler()
-                               self._callHandler.start()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_logout(self):
-               with qui_utils.notify_error(self._errorLog):
-                       for tab in self._tabsContents:
-                               tab.disable()
-                       if self._callHandler is not None:
-                               self._callHandler.stop()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_app_alert(self):
-               with qui_utils.notify_error(self._errorLog):
-                       if self._session.state == self._session.LOGGEDIN_STATE:
-                               messageType = {
-                                       (True, True): self._session.MESSAGE_ALL,
-                                       (True, False): self._session.MESSAGE_TEXTS,
-                                       (False, True): self._session.MESSAGE_VOICEMAILS,
-                               }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
-                               self._session.update_messages(messageType, force=True)
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_recipients_changed(self):
-               with qui_utils.notify_error(self._errorLog):
-                       if self._session.draft.get_num_contacts() == 0:
-                               return
-
-                       if self._smsEntryDialog is None:
-                               import dialogs
-                               self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
-                               self._smsEntryDialog.window.destroyed.connect(self._on_child_close)
-                               self._smsEntryDialog.window.closed.connect(self._on_child_close)
-                               self._smsEntryDialog.window.show()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_child_close(self, obj = None):
-               self._smsEntryDialog = None
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_login_requested(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       self._prompt_for_login()
-
-       @qt_compat.Slot(int)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_tab_changed(self, index):
-               with qui_utils.notify_error(self._errorLog):
-                       self._currentTab = index
-                       self._initialize_tab(index)
-                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
-                               self._app.ledHandler.off()
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_refresh(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       self._tabsContents[self._currentTab].refresh(force=True)
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_refresh_connection(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       self._session.refresh_connection()
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_import(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
-                       csvName = unicode(csvName)
-                       if not csvName:
-                               return
-                       import shutil
-                       shutil.copy2(csvName, self._app.fsContactsPath)
-                       if self._tabsContents[self.CONTACTS_TAB].has_child:
-                               self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_account(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings"
-                       self._show_account_dialog()
-
-
-def run():
-       try:
-               os.makedirs(constants._data_path_)
-       except OSError, e:
-               if e.errno != 17:
-                       raise
-
-       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
-       logging.basicConfig(level=logging.DEBUG, format=logFormat)
-       rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
-       rotating.setFormatter(logging.Formatter(logFormat))
-       root = logging.getLogger()
-       root.addHandler(rotating)
-       _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
-       _moduleLogger.info("OS: %s" % (os.uname()[0], ))
-       _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
-       _moduleLogger.info("Hostname: %s" % os.uname()[1])
-
-       try:
-               import gobject
-               gobject.threads_init()
-       except ImportError:
-               _moduleLogger.info("GObject support not available")
-       try:
-               import dbus
-               try:
-                       from dbus.mainloop.qt import DBusQtMainLoop
-                       DBusQtMainLoop(set_as_default=True)
-                       _moduleLogger.info("Using Qt mainloop")
-               except ImportError:
-                       try:
-                               from dbus.mainloop.glib import DBusGMainLoop
-                               DBusGMainLoop(set_as_default=True)
-                               _moduleLogger.info("Using GObject mainloop")
-                       except ImportError:
-                               _moduleLogger.info("Mainloop not available")
-       except ImportError:
-               _moduleLogger.info("DBus support not available")
-
-       app = QtGui.QApplication([])
-       handle = Dialcentral(app)
-       qtpie.init_pies()
-       return app.exec_()
-
-
-if __name__ == "__main__":
-       import sys
-
-       val = run()
-       sys.exit(val)
diff --git a/src/dialogs.py b/src/dialogs.py
deleted file mode 100644 (file)
index 8fbf328..0000000
+++ /dev/null
@@ -1,1192 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import functools
-import copy
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import constants
-from util import qwrappers
-from util import qui_utils
-from util import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class CredentialsDialog(object):
-
-       def __init__(self, app):
-               self._app = app
-               self._usernameField = QtGui.QLineEdit()
-               self._passwordField = QtGui.QLineEdit()
-               self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
-
-               self._credLayout = QtGui.QGridLayout()
-               self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
-               self._credLayout.addWidget(self._usernameField, 0, 1)
-               self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
-               self._credLayout.addWidget(self._passwordField, 1, 1)
-
-               self._loginButton = QtGui.QPushButton("&Login")
-               self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
-               self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
-
-               self._layout = QtGui.QVBoxLayout()
-               self._layout.addLayout(self._credLayout)
-               self._layout.addWidget(self._buttonLayout)
-
-               self._dialog = QtGui.QDialog()
-               self._dialog.setWindowTitle("Login")
-               self._dialog.setLayout(self._layout)
-               self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
-               self._buttonLayout.accepted.connect(self._dialog.accept)
-               self._buttonLayout.rejected.connect(self._dialog.reject)
-
-               self._closeWindowAction = QtGui.QAction(None)
-               self._closeWindowAction.setText("Close")
-               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
-               self._closeWindowAction.triggered.connect(self._on_close_window)
-
-               self._dialog.addAction(self._closeWindowAction)
-               self._dialog.addAction(app.quitAction)
-               self._dialog.addAction(app.fullscreenAction)
-
-       def run(self, defaultUsername, defaultPassword, parent=None):
-               self._dialog.setParent(parent, QtCore.Qt.Dialog)
-               try:
-                       self._usernameField.setText(defaultUsername)
-                       self._passwordField.setText(defaultPassword)
-
-                       response = self._dialog.exec_()
-                       if response == QtGui.QDialog.Accepted:
-                               return str(self._usernameField.text()), str(self._passwordField.text())
-                       elif response == QtGui.QDialog.Rejected:
-                               return None
-                       else:
-                               _moduleLogger.error("Unknown response")
-                               return None
-               finally:
-                       self._dialog.setParent(None, QtCore.Qt.Dialog)
-
-       def close(self):
-               try:
-                       self._dialog.reject()
-               except RuntimeError:
-                       _moduleLogger.exception("Oh well")
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_close_window(self, checked = True):
-               with qui_utils.notify_error(self._app.errorLog):
-                       self._dialog.reject()
-
-
-class AboutDialog(object):
-
-       def __init__(self, app):
-               self._app = app
-               self._title = QtGui.QLabel(
-                       "<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
diff --git a/src/examples/log_notifier.py b/src/examples/log_notifier.py
deleted file mode 100644 (file)
index 541ac18..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import sys
-import datetime
-import ConfigParser
-
-
-sys.path.insert(0,"/usr/lib/dialcentral/")
-
-
-import constants
-import alarm_notify
-
-
-def notify_on_change():
-       with open(constants._notifier_logpath_, "a") as file:
-               file.write("Notification: %r\n" % (datetime.datetime.now(), ))
-
-               config = ConfigParser.SafeConfigParser()
-               config.read(constants._user_settings_)
-               backend = alarm_notify.create_backend(config)
-               notifyUser = alarm_notify.is_changed(config, backend)
-
-               if notifyUser:
-                       file.write("\tChange occurred\n")
-
-
-if __name__ == "__main__":
-       notify_on_change()
diff --git a/src/examples/sound_notifier.py b/src/examples/sound_notifier.py
deleted file mode 100644 (file)
index c31e413..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import sys
-import ConfigParser
-import logging
-
-
-sys.path.insert(0,"/usr/lib/dialcentral/")
-
-
-import constants
-import alarm_notify
-
-
-def notify_on_change():
-       config = ConfigParser.SafeConfigParser()
-       config.read(constants._user_settings_)
-       backend = alarm_notify.create_backend(config)
-       notifyUser = alarm_notify.is_changed(config, backend)
-
-       config = ConfigParser.SafeConfigParser()
-       config.read(constants._custom_notifier_settings_)
-       soundFile = config.get("Sound Notifier", "soundfile")
-       soundFile = "/usr/lib/gv-notifier/alert.mp3"
-
-       if notifyUser:
-               import subprocess
-               import led_handler
-               logging.info("Changed, playing %s" % soundFile)
-               led = led_handler.LedHandler()
-               led.on()
-               soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True)
-       else:
-               logging.info("No Change")
-
-
-if __name__ == "__main__":
-       logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
-       logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__))
-       logging.info("OS: %s" % (os.uname()[0], ))
-       logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
-       logging.info("Hostname: %s" % os.uname()[1])
-       try:
-               notify_on_change()
-       except:
-               logging.exception("Error")
-               raise
diff --git a/src/gv_views.py b/src/gv_views.py
deleted file mode 100644 (file)
index 2bd0663..0000000
+++ /dev/null
@@ -1,977 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import datetime
-import string
-import itertools
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-from util import qtpie
-from util import qui_utils
-from util import misc as misc_utils
-
-import backends.null_backend as null_backend
-import backends.file_backend as file_backend
-import backends.qt_backend as qt_backend
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_SENTINEL_ICON = QtGui.QIcon()
-
-
-class Dialpad(object):
-
-       def __init__(self, app, session, errorLog):
-               self._app = app
-               self._session = session
-               self._errorLog = errorLog
-
-               self._plus = QtGui.QPushButton("+")
-               self._plus.clicked.connect(lambda: self._on_keypress("+"))
-               self._entry = QtGui.QLineEdit()
-
-               backAction = QtGui.QAction(None)
-               backAction.setText("Back")
-               backAction.triggered.connect(self._on_backspace)
-               backPieItem = qtpie.QActionPieItem(backAction)
-               clearAction = QtGui.QAction(None)
-               clearAction.setText("Clear")
-               clearAction.triggered.connect(self._on_clear_text)
-               clearPieItem = qtpie.QActionPieItem(clearAction)
-               backSlices = [
-                       qtpie.PieFiling.NULL_CENTER,
-                       clearPieItem,
-                       qtpie.PieFiling.NULL_CENTER,
-                       qtpie.PieFiling.NULL_CENTER,
-               ]
-               self._back = qtpie.QPieButton(backPieItem)
-               self._back.set_center(backPieItem)
-               for slice in backSlices:
-                       self._back.insertItem(slice)
-
-               self._entryLayout = QtGui.QHBoxLayout()
-               self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
-               self._entryLayout.addWidget(self._entry, 1000)
-               self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
-
-               smsIcon = self._app.get_icon("messages.png")
-               self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
-               self._smsButton.clicked.connect(self._on_sms_clicked)
-               self._smsButton.setSizePolicy(QtGui.QSizePolicy(
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.PushButton,
-               ))
-               callIcon = self._app.get_icon("dialpad.png")
-               self._callButton = QtGui.QPushButton(callIcon, "Call")
-               self._callButton.clicked.connect(self._on_call_clicked)
-               self._callButton.setSizePolicy(QtGui.QSizePolicy(
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.PushButton,
-               ))
-
-               self._padLayout = QtGui.QGridLayout()
-               rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
-               columns = [0, 1, 2] * 3
-               keys = [
-                       ("1", ""),
-                       ("2", "ABC"),
-                       ("3", "DEF"),
-                       ("4", "GHI"),
-                       ("5", "JKL"),
-                       ("6", "MNO"),
-                       ("7", "PQRS"),
-                       ("8", "TUV"),
-                       ("9", "WXYZ"),
-               ]
-               for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
-                       self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
-               self._zerothButton = QtGui.QPushButton("0")
-               self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
-               self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.PushButton,
-               ))
-               self._padLayout.addWidget(self._smsButton, 3, 0)
-               self._padLayout.addWidget(self._zerothButton)
-               self._padLayout.addWidget(self._callButton, 3, 2)
-
-               self._layout = QtGui.QVBoxLayout()
-               self._layout.addLayout(self._entryLayout, 0)
-               self._layout.addLayout(self._padLayout, 1000000)
-               self._widget = QtGui.QWidget()
-               self._widget.setLayout(self._layout)
-
-       @property
-       def toplevel(self):
-               return self._widget
-
-       def enable(self):
-               self._smsButton.setEnabled(True)
-               self._callButton.setEnabled(True)
-
-       def disable(self):
-               self._smsButton.setEnabled(False)
-               self._callButton.setEnabled(False)
-
-       def get_settings(self):
-               return {}
-
-       def set_settings(self, settings):
-               pass
-
-       def clear(self):
-               pass
-
-       def refresh(self, force = True):
-               pass
-
-       def _generate_key_button(self, center, letters):
-               button = QtGui.QPushButton("%s\n%s" % (center, letters))
-               button.setSizePolicy(QtGui.QSizePolicy(
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.MinimumExpanding,
-                       QtGui.QSizePolicy.PushButton,
-               ))
-               button.clicked.connect(lambda: self._on_keypress(center))
-               return button
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_keypress(self, key):
-               with qui_utils.notify_error(self._errorLog):
-                       self._entry.insert(key)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_backspace(self, toggled = False):
-               with qui_utils.notify_error(self._errorLog):
-                       self._entry.backspace()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_clear_text(self, toggled = False):
-               with qui_utils.notify_error(self._errorLog):
-                       self._entry.clear()
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_sms_clicked(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       number = misc_utils.make_ugly(str(self._entry.text()))
-                       self._entry.clear()
-
-                       contactId = number
-                       title = misc_utils.make_pretty(number)
-                       description = misc_utils.make_pretty(number)
-                       numbersWithDescriptions = [(number, "")]
-                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_call_clicked(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       number = misc_utils.make_ugly(str(self._entry.text()))
-                       self._entry.clear()
-
-                       contactId = number
-                       title = misc_utils.make_pretty(number)
-                       description = misc_utils.make_pretty(number)
-                       numbersWithDescriptions = [(number, "")]
-                       self._session.draft.clear()
-                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
-                       self._session.draft.call()
-
-
-class TimeCategories(object):
-
-       _NOW_SECTION = 0
-       _TODAY_SECTION = 1
-       _WEEK_SECTION = 2
-       _MONTH_SECTION = 3
-       _REST_SECTION = 4
-       _MAX_SECTIONS = 5
-
-       _NO_ELAPSED = datetime.timedelta(hours=1)
-       _WEEK_ELAPSED = datetime.timedelta(weeks=1)
-       _MONTH_ELAPSED = datetime.timedelta(days=30)
-
-       def __init__(self, parentItem):
-               self._timeItems = [
-                       QtGui.QStandardItem(description)
-                       for (i, description) in zip(
-                               xrange(self._MAX_SECTIONS),
-                               ["Now", "Today", "Week", "Month", "Past"],
-                       )
-               ]
-               for item in self._timeItems:
-                       item.setEditable(False)
-                       item.setCheckable(False)
-                       row = (item, )
-                       parentItem.appendRow(row)
-
-               self._today = datetime.datetime(1900, 1, 1)
-
-               self.prepare_for_update(self._today)
-
-       def prepare_for_update(self, newToday):
-               self._today = newToday
-               for item in self._timeItems:
-                       item.removeRows(0, item.rowCount())
-               try:
-                       hour = self._today.strftime("%X")
-                       day = self._today.strftime("%x")
-               except ValueError:
-                       _moduleLogger.exception("Can't format times")
-                       hour = "Now"
-                       day = "Today"
-               self._timeItems[self._NOW_SECTION].setText(hour)
-               self._timeItems[self._TODAY_SECTION].setText(day)
-
-       def add_row(self, rowDate, row):
-               elapsedTime = self._today - rowDate
-               todayTuple = self._today.timetuple()
-               rowTuple = rowDate.timetuple()
-               if elapsedTime < self._NO_ELAPSED:
-                       section = self._NOW_SECTION
-               elif todayTuple[0:3] == rowTuple[0:3]:
-                       section = self._TODAY_SECTION
-               elif elapsedTime < self._WEEK_ELAPSED:
-                       section = self._WEEK_SECTION
-               elif elapsedTime < self._MONTH_ELAPSED:
-                       section = self._MONTH_SECTION
-               else:
-                       section = self._REST_SECTION
-               self._timeItems[section].appendRow(row)
-
-       def get_item(self, timeIndex, rowIndex, column):
-               timeItem = self._timeItems[timeIndex]
-               item = timeItem.child(rowIndex, column)
-               return item
-
-
-class History(object):
-
-       DETAILS_IDX = 0
-       FROM_IDX = 1
-       MAX_IDX = 2
-
-       HISTORY_RECEIVED = "Received"
-       HISTORY_MISSED = "Missed"
-       HISTORY_PLACED = "Placed"
-       HISTORY_ALL = "All"
-
-       HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
-       HISTORY_COLUMNS = ["", "From"]
-       assert len(HISTORY_COLUMNS) == MAX_IDX
-
-       def __init__(self, app, session, errorLog):
-               self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
-               self._app = app
-               self._session = session
-               self._session.historyUpdated.connect(self._on_history_updated)
-               self._errorLog = errorLog
-
-               self._typeSelection = QtGui.QComboBox()
-               self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
-               self._typeSelection.setCurrentIndex(
-                       self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
-               )
-               self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
-               refreshIcon = qui_utils.get_theme_icon(
-                       ("view-refresh", "general_refresh", "gtk-refresh", ),
-                       _SENTINEL_ICON
-               )
-               if refreshIcon is not _SENTINEL_ICON:
-                       self._refreshButton = QtGui.QPushButton(refreshIcon, "")
-               else:
-                       self._refreshButton = QtGui.QPushButton("Refresh")
-               self._refreshButton.clicked.connect(self._on_refresh_clicked)
-               self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
-                       QtGui.QSizePolicy.Minimum,
-                       QtGui.QSizePolicy.Minimum,
-                       QtGui.QSizePolicy.PushButton,
-               ))
-               self._managerLayout = QtGui.QHBoxLayout()
-               self._managerLayout.addWidget(self._typeSelection, 1000)
-               self._managerLayout.addWidget(self._refreshButton, 0)
-
-               self._itemStore = QtGui.QStandardItemModel()
-               self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
-               self._categoryManager = TimeCategories(self._itemStore)
-
-               self._itemView = QtGui.QTreeView()
-               self._itemView.setModel(self._itemStore)
-               self._itemView.setUniformRowHeights(True)
-               self._itemView.setRootIsDecorated(False)
-               self._itemView.setIndentation(0)
-               self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-               self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
-               self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
-               self._itemView.setHeaderHidden(True)
-               self._itemView.setItemsExpandable(False)
-               self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
-               self._itemView.activated.connect(self._on_row_activated)
-
-               self._layout = QtGui.QVBoxLayout()
-               self._layout.addLayout(self._managerLayout)
-               self._layout.addWidget(self._itemView)
-               self._widget = QtGui.QWidget()
-               self._widget.setLayout(self._layout)
-
-               self._actionIcon = {
-                       "Placed": self._app.get_icon("placed.png"),
-                       "Missed": self._app.get_icon("missed.png"),
-                       "Received": self._app.get_icon("received.png"),
-               }
-
-               self._populate_items()
-
-       @property
-       def toplevel(self):
-               return self._widget
-
-       def enable(self):
-               self._itemView.setEnabled(True)
-
-       def disable(self):
-               self._itemView.setEnabled(False)
-
-       def get_settings(self):
-               return {
-                       "filter": self._selectedFilter,
-               }
-
-       def set_settings(self, settings):
-               selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
-               if selectedFilter in self.HISTORY_ITEM_TYPES:
-                       self._selectedFilter = selectedFilter
-                       self._typeSelection.setCurrentIndex(
-                               self.HISTORY_ITEM_TYPES.index(selectedFilter)
-                       )
-
-       def clear(self):
-               self._itemView.clear()
-
-       def refresh(self, force=True):
-               self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-
-               if self._selectedFilter == self.HISTORY_RECEIVED:
-                       self._session.update_history(self._session.HISTORY_RECEIVED, force)
-               elif self._selectedFilter == self.HISTORY_MISSED:
-                       self._session.update_history(self._session.HISTORY_MISSED, force)
-               elif self._selectedFilter == self.HISTORY_PLACED:
-                       self._session.update_history(self._session.HISTORY_PLACED, force)
-               elif self._selectedFilter == self.HISTORY_ALL:
-                       self._session.update_history(self._session.HISTORY_ALL, force)
-               else:
-                       assert False, "How did we get here?"
-
-               if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
-                       self._app.ledHandler.off()
-
-       def _populate_items(self):
-               self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
-
-               history = self._session.get_history()
-               history.sort(key=lambda item: item["time"], reverse=True)
-               for event in history:
-                       if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
-                               continue
-
-                       relTime = event["relTime"]
-                       action = event["action"]
-                       number = event["number"]
-                       prettyNumber = misc_utils.make_pretty(number)
-                       if prettyNumber.startswith("+1 "):
-                               prettyNumber = prettyNumber[len("+1 "):]
-                       name = event["name"]
-                       if not name or name == number:
-                               name = event["location"]
-                       if not name:
-                               name = "Unknown"
-
-                       detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime))
-                       detailsFont = detailsItem.font()
-                       detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 5))
-                       detailsItem.setFont(detailsFont)
-                       nameItem = QtGui.QStandardItem(name)
-                       nameFont = nameItem.font()
-                       nameFont.setPointSize(nameFont.pointSize() + 4)
-                       nameItem.setFont(nameFont)
-                       row = detailsItem, nameItem
-                       for item in row:
-                               item.setEditable(False)
-                               item.setCheckable(False)
-                       row[self.DETAILS_IDX].setData(event)
-                       self._categoryManager.add_row(event["time"], row)
-               self._itemView.expandAll()
-
-       @qt_compat.Slot(str)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_filter_changed(self, newItem):
-               with qui_utils.notify_error(self._errorLog):
-                       self._selectedFilter = str(newItem)
-                       self._populate_items()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_history_updated(self):
-               with qui_utils.notify_error(self._errorLog):
-                       self._populate_items()
-
-       @qt_compat.Slot()
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_refresh_clicked(self, arg = None):
-               with qui_utils.notify_error(self._errorLog):
-                       self.refresh(force=True)
-
-       @qt_compat.Slot(QtCore.QModelIndex)
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_row_activated(self, index):
-               with qui_utils.notify_error(self._errorLog):
-                       timeIndex = index.parent()
-                       if not timeIndex.isValid():
-                               return
-                       timeRow = timeIndex.row()
-                       row = index.row()
-                       detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
-                       fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
-                       contactDetails = detailsItem.data()
-
-                       title = unicode(fromItem.text())
-                       number = str(contactDetails["number"])
-                       contactId = number # ids don't seem too unique so using numbers
-
-                       descriptionRows = []
-                       for t in xrange(self._itemStore.rowCount()):
-                               randomTimeItem = self._itemStore.item(t, 0)
-                               for i in xrange(randomTimeItem.rowCount()):
-                                       iItem = randomTimeItem.child(i, 0)
-                                       iContactDetails = iItem.data()
-                                       iNumber = str(iContactDetails["number"])
-                                       if number != iNumber:
-                                               continue
-                                       relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
-                                       action = str(iContactDetails["action"])
-                                       number = str(iContactDetails["number"])
-                                       prettyNumber = misc_utils.make_pretty(number)
-                                       rowItems = relTime, action, prettyNumber
-                                       descriptionRows.append("<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 ""
diff --git a/src/led_handler.py b/src/led_handler.py
deleted file mode 100755 (executable)
index 0914105..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-
-import dbus
-
-
-class _NokiaLedHandler(object):
-
-       def __init__(self):
-               self._bus = dbus.SystemBus()
-               self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
-               self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
-
-               self._ledPattern = "PatternCommunicationChat"
-
-       def on(self):
-               self._mceRequest.req_led_pattern_activate(self._ledPattern)
-
-       def off(self):
-               self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
-
-
-class _NoLedHandler(object):
-
-       def __init__(self):
-               pass
-
-       def on(self):
-               pass
-
-       def off(self):
-               pass
-
-
-class LedHandler(object):
-
-       def __init__(self):
-               self._actual = None
-               self._isReal = False
-
-       def on(self):
-               self._lazy_init()
-               self._actual.on()
-
-       def off(self):
-               self._lazy_init()
-               self._actual.off()
-
-       @property
-       def isReal(self):
-               self._lazy_init()
-               self._isReal
-
-       def _lazy_init(self):
-               if self._actual is not None:
-                       return
-               try:
-                       self._actual = _NokiaLedHandler()
-                       self._isReal = True
-               except dbus.DBusException:
-                       self._actual = _NoLedHandler()
-                       self._isReal = False
-
-
-if __name__ == "__main__":
-       leds = _NokiaLedHandler()
-       leds.off()
diff --git a/src/session.py b/src/session.py
deleted file mode 100644 (file)
index dbdc3e4..0000000
+++ /dev/null
@@ -1,830 +0,0 @@
-from __future__ import with_statement
-
-import os
-import time
-import datetime
-import contextlib
-import logging
-
-try:
-       import cPickle
-       pickle = cPickle
-except ImportError:
-       import pickle
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-
-from util import qore_utils
-from util import qui_utils
-from util import concurrent
-from util import misc as misc_utils
-
-import constants
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class _DraftContact(object):
-
-       def __init__(self, messageId, title, description, numbersWithDescriptions):
-               self.messageId = messageId
-               self.title = title
-               self.description = description
-               self.numbers = numbersWithDescriptions
-               self.selectedNumber = numbersWithDescriptions[0][0]
-
-
-class Draft(QtCore.QObject):
-
-       sendingMessage = qt_compat.Signal()
-       sentMessage = qt_compat.Signal()
-       calling = qt_compat.Signal()
-       called = qt_compat.Signal()
-       cancelling = qt_compat.Signal()
-       cancelled = qt_compat.Signal()
-       error = qt_compat.Signal(str)
-
-       recipientsChanged = qt_compat.Signal()
-
-       def __init__(self, asyncQueue, backend, errorLog):
-               QtCore.QObject.__init__(self)
-               self._errorLog = errorLog
-               self._contacts = {}
-               self._asyncQueue = asyncQueue
-               self._backend = backend
-               self._busyReason = None
-               self._message = ""
-
-       def send(self):
-               assert 0 < len(self._contacts), "No contacts selected"
-               assert 0 < len(self._message), "No message to send"
-               numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
-               le = self._asyncQueue.add_async(self._send)
-               le.start(numbers, self._message)
-
-       def call(self):
-               assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
-               assert len(self._message) == 0, "Cannot send message with call"
-               (contact, ) = self._contacts.itervalues()
-               number = misc_utils.make_ugly(contact.selectedNumber)
-               le = self._asyncQueue.add_async(self._call)
-               le.start(number)
-
-       def cancel(self):
-               le = self._asyncQueue.add_async(self._cancel)
-               le.start()
-
-       def _get_message(self):
-               return self._message
-
-       def _set_message(self, message):
-               self._message = message
-
-       message = property(_get_message, _set_message)
-
-       def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
-               if self._busyReason is not None:
-                       raise RuntimeError("Please wait for %r" % self._busyReason)
-               # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
-               contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
-               self._contacts[contactId] = contactDetails
-               self.recipientsChanged.emit()
-
-       def remove_contact(self, contactId):
-               if self._busyReason is not None:
-                       raise RuntimeError("Please wait for %r" % self._busyReason)
-               assert contactId in self._contacts, "Contact missing"
-               del self._contacts[contactId]
-               self.recipientsChanged.emit()
-
-       def get_contacts(self):
-               return self._contacts.iterkeys()
-
-       def get_num_contacts(self):
-               return len(self._contacts)
-
-       def get_message_id(self, cid):
-               return self._contacts[cid].messageId
-
-       def get_title(self, cid):
-               return self._contacts[cid].title
-
-       def get_description(self, cid):
-               return self._contacts[cid].description
-
-       def get_numbers(self, cid):
-               return self._contacts[cid].numbers
-
-       def get_selected_number(self, cid):
-               return self._contacts[cid].selectedNumber
-
-       def set_selected_number(self, cid, number):
-               # @note I'm lazy, this isn't firing any kind of signal since only one
-               # controller right now and that is the viewer
-               assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
-               self._contacts[cid].selectedNumber = number
-
-       def clear(self):
-               if self._busyReason is not None:
-                       raise RuntimeError("Please wait for %r" % self._busyReason)
-               self._clear()
-
-       def _clear(self):
-               oldContacts = self._contacts
-               self._contacts = {}
-               self._message = ""
-               if oldContacts:
-                       self.recipientsChanged.emit()
-
-       @contextlib.contextmanager
-       def _busy(self, message):
-               if self._busyReason is not None:
-                       raise RuntimeError("Already busy doing %r" % self._busyReason)
-               try:
-                       self._busyReason = message
-                       yield
-               finally:
-                       self._busyReason = None
-
-       def _send(self, numbers, text):
-               self.sendingMessage.emit()
-               try:
-                       with self._busy("Sending Text"):
-                               with qui_utils.notify_busy(self._errorLog, "Sending Text"):
-                                       yield (
-                                               self._backend[0].send_sms,
-                                               (numbers, text),
-                                               {},
-                                       )
-                               self.sentMessage.emit()
-                               self._clear()
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-
-       def _call(self, number):
-               self.calling.emit()
-               try:
-                       with self._busy("Calling"):
-                               with qui_utils.notify_busy(self._errorLog, "Calling"):
-                                       yield (
-                                               self._backend[0].call,
-                                               (number, ),
-                                               {},
-                                       )
-                               self.called.emit()
-                               self._clear()
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-
-       def _cancel(self):
-               self.cancelling.emit()
-               try:
-                       with qui_utils.notify_busy(self._errorLog, "Cancelling"):
-                               yield (
-                                       self._backend[0].cancel,
-                                       (),
-                                       {},
-                               )
-                       self.cancelled.emit()
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-
-
-class Session(QtCore.QObject):
-
-       # @todo Somehow add support for csv contacts
-       # @BUG When loading without caches, downloads messages twice
-
-       stateChange = qt_compat.Signal(str)
-       loggedOut = qt_compat.Signal()
-       loggedIn = qt_compat.Signal()
-       callbackNumberChanged = qt_compat.Signal(str)
-
-       accountUpdated = qt_compat.Signal()
-       messagesUpdated = qt_compat.Signal()
-       newMessages = qt_compat.Signal()
-       historyUpdated = qt_compat.Signal()
-       dndStateChange = qt_compat.Signal(bool)
-       voicemailAvailable = qt_compat.Signal(str, str)
-
-       error = qt_compat.Signal(str)
-
-       LOGGEDOUT_STATE = "logged out"
-       LOGGINGIN_STATE = "logging in"
-       LOGGEDIN_STATE = "logged in"
-
-       MESSAGE_TEXTS = "Text"
-       MESSAGE_VOICEMAILS = "Voicemail"
-       MESSAGE_ALL = "All"
-
-       HISTORY_RECEIVED = "Received"
-       HISTORY_MISSED = "Missed"
-       HISTORY_PLACED = "Placed"
-       HISTORY_ALL = "All"
-
-       _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
-
-       _LOGGEDOUT_TIME = -1
-       _LOGGINGIN_TIME = 0
-
-       def __init__(self, errorLog, cachePath):
-               QtCore.QObject.__init__(self)
-               self._errorLog = errorLog
-               self._pool = qore_utils.FutureThread()
-               self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
-               self._backend = []
-               self._loggedInTime = self._LOGGEDOUT_TIME
-               self._loginOps = []
-               self._cachePath = cachePath
-               self._voicemailCachePath = None
-               self._username = None
-               self._password = None
-               self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
-               self._delayedRelogin = QtCore.QTimer()
-               self._delayedRelogin.setInterval(0)
-               self._delayedRelogin.setSingleShot(True)
-               self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
-
-               self._contacts = {}
-               self._accountUpdateTime = datetime.datetime(1971, 1, 1)
-               self._messages = []
-               self._cleanMessages = []
-               self._messageUpdateTime = datetime.datetime(1971, 1, 1)
-               self._history = []
-               self._historyUpdateTime = datetime.datetime(1971, 1, 1)
-               self._dnd = False
-               self._callback = ""
-
-       @property
-       def state(self):
-               return {
-                       self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
-                       self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
-               }.get(self._loggedInTime, self.LOGGEDIN_STATE)
-
-       @property
-       def draft(self):
-               return self._draft
-
-       def login(self, username, password):
-               assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
-               assert username != "", "No username specified"
-               if self._cachePath is not None:
-                       cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
-               else:
-                       cookiePath = None
-
-               if self._username != username or not self._backend:
-                       from backends import gv_backend
-                       del self._backend[:]
-                       self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
-
-               self._pool.start()
-               le = self._asyncQueue.add_async(self._login)
-               le.start(username, password)
-
-       def logout(self):
-               assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
-               _moduleLogger.info("Logging out")
-               self._pool.stop()
-               self._loggedInTime = self._LOGGEDOUT_TIME
-               self._backend[0].persist()
-               self._save_to_cache()
-               self._clear_voicemail_cache()
-               self.stateChange.emit(self.LOGGEDOUT_STATE)
-               self.loggedOut.emit()
-
-       def clear(self):
-               assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
-               self._backend[0].logout()
-               del self._backend[0]
-               self._clear_cache()
-               self._draft.clear()
-
-       def logout_and_clear(self):
-               assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
-               _moduleLogger.info("Logging out and clearing the account")
-               self._pool.stop()
-               self._loggedInTime = self._LOGGEDOUT_TIME
-               self.clear()
-               self.stateChange.emit(self.LOGGEDOUT_STATE)
-               self.loggedOut.emit()
-
-       def update_account(self, force = True):
-               if not force and self._contacts:
-                       return
-               le = self._asyncQueue.add_async(self._update_account), (), {}
-               self._perform_op_while_loggedin(le)
-
-       def refresh_connection(self):
-               le = self._asyncQueue.add_async(self._refresh_authentication)
-               le.start()
-
-       def get_contacts(self):
-               return self._contacts
-
-       def get_when_contacts_updated(self):
-               return self._accountUpdateTime
-
-       def update_messages(self, messageType, force = True):
-               if not force and self._messages:
-                       return
-               le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
-               self._perform_op_while_loggedin(le)
-
-       def get_messages(self):
-               return self._messages
-
-       def get_when_messages_updated(self):
-               return self._messageUpdateTime
-
-       def update_history(self, historyType, force = True):
-               if not force and self._history:
-                       return
-               le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
-               self._perform_op_while_loggedin(le)
-
-       def get_history(self):
-               return self._history
-
-       def get_when_history_updated(self):
-               return self._historyUpdateTime
-
-       def update_dnd(self):
-               le = self._asyncQueue.add_async(self._update_dnd), (), {}
-               self._perform_op_while_loggedin(le)
-
-       def set_dnd(self, dnd):
-               le = self._asyncQueue.add_async(self._set_dnd)
-               le.start(dnd)
-
-       def is_available(self, messageId):
-               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
-               return os.path.exists(actualPath)
-
-       def voicemail_path(self, messageId):
-               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
-               if not os.path.exists(actualPath):
-                       raise RuntimeError("Voicemail not available")
-               return actualPath
-
-       def download_voicemail(self, messageId):
-               le = self._asyncQueue.add_async(self._download_voicemail)
-               le.start(messageId)
-
-       def _set_dnd(self, dnd):
-               oldDnd = self._dnd
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
-                               yield (
-                                       self._backend[0].set_dnd,
-                                       (dnd, ),
-                                       {},
-                               )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._dnd = dnd
-               if oldDnd != self._dnd:
-                       self.dndStateChange.emit(self._dnd)
-
-       def get_dnd(self):
-               return self._dnd
-
-       def get_account_number(self):
-               if self.state != self.LOGGEDIN_STATE:
-                       return ""
-               return self._backend[0].get_account_number()
-
-       def get_callback_numbers(self):
-               if self.state != self.LOGGEDIN_STATE:
-                       return {}
-               return self._backend[0].get_callback_numbers()
-
-       def get_callback_number(self):
-               return self._callback
-
-       def set_callback_number(self, callback):
-               le = self._asyncQueue.add_async(self._set_callback_number)
-               le.start(callback)
-
-       def _set_callback_number(self, callback):
-               oldCallback = self._callback
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
-                       yield (
-                               self._backend[0].set_callback_number,
-                               (callback, ),
-                               {},
-                       )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._callback = callback
-               if oldCallback != self._callback:
-                       self.callbackNumberChanged.emit(self._callback)
-
-       def _login(self, username, password):
-               with qui_utils.notify_busy(self._errorLog, "Logging In"):
-                       self._loggedInTime = self._LOGGINGIN_TIME
-                       self.stateChange.emit(self.LOGGINGIN_STATE)
-                       finalState = self.LOGGEDOUT_STATE
-                       accountData = None
-                       try:
-                               if accountData is None and self._backend[0].is_quick_login_possible():
-                                       accountData = yield (
-                                               self._backend[0].refresh_account_info,
-                                               (),
-                                               {},
-                                       )
-                                       if accountData is not None:
-                                               _moduleLogger.info("Logged in through cookies")
-                                       else:
-                                               # Force a clearing of the cookies
-                                               yield (
-                                                       self._backend[0].logout,
-                                                       (),
-                                                       {},
-                                               )
-
-                               if accountData is None:
-                                       accountData = yield (
-                                               self._backend[0].login,
-                                               (username, password),
-                                               {},
-                                       )
-                                       if accountData is not None:
-                                               _moduleLogger.info("Logged in through credentials")
-
-                               if accountData is not None:
-                                       self._loggedInTime = int(time.time())
-                                       oldUsername = self._username
-                                       self._username = username
-                                       self._password = password
-                                       finalState = self.LOGGEDIN_STATE
-                                       if oldUsername != self._username:
-                                               needOps = not self._load()
-                                       else:
-                                               needOps = True
-
-                                       self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
-                                       try:
-                                               os.makedirs(self._voicemailCachePath)
-                                       except OSError, e:
-                                               if e.errno != 17:
-                                                       raise
-
-                                       self.loggedIn.emit()
-                                       self.stateChange.emit(finalState)
-                                       finalState = None # Mark it as already set
-                                       self._process_account_data(accountData)
-
-                                       if needOps:
-                                               loginOps = self._loginOps[:]
-                                       else:
-                                               loginOps = []
-                                       del self._loginOps[:]
-                                       for asyncOp, args, kwds in loginOps:
-                                               asyncOp.start(*args, **kwds)
-                               else:
-                                       self._loggedInTime = self._LOGGEDOUT_TIME
-                                       self.error.emit("Error logging in")
-                       except Exception, e:
-                               _moduleLogger.exception("Booh")
-                               self._loggedInTime = self._LOGGEDOUT_TIME
-                               _moduleLogger.exception("Reporting error to user")
-                               self.error.emit(str(e))
-                       finally:
-                               if finalState is not None:
-                                       self.stateChange.emit(finalState)
-                       if accountData is not None and self._callback:
-                               self.set_callback_number(self._callback)
-
-       def _update_account(self):
-               try:
-                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
-                               accountData = yield (
-                                       self._backend[0].refresh_account_info,
-                                       (),
-                                       {},
-                               )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._loggedInTime = int(time.time())
-               self._process_account_data(accountData)
-
-       def _refresh_authentication(self):
-               try:
-                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
-                               accountData = yield (
-                                       self._backend[0].refresh_account_info,
-                                       (),
-                                       {},
-                               )
-                               accountData = None
-               except Exception, e:
-                       _moduleLogger.exception("Passing to user")
-                       self.error.emit(str(e))
-                       # refresh_account_info does not normally throw, so it is fine if we
-                       # just quit early because something seriously wrong is going on
-                       return
-
-               if accountData is not None:
-                       self._loggedInTime = int(time.time())
-                       self._process_account_data(accountData)
-               else:
-                       self._delayedRelogin.start()
-
-       def _load(self):
-               updateMessages = len(self._messages) != 0
-               updateHistory = len(self._history) != 0
-               oldDnd = self._dnd
-               oldCallback = self._callback
-
-               self._messages = []
-               self._cleanMessages = []
-               self._history = []
-               self._dnd = False
-               self._callback = ""
-
-               loadedFromCache = self._load_from_cache()
-               if loadedFromCache:
-                       updateMessages = True
-                       updateHistory = True
-
-               if updateMessages:
-                       self.messagesUpdated.emit()
-               if updateHistory:
-                       self.historyUpdated.emit()
-               if oldDnd != self._dnd:
-                       self.dndStateChange.emit(self._dnd)
-               if oldCallback != self._callback:
-                       self.callbackNumberChanged.emit(self._callback)
-
-               return loadedFromCache
-
-       def _load_from_cache(self):
-               if self._cachePath is None:
-                       return False
-               cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
-
-               try:
-                       with open(cachePath, "rb") as f:
-                               dumpedData = pickle.load(f)
-               except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
-                       _moduleLogger.exception("Pickle fun loading")
-                       return False
-               except:
-                       _moduleLogger.exception("Weirdness loading")
-                       return False
-
-               try:
-                       version, build = dumpedData[0:2]
-               except ValueError:
-                       _moduleLogger.exception("Upgrade/downgrade fun")
-                       return False
-               except:
-                       _moduleLogger.exception("Weirdlings")
-                       return False
-
-               if misc_utils.compare_versions(
-                       self._OLDEST_COMPATIBLE_FORMAT_VERSION,
-                       misc_utils.parse_version(version),
-               ) <= 0:
-                       try:
-                               (
-                                       version, build,
-                                       messages, messageUpdateTime,
-                                       history, historyUpdateTime,
-                                       dnd, callback
-                               ) = dumpedData
-                       except ValueError:
-                               _moduleLogger.exception("Upgrade/downgrade fun")
-                               return False
-                       except:
-                               _moduleLogger.exception("Weirdlings")
-                               return False
-
-                       _moduleLogger.info("Loaded cache")
-                       self._messages = messages
-                       self._alert_on_messages(self._messages)
-                       self._messageUpdateTime = messageUpdateTime
-                       self._history = history
-                       self._historyUpdateTime = historyUpdateTime
-                       self._dnd = dnd
-                       self._callback = callback
-                       return True
-               else:
-                       _moduleLogger.debug(
-                               "Skipping cache due to version mismatch (%s-%s)" % (
-                                       version, build
-                               )
-                       )
-                       return False
-
-       def _save_to_cache(self):
-               _moduleLogger.info("Saving cache")
-               if self._cachePath is None:
-                       return
-               cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
-
-               try:
-                       dataToDump = (
-                               constants.__version__, constants.__build__,
-                               self._messages, self._messageUpdateTime,
-                               self._history, self._historyUpdateTime,
-                               self._dnd, self._callback
-                       )
-                       with open(cachePath, "wb") as f:
-                               pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
-                       _moduleLogger.info("Cache saved")
-               except (pickle.PickleError, IOError):
-                       _moduleLogger.exception("While saving")
-
-       def _clear_cache(self):
-               updateMessages = len(self._messages) != 0
-               updateHistory = len(self._history) != 0
-               oldDnd = self._dnd
-               oldCallback = self._callback
-
-               self._messages = []
-               self._messageUpdateTime = datetime.datetime(1971, 1, 1)
-               self._history = []
-               self._historyUpdateTime = datetime.datetime(1971, 1, 1)
-               self._dnd = False
-               self._callback = ""
-
-               if updateMessages:
-                       self.messagesUpdated.emit()
-               if updateHistory:
-                       self.historyUpdated.emit()
-               if oldDnd != self._dnd:
-                       self.dndStateChange.emit(self._dnd)
-               if oldCallback != self._callback:
-                       self.callbackNumberChanged.emit(self._callback)
-
-               self._save_to_cache()
-               self._clear_voicemail_cache()
-
-       def _clear_voicemail_cache(self):
-               import shutil
-               shutil.rmtree(self._voicemailCachePath, True)
-
-       def _update_messages(self, messageType):
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
-                               self._messages = yield (
-                                       self._backend[0].get_messages,
-                                       (messageType, ),
-                                       {},
-                               )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._messageUpdateTime = datetime.datetime.now()
-               self.messagesUpdated.emit()
-               self._alert_on_messages(self._messages)
-
-       def _update_history(self, historyType):
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType):
-                               self._history = yield (
-                                       self._backend[0].get_call_history,
-                                       (historyType, ),
-                                       {},
-                               )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._historyUpdateTime = datetime.datetime.now()
-               self.historyUpdated.emit()
-
-       def _update_dnd(self):
-               with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
-                       oldDnd = self._dnd
-                       try:
-                               assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
-                               self._dnd = yield (
-                                       self._backend[0].is_dnd,
-                                       (),
-                                       {},
-                               )
-                       except Exception, e:
-                               _moduleLogger.exception("Reporting error to user")
-                               self.error.emit(str(e))
-                               return
-                       if oldDnd != self._dnd:
-                               self.dndStateChange(self._dnd)
-
-       def _download_voicemail(self, messageId):
-               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
-               targetPath = "%s.%s.part" % (actualPath, time.time())
-               if os.path.exists(actualPath):
-                       self.voicemailAvailable.emit(messageId, actualPath)
-                       return
-               with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
-                       try:
-                               yield (
-                                       self._backend[0].download,
-                                       (messageId, targetPath),
-                                       {},
-                               )
-                       except Exception, e:
-                               _moduleLogger.exception("Passing to user")
-                               self.error.emit(str(e))
-                               return
-
-               if os.path.exists(actualPath):
-                       try:
-                               os.remove(targetPath)
-                       except:
-                               _moduleLogger.exception("Ignoring file problems with cache")
-                       self.voicemailAvailable.emit(messageId, actualPath)
-                       return
-               else:
-                       os.rename(targetPath, actualPath)
-                       self.voicemailAvailable.emit(messageId, actualPath)
-
-       def _perform_op_while_loggedin(self, op):
-               if self.state == self.LOGGEDIN_STATE:
-                       op, args, kwds = op
-                       op.start(*args, **kwds)
-               else:
-                       self._push_login_op(op)
-
-       def _push_login_op(self, asyncOp):
-               assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
-               if asyncOp in self._loginOps:
-                       _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
-                       return
-               self._loginOps.append(asyncOp)
-
-       def _process_account_data(self, accountData):
-               self._contacts = dict(
-                       (contactId, contactDetails)
-                       for contactId, contactDetails in accountData["contacts"].iteritems()
-                       # A zero contact id is the catch all for unknown contacts
-                       if contactId != "0"
-               )
-
-               self._accountUpdateTime = datetime.datetime.now()
-               self.accountUpdated.emit()
-
-       def _alert_on_messages(self, messages):
-               cleanNewMessages = list(self._clean_messages(messages))
-               cleanNewMessages.sort(key=lambda m: m["contactId"])
-               if self._cleanMessages:
-                       if self._cleanMessages != cleanNewMessages:
-                               self.newMessages.emit()
-               self._cleanMessages = cleanNewMessages
-
-       def _clean_messages(self, messages):
-               for message in messages:
-                       cleaned = dict(
-                               kv
-                               for kv in message.iteritems()
-                               if kv[0] not in
-                               [
-                                       "relTime",
-                                       "time",
-                                       "isArchived",
-                                       "isRead",
-                                       "isSpam",
-                                       "isTrash",
-                               ]
-                       )
-
-                       # Don't let outbound messages cause alerts, especially if the package has only outbound
-                       cleaned["messageParts"] = [
-                               tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
-                       ]
-                       if not cleaned["messageParts"]:
-                               continue
-
-                       yield cleaned
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_delayed_relogin(self):
-               try:
-                       username = self._username
-                       password = self._password
-                       self.logout()
-                       self.login(username, password)
-               except Exception, e:
-                       _moduleLogger.exception("Passing to user")
-                       self.error.emit(str(e))
-                       return
diff --git a/src/stream_gst.py b/src/stream_gst.py
deleted file mode 100644 (file)
index ce97fb6..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-import logging
-
-import gobject
-import gst
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(gobject.GObject):
-
-       # @bug Advertising state changes a bit early, should watch for GStreamer state change
-
-       STATE_PLAY = "play"
-       STATE_PAUSE = "pause"
-       STATE_STOP = "stop"
-
-       __gsignals__ = {
-               'state-change' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_STRING, ),
-               ),
-               'eof' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_STRING, ),
-               ),
-               'error' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
-               ),
-       }
-
-       def __init__(self):
-               gobject.GObject.__init__(self)
-               #Fields
-               self._uri = ""
-               self._elapsed = 0
-               self._duration = 0
-
-               #Set up GStreamer
-               self._player = gst.element_factory_make("playbin2", "player")
-               bus = self._player.get_bus()
-               bus.add_signal_watch()
-               bus.connect("message", self._on_message)
-
-               #Constants
-               self._timeFormat = gst.Format(gst.FORMAT_TIME)
-               self._seekFlag = gst.SEEK_FLAG_FLUSH
-
-       @property
-       def playing(self):
-               return self.state == self.STATE_PLAY
-
-       @property
-       def has_file(self):
-               return 0 < len(self._uri)
-
-       @property
-       def state(self):
-               state = self._player.get_state()[1]
-               return self._translate_state(state)
-
-       def set_file(self, uri):
-               if self._uri != uri:
-                       self._invalidate_cache()
-               if self.state != self.STATE_STOP:
-                       self.stop()
-
-               self._uri = uri
-               self._player.set_property("uri", uri)
-
-       def play(self):
-               if self.state == self.STATE_PLAY:
-                       _moduleLogger.info("Already play")
-                       return
-               _moduleLogger.info("Play")
-               self._player.set_state(gst.STATE_PLAYING)
-               self.emit("state-change", self.STATE_PLAY)
-
-       def pause(self):
-               if self.state == self.STATE_PAUSE:
-                       _moduleLogger.info("Already pause")
-                       return
-               _moduleLogger.info("Pause")
-               self._player.set_state(gst.STATE_PAUSED)
-               self.emit("state-change", self.STATE_PAUSE)
-
-       def stop(self):
-               if self.state == self.STATE_STOP:
-                       _moduleLogger.info("Already stop")
-                       return
-               self._player.set_state(gst.STATE_NULL)
-               _moduleLogger.info("Stopped")
-               self.emit("state-change", self.STATE_STOP)
-
-       @property
-       def elapsed(self):
-               try:
-                       self._elapsed = self._player.query_position(self._timeFormat, None)[0]
-               except:
-                       pass
-               return self._elapsed
-
-       @property
-       def duration(self):
-               try:
-                       self._duration = self._player.query_duration(self._timeFormat, None)[0]
-               except:
-                       _moduleLogger.exception("Query failed")
-               return self._duration
-
-       def seek_time(self, ns):
-               self._elapsed = ns
-               self._player.seek_simple(self._timeFormat, self._seekFlag, ns)
-
-       def _invalidate_cache(self):
-               self._elapsed = 0
-               self._duration = 0
-
-       def _translate_state(self, gstState):
-               return {
-                       gst.STATE_NULL: self.STATE_STOP,
-                       gst.STATE_PAUSED: self.STATE_PAUSE,
-                       gst.STATE_PLAYING: self.STATE_PLAY,
-               }.get(gstState, self.STATE_STOP)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_message(self, bus, message):
-               t = message.type
-               if t == gst.MESSAGE_EOS:
-                       self._player.set_state(gst.STATE_NULL)
-                       self.emit("eof", self._uri)
-               elif t == gst.MESSAGE_ERROR:
-                       self._player.set_state(gst.STATE_NULL)
-                       err, debug = message.parse_error()
-                       _moduleLogger.error("Error: %s, (%s)" % (err, debug))
-                       self.emit("error", err, debug)
-
-
-gobject.type_register(Stream)
diff --git a/src/stream_handler.py b/src/stream_handler.py
deleted file mode 100644 (file)
index 3c0c9e3..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import util.qt_compat as qt_compat
-QtCore = qt_compat.QtCore
-
-import util.misc as misc_utils
-try:
-       import stream_gst
-       stream = stream_gst
-except ImportError:
-       try:
-               import stream_osso
-               stream = stream_osso
-       except ImportError:
-               import stream_null
-               stream = stream_null
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class StreamToken(QtCore.QObject):
-
-       stateChange = qt_compat.Signal(str)
-       invalidated = qt_compat.Signal()
-       error = qt_compat.Signal(str)
-
-       STATE_PLAY = stream.Stream.STATE_PLAY
-       STATE_PAUSE = stream.Stream.STATE_PAUSE
-       STATE_STOP = stream.Stream.STATE_STOP
-
-       def __init__(self, stream):
-               QtCore.QObject.__init__(self)
-               self._stream = stream
-               self._stream.connect("state-change", self._on_stream_state)
-               self._stream.connect("eof", self._on_stream_eof)
-               self._stream.connect("error", self._on_stream_error)
-
-       @property
-       def state(self):
-               if self.isValid:
-                       return self._stream.state
-               else:
-                       return self.STATE_STOP
-
-       @property
-       def isValid(self):
-               return self._stream is not None
-
-       def play(self):
-               self._stream.play()
-
-       def pause(self):
-               self._stream.pause()
-
-       def stop(self):
-               self._stream.stop()
-
-       def invalidate(self):
-               if self._stream is None:
-                       return
-               _moduleLogger.info("Playback token invalidated")
-               self._stream = None
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_stream_state(self, s, state):
-               if not self.isValid:
-                       return
-               if state == self.STATE_STOP:
-                       self.invalidate()
-               self.stateChange.emit(state)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_stream_eof(self, s, uri):
-               if not self.isValid:
-                       return
-               self.invalidate()
-               self.stateChange.emit(self.STATE_STOP)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_stream_error(self, s, error, debug):
-               if not self.isValid:
-                       return
-               _moduleLogger.info("Error %s %s" % (error, debug))
-               self.error.emit(str(error))
-
-
-class StreamHandler(QtCore.QObject):
-
-       def __init__(self):
-               QtCore.QObject.__init__(self)
-               self._stream = stream.Stream()
-               self._token = StreamToken(self._stream)
-
-       def set_file(self, path):
-               self._token.invalidate()
-               self._token = StreamToken(self._stream)
-               self._stream.set_file(path)
-               return self._token
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_stream_state(self, s, state):
-               _moduleLogger.info("State change %r" % state)
-
-
-if __name__ == "__main__":
-       pass
-
diff --git a/src/stream_null.py b/src/stream_null.py
deleted file mode 100644 (file)
index 44fbbed..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(object):
-
-       STATE_PLAY = "play"
-       STATE_PAUSE = "pause"
-       STATE_STOP = "stop"
-
-       def __init__(self):
-               pass
-
-       def connect(self, signalName, slot):
-               pass
-
-       @property
-       def playing(self):
-               return False
-
-       @property
-       def has_file(self):
-               return False
-
-       @property
-       def state(self):
-               return self.STATE_STOP
-
-       def set_file(self, uri):
-               pass
-
-       def play(self):
-               pass
-
-       def pause(self):
-               pass
-
-       def stop(self):
-               pass
-
-       @property
-       def elapsed(self):
-               return 0
-
-       @property
-       def duration(self):
-               return 0
-
-       def seek_time(self, ns):
-               pass
-
-
-if __name__ == "__main__":
-       pass
-
diff --git a/src/stream_osso.py b/src/stream_osso.py
deleted file mode 100644 (file)
index abc453f..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-import logging
-
-import gobject
-import dbus
-
-import util.misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Stream(gobject.GObject):
-
-       STATE_PLAY = "play"
-       STATE_PAUSE = "pause"
-       STATE_STOP = "stop"
-
-       __gsignals__ = {
-               'state-change' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_STRING, ),
-               ),
-               'eof' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_STRING, ),
-               ),
-               'error' : (
-                       gobject.SIGNAL_RUN_LAST,
-                       gobject.TYPE_NONE,
-                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
-               ),
-       }
-
-       _SERVICE_NAME = "com.nokia.osso_media_server"
-       _OBJECT_PATH = "/com/nokia/osso_media_server"
-       _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
-
-       def __init__(self):
-               gobject.GObject.__init__(self)
-               #Fields
-               self._state = self.STATE_STOP
-               self._nextState = self.STATE_STOP
-               self._uri = ""
-               self._elapsed = 0
-               self._duration = 0
-
-               session_bus = dbus.SessionBus()
-
-               # Get the osso-media-player proxy object
-               oms_object = session_bus.get_object(
-                       self._SERVICE_NAME,
-                       self._OBJECT_PATH,
-                       introspect=False,
-                       follow_name_owner_changes=True,
-               )
-               # Use the audio interface
-               oms_audio_interface = dbus.Interface(
-                       oms_object,
-                       self._AUDIO_INTERFACE_NAME,
-               )
-               self._audioProxy = oms_audio_interface
-
-               self._audioProxy.connect_to_signal("state_changed", self._on_state_changed)
-               self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream)
-
-               error_signals = [
-                       "no_media_selected",
-                       "file_not_found",
-                       "type_not_found",
-                       "unsupported_type",
-                       "gstreamer",
-                       "dsp",
-                       "device_unavailable",
-                       "corrupted_file",
-                       "out_of_memory",
-                       "audio_codec_not_supported",
-               ]
-               for error in error_signals:
-                       self._audioProxy.connect_to_signal(error, self._on_error)
-
-       @property
-       def playing(self):
-               return self.state == self.STATE_PLAY
-
-       @property
-       def has_file(self):
-               return 0 < len(self._uri)
-
-       @property
-       def state(self):
-               return self._state
-
-       def set_file(self, uri):
-               if self._uri != uri:
-                       self._invalidate_cache()
-               if self.state != self.STATE_STOP:
-                       self.stop()
-
-               self._uri = uri
-               self._audioProxy.set_media_location(self._uri)
-
-       def play(self):
-               if self._nextState == self.STATE_PLAY:
-                       _moduleLogger.info("Already play")
-                       return
-               _moduleLogger.info("Play")
-               self._audioProxy.play()
-               self._nextState = self.STATE_PLAY
-               #self.emit("state-change", self.STATE_PLAY)
-
-       def pause(self):
-               if self._nextState == self.STATE_PAUSE:
-                       _moduleLogger.info("Already pause")
-                       return
-               _moduleLogger.info("Pause")
-               self._audioProxy.pause()
-               self._nextState = self.STATE_PAUSE
-               #self.emit("state-change", self.STATE_PLAY)
-
-       def stop(self):
-               if self._nextState == self.STATE_STOP:
-                       _moduleLogger.info("Already stop")
-                       return
-               self._audioProxy.stop()
-               _moduleLogger.info("Stopped")
-               self._nextState = self.STATE_STOP
-               #self.emit("state-change", self.STATE_STOP)
-
-       @property
-       def elapsed(self):
-               pos_info = self._audioProxy.get_position()
-               if isinstance(pos_info, tuple):
-                       self._elapsed, self._duration = pos_info
-               return self._elapsed
-
-       @property
-       def duration(self):
-               pos_info = self._audioProxy.get_position()
-               if isinstance(pos_info, tuple):
-                       self._elapsed, self._duration = pos_info
-               return self._duration
-
-       def seek_time(self, ns):
-               _moduleLogger.debug("Seeking to: %s", ns)
-               self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) )
-
-       def _invalidate_cache(self):
-               self._elapsed = 0
-               self._duration = 0
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_error(self, *args):
-               err, debug = "", repr(args)
-               _moduleLogger.error("Error: %s, (%s)" % (err, debug))
-               self.emit("error", err, debug)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_end_of_stream(self, *args):
-               self._state = self.STATE_STOP
-               self._nextState = self.STATE_STOP
-               self.emit("eof", self._uri)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_state_changed(self, state):
-               _moduleLogger.info("State: %s", state)
-               state = {
-                       "playing": self.STATE_PLAY,
-                       "paused": self.STATE_PAUSE,
-                       "stopped": self.STATE_STOP,
-               }[state]
-               if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP:
-                       # They seem to want to advertise stop right as the stream is starting, breaking the owner of this
-                       return
-               self._state = state
-               self._nextState = state
-               self.emit("state-change", state)
-
-
-gobject.type_register(Stream)
diff --git a/src/util/__init__.py b/src/util/__init__.py
deleted file mode 100644 (file)
index 4265cc3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-#!/usr/bin/env python
diff --git a/src/util/algorithms.py b/src/util/algorithms.py
deleted file mode 100644 (file)
index e94fb61..0000000
+++ /dev/null
@@ -1,664 +0,0 @@
-#!/usr/bin/env python
-
-"""
-@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448
-"""
-
-import itertools
-import functools
-import datetime
-import types
-import array
-import random
-
-
-def ordered_itr(collection):
-       """
-       >>> [v for v in ordered_itr({"a": 1, "b": 2})]
-       [('a', 1), ('b', 2)]
-       >>> [v for v in ordered_itr([3, 1, 10, -20])]
-       [-20, 1, 3, 10]
-       """
-       if isinstance(collection, types.DictType):
-               keys = list(collection.iterkeys())
-               keys.sort()
-               for key in keys:
-                       yield key, collection[key]
-       else:
-               values = list(collection)
-               values.sort()
-               for value in values:
-                       yield value
-
-
-def itercat(*iterators):
-       """
-       Concatenate several iterators into one.
-
-       >>> [v for v in itercat([1, 2, 3], [4, 1, 3])]
-       [1, 2, 3, 4, 1, 3]
-       """
-       for i in iterators:
-               for x in i:
-                       yield x
-
-
-def product(*args, **kwds):
-       # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
-       # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
-       pools = map(tuple, args) * kwds.get('repeat', 1)
-       result = [[]]
-       for pool in pools:
-               result = [x+[y] for x in result for y in pool]
-       for prod in result:
-               yield tuple(prod)
-
-
-def iterwhile(func, iterator):
-       """
-       Iterate for as long as func(value) returns true.
-       >>> through = lambda b: b
-       >>> [v for v in iterwhile(through, [True, True, False])]
-       [True, True]
-       """
-       iterator = iter(iterator)
-       while 1:
-               next = iterator.next()
-               if not func(next):
-                       raise StopIteration
-               yield next
-
-
-def iterfirst(iterator, count=1):
-       """
-       Iterate through 'count' first values.
-
-       >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)]
-       [1, 2, 3]
-       """
-       iterator = iter(iterator)
-       for i in xrange(count):
-               yield iterator.next()
-
-
-def iterstep(iterator, n):
-       """
-       Iterate every nth value.
-
-       >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)]
-       [1, 2, 3, 4, 5]
-       >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)]
-       [1, 3, 5]
-       >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)]
-       [1, 4]
-       """
-       iterator = iter(iterator)
-       while True:
-               yield iterator.next()
-               # skip n-1 values
-               for dummy in xrange(n-1):
-                       iterator.next()
-
-
-def itergroup(iterator, count, padValue = None):
-       """
-       Iterate in groups of 'count' values. If there
-       aren't enough values, the last result is padded with
-       None.
-
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print list(val)
-       [1, 2, 3]
-       [4, 5, 6]
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       (7, None, None)
-       >>> for val in itergroup("123456", 3):
-       ...     print tuple(val)
-       ('1', '2', '3')
-       ('4', '5', '6')
-       >>> for val in itergroup("123456", 3):
-       ...     print repr("".join(val))
-       '123'
-       '456'
-       """
-       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
-       nIterators = (paddedIterator, ) * count
-       return itertools.izip(*nIterators)
-
-
-def xzip(*iterators):
-       """Iterative version of builtin 'zip'."""
-       iterators = itertools.imap(iter, iterators)
-       while 1:
-               yield tuple([x.next() for x in iterators])
-
-
-def xmap(func, *iterators):
-       """Iterative version of builtin 'map'."""
-       iterators = itertools.imap(iter, iterators)
-       values_left = [1]
-
-       def values():
-               # Emulate map behaviour, i.e. shorter
-               # sequences are padded with None when
-               # they run out of values.
-               values_left[0] = 0
-               for i in range(len(iterators)):
-                       iterator = iterators[i]
-                       if iterator is None:
-                               yield None
-                       else:
-                               try:
-                                       yield iterator.next()
-                                       values_left[0] = 1
-                               except StopIteration:
-                                       iterators[i] = None
-                                       yield None
-       while 1:
-               args = tuple(values())
-               if not values_left[0]:
-                       raise StopIteration
-               yield func(*args)
-
-
-def xfilter(func, iterator):
-       """Iterative version of builtin 'filter'."""
-       iterator = iter(iterator)
-       while 1:
-               next = iterator.next()
-               if func(next):
-                       yield next
-
-
-def xreduce(func, iterator, default=None):
-       """Iterative version of builtin 'reduce'."""
-       iterator = iter(iterator)
-       try:
-               prev = iterator.next()
-       except StopIteration:
-               return default
-       single = 1
-       for next in iterator:
-               single = 0
-               prev = func(prev, next)
-       if single:
-               return func(prev, default)
-       return prev
-
-
-def daterange(begin, end, delta = datetime.timedelta(1)):
-       """
-       Form a range of dates and iterate over them.
-
-       Arguments:
-       begin -- a date (or datetime) object; the beginning of the range.
-       end   -- a date (or datetime) object; the end of the range.
-       delta -- (optional) a datetime.timedelta object; how much to step each iteration.
-                       Default step is 1 day.
-
-       Usage:
-       """
-       if not isinstance(delta, datetime.timedelta):
-               delta = datetime.timedelta(delta)
-
-       ZERO = datetime.timedelta(0)
-
-       if begin < end:
-               if delta <= ZERO:
-                       raise StopIteration
-               test = end.__gt__
-       else:
-               if delta >= ZERO:
-                       raise StopIteration
-               test = end.__lt__
-
-       while test(begin):
-               yield begin
-               begin += delta
-
-
-class LazyList(object):
-       """
-       A Sequence whose values are computed lazily by an iterator.
-
-       Module for the creation and use of iterator-based lazy lists.
-       this module defines a class LazyList which can be used to represent sequences
-       of values generated lazily. One can also create recursively defined lazy lists
-       that generate their values based on ones previously generated.
-
-       Backport to python 2.5 by Michael Pust
-       """
-
-       __author__ = 'Dan Spitz'
-
-       def __init__(self, iterable):
-               self._exhausted = False
-               self._iterator = iter(iterable)
-               self._data = []
-
-       def __len__(self):
-               """Get the length of a LazyList's computed data."""
-               return len(self._data)
-
-       def __getitem__(self, i):
-               """Get an item from a LazyList.
-               i should be a positive integer or a slice object."""
-               if isinstance(i, int):
-                       #index has not yet been yielded by iterator (or iterator exhausted
-                       #before reaching that index)
-                       if i >= len(self):
-                               self.exhaust(i)
-                       elif i < 0:
-                               raise ValueError('cannot index LazyList with negative number')
-                       return self._data[i]
-
-               #LazyList slices are iterators over a portion of the list.
-               elif isinstance(i, slice):
-                       start, stop, step = i.start, i.stop, i.step
-                       if any(x is not None and x < 0 for x in (start, stop, step)):
-                               raise ValueError('cannot index or step through a LazyList with'
-                                                               'a negative number')
-                       #set start and step to their integer defaults if they are None.
-                       if start is None:
-                               start = 0
-                       if step is None:
-                               step = 1
-
-                       def LazyListIterator():
-                               count = start
-                               predicate = (
-                                       (lambda: True)
-                                       if stop is None
-                                       else (lambda: count < stop)
-                               )
-                               while predicate():
-                                       try:
-                                               yield self[count]
-                                       #slices can go out of actual index range without raising an
-                                       #error
-                                       except IndexError:
-                                               break
-                                       count += step
-                       return LazyListIterator()
-
-               raise TypeError('i must be an integer or slice')
-
-       def __iter__(self):
-               """return an iterator over each value in the sequence,
-               whether it has been computed yet or not."""
-               return self[:]
-
-       def computed(self):
-               """Return an iterator over the values in a LazyList that have
-               already been computed."""
-               return self[:len(self)]
-
-       def exhaust(self, index = None):
-               """Exhaust the iterator generating this LazyList's values.
-               if index is None, this will exhaust the iterator completely.
-               Otherwise, it will iterate over the iterator until either the list
-               has a value for index or the iterator is exhausted.
-               """
-               if self._exhausted:
-                       return
-               if index is None:
-                       ind_range = itertools.count(len(self))
-               else:
-                       ind_range = range(len(self), index + 1)
-
-               for ind in ind_range:
-                       try:
-                               self._data.append(self._iterator.next())
-                       except StopIteration: #iterator is fully exhausted
-                               self._exhausted = True
-                               break
-
-
-class RecursiveLazyList(LazyList):
-
-       def __init__(self, prod, *args, **kwds):
-               super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds))
-
-
-class RecursiveLazyListFactory:
-
-       def __init__(self, producer):
-               self._gen = producer
-
-       def __call__(self, *a, **kw):
-               return RecursiveLazyList(self._gen, *a, **kw)
-
-
-def lazylist(gen):
-       """
-       Decorator for creating a RecursiveLazyList subclass.
-       This should decorate a generator function taking the LazyList object as its
-       first argument which yields the contents of the list in order.
-
-       >>> #fibonnacci sequence in a lazy list.
-       >>> @lazylist
-       ... def fibgen(lst):
-       ...     yield 0
-       ...     yield 1
-       ...     for a, b in itertools.izip(lst, lst[1:]):
-       ...             yield a + b
-       ...
-       >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence
-       >>> fibs = fibgen()
-       >>>
-       >>> #prime numbers in a lazy list.
-       >>> @lazylist
-       ... def primegen(lst):
-       ...     yield 2
-       ...     for candidate in itertools.count(3): #start at next number after 2
-       ...             #if candidate is not divisible by any smaller prime numbers,
-       ...             #it is a prime.
-       ...             if all(candidate % p for p in lst.computed()):
-       ...                     yield candidate
-       ...
-       >>> #same for primes- treat it like an infinitely long list containing all prime numbers.
-       >>> primes = primegen()
-       >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2]
-       0 1 1 2 3 5
-       >>> print list(fibs[:10]), list(primes[:10])
-       [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
-       """
-       return RecursiveLazyListFactory(gen)
-
-
-def map_func(f):
-       """
-       >>> import misc
-       >>> misc.validate_decorator(map_func)
-       """
-
-       @functools.wraps(f)
-       def wrapper(*args):
-               result = itertools.imap(f, args)
-               return result
-       return wrapper
-
-
-def reduce_func(function):
-       """
-       >>> import misc
-       >>> misc.validate_decorator(reduce_func(lambda x: x))
-       """
-
-       def decorator(f):
-
-               @functools.wraps(f)
-               def wrapper(*args):
-                       result = reduce(function, f(args))
-                       return result
-               return wrapper
-       return decorator
-
-
-def any_(iterable):
-       """
-       @note Python Version <2.5
-
-       >>> any_([True, True])
-       True
-       >>> any_([True, False])
-       True
-       >>> any_([False, False])
-       False
-       """
-
-       for element in iterable:
-               if element:
-                       return True
-       return False
-
-
-def all_(iterable):
-       """
-       @note Python Version <2.5
-
-       >>> all_([True, True])
-       True
-       >>> all_([True, False])
-       False
-       >>> all_([False, False])
-       False
-       """
-
-       for element in iterable:
-               if not element:
-                       return False
-       return True
-
-
-def for_every(pred, seq):
-       """
-       for_every takes a one argument predicate function and a sequence.
-       @param pred The predicate function should return true or false.
-       @returns true if every element in seq returns true for predicate, else returns false.
-
-       >>> for_every (lambda c: c > 5,(6,7,8,9))
-       True
-
-       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
-       """
-
-       for i in seq:
-               if not pred(i):
-                       return False
-       return True
-
-
-def there_exists(pred, seq):
-       """
-       there_exists takes a one argument predicate     function and a sequence.
-       @param pred The predicate function should return true or false.
-       @returns true if any element in seq returns true for predicate, else returns false.
-
-       >>> there_exists (lambda c: c > 5,(6,7,8,9))
-       True
-
-       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
-       """
-
-       for i in seq:
-               if pred(i):
-                       return True
-       return False
-
-
-def func_repeat(quantity, func, *args, **kwd):
-       """
-       Meant to be in connection with "reduce"
-       """
-       for i in xrange(quantity):
-               yield func(*args, **kwd)
-
-
-def function_map(preds, item):
-       """
-       Meant to be in connection with "reduce"
-       """
-       results = (pred(item) for pred in preds)
-
-       return results
-
-
-def functional_if(combiner, preds, item):
-       """
-       Combines the result of a list of predicates applied to item according to combiner
-
-       @see any, every for example combiners
-       """
-       pass_bool = lambda b: b
-
-       bool_results = function_map(preds, item)
-       return combiner(pass_bool, bool_results)
-
-
-def pushback_itr(itr):
-       """
-       >>> list(pushback_itr(xrange(5)))
-       [0, 1, 2, 3, 4]
-       >>>
-       >>> first = True
-       >>> itr = pushback_itr(xrange(5))
-       >>> for i in itr:
-       ...     print i
-       ...     if first and i == 2:
-       ...             first = False
-       ...             print itr.send(i)
-       0
-       1
-       2
-       None
-       2
-       3
-       4
-       >>>
-       >>> first = True
-       >>> itr = pushback_itr(xrange(5))
-       >>> for i in itr:
-       ...     print i
-       ...     if first and i == 2:
-       ...             first = False
-       ...             print itr.send(i)
-       ...             print itr.send(i)
-       0
-       1
-       2
-       None
-       None
-       2
-       2
-       3
-       4
-       >>>
-       >>> itr = pushback_itr(xrange(5))
-       >>> print itr.next()
-       0
-       >>> print itr.next()
-       1
-       >>> print itr.send(10)
-       None
-       >>> print itr.next()
-       10
-       >>> print itr.next()
-       2
-       >>> print itr.send(20)
-       None
-       >>> print itr.send(30)
-       None
-       >>> print itr.send(40)
-       None
-       >>> print itr.next()
-       40
-       >>> print itr.next()
-       30
-       >>> print itr.send(50)
-       None
-       >>> print itr.next()
-       50
-       >>> print itr.next()
-       20
-       >>> print itr.next()
-       3
-       >>> print itr.next()
-       4
-       """
-       for item in itr:
-               maybePushedBack = yield item
-               queue = []
-               while queue or maybePushedBack is not None:
-                       if maybePushedBack is not None:
-                               queue.append(maybePushedBack)
-                               maybePushedBack = yield None
-                       else:
-                               item = queue.pop()
-                               maybePushedBack = yield item
-
-
-def itr_available(queue, initiallyBlock = False):
-       if initiallyBlock:
-               yield queue.get()
-       while not queue.empty():
-               yield queue.get_nowait()
-
-
-class BloomFilter(object):
-       """
-       http://en.wikipedia.org/wiki/Bloom_filter
-       Sources:
-       http://code.activestate.com/recipes/577684-bloom-filter/
-       http://code.activestate.com/recipes/577686-bloom-filter/
-
-       >>> from random import sample
-       >>> from string import ascii_letters
-       >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut
-       ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas
-       ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota
-       ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey
-       ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon
-       ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah
-       ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split()
-       >>> bf = BloomFilter(num_bits=1000, num_probes=14)
-       >>> for state in states:
-       ...     bf.add(state)
-       >>> numStatesFound = sum(state in bf for state in states)
-       >>> numStatesFound, len(states)
-       (50, 50)
-       >>> trials = 100
-       >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials))
-       >>> numGarbageFound, trials
-       (0, 100)
-       """
-
-       def __init__(self, num_bits, num_probes):
-               num_words = (num_bits + 31) // 32
-               self._arr = array.array('B', [0]) * num_words
-               self._num_probes = num_probes
-
-       def add(self, key):
-               for i, mask in self._get_probes(key):
-                       self._arr[i] |= mask
-
-       def union(self, bfilter):
-               if self._match_template(bfilter):
-                       for i, b in enumerate(bfilter._arr):
-                               self._arr[i] |= b
-               else:
-                       # Union b/w two unrelated bloom filter raises this
-                       raise ValueError("Mismatched bloom filters")
-
-       def intersection(self, bfilter):
-               if self._match_template(bfilter):
-                       for i, b in enumerate(bfilter._arr):
-                               self._arr[i] &= b
-               else:
-                       # Intersection b/w two unrelated bloom filter raises this
-                       raise ValueError("Mismatched bloom filters")
-
-       def __contains__(self, key):
-               return all(self._arr[i] & mask for i, mask in self._get_probes(key))
-
-       def _match_template(self, bfilter):
-               return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes
-
-       def _get_probes(self, key):
-               hasher = random.Random(key).randrange
-               for _ in range(self._num_probes):
-                       array_index = hasher(len(self._arr))
-                       bit_index = hasher(32)
-                       yield array_index, 1 << bit_index
-
-
-if __name__ == "__main__":
-       import doctest
-       print doctest.testmod()
diff --git a/src/util/concurrent.py b/src/util/concurrent.py
deleted file mode 100644 (file)
index f5f6e1d..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import os
-import errno
-import time
-import functools
-import contextlib
-import logging
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class AsyncTaskQueue(object):
-
-       def __init__(self, taskPool):
-               self._asyncs = []
-               self._taskPool = taskPool
-
-       def add_async(self, func):
-               self.flush()
-               a = AsyncGeneratorTask(self._taskPool, func)
-               self._asyncs.append(a)
-               return a
-
-       def flush(self):
-               self._asyncs = [a for a in self._asyncs if not a.isDone]
-
-
-class AsyncGeneratorTask(object):
-
-       def __init__(self, pool, func):
-               self._pool = pool
-               self._func = func
-               self._run = None
-               self._isDone = False
-
-       @property
-       def isDone(self):
-               return self._isDone
-
-       def start(self, *args, **kwds):
-               assert self._run is None, "Task already started"
-               self._run = self._func(*args, **kwds)
-               trampoline, args, kwds = self._run.send(None) # priming the function
-               self._pool.add_task(
-                       trampoline,
-                       args,
-                       kwds,
-                       self.on_success,
-                       self.on_error,
-               )
-
-       @misc.log_exception(_moduleLogger)
-       def on_success(self, result):
-               _moduleLogger.debug("Processing success for: %r", self._func)
-               try:
-                       trampoline, args, kwds = self._run.send(result)
-               except StopIteration, e:
-                       self._isDone = True
-               else:
-                       self._pool.add_task(
-                               trampoline,
-                               args,
-                               kwds,
-                               self.on_success,
-                               self.on_error,
-                       )
-
-       @misc.log_exception(_moduleLogger)
-       def on_error(self, error):
-               _moduleLogger.debug("Processing error for: %r", self._func)
-               try:
-                       trampoline, args, kwds = self._run.throw(error)
-               except StopIteration, e:
-                       self._isDone = True
-               else:
-                       self._pool.add_task(
-                               trampoline,
-                               args,
-                               kwds,
-                               self.on_success,
-                               self.on_error,
-                       )
-
-       def __repr__(self):
-               return "<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)
diff --git a/src/util/coroutines.py b/src/util/coroutines.py
deleted file mode 100755 (executable)
index b1e539e..0000000
+++ /dev/null
@@ -1,623 +0,0 @@
-#!/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
diff --git a/src/util/go_utils.py b/src/util/go_utils.py
deleted file mode 100644 (file)
index 61e731d..0000000
+++ /dev/null
@@ -1,274 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import time
-import functools
-import threading
-import Queue
-import logging
-
-import gobject
-
-import algorithms
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-def make_idler(func):
-       """
-       Decorator that makes a generator-function into a function that will continue execution on next call
-       """
-       a = []
-
-       @functools.wraps(func)
-       def decorated_func(*args, **kwds):
-               if not a:
-                       a.append(func(*args, **kwds))
-               try:
-                       a[0].next()
-                       return True
-               except StopIteration:
-                       del a[:]
-                       return False
-
-       return decorated_func
-
-
-def async(func):
-       """
-       Make a function mainloop friendly. the function will be called at the
-       next mainloop idle state.
-
-       >>> import misc
-       >>> misc.validate_decorator(async)
-       """
-
-       @functools.wraps(func)
-       def new_function(*args, **kwargs):
-
-               def async_function():
-                       func(*args, **kwargs)
-                       return False
-
-               gobject.idle_add(async_function)
-
-       return new_function
-
-
-class Async(object):
-
-       def __init__(self, func, once = True):
-               self.__func = func
-               self.__idleId = None
-               self.__once = once
-
-       def start(self):
-               assert self.__idleId is None
-               if self.__once:
-                       self.__idleId = gobject.idle_add(self._on_once)
-               else:
-                       self.__idleId = gobject.idle_add(self.__func)
-
-       def is_running(self):
-               return self.__idleId is not None
-
-       def cancel(self):
-               if self.__idleId is not None:
-                       gobject.source_remove(self.__idleId)
-                       self.__idleId = None
-
-       def __call__(self):
-               return self.start()
-
-       @misc.log_exception(_moduleLogger)
-       def _on_once(self):
-               self.cancel()
-               try:
-                       self.__func()
-               except Exception:
-                       pass
-               return False
-
-
-class Timeout(object):
-
-       def __init__(self, func, once = True):
-               self.__func = func
-               self.__timeoutId = None
-               self.__once = once
-
-       def start(self, **kwds):
-               assert self.__timeoutId is None
-
-               callback = self._on_once if self.__once else self.__func
-
-               assert len(kwds) == 1
-               timeoutInSeconds = kwds["seconds"]
-               assert 0 <= timeoutInSeconds
-
-               if timeoutInSeconds == 0:
-                       self.__timeoutId = gobject.idle_add(callback)
-               else:
-                       self.__timeoutId = timeout_add_seconds(timeoutInSeconds, callback)
-
-       def is_running(self):
-               return self.__timeoutId is not None
-
-       def cancel(self):
-               if self.__timeoutId is not None:
-                       gobject.source_remove(self.__timeoutId)
-                       self.__timeoutId = None
-
-       def __call__(self, **kwds):
-               return self.start(**kwds)
-
-       @misc.log_exception(_moduleLogger)
-       def _on_once(self):
-               self.cancel()
-               try:
-                       self.__func()
-               except Exception:
-                       pass
-               return False
-
-
-_QUEUE_EMPTY = object()
-
-
-class FutureThread(object):
-
-       def __init__(self):
-               self.__workQueue = Queue.Queue()
-               self.__thread = threading.Thread(
-                       name = type(self).__name__,
-                       target = self.__consume_queue,
-               )
-               self.__isRunning = True
-
-       def start(self):
-               self.__thread.start()
-
-       def stop(self):
-               self.__isRunning = False
-               for _ in algorithms.itr_available(self.__workQueue):
-                       pass # eat up queue to cut down dumb work
-               self.__workQueue.put(_QUEUE_EMPTY)
-
-       def clear_tasks(self):
-               for _ in algorithms.itr_available(self.__workQueue):
-                       pass # eat up queue to cut down dumb work
-
-       def add_task(self, func, args, kwds, on_success, on_error):
-               task = func, args, kwds, on_success, on_error
-               self.__workQueue.put(task)
-
-       @misc.log_exception(_moduleLogger)
-       def __trampoline_callback(self, on_success, on_error, isError, result):
-               if not self.__isRunning:
-                       if isError:
-                               _moduleLogger.error("Masking: %s" % (result, ))
-                       isError = True
-                       result = StopIteration("Cancelling all callbacks")
-               callback = on_success if not isError else on_error
-               try:
-                       callback(result)
-               except Exception:
-                       _moduleLogger.exception("Callback errored")
-               return False
-
-       @misc.log_exception(_moduleLogger)
-       def __consume_queue(self):
-               while True:
-                       task = self.__workQueue.get()
-                       if task is _QUEUE_EMPTY:
-                               break
-                       func, args, kwds, on_success, on_error = task
-
-                       try:
-                               result = func(*args, **kwds)
-                               isError = False
-                       except Exception, e:
-                               _moduleLogger.error("Error, passing it back to the main thread")
-                               result = e
-                               isError = True
-                       self.__workQueue.task_done()
-
-                       gobject.idle_add(self.__trampoline_callback, on_success, on_error, isError, result)
-               _moduleLogger.debug("Shutting down worker thread")
-
-
-class AutoSignal(object):
-
-       def __init__(self, toplevel):
-               self.__disconnectPool = []
-               toplevel.connect("destroy", self.__on_destroy)
-
-       def connect_auto(self, widget, *args):
-               id = widget.connect(*args)
-               self.__disconnectPool.append((widget, id))
-
-       @misc.log_exception(_moduleLogger)
-       def __on_destroy(self, widget):
-               _moduleLogger.info("Destroy: %r (%s to clean up)" % (self, len(self.__disconnectPool)))
-               for widget, id in self.__disconnectPool:
-                       widget.disconnect(id)
-               del self.__disconnectPool[:]
-
-
-def throttled(minDelay, queue):
-       """
-       Throttle the calls to a function by queueing all the calls that happen
-       before the minimum delay
-
-       >>> import misc
-       >>> import Queue
-       >>> misc.validate_decorator(throttled(0, Queue.Queue()))
-       """
-
-       def actual_decorator(func):
-
-               lastCallTime = [None]
-
-               def process_queue():
-                       if 0 < len(queue):
-                               func, args, kwargs = queue.pop(0)
-                               lastCallTime[0] = time.time() * 1000
-                               func(*args, **kwargs)
-                       return False
-
-               @functools.wraps(func)
-               def new_function(*args, **kwargs):
-                       now = time.time() * 1000
-                       if (
-                               lastCallTime[0] is None or
-                               (now - lastCallTime >= minDelay)
-                       ):
-                               lastCallTime[0] = now
-                               func(*args, **kwargs)
-                       else:
-                               queue.append((func, args, kwargs))
-                               lastCallDelta = now - lastCallTime[0]
-                               processQueueTimeout = int(minDelay * len(queue) - lastCallDelta)
-                               gobject.timeout_add(processQueueTimeout, process_queue)
-
-               return new_function
-
-       return actual_decorator
-
-
-def _old_timeout_add_seconds(timeout, callback):
-       return gobject.timeout_add(timeout * 1000, callback)
-
-
-def _timeout_add_seconds(timeout, callback):
-       return gobject.timeout_add_seconds(timeout, callback)
-
-
-try:
-       gobject.timeout_add_seconds
-       timeout_add_seconds = _timeout_add_seconds
-except AttributeError:
-       timeout_add_seconds = _old_timeout_add_seconds
diff --git a/src/util/io.py b/src/util/io.py
deleted file mode 100644 (file)
index 4198f4b..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-#!/usr/bin/env python
-
-
-from __future__ import with_statement
-
-import os
-import pickle
-import contextlib
-import itertools
-import codecs
-from xml.sax import saxutils
-import csv
-try:
-       import cStringIO as StringIO
-except ImportError:
-       import StringIO
-
-
-@contextlib.contextmanager
-def change_directory(directory):
-       previousDirectory = os.getcwd()
-       os.chdir(directory)
-       currentDirectory = os.getcwd()
-
-       try:
-               yield previousDirectory, currentDirectory
-       finally:
-               os.chdir(previousDirectory)
-
-
-@contextlib.contextmanager
-def pickled(filename):
-       """
-       Here is an example usage:
-       with pickled("foo.db") as p:
-               p("users", list).append(["srid", "passwd", 23])
-       """
-
-       if os.path.isfile(filename):
-               data = pickle.load(open(filename))
-       else:
-               data = {}
-
-       def getter(item, factory):
-               if item in data:
-                       return data[item]
-               else:
-                       data[item] = factory()
-                       return data[item]
-
-       yield getter
-
-       pickle.dump(data, open(filename, "w"))
-
-
-@contextlib.contextmanager
-def redirect(object_, attr, value):
-       """
-       >>> import sys
-       ... with redirect(sys, 'stdout', open('stdout', 'w')):
-       ...     print "hello"
-       ...
-       >>> print "we're back"
-       we're back
-       """
-       orig = getattr(object_, attr)
-       setattr(object_, attr, value)
-       try:
-               yield
-       finally:
-               setattr(object_, attr, orig)
-
-
-def pathsplit(path):
-       """
-       >>> pathsplit("/a/b/c")
-       ['', 'a', 'b', 'c']
-       >>> pathsplit("./plugins/builtins.ini")
-       ['.', 'plugins', 'builtins.ini']
-       """
-       pathParts = path.split(os.path.sep)
-       return pathParts
-
-
-def commonpath(l1, l2, common=None):
-       """
-       >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1'))
-       (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1'])
-       >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini"))
-       (['.', 'plugins'], [''], ['builtins.ini'])
-       >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins"))
-       (['.', 'plugins'], ['builtins'], [])
-       """
-       if common is None:
-               common = []
-
-       if l1 == l2:
-               return l1, [], []
-
-       for i, (leftDir, rightDir) in enumerate(zip(l1, l2)):
-               if leftDir != rightDir:
-                       return l1[0:i], l1[i:], l2[i:]
-       else:
-               if leftDir == rightDir:
-                       i += 1
-               return l1[0:i], l1[i:], l2[i:]
-
-
-def relpath(p1, p2):
-       """
-       >>> relpath('/', '/')
-       './'
-       >>> relpath('/a/b/c/d', '/')
-       '../../../../'
-       >>> relpath('/a/b/c/d', '/a/b/c1/d1')
-       '../../c1/d1'
-       >>> relpath('/a/b/c/d', '/a/b/c1/d1/')
-       '../../c1/d1'
-       >>> relpath("./plugins/builtins", "./plugins")
-       '../'
-       >>> relpath("./plugins/", "./plugins/builtins.ini")
-       'builtins.ini'
-       """
-       sourcePath = os.path.normpath(p1)
-       destPath = os.path.normpath(p2)
-
-       (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath))
-       if len(sourceOnly) or len(destOnly):
-               relParts = itertools.chain(
-                       (('..' + os.sep) * len(sourceOnly), ),
-                       destOnly,
-               )
-               return os.path.join(*relParts)
-       else:
-               return "."+os.sep
-
-
-class UTF8Recoder(object):
-       """
-       Iterator that reads an encoded stream and reencodes the input to UTF-8
-       """
-       def __init__(self, f, encoding):
-               self.reader = codecs.getreader(encoding)(f)
-
-       def __iter__(self):
-               return self
-
-       def next(self):
-               return self.reader.next().encode("utf-8")
-
-
-class UnicodeReader(object):
-       """
-       A CSV reader which will iterate over lines in the CSV file "f",
-       which is encoded in the given encoding.
-       """
-
-       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
-               f = UTF8Recoder(f, encoding)
-               self.reader = csv.reader(f, dialect=dialect, **kwds)
-
-       def next(self):
-               row = self.reader.next()
-               return [unicode(s, "utf-8") for s in row]
-
-       def __iter__(self):
-               return self
-
-class UnicodeWriter(object):
-       """
-       A CSV writer which will write rows to CSV file "f",
-       which is encoded in the given encoding.
-       """
-
-       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
-               # Redirect output to a queue
-               self.queue = StringIO.StringIO()
-               self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
-               self.stream = f
-               self.encoder = codecs.getincrementalencoder(encoding)()
-
-       def writerow(self, row):
-               self.writer.writerow([s.encode("utf-8") for s in row])
-               # Fetch UTF-8 output from the queue ...
-               data = self.queue.getvalue()
-               data = data.decode("utf-8")
-               # ... and reencode it into the target encoding
-               data = self.encoder.encode(data)
-               # write to the target stream
-               self.stream.write(data)
-               # empty queue
-               self.queue.truncate(0)
-
-       def writerows(self, rows):
-               for row in rows:
-                       self.writerow(row)
-
-
-def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
-       # csv.py doesn't do Unicode; encode temporarily as UTF-8:
-       csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
-                                                       dialect=dialect, **kwargs)
-       for row in csv_reader:
-               # decode UTF-8 back to Unicode, cell by cell:
-               yield [unicode(cell, 'utf-8') for cell in row]
-
-
-def utf_8_encoder(unicode_csv_data):
-       for line in unicode_csv_data:
-               yield line.encode('utf-8')
-
-
-_UNESCAPE_ENTITIES = {
- "&quot;": '"',
- "&nbsp;": " ",
- "&#39;": "'",
-}
-
-
-_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys()))
-del _ESCAPE_ENTITIES[" "]
-
-
-def unescape(text):
-       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
-       return plain
-
-
-def escape(text):
-       fancy = saxutils.escape(text, _ESCAPE_ENTITIES)
-       return fancy
diff --git a/src/util/linux.py b/src/util/linux.py
deleted file mode 100644 (file)
index 4e77445..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env python
-
-
-import os
-import logging
-
-try:
-       from xdg import BaseDirectory as _BaseDirectory
-       BaseDirectory = _BaseDirectory
-except ImportError:
-       BaseDirectory = None
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_libc = None
-
-
-def set_process_name(name):
-       try: # change process name for killall
-               global _libc
-               if _libc is None:
-                       import ctypes
-                       _libc = ctypes.CDLL('libc.so.6')
-               _libc.prctl(15, name, 0, 0, 0)
-       except Exception, e:
-               _moduleLogger.warning('Unable to set processName: %s" % e')
-
-
-def get_new_resource(resourceType, resource, name):
-       if BaseDirectory is not None:
-               if resourceType == "data":
-                       base = BaseDirectory.xdg_data_home
-                       if base == "/usr/share/mime":
-                               # Ugly hack because somehow Maemo 4.1 seems to be set to this
-                               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
-               elif resourceType == "config":
-                       base = BaseDirectory.xdg_config_home
-               elif resourceType == "cache":
-                       base = BaseDirectory.xdg_cache_home
-               else:
-                       raise RuntimeError("Unknown type: "+resourceType)
-       else:
-               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
-
-       filePath = os.path.join(base, resource, name)
-       dirPath = os.path.dirname(filePath)
-       if not os.path.exists(dirPath):
-               # Looking before I leap to not mask errors
-               os.makedirs(dirPath)
-
-       return filePath
-
-
-def get_existing_resource(resourceType, resource, name):
-       if BaseDirectory is not None:
-               if resourceType == "data":
-                       base = BaseDirectory.xdg_data_home
-               elif resourceType == "config":
-                       base = BaseDirectory.xdg_config_home
-               elif resourceType == "cache":
-                       base = BaseDirectory.xdg_cache_home
-               else:
-                       raise RuntimeError("Unknown type: "+resourceType)
-       else:
-               base = None
-
-       if base is not None:
-               finalPath = os.path.join(base, name)
-               if os.path.exists(finalPath):
-                       return finalPath
-
-       altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource)
-       finalPath = os.path.join(altBase, name)
-       if os.path.exists(finalPath):
-               return finalPath
-       else:
-               raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), ))
diff --git a/src/util/misc.py b/src/util/misc.py
deleted file mode 100644 (file)
index 9b8d88c..0000000
+++ /dev/null
@@ -1,900 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-import sys
-import re
-import cPickle
-
-import functools
-import contextlib
-import inspect
-
-import optparse
-import traceback
-import warnings
-import string
-
-
-class AnyData(object):
-
-       pass
-
-
-_indentationLevel = [0]
-
-
-def log_call(logger):
-
-       def log_call_decorator(func):
-
-               @functools.wraps(func)
-               def wrapper(*args, **kwds):
-                       logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, ))
-                       _indentationLevel[0] += 1
-                       try:
-                               return func(*args, **kwds)
-                       finally:
-                               _indentationLevel[0] -= 1
-                               logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, ))
-
-               return wrapper
-
-       return log_call_decorator
-
-
-def log_exception(logger):
-
-       def log_exception_decorator(func):
-
-               @functools.wraps(func)
-               def wrapper(*args, **kwds):
-                       try:
-                               return func(*args, **kwds)
-                       except Exception:
-                               logger.exception(func.__name__)
-                               raise
-
-               return wrapper
-
-       return log_exception_decorator
-
-
-def printfmt(template):
-       """
-       This hides having to create the Template object and call substitute/safe_substitute on it. For example:
-
-       >>> num = 10
-       >>> word = "spam"
-       >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
-       I would like to order 10 units of spam, please
-       """
-       frame = inspect.stack()[-1][0]
-       try:
-               print string.Template(template).safe_substitute(frame.f_locals)
-       finally:
-               del frame
-
-
-def is_special(name):
-       return name.startswith("__") and name.endswith("__")
-
-
-def is_private(name):
-       return name.startswith("_") and not is_special(name)
-
-
-def privatize(clsName, attributeName):
-       """
-       At runtime, make an attributeName private
-
-       Example:
-       >>> class Test(object):
-       ...     pass
-       ...
-       >>> try:
-       ...     dir(Test).index("_Test__me")
-       ...     print dir(Test)
-       ... except:
-       ...     print "Not Found"
-       Not Found
-       >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
-       >>> try:
-       ...     dir(Test).index("_Test__me")
-       ...     print "Found"
-       ... except:
-       ...     print dir(Test)
-       0
-       Found
-       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
-       Hello World
-       >>>
-       >>> is_private(privatize(Test.__name__, "me"))
-       True
-       >>> is_special(privatize(Test.__name__, "me"))
-       False
-       """
-       return "".join(["_", clsName, "__", attributeName])
-
-
-def obfuscate(clsName, attributeName):
-       """
-       At runtime, turn a private name into the obfuscated form
-
-       Example:
-       >>> class Test(object):
-       ...     __me = "Hello World"
-       ...
-       >>> try:
-       ...     dir(Test).index("_Test__me")
-       ...     print "Found"
-       ... except:
-       ...     print dir(Test)
-       0
-       Found
-       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
-       Hello World
-       >>> is_private(obfuscate(Test.__name__, "__me"))
-       True
-       >>> is_special(obfuscate(Test.__name__, "__me"))
-       False
-       """
-       return "".join(["_", clsName, attributeName])
-
-
-class PAOptionParser(optparse.OptionParser, object):
-       """
-       >>> if __name__ == '__main__':
-       ...     #parser = PAOptionParser("My usage str")
-       ...     parser = PAOptionParser()
-       ...     parser.add_posarg("Foo", help="Foo usage")
-       ...     parser.add_posarg("Bar", dest="bar_dest")
-       ...     parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
-       ...     parser.add_option('--stocksym', dest='symbol')
-       ...     values, args = parser.parse_args()
-       ...     print values, args
-       ...
-
-       python mycp.py  -h
-       python mycp.py
-       python mycp.py  foo
-       python mycp.py  foo bar
-
-       python mycp.py foo bar lava
-       Usage: pa.py <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
diff --git a/src/util/overloading.py b/src/util/overloading.py
deleted file mode 100644 (file)
index 89cb738..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env python
-import new
-
-# Make the environment more like Python 3.0
-__metaclass__ = type
-from itertools import izip as zip
-import textwrap
-import inspect
-
-
-__all__ = [
-       "AnyType",
-       "overloaded"
-]
-
-
-AnyType = object
-
-
-class overloaded:
-       """
-       Dynamically overloaded functions.
-
-       This is an implementation of (dynamically, or run-time) overloaded
-       functions; also known as generic functions or multi-methods.
-
-       The dispatch algorithm uses the types of all argument for dispatch,
-       similar to (compile-time) overloaded functions or methods in C++ and
-       Java.
-
-       Most of the complexity in the algorithm comes from the need to support
-       subclasses in call signatures.  For example, if an function is
-       registered for a signature (T1, T2), then a call with a signature (S1,
-       S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass
-       of T2, and there are no other more specific matches (see below).
-
-       If there are multiple matches and one of those doesn't *dominate* all
-       others, the match is deemed ambiguous and an exception is raised.  A
-       subtlety here: if, after removing the dominated matches, there are
-       still multiple matches left, but they all map to the same function,
-       then the match is not deemed ambiguous and that function is used.
-       Read the method find_func() below for details.
-
-       @note Python 2.5 is required due to the use of predicates any() and all().
-       @note only supports positional arguments
-
-       @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514
-
-       >>> import misc
-       >>> misc.validate_decorator (overloaded)
-       >>>
-       >>>
-       >>>
-       >>>
-       >>> #################
-       >>> #Basics, with reusing names and without
-       >>> @overloaded
-       ... def foo(x):
-       ...     "prints x"
-       ...     print x
-       ...
-       >>> @foo.register(int)
-       ... def foo(x):
-       ...     "prints the hex representation of x"
-       ...     print hex(x)
-       ...
-       >>> from types import DictType
-       >>> @foo.register(DictType)
-       ... def foo_dict(x):
-       ...     "prints the keys of x"
-       ...     print [k for k in x.iterkeys()]
-       ...
-       >>> #combines all of the doc strings to help keep track of the specializations
-       >>> foo.__doc__  # doctest: +ELLIPSIS
-       "prints x\\n\\n...overloading.foo (<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))
diff --git a/src/util/qore_utils.py b/src/util/qore_utils.py
deleted file mode 100644 (file)
index 153558d..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class QThread44(QtCore.QThread):
-       """
-       This is to imitate QThread in Qt 4.4+ for when running on older version
-       See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
-       (On Lucid I have Qt 4.7 and this is still an issue)
-       """
-
-       def __init__(self, parent = None):
-               QtCore.QThread.__init__(self, parent)
-
-       def run(self):
-               self.exec_()
-
-
-class _WorkerThread(QtCore.QObject):
-
-       _taskComplete  = qt_compat.Signal(object)
-
-       def __init__(self, futureThread):
-               QtCore.QObject.__init__(self)
-               self._futureThread = futureThread
-               self._futureThread._addTask.connect(self._on_task_added)
-               self._taskComplete.connect(self._futureThread._on_task_complete)
-
-       @qt_compat.Slot(object)
-       def _on_task_added(self, task):
-               self.__on_task_added(task)
-
-       @misc.log_exception(_moduleLogger)
-       def __on_task_added(self, task):
-               if not self._futureThread._isRunning:
-                       _moduleLogger.error("Dropping task")
-
-               func, args, kwds, on_success, on_error = task
-
-               try:
-                       result = func(*args, **kwds)
-                       isError = False
-               except Exception, e:
-                       _moduleLogger.error("Error, passing it back to the main thread")
-                       result = e
-                       isError = True
-
-               taskResult = on_success, on_error, isError, result
-               self._taskComplete.emit(taskResult)
-
-
-class FutureThread(QtCore.QObject):
-
-       _addTask = qt_compat.Signal(object)
-
-       def __init__(self):
-               QtCore.QObject.__init__(self)
-               self._thread = QThread44()
-               self._isRunning = False
-               self._worker = _WorkerThread(self)
-               self._worker.moveToThread(self._thread)
-
-       def start(self):
-               self._thread.start()
-               self._isRunning = True
-
-       def stop(self):
-               self._isRunning = False
-               self._thread.quit()
-
-       def add_task(self, func, args, kwds, on_success, on_error):
-               assert self._isRunning, "Task queue not started"
-               task = func, args, kwds, on_success, on_error
-               self._addTask.emit(task)
-
-       @qt_compat.Slot(object)
-       def _on_task_complete(self, taskResult):
-               self.__on_task_complete(taskResult)
-
-       @misc.log_exception(_moduleLogger)
-       def __on_task_complete(self, taskResult):
-               on_success, on_error, isError, result = taskResult
-               if not self._isRunning:
-                       if isError:
-                               _moduleLogger.error("Masking: %s" % (result, ))
-                       isError = True
-                       result = StopIteration("Cancelling all callbacks")
-               callback = on_success if not isError else on_error
-               try:
-                       callback(result)
-               except Exception:
-                       _moduleLogger.exception("Callback errored")
diff --git a/src/util/qt_compat.py b/src/util/qt_compat.py
deleted file mode 100644 (file)
index 2ab7fa4..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-#try:
-#      import PySide.QtCore as _QtCore
-#      QtCore = _QtCore
-#      USES_PYSIDE = True
-#except ImportError:
-if True:
-       import sip
-       sip.setapi('QString', 2)
-       sip.setapi('QVariant', 2)
-       import PyQt4.QtCore as _QtCore
-       QtCore = _QtCore
-       USES_PYSIDE = False
-
-
-def _pyside_import_module(moduleName):
-       pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
-       return getattr(pyside, moduleName)
-
-
-def _pyqt4_import_module(moduleName):
-       pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
-       return getattr(pyside, moduleName)
-
-
-if USES_PYSIDE:
-       import_module = _pyside_import_module
-
-       Signal = QtCore.Signal
-       Slot = QtCore.Slot
-       Property = QtCore.Property
-else:
-       import_module = _pyqt4_import_module
-
-       Signal = QtCore.pyqtSignal
-       Slot = QtCore.pyqtSlot
-       Property = QtCore.pyqtProperty
-
-
-if __name__ == "__main__":
-       pass
-
diff --git a/src/util/qtpie.py b/src/util/qtpie.py
deleted file mode 100755 (executable)
index 6b77d5d..0000000
+++ /dev/null
@@ -1,1094 +0,0 @@
-#!/usr/bin/env python
-
-import math
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-_TWOPI = 2 * math.pi
-
-
-def _radius_at(center, pos):
-       delta = pos - center
-       xDelta = delta.x()
-       yDelta = delta.y()
-
-       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
-       return radius
-
-
-def _angle_at(center, pos):
-       delta = pos - center
-       xDelta = delta.x()
-       yDelta = delta.y()
-
-       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
-       angle = math.acos(xDelta / radius)
-       if 0 <= yDelta:
-               angle = _TWOPI - angle
-
-       return angle
-
-
-class QActionPieItem(object):
-
-       def __init__(self, action, weight = 1):
-               self._action = action
-               self._weight = weight
-
-       def action(self):
-               return self._action
-
-       def setWeight(self, weight):
-               self._weight = weight
-
-       def weight(self):
-               return self._weight
-
-       def setEnabled(self, enabled = True):
-               self._action.setEnabled(enabled)
-
-       def isEnabled(self):
-               return self._action.isEnabled()
-
-
-class PieFiling(object):
-
-       INNER_RADIUS_DEFAULT = 64
-       OUTER_RADIUS_DEFAULT = 192
-
-       SELECTION_CENTER = -1
-       SELECTION_NONE = -2
-
-       NULL_CENTER = QActionPieItem(QtGui.QAction(None))
-
-       def __init__(self):
-               self._innerRadius = self.INNER_RADIUS_DEFAULT
-               self._outerRadius = self.OUTER_RADIUS_DEFAULT
-               self._children = []
-               self._center = self.NULL_CENTER
-
-               self._cacheIndexToAngle = {}
-               self._cacheTotalWeight = 0
-
-       def insertItem(self, item, index = -1):
-               self._children.insert(index, item)
-               self._invalidate_cache()
-
-       def removeItemAt(self, index):
-               item = self._children.pop(index)
-               self._invalidate_cache()
-
-       def set_center(self, item):
-               if item is None:
-                       item = self.NULL_CENTER
-               self._center = item
-
-       def center(self):
-               return self._center
-
-       def clear(self):
-               del self._children[:]
-               self._center = self.NULL_CENTER
-               self._invalidate_cache()
-
-       def itemAt(self, index):
-               return self._children[index]
-
-       def indexAt(self, center, point):
-               return self._angle_to_index(_angle_at(center, point))
-
-       def innerRadius(self):
-               return self._innerRadius
-
-       def setInnerRadius(self, radius):
-               self._innerRadius = radius
-
-       def outerRadius(self):
-               return self._outerRadius
-
-       def setOuterRadius(self, radius):
-               self._outerRadius = radius
-
-       def __iter__(self):
-               return iter(self._children)
-
-       def __len__(self):
-               return len(self._children)
-
-       def __getitem__(self, index):
-               return self._children[index]
-
-       def _invalidate_cache(self):
-               self._cacheIndexToAngle.clear()
-               self._cacheTotalWeight = sum(child.weight() for child in self._children)
-               if self._cacheTotalWeight == 0:
-                       self._cacheTotalWeight = 1
-
-       def _index_to_angle(self, index, isShifted):
-               key = index, isShifted
-               if key in self._cacheIndexToAngle:
-                       return self._cacheIndexToAngle[key]
-               index = index % len(self._children)
-
-               baseAngle = _TWOPI / self._cacheTotalWeight
-
-               angle = math.pi / 2
-               if isShifted:
-                       if self._children:
-                               angle -= (self._children[0].weight() * baseAngle) / 2
-                       else:
-                               angle -= baseAngle / 2
-               while angle < 0:
-                       angle += _TWOPI
-
-               for i, child in enumerate(self._children):
-                       if index < i:
-                               break
-                       angle += child.weight() * baseAngle
-               while _TWOPI < angle:
-                       angle -= _TWOPI
-
-               self._cacheIndexToAngle[key] = angle
-               return angle
-
-       def _angle_to_index(self, angle):
-               numChildren = len(self._children)
-               if numChildren == 0:
-                       return self.SELECTION_CENTER
-
-               baseAngle = _TWOPI / self._cacheTotalWeight
-
-               iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
-               while iterAngle < 0:
-                       iterAngle += _TWOPI
-
-               oldIterAngle = iterAngle
-               for index, child in enumerate(self._children):
-                       iterAngle += child.weight() * baseAngle
-                       if oldIterAngle < angle and angle <= iterAngle:
-                               return index - 1 if index != 0 else numChildren - 1
-                       elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
-                               return index - 1 if index != 0 else numChildren - 1
-                       oldIterAngle = iterAngle
-
-
-class PieArtist(object):
-
-       ICON_SIZE_DEFAULT = 48
-
-       SHAPE_CIRCLE = "circle"
-       SHAPE_SQUARE = "square"
-       DEFAULT_SHAPE = SHAPE_SQUARE
-
-       BACKGROUND_FILL = "fill"
-       BACKGROUND_NOFILL = "no fill"
-
-       def __init__(self, filing, background = BACKGROUND_FILL):
-               self._filing = filing
-
-               self._cachedOuterRadius = self._filing.outerRadius()
-               self._cachedInnerRadius = self._filing.innerRadius()
-               canvasSize = self._cachedOuterRadius * 2 + 1
-               self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
-               self._mask = None
-               self._backgroundState = background
-               self.palette = None
-
-       def pieSize(self):
-               diameter = self._filing.outerRadius() * 2 + 1
-               return QtCore.QSize(diameter, diameter)
-
-       def centerSize(self):
-               painter = QtGui.QPainter(self._canvas)
-               text = self._filing.center().action().text()
-               fontMetrics = painter.fontMetrics()
-               if text:
-                       textBoundingRect = fontMetrics.boundingRect(text)
-               else:
-                       textBoundingRect = QtCore.QRect()
-               textWidth = textBoundingRect.width()
-               textHeight = textBoundingRect.height()
-
-               return QtCore.QSize(
-                       textWidth + self.ICON_SIZE_DEFAULT,
-                       max(textHeight, self.ICON_SIZE_DEFAULT),
-               )
-
-       def show(self, palette):
-               self.palette = palette
-
-               if (
-                       self._cachedOuterRadius != self._filing.outerRadius() or
-                       self._cachedInnerRadius != self._filing.innerRadius()
-               ):
-                       self._cachedOuterRadius = self._filing.outerRadius()
-                       self._cachedInnerRadius = self._filing.innerRadius()
-                       self._canvas = self._canvas.scaled(self.pieSize())
-
-               if self._mask is None:
-                       self._mask = QtGui.QBitmap(self._canvas.size())
-                       self._mask.fill(QtCore.Qt.color0)
-                       self._generate_mask(self._mask)
-                       self._canvas.setMask(self._mask)
-               return self._mask
-
-       def hide(self):
-               self.palette = None
-
-       def paint(self, selectionIndex):
-               painter = QtGui.QPainter(self._canvas)
-               painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
-
-               self.paintPainter(selectionIndex, painter)
-
-               return self._canvas
-
-       def paintPainter(self, selectionIndex, painter):
-               adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1)
-
-               numChildren = len(self._filing)
-               if numChildren == 0:
-                       self._paint_center_background(painter, adjustmentRect, selectionIndex)
-                       self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
-                       return self._canvas
-               else:
-                       for i in xrange(len(self._filing)):
-                               self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
-
-               self._paint_center_background(painter, adjustmentRect, selectionIndex)
-               self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
-
-               for i in xrange(len(self._filing)):
-                       self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex)
-
-       def _generate_mask(self, mask):
-               """
-               Specifies on the mask the shape of the pie menu
-               """
-               painter = QtGui.QPainter(mask)
-               painter.setPen(QtCore.Qt.color1)
-               painter.setBrush(QtCore.Qt.color1)
-               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
-                       painter.drawRect(mask.rect())
-               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
-                       painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
-               else:
-                       raise NotImplementedError(self.DEFAULT_SHAPE)
-
-       def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
-               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
-                       currentWidth = adjustmentRect.width()
-                       newWidth = math.sqrt(2) * currentWidth
-                       dx = (newWidth - currentWidth) / 2
-                       adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
-               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
-                       pass
-               else:
-                       raise NotImplementedError(self.DEFAULT_SHAPE)
-
-               if self._backgroundState == self.BACKGROUND_NOFILL:
-                       painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
-                       painter.setPen(self.palette.highlight().color())
-               else:
-                       if i == selectionIndex and self._filing[i].isEnabled():
-                               painter.setBrush(self.palette.highlight())
-                               painter.setPen(self.palette.highlight().color())
-                       else:
-                               painter.setBrush(self.palette.window())
-                               painter.setPen(self.palette.window().color())
-
-               a = self._filing._index_to_angle(i, True)
-               b = self._filing._index_to_angle(i + 1, True)
-               if b < a:
-                       b += _TWOPI
-               size = b - a
-               if size < 0:
-                       size += _TWOPI
-
-               startAngleInDeg = (a * 360 * 16) / _TWOPI
-               sizeInDeg = (size * 360 * 16) / _TWOPI
-               painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
-
-       def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex):
-               child = self._filing[i]
-
-               a = self._filing._index_to_angle(i, True)
-               b = self._filing._index_to_angle(i + 1, True)
-               if b < a:
-                       b += _TWOPI
-               middleAngle = (a + b) / 2
-               averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
-
-               sliceX = averageRadius * math.cos(middleAngle)
-               sliceY = - averageRadius * math.sin(middleAngle)
-
-               piePos = adjustmentRect.center()
-               pieX = piePos.x()
-               pieY = piePos.y()
-               self._paint_label(
-                       painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
-               )
-
-       def _paint_label(self, painter, action, isSelected, x, y):
-               text = action.text()
-               fontMetrics = painter.fontMetrics()
-               if text:
-                       textBoundingRect = fontMetrics.boundingRect(text)
-               else:
-                       textBoundingRect = QtCore.QRect()
-               textWidth = textBoundingRect.width()
-               textHeight = textBoundingRect.height()
-
-               icon = action.icon().pixmap(
-                       QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
-                       QtGui.QIcon.Normal,
-                       QtGui.QIcon.On,
-               )
-               iconWidth = icon.width()
-               iconHeight = icon.width()
-               averageWidth = (iconWidth + textWidth)/2
-               if not icon.isNull():
-                       iconRect = QtCore.QRect(
-                               x - averageWidth,
-                               y - iconHeight/2,
-                               iconWidth,
-                               iconHeight,
-                       )
-
-                       painter.drawPixmap(iconRect, icon)
-
-               if text:
-                       if isSelected:
-                               if action.isEnabled():
-                                       pen = self.palette.highlightedText()
-                                       brush = self.palette.highlight()
-                               else:
-                                       pen = self.palette.mid()
-                                       brush = self.palette.window()
-                       else:
-                               if action.isEnabled():
-                                       pen = self.palette.windowText()
-                               else:
-                                       pen = self.palette.mid()
-                               brush = self.palette.window()
-
-                       leftX = x - averageWidth + iconWidth
-                       topY = y + textHeight/2
-                       painter.setPen(pen.color())
-                       painter.setBrush(brush)
-                       painter.drawText(leftX, topY, text)
-
-       def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
-               if self._backgroundState == self.BACKGROUND_NOFILL:
-                       return
-               if len(self._filing) == 0:
-                       if self._backgroundState == self.BACKGROUND_NOFILL:
-                               painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
-                       else:
-                               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
-                                       painter.setBrush(self.palette.highlight())
-                               else:
-                                       painter.setBrush(self.palette.window())
-                       painter.setPen(self.palette.mid().color())
-
-                       painter.drawRect(adjustmentRect)
-               else:
-                       dark = self.palette.mid().color()
-                       light = self.palette.light().color()
-                       if self._backgroundState == self.BACKGROUND_NOFILL:
-                               background = QtGui.QBrush(QtCore.Qt.transparent)
-                       else:
-                               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
-                                       background = self.palette.highlight().color()
-                               else:
-                                       background = self.palette.window().color()
-
-                       innerRadius = self._cachedInnerRadius
-                       adjustmentCenterPos = adjustmentRect.center()
-                       innerRect = QtCore.QRect(
-                               adjustmentCenterPos.x() - innerRadius,
-                               adjustmentCenterPos.y() - innerRadius,
-                               innerRadius * 2 + 1,
-                               innerRadius * 2 + 1,
-                       )
-
-                       painter.setPen(QtCore.Qt.NoPen)
-                       painter.setBrush(background)
-                       painter.drawPie(innerRect, 0, 360 * 16)
-
-                       if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
-                               pass
-                       elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
-                               painter.setPen(QtGui.QPen(dark, 1))
-                               painter.setBrush(QtCore.Qt.NoBrush)
-                               painter.drawEllipse(adjustmentRect)
-                       else:
-                               raise NotImplementedError(self.DEFAULT_SHAPE)
-
-       def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex):
-               centerPos = adjustmentRect.center()
-               pieX = centerPos.x()
-               pieY = centerPos.y()
-
-               x = pieX
-               y = pieY
-
-               self._paint_label(
-                       painter,
-                       self._filing.center().action(),
-                       selectionIndex == PieFiling.SELECTION_CENTER,
-                       x, y
-               )
-
-
-class QPieDisplay(QtGui.QWidget):
-
-       def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
-               QtGui.QWidget.__init__(self, parent, flags)
-               self._filing = filing
-               self._artist = PieArtist(self._filing)
-               self._selectionIndex = PieFiling.SELECTION_NONE
-
-       def popup(self, pos):
-               self._update_selection(pos)
-               self.show()
-
-       def sizeHint(self):
-               return self._artist.pieSize()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def showEvent(self, showEvent):
-               mask = self._artist.show(self.palette())
-               self.setMask(mask)
-
-               QtGui.QWidget.showEvent(self, showEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def hideEvent(self, hideEvent):
-               self._artist.hide()
-               self._selectionIndex = PieFiling.SELECTION_NONE
-               QtGui.QWidget.hideEvent(self, hideEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def paintEvent(self, paintEvent):
-               canvas = self._artist.paint(self._selectionIndex)
-               offset = (self.size() - canvas.size()) / 2
-
-               screen = QtGui.QPainter(self)
-               screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
-
-               QtGui.QWidget.paintEvent(self, paintEvent)
-
-       def selectAt(self, index):
-               oldIndex = self._selectionIndex
-               self._selectionIndex = index
-               if self.isVisible():
-                       self.update()
-
-
-class QPieButton(QtGui.QWidget):
-
-       activated = qt_compat.Signal(int)
-       highlighted = qt_compat.Signal(int)
-       canceled = qt_compat.Signal()
-       aboutToShow = qt_compat.Signal()
-       aboutToHide = qt_compat.Signal()
-
-       BUTTON_RADIUS = 24
-       DELAY = 250
-
-       def __init__(self, buttonSlice, parent = None, buttonSlices = None):
-               # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
-               # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
-               QtGui.QWidget.__init__(self, parent)
-               self._cachedCenterPosition = self.rect().center()
-
-               self._filing = PieFiling()
-               self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
-               self._selectionIndex = PieFiling.SELECTION_NONE
-
-               self._buttonFiling = PieFiling()
-               self._buttonFiling.set_center(buttonSlice)
-               if buttonSlices is not None:
-                       for slice in buttonSlices:
-                               self._buttonFiling.insertItem(slice)
-               self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
-               self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL)
-               self._poppedUp = False
-               self._pressed = False
-
-               self._delayPopupTimer = QtCore.QTimer()
-               self._delayPopupTimer.setInterval(self.DELAY)
-               self._delayPopupTimer.setSingleShot(True)
-               self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
-               self._popupLocation = None
-
-               self._mousePosition = None
-               self.setFocusPolicy(QtCore.Qt.StrongFocus)
-               self.setSizePolicy(
-                       QtGui.QSizePolicy(
-                               QtGui.QSizePolicy.MinimumExpanding,
-                               QtGui.QSizePolicy.MinimumExpanding,
-                       )
-               )
-
-       def insertItem(self, item, index = -1):
-               self._filing.insertItem(item, index)
-
-       def removeItemAt(self, index):
-               self._filing.removeItemAt(index)
-
-       def set_center(self, item):
-               self._filing.set_center(item)
-
-       def set_button(self, item):
-               self.update()
-
-       def clear(self):
-               self._filing.clear()
-
-       def itemAt(self, index):
-               return self._filing.itemAt(index)
-
-       def indexAt(self, point):
-               return self._filing.indexAt(self._cachedCenterPosition, point)
-
-       def innerRadius(self):
-               return self._filing.innerRadius()
-
-       def setInnerRadius(self, radius):
-               self._filing.setInnerRadius(radius)
-
-       def outerRadius(self):
-               return self._filing.outerRadius()
-
-       def setOuterRadius(self, radius):
-               self._filing.setOuterRadius(radius)
-
-       def buttonRadius(self):
-               return self._buttonFiling.outerRadius()
-
-       def setButtonRadius(self, radius):
-               self._buttonFiling.setOuterRadius(radius)
-               self._buttonFiling.setInnerRadius(radius / 2)
-               self._buttonArtist.show(self.palette())
-
-       def minimumSizeHint(self):
-               return self._buttonArtist.centerSize()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mousePressEvent(self, mouseEvent):
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               self._mousePosition = lastMousePos
-               self._update_selection(self._cachedCenterPosition)
-
-               self.highlighted.emit(self._selectionIndex)
-
-               self._display.selectAt(self._selectionIndex)
-               self._pressed = True
-               self.update()
-               self._popupLocation = mouseEvent.globalPos()
-               self._delayPopupTimer.start()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_delayed_popup(self):
-               assert self._popupLocation is not None, "Widget location abuse"
-               self._popup_child(self._popupLocation)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mouseMoveEvent(self, mouseEvent):
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               if self._mousePosition is None:
-                       # Absolute
-                       self._update_selection(lastMousePos)
-               else:
-                       # Relative
-                       self._update_selection(
-                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
-                               ignoreOuter = True,
-                       )
-
-               if lastSelection != self._selectionIndex:
-                       self.highlighted.emit(self._selectionIndex)
-                       self._display.selectAt(self._selectionIndex)
-
-               if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
-                       self._on_delayed_popup()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mouseReleaseEvent(self, mouseEvent):
-               self._delayPopupTimer.stop()
-               self._popupLocation = None
-
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               if self._mousePosition is None:
-                       # Absolute
-                       self._update_selection(lastMousePos)
-               else:
-                       # Relative
-                       self._update_selection(
-                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
-                               ignoreOuter = True,
-                       )
-               self._mousePosition = None
-
-               self._activate_at(self._selectionIndex)
-               self._pressed = False
-               self.update()
-               self._hide_child()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def keyPressEvent(self, keyEvent):
-               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
-                       self._popup_child(QtGui.QCursor.pos())
-                       if self._selectionIndex != len(self._filing) - 1:
-                               nextSelection = self._selectionIndex + 1
-                       else:
-                               nextSelection = 0
-                       self._select_at(nextSelection)
-                       self._display.selectAt(self._selectionIndex)
-               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
-                       self._popup_child(QtGui.QCursor.pos())
-                       if 0 < self._selectionIndex:
-                               nextSelection = self._selectionIndex - 1
-                       else:
-                               nextSelection = len(self._filing) - 1
-                       self._select_at(nextSelection)
-                       self._display.selectAt(self._selectionIndex)
-               elif keyEvent.key() in [QtCore.Qt.Key_Space]:
-                       self._popup_child(QtGui.QCursor.pos())
-                       self._select_at(PieFiling.SELECTION_CENTER)
-                       self._display.selectAt(self._selectionIndex)
-               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
-                       self._delayPopupTimer.stop()
-                       self._popupLocation = None
-                       self._activate_at(self._selectionIndex)
-                       self._hide_child()
-               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
-                       self._delayPopupTimer.stop()
-                       self._popupLocation = None
-                       self._activate_at(PieFiling.SELECTION_NONE)
-                       self._hide_child()
-               else:
-                       QtGui.QWidget.keyPressEvent(self, keyEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def resizeEvent(self, resizeEvent):
-               self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
-               QtGui.QWidget.resizeEvent(self, resizeEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def showEvent(self, showEvent):
-               self._buttonArtist.show(self.palette())
-               self._cachedCenterPosition = self.rect().center()
-
-               QtGui.QWidget.showEvent(self, showEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def hideEvent(self, hideEvent):
-               self._display.hide()
-               self._select_at(PieFiling.SELECTION_NONE)
-               QtGui.QWidget.hideEvent(self, hideEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def paintEvent(self, paintEvent):
-               self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
-               if self._poppedUp:
-                       selectionIndex = PieFiling.SELECTION_CENTER
-               else:
-                       selectionIndex = PieFiling.SELECTION_NONE
-
-               screen = QtGui.QStylePainter(self)
-               screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
-               option = QtGui.QStyleOptionButton()
-               option.initFrom(self)
-               option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
-
-               screen.drawControl(QtGui.QStyle.CE_PushButton, option)
-               self._buttonArtist.paintPainter(selectionIndex, screen)
-
-               QtGui.QWidget.paintEvent(self, paintEvent)
-
-       def __iter__(self):
-               return iter(self._filing)
-
-       def __len__(self):
-               return len(self._filing)
-
-       def _popup_child(self, position):
-               self._poppedUp = True
-               self.aboutToShow.emit()
-
-               self._delayPopupTimer.stop()
-               self._popupLocation = None
-
-               position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
-               self._display.move(position)
-               self._display.show()
-
-               self.update()
-
-       def _hide_child(self):
-               self._poppedUp = False
-               self.aboutToHide.emit()
-               self._display.hide()
-               self.update()
-
-       def _select_at(self, index):
-               self._selectionIndex = index
-
-       def _update_selection(self, lastMousePos, ignoreOuter = False):
-               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
-               if radius < self._filing.innerRadius():
-                       self._select_at(PieFiling.SELECTION_CENTER)
-               elif radius <= self._filing.outerRadius() or ignoreOuter:
-                       self._select_at(self.indexAt(lastMousePos))
-               else:
-                       self._select_at(PieFiling.SELECTION_NONE)
-
-       def _activate_at(self, index):
-               if index == PieFiling.SELECTION_NONE:
-                       self.canceled.emit()
-                       return
-               elif index == PieFiling.SELECTION_CENTER:
-                       child = self._filing.center()
-               else:
-                       child = self.itemAt(index)
-
-               if child.action().isEnabled():
-                       child.action().trigger()
-                       self.activated.emit(index)
-               else:
-                       self.canceled.emit()
-
-
-class QPieMenu(QtGui.QWidget):
-
-       activated = qt_compat.Signal(int)
-       highlighted = qt_compat.Signal(int)
-       canceled = qt_compat.Signal()
-       aboutToShow = qt_compat.Signal()
-       aboutToHide = qt_compat.Signal()
-
-       def __init__(self, parent = None):
-               QtGui.QWidget.__init__(self, parent)
-               self._cachedCenterPosition = self.rect().center()
-
-               self._filing = PieFiling()
-               self._artist = PieArtist(self._filing)
-               self._selectionIndex = PieFiling.SELECTION_NONE
-
-               self._mousePosition = ()
-               self.setFocusPolicy(QtCore.Qt.StrongFocus)
-
-       def popup(self, pos):
-               self._update_selection(pos)
-               self.show()
-
-       def insertItem(self, item, index = -1):
-               self._filing.insertItem(item, index)
-               self.update()
-
-       def removeItemAt(self, index):
-               self._filing.removeItemAt(index)
-               self.update()
-
-       def set_center(self, item):
-               self._filing.set_center(item)
-               self.update()
-
-       def clear(self):
-               self._filing.clear()
-               self.update()
-
-       def itemAt(self, index):
-               return self._filing.itemAt(index)
-
-       def indexAt(self, point):
-               return self._filing.indexAt(self._cachedCenterPosition, point)
-
-       def innerRadius(self):
-               return self._filing.innerRadius()
-
-       def setInnerRadius(self, radius):
-               self._filing.setInnerRadius(radius)
-               self.update()
-
-       def outerRadius(self):
-               return self._filing.outerRadius()
-
-       def setOuterRadius(self, radius):
-               self._filing.setOuterRadius(radius)
-               self.update()
-
-       def sizeHint(self):
-               return self._artist.pieSize()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mousePressEvent(self, mouseEvent):
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               self._update_selection(lastMousePos)
-               self._mousePosition = lastMousePos
-
-               if lastSelection != self._selectionIndex:
-                       self.highlighted.emit(self._selectionIndex)
-                       self.update()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mouseMoveEvent(self, mouseEvent):
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               self._update_selection(lastMousePos)
-
-               if lastSelection != self._selectionIndex:
-                       self.highlighted.emit(self._selectionIndex)
-                       self.update()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def mouseReleaseEvent(self, mouseEvent):
-               lastSelection = self._selectionIndex
-
-               lastMousePos = mouseEvent.pos()
-               self._update_selection(lastMousePos)
-               self._mousePosition = ()
-
-               self._activate_at(self._selectionIndex)
-               self.update()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def keyPressEvent(self, keyEvent):
-               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
-                       if self._selectionIndex != len(self._filing) - 1:
-                               nextSelection = self._selectionIndex + 1
-                       else:
-                               nextSelection = 0
-                       self._select_at(nextSelection)
-                       self.update()
-               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
-                       if 0 < self._selectionIndex:
-                               nextSelection = self._selectionIndex - 1
-                       else:
-                               nextSelection = len(self._filing) - 1
-                       self._select_at(nextSelection)
-                       self.update()
-               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
-                       self._activate_at(self._selectionIndex)
-               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
-                       self._activate_at(PieFiling.SELECTION_NONE)
-               else:
-                       QtGui.QWidget.keyPressEvent(self, keyEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def showEvent(self, showEvent):
-               self.aboutToShow.emit()
-               self._cachedCenterPosition = self.rect().center()
-
-               mask = self._artist.show(self.palette())
-               self.setMask(mask)
-
-               lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
-               self._update_selection(lastMousePos)
-
-               QtGui.QWidget.showEvent(self, showEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def hideEvent(self, hideEvent):
-               self._artist.hide()
-               self._selectionIndex = PieFiling.SELECTION_NONE
-               QtGui.QWidget.hideEvent(self, hideEvent)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def paintEvent(self, paintEvent):
-               canvas = self._artist.paint(self._selectionIndex)
-
-               screen = QtGui.QPainter(self)
-               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
-
-               QtGui.QWidget.paintEvent(self, paintEvent)
-
-       def __iter__(self):
-               return iter(self._filing)
-
-       def __len__(self):
-               return len(self._filing)
-
-       def _select_at(self, index):
-               self._selectionIndex = index
-
-       def _update_selection(self, lastMousePos):
-               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
-               if radius < self._filing.innerRadius():
-                       self._selectionIndex = PieFiling.SELECTION_CENTER
-               elif radius <= self._filing.outerRadius():
-                       self._select_at(self.indexAt(lastMousePos))
-               else:
-                       self._selectionIndex = PieFiling.SELECTION_NONE
-
-       def _activate_at(self, index):
-               if index == PieFiling.SELECTION_NONE:
-                       self.canceled.emit()
-                       self.aboutToHide.emit()
-                       self.hide()
-                       return
-               elif index == PieFiling.SELECTION_CENTER:
-                       child = self._filing.center()
-               else:
-                       child = self.itemAt(index)
-
-               if child.isEnabled():
-                       child.action().trigger()
-                       self.activated.emit(index)
-               else:
-                       self.canceled.emit()
-               self.aboutToHide.emit()
-               self.hide()
-
-
-def init_pies():
-       PieFiling.NULL_CENTER.setEnabled(False)
-
-
-def _print(msg):
-       print msg
-
-
-def _on_about_to_hide(app):
-       app.exit()
-
-
-if __name__ == "__main__":
-       app = QtGui.QApplication([])
-       init_pies()
-
-       if False:
-               pie = QPieMenu()
-               pie.show()
-
-       if False:
-               singleAction = QtGui.QAction(None)
-               singleAction.setText("Boo")
-               singleItem = QActionPieItem(singleAction)
-               spie = QPieMenu()
-               spie.insertItem(singleItem)
-               spie.show()
-
-       if False:
-               oneAction = QtGui.QAction(None)
-               oneAction.setText("Chew")
-               oneItem = QActionPieItem(oneAction)
-               twoAction = QtGui.QAction(None)
-               twoAction.setText("Foo")
-               twoItem = QActionPieItem(twoAction)
-               iconTextAction = QtGui.QAction(None)
-               iconTextAction.setText("Icon")
-               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
-               iconTextItem = QActionPieItem(iconTextAction)
-               mpie = QPieMenu()
-               mpie.insertItem(oneItem)
-               mpie.insertItem(twoItem)
-               mpie.insertItem(oneItem)
-               mpie.insertItem(iconTextItem)
-               mpie.show()
-
-       if True:
-               oneAction = QtGui.QAction(None)
-               oneAction.setText("Chew")
-               oneAction.triggered.connect(lambda: _print("Chew"))
-               oneItem = QActionPieItem(oneAction)
-               twoAction = QtGui.QAction(None)
-               twoAction.setText("Foo")
-               twoAction.triggered.connect(lambda: _print("Foo"))
-               twoItem = QActionPieItem(twoAction)
-               iconAction = QtGui.QAction(None)
-               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
-               iconAction.triggered.connect(lambda: _print("Icon"))
-               iconItem = QActionPieItem(iconAction)
-               iconTextAction = QtGui.QAction(None)
-               iconTextAction.setText("Icon")
-               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
-               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
-               iconTextItem = QActionPieItem(iconTextAction)
-               mpie = QPieMenu()
-               mpie.set_center(iconItem)
-               mpie.insertItem(oneItem)
-               mpie.insertItem(twoItem)
-               mpie.insertItem(oneItem)
-               mpie.insertItem(iconTextItem)
-               mpie.show()
-               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
-               mpie.canceled.connect(lambda: _print("Canceled"))
-
-       if False:
-               oneAction = QtGui.QAction(None)
-               oneAction.setText("Chew")
-               oneAction.triggered.connect(lambda: _print("Chew"))
-               oneItem = QActionPieItem(oneAction)
-               twoAction = QtGui.QAction(None)
-               twoAction.setText("Foo")
-               twoAction.triggered.connect(lambda: _print("Foo"))
-               twoItem = QActionPieItem(twoAction)
-               iconAction = QtGui.QAction(None)
-               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
-               iconAction.triggered.connect(lambda: _print("Icon"))
-               iconItem = QActionPieItem(iconAction)
-               iconTextAction = QtGui.QAction(None)
-               iconTextAction.setText("Icon")
-               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
-               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
-               iconTextItem = QActionPieItem(iconTextAction)
-               pieFiling = PieFiling()
-               pieFiling.set_center(iconItem)
-               pieFiling.insertItem(oneItem)
-               pieFiling.insertItem(twoItem)
-               pieFiling.insertItem(oneItem)
-               pieFiling.insertItem(iconTextItem)
-               mpie = QPieDisplay(pieFiling)
-               mpie.show()
-
-       if False:
-               oneAction = QtGui.QAction(None)
-               oneAction.setText("Chew")
-               oneAction.triggered.connect(lambda: _print("Chew"))
-               oneItem = QActionPieItem(oneAction)
-               twoAction = QtGui.QAction(None)
-               twoAction.setText("Foo")
-               twoAction.triggered.connect(lambda: _print("Foo"))
-               twoItem = QActionPieItem(twoAction)
-               iconAction = QtGui.QAction(None)
-               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
-               iconAction.triggered.connect(lambda: _print("Icon"))
-               iconItem = QActionPieItem(iconAction)
-               iconTextAction = QtGui.QAction(None)
-               iconTextAction.setText("Icon")
-               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
-               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
-               iconTextItem = QActionPieItem(iconTextAction)
-               mpie = QPieButton(iconItem)
-               mpie.set_center(iconItem)
-               mpie.insertItem(oneItem)
-               mpie.insertItem(twoItem)
-               mpie.insertItem(oneItem)
-               mpie.insertItem(iconTextItem)
-               mpie.show()
-               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
-               mpie.canceled.connect(lambda: _print("Canceled"))
-
-       app.exec_()
diff --git a/src/util/qtpieboard.py b/src/util/qtpieboard.py
deleted file mode 100755 (executable)
index 50ae9ae..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-#!/usr/bin/env python
-
-
-from __future__ import division
-
-import os
-import warnings
-
-import qt_compat
-QtGui = qt_compat.import_module("QtGui")
-
-import qtpie
-
-
-class PieKeyboard(object):
-
-       SLICE_CENTER = -1
-       SLICE_NORTH = 0
-       SLICE_NORTH_WEST = 1
-       SLICE_WEST = 2
-       SLICE_SOUTH_WEST = 3
-       SLICE_SOUTH = 4
-       SLICE_SOUTH_EAST = 5
-       SLICE_EAST = 6
-       SLICE_NORTH_EAST = 7
-
-       MAX_ANGULAR_SLICES = 8
-
-       SLICE_DIRECTIONS = [
-               SLICE_CENTER,
-               SLICE_NORTH,
-               SLICE_NORTH_WEST,
-               SLICE_WEST,
-               SLICE_SOUTH_WEST,
-               SLICE_SOUTH,
-               SLICE_SOUTH_EAST,
-               SLICE_EAST,
-               SLICE_NORTH_EAST,
-       ]
-
-       SLICE_DIRECTION_NAMES = [
-               "CENTER",
-               "NORTH",
-               "NORTH_WEST",
-               "WEST",
-               "SOUTH_WEST",
-               "SOUTH",
-               "SOUTH_EAST",
-               "EAST",
-               "NORTH_EAST",
-       ]
-
-       def __init__(self):
-               self._layout = QtGui.QGridLayout()
-               self._widget = QtGui.QWidget()
-               self._widget.setLayout(self._layout)
-
-               self.__cells = {}
-
-       @property
-       def toplevel(self):
-               return self._widget
-
-       def add_pie(self, row, column, pieButton):
-               assert len(pieButton) == 8
-               self._layout.addWidget(pieButton, row, column)
-               self.__cells[(row, column)] = pieButton
-
-       def get_pie(self, row, column):
-               return self.__cells[(row, column)]
-
-
-class KeyboardModifier(object):
-
-       def __init__(self, name):
-               self.name = name
-               self.lock = False
-               self.once = False
-
-       @property
-       def isActive(self):
-               return self.lock or self.once
-
-       def on_toggle_lock(self, *args, **kwds):
-               self.lock = not self.lock
-
-       def on_toggle_once(self, *args, **kwds):
-               self.once = not self.once
-
-       def reset_once(self):
-               self.once = False
-
-
-def parse_keyboard_data(text):
-       return eval(text)
-
-
-def _enumerate_pie_slices(pieData, iconPaths):
-       for direction, directionName in zip(
-               PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES
-       ):
-               if directionName in pieData:
-                       sliceData = pieData[directionName]
-
-                       action = QtGui.QAction(None)
-                       try:
-                               action.setText(sliceData["text"])
-                       except KeyError:
-                               pass
-                       try:
-                               relativeIconPath = sliceData["path"]
-                       except KeyError:
-                               pass
-                       else:
-                               for iconPath in iconPaths:
-                                       absIconPath = os.path.join(iconPath, relativeIconPath)
-                                       if os.path.exists(absIconPath):
-                                               action.setIcon(QtGui.QIcon(absIconPath))
-                                               break
-                       pieItem = qtpie.QActionPieItem(action)
-                       actionToken = sliceData["action"]
-               else:
-                       pieItem = qtpie.PieFiling.NULL_CENTER
-                       actionToken = ""
-               yield direction, pieItem, actionToken
-
-
-def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths):
-       for (row, column), pieData in dataTree.iteritems():
-               pieItems = list(_enumerate_pie_slices(pieData, iconPaths))
-               assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0]
-               _, center, centerAction = pieItems.pop(0)
-
-               pieButton = qtpie.QPieButton(center)
-               pieButton.set_center(center)
-               keyboardHandler.map_slice_action(center, centerAction)
-               for direction, pieItem, action in pieItems:
-                       pieButton.insertItem(pieItem)
-                       keyboardHandler.map_slice_action(pieItem, action)
-               keyboard.add_pie(row, column, pieButton)
-
-
-class KeyboardHandler(object):
-
-       def __init__(self, keyhandler):
-               self.__keyhandler = keyhandler
-               self.__commandHandlers = {}
-               self.__modifiers = {}
-               self.__sliceActions = {}
-
-               self.register_modifier("Shift")
-               self.register_modifier("Super")
-               self.register_modifier("Control")
-               self.register_modifier("Alt")
-
-       def register_command_handler(self, command, handler):
-               # @todo Look into hooking these up directly to the pie actions
-               self.__commandHandlers["[%s]" % command] = handler
-
-       def unregister_command_handler(self, command):
-               # @todo Look into hooking these up directly to the pie actions
-               del self.__commandHandlers["[%s]" % command]
-
-       def register_modifier(self, modifierName):
-               mod = KeyboardModifier(modifierName)
-               self.register_command_handler(modifierName, mod.on_toggle_lock)
-               self.__modifiers["<%s>" % modifierName] = mod
-
-       def unregister_modifier(self, modifierName):
-               self.unregister_command_handler(modifierName)
-               del self.__modifiers["<%s>" % modifierName]
-
-       def map_slice_action(self, slice, action):
-               callback = lambda direction: self(direction, action)
-               slice.action().triggered.connect(callback)
-               self.__sliceActions[slice] = (action, callback)
-
-       def __call__(self, direction, action):
-               activeModifiers = [
-                       mod.name
-                       for mod in self.__modifiers.itervalues()
-                               if mod.isActive
-               ]
-
-               needResetOnce = False
-               if action.startswith("[") and action.endswith("]"):
-                       commandName = action[1:-1]
-                       if action in self.__commandHandlers:
-                               self.__commandHandlers[action](commandName, activeModifiers)
-                               needResetOnce = True
-                       else:
-                               warnings.warn("Unknown command: [%s]" % commandName)
-               elif action.startswith("<") and action.endswith(">"):
-                       modName = action[1:-1]
-                       for mod in self.__modifiers.itervalues():
-                               if mod.name == modName:
-                                       mod.on_toggle_once()
-                                       break
-                       else:
-                               warnings.warn("Unknown modifier: <%s>" % modName)
-               else:
-                       self.__keyhandler(action, activeModifiers)
-                       needResetOnce = True
-
-               if needResetOnce:
-                       for mod in self.__modifiers.itervalues():
-                               mod.reset_once()
diff --git a/src/util/qui_utils.py b/src/util/qui_utils.py
deleted file mode 100644 (file)
index 11b3453..0000000
+++ /dev/null
@@ -1,419 +0,0 @@
-import sys
-import contextlib
-import datetime
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-@contextlib.contextmanager
-def notify_error(log):
-       try:
-               yield
-       except:
-               log.push_exception()
-
-
-@contextlib.contextmanager
-def notify_busy(log, message):
-       log.push_busy(message)
-       try:
-               yield
-       finally:
-               log.pop(message)
-
-
-class ErrorMessage(object):
-
-       LEVEL_ERROR = 0
-       LEVEL_BUSY = 1
-       LEVEL_INFO = 2
-
-       def __init__(self, message, level):
-               self._message = message
-               self._level = level
-               self._time = datetime.datetime.now()
-
-       @property
-       def level(self):
-               return self._level
-
-       @property
-       def message(self):
-               return self._message
-
-       def __repr__(self):
-               return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
-
-
-class QErrorLog(QtCore.QObject):
-
-       messagePushed = qt_compat.Signal()
-       messagePopped = qt_compat.Signal()
-
-       def __init__(self):
-               QtCore.QObject.__init__(self)
-               self._messages = []
-
-       def push_busy(self, message):
-               _moduleLogger.info("Entering state: %s" % message)
-               self._push_message(message, ErrorMessage.LEVEL_BUSY)
-
-       def push_message(self, message):
-               self._push_message(message, ErrorMessage.LEVEL_INFO)
-
-       def push_error(self, message):
-               self._push_message(message, ErrorMessage.LEVEL_ERROR)
-
-       def push_exception(self):
-               userMessage = str(sys.exc_info()[1])
-               _moduleLogger.exception(userMessage)
-               self.push_error(userMessage)
-
-       def pop(self, message = None):
-               if message is None:
-                       del self._messages[0]
-               else:
-                       _moduleLogger.info("Exiting state: %s" % message)
-                       messageIndex = [
-                               i
-                               for (i, error) in enumerate(self._messages)
-                               if error.message == message
-                       ]
-                       # Might be removed out of order
-                       if messageIndex:
-                               del self._messages[messageIndex[0]]
-               self.messagePopped.emit()
-
-       def peek_message(self):
-               return self._messages[0]
-
-       def _push_message(self, message, level):
-               self._messages.append(ErrorMessage(message, level))
-               # Sort is defined as stable, so this should be fine
-               self._messages.sort(key=lambda x: x.level)
-               self.messagePushed.emit()
-
-       def __len__(self):
-               return len(self._messages)
-
-
-class ErrorDisplay(object):
-
-       _SENTINEL_ICON = QtGui.QIcon()
-
-       def __init__(self, errorLog):
-               self._errorLog = errorLog
-               self._errorLog.messagePushed.connect(self._on_message_pushed)
-               self._errorLog.messagePopped.connect(self._on_message_popped)
-
-               self._icons = None
-               self._severityLabel = QtGui.QLabel()
-               self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-
-               self._message = QtGui.QLabel()
-               self._message.setText("Boo")
-               self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-               self._message.setWordWrap(True)
-
-               self._closeLabel = None
-
-               self._controlLayout = QtGui.QHBoxLayout()
-               self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
-               self._controlLayout.addWidget(self._message, 1000)
-
-               self._widget = QtGui.QWidget()
-               self._widget.setLayout(self._controlLayout)
-               self._widget.hide()
-
-       @property
-       def toplevel(self):
-               return self._widget
-
-       def _show_error(self):
-               if self._icons is None:
-                       self._icons = {
-                               ErrorMessage.LEVEL_BUSY:
-                                       get_theme_icon(
-                                               #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
-                                               ("view-refresh", "general_refresh", "gtk-refresh", )
-                                       ).pixmap(32, 32),
-                               ErrorMessage.LEVEL_INFO:
-                                       get_theme_icon(
-                                               ("dialog-information", "general_notes", "gtk-info")
-                                       ).pixmap(32, 32),
-                               ErrorMessage.LEVEL_ERROR:
-                                       get_theme_icon(
-                                               ("dialog-error", "app_install_error", "gtk-dialog-error")
-                                       ).pixmap(32, 32),
-                       }
-               if self._closeLabel is None:
-                       closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
-                       if closeIcon is not self._SENTINEL_ICON:
-                               self._closeLabel = QtGui.QPushButton(closeIcon, "")
-                       else:
-                               self._closeLabel = QtGui.QPushButton("X")
-                       self._closeLabel.clicked.connect(self._on_close)
-                       self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
-               error = self._errorLog.peek_message()
-               self._message.setText(error.message)
-               self._severityLabel.setPixmap(self._icons[error.level])
-               self._widget.show()
-
-       @qt_compat.Slot()
-       @qt_compat.Slot(bool)
-       @misc.log_exception(_moduleLogger)
-       def _on_close(self, checked = False):
-               self._errorLog.pop()
-
-       @qt_compat.Slot()
-       @misc.log_exception(_moduleLogger)
-       def _on_message_pushed(self):
-               self._show_error()
-
-       @qt_compat.Slot()
-       @misc.log_exception(_moduleLogger)
-       def _on_message_popped(self):
-               if len(self._errorLog) == 0:
-                       self._message.setText("")
-                       self._widget.hide()
-               else:
-                       self._show_error()
-
-
-class QHtmlDelegate(QtGui.QStyledItemDelegate):
-
-       UNDEFINED_SIZE = -1
-
-       def __init__(self, *args, **kwd):
-               QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
-               self._width = self.UNDEFINED_SIZE
-
-       def paint(self, painter, option, index):
-               newOption = QtGui.QStyleOptionViewItemV4(option)
-               self.initStyleOption(newOption, index)
-               if newOption.widget is not None:
-                       style = newOption.widget.style()
-               else:
-                       style = QtGui.QApplication.style()
-
-               doc = QtGui.QTextDocument()
-               doc.setHtml(newOption.text)
-               doc.setTextWidth(newOption.rect.width())
-
-               newOption.text = ""
-               style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
-
-               ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
-               if newOption.state & QtGui.QStyle.State_Selected:
-                       ctx.palette.setColor(
-                               QtGui.QPalette.Text,
-                               newOption.palette.color(
-                                       QtGui.QPalette.Active,
-                                       QtGui.QPalette.HighlightedText
-                               )
-                       )
-               else:
-                       ctx.palette.setColor(
-                               QtGui.QPalette.Text,
-                               newOption.palette.color(
-                                       QtGui.QPalette.Active,
-                                       QtGui.QPalette.Text
-                               )
-                       )
-
-               textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
-               painter.save()
-               painter.translate(textRect.topLeft())
-               painter.setClipRect(textRect.translated(-textRect.topLeft()))
-               doc.documentLayout().draw(painter, ctx)
-               painter.restore()
-
-       def setWidth(self, width, model):
-               if self._width == width:
-                       return
-               self._width = width
-               for c in xrange(model.rowCount()):
-                       cItem = model.item(c, 0)
-                       for r in xrange(model.rowCount()):
-                               rItem = cItem.child(r, 0)
-                               rIndex = model.indexFromItem(rItem)
-                               self.sizeHintChanged.emit(rIndex)
-                               return
-
-       def sizeHint(self, option, index):
-               newOption = QtGui.QStyleOptionViewItemV4(option)
-               self.initStyleOption(newOption, index)
-
-               doc = QtGui.QTextDocument()
-               doc.setHtml(newOption.text)
-               if self._width != self.UNDEFINED_SIZE:
-                       width = self._width
-               else:
-                       width = newOption.rect.width()
-               doc.setTextWidth(width)
-               size = QtCore.QSize(doc.idealWidth(), doc.size().height())
-               return size
-
-
-class QSignalingMainWindow(QtGui.QMainWindow):
-
-       closed = qt_compat.Signal()
-       hidden = qt_compat.Signal()
-       shown = qt_compat.Signal()
-       resized = qt_compat.Signal()
-
-       def __init__(self, *args, **kwd):
-               QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
-
-       def closeEvent(self, event):
-               val = QtGui.QMainWindow.closeEvent(self, event)
-               self.closed.emit()
-               return val
-
-       def hideEvent(self, event):
-               val = QtGui.QMainWindow.hideEvent(self, event)
-               self.hidden.emit()
-               return val
-
-       def showEvent(self, event):
-               val = QtGui.QMainWindow.showEvent(self, event)
-               self.shown.emit()
-               return val
-
-       def resizeEvent(self, event):
-               val = QtGui.QMainWindow.resizeEvent(self, event)
-               self.resized.emit()
-               return val
-
-def set_current_index(selector, itemText, default = 0):
-       for i in xrange(selector.count()):
-               if selector.itemText(i) == itemText:
-                       selector.setCurrentIndex(i)
-                       break
-       else:
-               itemText.setCurrentIndex(default)
-
-
-def _null_set_stackable(window, isStackable):
-       pass
-
-
-def _maemo_set_stackable(window, isStackable):
-       window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
-
-
-try:
-       QtCore.Qt.WA_Maemo5StackedWindow
-       set_stackable = _maemo_set_stackable
-except AttributeError:
-       set_stackable = _null_set_stackable
-
-
-def _null_set_autorient(window, doAutoOrient):
-       pass
-
-
-def _maemo_set_autorient(window, doAutoOrient):
-       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
-
-
-try:
-       QtCore.Qt.WA_Maemo5AutoOrientation
-       set_autorient = _maemo_set_autorient
-except AttributeError:
-       set_autorient = _null_set_autorient
-
-
-def screen_orientation():
-       geom = QtGui.QApplication.desktop().screenGeometry()
-       if geom.width() <= geom.height():
-               return QtCore.Qt.Vertical
-       else:
-               return QtCore.Qt.Horizontal
-
-
-def _null_set_window_orientation(window, orientation):
-       pass
-
-
-def _maemo_set_window_orientation(window, orientation):
-       if orientation == QtCore.Qt.Vertical:
-               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
-               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
-       elif orientation == QtCore.Qt.Horizontal:
-               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
-               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
-       elif orientation is None:
-               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
-               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
-       else:
-               raise RuntimeError("Unknown orientation: %r" % orientation)
-
-
-try:
-       QtCore.Qt.WA_Maemo5LandscapeOrientation
-       QtCore.Qt.WA_Maemo5PortraitOrientation
-       set_window_orientation = _maemo_set_window_orientation
-except AttributeError:
-       set_window_orientation = _null_set_window_orientation
-
-
-def _null_show_progress_indicator(window, isStackable):
-       pass
-
-
-def _maemo_show_progress_indicator(window, isStackable):
-       window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
-
-
-try:
-       QtCore.Qt.WA_Maemo5ShowProgressIndicator
-       show_progress_indicator = _maemo_show_progress_indicator
-except AttributeError:
-       show_progress_indicator = _null_show_progress_indicator
-
-
-def _null_mark_numbers_preferred(widget):
-       pass
-
-
-def _newqt_mark_numbers_preferred(widget):
-       widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
-
-
-try:
-       QtCore.Qt.ImhPreferNumbers
-       mark_numbers_preferred = _newqt_mark_numbers_preferred
-except AttributeError:
-       mark_numbers_preferred = _null_mark_numbers_preferred
-
-
-def _null_get_theme_icon(iconNames, fallback = None):
-       icon = fallback if fallback is not None else QtGui.QIcon()
-       return icon
-
-
-def _newqt_get_theme_icon(iconNames, fallback = None):
-       for iconName in iconNames:
-               if QtGui.QIcon.hasThemeIcon(iconName):
-                       icon = QtGui.QIcon.fromTheme(iconName)
-                       break
-       else:
-               icon = fallback if fallback is not None else QtGui.QIcon()
-       return icon
-
-
-try:
-       QtGui.QIcon.fromTheme
-       get_theme_icon = _newqt_get_theme_icon
-except AttributeError:
-       get_theme_icon = _null_get_theme_icon
-
diff --git a/src/util/qwrappers.py b/src/util/qwrappers.py
deleted file mode 100644 (file)
index 2c50c8a..0000000
+++ /dev/null
@@ -1,328 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-from __future__ import division
-
-import logging
-
-import qt_compat
-QtCore = qt_compat.QtCore
-QtGui = qt_compat.import_module("QtGui")
-
-from util import qui_utils
-from util import misc as misc_utils
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class ApplicationWrapper(object):
-
-       DEFAULT_ORIENTATION = "Default"
-       AUTO_ORIENTATION = "Auto"
-       LANDSCAPE_ORIENTATION = "Landscape"
-       PORTRAIT_ORIENTATION = "Portrait"
-
-       def __init__(self, qapp, constants):
-               self._constants = constants
-               self._qapp = qapp
-               self._clipboard = QtGui.QApplication.clipboard()
-
-               self._errorLog = qui_utils.QErrorLog()
-               self._mainWindow = None
-
-               self._fullscreenAction = QtGui.QAction(None)
-               self._fullscreenAction.setText("Fullscreen")
-               self._fullscreenAction.setCheckable(True)
-               self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
-               self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
-
-               self._orientation = self.DEFAULT_ORIENTATION
-               self._orientationAction = QtGui.QAction(None)
-               self._orientationAction.setText("Next Orientation")
-               self._orientationAction.setCheckable(True)
-               self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
-               self._orientationAction.triggered.connect(self._on_next_orientation)
-
-               self._logAction = QtGui.QAction(None)
-               self._logAction.setText("Log")
-               self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
-               self._logAction.triggered.connect(self._on_log)
-
-               self._quitAction = QtGui.QAction(None)
-               self._quitAction.setText("Quit")
-               self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
-               self._quitAction.triggered.connect(self._on_quit)
-
-               self._aboutAction = QtGui.QAction(None)
-               self._aboutAction.setText("About")
-               self._aboutAction.triggered.connect(self._on_about)
-
-               self._qapp.lastWindowClosed.connect(self._on_app_quit)
-               self._mainWindow = self._new_main_window()
-               self._mainWindow.window.destroyed.connect(self._on_child_close)
-
-               self.load_settings()
-
-               self._mainWindow.show()
-               self._idleDelay = QtCore.QTimer()
-               self._idleDelay.setSingleShot(True)
-               self._idleDelay.setInterval(0)
-               self._idleDelay.timeout.connect(self._on_delayed_start)
-               self._idleDelay.start()
-
-       def load_settings(self):
-               raise NotImplementedError("Booh")
-
-       def save_settings(self):
-               raise NotImplementedError("Booh")
-
-       def _new_main_window(self):
-               raise NotImplementedError("Booh")
-
-       @property
-       def qapp(self):
-               return self._qapp
-
-       @property
-       def constants(self):
-               return self._constants
-
-       @property
-       def errorLog(self):
-               return self._errorLog
-
-       @property
-       def fullscreenAction(self):
-               return self._fullscreenAction
-
-       @property
-       def orientationAction(self):
-               return self._orientationAction
-
-       @property
-       def orientation(self):
-               return self._orientation
-
-       @property
-       def logAction(self):
-               return self._logAction
-
-       @property
-       def aboutAction(self):
-               return self._aboutAction
-
-       @property
-       def quitAction(self):
-               return self._quitAction
-
-       def set_orientation(self, orientation):
-               self._orientation = orientation
-               self._mainWindow.update_orientation(self._orientation)
-
-       @classmethod
-       def _next_orientation(cls, current):
-               return {
-                       cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
-                       cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
-                       cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
-                       cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
-               }[current]
-
-       def _close_windows(self):
-               if self._mainWindow is not None:
-                       self.save_settings()
-                       self._mainWindow.window.destroyed.disconnect(self._on_child_close)
-                       self._mainWindow.close()
-                       self._mainWindow = None
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_delayed_start(self):
-               self._mainWindow.start()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_app_quit(self, checked = False):
-               if self._mainWindow is not None:
-                       self.save_settings()
-                       self._mainWindow.destroy()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_child_close(self, obj = None):
-               if self._mainWindow is not None:
-                       self.save_settings()
-                       self._mainWindow = None
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_toggle_fullscreen(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       self._mainWindow.set_fullscreen(checked)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_next_orientation(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       self.set_orientation(self._next_orientation(self._orientation))
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_about(self, checked = True):
-               raise NotImplementedError("Booh")
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_log(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       with open(self._constants._user_logpath_, "r") as f:
-                               logLines = f.xreadlines()
-                               log = "".join(logLines)
-                               self._clipboard.setText(log)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_quit(self, checked = False):
-               with qui_utils.notify_error(self._errorLog):
-                       self._close_windows()
-
-
-class WindowWrapper(object):
-
-       def __init__(self, parent, app):
-               self._app = app
-
-               self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog)
-
-               self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight)
-               self._layout.setContentsMargins(0, 0, 0, 0)
-
-               self._superLayout = QtGui.QVBoxLayout()
-               self._superLayout.addWidget(self._errorDisplay.toplevel)
-               self._superLayout.setContentsMargins(0, 0, 0, 0)
-               self._superLayout.addLayout(self._layout)
-
-               centralWidget = QtGui.QWidget()
-               centralWidget.setLayout(self._superLayout)
-               centralWidget.setContentsMargins(0, 0, 0, 0)
-
-               self._window = qui_utils.QSignalingMainWindow(parent)
-               self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
-               qui_utils.set_stackable(self._window, True)
-               self._window.setCentralWidget(centralWidget)
-
-               self._closeWindowAction = QtGui.QAction(None)
-               self._closeWindowAction.setText("Close")
-               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
-               self._closeWindowAction.triggered.connect(self._on_close_window)
-
-               self._window.addAction(self._closeWindowAction)
-               self._window.addAction(self._app.quitAction)
-               self._window.addAction(self._app.fullscreenAction)
-               self._window.addAction(self._app.orientationAction)
-               self._window.addAction(self._app.logAction)
-
-       @property
-       def window(self):
-               return self._window
-
-       @property
-       def windowOrientation(self):
-               geom = self._window.size()
-               if geom.width() <= geom.height():
-                       return QtCore.Qt.Vertical
-               else:
-                       return QtCore.Qt.Horizontal
-
-       @property
-       def idealWindowOrientation(self):
-               if self._app.orientation ==  self._app.AUTO_ORIENTATION:
-                       windowOrientation = self.windowOrientation
-               elif self._app.orientation ==  self._app.DEFAULT_ORIENTATION:
-                       windowOrientation = qui_utils.screen_orientation()
-               elif self._app.orientation ==  self._app.LANDSCAPE_ORIENTATION:
-                       windowOrientation = QtCore.Qt.Horizontal
-               elif self._app.orientation ==  self._app.PORTRAIT_ORIENTATION:
-                       windowOrientation = QtCore.Qt.Vertical
-               else:
-                       raise RuntimeError("Bad! No %r for you" % self._app.orientation)
-               return windowOrientation
-
-       def walk_children(self):
-               return ()
-
-       def start(self):
-               pass
-
-       def close(self):
-               for child in self.walk_children():
-                       child.window.destroyed.disconnect(self._on_child_close)
-                       child.close()
-               self._window.close()
-
-       def destroy(self):
-               pass
-
-       def show(self):
-               self._window.show()
-               for child in self.walk_children():
-                       child.show()
-               self.set_fullscreen(self._app.fullscreenAction.isChecked())
-
-       def hide(self):
-               for child in self.walk_children():
-                       child.hide()
-               self._window.hide()
-
-       def set_fullscreen(self, isFullscreen):
-               if self._window.isVisible():
-                       if isFullscreen:
-                               self._window.showFullScreen()
-                       else:
-                               self._window.showNormal()
-               for child in self.walk_children():
-                       child.set_fullscreen(isFullscreen)
-
-       def update_orientation(self, orientation):
-               if orientation == self._app.DEFAULT_ORIENTATION:
-                       qui_utils.set_autorient(self.window, False)
-                       qui_utils.set_window_orientation(self.window, None)
-               elif orientation == self._app.AUTO_ORIENTATION:
-                       qui_utils.set_autorient(self.window, True)
-                       qui_utils.set_window_orientation(self.window, None)
-               elif orientation == self._app.LANDSCAPE_ORIENTATION:
-                       qui_utils.set_autorient(self.window, False)
-                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
-               elif orientation == self._app.PORTRAIT_ORIENTATION:
-                       qui_utils.set_autorient(self.window, False)
-                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
-               else:
-                       raise RuntimeError("Unknown orientation: %r" % orientation)
-               for child in self.walk_children():
-                       child.update_orientation(orientation)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_child_close(self, obj = None):
-               raise NotImplementedError("Booh")
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_close_window(self, checked = True):
-               with qui_utils.notify_error(self._errorLog):
-                       self.close()
-
-
-class AutoFreezeWindowFeature(object):
-
-       def __init__(self, app, window):
-               self._app = app
-               self._window = window
-               self._app.qapp.focusChanged.connect(self._on_focus_changed)
-               if self._app.qapp.focusWidget() is not None:
-                       self._window.setUpdatesEnabled(True)
-               else:
-                       self._window.setUpdatesEnabled(False)
-
-       def close(self):
-               self._app.qapp.focusChanged.disconnect(self._on_focus_changed)
-               self._window.setUpdatesEnabled(True)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_focus_changed(self, oldWindow, newWindow):
-               with qui_utils.notify_error(self._app.errorLog):
-                       if oldWindow is None and newWindow is not None:
-                               self._window.setUpdatesEnabled(True)
-                       elif oldWindow is not None and newWindow is None:
-                               self._window.setUpdatesEnabled(False)
diff --git a/src/util/time_utils.py b/src/util/time_utils.py
deleted file mode 100644 (file)
index 90ec84d..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-from datetime import tzinfo, timedelta, datetime
-
-ZERO = timedelta(0)
-HOUR = timedelta(hours=1)
-
-
-def first_sunday_on_or_after(dt):
-       days_to_go = 6 - dt.weekday()
-       if days_to_go:
-               dt += timedelta(days_to_go)
-       return dt
-
-
-# US DST Rules
-#
-# This is a simplified (i.e., wrong for a few cases) set of rules for US
-# DST start and end times. For a complete and up-to-date set of DST rules
-# and timezone definitions, visit the Olson Database (or try pytz):
-# http://www.twinsun.com/tz/tz-link.htm
-# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
-#
-# In the US, since 2007, DST starts at 2am (standard time) on the second
-# Sunday in March, which is the first Sunday on or after Mar 8.
-DSTSTART_2007 = datetime(1, 3, 8, 2)
-# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
-DSTEND_2007 = datetime(1, 11, 1, 1)
-# From 1987 to 2006, DST used to start at 2am (standard time) on the first
-# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
-# Sunday of October, which is the first Sunday on or after Oct 25.
-DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
-DSTEND_1987_2006 = datetime(1, 10, 25, 1)
-# From 1967 to 1986, DST used to start at 2am (standard time) on the last
-# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
-# 1am standard time) on the last Sunday of October, which is the first Sunday
-# on or after Oct 25.
-DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
-DSTEND_1967_1986 = DSTEND_1987_2006
-
-
-class USTimeZone(tzinfo):
-
-       def __init__(self, hours, reprname, stdname, dstname):
-               self.stdoffset = timedelta(hours=hours)
-               self.reprname = reprname
-               self.stdname = stdname
-               self.dstname = dstname
-
-       def __repr__(self):
-               return self.reprname
-
-       def tzname(self, dt):
-               if self.dst(dt):
-                       return self.dstname
-               else:
-                       return self.stdname
-
-       def utcoffset(self, dt):
-               return self.stdoffset + self.dst(dt)
-
-       def dst(self, dt):
-               if dt is None or dt.tzinfo is None:
-                       # An exception may be sensible here, in one or both cases.
-                       # It depends on how you want to treat them.  The default
-                       # fromutc() implementation (called by the default astimezone()
-                       # implementation) passes a datetime with dt.tzinfo is self.
-                       return ZERO
-               assert dt.tzinfo is self
-
-               # Find start and end times for US DST. For years before 1967, return
-               # ZERO for no DST.
-               if 2006 < dt.year:
-                       dststart, dstend = DSTSTART_2007, DSTEND_2007
-               elif 1986 < dt.year < 2007:
-                       dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
-               elif 1966 < dt.year < 1987:
-                       dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
-               else:
-                       return ZERO
-
-               start = first_sunday_on_or_after(dststart.replace(year=dt.year))
-               end = first_sunday_on_or_after(dstend.replace(year=dt.year))
-
-               # Can't compare naive to aware objects, so strip the timezone from
-               # dt first.
-               if start <= dt.replace(tzinfo=None) < end:
-                       return HOUR
-               else:
-                       return ZERO
-
-
-Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
-Central  = USTimeZone(-6, "Central",  "CST", "CDT")
-Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
-Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")
diff --git a/src/util/tp_utils.py b/src/util/tp_utils.py
deleted file mode 100644 (file)
index 7c55c42..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/usr/bin/env python
-
-import logging
-
-import dbus
-import telepathy
-
-import util.go_utils as gobject_utils
-import misc
-
-
-_moduleLogger = logging.getLogger(__name__)
-DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
-
-
-class WasMissedCall(object):
-
-       def __init__(self, bus, conn, chan, on_success, on_error):
-               self.__on_success = on_success
-               self.__on_error = on_error
-
-               self._requested = None
-               self._didMembersChange = False
-               self._didClose = False
-               self._didReport = False
-
-               self._onTimeout = gobject_utils.Timeout(self._on_timeout)
-               self._onTimeout.start(seconds=60)
-
-               chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal(
-                       "MembersChanged",
-                       self._on_members_changed,
-               )
-
-               chan[telepathy.interfaces.CHANNEL].connect_to_signal(
-                       "Closed",
-                       self._on_closed,
-               )
-
-               chan[DBUS_PROPERTIES].GetAll(
-                       telepathy.interfaces.CHANNEL_INTERFACE,
-                       reply_handler = self._on_got_all,
-                       error_handler = self._on_error,
-               )
-
-       def cancel(self):
-               self._report_error("by request")
-
-       def _report_missed_if_ready(self):
-               if self._didReport:
-                       pass
-               elif self._requested is not None and (self._didMembersChange or self._didClose):
-                       if self._requested:
-                               self._report_error("wrong direction")
-                       elif self._didClose:
-                               self._report_success()
-                       else:
-                               self._report_error("members added")
-               else:
-                       if self._didClose:
-                               self._report_error("closed too early")
-
-       def _report_success(self):
-               assert not self._didReport, "Double reporting a missed call"
-               self._didReport = True
-               self._onTimeout.cancel()
-               self.__on_success(self)
-
-       def _report_error(self, reason):
-               assert not self._didReport, "Double reporting a missed call"
-               self._didReport = True
-               self._onTimeout.cancel()
-               self.__on_error(self, reason)
-
-       @misc.log_exception(_moduleLogger)
-       def _on_got_all(self, properties):
-               self._requested = properties["Requested"]
-               self._report_missed_if_ready()
-
-       @misc.log_exception(_moduleLogger)
-       def _on_members_changed(self, message, added, removed, lp, rp, actor, reason):
-               if added:
-                       self._didMembersChange = True
-                       self._report_missed_if_ready()
-
-       @misc.log_exception(_moduleLogger)
-       def _on_closed(self):
-               self._didClose = True
-               self._report_missed_if_ready()
-
-       @misc.log_exception(_moduleLogger)
-       def _on_error(self, *args):
-               self._report_error(args)
-
-       @misc.log_exception(_moduleLogger)
-       def _on_timeout(self):
-               self._report_error("timeout")
-               return False
-
-
-class NewChannelSignaller(object):
-
-       def __init__(self, on_new_channel):
-               self._sessionBus = dbus.SessionBus()
-               self._on_user_new_channel = on_new_channel
-
-       def start(self):
-               self._sessionBus.add_signal_receiver(
-                       self._on_new_channel,
-                       "NewChannel",
-                       "org.freedesktop.Telepathy.Connection",
-                       None,
-                       None
-               )
-
-       def stop(self):
-               self._sessionBus.remove_signal_receiver(
-                       self._on_new_channel,
-                       "NewChannel",
-                       "org.freedesktop.Telepathy.Connection",
-                       None,
-                       None
-               )
-
-       @misc.log_exception(_moduleLogger)
-       def _on_new_channel(
-               self, channelObjectPath, channelType, handleType, handle, supressHandler
-       ):
-               connObjectPath = channel_path_to_conn_path(channelObjectPath)
-               serviceName = path_to_service_name(channelObjectPath)
-               try:
-                       self._on_user_new_channel(
-                               self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType
-                       )
-               except Exception:
-                       _moduleLogger.exception("Blocking exception from being passed up")
-
-
-class EnableSystemContactIntegration(object):
-
-       ACCOUNT_MGR_NAME = "org.freedesktop.Telepathy.AccountManager"
-       ACCOUNT_MGR_PATH = "/org/freedesktop/Telepathy/AccountManager"
-       ACCOUNT_MGR_IFACE_QUERY = "com.nokia.AccountManager.Interface.Query"
-       ACCOUNT_IFACE_COMPAT = "com.nokia.Account.Interface.Compat"
-       ACCOUNT_IFACE_COMPAT_PROFILE = "com.nokia.Account.Interface.Compat.Profile"
-       DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
-
-       def __init__(self, profileName):
-               self._bus = dbus.SessionBus()
-               self._profileName = profileName
-
-       def start(self):
-               self._accountManager = self._bus.get_object(
-                       self.ACCOUNT_MGR_NAME,
-                       self.ACCOUNT_MGR_PATH,
-               )
-               self._accountManagerQuery = dbus.Interface(
-                       self._accountManager,
-                       dbus_interface=self.ACCOUNT_MGR_IFACE_QUERY,
-               )
-
-               self._accountManagerQuery.FindAccounts(
-                       {
-                               self.ACCOUNT_IFACE_COMPAT_PROFILE: self._profileName,
-                       },
-                       reply_handler = self._on_found_accounts_reply,
-                       error_handler = self._on_error,
-               )
-
-       @misc.log_exception(_moduleLogger)
-       def _on_found_accounts_reply(self, accountObjectPaths):
-               for accountObjectPath in accountObjectPaths:
-                       print accountObjectPath
-                       account = self._bus.get_object(
-                               self.ACCOUNT_MGR_NAME,
-                               accountObjectPath,
-                       )
-                       accountProperties = dbus.Interface(
-                               account,
-                               self.DBUS_PROPERTIES,
-                       )
-                       accountProperties.Set(
-                               self.ACCOUNT_IFACE_COMPAT,
-                               "SecondaryVCardFields",
-                               ["TEL"],
-                               reply_handler = self._on_field_set,
-                               error_handler = self._on_error,
-                       )
-
-       @misc.log_exception(_moduleLogger)
-       def _on_field_set(self):
-               _moduleLogger.info("SecondaryVCardFields Set")
-
-       @misc.log_exception(_moduleLogger)
-       def _on_error(self, error):
-               _moduleLogger.error("%r" % (error, ))
-
-
-def channel_path_to_conn_path(channelObjectPath):
-       """
-       >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
-       '/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME'
-       """
-       return channelObjectPath.rsplit("/", 1)[0]
-
-
-def path_to_service_name(path):
-       """
-       >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
-       'org.freedesktop.Telepathy.ConnectionManager.theonering.gv.USERNAME'
-       """
-       return ".".join(path[1:].split("/")[0:7])
-
-
-def cm_from_path(path):
-       """
-       >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/gv/USERNAME/Channel1")
-       'theonering'
-       """
-       return path[1:].split("/")[4]
diff --git a/support/dialcentral.desktop b/support/dialcentral.desktop
deleted file mode 100644 (file)
index 3b446d7..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-[Desktop Entry]
-Encoding=UTF-8
-Version=1.0
-Type=Application
-Name=DialCentral
-Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py
-Icon=dialcentral
-Categories=Network;InstantMessaging;Qt;
diff --git a/support/icons/hicolor/26x26/hildon/dialcentral.png b/support/icons/hicolor/26x26/hildon/dialcentral.png
deleted file mode 100644 (file)
index df50c66..0000000
Binary files a/support/icons/hicolor/26x26/hildon/dialcentral.png and /dev/null differ
diff --git a/support/icons/hicolor/64x64/hildon/dialcentral.png b/support/icons/hicolor/64x64/hildon/dialcentral.png
deleted file mode 100644 (file)
index 8d98390..0000000
Binary files a/support/icons/hicolor/64x64/hildon/dialcentral.png and /dev/null differ
diff --git a/support/icons/hicolor/scalable/hildon/dialcentral.png b/support/icons/hicolor/scalable/hildon/dialcentral.png
deleted file mode 100644 (file)
index a875350..0000000
Binary files a/support/icons/hicolor/scalable/hildon/dialcentral.png and /dev/null differ