From 215562fb73b7b8756b713c9f803b23d1103fbe28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Adolfo=20Mesa=20Rold=C3=A1n?= Date: Fri, 14 Feb 2020 00:07:55 +0100 Subject: [PATCH] Init --- .gitignore | 42 + AUTHORS | 47 + CHANGELOG.md | 9 + CODE_OF_CONDUCT.md | 46 + CONTRIBUTING.md | 92 + COPYING | 674 ++++ ChangeLog | 1 + INSTALL | 370 ++ LICENSE | 674 ++++ Makefile.am | 5 + NEWS | 359 ++ README | 40 + TRANSLATION_PROGRESS.txt | 40 + _config.yml | 1 + b.sh | 6 + build-po.sh | 41 + build-pot.sh | 54 + compile.sh | 5 + configure.ac | 1401 +++++++ data/Makefile.am | 11 + data/applications/Makefile.am | 5 + data/applications/g15-config.desktop.in | 10 + data/autostart/Makefile.am | 12 + data/autostart/g15-indicator.desktop.in | 20 + data/autostart/g15-systemtray.desktop.in | 20 + data/autostart/gnome15.desktop.in | 21 + data/dbus/Makefile.am | 10 + data/dbus/g15-system-service.conf | 23 + data/dbus/org.gnome15.Gnome15.service | 3 + data/dbus/org.gnome15.SystemService.service | 4 + data/fonts/CyrKoi-VGA8.psf.gz | Bin 0 -> 2053 bytes data/fonts/tom-thumb.pcf | Bin 0 -> 24444 bytes data/icons/AwOken/Makefile.am | 1 + data/icons/AwOken/apps/128/Makefile.am | 5 + data/icons/AwOken/apps/128/gnome15.png | Bin 0 -> 9711 bytes data/icons/AwOken/apps/16/Makefile.am | 5 + data/icons/AwOken/apps/16/gnome15.png | Bin 0 -> 3440 bytes data/icons/AwOken/apps/22/Makefile.am | 5 + data/icons/AwOken/apps/22/gnome15.png | Bin 0 -> 3723 bytes data/icons/AwOken/apps/24/Makefile.am | 5 + data/icons/AwOken/apps/24/gnome15.png | Bin 0 -> 3730 bytes data/icons/AwOken/apps/48/Makefile.am | 5 + data/icons/AwOken/apps/48/gnome15.png | Bin 0 -> 5108 bytes data/icons/AwOken/apps/64/Makefile.am | 5 + data/icons/AwOken/apps/64/gnome15.png | Bin 0 -> 5814 bytes data/icons/AwOken/apps/Makefile.am | 1 + data/icons/AwOken/status/128/Makefile.am | 6 + .../128/logitech-g-keyboard-error-panel.png | Bin 0 -> 9384 bytes .../status/128/logitech-g-keyboard-panel.png | Bin 0 -> 9711 bytes data/icons/AwOken/status/16/Makefile.am | 6 + .../16/logitech-g-keyboard-error-panel.png | Bin 0 -> 3388 bytes .../status/16/logitech-g-keyboard-panel.png | Bin 0 -> 3440 bytes data/icons/AwOken/status/22/Makefile.am | 6 + .../22/logitech-g-keyboard-error-panel.png | Bin 0 -> 3670 bytes .../status/22/logitech-g-keyboard-panel.png | Bin 0 -> 3723 bytes data/icons/AwOken/status/24/Makefile.am | 6 + .../24/logitech-g-keyboard-error-panel.png | Bin 0 -> 3674 bytes .../status/24/logitech-g-keyboard-panel.png | Bin 0 -> 3730 bytes data/icons/AwOken/status/48/Makefile.am | 6 + .../48/logitech-g-keyboard-error-panel.png | Bin 0 -> 5024 bytes .../status/48/logitech-g-keyboard-panel.png | Bin 0 -> 5108 bytes data/icons/AwOken/status/64/Makefile.am | 6 + .../64/logitech-g-keyboard-error-panel.png | Bin 0 -> 5681 bytes .../status/64/logitech-g-keyboard-panel.png | Bin 0 -> 5814 bytes data/icons/AwOken/status/Makefile.am | 1 + data/icons/Makefile.am | 11 + data/icons/elementary/Makefile.am | 1 + data/icons/elementary/status/16/Makefile.am | 6 + .../16/logitech-g-keyboard-error-panel.svg | 225 ++ .../status/16/logitech-g-keyboard-panel.svg | 124 + data/icons/elementary/status/22/Makefile.am | 6 + .../22/logitech-g-keyboard-error-panel.svg | 235 ++ .../status/22/logitech-g-keyboard-panel.svg | 124 + data/icons/elementary/status/24/Makefile.am | 6 + .../24/logitech-g-keyboard-error-panel.svg | 440 +++ .../status/24/logitech-g-keyboard-panel.svg | 124 + data/icons/elementary/status/Makefile.am | 1 + data/icons/hicolor/16x16/Makefile.am | 1 + data/icons/hicolor/16x16/status/Makefile.am | 6 + .../status/logitech-g-keyboard-applet.png | Bin 0 -> 771 bytes .../logitech-g-keyboard-error-applet.png | Bin 0 -> 780 bytes data/icons/hicolor/22x22/Makefile.am | 1 + data/icons/hicolor/22x22/apps/Makefile.am | 5 + data/icons/hicolor/22x22/apps/gnome15.png | Bin 0 -> 872 bytes data/icons/hicolor/22x22/status/Makefile.am | 6 + .../status/logitech-g-keyboard-applet.png | Bin 0 -> 1102 bytes .../logitech-g-keyboard-error-applet.png | Bin 0 -> 1120 bytes data/icons/hicolor/24x24/Makefile.am | 1 + data/icons/hicolor/24x24/apps/Makefile.am | 5 + data/icons/hicolor/24x24/apps/gnome15.png | Bin 0 -> 872 bytes data/icons/hicolor/24x24/status/Makefile.am | 6 + .../status/logitech-g-keyboard-applet.png | Bin 0 -> 1002 bytes .../logitech-g-keyboard-error-applet.png | Bin 0 -> 1024 bytes data/icons/hicolor/48x48/Makefile.am | 1 + data/icons/hicolor/48x48/apps/Makefile.am | 5 + data/icons/hicolor/48x48/apps/gnome15.png | Bin 0 -> 2223 bytes data/icons/hicolor/64x64/Makefile.am | 1 + data/icons/hicolor/64x64/apps/Makefile.am | 5 + data/icons/hicolor/64x64/apps/gnome15.png | Bin 0 -> 3002 bytes data/icons/hicolor/Makefile.am | 1 + data/icons/hicolor/scalable/Makefile.am | 1 + data/icons/hicolor/scalable/apps/Makefile.am | 5 + data/icons/hicolor/scalable/apps/gnome15.svg | 3076 +++++++++++++++ .../hicolor/scalable/devices/Makefile.am | 5 + data/icons/hicolor/scalable/devices/g11.png | Bin 0 -> 65562 bytes data/icons/hicolor/scalable/devices/g110.png | Bin 0 -> 65432 bytes data/icons/hicolor/scalable/devices/g13.png | Bin 0 -> 43162 bytes data/icons/hicolor/scalable/devices/g15v1.png | Bin 0 -> 69706 bytes data/icons/hicolor/scalable/devices/g15v2.png | Bin 0 -> 71827 bytes data/icons/hicolor/scalable/devices/g19.png | Bin 0 -> 63693 bytes data/icons/hicolor/scalable/devices/g35.png | Bin 0 -> 44488 bytes data/icons/hicolor/scalable/devices/g510.png | Bin 0 -> 81868 bytes data/icons/hicolor/scalable/devices/g930.png | Bin 0 -> 67534 bytes .../icons/hicolor/scalable/devices/mx5500.png | Bin 0 -> 54305 bytes data/icons/hicolor/scalable/devices/z10.png | Bin 0 -> 63438 bytes .../icons/hicolor/scalable/status/Makefile.am | 6 + .../logitech-g-keyboard-error-panel.svg | 3076 +++++++++++++++ .../status/logitech-g-keyboard-panel.svg | 3076 +++++++++++++++ data/icons/ubuntu-mono-dark/Makefile.am | 1 + .../ubuntu-mono-dark/status/16/Makefile.am | 6 + .../16/logitech-g-keyboard-error-panel.svg | 279 ++ .../status/16/logitech-g-keyboard-panel.svg | 129 + .../ubuntu-mono-dark/status/22/Makefile.am | 6 + .../22/logitech-g-keyboard-error-panel.svg | 129 + .../status/22/logitech-g-keyboard-panel.svg | 129 + .../ubuntu-mono-dark/status/24/Makefile.am | 6 + .../24/logitech-g-keyboard-error-panel.svg | 134 + .../status/24/logitech-g-keyboard-panel.svg | 129 + .../icons/ubuntu-mono-dark/status/Makefile.am | 1 + data/icons/ubuntu-mono-light/Makefile.am | 1 + .../ubuntu-mono-light/status/16/Makefile.am | 6 + .../16/logitech-g-keyboard-error-panel.svg | 124 + .../status/16/logitech-g-keyboard-panel.svg | 124 + .../ubuntu-mono-light/status/22/Makefile.am | 6 + .../22/logitech-g-keyboard-error-panel.svg | 124 + .../status/22/logitech-g-keyboard-panel.svg | 124 + .../ubuntu-mono-light/status/24/Makefile.am | 6 + .../24/logitech-g-keyboard-error-panel.svg | 136 + .../status/24/logitech-g-keyboard-panel.svg | 136 + .../24/old_logitech-g-keyboard-panel.svg | 70 + .../ubuntu-mono-light/status/Makefile.am | 1 + data/images/Makefile.am | 57 + data/images/default-background.svg | 535 +++ data/images/g15key-error.png | Bin 0 -> 6257 bytes data/images/g15key.png | Bin 0 -> 5105 bytes data/images/g19-background.svg | 574 +++ data/images/key-back.png | Bin 0 -> 421 bytes data/images/key-down.png | Bin 0 -> 405 bytes data/images/key-g1.png | Bin 0 -> 575 bytes data/images/key-g10.png | Bin 0 -> 718 bytes data/images/key-g11.png | Bin 0 -> 587 bytes data/images/key-g12.png | Bin 0 -> 728 bytes data/images/key-g13.png | Bin 0 -> 746 bytes data/images/key-g14.png | Bin 0 -> 654 bytes data/images/key-g15.png | Bin 0 -> 743 bytes data/images/key-g16.png | Bin 0 -> 741 bytes data/images/key-g17.png | Bin 0 -> 704 bytes data/images/key-g18.png | Bin 0 -> 742 bytes data/images/key-g19.png | Bin 0 -> 737 bytes data/images/key-g2.png | Bin 0 -> 681 bytes data/images/key-g20.png | Bin 0 -> 789 bytes data/images/key-g21.png | Bin 0 -> 724 bytes data/images/key-g22.png | Bin 0 -> 694 bytes data/images/key-g3.png | Bin 0 -> 686 bytes data/images/key-g4.png | Bin 0 -> 594 bytes data/images/key-g5.png | Bin 0 -> 679 bytes data/images/key-g6.png | Bin 0 -> 687 bytes data/images/key-g7.png | Bin 0 -> 636 bytes data/images/key-g8.png | Bin 0 -> 670 bytes data/images/key-g9.png | Bin 0 -> 672 bytes data/images/key-l1.png | Bin 0 -> 376 bytes data/images/key-l2.png | Bin 0 -> 473 bytes data/images/key-l3.png | Bin 0 -> 475 bytes data/images/key-l4.png | Bin 0 -> 394 bytes data/images/key-l5.png | Bin 0 -> 473 bytes data/images/key-left.png | Bin 0 -> 406 bytes data/images/key-light.png | Bin 0 -> 651 bytes data/images/key-m1.png | Bin 0 -> 470 bytes data/images/key-m2.png | Bin 0 -> 569 bytes data/images/key-m3.png | Bin 0 -> 581 bytes data/images/key-menu.png | Bin 0 -> 559 bytes data/images/key-mr.png | Bin 0 -> 445 bytes data/images/key-mute.png | Bin 0 -> 594 bytes data/images/key-next.png | Bin 0 -> 530 bytes data/images/key-ok.png | Bin 0 -> 599 bytes data/images/key-play.png | Bin 0 -> 495 bytes data/images/key-prev.png | Bin 0 -> 509 bytes data/images/key-right.png | Bin 0 -> 401 bytes data/images/key-settings.png | Bin 0 -> 694 bytes data/images/key-stop.png | Bin 0 -> 290 bytes data/images/key-up.png | Bin 0 -> 386 bytes data/images/key-vol-down.png | Bin 0 -> 449 bytes data/images/key-vol-up.png | Bin 0 -> 540 bytes data/images/locked.png | Bin 0 -> 411 bytes data/images/mx5500-background.svg | 532 +++ data/themes/Makefile.am | 1 + data/themes/default/Makefile.am | 22 + .../default/default-confirmation-screen.svg | 162 + data/themes/default/default-error-screen.svg | 150 + .../default/default-menu-child-entry.svg | 223 ++ data/themes/default/default-menu-entry.svg | 221 ++ data/themes/default/default-menu-screen.svg | 67 + .../themes/default/default-menu-separator.svg | 122 + .../default/g19-confirmation-screen.svg | 263 ++ data/themes/default/g19-error-screen.svg | 233 ++ data/themes/default/g19-menu-child-entry.svg | 338 ++ data/themes/default/g19-menu-entry.svg | 407 ++ data/themes/default/g19-menu-screen.svg | 262 ++ data/themes/default/g19-menu-separator.svg | 210 + .../default/mx5500-confirmation-screen.svg | 177 + data/themes/default/mx5500-error-screen.svg | 171 + .../default/mx5500-menu-child-entry.svg | 181 + data/themes/default/mx5500-menu-entry.svg | 206 + data/themes/default/mx5500-menu-screen.svg | 146 + data/themes/default/mx5500-menu-separator.svg | 122 + data/udev/98-gnome15.rules.in | 8 + data/udev/99-gnome15-g15direct.rules.in | 30 + data/udev/99-gnome15-g19direct.rules.in | 8 + data/udev/99-gnome15-g930.rules.in | 6 + data/udev/99-gnome15-kernel.rules.in | 39 + data/udev/Makefile.am | 20 + data/ui/Makefile.am | 16 + data/ui/accounts.ui | 409 ++ data/ui/colorpicker.ui | 232 ++ data/ui/driver_g15.ui | 61 + data/ui/driver_g15direct.ui | 234 ++ data/ui/driver_g19.ui | 61 + data/ui/driver_g19direct.ui | 205 + data/ui/driver_g930.ui | 80 + data/ui/driver_gtk.ui | 118 + data/ui/driver_kernel.ui | 206 + data/ui/g15-config.ui | 3393 +++++++++++++++++ data/ui/macro-editor.ui | 901 +++++ data/ui/password.ui | 161 + data/ui/redblue.png | Bin 0 -> 15433 bytes data/ui/script-editor.ui | 1508 ++++++++ data/ui/turbo.png | Bin 0 -> 781 bytes data/ukeys/Makefile.am | 9 + data/ukeys/digital-joystick.keys | 44 + data/ukeys/joystick.keys | 44 + data/ukeys/keyboard.keys | 378 ++ data/ukeys/keysym-to-uinput | 289 ++ data/ukeys/mouse.keys | 18 + data/xcf/AwOkenIcon.xcf | Bin 0 -> 38089 bytes debian/changelog | 5 + debian/compat | 1 + debian/control | 22 + debian/copyright | 56 + debian/rules | 25 + debian/source/format | 1 + debug-g15-config.sh | 5 + debug-g15-systemtray.sh | 5 + debug.sh | 10 + docs/Gnome15.dia | Bin 0 -> 18338 bytes docs/style_guide.md | 84 + example.svg | 8 + i18n/Makefile.am | 30 + i18n/build-po.sh | 10 + i18n/build-pot.sh | 19 + i18n/colorpicker.en_GB.po | 30 + i18n/colorpicker.glade.h | 3 + i18n/colorpicker.pot | 30 + i18n/driver_g15.en_GB.po | 22 + i18n/driver_g15.glade.h | 1 + i18n/driver_g15.pot | 22 + i18n/driver_g15direct.en_GB.po | 42 + i18n/driver_g15direct.glade.h | 6 + i18n/driver_g15direct.pot | 42 + i18n/driver_g19.en_GB.po | 22 + i18n/driver_g19.glade.h | 1 + i18n/driver_g19.pot | 22 + i18n/driver_g19direct.en_GB.po | 34 + i18n/driver_g19direct.glade.h | 4 + i18n/driver_g19direct.pot | 34 + i18n/driver_g930.en_GB.po | 22 + i18n/driver_g930.glade.h | 1 + i18n/driver_g930.pot | 22 + i18n/driver_gtk.en_GB.po | 22 + i18n/driver_gtk.glade.h | 1 + i18n/driver_gtk.pot | 22 + i18n/driver_kernel.en_GB.po | 62 + i18n/driver_kernel.glade.h | 11 + i18n/driver_kernel.pot | 47 + i18n/g15-config.en_GB.po | 330 ++ i18n/g15-config.glade.h | 78 + i18n/g15-config.pot | 324 ++ i18n/gnome15-drivers.en_GB.po | 177 + i18n/gnome15-drivers.pot | 171 + i18n/gnome15.en_GB.po | 187 + i18n/gnome15.pot | 184 + i18n/macro-editor.en_GB.po | 138 + i18n/macro-editor.glade.h | 30 + i18n/macro-editor.pot | 136 + m4/ax_python_devel.m4 | 324 ++ m4/ax_python_module.m4 | 49 + man/Makefile.am | 14 + man/g15-config.1 | 33 + man/g15-desktop-service.1 | 44 + man/g15-indicator.1 | 27 + man/g15-system-service.1 | 35 + man/g15-systemtray.1 | 28 + mksvgheaders.py | 64 + src/Makefile.am | 94 + src/gamewrap/gamewrap | 101 + src/gamewrap/gw/__init__.py | 143 + src/gamewrap/gw/wraplet.py | 31 + src/gamewrap/ut2004.wlet | 58 + src/gnome-shell-extension/Makefile.am | 9 + src/gnome-shell-extension/extension.js | 750 ++++ src/gnome-shell-extension/icons/Makefile.am | 17 + .../icons/logitech-g11-symbolic.svg | 58 + .../icons/logitech-g110-symbolic.svg | 58 + .../icons/logitech-g13-symbolic.svg | 58 + .../icons/logitech-g15v1-symbolic.svg | 58 + .../icons/logitech-g15v2-symbolic.svg | 58 + .../icons/logitech-g19-symbolic.svg | 58 + .../icons/logitech-g35-symbolic.svg | 63 + .../icons/logitech-g510-symbolic.svg | 63 + .../icons/logitech-g930-symbolic.svg | 63 + .../icons/logitech-gamepanel-symbolic.svg | 63 + .../icons/logitech-mx5500-symbolic.svg | 63 + .../icons/logitech-source.svg | 74 + .../icons/logitech-virtual-symbolic.svg | 63 + .../icons/logitech-z10-symbolic.svg | 63 + src/gnome-shell-extension/metadata.json | 13 + src/gnome-shell-extension/stylesheet.css | 14 + src/gnome15/Makefile.am | 43 + src/gnome15/__init__.py | 0 src/gnome15/colorpicker.py | 253 ++ src/gnome15/dbusmenu.py | 198 + src/gnome15/drivers/Makefile.am | 28 + src/gnome15/drivers/__init__.py | 0 src/gnome15/drivers/driver_g15direct.py | 691 ++++ src/gnome15/drivers/driver_g19direct.py | 367 ++ src/gnome15/drivers/driver_g930.py | 291 ++ src/gnome15/drivers/driver_gtk.py | 428 +++ src/gnome15/drivers/driver_kernel.py | 1432 +++++++ src/gnome15/drivers/driver_mx5500.py | 406 ++ src/gnome15/drivers/fb.py | 191 + src/gnome15/drivers/pylibg15.py | 187 + src/gnome15/g15accounts.py | 538 +++ src/gnome15/g15actions.py | 72 + src/gnome15/g15config.py | 1993 ++++++++++ src/gnome15/g15dbus.py | 988 +++++ src/gnome15/g15dconf.py | 122 + src/gnome15/g15debug.py | 34 + src/gnome15/g15desktop.py | 1540 ++++++++ src/gnome15/g15devices.py | 485 +++ src/gnome15/g15driver.py | 794 ++++ src/gnome15/g15drivermanager.py | 118 + src/gnome15/g15exceptions.py | 27 + src/gnome15/g15globals.py.in | 53 + src/gnome15/g15gtk.py | 287 ++ src/gnome15/g15keyboard.py | 661 ++++ src/gnome15/g15keyio.py | 206 + src/gnome15/g15locale.py | 241 ++ src/gnome15/g15logging.py | 51 + src/gnome15/g15macroeditor.py | 1104 ++++++ src/gnome15/g15network.py | 79 + src/gnome15/g15notify.py | 62 + src/gnome15/g15plugin.py | 426 +++ src/gnome15/g15pluginmanager.py | 574 +++ src/gnome15/g15profile.py | 1173 ++++++ src/gnome15/g15screen.py | 1593 ++++++++ src/gnome15/g15service.py | 1138 ++++++ src/gnome15/g15system.py | 229 ++ src/gnome15/g15text.py | 161 + src/gnome15/g15theme.py | 2227 +++++++++++ src/gnome15/g15top.py | 206 + src/gnome15/g15uinput.py | 403 ++ src/gnome15/g15upgrade.py | 116 + src/gnome15/g15util.py | 287 ++ src/gnome15/lcdsink.py | 92 + src/gnome15/objgraph.py | 399 ++ src/gnome15/util/Makefile.am | 17 + src/gnome15/util/__init__.py | 0 src/gnome15/util/g15cairo.py | 289 ++ src/gnome15/util/g15convert.py | 85 + src/gnome15/util/g15gconf.py | 121 + src/gnome15/util/g15icontools.py | 137 + src/gnome15/util/g15markup.py | 48 + src/gnome15/util/g15os.py | 133 + src/gnome15/util/g15pythonlang.py | 186 + src/gnome15/util/g15scheduler.py | 61 + src/gnome15/util/g15svg.py | 152 + src/gnome15/util/g15uigconf.py | 278 ++ src/gnome15/util/jobqueue.py | 287 ++ src/libimpulse/Impulse.c | 285 ++ src/libimpulse/Impulse.h | 32 + src/libimpulse/Makefile.am | 14 + src/libimpulse/impulsemodule.c | 88 + src/libimpulse/test-libimpulse.c | 42 + src/plugins/Makefile.am | 163 + src/plugins/background/Makefile.am | 8 + src/plugins/background/background-160x43.png | Bin 0 -> 4197 bytes src/plugins/background/background-320x240.png | Bin 0 -> 87155 bytes src/plugins/background/background.py | 317 ++ src/plugins/background/background.ui | 268 ++ src/plugins/background/background2.py | 284 ++ .../background/i18n/background.en_GB.po | 66 + .../background/i18n/background.glade.h | 12 + src/plugins/background/i18n/background.pot | 66 + src/plugins/backlight/Makefile.am | 6 + src/plugins/backlight/backlight.py | 115 + src/plugins/backlight/default/Makefile.am | 5 + src/plugins/backlight/default/g19.svg | 286 ++ src/plugins/cairo-clock/Makefile.am | 8 + src/plugins/cairo-clock/cairo-clock.py | 440 +++ src/plugins/cairo-clock/cairo-clock.ui | 244 ++ src/plugins/cairo-clock/g15/Makefile.am | 1 + .../cairo-clock/g15/default/Makefile.am | 10 + .../cairo-clock/g15/default/clock-frame.gif | Bin 0 -> 168 bytes .../cairo-clock/g15/default/clock-glass.gif | Bin 0 -> 127 bytes .../g15/default/clock-hour-hand.gif | Bin 0 -> 103 bytes .../cairo-clock/g15/default/clock-marks.gif | Bin 0 -> 135 bytes .../g15/default/clock-minute-hand.gif | Bin 0 -> 99 bytes .../g15/default/clock-second-hand.gif | Bin 0 -> 101 bytes src/plugins/cairo-clock/g19/Makefile.am | 1 + .../cairo-clock/g19/default/Makefile.am | 16 + .../g19/default/clock-drop-shadow.svg | 144 + .../g19/default/clock-face-shadow.svg | 138 + .../cairo-clock/g19/default/clock-face.svg | 128 + .../cairo-clock/g19/default/clock-frame.svg | 221 ++ .../cairo-clock/g19/default/clock-glass.svg | 112 + .../g19/default/clock-hour-hand-shadow.svg | 125 + .../g19/default/clock-hour-hand.svg | 135 + .../cairo-clock/g19/default/clock-marks.svg | 1163 ++++++ .../g19/default/clock-minute-hand-shadow.svg | 125 + .../g19/default/clock-minute-hand.svg | 125 + .../g19/default/clock-second-hand-shadow.svg | 140 + .../g19/default/clock-second-hand.svg | 158 + .../cairo-clock/i18n/cairo-clock.en_GB.po | 54 + .../cairo-clock/i18n/cairo-clock.glade.h | 9 + src/plugins/cairo-clock/i18n/cairo-clock.pot | 54 + src/plugins/cairo-clock/mx5500/Makefile.am | 1 + .../cairo-clock/mx5500/default/Makefile.am | 10 + .../mx5500/default/clock-frame.gif | Bin 0 -> 151 bytes .../mx5500/default/clock-glass.gif | Bin 0 -> 88 bytes .../mx5500/default/clock-hour-hand.gif | Bin 0 -> 87 bytes .../mx5500/default/clock-marks.gif | Bin 0 -> 132 bytes .../mx5500/default/clock-minute-hand.gif | Bin 0 -> 85 bytes .../mx5500/default/clock-second-hand.gif | Bin 0 -> 86 bytes src/plugins/cal-evolution/Makefile.am | 7 + src/plugins/cal-evolution/cal-evolution.py | 131 + src/plugins/cal-evolution/cal-evolution.ui | 26 + src/plugins/cal-evolution/icon.png | Bin 0 -> 665 bytes src/plugins/cal-google/Makefile.am | 8 + src/plugins/cal-google/cal-google.py | 172 + src/plugins/cal-google/cal-google.ui | 97 + src/plugins/cal-google/icon.png | Bin 0 -> 1258 bytes src/plugins/cal-google/iso8601.py | 121 + src/plugins/cal/Makefile.am | 8 + src/plugins/cal/bell.gif | Bin 0 -> 59 bytes src/plugins/cal/cal.py | 516 +++ src/plugins/cal/cal.ui | 31 + src/plugins/cal/default/Makefile.am | 29 + src/plugins/cal/default/default-cell.svg | 145 + .../cal/default/default-menu-entry.svg | 200 + src/plugins/cal/default/default.svg | 313 ++ src/plugins/cal/default/g19-cell.svg | 133 + src/plugins/cal/default/g19-menu-entry.svg | 361 ++ src/plugins/cal/default/g19.svg | 528 +++ src/plugins/cal/i18n/cal.en_GB.po | 54 + src/plugins/cal/i18n/cal.pot | 52 + src/plugins/clock/Makefile.am | 8 + src/plugins/clock/clock.py | 366 ++ src/plugins/clock/clock.ui | 106 + src/plugins/clock/default/Makefile.am | 29 + .../clock/default/default-with-date.svg | 88 + src/plugins/clock/default/default.svg | 87 + src/plugins/clock/default/g19-with-date.svg | 109 + src/plugins/clock/default/g19.svg | 87 + .../clock/default/mx5500-with-date.svg | 488 +++ src/plugins/clock/default/mx5500.svg | 467 +++ src/plugins/clock/i18n/clock.en_GB.po | 30 + src/plugins/clock/i18n/clock.glade.h | 3 + src/plugins/clock/i18n/clock.pot | 30 + src/plugins/debug/Makefile.am | 7 + src/plugins/debug/debug.py | 473 +++ src/plugins/debug/default/Makefile.am | 25 + src/plugins/debug/default/default.svg | 132 + src/plugins/debug/default/g19.svg | 131 + src/plugins/debug/i18n/clock.en_GB.po | 30 + src/plugins/debug/i18n/clock.glade.h | 3 + src/plugins/debug/i18n/clock.pot | 30 + src/plugins/display/Makefile.am | 5 + src/plugins/display/display.py | 186 + src/plugins/fx/Makefile.am | 6 + src/plugins/fx/fx.py | 207 + src/plugins/fx/fx.ui | 192 + src/plugins/fx/i18n/fx.en_GB.po | 54 + src/plugins/fx/i18n/fx.glade.h | 9 + src/plugins/fx/i18n/fx.pot | 54 + src/plugins/g15daemon-server/Makefile.am | 6 + .../g15daemon-server/g15daemon-server.py | 493 +++ .../g15daemon-server/g15daemon-server.ui | 176 + .../i18n/g15daemon-server.en_GB.po | 30 + .../i18n/g15daemon-server.glade.h | 3 + .../i18n/g15daemon-server.pot | 30 + src/plugins/game-nexuiz/Makefile.am | 28 + src/plugins/game-nexuiz/default/Makefile.am | 25 + src/plugins/game-nexuiz/default/default.svg | 78 + src/plugins/game-nexuiz/default/g19.svg | 156 + .../game-nexuiz/game-nexuiz.g13.macros | 122 + .../game-nexuiz/game-nexuiz.g19.macros | 122 + src/plugins/game-nexuiz/game-nexuiz.py | 79 + src/plugins/game-nexuiz/resources/Makefile.am | 8 + .../game-nexuiz/resources/g19-background.jpg | Bin 0 -> 32075 bytes src/plugins/game-nexuiz/resources/icon.png | Bin 0 -> 8064 bytes src/plugins/google-analytics/Makefile.am | 7 + .../google-analytics/default/Makefile.am | 25 + .../google-analytics/default/default.svg | 287 ++ src/plugins/google-analytics/default/g19.svg | 351 ++ .../google-analytics/google-analytics.py | 376 ++ .../google-analytics/google-analytics.ui | 60 + src/plugins/im/Makefile.am | 5 + src/plugins/im/i18n/im.en_GB.po | 92 + src/plugins/im/i18n/im.pot | 89 + src/plugins/im/im.py | 544 +++ src/plugins/impulse15/Makefile.am | 29 + src/plugins/impulse15/i18n/impulse15.en_GB.po | 106 + src/plugins/impulse15/i18n/impulse15.glade.h | 22 + src/plugins/impulse15/i18n/impulse15.pot | 106 + src/plugins/impulse15/impulse15.py | 398 ++ src/plugins/impulse15/impulse15.ui | 632 +++ src/plugins/impulse15/themes/Makefile.am | 2 + .../impulse15/themes/circlelcd/Makefile.am | 5 + .../impulse15/themes/circlelcd/__init__.py | 53 + .../impulse15/themes/circlelcd/theme.conf | 7 + .../impulse15/themes/circleline/Makefile.am | 5 + .../impulse15/themes/circleline/__init__.py | 66 + .../impulse15/themes/circleline/theme.conf | 7 + .../impulse15/themes/default/Makefile.am | 5 + .../impulse15/themes/default/__init__.py | 67 + .../impulse15/themes/default/theme.conf | 9 + .../impulse15/themes/original/Makefile.am | 5 + .../impulse15/themes/original/__init__.py | 44 + .../impulse15/themes/original/theme.conf | 7 + src/plugins/indicator-messages/Makefile.am | 8 + .../indicator-messages/default/Makefile.am | 12 + .../default/default-entry.svg | 150 + .../indicator-messages/default/default.svg | 91 + .../indicator-messages/default/g19-entry.svg | 294 ++ .../default/g19-separator.svg | 210 + .../indicator-messages/default/g19.svg | 166 + .../indicator_messages_default_common.py | 56 + .../indicator_messages_default_default.py | 10 + .../default/indicator_messages_default_g19.py | 9 + .../i18n/indicator-messages.en_GB.po | 46 + .../i18n/indicator-messages.glade.h | 7 + .../i18n/indicator-messages.pot | 46 + .../indicator-messages/indicator-messages.py | 258 ++ .../indicator-messages/indicator-messages.ui | 101 + .../indicator-messages/mono-mail-error.gif | Bin 0 -> 72 bytes .../indicator-messages/mono-mail-new.gif | Bin 0 -> 78 bytes src/plugins/keyhelp/Makefile.am | 6 + src/plugins/keyhelp/default/Makefile.am | 8 + .../keyhelp/default/default-menu-entry.svg | 229 ++ .../keyhelp/default/default-menu-screen.svg | 152 + src/plugins/keyhelp/default/g19.svg | 487 +++ src/plugins/keyhelp/keyhelp.py | 125 + src/plugins/lcdbiff/Makefile.am | 13 + src/plugins/lcdbiff/default/Makefile.am | 26 + .../lcdbiff/default/default-menu-entry.svg | 226 ++ src/plugins/lcdbiff/i18n/imap.en_GB.po | 48 + src/plugins/lcdbiff/i18n/imap.glade.h | 8 + src/plugins/lcdbiff/i18n/imap.pot | 45 + src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po | 62 + src/plugins/lcdbiff/i18n/lcdbiff.glade.h | 11 + src/plugins/lcdbiff/i18n/lcdbiff.pot | 62 + src/plugins/lcdbiff/i18n/password.en_GB.po | 38 + src/plugins/lcdbiff/i18n/password.glade.h | 5 + src/plugins/lcdbiff/i18n/password.pot | 38 + src/plugins/lcdbiff/i18n/pop3.en_GB.po | 44 + src/plugins/lcdbiff/i18n/pop3.glade.h | 7 + src/plugins/lcdbiff/i18n/pop3.pot | 41 + src/plugins/lcdbiff/imap.ui | 175 + src/plugins/lcdbiff/lcdbiff.py | 517 +++ src/plugins/lcdbiff/mono-mail-error.gif | Bin 0 -> 72 bytes src/plugins/lcdbiff/mono-mail-new.gif | Bin 0 -> 78 bytes src/plugins/lcdbiff/mono-mail-refresh.gif | Bin 0 -> 84 bytes src/plugins/lcdbiff/password.ui | 159 + src/plugins/lcdbiff/pop3.ui | 122 + src/plugins/lcdshot/Makefile.am | 6 + src/plugins/lcdshot/i18n/lcdshot.en_GB.po | 50 + src/plugins/lcdshot/i18n/lcdshot.glade.h | 8 + src/plugins/lcdshot/i18n/lcdshot.pot | 50 + src/plugins/lcdshot/lcdshot.py | 240 ++ src/plugins/lcdshot/lcdshot.ui | 200 + src/plugins/lens/Makefile.am | 11 + src/plugins/lens/gnome15.lens | 15 + src/plugins/lens/gnome15.svg | 3076 +++++++++++++++ src/plugins/lens/lens.py | 280 ++ src/plugins/macro-recorder/Makefile.am | 6 + .../macro-recorder/default/Makefile.am | 6 + .../macro-recorder/default/default.svg | 157 + src/plugins/macro-recorder/default/g19.svg | 173 + .../i18n/macro-recorder.en_GB.po | 60 + .../macro-recorder/i18n/macro-recorder.pot | 53 + src/plugins/macro-recorder/macro-recorder.py | 334 ++ src/plugins/macros/Makefile.am | 6 + src/plugins/macros/default/Makefile.am | 8 + .../macros/default/default-menu-entry.svg | 229 ++ .../macros/default/default-menu-screen.svg | 152 + src/plugins/macros/default/g19-menu-entry.svg | 449 +++ .../macros/default/g19-menu-screen.svg | 237 ++ src/plugins/macros/i18n/macros.en_GB.po | 46 + src/plugins/macros/i18n/macros.glade.h | 7 + src/plugins/macros/i18n/macros.pot | 46 + src/plugins/macros/macros.py | 218 ++ src/plugins/macros/macros.ui | 101 + src/plugins/mediaplayer/Makefile.am | 7 + src/plugins/mediaplayer/default/Makefile.am | 8 + src/plugins/mediaplayer/default/WIPg19.svg | 939 +++++ .../mediaplayer/default/default-mediakeys.svg | 987 +++++ src/plugins/mediaplayer/default/default.svg | 394 ++ .../mediaplayer/default/g19-mediakeys.svg | 1224 ++++++ src/plugins/mediaplayer/default/g19.svg | 1178 ++++++ src/plugins/mediaplayer/gtkplayer.py | 72 + .../mediaplayer/i18n/videoplayer.en_GB.po | 64 + src/plugins/mediaplayer/i18n/videoplayer.pot | 61 + src/plugins/mediaplayer/mediaplayer.py | 1077 ++++++ src/plugins/mediaplayer/oldvideoplayer.py | 334 ++ src/plugins/menu/Makefile.am | 5 + src/plugins/menu/i18n/menu.en_GB.po | 56 + src/plugins/menu/i18n/menu.pot | 55 + src/plugins/menu/menu.py | 185 + src/plugins/mounts/Makefile.am | 7 + src/plugins/mounts/default/Makefile.am | 7 + .../mounts/default/default-menu-entry.svg | 237 ++ src/plugins/mounts/default/g19-menu-entry.svg | 588 +++ src/plugins/mounts/i18n/mounts.en_GB.po | 46 + src/plugins/mounts/i18n/mounts.glade.h | 7 + src/plugins/mounts/i18n/mounts.pot | 46 + src/plugins/mounts/mounts.py | 345 ++ src/plugins/mounts/mounts.ui | 100 + src/plugins/mpris/Makefile.am | 6 + src/plugins/mpris/bigcover/Makefile.am | 6 + src/plugins/mpris/bigcover/bigcover.theme | 4 + src/plugins/mpris/bigcover/g19.svg | 427 +++ src/plugins/mpris/default/Makefile.am | 9 + src/plugins/mpris/default/default.svg | 254 ++ src/plugins/mpris/default/g19.svg | 542 +++ src/plugins/mpris/default/mx5500.svg | 241 ++ src/plugins/mpris/default/pause.gif | Bin 0 -> 71 bytes src/plugins/mpris/default/play.gif | Bin 0 -> 48 bytes src/plugins/mpris/i18n/mpris.en_GB.po | 42 + src/plugins/mpris/i18n/mpris.pot | 38 + src/plugins/mpris/mpris.py | 768 ++++ src/plugins/mpris/mpris.ui | 79 + src/plugins/nm/Makefile.am | 6 + src/plugins/nm/default/Makefile.am | 7 + src/plugins/nm/default/default.svg | 91 + src/plugins/nm/default/g19.svg | 166 + src/plugins/nm/nm.py | 67 + src/plugins/notify-lcd/Makefile.am | 7 + src/plugins/notify-lcd/default/Makefile.am | 10 + .../notify-lcd/default/default-nobody.svg | 168 + src/plugins/notify-lcd/default/default.svg | 196 + src/plugins/notify-lcd/default/g19-nobody.svg | 274 ++ src/plugins/notify-lcd/default/g19.svg | 316 ++ .../notify-lcd/default/mx5500-nobody.svg | 156 + src/plugins/notify-lcd/default/mx5500.svg | 184 + .../notify-lcd/i18n/notify-lcd.en_GB.po | 74 + .../notify-lcd/i18n/notify-lcd.glade.h | 14 + src/plugins/notify-lcd/i18n/notify-lcd.pot | 74 + src/plugins/notify-lcd/notify-lcd.py | 645 ++++ src/plugins/notify-lcd/notify-lcd.ui | 352 ++ src/plugins/notify-lcd2/Makefile.am | 7 + src/plugins/notify-lcd2/default/Makefile.am | 8 + .../notify-lcd2/default/default-nobody.svg | 160 + src/plugins/notify-lcd2/default/default.svg | 176 + .../notify-lcd2/default/g19-nobody.svg | 258 ++ src/plugins/notify-lcd2/default/g19.svg | 281 ++ src/plugins/notify-lcd2/notify-lcd.ui | 331 ++ src/plugins/notify-lcd2/notify-lcd2.py | 528 +++ src/plugins/panel/Makefile.am | 6 + src/plugins/panel/i18n/panel.en_GB.po | 54 + src/plugins/panel/i18n/panel.glade.h | 9 + src/plugins/panel/i18n/panel.pot | 54 + src/plugins/panel/panel.py | 220 ++ src/plugins/panel/panel.ui | 213 ++ src/plugins/pommodoro/Machovka_tomato.png | Bin 0 -> 7776 bytes .../pommodoro/Machovka_tomato_green.png | Bin 0 -> 6391 bytes src/plugins/pommodoro/Makefile.am | 13 + src/plugins/pommodoro/default/Makefile.am | 28 + .../pommodoro/default/default-timerover.svg | 161 + src/plugins/pommodoro/default/default.svg | 202 + .../pommodoro/default/g19-timerover.svg | 368 ++ src/plugins/pommodoro/default/g19.svg | 449 +++ src/plugins/pommodoro/pommodoro.py | 525 +++ src/plugins/pommodoro/pommodoro.ui | 206 + src/plugins/pommodoro/tomato_1bpp.png | Bin 0 -> 133 bytes src/plugins/pommodoro/tomato_empty_1bpp.png | Bin 0 -> 129 bytes src/plugins/ppastats/Makefile.am | 7 + src/plugins/ppastats/default/Makefile.am | 8 + .../ppastats/default/default-menu-entry.svg | 191 + src/plugins/ppastats/default/default.svg | 127 + .../ppastats/default/g19-menu-entry.svg | 160 + src/plugins/ppastats/default/g19.svg | 233 ++ src/plugins/ppastats/ppastats.py | 231 ++ src/plugins/ppastats/ppastats.ui | 271 ++ src/plugins/processes/Makefile.am | 5 + src/plugins/processes/i18n/processes.en_GB.po | 99 + src/plugins/processes/i18n/processes.pot | 93 + src/plugins/processes/processes.py | 392 ++ src/plugins/profiles/Makefile.am | 5 + src/plugins/profiles/bw-locked-inverted.gif | Bin 0 -> 54 bytes src/plugins/profiles/bw-locked.gif | Bin 0 -> 79 bytes src/plugins/profiles/profiles.py | 213 ++ src/plugins/rss/Makefile.am | 7 + src/plugins/rss/default/Makefile.am | 9 + .../rss/default/default-menu-entry.svg | 205 + .../rss/default/default-menu-screen.svg | 131 + src/plugins/rss/default/g19-menu-entry.svg | 161 + src/plugins/rss/default/g19-menu-screen.svg | 245 ++ src/plugins/rss/default/mx5500-menu-entry.svg | 215 ++ src/plugins/rss/i18n/rss.en_GB.po | 46 + src/plugins/rss/i18n/rss.glade.h | 7 + src/plugins/rss/i18n/rss.pot | 46 + src/plugins/rss/rss.py | 379 ++ src/plugins/rss/rss.ui | 299 ++ src/plugins/runapp/Makefile.am | 5 + src/plugins/runapp/background-160x43.png | Bin 0 -> 4197 bytes src/plugins/runapp/background-320x240.png | Bin 0 -> 87155 bytes src/plugins/runapp/default/default.svg | 248 ++ src/plugins/runapp/i18n/background.en_GB.po | 66 + src/plugins/runapp/i18n/background.glade.h | 12 + src/plugins/runapp/i18n/background.pot | 66 + src/plugins/runapp/runapp.py | 149 + src/plugins/screensaver/Makefile.am | 7 + src/plugins/screensaver/default/Makefile.am | 10 + .../screensaver/default/default-nobody.svg | 94 + src/plugins/screensaver/default/default.svg | 122 + .../screensaver/default/g19-nobody.svg | 122 + src/plugins/screensaver/default/g19.svg | 172 + .../screensaver/default/mx5500-nobody.svg | 103 + src/plugins/screensaver/default/mx5500.svg | 144 + .../screensaver/i18n/screensaver.en_GB.po | 34 + .../screensaver/i18n/screensaver.glade.h | 4 + src/plugins/screensaver/i18n/screensaver.pot | 34 + src/plugins/screensaver/screensaver.py | 219 ++ src/plugins/screensaver/screensaver.ui | 153 + src/plugins/sense/Makefile.am | 7 + src/plugins/sense/default/Makefile.am | 14 + src/plugins/sense/default/default-fan.svg | 254 ++ .../sense/default/default-menu-entry.svg | 272 ++ src/plugins/sense/default/default-none.svg | 100 + src/plugins/sense/default/default-volt.svg | 250 ++ src/plugins/sense/default/default.svg | 248 ++ src/plugins/sense/default/g19-fan.svg | 1268 ++++++ src/plugins/sense/default/g19-menu-entry.svg | 353 ++ src/plugins/sense/default/g19-none.svg | 693 ++++ src/plugins/sense/default/g19-volt.svg | 1269 ++++++ src/plugins/sense/default/g19.svg | 1358 +++++++ .../sense/default/i18n/default-none.en_GB.po | 22 + src/plugins/sense/default/i18n/default-none.h | 1 + .../sense/default/i18n/default-none.pot | 22 + .../sense/default/i18n/g19-none.en_GB.po | 22 + src/plugins/sense/default/i18n/g19-none.h | 1 + src/plugins/sense/default/i18n/g19-none.pot | 22 + src/plugins/sense/i18n/sense.en_GB.po | 46 + src/plugins/sense/i18n/sense.glade.h | 7 + src/plugins/sense/i18n/sense.pot | 46 + src/plugins/sense/sense.py | 566 +++ src/plugins/sense/sense.ui | 217 ++ src/plugins/stopwatch/Makefile.am | 11 + src/plugins/stopwatch/default/Makefile.am | 36 + .../stopwatch/default/default-one_timer.svg | 142 + .../stopwatch/default/default-two_timers.svg | 235 ++ src/plugins/stopwatch/default/default.svg | 77 + .../stopwatch/default/g19-one_timer.svg | 265 ++ .../stopwatch/default/g19-two_timers.svg | 464 +++ src/plugins/stopwatch/default/g19.svg | 77 + .../stopwatch/default/i18n/default.en_GB.po | 22 + src/plugins/stopwatch/default/i18n/default.h | 1 + .../stopwatch/default/i18n/default.pot | 22 + .../stopwatch/default/i18n/g19.en_GB.po | 22 + src/plugins/stopwatch/default/i18n/g19.h | 1 + src/plugins/stopwatch/default/i18n/g19.pot | 22 + .../stopwatch/default/i18n/mx5500.en_GB.po | 22 + src/plugins/stopwatch/default/i18n/mx5500.h | 1 + src/plugins/stopwatch/default/i18n/mx5500.pot | 22 + .../stopwatch/default/mx5500-one_timer.svg | 127 + .../stopwatch/default/mx5500-two_timers.svg | 177 + src/plugins/stopwatch/default/mx5500.svg | 77 + src/plugins/stopwatch/default/playpause.gif | Bin 0 -> 82 bytes src/plugins/stopwatch/default/reset.gif | Bin 0 -> 77 bytes src/plugins/stopwatch/default/up.gif | Bin 0 -> 71 bytes src/plugins/stopwatch/i18n/stopwatch.en_GB.po | 62 + src/plugins/stopwatch/i18n/stopwatch.glade.h | 11 + src/plugins/stopwatch/i18n/stopwatch.pot | 62 + src/plugins/stopwatch/preferences.py | 97 + src/plugins/stopwatch/stopwatch.py | 296 ++ src/plugins/stopwatch/stopwatch.ui | 625 +++ src/plugins/stopwatch/timer.py | 91 + src/plugins/sysmon/Makefile.am | 7 + src/plugins/sysmon/default/Makefile.am | 7 + src/plugins/sysmon/default/default.svg | 224 ++ src/plugins/sysmon/default/g19.svg | 482 +++ .../sysmon/default/i18n/default.en_GB.po | 0 src/plugins/sysmon/default/i18n/default.pot | 0 src/plugins/sysmon/default/i18n/g19.en_GB.po | 0 src/plugins/sysmon/default/i18n/g19.pot | 0 .../sysmon/default/i18n/mx5500.en_GB.po | 0 src/plugins/sysmon/default/i18n/mx5500.pot | 0 src/plugins/sysmon/default/mx5500.svg | 200 + src/plugins/sysmon/dials/Makefile.am | 9 + src/plugins/sysmon/dials/g19-large-needle.svg | 139 + src/plugins/sysmon/dials/g19-small-needle.svg | 130 + src/plugins/sysmon/dials/g19-tiny-needle.svg | 140 + src/plugins/sysmon/dials/g19.svg | 1024 +++++ src/plugins/sysmon/dials/sysmon_dials_g19.py | 52 + src/plugins/sysmon/graphs/Makefile.am | 8 + src/plugins/sysmon/graphs/default.svg | 215 ++ src/plugins/sysmon/graphs/g19.svg | 358 ++ src/plugins/sysmon/graphs/graphs.theme | 7 + src/plugins/sysmon/graphs/i18n/g19.en_GB.po | 22 + src/plugins/sysmon/graphs/i18n/g19.h | 1 + src/plugins/sysmon/graphs/i18n/g19.pot | 22 + .../sysmon/graphs/sysmon_graphs_default.py | 156 + src/plugins/sysmon/i18n/sysmon.en_GB.po | 26 + src/plugins/sysmon/i18n/sysmon.glade.h | 2 + src/plugins/sysmon/i18n/sysmon.pot | 26 + src/plugins/sysmon/sysmon.py | 472 +++ src/plugins/sysmon/sysmon.ui | 66 + src/plugins/tails/LICENSE | 165 + src/plugins/tails/Makefile.am | 9 + src/plugins/tails/README | 53 + src/plugins/tails/default/Makefile.am | 8 + .../tails/default/default-menu-entry.svg | 203 + .../tails/default/default-menu-screen.svg | 145 + src/plugins/tails/default/g19-menu-entry.svg | 139 + src/plugins/tails/default/g19-menu-screen.svg | 234 ++ src/plugins/tails/i18n/tails.en_GB.po | 46 + src/plugins/tails/i18n/tails.glade.h | 7 + src/plugins/tails/i18n/tails.pot | 46 + src/plugins/tails/tailer/Makefile.am | 5 + src/plugins/tails/tailer/__init__.py | 300 ++ src/plugins/tails/tails.py | 356 ++ src/plugins/tails/tails.ui | 272 ++ src/plugins/things/Makefile.am | 9 + src/plugins/things/cg.stuff/Makefile.am | 5 + src/plugins/things/cg.stuff/cairo.svg | 554 +++ src/plugins/things/clouds.stuff/Makefile.am | 6 + src/plugins/things/clouds.stuff/README | 5 + src/plugins/things/clouds.stuff/clouds.svg | 1355 +++++++ src/plugins/things/cloudsthingum.py | 130 + src/plugins/things/test1.py | 341 ++ src/plugins/things/things.py | 164 + src/plugins/trafficstats/Makefile.am | 10 + src/plugins/trafficstats/default/Makefile.am | 27 + src/plugins/trafficstats/default/default.svg | 282 ++ src/plugins/trafficstats/default/g19.svg | 303 ++ src/plugins/trafficstats/default/mx5500.svg | 679 ++++ src/plugins/trafficstats/trafficstats.png | Bin 0 -> 489 bytes src/plugins/trafficstats/trafficstats.py | 307 ++ src/plugins/trafficstats/trafficstats.ui | 191 + src/plugins/tweak/Makefile.am | 6 + src/plugins/tweak/i18n/tweak.en_GB.po | 110 + src/plugins/tweak/i18n/tweak.glade.h | 25 + src/plugins/tweak/i18n/tweak.pot | 102 + src/plugins/tweak/tweak.py | 62 + src/plugins/tweak/tweak.ui | 596 +++ src/plugins/voip-mumble/Makefile.am | 6 + src/plugins/voip-mumble/logo.png | Bin 0 -> 15041 bytes src/plugins/voip-mumble/voip-mumble.py | 115 + src/plugins/voip-teamspeak3/Makefile.am | 8 + src/plugins/voip-teamspeak3/logo.png | Bin 0 -> 15041 bytes src/plugins/voip-teamspeak3/test.py | 34 + src/plugins/voip-teamspeak3/ts3/Makefile.am | 8 + src/plugins/voip-teamspeak3/ts3/__init__.py | 206 + src/plugins/voip-teamspeak3/ts3/message.py | 226 ++ .../voip-teamspeak3/voip-teamspeak3.py | 673 ++++ src/plugins/voip/Makefile.am | 18 + src/plugins/voip/buddies-only/Makefile.am | 31 + .../voip/buddies-only/buddies-only.theme | 4 + .../voip/buddies-only/default-menu-entry.svg | 296 ++ .../voip/buddies-only/default-menu-screen.svg | 215 ++ .../voip/buddies-only/g19-menu-entry.svg | 411 ++ .../voip/buddies-only/g19-menu-screen.svg | 326 ++ src/plugins/voip/default/Makefile.am | 31 + .../voip/default/default-menu-entry.svg | 296 ++ .../voip/default/default-menu-screen.svg | 215 ++ .../default/default-message-menu-entry.svg | 183 + src/plugins/voip/default/default.theme | 4 + src/plugins/voip/default/g19-menu-entry.svg | 405 ++ src/plugins/voip/default/g19-menu-screen.svg | 410 ++ .../voip/default/g19-message-menu-entry.svg | 204 + src/plugins/voip/default_audio-high.gif | Bin 0 -> 78 bytes src/plugins/voip/default_audio-muted.gif | Bin 0 -> 77 bytes src/plugins/voip/default_available.gif | Bin 0 -> 76 bytes src/plugins/voip/default_away.gif | Bin 0 -> 76 bytes .../default_microphone-sensitivity-high.gif | Bin 0 -> 76 bytes .../default_microphone-sensitivity-muted.gif | Bin 0 -> 75 bytes src/plugins/voip/default_record.gif | Bin 0 -> 71 bytes .../voip/g19_microphone-sensitivity-high.png | Bin 0 -> 3420 bytes .../voip/g19_microphone-sensitivity-muted.png | Bin 0 -> 1160 bytes src/plugins/voip/voip.py | 942 +++++ src/plugins/voip/voip.ui | 115 + src/plugins/volume/Makefile.am | 8 + src/plugins/volume/default/Makefile.am | 7 + src/plugins/volume/default/default.svg | 170 + src/plugins/volume/default/g19.svg | 154 + src/plugins/volume/default/mx5500.svg | 171 + src/plugins/volume/i18n/volume.en_GB.po | 26 + src/plugins/volume/i18n/volume.glade.h | 2 + src/plugins/volume/i18n/volume.pot | 26 + src/plugins/volume/volume.py | 415 ++ src/plugins/volume/volume.ui | 179 + src/plugins/weather-noaa/Makefile.am | 7 + src/plugins/weather-noaa/icon.png | Bin 0 -> 1784 bytes src/plugins/weather-noaa/weather-noaa.py | 156 + src/plugins/weather-noaa/weather-noaa.ui | 96 + src/plugins/weather-yahoo/Makefile.am | 7 + src/plugins/weather-yahoo/icon.png | Bin 0 -> 1784 bytes src/plugins/weather-yahoo/weather-yahoo.py | 383 ++ src/plugins/weather-yahoo/weather-yahoo.ui | 105 + src/plugins/weather/Makefile.am | 8 + src/plugins/weather/default/Makefile.am | 16 + src/plugins/weather/default/default.svg | 248 ++ src/plugins/weather/default/g19.svg | 331 ++ src/plugins/weather/default/mono-clouds.gif | Bin 0 -> 78 bytes .../weather/default/mono-dark-clouds.gif | Bin 0 -> 78 bytes .../weather/default/mono-few-clouds.gif | Bin 0 -> 78 bytes src/plugins/weather/default/mono-fog.gif | Bin 0 -> 76 bytes .../weather/default/mono-more-clouds.gif | Bin 0 -> 78 bytes src/plugins/weather/default/mono-rain.gif | Bin 0 -> 78 bytes src/plugins/weather/default/mono-snow.gif | Bin 0 -> 74 bytes src/plugins/weather/default/mono-sunny.gif | Bin 0 -> 78 bytes src/plugins/weather/default/mono-thunder.gif | Bin 0 -> 74 bytes src/plugins/weather/default/mx5500.svg | 290 ++ src/plugins/weather/forecasts/Makefile.am | 17 + src/plugins/weather/forecasts/default.svg | 241 ++ src/plugins/weather/forecasts/forecasts.theme | 4 + src/plugins/weather/forecasts/g19.svg | 378 ++ src/plugins/weather/forecasts/mono-clouds.gif | Bin 0 -> 78 bytes .../weather/forecasts/mono-dark-clouds.gif | Bin 0 -> 78 bytes .../weather/forecasts/mono-few-clouds.gif | Bin 0 -> 78 bytes src/plugins/weather/forecasts/mono-fog.gif | Bin 0 -> 76 bytes .../weather/forecasts/mono-more-clouds.gif | Bin 0 -> 78 bytes src/plugins/weather/forecasts/mono-rain.gif | Bin 0 -> 78 bytes src/plugins/weather/forecasts/mono-snow.gif | Bin 0 -> 74 bytes src/plugins/weather/forecasts/mono-sunny.gif | Bin 0 -> 78 bytes .../weather/forecasts/mono-thunder.gif | Bin 0 -> 74 bytes src/plugins/weather/forecasts/mx5500.svg | 290 ++ src/plugins/weather/i18n/weather.en_GB.po | 74 + src/plugins/weather/i18n/weather.glade.h | 15 + src/plugins/weather/i18n/weather.pot | 70 + src/plugins/weather/pywapi.py | 334 ++ src/plugins/weather/weather.py | 537 +++ src/plugins/weather/weather.ui | 356 ++ src/plugins/webkitbrowser/Makefile.am | 6 + src/plugins/webkitbrowser/default/Makefile.am | 5 + src/plugins/webkitbrowser/default/g19.svg | 223 ++ src/plugins/webkitbrowser/webkitbrowser.py | 96 + src/pylibg19/Makefile.am | 1 + src/pylibg19/g19/Makefile.am | 11 + src/pylibg19/g19/__init__.py | 0 src/pylibg19/g19/g19.py | 442 +++ src/pylibg19/g19/globals.py | 18 + src/pylibg19/g19/keys.py | 205 + src/pylibg19/g19/receivers.py | 311 ++ src/pylibg19/g19/runnable.py | 85 + src/scripts/Makefile.am | 16 + src/scripts/evtest | 77 + src/scripts/g15-config | 61 + src/scripts/g15-desktop-service | 112 + src/scripts/g15-diag | 194 + src/scripts/g15-indicator | 110 + src/scripts/g15-launch | 67 + src/scripts/g15-support-dump | 130 + src/scripts/g15-system-service | 105 + src/scripts/g15-systemtray | 129 + src/scripts/lg4l-image | 204 + src/scripts/libg15test | 86 + 976 files changed, 144968 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 TRANSLATION_PROGRESS.txt create mode 100644 _config.yml create mode 100755 b.sh create mode 100755 build-po.sh create mode 100755 build-pot.sh create mode 100755 compile.sh create mode 100644 configure.ac create mode 100644 data/Makefile.am create mode 100644 data/applications/Makefile.am create mode 100644 data/applications/g15-config.desktop.in create mode 100644 data/autostart/Makefile.am create mode 100644 data/autostart/g15-indicator.desktop.in create mode 100644 data/autostart/g15-systemtray.desktop.in create mode 100644 data/autostart/gnome15.desktop.in create mode 100644 data/dbus/Makefile.am create mode 100644 data/dbus/g15-system-service.conf create mode 100644 data/dbus/org.gnome15.Gnome15.service create mode 100644 data/dbus/org.gnome15.SystemService.service create mode 100644 data/fonts/CyrKoi-VGA8.psf.gz create mode 100644 data/fonts/tom-thumb.pcf create mode 100644 data/icons/AwOken/Makefile.am create mode 100644 data/icons/AwOken/apps/128/Makefile.am create mode 100644 data/icons/AwOken/apps/128/gnome15.png create mode 100644 data/icons/AwOken/apps/16/Makefile.am create mode 100644 data/icons/AwOken/apps/16/gnome15.png create mode 100644 data/icons/AwOken/apps/22/Makefile.am create mode 100644 data/icons/AwOken/apps/22/gnome15.png create mode 100644 data/icons/AwOken/apps/24/Makefile.am create mode 100644 data/icons/AwOken/apps/24/gnome15.png create mode 100644 data/icons/AwOken/apps/48/Makefile.am create mode 100644 data/icons/AwOken/apps/48/gnome15.png create mode 100644 data/icons/AwOken/apps/64/Makefile.am create mode 100644 data/icons/AwOken/apps/64/gnome15.png create mode 100644 data/icons/AwOken/apps/Makefile.am create mode 100644 data/icons/AwOken/status/128/Makefile.am create mode 100644 data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/128/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/16/Makefile.am create mode 100644 data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/16/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/22/Makefile.am create mode 100644 data/icons/AwOken/status/22/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/22/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/24/Makefile.am create mode 100644 data/icons/AwOken/status/24/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/24/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/48/Makefile.am create mode 100644 data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/48/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/64/Makefile.am create mode 100644 data/icons/AwOken/status/64/logitech-g-keyboard-error-panel.png create mode 100644 data/icons/AwOken/status/64/logitech-g-keyboard-panel.png create mode 100644 data/icons/AwOken/status/Makefile.am create mode 100644 data/icons/Makefile.am create mode 100644 data/icons/elementary/Makefile.am create mode 100644 data/icons/elementary/status/16/Makefile.am create mode 100644 data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/elementary/status/16/logitech-g-keyboard-panel.svg create mode 100644 data/icons/elementary/status/22/Makefile.am create mode 100644 data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/elementary/status/22/logitech-g-keyboard-panel.svg create mode 100644 data/icons/elementary/status/24/Makefile.am create mode 100644 data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/elementary/status/24/logitech-g-keyboard-panel.svg create mode 100644 data/icons/elementary/status/Makefile.am create mode 100644 data/icons/hicolor/16x16/Makefile.am create mode 100644 data/icons/hicolor/16x16/status/Makefile.am create mode 100644 data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png create mode 100644 data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png create mode 100644 data/icons/hicolor/22x22/Makefile.am create mode 100644 data/icons/hicolor/22x22/apps/Makefile.am create mode 100644 data/icons/hicolor/22x22/apps/gnome15.png create mode 100644 data/icons/hicolor/22x22/status/Makefile.am create mode 100644 data/icons/hicolor/22x22/status/logitech-g-keyboard-applet.png create mode 100644 data/icons/hicolor/22x22/status/logitech-g-keyboard-error-applet.png create mode 100644 data/icons/hicolor/24x24/Makefile.am create mode 100644 data/icons/hicolor/24x24/apps/Makefile.am create mode 100644 data/icons/hicolor/24x24/apps/gnome15.png create mode 100644 data/icons/hicolor/24x24/status/Makefile.am create mode 100644 data/icons/hicolor/24x24/status/logitech-g-keyboard-applet.png create mode 100644 data/icons/hicolor/24x24/status/logitech-g-keyboard-error-applet.png create mode 100644 data/icons/hicolor/48x48/Makefile.am create mode 100644 data/icons/hicolor/48x48/apps/Makefile.am create mode 100644 data/icons/hicolor/48x48/apps/gnome15.png create mode 100644 data/icons/hicolor/64x64/Makefile.am create mode 100644 data/icons/hicolor/64x64/apps/Makefile.am create mode 100644 data/icons/hicolor/64x64/apps/gnome15.png create mode 100644 data/icons/hicolor/Makefile.am create mode 100644 data/icons/hicolor/scalable/Makefile.am create mode 100644 data/icons/hicolor/scalable/apps/Makefile.am create mode 100644 data/icons/hicolor/scalable/apps/gnome15.svg create mode 100644 data/icons/hicolor/scalable/devices/Makefile.am create mode 100644 data/icons/hicolor/scalable/devices/g11.png create mode 100644 data/icons/hicolor/scalable/devices/g110.png create mode 100644 data/icons/hicolor/scalable/devices/g13.png create mode 100644 data/icons/hicolor/scalable/devices/g15v1.png create mode 100644 data/icons/hicolor/scalable/devices/g15v2.png create mode 100644 data/icons/hicolor/scalable/devices/g19.png create mode 100644 data/icons/hicolor/scalable/devices/g35.png create mode 100644 data/icons/hicolor/scalable/devices/g510.png create mode 100644 data/icons/hicolor/scalable/devices/g930.png create mode 100644 data/icons/hicolor/scalable/devices/mx5500.png create mode 100644 data/icons/hicolor/scalable/devices/z10.png create mode 100644 data/icons/hicolor/scalable/status/Makefile.am create mode 100644 data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/Makefile.am create mode 100644 data/icons/ubuntu-mono-dark/status/16/Makefile.am create mode 100644 data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/22/Makefile.am create mode 100644 data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/24/Makefile.am create mode 100644 data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-dark/status/Makefile.am create mode 100644 data/icons/ubuntu-mono-light/Makefile.am create mode 100644 data/icons/ubuntu-mono-light/status/16/Makefile.am create mode 100644 data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/22/Makefile.am create mode 100644 data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/24/Makefile.am create mode 100644 data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg create mode 100644 data/icons/ubuntu-mono-light/status/Makefile.am create mode 100644 data/images/Makefile.am create mode 100644 data/images/default-background.svg create mode 100644 data/images/g15key-error.png create mode 100644 data/images/g15key.png create mode 100644 data/images/g19-background.svg create mode 100644 data/images/key-back.png create mode 100644 data/images/key-down.png create mode 100644 data/images/key-g1.png create mode 100644 data/images/key-g10.png create mode 100644 data/images/key-g11.png create mode 100644 data/images/key-g12.png create mode 100644 data/images/key-g13.png create mode 100644 data/images/key-g14.png create mode 100644 data/images/key-g15.png create mode 100644 data/images/key-g16.png create mode 100644 data/images/key-g17.png create mode 100644 data/images/key-g18.png create mode 100644 data/images/key-g19.png create mode 100644 data/images/key-g2.png create mode 100644 data/images/key-g20.png create mode 100644 data/images/key-g21.png create mode 100644 data/images/key-g22.png create mode 100644 data/images/key-g3.png create mode 100644 data/images/key-g4.png create mode 100644 data/images/key-g5.png create mode 100644 data/images/key-g6.png create mode 100644 data/images/key-g7.png create mode 100644 data/images/key-g8.png create mode 100644 data/images/key-g9.png create mode 100644 data/images/key-l1.png create mode 100644 data/images/key-l2.png create mode 100644 data/images/key-l3.png create mode 100644 data/images/key-l4.png create mode 100644 data/images/key-l5.png create mode 100644 data/images/key-left.png create mode 100644 data/images/key-light.png create mode 100644 data/images/key-m1.png create mode 100644 data/images/key-m2.png create mode 100644 data/images/key-m3.png create mode 100644 data/images/key-menu.png create mode 100644 data/images/key-mr.png create mode 100644 data/images/key-mute.png create mode 100644 data/images/key-next.png create mode 100644 data/images/key-ok.png create mode 100644 data/images/key-play.png create mode 100644 data/images/key-prev.png create mode 100644 data/images/key-right.png create mode 100644 data/images/key-settings.png create mode 100644 data/images/key-stop.png create mode 100644 data/images/key-up.png create mode 100644 data/images/key-vol-down.png create mode 100644 data/images/key-vol-up.png create mode 100644 data/images/locked.png create mode 100644 data/images/mx5500-background.svg create mode 100644 data/themes/Makefile.am create mode 100644 data/themes/default/Makefile.am create mode 100644 data/themes/default/default-confirmation-screen.svg create mode 100644 data/themes/default/default-error-screen.svg create mode 100644 data/themes/default/default-menu-child-entry.svg create mode 100644 data/themes/default/default-menu-entry.svg create mode 100644 data/themes/default/default-menu-screen.svg create mode 100644 data/themes/default/default-menu-separator.svg create mode 100644 data/themes/default/g19-confirmation-screen.svg create mode 100644 data/themes/default/g19-error-screen.svg create mode 100644 data/themes/default/g19-menu-child-entry.svg create mode 100644 data/themes/default/g19-menu-entry.svg create mode 100644 data/themes/default/g19-menu-screen.svg create mode 100644 data/themes/default/g19-menu-separator.svg create mode 100644 data/themes/default/mx5500-confirmation-screen.svg create mode 100644 data/themes/default/mx5500-error-screen.svg create mode 100644 data/themes/default/mx5500-menu-child-entry.svg create mode 100644 data/themes/default/mx5500-menu-entry.svg create mode 100644 data/themes/default/mx5500-menu-screen.svg create mode 100644 data/themes/default/mx5500-menu-separator.svg create mode 100644 data/udev/98-gnome15.rules.in create mode 100644 data/udev/99-gnome15-g15direct.rules.in create mode 100644 data/udev/99-gnome15-g19direct.rules.in create mode 100644 data/udev/99-gnome15-g930.rules.in create mode 100644 data/udev/99-gnome15-kernel.rules.in create mode 100644 data/udev/Makefile.am create mode 100644 data/ui/Makefile.am create mode 100644 data/ui/accounts.ui create mode 100644 data/ui/colorpicker.ui create mode 100644 data/ui/driver_g15.ui create mode 100644 data/ui/driver_g15direct.ui create mode 100644 data/ui/driver_g19.ui create mode 100644 data/ui/driver_g19direct.ui create mode 100644 data/ui/driver_g930.ui create mode 100644 data/ui/driver_gtk.ui create mode 100644 data/ui/driver_kernel.ui create mode 100644 data/ui/g15-config.ui create mode 100644 data/ui/macro-editor.ui create mode 100644 data/ui/password.ui create mode 100644 data/ui/redblue.png create mode 100644 data/ui/script-editor.ui create mode 100644 data/ui/turbo.png create mode 100644 data/ukeys/Makefile.am create mode 100644 data/ukeys/digital-joystick.keys create mode 100644 data/ukeys/joystick.keys create mode 100644 data/ukeys/keyboard.keys create mode 100644 data/ukeys/keysym-to-uinput create mode 100644 data/ukeys/mouse.keys create mode 100644 data/xcf/AwOkenIcon.xcf create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100755 debug-g15-config.sh create mode 100755 debug-g15-systemtray.sh create mode 100755 debug.sh create mode 100644 docs/Gnome15.dia create mode 100644 docs/style_guide.md create mode 100644 example.svg create mode 100644 i18n/Makefile.am create mode 100755 i18n/build-po.sh create mode 100755 i18n/build-pot.sh create mode 100644 i18n/colorpicker.en_GB.po create mode 100644 i18n/colorpicker.glade.h create mode 100644 i18n/colorpicker.pot create mode 100644 i18n/driver_g15.en_GB.po create mode 100644 i18n/driver_g15.glade.h create mode 100644 i18n/driver_g15.pot create mode 100644 i18n/driver_g15direct.en_GB.po create mode 100644 i18n/driver_g15direct.glade.h create mode 100644 i18n/driver_g15direct.pot create mode 100644 i18n/driver_g19.en_GB.po create mode 100644 i18n/driver_g19.glade.h create mode 100644 i18n/driver_g19.pot create mode 100644 i18n/driver_g19direct.en_GB.po create mode 100644 i18n/driver_g19direct.glade.h create mode 100644 i18n/driver_g19direct.pot create mode 100644 i18n/driver_g930.en_GB.po create mode 100644 i18n/driver_g930.glade.h create mode 100644 i18n/driver_g930.pot create mode 100644 i18n/driver_gtk.en_GB.po create mode 100644 i18n/driver_gtk.glade.h create mode 100644 i18n/driver_gtk.pot create mode 100644 i18n/driver_kernel.en_GB.po create mode 100644 i18n/driver_kernel.glade.h create mode 100644 i18n/driver_kernel.pot create mode 100644 i18n/g15-config.en_GB.po create mode 100644 i18n/g15-config.glade.h create mode 100644 i18n/g15-config.pot create mode 100644 i18n/gnome15-drivers.en_GB.po create mode 100644 i18n/gnome15-drivers.pot create mode 100644 i18n/gnome15.en_GB.po create mode 100644 i18n/gnome15.pot create mode 100644 i18n/macro-editor.en_GB.po create mode 100644 i18n/macro-editor.glade.h create mode 100644 i18n/macro-editor.pot create mode 100644 m4/ax_python_devel.m4 create mode 100644 m4/ax_python_module.m4 create mode 100644 man/Makefile.am create mode 100644 man/g15-config.1 create mode 100644 man/g15-desktop-service.1 create mode 100644 man/g15-indicator.1 create mode 100644 man/g15-system-service.1 create mode 100644 man/g15-systemtray.1 create mode 100755 mksvgheaders.py create mode 100644 src/Makefile.am create mode 100755 src/gamewrap/gamewrap create mode 100644 src/gamewrap/gw/__init__.py create mode 100644 src/gamewrap/gw/wraplet.py create mode 100644 src/gamewrap/ut2004.wlet create mode 100644 src/gnome-shell-extension/Makefile.am create mode 100644 src/gnome-shell-extension/extension.js create mode 100644 src/gnome-shell-extension/icons/Makefile.am create mode 100644 src/gnome-shell-extension/icons/logitech-g11-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g110-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g13-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g19-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g35-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g510-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-g930-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-source.svg create mode 100644 src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg create mode 100644 src/gnome-shell-extension/icons/logitech-z10-symbolic.svg create mode 100644 src/gnome-shell-extension/metadata.json create mode 100644 src/gnome-shell-extension/stylesheet.css create mode 100644 src/gnome15/Makefile.am create mode 100644 src/gnome15/__init__.py create mode 100644 src/gnome15/colorpicker.py create mode 100644 src/gnome15/dbusmenu.py create mode 100644 src/gnome15/drivers/Makefile.am create mode 100644 src/gnome15/drivers/__init__.py create mode 100644 src/gnome15/drivers/driver_g15direct.py create mode 100644 src/gnome15/drivers/driver_g19direct.py create mode 100644 src/gnome15/drivers/driver_g930.py create mode 100644 src/gnome15/drivers/driver_gtk.py create mode 100644 src/gnome15/drivers/driver_kernel.py create mode 100644 src/gnome15/drivers/driver_mx5500.py create mode 100644 src/gnome15/drivers/fb.py create mode 100644 src/gnome15/drivers/pylibg15.py create mode 100644 src/gnome15/g15accounts.py create mode 100644 src/gnome15/g15actions.py create mode 100644 src/gnome15/g15config.py create mode 100644 src/gnome15/g15dbus.py create mode 100644 src/gnome15/g15dconf.py create mode 100644 src/gnome15/g15debug.py create mode 100644 src/gnome15/g15desktop.py create mode 100644 src/gnome15/g15devices.py create mode 100644 src/gnome15/g15driver.py create mode 100644 src/gnome15/g15drivermanager.py create mode 100644 src/gnome15/g15exceptions.py create mode 100644 src/gnome15/g15globals.py.in create mode 100644 src/gnome15/g15gtk.py create mode 100644 src/gnome15/g15keyboard.py create mode 100644 src/gnome15/g15keyio.py create mode 100644 src/gnome15/g15locale.py create mode 100644 src/gnome15/g15logging.py create mode 100644 src/gnome15/g15macroeditor.py create mode 100644 src/gnome15/g15network.py create mode 100644 src/gnome15/g15notify.py create mode 100644 src/gnome15/g15plugin.py create mode 100644 src/gnome15/g15pluginmanager.py create mode 100644 src/gnome15/g15profile.py create mode 100644 src/gnome15/g15screen.py create mode 100644 src/gnome15/g15service.py create mode 100644 src/gnome15/g15system.py create mode 100644 src/gnome15/g15text.py create mode 100644 src/gnome15/g15theme.py create mode 100644 src/gnome15/g15top.py create mode 100644 src/gnome15/g15uinput.py create mode 100644 src/gnome15/g15upgrade.py create mode 100644 src/gnome15/g15util.py create mode 100644 src/gnome15/lcdsink.py create mode 100644 src/gnome15/objgraph.py create mode 100644 src/gnome15/util/Makefile.am create mode 100644 src/gnome15/util/__init__.py create mode 100644 src/gnome15/util/g15cairo.py create mode 100644 src/gnome15/util/g15convert.py create mode 100644 src/gnome15/util/g15gconf.py create mode 100644 src/gnome15/util/g15icontools.py create mode 100644 src/gnome15/util/g15markup.py create mode 100644 src/gnome15/util/g15os.py create mode 100644 src/gnome15/util/g15pythonlang.py create mode 100644 src/gnome15/util/g15scheduler.py create mode 100644 src/gnome15/util/g15svg.py create mode 100644 src/gnome15/util/g15uigconf.py create mode 100644 src/gnome15/util/jobqueue.py create mode 100644 src/libimpulse/Impulse.c create mode 100644 src/libimpulse/Impulse.h create mode 100644 src/libimpulse/Makefile.am create mode 100644 src/libimpulse/impulsemodule.c create mode 100644 src/libimpulse/test-libimpulse.c create mode 100644 src/plugins/Makefile.am create mode 100644 src/plugins/background/Makefile.am create mode 100644 src/plugins/background/background-160x43.png create mode 100644 src/plugins/background/background-320x240.png create mode 100644 src/plugins/background/background.py create mode 100644 src/plugins/background/background.ui create mode 100644 src/plugins/background/background2.py create mode 100644 src/plugins/background/i18n/background.en_GB.po create mode 100644 src/plugins/background/i18n/background.glade.h create mode 100644 src/plugins/background/i18n/background.pot create mode 100644 src/plugins/backlight/Makefile.am create mode 100644 src/plugins/backlight/backlight.py create mode 100644 src/plugins/backlight/default/Makefile.am create mode 100644 src/plugins/backlight/default/g19.svg create mode 100644 src/plugins/cairo-clock/Makefile.am create mode 100644 src/plugins/cairo-clock/cairo-clock.py create mode 100644 src/plugins/cairo-clock/cairo-clock.ui create mode 100644 src/plugins/cairo-clock/g15/Makefile.am create mode 100644 src/plugins/cairo-clock/g15/default/Makefile.am create mode 100644 src/plugins/cairo-clock/g15/default/clock-frame.gif create mode 100644 src/plugins/cairo-clock/g15/default/clock-glass.gif create mode 100644 src/plugins/cairo-clock/g15/default/clock-hour-hand.gif create mode 100644 src/plugins/cairo-clock/g15/default/clock-marks.gif create mode 100644 src/plugins/cairo-clock/g15/default/clock-minute-hand.gif create mode 100644 src/plugins/cairo-clock/g15/default/clock-second-hand.gif create mode 100644 src/plugins/cairo-clock/g19/Makefile.am create mode 100644 src/plugins/cairo-clock/g19/default/Makefile.am create mode 100644 src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-face-shadow.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-face.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-frame.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-glass.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-hour-hand.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-marks.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-minute-hand.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg create mode 100644 src/plugins/cairo-clock/g19/default/clock-second-hand.svg create mode 100644 src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po create mode 100644 src/plugins/cairo-clock/i18n/cairo-clock.glade.h create mode 100644 src/plugins/cairo-clock/i18n/cairo-clock.pot create mode 100644 src/plugins/cairo-clock/mx5500/Makefile.am create mode 100644 src/plugins/cairo-clock/mx5500/default/Makefile.am create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-frame.gif create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-glass.gif create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-marks.gif create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-minute-hand.gif create mode 100644 src/plugins/cairo-clock/mx5500/default/clock-second-hand.gif create mode 100644 src/plugins/cal-evolution/Makefile.am create mode 100644 src/plugins/cal-evolution/cal-evolution.py create mode 100644 src/plugins/cal-evolution/cal-evolution.ui create mode 100644 src/plugins/cal-evolution/icon.png create mode 100644 src/plugins/cal-google/Makefile.am create mode 100644 src/plugins/cal-google/cal-google.py create mode 100644 src/plugins/cal-google/cal-google.ui create mode 100644 src/plugins/cal-google/icon.png create mode 100644 src/plugins/cal-google/iso8601.py create mode 100644 src/plugins/cal/Makefile.am create mode 100644 src/plugins/cal/bell.gif create mode 100644 src/plugins/cal/cal.py create mode 100644 src/plugins/cal/cal.ui create mode 100644 src/plugins/cal/default/Makefile.am create mode 100644 src/plugins/cal/default/default-cell.svg create mode 100644 src/plugins/cal/default/default-menu-entry.svg create mode 100644 src/plugins/cal/default/default.svg create mode 100644 src/plugins/cal/default/g19-cell.svg create mode 100644 src/plugins/cal/default/g19-menu-entry.svg create mode 100644 src/plugins/cal/default/g19.svg create mode 100644 src/plugins/cal/i18n/cal.en_GB.po create mode 100644 src/plugins/cal/i18n/cal.pot create mode 100644 src/plugins/clock/Makefile.am create mode 100644 src/plugins/clock/clock.py create mode 100644 src/plugins/clock/clock.ui create mode 100644 src/plugins/clock/default/Makefile.am create mode 100644 src/plugins/clock/default/default-with-date.svg create mode 100644 src/plugins/clock/default/default.svg create mode 100644 src/plugins/clock/default/g19-with-date.svg create mode 100644 src/plugins/clock/default/g19.svg create mode 100644 src/plugins/clock/default/mx5500-with-date.svg create mode 100644 src/plugins/clock/default/mx5500.svg create mode 100644 src/plugins/clock/i18n/clock.en_GB.po create mode 100644 src/plugins/clock/i18n/clock.glade.h create mode 100644 src/plugins/clock/i18n/clock.pot create mode 100644 src/plugins/debug/Makefile.am create mode 100644 src/plugins/debug/debug.py create mode 100644 src/plugins/debug/default/Makefile.am create mode 100644 src/plugins/debug/default/default.svg create mode 100644 src/plugins/debug/default/g19.svg create mode 100644 src/plugins/debug/i18n/clock.en_GB.po create mode 100644 src/plugins/debug/i18n/clock.glade.h create mode 100644 src/plugins/debug/i18n/clock.pot create mode 100644 src/plugins/display/Makefile.am create mode 100644 src/plugins/display/display.py create mode 100644 src/plugins/fx/Makefile.am create mode 100644 src/plugins/fx/fx.py create mode 100644 src/plugins/fx/fx.ui create mode 100644 src/plugins/fx/i18n/fx.en_GB.po create mode 100644 src/plugins/fx/i18n/fx.glade.h create mode 100644 src/plugins/fx/i18n/fx.pot create mode 100644 src/plugins/g15daemon-server/Makefile.am create mode 100644 src/plugins/g15daemon-server/g15daemon-server.py create mode 100644 src/plugins/g15daemon-server/g15daemon-server.ui create mode 100644 src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po create mode 100644 src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h create mode 100644 src/plugins/g15daemon-server/i18n/g15daemon-server.pot create mode 100644 src/plugins/game-nexuiz/Makefile.am create mode 100644 src/plugins/game-nexuiz/default/Makefile.am create mode 100644 src/plugins/game-nexuiz/default/default.svg create mode 100644 src/plugins/game-nexuiz/default/g19.svg create mode 100644 src/plugins/game-nexuiz/game-nexuiz.g13.macros create mode 100644 src/plugins/game-nexuiz/game-nexuiz.g19.macros create mode 100644 src/plugins/game-nexuiz/game-nexuiz.py create mode 100644 src/plugins/game-nexuiz/resources/Makefile.am create mode 100644 src/plugins/game-nexuiz/resources/g19-background.jpg create mode 100644 src/plugins/game-nexuiz/resources/icon.png create mode 100644 src/plugins/google-analytics/Makefile.am create mode 100644 src/plugins/google-analytics/default/Makefile.am create mode 100644 src/plugins/google-analytics/default/default.svg create mode 100644 src/plugins/google-analytics/default/g19.svg create mode 100644 src/plugins/google-analytics/google-analytics.py create mode 100644 src/plugins/google-analytics/google-analytics.ui create mode 100644 src/plugins/im/Makefile.am create mode 100644 src/plugins/im/i18n/im.en_GB.po create mode 100644 src/plugins/im/i18n/im.pot create mode 100644 src/plugins/im/im.py create mode 100644 src/plugins/impulse15/Makefile.am create mode 100644 src/plugins/impulse15/i18n/impulse15.en_GB.po create mode 100644 src/plugins/impulse15/i18n/impulse15.glade.h create mode 100644 src/plugins/impulse15/i18n/impulse15.pot create mode 100644 src/plugins/impulse15/impulse15.py create mode 100644 src/plugins/impulse15/impulse15.ui create mode 100644 src/plugins/impulse15/themes/Makefile.am create mode 100644 src/plugins/impulse15/themes/circlelcd/Makefile.am create mode 100644 src/plugins/impulse15/themes/circlelcd/__init__.py create mode 100644 src/plugins/impulse15/themes/circlelcd/theme.conf create mode 100644 src/plugins/impulse15/themes/circleline/Makefile.am create mode 100644 src/plugins/impulse15/themes/circleline/__init__.py create mode 100644 src/plugins/impulse15/themes/circleline/theme.conf create mode 100644 src/plugins/impulse15/themes/default/Makefile.am create mode 100644 src/plugins/impulse15/themes/default/__init__.py create mode 100644 src/plugins/impulse15/themes/default/theme.conf create mode 100644 src/plugins/impulse15/themes/original/Makefile.am create mode 100644 src/plugins/impulse15/themes/original/__init__.py create mode 100644 src/plugins/impulse15/themes/original/theme.conf create mode 100644 src/plugins/indicator-messages/Makefile.am create mode 100644 src/plugins/indicator-messages/default/Makefile.am create mode 100644 src/plugins/indicator-messages/default/default-entry.svg create mode 100644 src/plugins/indicator-messages/default/default.svg create mode 100644 src/plugins/indicator-messages/default/g19-entry.svg create mode 100644 src/plugins/indicator-messages/default/g19-separator.svg create mode 100644 src/plugins/indicator-messages/default/g19.svg create mode 100644 src/plugins/indicator-messages/default/indicator_messages_default_common.py create mode 100644 src/plugins/indicator-messages/default/indicator_messages_default_default.py create mode 100644 src/plugins/indicator-messages/default/indicator_messages_default_g19.py create mode 100644 src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po create mode 100644 src/plugins/indicator-messages/i18n/indicator-messages.glade.h create mode 100644 src/plugins/indicator-messages/i18n/indicator-messages.pot create mode 100644 src/plugins/indicator-messages/indicator-messages.py create mode 100644 src/plugins/indicator-messages/indicator-messages.ui create mode 100644 src/plugins/indicator-messages/mono-mail-error.gif create mode 100644 src/plugins/indicator-messages/mono-mail-new.gif create mode 100644 src/plugins/keyhelp/Makefile.am create mode 100644 src/plugins/keyhelp/default/Makefile.am create mode 100644 src/plugins/keyhelp/default/default-menu-entry.svg create mode 100644 src/plugins/keyhelp/default/default-menu-screen.svg create mode 100644 src/plugins/keyhelp/default/g19.svg create mode 100644 src/plugins/keyhelp/keyhelp.py create mode 100644 src/plugins/lcdbiff/Makefile.am create mode 100644 src/plugins/lcdbiff/default/Makefile.am create mode 100644 src/plugins/lcdbiff/default/default-menu-entry.svg create mode 100644 src/plugins/lcdbiff/i18n/imap.en_GB.po create mode 100644 src/plugins/lcdbiff/i18n/imap.glade.h create mode 100644 src/plugins/lcdbiff/i18n/imap.pot create mode 100644 src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po create mode 100644 src/plugins/lcdbiff/i18n/lcdbiff.glade.h create mode 100644 src/plugins/lcdbiff/i18n/lcdbiff.pot create mode 100644 src/plugins/lcdbiff/i18n/password.en_GB.po create mode 100644 src/plugins/lcdbiff/i18n/password.glade.h create mode 100644 src/plugins/lcdbiff/i18n/password.pot create mode 100644 src/plugins/lcdbiff/i18n/pop3.en_GB.po create mode 100644 src/plugins/lcdbiff/i18n/pop3.glade.h create mode 100644 src/plugins/lcdbiff/i18n/pop3.pot create mode 100644 src/plugins/lcdbiff/imap.ui create mode 100644 src/plugins/lcdbiff/lcdbiff.py create mode 100644 src/plugins/lcdbiff/mono-mail-error.gif create mode 100644 src/plugins/lcdbiff/mono-mail-new.gif create mode 100644 src/plugins/lcdbiff/mono-mail-refresh.gif create mode 100644 src/plugins/lcdbiff/password.ui create mode 100644 src/plugins/lcdbiff/pop3.ui create mode 100644 src/plugins/lcdshot/Makefile.am create mode 100644 src/plugins/lcdshot/i18n/lcdshot.en_GB.po create mode 100644 src/plugins/lcdshot/i18n/lcdshot.glade.h create mode 100644 src/plugins/lcdshot/i18n/lcdshot.pot create mode 100644 src/plugins/lcdshot/lcdshot.py create mode 100644 src/plugins/lcdshot/lcdshot.ui create mode 100644 src/plugins/lens/Makefile.am create mode 100644 src/plugins/lens/gnome15.lens create mode 100644 src/plugins/lens/gnome15.svg create mode 100644 src/plugins/lens/lens.py create mode 100644 src/plugins/macro-recorder/Makefile.am create mode 100644 src/plugins/macro-recorder/default/Makefile.am create mode 100644 src/plugins/macro-recorder/default/default.svg create mode 100644 src/plugins/macro-recorder/default/g19.svg create mode 100644 src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po create mode 100644 src/plugins/macro-recorder/i18n/macro-recorder.pot create mode 100644 src/plugins/macro-recorder/macro-recorder.py create mode 100644 src/plugins/macros/Makefile.am create mode 100644 src/plugins/macros/default/Makefile.am create mode 100644 src/plugins/macros/default/default-menu-entry.svg create mode 100644 src/plugins/macros/default/default-menu-screen.svg create mode 100644 src/plugins/macros/default/g19-menu-entry.svg create mode 100644 src/plugins/macros/default/g19-menu-screen.svg create mode 100644 src/plugins/macros/i18n/macros.en_GB.po create mode 100644 src/plugins/macros/i18n/macros.glade.h create mode 100644 src/plugins/macros/i18n/macros.pot create mode 100644 src/plugins/macros/macros.py create mode 100644 src/plugins/macros/macros.ui create mode 100644 src/plugins/mediaplayer/Makefile.am create mode 100644 src/plugins/mediaplayer/default/Makefile.am create mode 100644 src/plugins/mediaplayer/default/WIPg19.svg create mode 100644 src/plugins/mediaplayer/default/default-mediakeys.svg create mode 100644 src/plugins/mediaplayer/default/default.svg create mode 100644 src/plugins/mediaplayer/default/g19-mediakeys.svg create mode 100644 src/plugins/mediaplayer/default/g19.svg create mode 100644 src/plugins/mediaplayer/gtkplayer.py create mode 100644 src/plugins/mediaplayer/i18n/videoplayer.en_GB.po create mode 100644 src/plugins/mediaplayer/i18n/videoplayer.pot create mode 100644 src/plugins/mediaplayer/mediaplayer.py create mode 100644 src/plugins/mediaplayer/oldvideoplayer.py create mode 100644 src/plugins/menu/Makefile.am create mode 100644 src/plugins/menu/i18n/menu.en_GB.po create mode 100644 src/plugins/menu/i18n/menu.pot create mode 100644 src/plugins/menu/menu.py create mode 100644 src/plugins/mounts/Makefile.am create mode 100644 src/plugins/mounts/default/Makefile.am create mode 100644 src/plugins/mounts/default/default-menu-entry.svg create mode 100644 src/plugins/mounts/default/g19-menu-entry.svg create mode 100644 src/plugins/mounts/i18n/mounts.en_GB.po create mode 100644 src/plugins/mounts/i18n/mounts.glade.h create mode 100644 src/plugins/mounts/i18n/mounts.pot create mode 100644 src/plugins/mounts/mounts.py create mode 100644 src/plugins/mounts/mounts.ui create mode 100644 src/plugins/mpris/Makefile.am create mode 100644 src/plugins/mpris/bigcover/Makefile.am create mode 100644 src/plugins/mpris/bigcover/bigcover.theme create mode 100644 src/plugins/mpris/bigcover/g19.svg create mode 100644 src/plugins/mpris/default/Makefile.am create mode 100644 src/plugins/mpris/default/default.svg create mode 100644 src/plugins/mpris/default/g19.svg create mode 100644 src/plugins/mpris/default/mx5500.svg create mode 100644 src/plugins/mpris/default/pause.gif create mode 100644 src/plugins/mpris/default/play.gif create mode 100644 src/plugins/mpris/i18n/mpris.en_GB.po create mode 100644 src/plugins/mpris/i18n/mpris.pot create mode 100644 src/plugins/mpris/mpris.py create mode 100644 src/plugins/mpris/mpris.ui create mode 100644 src/plugins/nm/Makefile.am create mode 100644 src/plugins/nm/default/Makefile.am create mode 100644 src/plugins/nm/default/default.svg create mode 100644 src/plugins/nm/default/g19.svg create mode 100644 src/plugins/nm/nm.py create mode 100644 src/plugins/notify-lcd/Makefile.am create mode 100644 src/plugins/notify-lcd/default/Makefile.am create mode 100644 src/plugins/notify-lcd/default/default-nobody.svg create mode 100644 src/plugins/notify-lcd/default/default.svg create mode 100644 src/plugins/notify-lcd/default/g19-nobody.svg create mode 100644 src/plugins/notify-lcd/default/g19.svg create mode 100644 src/plugins/notify-lcd/default/mx5500-nobody.svg create mode 100644 src/plugins/notify-lcd/default/mx5500.svg create mode 100644 src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po create mode 100644 src/plugins/notify-lcd/i18n/notify-lcd.glade.h create mode 100644 src/plugins/notify-lcd/i18n/notify-lcd.pot create mode 100644 src/plugins/notify-lcd/notify-lcd.py create mode 100644 src/plugins/notify-lcd/notify-lcd.ui create mode 100644 src/plugins/notify-lcd2/Makefile.am create mode 100644 src/plugins/notify-lcd2/default/Makefile.am create mode 100644 src/plugins/notify-lcd2/default/default-nobody.svg create mode 100644 src/plugins/notify-lcd2/default/default.svg create mode 100644 src/plugins/notify-lcd2/default/g19-nobody.svg create mode 100644 src/plugins/notify-lcd2/default/g19.svg create mode 100644 src/plugins/notify-lcd2/notify-lcd.ui create mode 100644 src/plugins/notify-lcd2/notify-lcd2.py create mode 100644 src/plugins/panel/Makefile.am create mode 100644 src/plugins/panel/i18n/panel.en_GB.po create mode 100644 src/plugins/panel/i18n/panel.glade.h create mode 100644 src/plugins/panel/i18n/panel.pot create mode 100644 src/plugins/panel/panel.py create mode 100644 src/plugins/panel/panel.ui create mode 100644 src/plugins/pommodoro/Machovka_tomato.png create mode 100644 src/plugins/pommodoro/Machovka_tomato_green.png create mode 100644 src/plugins/pommodoro/Makefile.am create mode 100644 src/plugins/pommodoro/default/Makefile.am create mode 100644 src/plugins/pommodoro/default/default-timerover.svg create mode 100644 src/plugins/pommodoro/default/default.svg create mode 100644 src/plugins/pommodoro/default/g19-timerover.svg create mode 100644 src/plugins/pommodoro/default/g19.svg create mode 100644 src/plugins/pommodoro/pommodoro.py create mode 100644 src/plugins/pommodoro/pommodoro.ui create mode 100644 src/plugins/pommodoro/tomato_1bpp.png create mode 100644 src/plugins/pommodoro/tomato_empty_1bpp.png create mode 100644 src/plugins/ppastats/Makefile.am create mode 100644 src/plugins/ppastats/default/Makefile.am create mode 100644 src/plugins/ppastats/default/default-menu-entry.svg create mode 100644 src/plugins/ppastats/default/default.svg create mode 100644 src/plugins/ppastats/default/g19-menu-entry.svg create mode 100644 src/plugins/ppastats/default/g19.svg create mode 100644 src/plugins/ppastats/ppastats.py create mode 100644 src/plugins/ppastats/ppastats.ui create mode 100644 src/plugins/processes/Makefile.am create mode 100644 src/plugins/processes/i18n/processes.en_GB.po create mode 100644 src/plugins/processes/i18n/processes.pot create mode 100644 src/plugins/processes/processes.py create mode 100644 src/plugins/profiles/Makefile.am create mode 100644 src/plugins/profiles/bw-locked-inverted.gif create mode 100644 src/plugins/profiles/bw-locked.gif create mode 100644 src/plugins/profiles/profiles.py create mode 100644 src/plugins/rss/Makefile.am create mode 100644 src/plugins/rss/default/Makefile.am create mode 100644 src/plugins/rss/default/default-menu-entry.svg create mode 100644 src/plugins/rss/default/default-menu-screen.svg create mode 100644 src/plugins/rss/default/g19-menu-entry.svg create mode 100644 src/plugins/rss/default/g19-menu-screen.svg create mode 100644 src/plugins/rss/default/mx5500-menu-entry.svg create mode 100644 src/plugins/rss/i18n/rss.en_GB.po create mode 100644 src/plugins/rss/i18n/rss.glade.h create mode 100644 src/plugins/rss/i18n/rss.pot create mode 100644 src/plugins/rss/rss.py create mode 100644 src/plugins/rss/rss.ui create mode 100644 src/plugins/runapp/Makefile.am create mode 100644 src/plugins/runapp/background-160x43.png create mode 100644 src/plugins/runapp/background-320x240.png create mode 100644 src/plugins/runapp/default/default.svg create mode 100644 src/plugins/runapp/i18n/background.en_GB.po create mode 100644 src/plugins/runapp/i18n/background.glade.h create mode 100644 src/plugins/runapp/i18n/background.pot create mode 100644 src/plugins/runapp/runapp.py create mode 100644 src/plugins/screensaver/Makefile.am create mode 100644 src/plugins/screensaver/default/Makefile.am create mode 100644 src/plugins/screensaver/default/default-nobody.svg create mode 100644 src/plugins/screensaver/default/default.svg create mode 100644 src/plugins/screensaver/default/g19-nobody.svg create mode 100644 src/plugins/screensaver/default/g19.svg create mode 100644 src/plugins/screensaver/default/mx5500-nobody.svg create mode 100644 src/plugins/screensaver/default/mx5500.svg create mode 100644 src/plugins/screensaver/i18n/screensaver.en_GB.po create mode 100644 src/plugins/screensaver/i18n/screensaver.glade.h create mode 100644 src/plugins/screensaver/i18n/screensaver.pot create mode 100644 src/plugins/screensaver/screensaver.py create mode 100644 src/plugins/screensaver/screensaver.ui create mode 100644 src/plugins/sense/Makefile.am create mode 100644 src/plugins/sense/default/Makefile.am create mode 100644 src/plugins/sense/default/default-fan.svg create mode 100644 src/plugins/sense/default/default-menu-entry.svg create mode 100644 src/plugins/sense/default/default-none.svg create mode 100644 src/plugins/sense/default/default-volt.svg create mode 100644 src/plugins/sense/default/default.svg create mode 100644 src/plugins/sense/default/g19-fan.svg create mode 100644 src/plugins/sense/default/g19-menu-entry.svg create mode 100644 src/plugins/sense/default/g19-none.svg create mode 100644 src/plugins/sense/default/g19-volt.svg create mode 100644 src/plugins/sense/default/g19.svg create mode 100644 src/plugins/sense/default/i18n/default-none.en_GB.po create mode 100644 src/plugins/sense/default/i18n/default-none.h create mode 100644 src/plugins/sense/default/i18n/default-none.pot create mode 100644 src/plugins/sense/default/i18n/g19-none.en_GB.po create mode 100644 src/plugins/sense/default/i18n/g19-none.h create mode 100644 src/plugins/sense/default/i18n/g19-none.pot create mode 100644 src/plugins/sense/i18n/sense.en_GB.po create mode 100644 src/plugins/sense/i18n/sense.glade.h create mode 100644 src/plugins/sense/i18n/sense.pot create mode 100644 src/plugins/sense/sense.py create mode 100644 src/plugins/sense/sense.ui create mode 100644 src/plugins/stopwatch/Makefile.am create mode 100644 src/plugins/stopwatch/default/Makefile.am create mode 100644 src/plugins/stopwatch/default/default-one_timer.svg create mode 100644 src/plugins/stopwatch/default/default-two_timers.svg create mode 100644 src/plugins/stopwatch/default/default.svg create mode 100644 src/plugins/stopwatch/default/g19-one_timer.svg create mode 100644 src/plugins/stopwatch/default/g19-two_timers.svg create mode 100644 src/plugins/stopwatch/default/g19.svg create mode 100644 src/plugins/stopwatch/default/i18n/default.en_GB.po create mode 100644 src/plugins/stopwatch/default/i18n/default.h create mode 100644 src/plugins/stopwatch/default/i18n/default.pot create mode 100644 src/plugins/stopwatch/default/i18n/g19.en_GB.po create mode 100644 src/plugins/stopwatch/default/i18n/g19.h create mode 100644 src/plugins/stopwatch/default/i18n/g19.pot create mode 100644 src/plugins/stopwatch/default/i18n/mx5500.en_GB.po create mode 100644 src/plugins/stopwatch/default/i18n/mx5500.h create mode 100644 src/plugins/stopwatch/default/i18n/mx5500.pot create mode 100644 src/plugins/stopwatch/default/mx5500-one_timer.svg create mode 100644 src/plugins/stopwatch/default/mx5500-two_timers.svg create mode 100644 src/plugins/stopwatch/default/mx5500.svg create mode 100644 src/plugins/stopwatch/default/playpause.gif create mode 100644 src/plugins/stopwatch/default/reset.gif create mode 100644 src/plugins/stopwatch/default/up.gif create mode 100644 src/plugins/stopwatch/i18n/stopwatch.en_GB.po create mode 100644 src/plugins/stopwatch/i18n/stopwatch.glade.h create mode 100644 src/plugins/stopwatch/i18n/stopwatch.pot create mode 100644 src/plugins/stopwatch/preferences.py create mode 100644 src/plugins/stopwatch/stopwatch.py create mode 100644 src/plugins/stopwatch/stopwatch.ui create mode 100644 src/plugins/stopwatch/timer.py create mode 100644 src/plugins/sysmon/Makefile.am create mode 100644 src/plugins/sysmon/default/Makefile.am create mode 100644 src/plugins/sysmon/default/default.svg create mode 100644 src/plugins/sysmon/default/g19.svg create mode 100644 src/plugins/sysmon/default/i18n/default.en_GB.po create mode 100644 src/plugins/sysmon/default/i18n/default.pot create mode 100644 src/plugins/sysmon/default/i18n/g19.en_GB.po create mode 100644 src/plugins/sysmon/default/i18n/g19.pot create mode 100644 src/plugins/sysmon/default/i18n/mx5500.en_GB.po create mode 100644 src/plugins/sysmon/default/i18n/mx5500.pot create mode 100644 src/plugins/sysmon/default/mx5500.svg create mode 100644 src/plugins/sysmon/dials/Makefile.am create mode 100644 src/plugins/sysmon/dials/g19-large-needle.svg create mode 100644 src/plugins/sysmon/dials/g19-small-needle.svg create mode 100644 src/plugins/sysmon/dials/g19-tiny-needle.svg create mode 100644 src/plugins/sysmon/dials/g19.svg create mode 100644 src/plugins/sysmon/dials/sysmon_dials_g19.py create mode 100644 src/plugins/sysmon/graphs/Makefile.am create mode 100644 src/plugins/sysmon/graphs/default.svg create mode 100644 src/plugins/sysmon/graphs/g19.svg create mode 100644 src/plugins/sysmon/graphs/graphs.theme create mode 100644 src/plugins/sysmon/graphs/i18n/g19.en_GB.po create mode 100644 src/plugins/sysmon/graphs/i18n/g19.h create mode 100644 src/plugins/sysmon/graphs/i18n/g19.pot create mode 100644 src/plugins/sysmon/graphs/sysmon_graphs_default.py create mode 100644 src/plugins/sysmon/i18n/sysmon.en_GB.po create mode 100644 src/plugins/sysmon/i18n/sysmon.glade.h create mode 100644 src/plugins/sysmon/i18n/sysmon.pot create mode 100644 src/plugins/sysmon/sysmon.py create mode 100644 src/plugins/sysmon/sysmon.ui create mode 100644 src/plugins/tails/LICENSE create mode 100644 src/plugins/tails/Makefile.am create mode 100644 src/plugins/tails/README create mode 100644 src/plugins/tails/default/Makefile.am create mode 100644 src/plugins/tails/default/default-menu-entry.svg create mode 100644 src/plugins/tails/default/default-menu-screen.svg create mode 100644 src/plugins/tails/default/g19-menu-entry.svg create mode 100644 src/plugins/tails/default/g19-menu-screen.svg create mode 100644 src/plugins/tails/i18n/tails.en_GB.po create mode 100644 src/plugins/tails/i18n/tails.glade.h create mode 100644 src/plugins/tails/i18n/tails.pot create mode 100644 src/plugins/tails/tailer/Makefile.am create mode 100644 src/plugins/tails/tailer/__init__.py create mode 100644 src/plugins/tails/tails.py create mode 100644 src/plugins/tails/tails.ui create mode 100644 src/plugins/things/Makefile.am create mode 100644 src/plugins/things/cg.stuff/Makefile.am create mode 100644 src/plugins/things/cg.stuff/cairo.svg create mode 100644 src/plugins/things/clouds.stuff/Makefile.am create mode 100644 src/plugins/things/clouds.stuff/README create mode 100644 src/plugins/things/clouds.stuff/clouds.svg create mode 100644 src/plugins/things/cloudsthingum.py create mode 100644 src/plugins/things/test1.py create mode 100644 src/plugins/things/things.py create mode 100644 src/plugins/trafficstats/Makefile.am create mode 100644 src/plugins/trafficstats/default/Makefile.am create mode 100644 src/plugins/trafficstats/default/default.svg create mode 100644 src/plugins/trafficstats/default/g19.svg create mode 100644 src/plugins/trafficstats/default/mx5500.svg create mode 100644 src/plugins/trafficstats/trafficstats.png create mode 100644 src/plugins/trafficstats/trafficstats.py create mode 100644 src/plugins/trafficstats/trafficstats.ui create mode 100644 src/plugins/tweak/Makefile.am create mode 100644 src/plugins/tweak/i18n/tweak.en_GB.po create mode 100644 src/plugins/tweak/i18n/tweak.glade.h create mode 100644 src/plugins/tweak/i18n/tweak.pot create mode 100644 src/plugins/tweak/tweak.py create mode 100644 src/plugins/tweak/tweak.ui create mode 100644 src/plugins/voip-mumble/Makefile.am create mode 100644 src/plugins/voip-mumble/logo.png create mode 100644 src/plugins/voip-mumble/voip-mumble.py create mode 100644 src/plugins/voip-teamspeak3/Makefile.am create mode 100644 src/plugins/voip-teamspeak3/logo.png create mode 100644 src/plugins/voip-teamspeak3/test.py create mode 100644 src/plugins/voip-teamspeak3/ts3/Makefile.am create mode 100644 src/plugins/voip-teamspeak3/ts3/__init__.py create mode 100644 src/plugins/voip-teamspeak3/ts3/message.py create mode 100644 src/plugins/voip-teamspeak3/voip-teamspeak3.py create mode 100644 src/plugins/voip/Makefile.am create mode 100644 src/plugins/voip/buddies-only/Makefile.am create mode 100644 src/plugins/voip/buddies-only/buddies-only.theme create mode 100644 src/plugins/voip/buddies-only/default-menu-entry.svg create mode 100644 src/plugins/voip/buddies-only/default-menu-screen.svg create mode 100644 src/plugins/voip/buddies-only/g19-menu-entry.svg create mode 100644 src/plugins/voip/buddies-only/g19-menu-screen.svg create mode 100644 src/plugins/voip/default/Makefile.am create mode 100644 src/plugins/voip/default/default-menu-entry.svg create mode 100644 src/plugins/voip/default/default-menu-screen.svg create mode 100644 src/plugins/voip/default/default-message-menu-entry.svg create mode 100644 src/plugins/voip/default/default.theme create mode 100644 src/plugins/voip/default/g19-menu-entry.svg create mode 100644 src/plugins/voip/default/g19-menu-screen.svg create mode 100644 src/plugins/voip/default/g19-message-menu-entry.svg create mode 100644 src/plugins/voip/default_audio-high.gif create mode 100644 src/plugins/voip/default_audio-muted.gif create mode 100644 src/plugins/voip/default_available.gif create mode 100644 src/plugins/voip/default_away.gif create mode 100644 src/plugins/voip/default_microphone-sensitivity-high.gif create mode 100644 src/plugins/voip/default_microphone-sensitivity-muted.gif create mode 100644 src/plugins/voip/default_record.gif create mode 100644 src/plugins/voip/g19_microphone-sensitivity-high.png create mode 100644 src/plugins/voip/g19_microphone-sensitivity-muted.png create mode 100644 src/plugins/voip/voip.py create mode 100644 src/plugins/voip/voip.ui create mode 100644 src/plugins/volume/Makefile.am create mode 100644 src/plugins/volume/default/Makefile.am create mode 100644 src/plugins/volume/default/default.svg create mode 100644 src/plugins/volume/default/g19.svg create mode 100644 src/plugins/volume/default/mx5500.svg create mode 100644 src/plugins/volume/i18n/volume.en_GB.po create mode 100644 src/plugins/volume/i18n/volume.glade.h create mode 100644 src/plugins/volume/i18n/volume.pot create mode 100644 src/plugins/volume/volume.py create mode 100644 src/plugins/volume/volume.ui create mode 100644 src/plugins/weather-noaa/Makefile.am create mode 100644 src/plugins/weather-noaa/icon.png create mode 100644 src/plugins/weather-noaa/weather-noaa.py create mode 100644 src/plugins/weather-noaa/weather-noaa.ui create mode 100644 src/plugins/weather-yahoo/Makefile.am create mode 100644 src/plugins/weather-yahoo/icon.png create mode 100644 src/plugins/weather-yahoo/weather-yahoo.py create mode 100644 src/plugins/weather-yahoo/weather-yahoo.ui create mode 100644 src/plugins/weather/Makefile.am create mode 100644 src/plugins/weather/default/Makefile.am create mode 100644 src/plugins/weather/default/default.svg create mode 100644 src/plugins/weather/default/g19.svg create mode 100644 src/plugins/weather/default/mono-clouds.gif create mode 100644 src/plugins/weather/default/mono-dark-clouds.gif create mode 100644 src/plugins/weather/default/mono-few-clouds.gif create mode 100644 src/plugins/weather/default/mono-fog.gif create mode 100644 src/plugins/weather/default/mono-more-clouds.gif create mode 100644 src/plugins/weather/default/mono-rain.gif create mode 100644 src/plugins/weather/default/mono-snow.gif create mode 100644 src/plugins/weather/default/mono-sunny.gif create mode 100644 src/plugins/weather/default/mono-thunder.gif create mode 100644 src/plugins/weather/default/mx5500.svg create mode 100644 src/plugins/weather/forecasts/Makefile.am create mode 100644 src/plugins/weather/forecasts/default.svg create mode 100644 src/plugins/weather/forecasts/forecasts.theme create mode 100644 src/plugins/weather/forecasts/g19.svg create mode 100644 src/plugins/weather/forecasts/mono-clouds.gif create mode 100644 src/plugins/weather/forecasts/mono-dark-clouds.gif create mode 100644 src/plugins/weather/forecasts/mono-few-clouds.gif create mode 100644 src/plugins/weather/forecasts/mono-fog.gif create mode 100644 src/plugins/weather/forecasts/mono-more-clouds.gif create mode 100644 src/plugins/weather/forecasts/mono-rain.gif create mode 100644 src/plugins/weather/forecasts/mono-snow.gif create mode 100644 src/plugins/weather/forecasts/mono-sunny.gif create mode 100644 src/plugins/weather/forecasts/mono-thunder.gif create mode 100644 src/plugins/weather/forecasts/mx5500.svg create mode 100644 src/plugins/weather/i18n/weather.en_GB.po create mode 100644 src/plugins/weather/i18n/weather.glade.h create mode 100644 src/plugins/weather/i18n/weather.pot create mode 100644 src/plugins/weather/pywapi.py create mode 100644 src/plugins/weather/weather.py create mode 100644 src/plugins/weather/weather.ui create mode 100644 src/plugins/webkitbrowser/Makefile.am create mode 100644 src/plugins/webkitbrowser/default/Makefile.am create mode 100644 src/plugins/webkitbrowser/default/g19.svg create mode 100644 src/plugins/webkitbrowser/webkitbrowser.py create mode 100644 src/pylibg19/Makefile.am create mode 100644 src/pylibg19/g19/Makefile.am create mode 100644 src/pylibg19/g19/__init__.py create mode 100644 src/pylibg19/g19/g19.py create mode 100644 src/pylibg19/g19/globals.py create mode 100644 src/pylibg19/g19/keys.py create mode 100644 src/pylibg19/g19/receivers.py create mode 100644 src/pylibg19/g19/runnable.py create mode 100644 src/scripts/Makefile.am create mode 100755 src/scripts/evtest create mode 100755 src/scripts/g15-config create mode 100755 src/scripts/g15-desktop-service create mode 100755 src/scripts/g15-diag create mode 100755 src/scripts/g15-indicator create mode 100755 src/scripts/g15-launch create mode 100755 src/scripts/g15-support-dump create mode 100755 src/scripts/g15-system-service create mode 100755 src/scripts/g15-systemtray create mode 100755 src/scripts/lg4l-image create mode 100755 src/scripts/libg15test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b0f95a --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +*.*~ +Makefile +Makefile.in +config.log +config.status +gnome15-*.tar.gz +py-compile +compile +missing +install-sh +autom4te.cache +configure +GNOME_G15Applet.server +g15-config.desktop +g15-macros.desktop +g15-indicator.desktop +g15-systemtray.desktop +gnome15.desktop +ltoptions.m4 +ltsugar.m4 +ltversion.m4 +lt~obsolete.m4 +aclocal.m4 +libtool.m4 +*.o +*.la +*.lo +.deps +*.pyc +*.pyo +ltmain.sh +libtool +depcomp +config.sub +config.guess +src/pylibg19/dist +src/pylibg19/MANIFEST +src/pylibg19/build +.libs +en_GB +data/udev/*.rules +src/gnome15/g15globals.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1df4ffc --- /dev/null +++ b/AUTHORS @@ -0,0 +1,47 @@ +Authors: +* Brett Smith +* Nuno Araujo +* NoXPhasma +* Huskynarr + +Contains work based on "Logitech-G19-Linux-Daemon" [1], written by +"MultiCoreNop" [2]. + +Contains work based on Impulse [3], written by Ian Halpern [4]. + +Contains code taken from Pitivi [5], authored by Edward Hervey [6]. + +Contains objgraph [7], written by Marius Gedminas [8], released under +the MIT license. + +Contains code from tailer [9], written by Mike Thornton, released under +the MIT license. + +Contains code from Things [10], written by Donn. C. Ingle [11]. + +Contains code from python-teamspeak3 [12], written by Adam +Coddington, released under the MIT license. + +Contains code from pywapi [13], written by Eugene Kaznacheev [14], +released under the MIT license. + +Some of the graphical work was made by Andrea Calabrò. + +Images src/plugins/pommodoro/Machovka_tomato.png was taken from [15] +and was released under the public domain. + +1. http://github.com/MultiCoreNop/Logitech-G19-Linux-Daemon +2. http://github.com/MultiCoreNop +3. http://impulse.ian-halpern.com/ +4. +5. http://www.pitivi.org +6. Edward Hervey +7. http://mg.pov.lt/objgraph/ +8. Marius Gedminas +9. http://github.com/six8/pytailer +10. https://savannah.nongnu.org/projects/things/ +11. Donn.C.Ingle +12. https://bitbucket.org/latestrevision/python-teamspeak3/ +13. https://code.google.com/p/python-weather-api/ +14. Eugene Kaznacheev +15. http://openclipart.org/detail/2542/tomato-by-machovka diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8dfeb52 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## 1.0.0 (2017-06-18) + +Bugfixes: + + - `function --params` this is only for an example + +Features: + + - Add an not exist function (#commitid, @huskynarr) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0313a33 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@gnome15.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9f1ab7b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ChangeLog @@ -0,0 +1 @@ + diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..6e90e07 --- /dev/null +++ b/INSTALL @@ -0,0 +1,370 @@ +Installation Instructions +************************* + +Copyright (C) 1994-1996, 1999-2002, 2004-2012 Free Software Foundation, +Inc. + + Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without warranty of any kind. + +Basic Installation +================== + + Briefly, the shell commands `./configure; make; make install' should +configure, build, and install this package. The following +more-detailed instructions are generic; see the `README' file for +instructions specific to this package. Some packages provide this +`INSTALL' file but do not implement all of the features documented +below. The lack of an optional feature in a given package is not +necessarily a bug. More recommendations for GNU packages can be found +in *note Makefile Conventions: (standards)Makefile Conventions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. Caching is +disabled by default to prevent problems with accidental use of stale +cache files. + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You need `configure.ac' if +you want to change it or regenerate `configure' using a newer version +of `autoconf'. + + The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. + + Running `configure' might take a while. While running, it prints + some messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package, generally using the just-built uninstalled binaries. + + 4. Type `make install' to install the programs and any data files and + documentation. When installing into a prefix owned by root, it is + recommended that the package be configured and built as a regular + user, and only the `make install' phase executed with root + privileges. + + 5. Optionally, type `make installcheck' to repeat any self-tests, but + this time using the binaries in their final installed location. + This target does not install anything. Running this target as a + regular user, particularly if the prior `make install' required + root privileges, verifies that the installation completed + correctly. + + 6. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + + 7. Often, you can also type `make uninstall' to remove the installed + files again. In practice, not all packages have tested that + uninstallation works correctly, even though it is required by the + GNU Coding Standards. + + 8. Some packages, particularly those that use Automake, provide `make + distcheck', which can by used by developers to test that all other + targets like `make install' and `make uninstall' work correctly. + This target is generally not run by end users. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. Run `./configure --help' +for details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c99 CFLAGS=-g LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you can use GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. This +is known as a "VPATH" build. + + With a non-GNU `make', it is safer to compile the package for one +architecture at a time in the source code directory. After you have +installed the package for one architecture, use `make distclean' before +reconfiguring for another architecture. + + On MacOS X 10.5 and later systems, you can create libraries and +executables that work on multiple system types--known as "fat" or +"universal" binaries--by specifying multiple `-arch' options to the +compiler but only a single `-arch' option to the preprocessor. Like +this: + + ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ + CPP="gcc -E" CXXCPP="g++ -E" + + This is not guaranteed to produce working output in all cases, you +may have to build one architecture at a time and combine the results +using the `lipo' tool if you have problems. + +Installation Names +================== + + By default, `make install' installs the package's commands under +`/usr/local/bin', include files under `/usr/local/include', etc. You +can specify an installation prefix other than `/usr/local' by giving +`configure' the option `--prefix=PREFIX', where PREFIX must be an +absolute file name. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +pass the option `--exec-prefix=PREFIX' to `configure', the package uses +PREFIX as the prefix for installing programs and libraries. +Documentation and other data files still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=DIR' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. In general, the +default for these options is expressed in terms of `${prefix}', so that +specifying just `--prefix' will affect all of the other directory +specifications that were not explicitly provided. + + The most portable way to affect installation locations is to pass the +correct locations to `configure'; however, many packages provide one or +both of the following shortcuts of passing variable assignments to the +`make install' command line to change installation locations without +having to reconfigure or recompile. + + The first method involves providing an override variable for each +affected directory. For example, `make install +prefix=/alternate/directory' will choose an alternate location for all +directory configuration variables that were expressed in terms of +`${prefix}'. Any directories that were specified during `configure', +but not in terms of `${prefix}', must each be overridden at install +time for the entire installation to be relocated. The approach of +makefile variable overrides for each directory variable is required by +the GNU Coding Standards, and ideally causes no recompilation. +However, some platforms have known limitations with the semantics of +shared libraries that end up requiring recompilation when using this +method, particularly noticeable in packages that use GNU Libtool. + + The second method involves providing the `DESTDIR' variable. For +example, `make install DESTDIR=/alternate/directory' will prepend +`/alternate/directory' before all installation names. The approach of +`DESTDIR' overrides is not required by the GNU Coding Standards, and +does not work on platforms that have drive letters. On the other hand, +it does better at avoiding recompilation issues, and works well even +when some directory options were not specified in terms of `${prefix}' +at `configure' time. + +Optional Features +================= + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + + Some packages offer the ability to configure how verbose the +execution of `make' will be. For these packages, running `./configure +--enable-silent-rules' sets the default to minimal output, which can be +overridden with `make V=1'; while running `./configure +--disable-silent-rules' sets the default to verbose, which can be +overridden with `make V=0'. + +Particular systems +================== + + On HP-UX, the default C compiler is not ANSI C compatible. If GNU +CC is not installed, it is recommended to use the following options in +order to use an ANSI C compiler: + + ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" + +and if that doesn't work, install pre-built binaries of GCC for HP-UX. + + HP-UX `make' updates targets which have the same time stamps as +their prerequisites, which makes it generally unusable when shipped +generated files such as `configure' are involved. Use GNU `make' +instead. + + On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot +parse its `' header file. The option `-nodtk' can be used as +a workaround. If GNU CC is not installed, it is therefore recommended +to try + + ./configure CC="cc" + +and if that doesn't work, try + + ./configure CC="cc -nodtk" + + On Solaris, don't put `/usr/ucb' early in your `PATH'. This +directory contains several dysfunctional programs; working variants of +these programs are available in `/usr/bin'. So, if you need `/usr/ucb' +in your `PATH', put it _after_ `/usr/bin'. + + On Haiku, software installed for all users goes in `/boot/common', +not `/usr/local'. It is recommended to use the following options: + + ./configure --prefix=/boot/common + +Specifying the System Type +========================== + + There may be some features `configure' cannot figure out +automatically, but needs to determine by the type of machine the package +will run on. Usually, assuming the package is built to be run on the +_same_ architectures, `configure' can figure that out, but if it prints +a message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS + KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the option `--target=TYPE' to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + + Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +causes the specified `gcc' to be used as the C compiler (unless it is +overridden in the site shell script). + +Unfortunately, this technique does not work for `CONFIG_SHELL' due to +an Autoconf limitation. Until the limitation is lifted, you can use +this workaround: + + CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash + +`configure' Invocation +====================== + + `configure' recognizes the following options to control how it +operates. + +`--help' +`-h' + Print a summary of all of the options to `configure', and exit. + +`--help=short' +`--help=recursive' + Print a summary of the options unique to this package's + `configure', and exit. The `short' variant lists options used + only in the top level, while the `recursive' variant lists options + also present in any nested packages. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`--prefix=DIR' + Use DIR as the installation prefix. *note Installation Names:: + for more details, including other options available for fine-tuning + the installation locations. + +`--no-create' +`-n' + Run the configure checks, but stop before creating any output + files. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..c0edabf --- /dev/null +++ b/Makefile.am @@ -0,0 +1,5 @@ +SUBDIRS = src data man i18n + +# For Python autoconf macros +ACLOCAL_AMFLAGS = -I m4 + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..31ae182 --- /dev/null +++ b/NEWS @@ -0,0 +1,359 @@ +gnome15 0.11.0 (2018-02-10) +=========================== + +New community at https://gnome15.org/ + + +gnome15 0.10.2 (2015-05-14) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.10.0 is a major release because of the changes to the kernel +driver, which makes it incopatible with the 0.9.x series. + +So, users wanting to stick with gnome15-0.9.x should use branch `pre-refactor` +of the kernel drivers found at https://github.com/CMoH/lg4l, while +gnome15-0.10.x would rely on branch `master`. + +Also, this rather unofficial release is made outside russo79.com, since +the website is down for quite a while. The rest of the release is the same +as 0.9.8. + +A new "Pommodoro Timer" plugin is now available. +The Gnome shell extension now supports Gnome 3.10 + +Changes since 0.10.1 +-------------------- + +* g15-config: fix import error with pygobject-3.14.0 + +Changes since 0.10.0 +-------------------- + +* driver_kernel: fix keymaps for G110 and G15v2 + +Changes since 0.9.7 +------------------- + +* Break compatibility with old kernel module implementation. +* The Gnome Shell extension now displays the plugin list in + alphabetical order. (https://projects.russo79.com/issues/172) +* A new "Pommodoro Timer" plugin is now available. + (https://projects.russo79.com/issues/240) +* The Gnome Shell extension now supports Gnome 3.10. + (https://projects.russo79.com/issues/290) + +Bugs fixed in this release +-------------------------- + +* The build now displays an error error if neither PIL or pillow python + libraries are available. (https://projects.russo79.com/issues/286) +* The stop key (multimedia) now sends the correct keycode when Gnome15 + is running. (https://projects.russo79.com/issues/291) +* The weather plugin now keeps refreshing its data even if it is not + the active plugin. (https://projects.russo79.com/issues/298) +* The screensaver plugin now correctly displays the user message on + G19 keyboards. (https://projects.russo79.com/issues/299) +* The g15top module now provides a uptime method. This module is used + as a fallback replacement for python-gtop on systems that don't + provide it. (https://projects.russo79.com/issues/301) + +Under the hood changes for this release +--------------------------------------- + +* Exceptions are now logged in a consistent way + (https://projects.russo79.com/issues/269) +* XDG directories are now used instead of hard coded paths. + (https://projects.russo79.com/issues/278) +* mono icons are no longer installed by default. These icons are only + used on Ubuntu systems. (https://projects.russo79.com/issues/285) +* The python interpreter for Gnome15 scripts was changed from 'python' + to 'python2'. 'python2' is available on all the supported + distributions except Debian 7 (Wheezy). Users of Debian 7 building + Gnome15 from source must manually change the shebang lines of the + files in the src/scripts directory. + (https://projects.russo79.com/issues/289) +* User custom plugins should now be installed in + $XDG_DATA_HOME/gnome15/plugins instead of + $XDG_CONFIG_HOME/gnome15/plugins. + Support for $XDG_CONFIG_HOME/gnome15/plugins will be removed in a + future release. + +Have contributed code to this release: + +* Bram Faas + +gnome15 0.9.7 (2013-10-05) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.9.7 is mainly a bug fix release with some enhancements. +Some minor changes took place with the configuration tool. +The "Driver" tab no longer exists and the driver can now be selected +directly from the "Keyboard" tab. A new button is used to display the +driver options. + +Changes since 0.9.6 +------------------- + +* The volume monitor plugin now allows the selection of the sound card + to monitor. (https://projects.russo79.com/issues/212) +* The sense plugin now supports udisks version 2. + (https://projects.russo79.com/issues/261) +* Users using the g15direct driver can now make the G13 joystick behave + as a digital joystick. (https://projects.russo79.com/issues/280) + +Bugs fixed in this release +-------------------------- + +* The sense plugin now gracefully handles errors when it cannot connect + to a sensor source. (https://projects.russo79.com/issues/235) +* The screensaver plugin now detects Gnome Shell integrated screensaver. + (https://projects.russo79.com/issues/257) +* Fix a issue with the build system that automatically tries to build + appindicator even when the appindicator python library was not + available. (https://projects.russo79.com/issues/258) +* Fixed the license displayed in the About dialogs. + (https://projects.russo79.com/issues/259) +* Fixed the detection of the pillow library when building Gnome15. + (https://projects.russo79.com/issues/260) +* The sense plugin udisks sensor source is now periodically refreshed. + (https://projects.russo79.com/issues/262) +* The Display resolution plugin kept checking the display resolution + even when the plugin was disabled. + (https://projects.russo79.com/issues/266) +* The impulse plugin wasn't correctly linked to the libpulse and fftw3 + libraries. (https://projects.russo79.com/issues/267) +* The virtual joysticks simulated by Gnome15 are now calibrated to at + they center instead of their top-left position. + (https://projects.russo79.com/issues/271) +* Simulation of joystick events using G keys has been fixed and no + longer throws an exception. + (https://projects.russo79.com/issues/273) +* No longer display a "Unknown property: GtkMenu.ubuntu-local" when + displaying some windows. (https://projects.russo79.com/issues/274) +* Correctly simulate joystick axis movements when using the kernel + driver. (https://projects.russo79.com/issues/275) +* Correctly handle changes of the kernel driver options. + (https://projects.russo79.com/issues/276) +* The g15-system-service can now be manually stopped (useful for + development only). (https://projects.russo79.com/issues/281) + +Under the hood changes for this release +--------------------------------------- + +* The GetPagesBelowPriority D-Bus method is now marked as deprecated + and should no longer be used. + (https://projects.russo79.com/issues/270) + +Have contributed code to this release: + +* NoXPhasma + +gnome15 0.9.6 (2013-09-01) +========================== + +Gnome15 is a suite of tools for the Logitech G series keyboards and +headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 and +the Z-10 speakers aiming to provide the best integration possible with +the Linux Desktop. + +Gnome15 provides: +* A panel indicator (or applet) +* A configuration tool +* A macro system +* A set of plugins + +Developers can extend Gnome15 by writing plugins in the Python +programming language, or they can use the provided D-Bus API. +Gnome15 uses GNOME technologies, while staying well integrated with +other desktops such as Ubuntu Unity and XFCE. It should also work with +KDE. + +About this release +------------------ + +Release 0.9.6 was focused on simplifying and cleaning up the source +code tree as well as the build system. +The new structure has less levels of depth and should be simpler to +maintain on the long term. +Most of the previous sub-projects that were maintained separatedly are +now aggregated into a single tree. +Besides some external dependencies, gnome15 can now be build by +issuing a single './configure; make; make install' command. + +The kernel modules have also been updated with the latest upstream +changes. + +Of course, some bugs were also fixed. + +This is also the first release of Gnome15 to be a 'official' one. +Brett Smith, the original maintainer has decided to retire himself from +the project, and he supports what was until now a unofficial fork of +Gnome15. + +He will however keep contributing to the project. + +Changes since 0.9.5 +------------------- + +* https://projects.russo79.com/issues/256 + +Bugs fixed in this release +-------------------------- + +* https://projects.russo79.com/issues/130 +* https://projects.russo79.com/issues/242 +* https://projects.russo79.com/issues/246 +* https://projects.russo79.com/issues/247 +* https://projects.russo79.com/issues/248 +* https://projects.russo79.com/issues/250 +* https://projects.russo79.com/issues/253 +* https://projects.russo79.com/issues/254 + +Under the hood changes for this release +--------------------------------------- + +* https://projects.russo79.com/issues/171 +* https://projects.russo79.com/issues/219 +* https://projects.russo79.com/issues/245 + +gnome15 0.9.5 (2013-07-03) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since almost +seven months now. + +Changes since 0.9.4: + +* https://projects.russo79.com/issues/195 +* https://projects.russo79.com/issues/208 +* https://projects.russo79.com/issues/209 + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/173 +* https://projects.russo79.com/issues/191 +* https://projects.russo79.com/issues/194 +* https://projects.russo79.com/issues/220 +* https://projects.russo79.com/issues/223 +* https://projects.russo79.com/issues/227 +* https://projects.russo79.com/issues/228 +* https://projects.russo79.com/issues/229 +* https://projects.russo79.com/issues/232 +* https://projects.russo79.com/issues/233 +* https://projects.russo79.com/issues/234 + +Under the hood changes for this release: + +* https://projects.russo79.com/issues/190 +* https://projects.russo79.com/issues/218 +* https://projects.russo79.com/issues/226 +* https://projects.russo79.com/issues/236 + +gnome15 0.9.4 (2013-06-04) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since almost +six months now. + +Changes since 0.9.3: + +* https://projects.russo79.com/issues/196 + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/160 +* https://projects.russo79.com/issues/162 +* https://projects.russo79.com/issues/167 +* https://projects.russo79.com/issues/174 +* https://projects.russo79.com/issues/181 +* https://projects.russo79.com/issues/183 +* https://projects.russo79.com/issues/186 +* https://projects.russo79.com/issues/187 +* https://projects.russo79.com/issues/188 +* https://projects.russo79.com/issues/189 +* https://projects.russo79.com/issues/191 +* https://projects.russo79.com/issues/192 +* https://projects.russo79.com/issues/194 +* https://projects.russo79.com/issues/211 + +Under the hood changes for this release: + +* https://projects.russo79.com/issues/193 + +gnome15 0.9.3 (2013-04-29) +========================== + +This is an "unofficial" release of Gnome15. +There was no news from the original author since four +months now. + +Changes since 0.9.2: + +* Update URL for the project. + +Bugs fixed in this release: + +* https://projects.russo79.com/issues/113 +* https://projects.russo79.com/issues/148 +* https://projects.russo79.com/issues/149 +* https://projects.russo79.com/issues/150 +* https://projects.russo79.com/issues/156 +* https://projects.russo79.com/issues/160 +* https://projects.russo79.com/issues/161 +* https://projects.russo79.com/issues/162 +* https://projects.russo79.com/issues/163 +* https://projects.russo79.com/issues/167 +* https://projects.russo79.com/issues/170 + +gnome15 0.5.0 (2011-03-09) + + Big changes under the hood. g15-desktop-service is now the + process that manages the plugins, LCD and macros. Panel + integration is now provided by separate packages. + + Macro creation and editing in the configuration UI is now + possible. + + Lots of bug fixes as well (see the changelog). diff --git a/README b/README new file mode 100644 index 0000000..8682c75 --- /dev/null +++ b/README @@ -0,0 +1,40 @@ +STATUS OF GNOME15 +================= + +Gnome15 is currently **not complete maintained**. +The original primary repository has been unavailable since November 2014 due to a hosting server crash. +This fork was made to add a feature and has not been updated since November 2013, but it appears to be the latest snapshot of the repository that is currently publicly available. + +I intend to bring this repository up to date with the latest version (the version before the server crash) using the code contained in the latest distribution packages available. +We want to maintain it, so many we can. Feel free to work with. + +Gnome15 +======= + +A set of tools for configuring the Logitech G15 keyboard. + +Contains pylibg19, a library providing support for the Logitech G19 until there +is kernel support available. It was based "Logitech-G19-Linux-Daemon" [1], +the work of "MultiCoreNop" [2]. + +1. http://github.com/MultiCoreNop/Logitech-G19-Linux-Daemon +2. http://github.com/MultiCoreNop + +Installation +============ + +See the 'INSTALL' file or the [Wiki Entry](https://github.com/Huskynarr/gnome15/wiki/INSTALL) + +How to report bugs +================== + +Issues can be submited on the [github website](https://github.com/Huskynarr/gnome15/issues) [3]. + +3. https://github.com/Huskynarr/gnome15/issues + +Requirements +============ + +- Python 2.6 +- PyUSB 0.4 +- PIL (Python Image Library, just about any version should be ok) diff --git a/TRANSLATION_PROGRESS.txt b/TRANSLATION_PROGRESS.txt new file mode 100644 index 0000000..baab97c --- /dev/null +++ b/TRANSLATION_PROGRESS.txt @@ -0,0 +1,40 @@ +SVG +=== + +themes/default/mx5500-menu-screen.svg +themes/default/default-menu-screen.svg +themes/default/default-menu-child-entry.svg +themes/default/default-error-screen.svg +themes/default/default-confirmation-screen.svg +themes/default/mx5500-confirmation-screen.svg +themes/default/g19-confirmation-screen.svg +themes/default/mx5500-menu-separator.svg +themes/default/mx5500-error-screen.svg +themes/default/g19-menu-screen.svg +themes/default/default-menu-entry.svg +themes/default/default-menu-separator.svg +themes/default/g19-error-screen.svg +themes/default/mx5500-menu-child-entry.svg +themes/default/mx5500-menu-entry.svg +themes/default/g19-menu-entry.svg +themes/default/g19-menu-child-entry.svg +themes/default/g19-menu-separator.svg +main/resources/images/g19-background.svg +main/resources/images/mx5500-background.svg +main/resources/images/default-background.svg +main/resources/icons/hicolor/scalable/apps/gnome15.svg +main/resources/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg +main/resources/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg + + +Desktop +======= + +gnome/applications/g15-config.desktop +gnome/applications/g15-config.desktop.in +gnome/autostart/gnome15.desktop +gnome/autostart/gnome15.desktop.in +gnome/autostart/g15-indicator.desktop.in +gnome/autostart/g15-systemtray.desktop.in +gnome/autostart/g15-indicator.desktop +gnome/autostart/g15-systemtray.desktop diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..9da9a02 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-dinky \ No newline at end of file diff --git a/b.sh b/b.sh new file mode 100755 index 0000000..30c2cec --- /dev/null +++ b/b.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SUFFIX="" +VERSION=$(grep "AC_INIT" configure.in|awk -F, '{ print $2 }'|awk -F\) '{ print $1 }'|sed 's/ //g') + +autoreconf -f && ./configure --enable-udev=/lib/udev/rules.d && make && make dist && cp gnome15-${VERSION}.tar.gz ~/Workspaces/home\:tanktarta\:gnome15${SUFFIX}/gnome15 diff --git a/build-po.sh b/build-po.sh new file mode 100755 index 0000000..f57876f --- /dev/null +++ b/build-po.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +echo -e "Locale: \c" +read locale +if [ -n "${locale}" ]; then + + cd $(dirname $0) + basedir=$(pwd) + for i in src/*; do + if [ -d ${i} ]; then + modname=$(basename $i) + echo $modname + pushd ${i} >/dev/null + mkdir -p i18n + + # Generate python / ui + pushd i18n >/dev/null + for i in *.pot; do + bn=$(basename $i .pot).${locale}.po + msginit --no-translator --input=${i} --output=${bn} --locale=${locale} + done + popd >/dev/null + + # Generate theme + for j in * + do + if [ -d $j/i18n ]; then + pushd $j/i18n >/dev/null + for k in *.pot ; do + bn=$(basename $k .pot).${locale}.po + echo "$k -> $bn [$locale]" + msginit --no-translator --input=${k} --output=${bn} --locale=${locale} + done + popd >/dev/null + fi + done + + popd >/dev/null + fi + done +fi diff --git a/build-pot.sh b/build-pot.sh new file mode 100755 index 0000000..f9b502a --- /dev/null +++ b/build-pot.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# +# Creates the initial i18n structure for plugins +# + +cd $(dirname $0) +basedir=$(pwd) +for i in src/*; do + if [ -d ${i} ]; then + modname=$(basename $i) + pushd ${i} + mkdir -p i18n + + # Python + xgettext --language=Python --keyword=_ --output=i18n/${modname}.pot *.py + + # Theme files + + for m in *; do + if [ -d "$m" ]; then + pushd $m + if [ $(ls *.svg 2>/dev/null|wc -l) -gt 0 ]; then + mkdir -p i18n + echo "Found SVG" + for s in *.svg; do + echo "Generating C header$s" + svgname=$(basename ${s} .svg) + ${basedir}/../gnome15/mksvgheaders.py ${s} > i18n/${svgname}.h + if [ -s i18n/${svgname}.h ]; then + echo "Generating POT for ${svgname}.h" + xgettext --language=Python --keyword=_ --keyword=N_ --output=i18n/${svgname}.pot i18n/${svgname}.h + else + rm -f i18n/${svgname}.h + fi + done + fi + popd + fi + done + + # .ui files + if [ $(ls *.ui 2>/dev/null|wc -l) -gt 0 ]; then + for i in *.ui; do + intltool-extract --type=gettext/glade ${i} + uiname=$(basename $i .ui) + mv -f ${i}.h i18n + xgettext --language=Python --keyword=_ --keyword=N_ --output=i18n/${uiname}.pot i18n/${i}.h + done + fi + + popd + fi +done diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..9b110fd --- /dev/null +++ b/compile.sh @@ -0,0 +1,5 @@ +autoreconf -i +./configure +make +sudo make install + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b66330d --- /dev/null +++ b/configure.ac @@ -0,0 +1,1401 @@ +AC_INIT([gnome15], [0.11.0], [bugs@gnome15.org]) +AC_CONFIG_SRCDIR([src/gnome15/g15service.py]) +AM_INIT_AUTOMAKE([tar-ustar]) +AM_MAINTAINER_MODE + +AC_CONFIG_MACRO_DIR([m4]) + +dnl +dnl Dependencies +dnl + +dnl C compiler toolchain +LT_PREREQ([2.2.6]) +LT_INIT() + +AC_PROG_CC + +dnl Python +AM_PATH_PYTHON +AX_PYTHON_DEVEL([2.6]) + +dnl Python modules (mandatory) +PKG_CHECK_MODULES(PYGTK, pygtk-2.0) +AC_SUBST(PYGTK_CFLAGS) +AC_SUBST(PYGTK_LIBS) + +AX_PYTHON_MODULE(keyring, []) +AS_IF([test "x${HAVE_PYMOD_KEYRING}" = "xno"], + [AC_MSG_ERROR([Requires Python Keyring Library])]) + +AX_PYTHON_MODULE(virtkey, []) +AS_IF([test "x${HAVE_PYMOD_VIRTKEY}" = "xno"], + [AC_MSG_ERROR([Requires Python Virtkey Library])]) + +AX_PYTHON_MODULE(PIL.Image, []) +AS_IF([test "x${HAVE_PYMOD_PIL_IMAGE}" = "xno"], + [AC_MSG_ERROR([Requires Python Image Library])]) + +AX_PYTHON_MODULE(cairo, []) +AS_IF([test "x${HAVE_PYMOD_CAIRO}" = "xno"], + [AC_MSG_ERROR([Requires Python bindings for the Cairo vector graphics library])]) + +AX_PYTHON_MODULE(dbus, []) +AS_IF([test "x${HAVE_PYMOD_DBUS}" = "xno"], + [AC_MSG_ERROR([Requires DBUS bindings for Python])]) + +AX_PYTHON_MODULE(pyinotify, []) +AS_IF([test "x${HAVE_PYMOD_PYINOTIFY}" = "xno"], + [AC_MSG_ERROR([Requires Pyinotify bindings for Python])]) + +AX_PYTHON_MODULE(lxml, []) +AS_IF([test "x${HAVE_PYMOD_LXML}" = "xno"], + [AC_MSG_ERROR([Requires LXML bindings for Python])]) + +AX_PYTHON_MODULE(gobject, []) +AS_IF([test "x${HAVE_PYMOD_GOBJECT}" = "xno"], + [AC_MSG_ERROR([Requires GObject for Python])]) + +AX_PYTHON_MODULE(xdg, []) +AS_IF([test "x${HAVE_PYMOD_XDG}" = "xno"], + [AC_MSG_ERROR([Requires Python XDG])]) + +AX_PYTHON_MODULE(usb, []) +AS_IF([test "x${HAVE_PYMOD_USB}" = "xno"], + [AC_MSG_ERROR([Requires PyUSB, python bindings for libusb])]) + +AX_PYTHON_MODULE(gconf, []) +AS_IF([test "x${HAVE_PYMOD_GCONF}" = "xno"], + [AC_MSG_ERROR([Requires GConf bindings for Python])]) + +AX_PYTHON_MODULE(rsvg, []) +AS_IF([test "x${HAVE_PYMOD_RSVG}" = "xno"], + [AC_MSG_ERROR([Requires RSVG for Python])]) + +AX_PYTHON_MODULE(pango, []) +AS_IF([test "x${HAVE_PYMOD_PANGO}" = "xno"], + [AC_MSG_ERROR([Requires Pango for Python])]) + +AX_PYTHON_MODULE(uinput, []) +AS_IF([test "x${HAVE_PYMOD_UINPUT}" = "xno"], + [AC_MSG_ERROR([Requires Python uinput and libsuinput])]) + +AX_PYTHON_MODULE(Xlib, []) +AS_IF([test "x${HAVE_PYMOD_XLIB}" = "xno"], + [AC_MSG_ERROR([Requires Python Xlib - Python Xlib bindings])]) + +dnl Python modules (optional) +AX_PYTHON_MODULE(setproctitle, []) +AS_IF([test "x${HAVE_PYMOD_SETPROCTITLE}" = "xno"], + [AC_MSG_WARN([It is recommend that setproctitle is installed])]) + +AX_PYTHON_MODULE(pyudev, []) +AS_IF([test "x${HAVE_PYUDEV}" = "xno"], + [AC_MSG_WARN([It is recommended that PyUdev is installed. Without this, there will be no hot-plugging support])]) + +dnl Python modules (mandatory for some drivers) +AX_PYTHON_MODULE(pyinputevent, []) +AS_IF([test "x${HAVE_PYMOD_PYINPUTEVENT}" = "xyes"], + [have_pyinputevent=yes], + [have_pyinputevent=no]) + +dnl Python modules (mandatory for some plugins) +AX_PYTHON_MODULE(gst, []) +AS_IF([test "x${HAVE_PYMOD_GST}" = "xyes"], + [have_gst=yes], + [have_gst=no]) + +AX_PYTHON_MODULE(appindicator, []) +AS_IF([test "x${HAVE_PYMOD_APPINDICATOR}" = "xyes"], + [have_appindicator=yes], + [have_appindicator=no]) + +AX_PYTHON_MODULE(telepathy, []) +AS_IF([test "x${HAVE_PYMOD_TELEPATHY}" = "xyes"], + [have_telepathy=yes], + [have_telepathy=no]) + +AX_PYTHON_MODULE(alsaaudio, []) +AS_IF([test "x${HAVE_PYMOD_ALSAAUDIO}" = "xyes"], + [have_alsaaudio=yes], + [have_alsaaudio=no]) + +AX_PYTHON_MODULE(feedparser, []) +AS_IF([test "x${HAVE_PYMOD_FEEDPARSER}" = "xyes"], + [have_feedparser=yes], + [have_feedparser=no]) + +AX_PYTHON_MODULE(vobject, []) +AS_IF([test "x${HAVE_PYMOD_VOBJECT}" = "xyes" ], + [have_vobject=yes], + [have_vobject=no]) + +AX_PYTHON_MODULE(gdata.calendar, []) +AS_IF([test "x${HAVE_PYMOD_GDATA_CALENDAR}" = "xyes" ], + [have_gdata_calendar=yes], + [have_gdata_calendar=no]) + +AX_PYTHON_MODULE(gdata.analytics, []) +AS_IF([test "x${HAVE_PYMOD_GDATA_ANALYTICS}" = "xyes" ], + [have_gdata_analytics=yes], + [have_gdata_analytics=no]) + +AX_PYTHON_MODULE(cairoplot, []) +AS_IF([test "x${HAVE_PYMOD_CAIROPLOT}" = "xyes" ], + [have_cairoplot=yes], + [have_cairoplot=no]) + +AX_PYTHON_MODULE(sensors, []) +AS_IF([test "x${HAVE_PYMOD_SENSORS}" = "xyes" ], + [have_sensors=yes], + [have_sensors=no]) + +dnl libg15 (Gnome15 version) +AC_CHECK_LIB(g15, initLibG15, + [have_libg15=yes], + [have_libg15=no]) + +dnl fftw3 +PKG_CHECK_MODULES(FFTW, fftw3, + [have_fftw3=yes], + [have_fftw3=no]) +AC_SUBST(FFTW_CFLAGS) +AC_SUBST(FFTW_LIBS) + +dnl libpulse +PKG_CHECK_MODULES(PULSE, libpulse, + [have_pulse=yes], + [have_pulse=no]) +AC_SUBST(PULSE_CFLAGS) +AC_SUBST(PULSE_LIBS) + +dnl +dnl Parse configure arguments +dnl + +dnl Enabled locales +AC_ARG_VAR(ENABLED_LOCALES, [List of locales to enable]) +AS_IF([test -z "${ENABLED_LOCALES}"], + AC_SUBST([ENABLED_LOCALES], [en_GB])) + +dnl Name of Fixed size font +AC_ARG_VAR(FIXED_SIZE_FONT, [Font to use for fixed fonts. Defaults to Fixed]) +AS_IF([test -z "${FIXED_SIZE_FONT}"], + AC_SUBST([FIXED_SIZE_FONT], [Fixed])) + +dnl udev file path and settings +AC_ARG_VAR(UDEV_RULES_PATH, [Path for udev rules. Defaults to /lib/udev/rules.d]) +AS_IF([test -z "${UDEV_RULES_PATH}"], + AC_SUBST([UDEV_RULES_PATH], [/lib/udev/rules.d])) +AC_ARG_VAR(DEVICEGROUP, [Group that the devices will be owned by. Defaults to plugdev]) +AS_IF([test -z "${DEVICEGROUP}"], + AC_SUBST([DEVICEGROUP], [plugdev])) +AC_ARG_VAR(DEVICEMODE, [Permissions of the devices. Defaults to 0660]) +AS_IF([test -z "${DEVICEMODE}"], + AC_SUBST([DEVICEMODE], [0660])) + +dnl Drivers +AC_ARG_ENABLE([driver-kernel], + [AS_HELP_STRING([--enable-driver-kernel], + [Enable Kernel driver support (requires pyinputevent and lg4l kernel drivers).])], + [case "${enableval}" in + yes) driver_kernel=yes ;; + no) driver_kernel=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-kernel]) ;; + esac], + [driver_kernel=${have_pyinputevent}]) +AS_IF([test "x${driver_kernel}" = "xyes" -a "x$have_pyinputevent" = "xno"], + [AC_MSG_ERROR([kernel driver cannot be built without pyinputevent])]) +AM_CONDITIONAL([ENABLE_DRIVER_KERNEL], [test x$driver_kernel = xyes]) + +AC_ARG_ENABLE([driver-g19direct], + [AS_HELP_STRING([--enable-driver-g19direct], + [Enable G19Direct driver support.])], + [case "${enableval}" in + yes) driver_g19direct=yes ;; + no) driver_g19direct=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g19direct]) ;; + esac], + [driver_g19direct=yes]) +AM_CONDITIONAL([ENABLE_DRIVER_G19DIRECT], [test x$driver_g19direct = xyes]) + +AC_ARG_ENABLE([driver-g930], + [AS_HELP_STRING([--enable-driver-g930], + [Enable G930 headset driver support])], + [case "${enableval}" in + yes) driver_g930=yes ;; + no) driver_g930=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g930]) ;; + esac], + [driver_g930=yes]) +AM_CONDITIONAL([ENABLE_DRIVER_G930], [test x$driver_g930 = xyes]) + +AC_ARG_ENABLE([driver-g15direct], + [AS_HELP_STRING([--enable-driver-g15direct], + [Enable G15 direct driver support (requires libg15).])], + [case "${enableval}" in + yes) driver_g15direct=yes ;; + no) driver_g15direct=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-driver-g15direct]) ;; + esac], + [driver_g15direct=${have_libg15}]) +AS_IF([test "x${driver_g15direct}" = "xyes" -a "x$have_libg15" = "xno"], + [AC_MSG_ERROR([g5direct driver cannot be built without libg15])]) +AM_CONDITIONAL([ENABLE_DRIVER_G15DIRECT], [test x$driver_g15direct = xyes]) + +dnl System Tray +AC_ARG_ENABLE([systemtray], + [AS_HELP_STRING([--enable-systemtray], + [Enable System Tray panel integration.])], + [case "${enableval}" in + yes) systemtray=yes ;; + no) systemtray=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-systemtray]) ;; + esac], + systemtray=yes) +AM_CONDITIONAL([ENABLE_SYSTEMTRAY], [test x$systemtray = xyes]) + +dnl Ubuntu Indicator +AC_ARG_ENABLE([indicator], + [AS_HELP_STRING([--enable-indicator], + [Enable Ubuntu Indicator integration.])], + [case "${enableval}" in + yes) indicator=yes ;; + no) indicator=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-indicator]) ;; + esac], + indicator=${have_appindicator}) +AS_IF([test "x${indicator}" = "xyes" -a "x$have_appindicator" = "xno"], + [AC_MSG_ERROR([Ubuntu indicator cannot be built without appindicator python module])]) +AM_CONDITIONAL([ENABLE_INDICATOR], [test x$indicator = xyes]) + +dnl Gnome Shell Extension +AC_ARG_ENABLE([gnome-shell-extension], + [AS_HELP_STRING([--enable-gnome-shell-extension], + [Enable Gnome Shell extension.])], + [case "${enableval}" in + yes) gnome_shell_extension=yes ;; + no) gnome_shell_extension=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-gnome-shell-extension]) ;; + esac], + gnome_shell_extension=yes) +AM_CONDITIONAL([ENABLE_GNOME_SHELL_EXTENSION], [test x$gnome_shell_extension = xyes]) + +dnl Icons + +AC_ARG_ENABLE([icons-awoken], + [AS_HELP_STRING([--enable-icons-awoken], + [Build and deploy awoken icons.])], + [case "${enableval}" in + yes) icons_awoken=yes ;; + no) icons_awoken=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-icons-awoken]) ;; + esac], + [icons_awoken=no]) +AM_CONDITIONAL([ENABLE_ICONS_AWOKEN], [test x$icons_awoken = xyes]) + +AC_ARG_ENABLE([icons-mono], + [AS_HELP_STRING([--enable-icons-mono], + [Build and deploy monochrome icons.])], + [case "${enableval}" in + yes) icons_mono=yes ;; + no) icons_mono=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-icons-mono]) ;; + esac], + [icons_mono=no]) +AM_CONDITIONAL([ENABLE_ICONS_MONO], [test x$icons_mono = xyes]) + +dnl +dnl Plugins +dnl + +dnl Each plugin by default is only enabled +dnl if dependencies are available. Each plugin may also be +dnl individually enabled / disabled using configure options + +dnl Background plugin +AC_ARG_ENABLE([plugin-background], + [AS_HELP_STRING([--enable-plugin-background], + [Enable Background plugin.])], + [case "${enableval}" in + yes) plugin_background=yes ;; + no) plugin_background=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-background]) ;; + esac], + [plugin_background=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_BACKGROUND], [test x$plugin_background = xyes]) + +dnl Cairo Clock plugin +AC_ARG_ENABLE([plugin-cairo-clock], + [AS_HELP_STRING([--enable-plugin-cairo-clock], + [Enable Cairo Clock plugin.])], + [case "${enableval}" in + yes) plugin_cairo_clock=yes ;; + no) plugin_cairo_clock=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cairo-clock]) ;; + esac], + [plugin_cairo_clock=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAIRO_CLOCK], [test x$plugin_cairo_clock = xyes]) + +dnl Run App plugin +AC_ARG_ENABLE([plugin-rundapp], + [AS_HELP_STRING([--enable-plugin-rundapp], + [Enable Run App plugin.])], + [case "${enableval}" in + yes) plugin_rundapp=yes ;; + no) plugin_rundapp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-rundapp]) ;; + esac], + [plugin_rundapp=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_RUNDAPP], [test x$plugin_cairo_clock = xyes]) + +dnl Clock plugin +AC_ARG_ENABLE([plugin-clock], + [AS_HELP_STRING([--enable-plugin-clock], + [Enable Simple Clock plugin (plugin used in website example).])], + [case "${enableval}" in + yes) plugin_clock=yes ;; + no) plugin_clock=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-clock]) ;; + esac], + [plugin_clock=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CLOCK], [test x$plugin_clock = xyes]) + +dnl Special effects plugin +AC_ARG_ENABLE([plugin-fx], + [AS_HELP_STRING([--enable-plugin-fx], + [Enable Special Effects plugin.])], + [case "${enableval}" in + yes) plugin_fx=yes ;; + no) plugin_fx=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-fx]) ;; + esac], + [plugin_fx=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_FX], [test x$plugin_fx = xyes]) + +dnl G15daemon Server plugin +AC_ARG_ENABLE([plugin-g15daemon-server], + [AS_HELP_STRING([--enable-plugin-g15daemon-server], + [Enable G15Daemon Server plugin (network server with protocol compatible with g15daemon, allows g15daemon scripts to be used when it is not installed).])], + [case "${enableval}" in + yes) plugin_g15daemon_server=yes ;; + no) plugin_g15daemon_server=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-g15daemon-server]) ;; + esac], + [plugin_g15daemon_server=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_G15DAEMON_SERVER], [test x$plugin_g15daemon_server = xyes]) + +dnl Instant Messenger plugin +AC_ARG_ENABLE([plugin-im], + [AS_HELP_STRING([--enable-plugin-im], + [Enable Instant Messenger plugin. Displays current contact list and status (currently works with Telepathy framework based clients).])], + [case "${enableval}" in + yes) plugin_im=yes ;; + no) plugin_im=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-im]) ;; + esac], + [plugin_im=${have_telepathy}]) +AS_IF([test "x${plugin_im}" = "xyes" -a "x$have_telepathy" = "xno"], + [AC_MSG_ERROR([Plugin Instant Messenger cannot be built without telepathy python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_IM], [test x$plugin_im = xyes]) + +dnl Macro Recorder plugin +AC_ARG_ENABLE([plugin-macro-recorder], + [AS_HELP_STRING([--enable-plugin-macro-recorder], + [Enable Macro Recorder plugin.])], + [case "${enableval}" in + yes) plugin_macro_recorder=yes ;; + no) plugin_macro_recorder=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-macro-recorder]) ;; + esac], + [plugin_macro_recorder=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MACRO_RECORDER], [test x$plugin_macro_recorder = xyes]) + +dnl Macro Display plugin +AC_ARG_ENABLE([plugin-macros], + [AS_HELP_STRING([--enable-plugin-macros], + [Enable Macro Display plugin.])], + [case "${enableval}" in + yes) plugin_macros=yes ;; + no) plugin_macros=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-macros]) ;; + esac], + [plugin_macros=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MACROS], [test x$plugin_macros = xyes]) + +dnl Profile selection plugin +AC_ARG_ENABLE([plugin-profiles], + [AS_HELP_STRING([--enable-plugin-profiles], + [Enable profile selector plugin.])], + [case "${enableval}" in + yes) plugin_profiles=yes ;; + no) plugin_profiles=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-profiles]) ;; + esac], + [plugin_profiles=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PROFILES], [test x$plugin_profiles = xyes]) + +dnl Menu plugin +AC_ARG_ENABLE([plugin-menu], + [AS_HELP_STRING([--enable-plugin-menu], + [Enable Menu plugin.])], + [case "${enableval}" in + yes) plugin_menu=yes ;; + no) plugin_menu=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-menu]) ;; + esac], + [plugin_menu=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MENU], [test x$plugin_menu = xyes]) + +dnl Mounts plugin +AC_ARG_ENABLE([plugin-mounts], + [AS_HELP_STRING([--enable-plugin-mounts], + [Enable Mounts plugin.])], + [case "${enableval}" in + yes) plugin_mounts=yes ;; + no) plugin_mounts=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mounts]) ;; + esac], + [plugin_mounts=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MOUNTS], [test x$plugin_mounts = xyes]) + +dnl MPRIS plugin +AC_ARG_ENABLE([plugin-mpris], + [AS_HELP_STRING([--enable-plugin-mpris], + [Enable MPRIS plugin. Displays currently playing media players that support MPRIS])], + [case "${enableval}" in + yes) plugin_mpris=yes ;; + no) plugin_mpris=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mpris]) ;; + esac], + [plugin_mpris=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_MPRIS], [test x$plugin_mpris = xyes]) + +dnl Notify plugin +AC_ARG_ENABLE([plugin-notify-lcd], + [AS_HELP_STRING([--enable-plugin-notify-lcd], + [Enable Notify LCD plugin. Takes over as notification daemon and displays messages on LCD, blinks keyboard])], + [case "${enableval}" in + yes) plugin_notify_lcd=yes ;; + no) plugin_notify_lcd=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-notify-lcd]) ;; + esac], + [plugin_notify_lcd=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_NOTIFY_LCD], [test x$plugin_notify_lcd = xyes]) + +dnl Panel plugin +AC_ARG_ENABLE([plugin-panel], + [AS_HELP_STRING([--enable-plugin-panel], + [Enable Panel plugin. Reserves area of screen for other plugins to display permanent information])], + [case "${enableval}" in + yes) plugin_panel=yes ;; + no) plugin_panel=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-panel]) ;; + esac], + [plugin_panel=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PANEL], [test x$plugin_panel = xyes]) + +dnl Screensaver plugin +AC_ARG_ENABLE([plugin-screensaver], + [AS_HELP_STRING([--enable-plugin-screensaver], + [Enable Screensaver plugin. Displays mesage and dims keyboard when desktop screesaver is activated])], + [case "${enableval}" in + yes) plugin_screensaver=yes ;; + no) plugin_screensaver=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-screensaver]) ;; + esac], + [plugin_screensaver=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_SCREENSAVER], [test x$plugin_screensaver = xyes]) + +dnl Stopwatch plugin +AC_ARG_ENABLE([plugin-stopwatch], + [AS_HELP_STRING([--enable-plugin-stopwatch], + [Enable Stopwatch plugin. Dual mode, dual timer stopwatch])], + [case "${enableval}" in + yes) plugin_stopwatch=yes ;; + no) plugin_stopwatch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-stopwatch]) ;; + esac], + [plugin_stopwatch=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_STOPWATCH], [test x$plugin_stopwatch = xyes]) + +dnl Media Player plugin +AC_ARG_ENABLE([plugin-mediaplayer], + [AS_HELP_STRING([--enable-plugin-mediaplayer], + [Enable Media Player plugin. Requires GStreamer])], + [case "${enableval}" in + yes) plugin_mediaplayer=yes ;; + no) plugin_mediaplayer=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-mediaplayer]) ;; + esac], + [plugin_mediaplayer=${have_gst}]) +AS_IF([test "x${plugin_mediaplayer}" = "xyes" -a "x$have_gst" = "xno"], + [AC_MSG_ERROR([Plugin Mediaplayer cannot be built without gst python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_MEDIAPLAYER], [test x$plugin_mediaplayer = xyes]) + +dnl Weather plugin +AC_ARG_ENABLE([plugin-weather], + [AS_HELP_STRING([--enable-plugin-weather], + [Enable Weather plugin. Requires additional backend plugin such as weather-noaa])], + [case "${enableval}" in + yes) plugin_weather=yes ;; + no) plugin_weather=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather]) ;; + esac], + [plugin_weather=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER], [test x$plugin_weather = xyes]) + +AC_ARG_ENABLE([plugin-weather-noaa], + [AS_HELP_STRING([--enable-plugin-weather-noaa], + [Enable NOAA support for the Weather plugin.])], + [case "${enableval}" in + yes) plugin_weather_noaa=yes ;; + no) plugin_weather_noaa=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather-noaa]) ;; + esac], + [plugin_weather_noaa=${plugin_weather}]) +AS_IF([test "x${plugin_weather_noaa}" = "xyes" -a "x$plugin_weather" = "xno"], + [AC_MSG_ERROR([Weather NOAA backend plugin cannot be built without Weather plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER_NOAA], [test x$plugin_weather_noaa = xyes]) + +AC_ARG_ENABLE([plugin-weather-yahoo], + [AS_HELP_STRING([--enable-plugin-weather-yahoo], + [Enable Yahoo support for the Weather plugin.])], + [case "${enableval}" in + yes) plugin_weather_yahoo=yes ;; + no) plugin_weather_yahoo=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-weather-yahoo]) ;; + esac], + [plugin_weather_yahoo=${plugin_weather}]) +AS_IF([test "x${plugin_weather_yahoo}" = "xyes" -a "x$plugin_weather" = "xno"], + [AC_MSG_ERROR([Weather Yahoo backend plugin cannot be built without Weather plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEATHER_YAHOO], [test x$plugin_weather_yahoo = xyes]) + +dnl Indicator Messages Plugin +AC_ARG_ENABLE([plugin-indicator-messages], + [AS_HELP_STRING([--enable-plugin-indicator-messages], + [Enable Indicator Messages plugin.])], + [case "${enableval}" in + yes) plugin_indicator_messages=yes ;; + no) plugin_indicator_messages=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-indicator-messages]) ;; + esac], + [plugin_indicator_messages=${have_appindicator}]) +AS_IF([test "x${plugin_indicator_messages}" = "xyes" -a "x$have_appindicator" = "xno"], + [AC_MSG_ERROR([Plugin Indicator Messages cannot be built without appindicator python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_INDICATOR_MESSAGES], [test x$plugin_indicator_messages = xyes]) + +dnl ALSA Volume Monitor Plugin +AC_ARG_ENABLE([plugin-volume], + [AS_HELP_STRING([--enable-volume], + [Enable ALSA volume monitor plugin. Requires python-alsaaudio])], + [case "${enableval}" in + yes) plugin_volume=yes ;; + no) plugin_volume=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-volume]) ;; + esac], + [plugin_volume=${have_alsaaudio}]) +AS_IF([test "x${plugin_volume}" = "xyes" -a "x$have_alsaaudio" = "xno"], + [AC_MSG_ERROR([Plugin Volume cannot be built without alsaaudio python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOLUME], [test x$plugin_volume = xyes]) + +dnl RSS Plugin +AC_ARG_ENABLE([plugin-rss], + [AS_HELP_STRING([--enable-plugin-rss], + [Enable RSS feed plugin. Requires python feedparser])], + [case "${enableval}" in + yes) plugin_rss=yes ;; + no) plugin_rss=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-rss]) ;; + esac], + [plugin_rss=${have_feedparser}]) +AS_IF([test "x${plugin_rss}" = "xyes" -a "x$have_feedparser" = "xno"], + [AC_MSG_ERROR([Plugin RSS cannot be built without feedparser python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_RSS], [test x$plugin_rss = xyes]) + +dnl System Monitor Plugin +AC_ARG_ENABLE([plugin-sysmon], + [AS_HELP_STRING([--enable-plugin-sysmon], + [Enable System Monitor plugin. Recommends python gtop])], + [case "${enableval}" in + yes) plugin_sysmon=yes ;; + no) plugin_sysmon=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-sysmon]) ;; + esac], + [plugin_sysmon=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_SYSMON], [test x$plugin_sysmon = xyes]) + +dnl Processes Plugin +AC_ARG_ENABLE([plugin-processes], + [AS_HELP_STRING([--enable-plugin-processes], + [Enable Processes plugin. Recommends python gtop])], + [case "${enableval}" in + yes) plugin_processes=yes ;; + no) plugin_processes=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-processes]) ;; + esac], + [plugin_processes=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_PROCESSES], [test x$plugin_processes = xyes]) + +dnl Debug Plugin +AC_ARG_ENABLE([plugin-debug], + [AS_HELP_STRING([--enable-plugin-debug], + [Enable Debug plugin.])], + [case "${enableval}" in + yes) plugin_debug=yes ;; + no) plugin_debug=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-debug]) ;; + esac], + [plugin_debug=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_DEBUG], [test x$plugin_debug = xyes]) + +dnl Calendar plugin (base) +AC_ARG_ENABLE([plugin-cal], + [AS_HELP_STRING([--enable-plugin-cal], + [Enable calendar plugin. (required for any calendar support)])], + [case "${enableval}" in + yes) plugin_cal=yes ;; + no) plugin_cal=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal]) ;; + esac], + [plugin_cal=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL], [test x$plugin_cal = xyes]) + +dnl Evolution Calendar plugin +AS_IF([test "x${plugin_cal}" = "xyes" -a "x${have_vobject}" = "xyes" ], + [deps_plugin_cal_evolution=yes], + [deps_plugin_cal_evolution=no]) +AC_ARG_ENABLE([plugin-cal-evolution], + [AS_HELP_STRING([--enable-plugin-cal-evolution], + [Enable Evolution calendar plugin. Requires python vobject])], + [case "${enableval}" in + yes) plugin_cal_evolution=yes ;; + no) plugin_cal_evolution=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal-evolution]) ;; + esac], + [plugin_cal_evolution=${deps_plugin_cal_evolution}]) +AS_IF([test "x${plugin_cal_evolution}" = "xyes" -a "x$plugin_cal" = "xno"], + [AC_MSG_ERROR([Plugin Evolution calendar cannot be built without Calendar plugin])]) +AS_IF([test "x${plugin_cal_evolution}" = "xyes" -a "x$have_vobject" = "xno"], + [AC_MSG_ERROR([Plugin Evolution calendar cannot be built without vobject python module])]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL_EVOLUTION], [test x$plugin_cal_evolution = xyes]) + +dnl Google Calendar plugin +AS_IF([test "x${plugin_cal}" = "xyes" -a "x${have_gdata_calendar}" = "xyes" ], + [deps_plugin_cal_google=yes], + [deps_plugin_cal_google=no]) +AC_ARG_ENABLE([plugin-cal-google], + [AS_HELP_STRING([--enable-plugin-cal-google], + [Enable Google calendar plugin. Requires calendar plugin, python gdata])], + [case "${enableval}" in + yes) plugin_cal_google=yes ;; + no) plugin_cal_google=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-cal-google]) ;; + esac], + [plugin_cal_google=${deps_plugin_cal_google}]) +AS_IF([test "x${plugin_cal_google}" = "xyes" -a "x$plugin_cal" = "xno"], + [AC_MSG_ERROR([Plugin Google calendar cannot be built without Calendar plugin])]) +AS_IF([test "x${plugin_cal_google}" = "xyes" -a "x$have_gdata_calendar" = "xno"], + [AC_MSG_ERROR([Plugin Google calendar cannot be built without gdata python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_CAL_GOOGLE], [test x$plugin_cal_google = xyes]) + +dnl Google Analytics plugin +AS_IF([test "x${have_gdata_analytics}" = "xyes" -a "x${have_cairoplot}" = "xyes" ], + [deps_plugin_google_analytics=yes], + [deps_plugin_google_analytics=no]) +AC_ARG_ENABLE([plugin-google-analytics], + [AS_HELP_STRING([--enable-plugin-google-analytics], + [Enable Google Analytics plugin. Requires python gdata and cairoplot])], + [case "${enableval}" in + yes) plugin_google_analytics=yes ;; + no) plugin_google_analytics=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-google-analytics]) ;; + esac], + [plugin_google_analytics=${deps_plugin_google_analytics}]) +AS_IF([test "x${plugin_google_analytics}" = "xyes" -a "x$have_gdata_analytics" = "xno"], + [AC_MSG_ERROR([Plugin Google analytics cannot be built without gdata python modules])]) +AS_IF([test "x${plugin_google_analytics}" = "xyes" -a "x$have_cairoplot" = "xno"], + [AC_MSG_ERROR([Plugin Google analytics cannot be built without cairoplot python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_GOOGLE_ANALYTICS], [test x$plugin_google_analytics = xyes]) + +dnl POP3/IMAP Email Checker plugin +AC_ARG_ENABLE([plugin-lcdbiff], + [AS_HELP_STRING([--enable-plugin-lcdbiff], + [Enable POP3 / IMAP email checker.])], + [case "${enableval}" in + yes) plugin_lcdbiff=yes ;; + no) plugin_lcdbiff=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lcdbiff]) ;; + esac], + [plugin_lcdbiff=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_LCDBIFF], [test x$plugin_lcdbiff = xyes]) + +dnl Sensors plugin +AC_ARG_ENABLE([plugin-sense], + [AS_HELP_STRING([--enable-plugin-sense], + [Enable Sense plugin. Requires pysensors])], + [case "${enableval}" in + yes) plugin_sense=yes ;; + no) plugin_sense=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-sense]) ;; + esac], + [plugin_sense=${have_sensors}]) +AS_IF([test "x${plugin_sense}" = "xyes" -a "x$have_sensors" = "xno"], + [AC_MSG_ERROR([Plugin Sense cannot be built without sensors python modules])]) +AM_CONDITIONAL([ENABLE_PLUGIN_SENSE], [test x$plugin_sense = xyes]) + +dnl LCDShot plugin +AC_ARG_ENABLE([plugin-lcdshot], + [AS_HELP_STRING([--enable-plugin-lcdshot], + [Enable LCDShot plugin. Take a picture of whatever is on the LCD])], + [case "${enableval}" in + yes) plugin_lcdshot=yes ;; + no) plugin_lcdshot=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lcdshot]) ;; + esac], + [plugin_lcdshot=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_LCDSHOT], [test x$plugin_lcdshot = xyes]) + +dnl Tweak plugin +AC_ARG_ENABLE([plugin-tweak], + [AS_HELP_STRING([--enable-plugin-tweak], + [Enable Tweak plugin.])], + [case "${enableval}" in + yes) plugin_tweak=yes ;; + no) plugin_tweak=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-tweak]) ;; + esac], + [plugin_tweak=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TWEAK], [test x$plugin_tweak = xyes]) + +dnl Tails plugin +AC_ARG_ENABLE([plugin-tails], + [AS_HELP_STRING([--enable-plugin-tails], + [Enable Tails.])], + [case "${enableval}" in + yes) plugin_tails=yes ;; + no) plugin_tails=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-tails]) ;; + esac], + [plugin_tails=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TAILS], [test x$plugin_tails = xyes]) + +dnl Display plugin +AC_ARG_ENABLE([plugin-display], + [AS_HELP_STRING([--enable-plugin-display], + [Enable Display (XRandR for resolutions and rotation).])], + [case "${enableval}" in + yes) plugin_display=yes ;; + no) plugin_display=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-display]) ;; + esac], + [plugin_display=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_DISPLAY], [test x$plugin_display = xyes]) + +dnl Voip +AC_ARG_ENABLE([plugin-voip], + [AS_HELP_STRING([--enable-plugin-voip], + [Enable Voip plugin. Integrate with Voip apps. Requires backend plugin as well (e.g. voip-teamspeak3)])], + [case "${enableval}" in + yes) plugin_voip=yes ;; + no) plugin_voip=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-voip]) ;; + esac], + [plugin_voip=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOIP], [test x$plugin_voip = xyes]) + +AC_ARG_ENABLE([plugin-voip-teamspeak3], + [AS_HELP_STRING([--enable-plugin-voip-teamspeak3], + [Enable Teamspeak3 plugin. Requires Voip plugin as well])], + [case "${enableval}" in + yes) plugin_voip_teamspeak3=yes ;; + no) plugin_voip_teamspeak3=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-voip-teamspeak3]) ;; + esac], + [plugin_voip_teamspeak3=${plugin_voip}]) +AS_IF([test "x${plugin_voip_teamspeak3}" = "xyes" -a "x$plugin_voip" = "xno"], + [AC_MSG_ERROR([Plugin Teamspeak3 cannot be built without Voip plugin])]) +AM_CONDITIONAL([ENABLE_PLUGIN_VOIP_TEAMSPEAK3], [test x$plugin_voip_teamspeak3 = xyes]) + +dnl Traffic Stats Plugin +AC_ARG_ENABLE([plugin-trafficstats], + [AS_HELP_STRING([--enable-plugin-trafficstats], + [Enable Traffic Stats plugin.])], + [case "${enableval}" in + yes) plugin_trafficstats=yes ;; + no) plugin_trafficstats=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-trafficstats]) ;; + esac], + [plugin_trafficstats=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_TRAFFIC_STATS], [test x$plugin_trafficstats = xyes]) + +dnl impulse15 plugin +AS_IF([test "x${have_fftw3}" = "xyes" \ + -a "x${have_pulse}" = "xyes"], + [deps_plugin_impulse15=yes], + [deps_plugin_impulse15=no]) +AC_ARG_ENABLE([plugin-impulse15], + [AS_HELP_STRING([--enable-plugin-impulse15], + [Enable Impulse15 plugin.])], + [case "${enableval}" in + yes) plugin_impulse15=yes ;; + no) plugin_impulse15=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-impulse15]) ;; + esac], + [plugin_impulse15=${deps_plugin_impulse15}]) +AS_IF([test "x${plugin_impulse15}" = "xyes" -a "x$have_fftw3" = "xno"], + [AC_MSG_ERROR([Plugin Impulse15 cannot be built without fftw3])]) +AS_IF([test "x${plugin_impulse15}" = "xyes" -a "x$have_pulse" = "xno"], + [AC_MSG_ERROR([Plugin Impulse15 cannot be built without pulse library])]) +AM_CONDITIONAL([ENABLE_PLUGIN_IMPULSE15], [test x$plugin_impulse15 = xyes]) + +dnl Pommodoro Timer plugin +AC_ARG_ENABLE([plugin-pommodoro], + [AS_HELP_STRING([--enable-plugin-pommodoro], + [Enable Pommodoro Timer plugin.])], + [case "${enableval}" in + yes) plugin_pommodoro=yes ;; + no) plugin_pommodoro=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-pommodoro]) ;; + esac], + [plugin_pommodoro=yes]) +AM_CONDITIONAL([ENABLE_PLUGIN_POMMODORO], [test x$plugin_pommodoro = xyes]) + +dnl +dnl Experimental (under development) plugins +dnl + +dnl Nexuiz plugin +AC_ARG_ENABLE([plugin-game-nexuiz], + [AS_HELP_STRING([--enable-plugin-game-nexuiz], + [Enable Nexuiz plugin.])], + [case "${enableval}" in + yes) plugin_game_nexuiz=yes ;; + no) plugin_game_nexuiz=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-game-nexuiz]) ;; + esac], + [plugin_game_nexuiz=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_GAME_NEXUIZ], [test x$plugin_game_nexuiz = xyes]) + +dnl Backlight plugin +AC_ARG_ENABLE([plugin-backlight], + [AS_HELP_STRING([--enable-plugin-backlight], + [Enable Backlight plugin.])], + [case "${enableval}" in + yes) plugin_backlight=yes ;; + no) plugin_backlight=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-backlight]) ;; + esac], + [plugin_backlight=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_BACKLIGHT], [test x$plugin_backlight = xyes]) + +dnl Notify 2 plugin +AC_ARG_ENABLE([plugin-notify-lcd2], + [AS_HELP_STRING([--enable-plugin-notify-lcd2], + [Enable Notify LCD plugin. Takes over as notification daemon and displays messages on LCD, blinks keyboard])], + [case "${enableval}" in + yes) plugin_notify_lcd2=yes ;; + no) plugin_notify_lcd2=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-notify-lcd2]) ;; + esac], + [plugin_notify_lcd2=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_NOTIFY_LCD2], [test x$plugin_notify_lcd2 = xyes]) + +dnl PPAStats plugin +AC_ARG_ENABLE([plugin-ppastats], + [AS_HELP_STRING([--enable-plugin-ppastats], + [Enable PPAStats plugin.])], + [case "${enableval}" in + yes) plugin_ppastats=yes ;; + no) plugin_ppastats=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-ppastats]) ;; + esac], + [plugin_ppastats=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_PPASTATS], [test x$plugin_ppastats = xyes]) + +dnl NM plugin +AC_ARG_ENABLE([plugin-nm], + [AS_HELP_STRING([--enable-plugin-nm], + [Enable NM (Network Manager) plugin.])], + [case "${enableval}" in + yes) plugin_nm=yes ;; + no) plugin_nm=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-nm]) ;; + esac], + [plugin_nm=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_NM], [test x$plugin_nm = xyes]) + +dnl Lens plugin +AC_ARG_ENABLE([plugin-lens], + [AS_HELP_STRING([--enable-plugin-lens], + [Enable Unity Lens plugin.])], + [case "${enableval}" in + yes) plugin_lens=yes ;; + no) plugin_lens=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-lens]) ;; + esac], + [plugin_lens=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_LENS], [test x$plugin_lens = xyes]) + +dnl WebKit browser plugin +AC_ARG_ENABLE([plugin-webkit-browser], + [AS_HELP_STRING([--enable-plugin-webkit-browser], + [Enable Webkit browser plugin.])], + [case "${enableval}" in + yes) plugin_webkit_browser=yes ;; + no) plugin_webkit_browser=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-webkit-browser]) ;; + esac], + [plugin_webkit_browser=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_WEBKIT_BROWSER], [test x$plugin_webkit_browser = xyes]) + +dnl Things plugin +AC_ARG_ENABLE([plugin-things], + [AS_HELP_STRING([--enable-plugin-things], + [Enable Things python animation API plugin.])], + [case "${enableval}" in + yes) plugin_things=yes ;; + no) plugin_things=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-plugin-things]) ;; + esac], + [plugin_things=no]) +AM_CONDITIONAL([ENABLE_PLUGIN_THINGS], [test x$plugin_things = xyes]) + +AC_OUTPUT([ +Makefile +data/Makefile +data/applications/Makefile +data/applications/g15-config.desktop +data/autostart/Makefile +data/autostart/gnome15.desktop +data/autostart/g15-systemtray.desktop +data/autostart/g15-indicator.desktop +data/dbus/Makefile +data/icons/Makefile +data/icons/hicolor/Makefile +data/icons/hicolor/16x16/Makefile +data/icons/hicolor/16x16/status/Makefile +data/icons/hicolor/22x22/Makefile +data/icons/hicolor/22x22/apps/Makefile +data/icons/hicolor/22x22/status/Makefile +data/icons/hicolor/24x24/Makefile +data/icons/hicolor/24x24/apps/Makefile +data/icons/hicolor/24x24/status/Makefile +data/icons/hicolor/48x48/Makefile +data/icons/hicolor/48x48/apps/Makefile +data/icons/hicolor/64x64/Makefile +data/icons/hicolor/64x64/apps/Makefile +data/icons/hicolor/scalable/Makefile +data/icons/hicolor/scalable/apps/Makefile +data/icons/hicolor/scalable/status/Makefile +data/icons/hicolor/scalable/devices/Makefile +data/icons/AwOken/Makefile +data/icons/AwOken/status/Makefile +data/icons/AwOken/status/16/Makefile +data/icons/AwOken/status/22/Makefile +data/icons/AwOken/status/24/Makefile +data/icons/AwOken/status/48/Makefile +data/icons/AwOken/status/64/Makefile +data/icons/AwOken/status/128/Makefile +data/icons/AwOken/apps/Makefile +data/icons/AwOken/apps/16/Makefile +data/icons/AwOken/apps/22/Makefile +data/icons/AwOken/apps/24/Makefile +data/icons/AwOken/apps/48/Makefile +data/icons/AwOken/apps/64/Makefile +data/icons/AwOken/apps/128/Makefile +data/icons/ubuntu-mono-dark/Makefile +data/icons/ubuntu-mono-dark/status/Makefile +data/icons/ubuntu-mono-dark/status/16/Makefile +data/icons/ubuntu-mono-dark/status/22/Makefile +data/icons/ubuntu-mono-dark/status/24/Makefile +data/icons/ubuntu-mono-light/Makefile +data/icons/ubuntu-mono-light/status/Makefile +data/icons/ubuntu-mono-light/status/16/Makefile +data/icons/ubuntu-mono-light/status/22/Makefile +data/icons/ubuntu-mono-light/status/24/Makefile +data/images/Makefile +data/themes/Makefile +data/themes/default/Makefile +data/udev/Makefile +data/udev/98-gnome15.rules +data/udev/99-gnome15-kernel.rules +data/udev/99-gnome15-g15direct.rules +data/udev/99-gnome15-g19direct.rules +data/udev/99-gnome15-g930.rules +data/ukeys/Makefile +data/ui/Makefile +i18n/Makefile +man/Makefile +src/Makefile +src/pylibg19/Makefile +src/pylibg19/g19/Makefile +src/libimpulse/Makefile +src/gnome15/Makefile +src/gnome15/g15globals.py +src/gnome15/drivers/Makefile +src/gnome15/util/Makefile +src/scripts/Makefile +src/gnome-shell-extension/Makefile +src/gnome-shell-extension/icons/Makefile +src/plugins/Makefile +src/plugins/cal/Makefile +src/plugins/cal/default/Makefile +src/plugins/cal-evolution/Makefile +src/plugins/cal-google/Makefile +src/plugins/lcdbiff/Makefile +src/plugins/lcdbiff/default/Makefile +src/plugins/debug/Makefile +src/plugins/debug/default/Makefile +src/plugins/background/Makefile +src/plugins/cairo-clock/Makefile +src/plugins/cairo-clock/g15/Makefile +src/plugins/cairo-clock/g15/default/Makefile +src/plugins/cairo-clock/g19/Makefile +src/plugins/cairo-clock/g19/default/Makefile +src/plugins/cairo-clock/mx5500/Makefile +src/plugins/cairo-clock/mx5500/default/Makefile +src/plugins/clock/Makefile +src/plugins/clock/default/Makefile +src/plugins/fx/Makefile +src/plugins/g15daemon-server/Makefile +src/plugins/macro-recorder/Makefile +src/plugins/macro-recorder/default/Makefile +src/plugins/macros/Makefile +src/plugins/macros/default/Makefile +src/plugins/mpris/Makefile +src/plugins/mpris/default/Makefile +src/plugins/mpris/bigcover/Makefile +src/plugins/mounts/Makefile +src/plugins/mounts/default/Makefile +src/plugins/runapp/Makefile +src/plugins/menu/Makefile +src/plugins/panel/Makefile +src/plugins/pommodoro/Makefile +src/plugins/pommodoro/default/Makefile +src/plugins/profiles/Makefile +src/plugins/processes/Makefile +src/plugins/im/Makefile +src/plugins/indicator-messages/Makefile +src/plugins/lcdshot/Makefile +src/plugins/notify-lcd/Makefile +src/plugins/notify-lcd/default/Makefile +src/plugins/screensaver/Makefile +src/plugins/screensaver/default/Makefile +src/plugins/stopwatch/Makefile +src/plugins/stopwatch/default/Makefile +src/plugins/sense/Makefile +src/plugins/sense/default/Makefile +src/plugins/sysmon/Makefile +src/plugins/sysmon/default/Makefile +src/plugins/sysmon/graphs/Makefile +src/plugins/rss/Makefile +src/plugins/rss/default/Makefile +src/plugins/sysmon/dials/Makefile +src/plugins/tweak/Makefile +src/plugins/volume/Makefile +src/plugins/volume/default/Makefile +src/plugins/mediaplayer/Makefile +src/plugins/mediaplayer/default/Makefile +src/plugins/weather/Makefile +src/plugins/weather/default/Makefile +src/plugins/weather/forecasts/Makefile +src/plugins/weather-noaa/Makefile +src/plugins/weather-yahoo/Makefile +src/plugins/tails/Makefile +src/plugins/tails/tailer/Makefile +src/plugins/tails/default/Makefile +src/plugins/display/Makefile +src/plugins/voip/Makefile +src/plugins/voip/default/Makefile +src/plugins/voip-teamspeak3/Makefile +src/plugins/voip-teamspeak3/ts3/Makefile +src/plugins/google-analytics/Makefile +src/plugins/google-analytics/default/Makefile +src/plugins/trafficstats/Makefile +src/plugins/trafficstats/default/Makefile +src/plugins/impulse15/Makefile +src/plugins/impulse15/themes/Makefile +src/plugins/impulse15/themes/default/Makefile +src/plugins/impulse15/themes/circlelcd/Makefile +src/plugins/impulse15/themes/circleline/Makefile +src/plugins/impulse15/themes/original/Makefile +src/plugins/game-nexuiz/Makefile +src/plugins/game-nexuiz/default/Makefile +src/plugins/game-nexuiz/resources/Makefile +src/plugins/backlight/Makefile +src/plugins/backlight/default/Makefile +src/plugins/notify-lcd2/Makefile +src/plugins/notify-lcd2/default/Makefile +src/plugins/ppastats/Makefile +src/plugins/ppastats/default/Makefile +src/plugins/nm/Makefile +src/plugins/nm/default/Makefile +src/plugins/lens/Makefile +src/plugins/webkitbrowser/Makefile +src/plugins/webkitbrowser/default/Makefile +src/plugins/things/Makefile +src/plugins/things/cg.stuff/Makefile +src/plugins/things/clouds.stuff/Makefile +]) + +AS_ECHO("Available features :-") +AS_ECHO("") +AS_ECHO("Panel Integration") +AS_ECHO("-----------------") +AS_ECHO_N("systemtray - ") +AS_IF([test "x$systemtray" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("Indicator - ") +AS_IF([test "x$indicator" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("gnome-shell-extension - ") +AS_IF([test "x$gnome_shell_extension" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Drivers") +AS_ECHO("-------") +AS_ECHO_N("g15direct - ") +AS_IF([test "x$driver_g15direct" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g930 - ") +AS_IF([test "x$driver_g930" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g19direct - ") +AS_IF([test "x$driver_g19direct" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("Kernel - ") +AS_IF([test "x$driver_kernel" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Icons") +AS_ECHO("-----") +AS_ECHO_N("icons-awoken - ") +AS_IF([test "x$icons_awoken" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("icons-mono - ") +AS_IF([test "x$icons_mono" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("") +AS_ECHO("Enabled Plugins") +AS_ECHO("---------------") +AS_ECHO_N("volume - ") +AS_IF([test "x$plugin_volume" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("rss - ") +AS_IF([test "x$plugin_rss" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("processes - ") +AS_IF([test "x$plugin_processes" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("sysmon - ") +AS_IF([test "x$plugin_sysmon" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal - ") +AS_IF([test "x$plugin_cal" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal-evolution - ") +AS_IF([test "x$plugin_cal_evolution" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cal-google - ") +AS_IF([test "x$plugin_cal_google" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lcdbiff - ") +AS_IF([test "x$plugin_lcdbiff" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("background - ") +AS_IF([test "x$plugin_background" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("cairo-clock - ") +AS_IF([test "x$plugin_cairo_clock" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("clock - ") +AS_IF([test "x$plugin_clock" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("fx - ") +AS_IF([test "x$plugin_fx" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("g15daemon-server - ") +AS_IF([test "x$plugin_g15daemon_server" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("im - ") +AS_IF([test "x$plugin_im" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("indicator-messages - ") +AS_IF([test "x$plugin_indicator_messages" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lcdshot - ") +AS_IF([test "x$plugin_lcdshot" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("macro-recorder - ") +AS_IF([test "x$plugin_macro_recorder" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("macros - ") +AS_IF([test "x$plugin_macros" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("profiles - ") +AS_IF([test "x$plugin_profiles" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("menu - ") +AS_IF([test "x$plugin_menu" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mounts - ") +AS_IF([test "x$plugin_mounts" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mpris - ") +AS_IF([test "x$plugin_mpris" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("notify-lcd - ") +AS_IF([test "x$plugin_notify_lcd" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("panel - ") +AS_IF([test "x$plugin_panel" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("screensaver - ") +AS_IF([test "x$plugin_screensaver" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("stopwatch - ") +AS_IF([test "x$plugin_stopwatch" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("tweak - ") +AS_IF([test "x$plugin_tweak" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("mediaplayer - ") +AS_IF([test "x$plugin_mediaplayer" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather - ") +AS_IF([test "x$plugin_weather" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather-noaa - ") +AS_IF([test "x$plugin_weather_noaa" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("weather-yahoo - ") +AS_IF([test "x$plugin_weather_yahoo" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("sense - ") +AS_IF([test "x$plugin_sense" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("tails - ") +AS_IF([test "x$plugin_tails" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("display - ") +AS_IF([test "x$plugin_display" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("voip - ") +AS_IF([test "x$plugin_voip" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("voip-teamspeak3 - ") +AS_IF([test "x$plugin_voip_teamspeak3" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("google-analytics - ") +AS_IF([test "x$plugin_google_analytics" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("debug - ") +AS_IF([test "x$plugin_debug" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("trafficstats - ") +AS_IF([test "x$plugin_trafficstats" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("pommodoro - ") +AS_IF([test "x$plugin_pommodoro" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("game-nexuiz - ") +AS_IF([test "x$plugin_game_nexuiz" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("backlight - ") +AS_IF([test "x$plugin_backlight" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("notify-lcd2 - ") +AS_IF([test "x$plugin_notify_lcd2" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("ppastats - ") +AS_IF([test "x$plugin_ppastats" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("nm - ") +AS_IF([test "x$plugin_nm" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("lens - ") +AS_IF([test "x$plugin_lens" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("webkitbrowser - ") +AS_IF([test "x$plugin_webkit_browser" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("things - ") +AS_IF([test "x$plugin_things" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO_N("impulse15 - ") +AS_IF([test "x$plugin_impulse15" = "xyes" ], + [AS_ECHO("Enabled")], + [AS_ECHO("Disabled")]) +AS_ECHO("See ./configure --help for descriptions of these plugins and options to enable and disable them.") +AS_ECHO("") +AS_ECHO("Other Configuration") +AS_ECHO("-------------------") +AS_ECHO("Fixed size font name - ${FIXED_SIZE_FONT}") +AS_ECHO("udev rules path - ${UDEV_RULES_PATH}") +AS_ECHO("Device group - ${DEVICEGROUP}") +AS_ECHO("Device mode - ${DEVICEMODE}") +AS_ECHO_N("Hotplugging support - ") +AS_IF([test "x${HAVE_PYUDEV}" = "xno"], + [AS_ECHO("No")], + [AS_ECHO("Yes")]) +AS_ECHO("") +AS_ECHO("Building i18n for locales: ${ENABLED_LOCALES}") +AS_ECHO("") diff --git a/data/Makefile.am b/data/Makefile.am new file mode 100644 index 0000000..e466685 --- /dev/null +++ b/data/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = \ + applications \ + autostart \ + dbus \ + udev \ + themes \ + images \ + ukeys \ + ui \ + icons + diff --git a/data/applications/Makefile.am b/data/applications/Makefile.am new file mode 100644 index 0000000..9374e0b --- /dev/null +++ b/data/applications/Makefile.am @@ -0,0 +1,5 @@ +appdir = $(datadir)/applications +app_DATA = g15-config.desktop + +EXTRA_DIST = \ + $(app_DATA) diff --git a/data/applications/g15-config.desktop.in b/data/applications/g15-config.desktop.in new file mode 100644 index 0000000..62516b1 --- /dev/null +++ b/data/applications/g15-config.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Logitech G Keyboard Configuration +Comment=Configure your Logitech G15 or G19 keyboard +Exec=g15-config +Icon=gnome15 +Terminal=false +StartupNotify=true +Type=Application +Categories=GTK;Settings;HardwareSettings; diff --git a/data/autostart/Makefile.am b/data/autostart/Makefile.am new file mode 100644 index 0000000..f7a78a1 --- /dev/null +++ b/data/autostart/Makefile.am @@ -0,0 +1,12 @@ +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray.desktop +endif +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator.desktop +endif + +autostartdir = $(sysconfdir)/xdg/autostart +autostart_DATA = gnome15.desktop $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) + +EXTRA_DIST = \ + gnome15.desktop g15-systemtray.desktop g15-indicator.desktop diff --git a/data/autostart/g15-indicator.desktop.in b/data/autostart/g15-indicator.desktop.in new file mode 100644 index 0000000..0c809e5 --- /dev/null +++ b/data/autostart/g15-indicator.desktop.in @@ -0,0 +1,20 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Indicator +Name[fr]=Indicateur Logitech Clavier G +Name[it]=Tastiera Logitech G Indicatore +Icon=gnome15 +Comment=Panel indicator allowing control and monitor of the Gnome15 desktop service for Logitech G keyboards. +Comment[fr]=Voyant panneau permettant le contrôle et le suivi du service Gnome15 bureau pour les claviers Logitech G. +Comment[it]=Pannello indicatore che consente il controllo e il monitoraggio del servizio Gnome15 desktop per tastiere Logitech G +Exec=g15-indicator +Terminal=false +Type=Application +Categories=TrayIcon;GTK +GenericName= +#X-GNOME-Autostart-Delay=0 +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 \ No newline at end of file diff --git a/data/autostart/g15-systemtray.desktop.in b/data/autostart/g15-systemtray.desktop.in new file mode 100644 index 0000000..7382279 --- /dev/null +++ b/data/autostart/g15-systemtray.desktop.in @@ -0,0 +1,20 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Tray Icon +Name[fr]=Clavier Logitech G Icône System Tray +Name[it]=Logitech G Keyboard Tray Icon di sistema +Icon=gnome15 +Comment=Tray icon allowing control and monitor of the Gnome15 desktop service for Logitech G keyboards. +Comment[fr]=Icône permet de contrôler et de surveiller le service Gnome15 bureau pour les claviers Logitech G. +Comment[it]=Tray icon che consente il controllo e il monitoraggio del servizio Gnome15 desktop per tastiere Logitech G. +Exec=g15-systemtray +Terminal=false +Type=Application +Categories=TrayIcon;GTK +GenericName= +#X-GNOME-Autostart-Delay=0 +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 diff --git a/data/autostart/gnome15.desktop.in b/data/autostart/gnome15.desktop.in new file mode 100644 index 0000000..a993025 --- /dev/null +++ b/data/autostart/gnome15.desktop.in @@ -0,0 +1,21 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Logitech G Keyboard Desktop Service +Name[fr]=Clavier Logitech G Desktop Service +Name[it]=Tastiera Logitech G Desktop Servizio +Icon=gnome15 +Comment=Logitech G series keyboard desktop service. +Comment[fr]=Logitech série G clavier de bureau de service. +Comment[it]=Serie G di Logitech tastiera servizio desktop. +Exec=g15-desktop-service +Terminal=false +Type=Application +Categories=System;GTK +GenericName= +#X-GNOME-Autostart-Delay=8 +X-GNOME-Autostart-Phase=WindowManager +#AutostartCondition=GNOME /desktop/gnome/gnome15/enabled +#X-GNOME-Autostart-Phase=Applications +#X-GNOME-AutoRestart=true +#X-Ubuntu-Gettext-Domain=gnome15 \ No newline at end of file diff --git a/data/dbus/Makefile.am b/data/dbus/Makefile.am new file mode 100644 index 0000000..422e155 --- /dev/null +++ b/data/dbus/Makefile.am @@ -0,0 +1,10 @@ +if ENABLE_DRIVER_KERNEL + systemdbusconfdir = $(sysconfdir)/dbus-1/system.d + systemdbusconf_DATA = g15-system-service.conf + systemdbusdir = $(datadir)/dbus-1/system-services + systemdbus_DATA = org.gnome15.SystemService.service +endif + +dbusdir = $(datadir)/dbus-1/services +dbus_DATA = org.gnome15.Gnome15.service +EXTRA_DIST = org.gnome15.Gnome15.service org.gnome15.SystemService.service g15-system-service.conf diff --git a/data/dbus/g15-system-service.conf b/data/dbus/g15-system-service.conf new file mode 100644 index 0000000..21c87d2 --- /dev/null +++ b/data/dbus/g15-system-service.conf @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/dbus/org.gnome15.Gnome15.service b/data/dbus/org.gnome15.Gnome15.service new file mode 100644 index 0000000..78ebb73 --- /dev/null +++ b/data/dbus/org.gnome15.Gnome15.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome15.Gnome15 +#Exec=g15-desktop-service diff --git a/data/dbus/org.gnome15.SystemService.service b/data/dbus/org.gnome15.SystemService.service new file mode 100644 index 0000000..26a7596 --- /dev/null +++ b/data/dbus/org.gnome15.SystemService.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.gnome15.SystemService +Exec=/usr/bin/g15-system-service +User=root diff --git a/data/fonts/CyrKoi-VGA8.psf.gz b/data/fonts/CyrKoi-VGA8.psf.gz new file mode 100644 index 0000000000000000000000000000000000000000..e150703378064114aff2ed238bb9a85c7f3ffb9a GIT binary patch literal 2053 zcmV+g2>SOQiwFSE1g}j315K5CbQIMc$G@=SN_)1rKp-Q=>?Ck`KL!F}fhD{lfDce! z;%k5~MA){ekFe^v?Xf*Q{?R{z9;s4m@wBI&(o;_BY0*}yL$o8-F;s11%BB&mjkOQD zr5wz$+nIjvo!K1k=JWmCd*^rW{oQApwUMJ>SfOPA(2`+N1GXKHemYZz6m6NpS4yR# zUM!W+%oHL@%bD+0DNK}2v!$u21wEzGe@mqvel4nYfNBWH)`Fbz+WC|=WXYkP8#JS6}{!|0hq03 zp~o*EmC}`8+HzgjN+X?0CX*@lw>2ANxAKFGLa+9&cgO`99*j|sOr~_xa^dt0Tpk>B zpl1sVHwVW{rc5i4Hkb4qjIM9CgJcqc>sFG<3hUj9$NE6Ew^;V%pR`5k&nbKcZfu=t znRQUZs{lIml#Isl*q%<#!$*%Fe_FKrD%*R3{pGXSY~JI(**WPK$a&cT>Qp>+a4l0$B}bw|ZE%O$dAe6q zXqweca*{=_2v;tzSCM5;&OgaoF0CpDu7jLy$tlV8lq>E;{vLNUtKviob#ei?cw7Ka z)v9`^1)z=mp=Rtv<%;XZ{Uw2c&ZvKf3*bb@NUvwvi{psqFKuri`(a5<$W!qEK}zTO z8LjIbx;|Ro2gP~QtTWB?@&FtiP35wf77BD%Kd{7=C?L&L9zMRss7&MvNn1_T^I>%v zE6UY7r0x+aQmW^Ler4%bJs%adU=^-obiO@LUH}ihu}#VYFV9JQo_&>qq$+cHqW301 zcX#*2bLTE{zoPzlU+UF)p&vVdR&a-WoxU@;Ona1_xu0hGv>&$+*kIc zRRPHLyb$e|?^jfPMFt|RdMSV?E@_=q3emg9g@g?3BCX2+hSWaSt9^ESC-1|H&ylPz zv)+!+O-hC5LQ7TSgc=2CdqVz=s`<>~Z#Ga9C%G%t{i&#OMY}{RcTMh#^riZ}7M72w&!+4mC@()!xjFa-OevfVS|3OB@5h9FBnK-VD8Yh#kRsCmF zzjFM@*2d#R{<6MS&8$}WtN!AC!nhp88OCwf)KudiE=%HARXe7u;w0BW&bu5Zp7fl2 z0jlo~E)sNnm2=H$K>QjXsVFTU5lIv<_G@2YMF z?vNV{--FJ6$d_>V9jv}@x*7G8(xY>dQV)yr?`L4DKJttDm$y5|8C1uqT2qhuaUPDX zC$B4?e7-BL!~Zbx{p@ORMYjmUM;lIx7o;VRCB|uCSVJa|V~Gs)hl$D5>0}0(Ll%%` zvW9FR8_5l1Vk3KCFF(cs=3c}>>Phawv!uotZ-n8?%zd9c)^JL^M9;$w&xxN)OOA;X z)SvRJJ>`t}A9v2cF~AsMj5HF4W?W$;jVp~&#^;RBv!@37HIhkWG7)&3qfKFMDw#&6 zlNtC9YiBYyi_FGYW1KOz@h3+m9VJx)O)zF!U0fq3imQ1tet6>0+9gDy9g*+O!xa#)>h_jT8y- zRq>E`P#hBXi~CsfBer;sJWu#Wg>PU4Z;3|E;!5fmQcHAlHMxdNB2&mzGL6h6v&d{R zm&_ycNfTK_vScw?LY9&oSw@zVRb(AmPg=<)aviyz+(`b8E!3^#U)VwIB8O!xoOdfR zCh?w;Eu70XvYqT8x02h)?PNFELm)=|M5AaBhRBF|aV={W zh^1nQSS+$)k!ThRS(6f1iP7Q<%xxw&k(Cox~z-WFN7~e$r0vA_vLW z$TvtA*-O4f8jZ8m6WqsN;1o~CgoZGDm!(%pFX<=mkq^mbQX!|w>*P)H7I}x9Bkz-s j$v?@z$$!YFvN>fT%uO(V&p`xZzDJA0f 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 E0rnN;DF6Tf literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/Makefile.am b/data/icons/AwOken/Makefile.am new file mode 100644 index 0000000..7a187e1 --- /dev/null +++ b/data/icons/AwOken/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status apps diff --git a/data/icons/AwOken/apps/128/Makefile.am b/data/icons/AwOken/apps/128/Makefile.am new file mode 100644 index 0000000..085a971 --- /dev/null +++ b/data/icons/AwOken/apps/128/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/128x128/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/128/gnome15.png b/data/icons/AwOken/apps/128/gnome15.png new file mode 100644 index 0000000000000000000000000000000000000000..809d87dc1aeff0e311503be49b84d17815153f20 GIT binary patch literal 9711 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 literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/apps/16/Makefile.am b/data/icons/AwOken/apps/16/Makefile.am new file mode 100644 index 0000000..9d1f1f8 --- /dev/null +++ b/data/icons/AwOken/apps/16/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/16x16/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/16/gnome15.png b/data/icons/AwOken/apps/16/gnome15.png new file mode 100644 index 0000000000000000000000000000000000000000..9335d444977f6c1907fc7d02c055f556392b290d GIT binary patch literal 3440 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 literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/apps/48/Makefile.am b/data/icons/AwOken/apps/48/Makefile.am new file mode 100644 index 0000000..b621143 --- /dev/null +++ b/data/icons/AwOken/apps/48/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/AwOken/clear/48x48/apps +images_DATA = gnome15.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/apps/48/gnome15.png b/data/icons/AwOken/apps/48/gnome15.png new file mode 100644 index 0000000000000000000000000000000000000000..0121756da2e95a7eaca56a8f62a8a5ff5337c025 GIT binary patch literal 5108 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 Aw*UYD literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/apps/Makefile.am b/data/icons/AwOken/apps/Makefile.am new file mode 100644 index 0000000..1dd1756 --- /dev/null +++ b/data/icons/AwOken/apps/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 48 64 128 diff --git a/data/icons/AwOken/status/128/Makefile.am b/data/icons/AwOken/status/128/Makefile.am new file mode 100644 index 0000000..97774a4 --- /dev/null +++ b/data/icons/AwOken/status/128/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/128x128/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/128/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..b512675cfd59bc1c34a8fa2368218730adc55296 GIT binary patch literal 9384 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 literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/16/Makefile.am b/data/icons/AwOken/status/16/Makefile.am new file mode 100644 index 0000000..ffdf9e5 --- /dev/null +++ b/data/icons/AwOken/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/16x16/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/16/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..0b3d6d5dc8b0a531d4b54caeaf67d93ca86d1727 GIT binary patch literal 3388 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 literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/22/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/22/logitech-g-keyboard-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..0178d0972aac0bcdcd0c77ebda925fbd8d8a7023 GIT binary patch literal 3723 zcmV;64s`K}P)j1^HV42lZa2jn55j)S9!ipu-pd!uXCy!YnK{>2n?1;Gf_2w z45>mM5#WQz#Kz&|EGkvK~TfD`~gdX7S-06<0ofSs5oQvjd@0AR~wV&ec%EdXFAf9BHw zfSvf6djSAjlpz%XppgI|6J>}*0BAb^tj|`8MF3bZ02F3R#5n-iEdVe{S7t~6u(trf z&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_<@>e|ZE3OddDgXd@nX){&BsoQaTL>+2 z2Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&JM25&Nhy=4qq+m zzXtyzVq)X|<DpKG zaQJ>aJVl|9x!Kv}EM4F8AGNmGkLXs)PCDQ+7;@>R$ z13uq10I+I40eg`xs9j?N_Dd%aSaiVR_W%I$yKlkNCzL=651DUOSSq$Ed=-((3YAKgCY2j1FI1_jrmEhm3sv(~%T$l4 zUQ>OpMpZLYTc&xiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY z$4Vz$Cr4+G&IO(4Q`uA9rwXSQO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ z9DOhSRQ+xGr}ak+SO&8UBnI0I&KNw!HF0k|9WTe*@liuv!$3o&VU=N*;e?U7(LAHo zMvX=fjA_PP<0Rv4#%;!P6gpNq-kQ#w?mvCS^p@!_XIRe=&)75LwiC-K#A%&Vo6|>U7iYP1gY$@siA#dZ zE|)$on;XX6$i3uBboFsv;d;{botv|p!tJQrukJSPY3_&IpUgC$DV|v~bI`-cL*P;6 z(LW2Hl`w1HtbR{JPl0E(=OZs;FOgTR*RZ#xcdGYc?-xGyK60PqKI1$$-ZI`wBr znsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~ z=u}!L*D43HXJuDrzs-rtIhL!QE6wf9v&!3$H=OUE|LqdO65*1zrG`saEge|qy{u|E zvOIBl+X~|q1uKSD2CO`|inc0k)laMKSC_7Sy(W51Yk^+D%7VeQ0c-0ERSM;Wee2xU z?Ojh;FInHUVfu!h8$K0@imnvf7nc=(*eKk1(e4|2y!JHg)!SRV_x(P}zS~s+RZZ1q)n)rh`?L2yu8FGY_?G)^U9C=S zaqY(g(gXbmBM!FLxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k z9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC z$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMkx)5~X(nbG^=R3SR&Rp`ibn>#> zOB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$ z-M#aAZ}-Lb_1_lVesU-M&da;mcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&Yp!3n( zNB0JWgU|kv^^Xrj1&^7J%Z3ex>z+71IXU7#a{cN2r$f(V&nBK1{-XZNt``^}my^G3e5L*B!0Q>W+s4Ai9=^$VGcjKDR{QP2cieX!@1x%jPvm?ce<=TG z`LXp=(5L&88IzO$1Ou4!{5mfCvj6}924YJ`L;(K){{a7>y{D4^000SaNLh0L0BAP= z0BAP>oq!pi00007bV*G`2igk<4HXhuqL@wq00YlSL_t(I%e7U{YaLY-{?5HK!<)ou zeSN-YvuM>tT55>62uf_A5VC76zM_;WF7L%E?ERbS3zjC zi;@?bMyf)fzBE7Hpc9zH`qI%C)|}Zd8@+74`-roKaz$vl!9;*76bM9`t-F`QYYdet!Pv(tiR#BO@bk%*@Q( zuGj0Il~|7BxZ_)gSXJrdlLrM&t3NOPBg)XJAOjiya}MU zR;xW792|TWMbYIz+-|pRmSt3C@UO>#oS9WsaeRFIQ>)c_03ey1oczjK`#}H!$gH(c zRfvchV~ClltGsi_)(qdd=>BBCOq zA*_JwCJW>p;;_-}Q<5YfW?41>AYWNoxm6U!BN53}wQ$Z!cjy6oCcJLKq9{-XbGg62 z|0aOVjg5_8&YwU3$?)*-rvU81!NEfS8f;Tl6h(11Jnub31m62jcmT9ot=^uSo8#^6 z?WLuqrC(Q8R_>Ii^Z>G=D5~E3*jn4cKX;{Hut7srB0{xV{bFcnXn0^?;GT1CPeg=> zGGolSo}Qlfn7Q)8T{vTfdhZQ0XJK}S>mJAPjY_3*LsieD9|o9+L}!T0v1y!h`+IwP z-vGD(K)U`6YK$>N7xi>mG`n(mTx@*0?y9)GM`nQOTI_G);Xdo>Wi>ewTf-xq{2eQ^m pxXspDF~(37McQmO|HS{f{sGp}|1j1^HV42lZa2jn55j)S9!ipu-pd!uXCy!YnK{>2n?1;Gf_2w z45>mM5#WQz#Kz&|EGkvK~TfD`~gdX7S-06<0ofSs5oQvjd@0AR~wV&ec%EdXFAf9BHw zfSvf6djSAjlpz%XppgI|6J>}*0BAb^tj|`8MF3bZ02F3R#5n-iEdVe{S7t~6u(trf z&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_<@>e|ZE3OddDgXd@nX){&BsoQaTL>+2 z2Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&JM25&Nhy=4qq+m zzXtyzVq)X|<DpKG zaQJ>aJVl|9x!Kv}EM4F8AGNmGkLXs)PCDQ+7;@>R$ z13uq10I+I40eg`xs9j?N_Dd%aSaiVR_W%I$yKlkNCzL=651DUOSSq$Ed=-((3YAKgCY2j1FI1_jrmEhm3sv(~%T$l4 zUQ>OpMpZLYTc&xiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY z$4Vz$Cr4+G&IO(4Q`uA9rwXSQO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ z9DOhSRQ+xGr}ak+SO&8UBnI0I&KNw!HF0k|9WTe*@liuv!$3o&VU=N*;e?U7(LAHo zMvX=fjA_PP<0Rv4#%;!P6gpNq-kQ#w?mvCS^p@!_XIRe=&)75LwiC-K#A%&Vo6|>U7iYP1gY$@siA#dZ zE|)$on;XX6$i3uBboFsv;d;{botv|p!tJQrukJSPY3_&IpUgC$DV|v~bI`-cL*P;6 z(LW2Hl`w1HtbR{JPl0E(=OZs;FOgTR*RZ#xcdGYc?-xGyK60PqKI1$$-ZI`wBr znsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~ z=u}!L*D43HXJuDrzs-rtIhL!QE6wf9v&!3$H=OUE|LqdO65*1zrG`saEge|qy{u|E zvOIBl+X~|q1uKSD2CO`|inc0k)laMKSC_7Sy(W51Yk^+D%7VeQ0c-0ERSM;Wee2xU z?Ojh;FInHUVfu!h8$K0@imnvf7nc=(*eKk1(e4|2y!JHg)!SRV_x(P}zS~s+RZZ1q)n)rh`?L2yu8FGY_?G)^U9C=S zaqY(g(gXbmBM!FLxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k z9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC z$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMkx)5~X(nbG^=R3SR&Rp`ibn>#> zOB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$ z-M#aAZ}-Lb_1_lVesU-M&da;mcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&Yp!3n( zNB0JWgU|kv^^Xrj1&^7J%Z3ex>z+71IXU7#a{cN2r$f(V&nBK1{-XZNt``^}my^G3e5L*B!0Q>W+s4Ai9=^$VGcjKDR{QP2cieX!@1x%jPvm?ce<=TG z`LXp=(5L&88IzO$1Ou4!{5mfCvj6}924YJ`L;(K){{a7>y{D4^000SaNLh0L0BAP= z0BAP>oq!pi00007bV*G`2igk<4HPhy+34#400W*$L_t(Y$F){FZ&XJV{?3`Zdv}c- zBR?RCWh2Xo4pGpgFn_=f1=KVypy3Ckpm34Gp$bJRN1e^u>)R% zCGXxlXU<8nGuN{=bYZ2HcK6Pl^F7XEfp7ex^kI8@`@Wg|_`d`tD(kv_w7R-#Mfx#> zaKEamOWymB04zHUGfO}$g0eiInH5ANF^->^nfYmbef?JegC>E9u!#KL>-FeDRjtrz zP!MxqBFi#DRhtB=%7t^Llat4PJbn6Tb93`=5n%u*vP0DAbn3;$#h(GJnw`dj*%)L~ z1u^Fo03r|(Ha0eX6OqxqJV7QE%q&SW3T}qutu5%=w@2GNeGFf}M)&4Ti1#2Ouyfek+iNc`FP~2! zBJkdGQ521?erE?$M@Ik=KF-hMyN3@Oy(g|-#l*@=1I_?g2p9|oPDIWpP}Sz0OdY;? zgDE1MsG@uK?g(jSE=8%5G>Q2aB}&1QIjRcm?t+O>nE}(&KnUQh5mKOJFf${BFcv3o zG#f>^!xb!-~dBa99Pw-H_RX+P}Qan_$&i6gGLl#VF6}_k_dWq1PuWi!U#k*O3unF zBB!$DRg@cTs?clKP?@2_414hc8VxivgnU#)-E$QhfAT)CNYQ%_2?2KF295wYBFydY z!=68fyn6?E{~mOF+>GL&3K41U=E=rP4jFxm2Zs0V;ls0Mn05~Hy&meVEhvB%5twMi zR27KKR23nF^ByDwW;UW659`bfK0SDVzph-tVF>WiqZ3tp5W&B*v+!lvAWyNr+6N&T zNz8y5h#6Xz(A&51i5b@K!|EEL(}8un&CGI9-sI(%5}1gXh;*Z7qbf;5EM1zKI%ONH zj+vp}qbLffD(6);HjB!#oZH>q{j;j7tz>$()r^Q3zyJW7SxH3JZnynlFz7nxq}6J% z_Z|zeywC1~3Fq7|OG`_QRA-YgGn{ixx2ErD!8qsOoP(;OEX(nwPeebM*@ds{am@=N sn{^hS5rD>7W)5a{U}p8V{?~&40IDzJ&Ul4f#sB~S07*qoM6N<$g0vRgHvj+t literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/24/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/24/logitech-g-keyboard-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..cda0461aa3f2060e76b784830dfcfac18605f5a0 GIT binary patch literal 3730 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 literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/48/Makefile.am b/data/icons/AwOken/status/48/Makefile.am new file mode 100644 index 0000000..1070fbb --- /dev/null +++ b/data/icons/AwOken/status/48/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/AwOken/clear/48x48/status +images_DATA = logitech-g-keyboard-error-panel.png \ + logitech-g-keyboard-panel.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png b/data/icons/AwOken/status/48/logitech-g-keyboard-error-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..f93c83faea0ebaf6a5f6476acc1d5f40b1ba1a77 GIT binary patch literal 5024 zcmV;R6JP9!P)j1^HV42lZa2jn55j)S9!ipu-pd!uXCy!YnK{>2n?1;Gf_2w z45>mM5#WQz#Kz&|EGkvK~TfD`~gdX7S-06<0ofSs5oQvjd@0AR~wV&ec%EdXFAf9BHw zfSvf6djSAjlpz%XppgI|6J>}*0BAb^tj|`8MF3bZ02F3R#5n-iEdVe{S7t~6u(trf z&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_<@>e|ZE3OddDgXd@nX){&BsoQaTL>+2 z2Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&JM25&Nhy=4qq+m zzXtyzVq)X|<DpKG zaQJ>aJVl|9x!Kv}EM4F8AGNmGkLXs)PCDQ+7;@>R$ z13uq10I+I40eg`xs9j?N_Dd%aSaiVR_W%I$yKlkNCzL=651DUOSSq$Ed=-((3YAKgCY2j1FI1_jrmEhm3sv(~%T$l4 zUQ>OpMpZLYTc&xiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY z$4Vz$Cr4+G&IO(4Q`uA9rwXSQO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ z9DOhSRQ+xGr}ak+SO&8UBnI0I&KNw!HF0k|9WTe*@liuv!$3o&VU=N*;e?U7(LAHo zMvX=fjA_PP<0Rv4#%;!P6gpNq-kQ#w?mvCS^p@!_XIRe=&)75LwiC-K#A%&Vo6|>U7iYP1gY$@siA#dZ zE|)$on;XX6$i3uBboFsv;d;{botv|p!tJQrukJSPY3_&IpUgC$DV|v~bI`-cL*P;6 z(LW2Hl`w1HtbR{JPl0E(=OZs;FOgTR*RZ#xcdGYc?-xGyK60PqKI1$$-ZI`wBr znsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~ z=u}!L*D43HXJuDrzs-rtIhL!QE6wf9v&!3$H=OUE|LqdO65*1zrG`saEge|qy{u|E zvOIBl+X~|q1uKSD2CO`|inc0k)laMKSC_7Sy(W51Yk^+D%7VeQ0c-0ERSM;Wee2xU z?Ojh;FInHUVfu!h8$K0@imnvf7nc=(*eKk1(e4|2y!JHg)!SRV_x(P}zS~s+RZZ1q)n)rh`?L2yu8FGY_?G)^U9C=S zaqY(g(gXbmBM!FLxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k z9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC z$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMkx)5~X(nbG^=R3SR&Rp`ibn>#> zOB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$ z-M#aAZ}-Lb_1_lVesU-M&da;mcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&Yp!3n( zNB0JWgU|kv^^Xrj1&^7J%Z3ex>z+71IXU7#a{cN2r$f(V&nBK1{-XZNt``^}my^G3e5L*B!0Q>W+s4Ai9=^$VGcjKDR{QP2cieX!@1x%jPvm?ce<=TG z`LXp=(5L&88IzO$1Ou4!{5mfCvj6}924YJ`L;(K){{a7>y{D4^000SaNLh0L0BAP= z0BAP>oq!pi00007bV*G`2igk<4HG$|M8=2!00`tsL_t(&-tAh?ixpQE{?55o^wdrPJ!cm0-txZts-91UVPH~F^sA~{_nx2M`Od8d{yhFXUg)6o#_H!hr^-G^Bk&*EX$y({~b#b)viTEu+|C@@x6QZasUj# z)hUqWc@F2?*3FwYKWVjEb2BWiBEm!zTM9&^-pl&5D6rPHvn<=G8nCsswYRde@}a8Y z;lqcGIC8QI7lp-lKl?fh)ldYp@4|MS_|)@e_gwF?ZW*0{3kPnl2U4E zRe+gQMBx=x)yc$*LMZkkh=_>(e*XOVzYPY1QogAQq=Gh^&F-vWtSKlEjg*L_8WS*H z*N@l9aTb77$u?Di_;1iUgjZ_0GK`!YjaJvl+o{BvCObWyy6Rw?e)mS}>Zva5&+i%hR>@&2OF~^L8sIM9nD!5lLT5u7Rp zs<8fymt~?-2?EeMd2(bl)iQ*_+^R}1qCnePOIempgj29o_tq^8R#&m$EpLc05W$`Z zda4+zB0F&cytD*}V7Re?-km$hyb?oI9J+Af2@?4T2woTr1_c1F5`Jn9_`!{|csS>t zByO*V?u{E|*cc-8MX)1+UaN)q_ut3Dn{STgP?)Rt%P;7B`6ak0;6wnAU;%l8u?B-d z=BdMMDCC?gV;%shDpk3Um@&9>2ifnxqiGBb5v(e9MbJBN0Q*1q0NidvoCA4*;`nYJ zKaS?*%g}Cjq+q0^bFNHc5g`B+4EH=aP`b3VqzDwi-WOkBA2V_BL4f_3ZyTqDr9>bw6}*M%fLQq`>No#-hwt7a3eYbVji)%WX>vx zR1pO3R0Z8Hq(F-K!ykV{PK3-a6}o>vz&t{g zfcZWo0^iXJBL#ukler+gG3ME+M5?KXjNS9d01O7;BqX%afKvrUbYN^QRp{f#=&!8- z&W%^+BS)aej*ZdkSjm7)Gm*Ix>-}$zV6bd>e8n6iYT;Q0f|b`>Tj+iB4d$II(W|Qp zSy@5z=+TK#_PxmjshsM8G<;(1%MxGVei{r$@t+W~G#MMrSnz+HD%3d)lX3C8X>^SB zz*DMl%1OqmBo-GTs*q?R+TMm*TPAV3Xz&CBFdo4&uY8pus{~FDu6r@6#$yRA&W+sp z$}4bDw{D1#Sqlt@qt2?PAXZZGD^c!0`LE*I<3|Qq@v{?A9%~ogGM^43Pq5KGcM5 zT7Ob+1S3Ax19g*$=Rg`Pdg>HNH;=SmJ0k~rNLs@VK` z{+>9#j^i23p!eQG_x5dYzdvHfUWtqdO;yim+^CHt!aOtfF=N3PEE$7EW3bQX2}LYcA`e#~&@<&gDBl4_ z$0kb`5qXlJ7cXM>s$RC%Qu<}4rdbc4l|V$u z^ITOmv({QwRp(qmL^i^DZ8RFae!suZS}V*f!2``^({?(YH2}jD-luMCc?A}(UcLGk zW6WH)+bxQspm>t1?e_FHGqZ?voO9hg&l^OvAR=UpA#ZiiXb0O`Ynw$;IA+#J`Qp%_ zLkF*1xpJnC%w0Nd+Cr0-{~%O|;dnzV7$<%f8l)gxxECjLmSu3x zi7|$s7D7ryMcfa^f~MDX9bAke-ooW|oQ$!@f^s2o!#m6jYb~-YV^u|-=Tkp)=V#BJ zJq+M*zu(WRHkRs0m55}j%Ep*M-6tr&*XmT2Bd=+*+3br5_xt^#Y&%CDpy*l`7Z-bL zYisKd9z1vyzMD}+ch~YKKjRnwT>!a1Yd5_C81H!-b)@X?cjJ$G6^8}T2QaMtSA)QA qYo1LzTi-h0000j1^HV42lZa2jn55j)S9!ipu-pd!uXCy!YnK{>2n?1;Gf_2w z45>mM5#WQz#Kz&|EGkvK~TfD`~gdX7S-06<0ofSs5oQvjd@0AR~wV&ec%EdXFAf9BHw zfSvf6djSAjlpz%XppgI|6J>}*0BAb^tj|`8MF3bZ02F3R#5n-iEdVe{S7t~6u(trf z&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_<@>e|ZE3OddDgXd@nX){&BsoQaTL>+2 z2Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&JM25&Nhy=4qq+m zzXtyzVq)X|<DpKG zaQJ>aJVl|9x!Kv}EM4F8AGNmGkLXs)PCDQ+7;@>R$ z13uq10I+I40eg`xs9j?N_Dd%aSaiVR_W%I$yKlkNCzL=651DUOSSq$Ed=-((3YAKgCY2j1FI1_jrmEhm3sv(~%T$l4 zUQ>OpMpZLYTc&xiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY z$4Vz$Cr4+G&IO(4Q`uA9rwXSQO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ z9DOhSRQ+xGr}ak+SO&8UBnI0I&KNw!HF0k|9WTe*@liuv!$3o&VU=N*;e?U7(LAHo zMvX=fjA_PP<0Rv4#%;!P6gpNq-kQ#w?mvCS^p@!_XIRe=&)75LwiC-K#A%&Vo6|>U7iYP1gY$@siA#dZ zE|)$on;XX6$i3uBboFsv;d;{botv|p!tJQrukJSPY3_&IpUgC$DV|v~bI`-cL*P;6 z(LW2Hl`w1HtbR{JPl0E(=OZs;FOgTR*RZ#xcdGYc?-xGyK60PqKI1$$-ZI`wBr znsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~ z=u}!L*D43HXJuDrzs-rtIhL!QE6wf9v&!3$H=OUE|LqdO65*1zrG`saEge|qy{u|E zvOIBl+X~|q1uKSD2CO`|inc0k)laMKSC_7Sy(W51Yk^+D%7VeQ0c-0ERSM;Wee2xU z?Ojh;FInHUVfu!h8$K0@imnvf7nc=(*eKk1(e4|2y!JHg)!SRV_x(P}zS~s+RZZ1q)n)rh`?L2yu8FGY_?G)^U9C=S zaqY(g(gXbmBM!FLxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k z9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC z$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMkx)5~X(nbG^=R3SR&Rp`ibn>#> zOB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$ z-M#aAZ}-Lb_1_lVesU-M&da;mcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&Yp!3n( zNB0JWgU|kv^^Xrj1&^7J%Z3ex>z+71IXU7#a{cN2r$f(V&nBK1{-XZNt``^}my^G3e5L*B!0Q>W+s4Ai9=^$VGcjKDR{QP2cieX!@1x%jPvm?ce<=TG z`LXp=(5L&88IzO$1Ou4!{5mfCvj6}924YJ`L;(K){{a7>y{D4^000SaNLh0L0BAP= z0BAP>oq!pi00007bV*G`2igk<4HN~#YAnbA00}rrL_t(&-tAdmXjRD>|9vy(+_x+m# zd~tknyw*YCja#>F{Q|(i|Ej>u{I$NmzNY{f0HWZzs8*{gj$<%0q9_70|93P=l(`lW zK`CX3NIZG+BnBV==qvzH9LLaF&)mIx_rnz{Rx~xRI2&UG5&4<|5wW#0FUJC09#f7k$(n21bx&#s}J*6Pv3DsB-Yi0FfzJ9qxPQmNGBs|z4pAW4$>Mrq8< zGUJ(Sz&RKFdO#EalLbCn?nTU8L+$7lYR;n(dzx0pm=b_85ou3z5fSBt5D5__o~0H= zMfYAIM5+;`;UL{{V0gKvPdoPsgTBo4&ql*RBgv$~Iq7fj=|`z^9D6)>>!9YPQ^` z>FMd;A3l8eT>vitl&tV>ut=c^14te{dK7hccdwLEwuKJO%!??LM&Zmb>W4V@k}N+M z8ymZL@{!SzJ04zT8%=^^2|)H1E57IMNt$jp_5bA z%*@P3{r&wHtnjmzE(w4FfE57p{r&ylX>V^|!^~!Mbad?E#fuLBlcw{G20r>xNL zDwRqOfJhU*t`FG3`7n8Ct!s{Jj49o|ef#pl!osxGyavz&pnco6ZR-ylIPgKCP}t)8 z(E9c3v3c`mluD(cJ9q9}Op+w8l#-5)F7h~KRVtOpTCcGvq_wVb-tjxld_+WZb90|u zy?S-Xa$Z@g6!-4k`^`Oj_WV{#*=~$sBBCI^b4yE0Z(m>E6=Tc`7X*Btb53lGApqnQ z_j!DvW>Qa+%*=H8^5vfbn6p$M0L=gjyLa#Yoe-kH&Quv8*xqa9|J=y^FNgBNG!79|NfPIw}^6-%66? zEx)YxLAuD)P%-DldT6a{aoM+q&CcwX2w~3fLA&cpr80Tp!iCEtBO?=D^lCkI>eP=r zJ3HSB=i1(6Nl^)}MpvZF45aFSP(z|WH1-QArwbh>w#2M-=RGsaY%6NigBO9&yfO?*{LB^`EiA&{&i z)dEh_At3vVjWJlgdi6H~0BU%6_=Pd1Y%_Q&ol*oM0Ianp-%43>Eo%Ws83(|3U<-_x z8O37pZCkWOUh6kMIy(9r_bPv`dv6^OOUC$Mlv0fW@EtfK12QuspU;2WCa=T_U;r$f zK7IOXwOXCBH4OUzygHw&Wu}UJecp9=78h zmDmdcWcQ0Q*G;2R7QMuBiOkU{ILR2pYuB!Q$Cl5{c6bHA^P4ws{$*%r=!ey6bwnvu zwzX4DIP0*SW&!7a?sQ=^tDK0k8u_%EB8OP1BcF>IrD4l1264~OP4Nv+}YXr z$-aI2wzsyncH6aRc6Rp3)YR0BS3}kENm~D1uR>_J7V@usJ_C?lU0v_?_V)g9XlO`V zLFO!o%E-vb%X8<>U9-O8nwF~9KV&97zYM9jOA%!yvT#BgCmo1GhYsCl=FRu+-HQOs zSP&H}ELp0O*AJUO2I=`rsuf--6Dy@^VKj&a9As5@;NZc7f9mh={}F(d09FC`5`Y!} zIWJ!bYmH|6tj&$9R4Ns>ZWy|KUu>uvO$(!8Yv?B-w|&^z+4Z2!4oH%NXnJSmde=V2G6U?jt#8S$llyXfR#~b`TQ@cf9Yh4`!LMAk10pD~1 z_sCC3e*0%Z`T$u<*`6fH+xdKcNkQjRl8DH?$7$VrXJ!LbmqPwTCzJmzz-cLPIPw~F z_(h%H5P5?68JYa;6bB|lk@fR{XDVZi$=iw5tUfkvTU-dSC}(J`IV|Y>^2Avom@Apt z39`f|GJU|&0l?JLr%(UJ%s){|QTSyhqgiKvdnAryX68sKrI?wu);S_lUaaSQKEF^d zms^!mMhIb?1(GCDrBdlWfNB`t>uzjW0EOenkH06SY?_~+&*gGC@?Vm+*AxC1LI`6_ zNozeH$8nyB3dR_cQj*m=hzL?jrIbo?xttb4um?W3YSpT*96NSwb5=1I)%h+^?C9t? zqm)8hTU%JrWtA5-{yGl(8)8m5{2xNeRR0!k@FQ6!idaU9qE&>e5xy0r^HSGin{(>9i}iYjAF#LPlU zS;_hYWuug4X5nvBCP`8@#)xvcob!W#+w66|QY;o1?%%&ZJT^8q>B5Nv`Q@r9A z|ChE*i0gguvB@j4#hjm=rT0b0WXxB=ZXhT_7|M87@6bWfwp=fs_DPnP`6&MXcl-j1^HV42lZa2jn55j)S9!ipu-pd!uXCy!YnK{>2n?1;Gf_2w z45>mM5#WQz#Kz&|EGkvK~TfD`~gdX7S-06<0ofSs5oQvjd@0AR~wV&ec%EdXFAf9BHw zfSvf6djSAjlpz%XppgI|6J>}*0BAb^tj|`8MF3bZ02F3R#5n-iEdVe{S7t~6u(trf z&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_<@>e|ZE3OddDgXd@nX){&BsoQaTL>+2 z2Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&JM25&Nhy=4qq+m zzXtyzVq)X|<DpKG zaQJ>aJVl|9x!Kv}EM4F8AGNmGkLXs)PCDQ+7;@>R$ z13uq10I+I40eg`xs9j?N_Dd%aSaiVR_W%I$yKlkNCzL=651DUOSSq$Ed=-((3YAKgCY2j1FI1_jrmEhm3sv(~%T$l4 zUQ>OpMpZLYTc&xiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY z$4Vz$Cr4+G&IO(4Q`uA9rwXSQO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ z9DOhSRQ+xGr}ak+SO&8UBnI0I&KNw!HF0k|9WTe*@liuv!$3o&VU=N*;e?U7(LAHo zMvX=fjA_PP<0Rv4#%;!P6gpNq-kQ#w?mvCS^p@!_XIRe=&)75LwiC-K#A%&Vo6|>U7iYP1gY$@siA#dZ zE|)$on;XX6$i3uBboFsv;d;{botv|p!tJQrukJSPY3_&IpUgC$DV|v~bI`-cL*P;6 z(LW2Hl`w1HtbR{JPl0E(=OZs;FOgTR*RZ#xcdGYc?-xGyK60PqKI1$$-ZI`wBr znsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~ z=u}!L*D43HXJuDrzs-rtIhL!QE6wf9v&!3$H=OUE|LqdO65*1zrG`saEge|qy{u|E zvOIBl+X~|q1uKSD2CO`|inc0k)laMKSC_7Sy(W51Yk^+D%7VeQ0c-0ERSM;Wee2xU z?Ojh;FInHUVfu!h8$K0@imnvf7nc=(*eKk1(e4|2y!JHg)!SRV_x(P}zS~s+RZZ1q)n)rh`?L2yu8FGY_?G)^U9C=S zaqY(g(gXbmBM!FLxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k z9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC z$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMkx)5~X(nbG^=R3SR&Rp`ibn>#> zOB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$ z-M#aAZ}-Lb_1_lVesU-M&da;mcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&Yp!3n( zNB0JWgU|kv^^Xrj1&^7J%Z3ex>z+71IXU7#a{cN2r$f(V&nBK1{-XZNt``^}my^G3e5L*B!0Q>W+s4Ai9=^$VGcjKDR{QP2cieX!@1x%jPvm?ce<=TG z`LXp=(5L&88IzO$1Ou4!{5mfCvj6}924YJ`L;(K){{a7>y{D4^000SaNLh0L0BAP= z0BAP>oq!pi00007bV*G`2igk<4HFfSoT_U801I_VL_t(|+U;9Qh#b`#{{FwJU(?-_ zNhT&ff)dm$qWD0vFyJbx=k%{8xMRnTk>k&uOC$Fs?uY7MM+QXX^P80kfB@7zA!w2$^k-$<7^7sl z2had8@~;;OAV1H{boT7ow>q6pdUqUXf%}9~R}o<%(jC;K84iaB>h=16%I;HDY;JC5 z$B!R>*R$jlz_>~PGp6(2d++_rGTaAX0Ps8l@B}(4xuy$@1poqIKY%~V_-E#25z&`k zdg3)>=qaN6b9-HRD^7sy2vdq^jf# zq9Xp}fy^5#e>6#w?#nN~`~ZN}EX#a8<`;k?!V=O0Fz9x>88fej#3e~G^IQ;o^7B3+ z?tkojgt0}pK7gLb!(svm9ufeCnRBW8*Na{d-WR*q{ja)jfk+?HT{>Q+0E!r`lB`cm z=pvz(ofX8!Tf`bX*Q*rp+(9tr>oKCrH0bVi&zZphcfS4_!%LT7u3Uk+etnA2q|*WK z-HY0T52AkXAev7+fu!4=ThNQR&q4z58INhTh%jRehzO(Ke#7n4r;+{g3lab+5$Z%> zPjdnQ{XXRKWthvCvH8_kSO-8WD_DBr1$2%bn~5tiLH|z#u*l#}SgHybgXu1j3`sven@Wcr;4jtN&pqb8*0rFrUo^uQq zy<9{vzIqimPMtzs1Z`$Ch)`$F3kjFc)D{;u%HA#3iO>Mh7eS_qOclw#eNk(Rxxluv zfR{fBK+{!pKGQ^0=nmPHE4Y5@6q?LvF{4R@#O|sJ1Hf2qYdEgm6Kf@@eXf50{ov*0 zDP~KwPXQl`xqvq?1L!cbW?5Dm*>kCHZsPi>Q)n?`i5U&%{AFXS$+6v?5T?em0&uQk z?0xN{NAveM3oNKRQcHKKd(3MA3}Bb7+@=_f4NgS((f{?=JWv8ariwjBk50K*=!RZVwFH27$^r;L(+R-8E+R8N zcIW(gBsXuOl_Y3d=2aCVRrE#B7lGNe3+-24LG$q8nJUojM1=mOOStppml#~Uh{mIj zLWlrk=E?naS6Xk$0)k#3#|fs)JYor|iovI!PBc$6W%dIR+z|mk^%VBJ`sx(3v0$aD zXgvNnnuiWSdOgU^n=@ADKj(yDuQLFR@UdkJ$h933>J!wzZ~-Y18bqjBVvN=PKo$DP zBUrY~JM%3ou<~lL-3GVYmFYR4jdm&ql-2?Mx&DOPP zv=4AvFRDq57X-9hj=kLB&!WV=DWHxOjo&eM< zc9I7km`P!=%59O3v#J+}@v*HKP}U-6*RG-Ist=b(0V-Q{g>A=x zd18alh^j)?)<9PK&i|bSV0AUxQipCZGiHJ2h$zPqeCFM`c{i@|FXI&*VL)-$L}Wtf zXf%Uvx<-fE?HO~7r7m~P-Z@bQ$>RaOSX+> zB8=^|EK-j9W5pD-a26;lO=Ia$M1Xc1((g|Q@SbA;dh6DdRrx}UMR11&)a>q1BZnsx z0Ut%pQIQMqA`n>BOR#Gf!~q=kzMSA|*QWeE>?^7?I91$70~Elv&IXu#~=onwCP{?rDtWMc#J-FJoB3bR0=br!f`*;L%Z0w`)m z!T=WTbmYhc$G3Q&gOch*XfQ+1o`tTh%{=D|(`ldPEXl($7HOz>p6DO?lh`uUm%e*B zKldCmo3qEB71W8)w7Y)r0p#bOCq#Sx847`d$RBgj&NlPt69yrbUZ}A{br03UlVn-P*xTj@r1U zRd_%Z_&8GwOfCYiy@ml%4ud;OaDZt$Nzh3WbeYjf5_Fi+WyW%npli=J?G)O)bwH67 z6!kB2#egE5E=*m1U#|d!8F=$e42aMZLC+WrY<6(9LCeuxAf?BG@nnJrVR(F%rqM1ldgC_9;Bk2GgOp ziz>1#o1)J@hAvr#Eu@R4z#_><5n2F9Q{eU2G5q#hWarOgWXz-{aJ7Nea;yk)0?KT4 z=`xEHOw1I)?OQ^H{l+H}8&q|&{;(3;cR}gtr=ib0gY1VNFur&ZeB}x_SRX)Mp(Bnt zX9@S+2YL8m=)r@KhaLiX9jDVSGv`oQl``kX10sH}c1c?_0>?7L)HsI^L!NvRN`&n9 z-@$8Z(2Wh~XtI=0iGW4}+Gs#K9q7smbZN;S=cGm#qb>=OBK&s{go&U#R@og=W3nZ+nnyM6p(t;{Hu zY{q7~)E;GJWo7l&ty}96;Rz;dfa?Id_Sr1BZv^0$9dEehEZ~>9Z@>NaKhrcNV+@bS zLvnN}e}m6|lh=<4d~-^H7XDk?q8klM%+7PaQ@FMC&GjJi1E(p)f|PTg+T zR}i_SeBP2iBuV0G0%c}eI0krxQ!G7&_+U}`i~aWqCHp_eP-C?`5jiHDMyJ;Q8Dq!= z-EgjgnQ2ixKqrbfA|jh2LJn08;in>iG$Kg3c8$m51i+ph2zZ<%Ny`}1o9kv~ z5)tEDNF>M&0W62ilx!J*o3&akv)Lvq698fIbyde6d7A?0+hc3qB?!3t;TFZThRK>m z_x+N~B3d$;WH9+yYG{vICIE*Mq*(0ZsG^s<52k7hKI@=?tOyzR(%@h2BhP!=|7-g% X`o7_XU0-Ik00000NkvXXu0mjfYH+vs literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/64/logitech-g-keyboard-panel.png b/data/icons/AwOken/status/64/logitech-g-keyboard-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..a20028631fd209f300d9ecb7cdd8ef268db29034 GIT binary patch literal 5814 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 Aw*UYD literal 0 HcmV?d00001 diff --git a/data/icons/AwOken/status/Makefile.am b/data/icons/AwOken/status/Makefile.am new file mode 100644 index 0000000..1dd1756 --- /dev/null +++ b/data/icons/AwOken/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 48 64 128 diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am new file mode 100644 index 0000000..09b792b --- /dev/null +++ b/data/icons/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = hicolor +if ENABLE_ICONS_AWOKEN +SUBDIRS += AwOken +endif +if ENABLE_ICONS_MONO +SUBDIRS += \ + ubuntu-mono-dark \ + ubuntu-mono-light +endif + +EXTRA_DIST= AwOken ubuntu-mono-dark ubuntu-mono-light diff --git a/data/icons/elementary/Makefile.am b/data/icons/elementary/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/elementary/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/elementary/status/16/Makefile.am b/data/icons/elementary/status/16/Makefile.am new file mode 100644 index 0000000..76ff8d8 --- /dev/null +++ b/data/icons/elementary/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..6c35d9a --- /dev/null +++ b/data/icons/elementary/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + diff --git a/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..b48e4e5 --- /dev/null +++ b/data/icons/elementary/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/22/Makefile.am b/data/icons/elementary/status/22/Makefile.am new file mode 100644 index 0000000..26fe3eb --- /dev/null +++ b/data/icons/elementary/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..8d30176 --- /dev/null +++ b/data/icons/elementary/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + diff --git a/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..b8d215d --- /dev/null +++ b/data/icons/elementary/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/24/Makefile.am b/data/icons/elementary/status/24/Makefile.am new file mode 100644 index 0000000..05a36ad --- /dev/null +++ b/data/icons/elementary/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/elementary/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..9753175 --- /dev/null +++ b/data/icons/elementary/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + G + + + + + + + + diff --git a/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg b/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..21e0008 --- /dev/null +++ b/data/icons/elementary/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/elementary/status/Makefile.am b/data/icons/elementary/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/elementary/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/icons/hicolor/16x16/Makefile.am b/data/icons/hicolor/16x16/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/hicolor/16x16/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/hicolor/16x16/status/Makefile.am b/data/icons/hicolor/16x16/status/Makefile.am new file mode 100644 index 0000000..d38627c --- /dev/null +++ b/data/icons/hicolor/16x16/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/16x16/status +images_DATA = logitech-g-keyboard-error-applet.png \ + logitech-g-keyboard-applet.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png b/data/icons/hicolor/16x16/status/logitech-g-keyboard-applet.png new file mode 100644 index 0000000000000000000000000000000000000000..9cd42c6b85a4e69707dc284805a254172fe232f8 GIT binary patch literal 771 zcmV+e1N{7nP)Px#24YJ`L;%77y#Q}|9T83d000SaNLh0L01ejw01ejxLMWSf00007bV*G`2igk+ z4JIq#dD@r&00M(aL_t(I%YBnSXw_E~$3N%X`+L8<7xRcTi4h@Fq!1HR0xi)FP6d|^ zf<-60INGtaATC1bQs`85&<-th>C~kZoI_j1;-BhNC}Kq;NyTS0d3pEVufy*}+QkF+ z!hw6foO3?kYx74h^XD(80r)rxJ;hQ1100~{bwmU2qAO+qy#2u?4B%W(?LWR-`Q+)n zYX_x4v8ELL=LV3#JosA&mTw+9b@-dJW40LqZhpUVX6?uGUv#Sy+aopZ-n>SXl97Z2 zMX=TH^XS&kCw{f_0AKlM66^P_9~^6y2nHdN1Q{cD_QK>mKJ^Z^dzPxxWqf=$b$J=r z@BFs-r9|9*hFMNh5kdra+@RjG$n1L`GB&Y~G|2pO|2_{^E)gvuGssCB1&dywlmS_V z=z&&UQ*vVd$VVgse_j0=_ljgS-8v(+Ktg3e5V#Daf=a>6h*oG%9bj~P509_?z@Ybt zvb}-A2-WBWAp|fe5?SC_v*@CsK#T-$C|GFGZnv48-Fu+{)F+Rlf>=}Ro%r11doIeUl%uC2|J7%_m~+TBM)x;#Axa<^ZqI7=|!GD z`Thi^NcoLt zyWc0pPMTaeeEX~!5nyU||AqdsPg`que_ucuN(qvJR6;X~XEV#VXDl!=w|skgZtmic zMgTH|U54dI-mZX%O5FL;K*2l#iO?FLDv^7Fz$>WWD}2{4W$^$2002ovPDHLkV1i$f BS$F^d literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png b/data/icons/hicolor/16x16/status/logitech-g-keyboard-error-applet.png new file mode 100644 index 0000000000000000000000000000000000000000..b27ff2c908b8d63883fa433cf550890ed07add9b GIT binary patch literal 780 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/Makefile.am b/data/icons/hicolor/Makefile.am new file mode 100644 index 0000000..01eff3c --- /dev/null +++ b/data/icons/hicolor/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16x16 22x22 24x24 48x48 64x64 scalable diff --git a/data/icons/hicolor/scalable/Makefile.am b/data/icons/hicolor/scalable/Makefile.am new file mode 100644 index 0000000..dbbb83c --- /dev/null +++ b/data/icons/hicolor/scalable/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = apps status devices diff --git a/data/icons/hicolor/scalable/apps/Makefile.am b/data/icons/hicolor/scalable/apps/Makefile.am new file mode 100644 index 0000000..a9adaed --- /dev/null +++ b/data/icons/hicolor/scalable/apps/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/apps +images_DATA = gnome15.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/apps/gnome15.svg b/data/icons/hicolor/scalable/apps/gnome15.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/data/icons/hicolor/scalable/apps/gnome15.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/hicolor/scalable/devices/Makefile.am b/data/icons/hicolor/scalable/devices/Makefile.am new file mode 100644 index 0000000..82eb519 --- /dev/null +++ b/data/icons/hicolor/scalable/devices/Makefile.am @@ -0,0 +1,5 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/devices +images_DATA = g110.png g15v1.png g15v2.png g19.png g510.png g930.png z10.png g13.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/devices/g11.png b/data/icons/hicolor/scalable/devices/g11.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe6d3dedcda3a2f339b133e7d2f71a85ba120f2 GIT binary patch literal 65562 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 BIo1FG literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/devices/g13.png b/data/icons/hicolor/scalable/devices/g13.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe0ef9fb79a02084cb112522a2f2b83cb6239e7 GIT binary patch literal 43162 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

  • j(j4i9`d?{n5g6MHq9Z-}MT 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
  • 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 O%F@jCcGWGPg#Q8Y)w3V~ literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/devices/z10.png b/data/icons/hicolor/scalable/devices/z10.png new file mode 100644 index 0000000000000000000000000000000000000000..dfb9dc5ec71f5d2b3174fc2fc2963b76baa59e52 GIT binary patch literal 63438 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/status/Makefile.am b/data/icons/hicolor/scalable/status/Makefile.am new file mode 100644 index 0000000..3f0ae9f --- /dev/null +++ b/data/icons/hicolor/scalable/status/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/hicolor/scalable/status +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg b/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1524705 --- /dev/null +++ b/data/icons/hicolor/scalable/status/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg b/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/data/icons/hicolor/scalable/status/logitech-g-keyboard-panel.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/data/icons/ubuntu-mono-dark/Makefile.am b/data/icons/ubuntu-mono-dark/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/ubuntu-mono-dark/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/ubuntu-mono-dark/status/16/Makefile.am b/data/icons/ubuntu-mono-dark/status/16/Makefile.am new file mode 100644 index 0000000..7dd3c2b --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..d698552 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..c607eed --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/22/Makefile.am b/data/icons/ubuntu-mono-dark/status/22/Makefile.am new file mode 100644 index 0000000..7df1dd9 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..c464548 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..2043e54 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/24/Makefile.am b/data/icons/ubuntu-mono-dark/status/24/Makefile.am new file mode 100644 index 0000000..885ae38 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-dark/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..9c94f98 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..c58d208 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-dark/status/Makefile.am b/data/icons/ubuntu-mono-dark/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/ubuntu-mono-dark/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/icons/ubuntu-mono-light/Makefile.am b/data/icons/ubuntu-mono-light/Makefile.am new file mode 100644 index 0000000..d52da2b --- /dev/null +++ b/data/icons/ubuntu-mono-light/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = status diff --git a/data/icons/ubuntu-mono-light/status/16/Makefile.am b/data/icons/ubuntu-mono-light/status/16/Makefile.am new file mode 100644 index 0000000..9b24ffd --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/16 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..16362b6 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..7e4df24 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/16/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/22/Makefile.am b/data/icons/ubuntu-mono-light/status/22/Makefile.am new file mode 100644 index 0000000..ba851a9 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/22 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1582e3b --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..d09282b --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/22/logitech-g-keyboard-panel.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + G + + diff --git a/data/icons/ubuntu-mono-light/status/24/Makefile.am b/data/icons/ubuntu-mono-light/status/24/Makefile.am new file mode 100644 index 0000000..4242660 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/Makefile.am @@ -0,0 +1,6 @@ +imagesdir = $(datadir)/icons/ubuntu-mono-light/status/24 +images_DATA = logitech-g-keyboard-error-panel.svg \ + logitech-g-keyboard-panel.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg new file mode 100644 index 0000000..1f7b262 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-error-panel.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + + diff --git a/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..76cc1ce --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/logitech-g-keyboard-panel.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + G + + + diff --git a/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg b/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg new file mode 100644 index 0000000..187cfa1 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/24/old_logitech-g-keyboard-panel.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/icons/ubuntu-mono-light/status/Makefile.am b/data/icons/ubuntu-mono-light/status/Makefile.am new file mode 100644 index 0000000..fe7d9a1 --- /dev/null +++ b/data/icons/ubuntu-mono-light/status/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = 16 22 24 diff --git a/data/images/Makefile.am b/data/images/Makefile.am new file mode 100644 index 0000000..4a619b4 --- /dev/null +++ b/data/images/Makefile.am @@ -0,0 +1,57 @@ +imagesdir = $(datadir)/gnome15/images +images_DATA = g15key.png \ + g15key-error.png \ + g19-background.svg \ + mx5500-background.svg \ + default-background.svg \ + locked.png \ + key-g1.png \ + key-g2.png \ + key-g3.png \ + key-g4.png \ + key-g5.png \ + key-g6.png \ + key-g7.png \ + key-g8.png \ + key-g9.png \ + key-g10.png \ + key-g11.png \ + key-g12.png \ + key-g13.png \ + key-g14.png \ + key-g15.png \ + key-g16.png \ + key-g17.png \ + key-g18.png \ + key-g19.png \ + key-g20.png \ + key-g21.png \ + key-g22.png \ + key-l1.png \ + key-l2.png \ + key-l3.png \ + key-l4.png \ + key-l5.png \ + key-menu.png \ + key-left.png \ + key-right.png \ + key-up.png \ + key-down.png \ + key-ok.png \ + key-back.png \ + key-settings.png \ + key-light.png \ + key-m1.png \ + key-m2.png \ + key-m3.png \ + key-mr.png \ + key-vol-up.png \ + key-vol-down.png \ + key-mute.png \ + key-next.png \ + key-prev.png \ + key-play.png \ + key-stop.png + +EXTRA_DIST = \ + $(images_DATA) diff --git a/data/images/default-background.svg b/data/images/default-background.svg new file mode 100644 index 0000000..7d7ab42 --- /dev/null +++ b/data/images/default-background.svg @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Gnome15 + Linux Desktop integration for the Logitech G Series + + Version ${version} + + + diff --git a/data/images/g15key-error.png b/data/images/g15key-error.png new file mode 100644 index 0000000000000000000000000000000000000000..35e6b9163481dea48b72c7422916acdb86049381 GIT binary patch literal 6257 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 bPuKqi3&_W_$A!;*00000NkvXXu0mjfD^UC) literal 0 HcmV?d00001 diff --git a/data/images/g15key.png b/data/images/g15key.png new file mode 100644 index 0000000000000000000000000000000000000000..96cc316f57d36af3b18b0672a71bf247373959bd GIT binary patch literal 5105 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 T?LXwd00000NkvXXu0mjf1>I7o literal 0 HcmV?d00001 diff --git a/data/images/g19-background.svg b/data/images/g19-background.svg new file mode 100644 index 0000000..08141a2 --- /dev/null +++ b/data/images/g19-background.svg @@ -0,0 +1,574 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Gnome15 + Linux Desktop integration for the Logitech G-Series + + Version ${version} + + ${text} + + diff --git a/data/images/key-back.png b/data/images/key-back.png new file mode 100644 index 0000000000000000000000000000000000000000..01b92d2bb8b343066ea3fd13f1bf76b0c51b7cd5 GIT binary patch literal 421 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcCCS`k`pyI>BwOMdQR1AR zo12JC5i7V{mw zE_HqFW`;-il6aKPPI+)$q=Q@ZgnPmgx#F4eGZ-1S=}*wAnXWMLxmpkFulWWM2mdj| zyK6|@5vkWxVw4k+6SXqbX}@56p)V)%pZDvt49j_M1ZL0CSTkRDLZ=Nwh-1*!uR)D( zu9_{6tYBDis^ho6w+08pg(D&)w+;hSGC4b?KS$4hqzGVT!fx*+& K&t;ucLK6Tf(3q|O literal 0 HcmV?d00001 diff --git a/data/images/key-down.png b/data/images/key-down.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c6eee994ad71837d8817184d987389a7394ef4 GIT binary patch literal 405 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcC8b%ixI_^sBwOMdQR1AR zo121pCZGY`mEFn*l=c*-W9c@Nr!4y-R^@R6OtD10hhct^&|X(fS+9lWa)q z5wx0Xve^yZ8Q(S)mkX}>w!zl9p!LJFx%xHieatJeF0AZcyJkO|zSakhWAiNT-0L`4 zELm_kB}s6Hblpp#I-`l=dZDa&9NhaQemDp1y|c07aiCD2>VGwfOJ%=P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JC;IlWE2P0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004& zNkl*kNG6(?$n9NDP>*2^Jn7PVwna2Fj^fL zt!^+jOs10Jdc*6*jLilk%Oc2eihm1a?`J0*{?&$jCM{W~$PfY`2B6iIWs1jQR4Nvz z)qJ2__L2?{5@fSlW$XG8@c%E+0H~_izp4c=1i+?}fRO4>GN}D1eFYdy74Vb1Dq8>m N002ovPDHLkV1m@p?veli literal 0 HcmV?d00001 diff --git a/data/images/key-g10.png b/data/images/key-g10.png new file mode 100644 index 0000000000000000000000000000000000000000..660251f7408841623901d9b1e31003a8ea5ea07e GIT binary patch literal 718 zcmV;<0x|uGP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JWjJ8S(@G0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006d zNkl6#snPDcFm}GK)b6yGQ*6c`&!bdN5Gv$8M;Q2GyZ9EJ9`Rwse+` zl5`S7%jl*Nv^oV^(%8DzE}MaSnL%>Lk*H?(dD!*2IWuhtIhP)M;lrEH_q`8~_hG(3 zIs5?b16Tt<|M;v`dihb+eDwet?>(yk{5}AE06K8+aTI_lDkC`#Zz3-$tWg?f&;ML0+oNsG$E+7QvlCMEU3skh=RB!R!0!*WT)RKzsu1*_= zH*+W+Cn{R5=UQ)%ZJT4aZ~exp{?-}}JJx9phpTyIa3vaH!pd@rI@;SQD9bcB&`;~S zM#^lG9wZWc@mNCxJsKIIlJ(0~z>DWooWFOskMY#gNxOi@qoYjd?YUah$HvAtqM}9X zy5=e%m(6fKD9emLrI+mjzJAFu;Y4F20dO4O4nMV|(%?Wp9jUK(6|l8waQ=`aF*c0? zb0l7-y4`&ws_o2^r%u=8RM!zgFfNr!b{8nh8%{Pgohe`PNf171-xkw#opB%#V617H zUBL9rG$-%GVmKEHAt=ijipTdYV19mK*N)K$V^vk1ZTN6__>VSJW|NK!>9zxBo14k{ z`Qz_w2!I4YH*Z|0@v$+=WiwJo=0J?PX0=-q{XtS_E9~R$J zcULFv>G}U(fV_wO^F9EF0famfknsFT2CpBb+lC5|TZfyW%m4rY07*qoM6N<$f-ht= A`~Uy| literal 0 HcmV?d00001 diff --git a/data/images/key-g11.png b/data/images/key-g11.png new file mode 100644 index 0000000000000000000000000000000000000000..7f40ef7ac875e66678651c8f86b1f05f20b20750 GIT binary patch literal 587 zcmV-R0<`^!P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Jd{Vj%f@40013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004^ zNkl4>xI+MoF5)FxoYO zG}|jvQp`XXMJh2GdUhJLYo+!?(zkTrz`K{-5vIe;Ai7L48|0MxnGl1xDwD{-ymkj!L|*g6WZS(gww3ga%5#_LNR`CJxf zr!g#=Ox*);94_MLpoRcy2m>)pnO>eJY6v}j)nV^YvvX_n8`DkkB24pR*=A)x<~BtZ zvzat%HF?}Fs#eOBOvGu==VL!kYPEC_2++Im)-!lN3@hL;7-WFMz9QPkwWmj-hCtOy zxo7Zx7*@c;eVzf+IvoM%x(%Ov>iUX2ZWoQKR6T?D!zh?gWNkeXiFfAA?A)I)ENe1( z$tLysc>n+>SAdf%M$?ATSd>j}JDpfC8lh3E;pO?Ep9O3`_Wk}}+c24kcNL^q8;UUP z0SC~=`Wl5pA`x&r_J002ovPDHLkV1nK|>mvXF literal 0 HcmV?d00001 diff --git a/data/images/key-g12.png b/data/images/key-g12.png new file mode 100644 index 0000000000000000000000000000000000000000..530aa1c20acc203cb2a35f71128d09099f820207 GIT binary patch literal 728 zcmV;}0w?{6P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JeS*KNYb60013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006n zNklt+*veV2x#M#qF@nYGASH z2zWggF)==dx5*`JeamBYWf@aXCUL>-HU_ZSFdLl)H5JrUU99hRFCvd0;m!OjjE6&T zx3}vsgfj2={iLRv`sE&m4dG&ZCtIt*=hhv!;bOY;hYF0V%CF1O290h6HQ?uU);uu~3MsE6dDpe&tePfe#-{ z(ACmns%|p^3$N!Z68L`?)Bwm@SU)QPumf;eL_o3pNd~JQr9S};^$Q@uye`530000< KMNUMnLSTaSurc@m literal 0 HcmV?d00001 diff --git a/data/images/key-g13.png b/data/images/key-g13.png new file mode 100644 index 0000000000000000000000000000000000000000..9900e87bdeb61af015584521fa9a280ba2f1efa9 GIT binary patch literal 746 zcmVPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Je#iTHIX#0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006( zNkltFIxUu`TAArb}OL7p#Sb-~-!!X+egH0o# z)6;>`+X;M_n#8xSB62g+c=9+2S6f^41RRHFsiz=C1}Snyj04cya~2Qo-$U`!M?^

    qF4!vIt7v*d1Xr#h>Z z0)8JYFBK_0o}wF}5IcCFzMdk(!}ML=)D$=H{kS7xN}? zyy>ru+mWLV0D#3}!Ql04+A$nUjI+n9t8b64zm~ z*;q`Hr0Qch6b}D#4AbMO-@1ZZv6v=@vbjMg+-_|T03Z&abG<#3h{q{6Gfm>!Doti5 z=aQ=S_P%x0czysI|Krl!^$nY;2 cjQ*7V0N=^HaTf+mg8%>k07*qoM6N<$f{7kMK>z>% literal 0 HcmV?d00001 diff --git a/data/images/key-g14.png b/data/images/key-g14.png new file mode 100644 index 0000000000000000000000000000000000000000..6e90464d8383618f82413c493acd14774168ba1f GIT binary patch literal 654 zcmV;90&)F`P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Jfi8Rt4_>0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0005y zNkl}I?d>e&_W7fbr$d&F4eae^ z1lZwl5Pjzad9C^XHU;f3tx+O|1W3&kT+33 o-w2=yfYT%c5!0_^F#A&a4?c?t%`o=nZ2$lO07*qoM6N<$f_ixrf&c&j literal 0 HcmV?d00001 diff --git a/data/images/key-g15.png b/data/images/key-g15.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac92442f6d4d86e193a347933dfd2af3011fcff GIT binary patch literal 743 zcmV?P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Jg1&g0Q{-0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006$ zNkl%^eSXl}py_-Tfw2C*cytvfVR2o2|!8_kuP?kVh%8S()jpDx0b4_e!uhU7&lA_;i zYc1Ml$K5-5Ik~Zmjg5*}IeD2wi_I#6ON#+onwx3R>)F%Y#pGs!Gt)lyxm@DI8l#bq zM@INv`ld+WVdn!^@)@vCN~p`1fGtKk+Q^( zi8$wH-!rtbOndvSLNb}dS|lt$PSw^Dz`mmk%+Il>yNkzkIz#`2=a5*Y zieLLkmRIJPv**g@^!4|n?sOd@(Fo%41g2iP0RWdR77PtP%3s2tU!w*CR>-4x7bvxxcq z{?alW9vs|PhBMQ?KbpXl`$Z9k^mUIH8XAh15CAm*JM8V87$0Xiw93@ZHiLl$x}Q(d zR9~++UL^v$>S_)R3~+rd%Fo+deC_q{+SMy8sQLe2fS8K)W0e3712CyXz@_?=3~E10 Ze*kWO*7^+reaHX+002ovPDHLkV1hDSKcWBt literal 0 HcmV?d00001 diff --git a/data/images/key-g16.png b/data/images/key-g16.png new file mode 100644 index 0000000000000000000000000000000000000000..0e5015393842aed92d1727221e3cb62d825f435a GIT binary patch literal 741 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 P00000NkvXXu0mjfWV|9N literal 0 HcmV?d00001 diff --git a/data/images/key-g20.png b/data/images/key-g20.png new file mode 100644 index 0000000000000000000000000000000000000000..3400fa6e5efbc68455314a24b8a568027dbd4ff1 GIT binary patch literal 789 zcmV+w1M2*VP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Jwd8ZQ_ss0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0007P zNklVv*j>gu362oP=npKeE>v7Y9t3Ck3|%zamZ$SAhSsX z7!Bvp@94waj2EjbA_9IN2A@5}DZM^DQK3NF^;SH2{0Nio_i)&4&{tIsIQZ7_-5Yh>?BGivYrb~CgyfoERuNg=Q^DzFK14LPM5Jxg-*A18#X|#(ZGb6 zX%E#@SCd*0sH44&78jy4Iy^*O77Hi77<^9OqF-rk^#`4-tmNF=Jw42M_+Wtj*RGlw z58QJ~1>EWFWkQpw@vrwfvA#x>Opx7bWnaMWe_n1bV=*3+3iuQXGC@;V_{Rbc7Zp+T%V(lwf<}g3(q5&KV`4nU zdCI&z#_^@^QUP|Wl?k;*Lt2w;0*(|H)8ayu?~Pgz(&Fu6ig|NjC+Wb7Zw0iXn+l}Ug__9q$Sew6+KKPzw0 Tm3_&w00000NkvXXu0mjfdl^ri literal 0 HcmV?d00001 diff --git a/data/images/key-g21.png b/data/images/key-g21.png new file mode 100644 index 0000000000000000000000000000000000000000..7f59e725e8bcbb9997d52cdefd713a2088936c88 GIT binary patch literal 724 zcmV;_0xSKAP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Jw$kW$$GG0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006j zNkl)P(4)8gZXFlcJWz! zl9BYXO)+{bW=T$k+K4QtBSmK0sAf{iO4NJ(df58mcK4^Eye}O%aQU6@aK7I;-{Jm% ztXTnc14shkKlWsmmH4#L`g!=-EIV*JjUC(*BA@<;b?6wUPO{G6Pf~b z4b(M5TqyIN$3yB`Nx$0V7T5t*HN^GZ9bRZ|HYon~t($qcf_V0$!{ro!*g}-;ZEdtE z3j2C{xW1O=>~x3&swxtnPd?-4^e1j_edooN7Ln^69u}CRH8s5N^>R1z^h1@=k0dd6veO^Kk+t2Ve(@By_j6@=iJ8p}E=K41=;1i+ zO-*@;^Ka(<3W5Lsf+_$h6YHmB02TmtlL)A$KgnSBqx1*oe+wY(te2$#0000Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JxO8-+KH20013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006F zNkl{^Ek)lPP3tW(lKQ!wddQ$bxF>Yt`=!MV5> zmn1C}M^QO#yfrvNsp%2*4$uk@drP6+`;jNhmJSuPvILF?{exRb%OL7EfqNvo%;aIqEI4lOjp$kk; zP4Z!Bk)J;*tgbBcYWH20Rm|!OZCptT&^mSdt&}HfUf_OE}BHjd38$^aKnj){W89C$g ziC5Dsow31tHBAa1*F+3mvaepsf!0=wcr#2x8{Qos-?a^2JexJy(OWln5E}-b3eo+P7fX^WVF~_fD cIDIMo1FJyYwX^F~Z~y=R07*qoM6N<$f}DFLR{#J2 literal 0 HcmV?d00001 diff --git a/data/images/key-g3.png b/data/images/key-g3.png new file mode 100644 index 0000000000000000000000000000000000000000..69386280ea52d82f6859d294d6f5daf7f7e805ca GIT binary patch literal 686 zcmV;f0#W^mP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JD!F_`wDM0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00067 zNklm_9FI*VroHysZ!@JCb zUu6d-fK5RBdJoi8+L%gA3waOSjeM&{>0MrMF#r&ckwL1^=ns&K6}d6*Dr*pr*SLFUzwm9me$T{P3)t!c#2@LzZOiro*B$G)7FI?ov(PIQvm25UkJRYa}M7QL-PP<4X z66A6@+~>~A_W{4ZxdiTq!*Uk^|K)wn_^IArxpr)Hq!Hg3SX28DdOzv+Lr-6zb!S<_tWTo0B5LKJPB9k%5yv zJrZAevDDllTv%L?&o{=$aVrXkJ30ud>K^W)_IBRAeZzWc6+OR=!{LyauItT@;k~J; zx?}kI)l12potfE#S4uy`pvO};0V@!y%wTf~v}s`T4rL|Nk#&0n#Swr>%euP)ss#*YqnHW?xEw0!If_ U*eTI@1poj507*qoM6N<$f_;`N82|tP literal 0 HcmV?d00001 diff --git a/data/images/key-g4.png b/data/images/key-g4.png new file mode 100644 index 0000000000000000000000000000000000000000..a524e3f644e455e84f524e00dca572ac159cfab3 GIT binary patch literal 594 zcmV-Y0Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JM3}w`;!u0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00050 zNklv>#50gu}y8>G$> zI7MpQF3m8;Tws(vsg_m($bTO;V{y@sn8d04u?uY%uH{((G&2Vxg--p02M% zva)RR0J+PIY|v%35`c!=0D#)eW;%@>)1!1xy*AsYx!(fF=d!Yam1R`{_I7vZrBWs* z&&&Bu1FWyE$_5^{t8VP0x!OV1f>3P3a3m~0Py2l6=QwmQ3>F39$G-)NbjyK&>RUyR z4i$5o-U0w*0BC${ltOE3lu2KZR4P(35vT2~P2&3cRE@t1@c%Dp0U+vFUo-+}1;FV9 g;Me_12E8w(ujy!~JE5Cd&;S4c07*qoM6N<$f<{618UO$Q literal 0 HcmV?d00001 diff --git a/data/images/key-g5.png b/data/images/key-g5.png new file mode 100644 index 0000000000000000000000000000000000000000..b27eb278acb21782850f4202b2aa1f5f320ade73 GIT binary patch literal 679 zcmV;Y0$BZtP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JE%q@^ozg0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00060 zNklip((A=0a*I#$B$qtT<$dq@zHjaeKcE0N zfo(wkF}EwV6n-n7Ie@}{G6N_M;0EC1;Bg!%+A5Kpz#3$2l{hSe9gAVwK)`>A<@-yl zzl*cCTO^g-;OXNC=lc4peC;k5Qr2qH42F|Bp{gQf&5&1yhjj1R-d;mry&8rI3 zdwp+VL8Pq7$9zs+ufCMzdP0IDSLH}kli9nqh0SzIZ{TD{hX8fsO~l{U)US;O>o#DR zkKO+3bVSTI_4d>h?oK!9OqyIS&&s0^0R286vvYU;{s?|XDrHRu1O6(_Y`~foMb|%l z$Tv2G;Ye72zCJd_X^#hIYbz6~y3gC@bh7qpmF(7g_I8VSy`Sv=j{Ro-JQEiyYj zEnAxz`MUF2o`wpy N002ovPDHLkV1k`QEZP77 literal 0 HcmV?d00001 diff --git a/data/images/key-g6.png b/data/images/key-g6.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9ff10257b7bfe5e9cbdf273b237131a9b3ccd5 GIT binary patch literal 687 zcmV;g0#N;lP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JNx}X={Z50013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00068 zNkl z#ED=qM&m>YgQne8yq+bsgw{c%Qd`uh^f_3+7~9f#iQM8#Uh>a(zkKiAyZ7$<;HSBP zn?MGTf9#oZEH}RePwznBKRpe&9l$UUWan)+P}IsL*^fPz)yn0t%^I-T>L$?FdzQKT zv%F3ieBLaQT28XK5T&QPyMjI3-p=Ael#SdvNi)H%NQ4GWtD8j4P94*Ak*)7F{lY-M zV*PDx^5xTJ1ryRox1ENX6*$+|ODLoRV45bUPM@K9?>0rGpwC(b?oCgtw;1R@Uw5vTUOZQPv&Zu{|AXoeR^Fu4h6lV}0jhq6 zUwr;UPOV+LdPUx5R^`s*r0i*Iw3fi?N?L8`ZE2~S!1~%dwYK%`tJ2}|aceygi^Y}h z=r{s^=5V4poK<<#G?nf-enRPB@T9ddoR7{ceSL6{qk#aPrY6R8y((`Q38hP=lG0A6 z)A|_RotoNq3}erpD)x}yFNOR@1rrH}t@QvP8p!2K7iD&4MpDa3DSph0vGh_NKA4k0 zXJ-X}tgB0&Jbom(_iJJrujKZ`gfwWH)QR!`EFf!R{Hz1m1q5tLAY%KK47)F-KLBc~ VMh%0lw_^YR002ovPDHLkV1iVPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JNRGZwFie0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0005g zNklA=*SpD#6Jw&_at8$_kFHf~ou{bXNtnQnpxh33frT6I%&Z znnkhL#X`lxY;=3S@FPY{SWt9{L{yydSQsCEogv?aN(Z!?%{IJ%mW_a1n>nw z0s#MWBr?66c+x!n28~}v9Dq{=@EL#~Zx0uMw2~P~DeOT~$&AA`IAAk!3TSO<#_YE+ zc6YXMb(uzNe-A6mOK5Cpc*dh?NFx`dn>ri_kftGx?ygS3m2pyk&U3jTd!enhMO?D8 zwMlzYY!Z?DT}u$SOyI!K`PCi;5VJ9gwEMj~(s#3786ngn+KjPygicvC&b| zG`R5NyA^)+!qH(&2=Ho}bj&Z$Rg{Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JV1lqC3(60013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0005? zNklV*WdQnOhE5TdvESd%h zm0nB~#e+W(qtPZ!EgscGjYLZmsXuzCM7$`|L(|u8*|@Nvd+ETy@aALYdml5r2mCS% zfPMgZ0CdluFZHtEuja=O(D+|J24FD(cm=?Xhqp=qMMFuFN6^O#hLRk*nGrghDgm$O z8D^%ZaIlxb^;HqM!z|*fF*G$c%5c2djQ+mY*od#;{PY{9qY=DleGY-=RY{bc)FHp0 z==P?pclg>FHWUmhV!FFJ)wV%7(c<;6B75mwayK_qmEBH*ZwBc5`5C2>2^tOr82;ky zl;NMaTrA$tq_q)vKQY0I_}bf4_j+kzp5f~yiN(uHi`oc$Im)pj)eZ*%P?Y#LzP6@@ zQptoYxAl3G9*g3g1j-Jf(fB7tq`s+Es}+JE$j&&91IKaNhA=ug$!^l(^~iEc3ViyQ zWM@ws8d&^cJ+4iGmDmc4dwY6NFG;Af+cD(#E8;~_R8-IDbZTG2(UFlm*D#e#FzmZ< zSP>H*AJ;a70LTDPSLaKbibN=Pn5E+756bNB(A?|{Np*EHtibd1c5IB&JKJ=9Riu;r zR|*A#RAIA`N{|0<0SY?yFPH$B0Z2L#2d-~a#s07*qoM6N<$ Ef(yeVy8r+H literal 0 HcmV?d00001 diff --git a/data/images/key-g9.png b/data/images/key-g9.png new file mode 100644 index 0000000000000000000000000000000000000000..0c955a227048fba01adc7b2046bf4514d824f50c GIT binary patch literal 672 zcmV;R0$=@!P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3JVYQchbxN0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0005^ zNkl0xAD}0*xv2#e42p-+O0)-wt<5PU z^ccKoQ~c=Jh)O~{YhsLU*ebXOW75PBwFhN-Fua<&ab=EjgW|_8Q21{@24FG(m;}&=m$zyFMXuD6I_P6ES89iD#zSXQ6(HFA zvHU%VqvQduFN=^;A~rWd=;`h*PsDL>&(0zdk0Fyj!;jS!v|6pt5>atdXIw6#+nb91 zc4&}sKhMoQ!gJXSHCQZa+n@|M1UpNVOzcx%Z!guD%{2AlJ!Q|+6pe)GliSVsQc7g} zuFg&tfA#yd30MdOSdyW^0oA=050i}F(caGD@b;EA0Vh8tmgIGPJpoX>2Bj+~} zq|crdP;m%({vIMy-_$Q24}3m9WLd_T)5*?_Mx(YN{IgY{rbsrO00h1dxcwP{`*X2zGWJjqYe)L!a0CDbhY;A32Tm#ROXMUcflt}qs zS=!tPQA=~P>h}Eq7w`aN9qY>m0962NItg&={v?CmkJ3N-^>~TDruG^D0000o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)Acr7RmgnO6@eBwOMdQR1AR zo12%TuJhG^Hj*G{{V^+sMnA~#?@Rs&`yQHHlA|+Nxw!3D=yZ8Uj-S|9tdM;NxK5 zhmX`eA03sS_dlCCP{eau>ZQ`TeU004U0s%BO64=#XCB(5<9T_CMc>}@R>uo&`5cp& zX0n#Q{(tfgGd{Z~KXdduE5397`On0CrR!HIgG^)hq{jANUTxokelcy=wQb+cSq2Ig N22WQ%mvv4FO#qn{iah`T literal 0 HcmV?d00001 diff --git a/data/images/key-l2.png b/data/images/key-l2.png new file mode 100644 index 0000000000000000000000000000000000000000..678c95b5a2e25973adede287b79b1b6b82a9f492 GIT binary patch literal 473 zcmV;~0Ve*5P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3L`KcOi^0^0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0003n zNklbfD z)+rGF4pp&&P13!mx%uRspy#+L76^*9N>X(8~rZGfHuUVvxX z*8}k0*{9?YJKA>k>98gT%c=#CbhL|yI1XVL62~!VnzC3d2!fz7#JAOYs!Kle`CR9! zs!Ef|r1mvjk|fb! zF!;$#Xby0$gHIGiwLUim$g=F$0ROYV0k#(7Zw+t{JXr#Gv;HN+_NVj%u{QeO7s(>` P00000NkvXXu0mjfT2{S$ literal 0 HcmV?d00001 diff --git a/data/images/key-l3.png b/data/images/key-l3.png new file mode 100644 index 0000000000000000000000000000000000000000..c90825b284d88855797b744653dacf5a889794b3 GIT binary patch literal 475 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcrNU5t<+}k;NVdc^qQp5r zH#aq}gu%HeHL)Z$MWH;iBts!2BUQoO(>LIKifSIvIwMaP#}JR>Z>Md{YH<)~`@h$k z={l?TXMt@^O}=khBW~WlabwAb4d=5t%Jwz%PF=gy?m*!jg{EGX^^-gWm8W~3iLA(* z%)e|_;8R8iCB-F|=?%BLh#s7ix z$+n_`Wae{sL>yce`yP~Ner(b4SfVsmnS<%YF1zR>^DkGO4CzUY{35+-)x8?t?kx_1 zB2Oy&w&m`>duQ9*Qmul_O*;J_*S$Vxxp_^)s#P5wCqCOG^{mx@P-7=77^z(($3sjN$&Ol|CSUF(+XHAh<#e;mI4-FD`(ojKggSl*Ob zf2y57NoC<84UH}(Bc9~2)BpZA@it6K{#V5PfboE_I%EEwaH9`#UiJ)*Qx*9Y8jt+} P1~Y@FtDnm{r-UW|PCvWY literal 0 HcmV?d00001 diff --git a/data/images/key-l4.png b/data/images/key-l4.png new file mode 100644 index 0000000000000000000000000000000000000000..2587a7fd0bd674491d22d9e473b30752222f7611 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcrNaGyO>YNKNVdc^qQp5r zH#aq}gu%HeHL)Z$MWH;iBts!2BUQoO(>LIKifSHE;X_Xs#}JR>Pp8@PH8}{h?w@+3 zrR~P-0!}Wy1mDN37K~F{%{M%?iD~AJI5DrGsm+GFQ_;=!Poinv$&lTXD}R?6?JhXP zzU=_xh4#l6u6}wcdfz|bhtao&_l(jBXTRt)+$x!-Q|O#(Wj)96uwgTk%G}3-n{=e- z7H^OK&GXiGl1uLGzEhR68#KB?u5Vql^)ffd;ecyVroM|U`j(3dur!|9G(}};rq!=o zabf`?tty)5KQI54R>1eg`ldv2`CqZ?pZ2Bk^%On0IGK0nw`jgUN23EQ*Th#IkcqGq j+EDyY_3+o_UpST>+_>F&?e`p@XBj+Q{an^LB{Ts5UgMW8 literal 0 HcmV?d00001 diff --git a/data/images/key-l5.png b/data/images/key-l5.png new file mode 100644 index 0000000000000000000000000000000000000000..45519f3c3ef5c2d7d6f3183b8183b7fae035707d GIT binary patch literal 473 zcmV;~0Ve*5P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3M3U82@~4@0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0003n zNklc16x4eSlh1FlwR@j4uwxu0#glm175j5 zPr#?>B6&s)wdmqd#stL-HXx-u=p43fANj`YxoQHg>#9A1*=z75sOy@xZ3%+lu(sK3t~&##{CJWi`L(v+@1^hiV^X`k z0n;>P6h*SCD!JS3*Gsvo6a{Qau>qY*>! P00000NkvXXu0mjfS@*y~ literal 0 HcmV?d00001 diff --git a/data/images/key-left.png b/data/images/key-left.png new file mode 100644 index 0000000000000000000000000000000000000000..78adfe752101dbeb778dbbe41f056c99cb6bb7b1 GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcC1oKe9C8^bBwOMdQR1AR zo12jmA%_oYA&)}Z<@CL-T#-I^FC~6PM;+taozr5WyVc#@8LiW?#&!01Lf2nx(FNqKR-o?AvP4Jv~#QZCJlcFcQ uUy<+57r?MfNHgZj<~dUFCk^&axyRW3_eOnN;b%}lGkCiCxvXPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3MxBTTr-*g0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0005v zNkl5%zcK*o)$uZ_hdxgnk>xeB_ zIdJm~T@4WMKubM@8==nwktV<V>h#G1|ISm=}gCgIJ2u}A*%LtfD%}k?*uw}}F zgbz0RzX@}h5;drhcCO2>b?N60s9)-TlLK~87csf(gs)VW@QM5)COF+QwIfidbB9gV zt05R_1*ak7YDnY`Tlr>HaT$)Anv4~lJ;Bl`tY1|z2h7IPk1&(|&@Iy<2f-COX4M=x zxG#3Ni6cm!m+F(}B2VQ?@m0Hmpc{tUVDGlr;l6r~qh1(jf$bceb!`5@E-uU lG=LRqM=Le|l2PkV=^KlbAA!NkrL+J5002ovPDHLkV1nYX5NH4Z literal 0 HcmV?d00001 diff --git a/data/images/key-m1.png b/data/images/key-m1.png new file mode 100644 index 0000000000000000000000000000000000000000..c79f6bac001e88ca39cb5810c525626987cf77c9 GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)Ac#m>9^%%3+vA=whwh!W@g z+}zZ>5(ej@)Wnk16ovB4k_?5Aj8p}8Pv3y|DXMuu>vTO`978;gzn!+<>u`X`vHDCe z1@FtdOcn$PusdFtDgaW)oAe)Wdrark+hz5WXJ6ao=4C4u_w5q#aag{@v^m3DCeKUS zSKx_S`N!td4-52a?*DzapP5OmfThAA=EDaey~&oJ9668NKh9uKx`6#nu-z+%C92bO z7P0M{c4HxrsL0}3;T_*E?X2ZoFMq-0L8^M}Wxcv}uWmJ-bu0Vp&zBRSWcBs_^Rj5; zBF$)l-t;4Vs-JXju+3S2_~xo7#s%ggM}pb&4!^1Fk1p82a&UPH<6nkbVlO&woEQA? zhwr%mh8op}0_P6%K1gA;@?8>2!wM75` literal 0 HcmV?d00001 diff --git a/data/images/key-m2.png b/data/images/key-m2.png new file mode 100644 index 0000000000000000000000000000000000000000..77d87225a7d2f78797dc6b2af702ef5e8fced4d1 GIT binary patch literal 569 zcmV-90>=G`P)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3I`)=8u23l0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004y zNkl?=S=B%$dt~2L#MO1V{twACGiZ%gh(? zR2PIBM+z{@z#6bf_hAD{QZ*7c`XD1!wVd$rR< zJTPSkS1(*X!`2OK-*;f35q<@BvJkL+zh`3@);%>o(hmzxm`lR`gN7?8z14&Rir}p< zVS`*5%I`3rtO`i5O4kG|;>+cv8lE;sR9>I5VXVjqu{OJ=z(o7*iDcu!ej!nuO0S*R@X3QZ6E*2Ah`QP?NcB()7&dM z?u4Jh(KhifyMEsZ|NjLBAfuyxMg~kkNGAguxPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3I{QmqkwAw0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004; zNklpn4))-CF zA81lC0)v9c%|N9;R9f0lnhjS`D>HiLTl(Pg-E+^me24G7%e^3A02YB5kpJTmD`*)g z6OT4P_~VEI1}!iTcxgP0Kw49<#Ev?MYYKL#h9*>ou394Rsf=w0aQy;S39d#fbDEt3 z1G5eoZ-pKi{I{@?fc?8VIM62xb{VVJOt!&V7kFBrMS>|i?DfK+QDFn=Y)RN9i}))H znBeRQ9#Pfi}e8EM}-aqAD~wTErNr|+IzOT z1usQl^-^I2fd`QFFkyveorr6}q!+x<0YfH*4ICyRmj_?F;Lr~o=nn}tOR(Ij@E=I$ z;PeTG%@9tB_#@1E*ma6yYLUQuQp!E&>TnE;wpX+N{uM4V;4p|A#_tsCZ+TdcDRiJH z>}!X>H}+4TK_mksX0SPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3K1Exj-5sT0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004o zNkl#X()^+xls^BhC|fY($rcl(IC`+(C8n~+EiPE z1T~dQ)SycyISA<_W%yBng$?hG>s1L0@;dau;hcLOc+bQ0UJfq^Sb;qt162PwGRiC~ zHN(?w7+fD|z-k0Sz$)!WH&8MtmblReS%YGSZs{dIK%_U;ysR z;I*mTt`~L&Vfh9kG936Ia4k5=1VCs%wGM{fp`Hn21LY*T5hFJRy_iZEsepS@9lL-B0%5)7vxooo5CN0H6S$yH^`1YJLIpk`>Spyi@@ zkrQ*`K2Q8wzJ9CSmP9_DgnXq%0m(AFeTYbwwcK_BE}r1I0+F2f{tn?ZJiJ2a4hrv$ x{QoNM2mx6g=Vy(81qkXSU|;tq8G1iTKLG+j&NZ}Gp0fY|002ovPDHLkV1mP?-~j*t literal 0 HcmV?d00001 diff --git a/data/images/key-mr.png b/data/images/key-mr.png new file mode 100644 index 0000000000000000000000000000000000000000..77e432d60d54d4ebdf1ae7295bf104319bee5f81 GIT binary patch literal 445 zcmV;u0Yd(XP)Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3J4d?HW^s}0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0003L zNkll znaLssLL1n@Fl;-sGHxgSP;^(-H1$)Dp5_}R&M`yyW0}QRrCRXm3IaD}in72Nj;tQr zm=9t~cH}`di0R0NHJOnl(CnB|w2v0}QfC6@c)`^VBo=mQXvluFA1v^t&ZNGpOQ#ad zF^>Cj)Y~wK^L^Y$4?H#qJanvY=&K1Q*hLZ7Nyp$HxbB0%J+^R&&8RPf_m)z$HsO%) zNw|yautO7W5H4!^ZTd=XCxGxA_fH9b%68Mo6sVE>k`%(G@mB1;9%ijUg00000NkvXXu0mjf#R{|4 literal 0 HcmV?d00001 diff --git a/data/images/key-mute.png b/data/images/key-mute.png new file mode 100644 index 0000000000000000000000000000000000000000..5e139d06999d17d5f1d80e4b5c633ac4836c8e9c GIT binary patch literal 594 zcmV-Y0Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipo4 z3o|vH-hVj&0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00050 zNkl{8MWPYa zp}Ma7-P1r+Wj9Teolb`Um`o-)pU;`eWFBcpczC^DCrMVTRVs=?+qOxP?RNW-{o&xX zT8$)WSr%1Q^$YhWkj-XUEEc_NHk%$?(=@KvYrk-R0+mXIw-FK^P9Oq!aDnM`%3LnT z)oSIz1AK;*5Flo=8H5lRjYcRG3NTF*mSv$_F8e;qw}5Ti5JF%$9KtXR4?Y+S5Rb>7 zE8w~=N~O}ba79rNG#>7ks;XQp7GBSV5ImpH-o3c|37>KY5{U%H<8h>K{%{LqS@z)F zZWp~?53yJbr_%{h#E&JCWto%7ge2K&wLYz-(`oK@yHG9oRY21;9*@T_@JpsrDLRf5 zSRf8y3m|u|JdWcapU-2z-+#M)I2>RY2A0cZkm_$G0R7*dx(C0O&~+WI>-rVwM>>*8 gk`!np``4lL4GFKg1RfOct^fc407*qoM6N<$f-RoyTmS$7 literal 0 HcmV?d00001 diff --git a/data/images/key-next.png b/data/images/key-next.png new file mode 100644 index 0000000000000000000000000000000000000000..8e8d8540cd790c8a83077dd781886a5f2394b225 GIT binary patch literal 530 zcmV+t0`2{YP)Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipo4 z3oj>#5K}Aw0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004L zNkl1^ z!=@UT&1N(j4W`p+>S7&MX$;HdvJ^rn9U?kx(imob9OnQNc+JSbXXaNjvc8o50B5Px#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3KAE68{>ii0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00055 zNklt)-$B%7Pm1ZO(0TOWfcNp5!FYJ@?$_eeV0dIp=^33or*nfYdLK zh!V?!DtP!00%wmfU@-$Tz%YLgEAS*KDXGF3L?tC1hQWlv$otIMm=pY!;4Xsq6r4Z7 z<}GYQGuNg(f=fF;V88?f+o0c;IbU7?eP!UWf$!`WOE6w7f}cJ>_X+fbWDekUz=$Ka z=wywYm6Qio8*~`(tHc+t;V1!zahazPaB2$<+F{8p^I!}nE_4`h7YTlx5Pbh$@Wx_o z!L4R6NpP72?-{)5DZz7&uU_5-e|*(mQ>=7Vz68lPojsub>kg~n^{UK-9zGX#6@srq zr-1`is;6tA(<&QTc8mGlJ8i**E5T>$K>EUf=1yn^)|+57#|FK21A+70`@l{s4A|x8 zp;Oq7ebYet#gq$rOQEGiuIWhv4&ty9&TOu11HGlN*RGT^4qYJ~2L8=iU;?5B#*dnT le4yVT1M`Mo$uRm-`T_c?(ghAtQ`7(e002ovPDHLkV1fxW^x^;j literal 0 HcmV?d00001 diff --git a/data/images/key-play.png b/data/images/key-play.png new file mode 100644 index 0000000000000000000000000000000000000000..914184f721cf3d7fc7cbbb5a50083c9181c8cc13 GIT binary patch literal 495 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-9dst z@Y8vBJ&@uo@Q5sCVBi)8VMc~ob0mO*>?NMQuI#tD#JTnOm+U(=9VjGQ;u=xnoS&PU znpeW$T$GwvlA5AWo>`Ki5R#Fq;O^-g@IFN~4``j6r;B5V$MK`n*5@B`5NWXQ^A7g0 zcDz~;kz<>*YLViZM33eJVLoRR)qjd-eXvNet!GeKqQapju(m zznirC`_sEm?;T_peIOaZ{IWN>_C)AIjh|kQkKS)&(n@{6S>a+^ejxf~z@+M~hg(ZD zj*FZ4ChxzmZ75JVdERqFsa^w~$4ky$GWpxH=i`1>2CKPzg)*1lmI*qoSnJR>Ws(BN zjT|$R*}h+yB(G*oHQ<@)xBSZMQXp?zuCS}%;fDgT*ROJ?{MdIsZ~Nl=@8fIi&&BC3 zU2-=sdDb&mrcZNRH{ayBUK*)4J*z)q_uWSpe7$auwNCmAN${{mOgoxl^z(D#&R>E( z$CIn}%6U$@^F;PXXu`&b3t3xdoPTcqp54BxbZwY-tAP6VAIYDNRR&gA>oH4x2$*od h`(eCM*S{(InDy5D?N!^VcncWx44$rjF6*2UngH+b#>fBw literal 0 HcmV?d00001 diff --git a/data/images/key-prev.png b/data/images/key-prev.png new file mode 100644 index 0000000000000000000000000000000000000000..69bffa71b894c95c2e0713d381ffc7a5b2571a8b GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-9dst z@Y8vBJ&@uo@Q5sCVBi)8VMc~ob0mO*>?NMQuI#tD#JTk?i{$hCfkLt+t`Q~9`MJ5N zc_j?aMX8A;sVNHOnI#ztAsML(?w-B@?^9IsfYyb0x;Tb-96vfuG5?T*MB9A1B@5eh zcvn1(a^3K?LsfN$QM=-be~q0xPkHjxGqyT}J^IPmu}p2RqZ{OW0q_q_dLyY-klbK9yjH0zikgc{GhjORo1 z*qVK~rP>_VUk^6(mu-B#>&CXTReSgCJn^yO%o(ly%(Kt7`7L+ee_!2m(xHf@>m9s2 zCE848`!*?P^w`=LM^69s+QQT~SWm3m`1$X;JhSVb6&5ldZ2HS~`--@hZ$8EO=T@V@ xrmI;`)4c!7hBL@kXs&GRzqCGOO0DK!wk+}Nvq2gryMPhF;OXk;vd$@?2>{iH%tinJ literal 0 HcmV?d00001 diff --git a/data/images/key-right.png b/data/images/key-right.png new file mode 100644 index 0000000000000000000000000000000000000000..3c99ebcf597732887fe193932693cdf13bd6649f GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcCCwRjEp-J@NVdc^qQp5r zH#aq}gu%HeHL)Z$MWH;iBts!2BUQoO(>LIKifSHE;R{a}#}JR>N2e+BA2JYV-Cybu z;UU(f$Y|Xx%h&99=J8Ka4w(f%(swjE`t`8rG$nE@;t+FM_QK-Y^f@{Arf0wZ^+1(B zfc+SaQkWJK8KldGs+%X zv|ZTFV(?|*I|rLdjRyOeWJMdcZP*=lqjAwA!8J_X3=F3i*tATlI-9B}GEJ3jgI~hb z8S__r&iHS{d1_guW`na@`oDX+YNoFjmbUQK&dk4AAZSbwg|u_|h0 qGx#~}+d9|l2aZ*#94^zSWn$WY!-jjw`7=O2GkCiCxvXPx#24YJ`L;&po004~533nO*000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipc7 z3Lzmz$`dO90013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0006F zNkl>O%Qd>-vYs%s21xe~ zU;@{H8X#k~T0fV`K=RccNW9fn0aFLA0q59x>;b-NeM$B+8NAo}awuc38$N&Az6z#P z15Fpi_rb$sqhn7zhhkHWS6P9ByPz_oM*30;UYOxB%~7z}flD z@9}w9Z?Z6>V_6nHpO0?0JBAjVN~W$e-vIn1uI508pY%BkVqs(QYoF? z(_Tc*4A`5Z{KCr8uy18a>h-#0vsp0=@UEmC+SA|mZ}TXMOaB$LTe-0FAu zokIf!#0`=5nn?4L$d?Z?h~~?p*tRX9P-x^H;BTiOufF*n3|bUG~}BBfGEs@1AA z8V!j=B4V26$mvK23}9BZ^__vMtJP}ZcDwQW{e;6|Y}+Q4N>Qm)C=?2GI-OB;w?NMQuI#tD#JLUBUA0qs{ima*TMJ3i+wE~ zr__GiaopmhBj0w3=N`$w-zL4^U+s3}_}tzUuLUe1q!6p{SJwZ=!x<_CzUxyqdB0@N XPO6T)!@B1`&=Cxtu6{1-oD!M<`=Vir literal 0 HcmV?d00001 diff --git a/data/images/key-up.png b/data/images/key-up.png new file mode 100644 index 0000000000000000000000000000000000000000..02fd26fa59f8a5efbb0c111ccc390cb56b840b78 GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-CY>o z0$Dw0Im@MhG-rWFWHAE+w=f7ZGR&GI0Tg5}@$_|Nzs)AcB_(ybSkxRSBwOMdQR1AR zo12)@YQ{Nw{2O=@*nC#l=RR0?RQILgWQXqCiej=4|7>fx-M`91 z%Jl(Io$czii%U2z7c?9c`}SRmMMk>p@cU#fp{mG&j$<;rl^S(+ta5p$yQA;Gl?As? zY3^xsjtFA6Qq$4jurKwzZL5Lpq0g+_0x#Me0K=>U5grMu|5TlS1^;4X Xvzr-YBIKP2^dy6)tDnm{r-UW|q#cI3 literal 0 HcmV?d00001 diff --git a/data/images/key-vol-down.png b/data/images/key-vol-down.png new file mode 100644 index 0000000000000000000000000000000000000000..97d0c60200dcada3180ee9a79d621946269c36fa GIT binary patch literal 449 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!3HE3M$U5qQjEnx?oJHr&dIz4a@dl*-9dst z@Y8vBJ&@uo@Q5sCVBi)8VMc~ob0mO*>?NMQuI#tD#JSDc4O3t51q#WQxJHyX=jZ08 z=9Mrw7o{eaq^2m8XO?6rgk+>DxO@5ryiZZh16n8H>EamTas2MIja@AU0&Vq=v~tp? zhef;PWiG zj)}SE1&;!20kcB9%=Ytf={4DPwa$;{7yfPXm|xpsxJ_K?=;q|}n|#Dv7ar<++v+*V zLvtw?gXf08LYc)n-YYI;zMT-nb$i?NJD+WyRPs%YV&!OJ2n~HWRmmdaKl_4ADG$wX z=gV7?)e|I+-QWCR$*YA1Jc6zlU(KzYx1I6Du7BALnX|6=FUXDlv2Xv6@av{Nmuu#; z&M7W9H~r2gosw8~ua}1R3uN3a`kME>pUQM+y9kHkgI(*iI;Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipo4 z3pF*%gxJ3T0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0004V zNkl4{J-=JX_6bc2eU#(W@cDsR2 zQ>hdHuIqBST%rTi>ve|1Vd!0*PABiXSS+$wEPkD<@nmJQS>N%e0|ZGKz<4}HL=cfH z`*b?>?6FwvwI`5BB>r&<8jS|JuA}SvmAz7_c=lUY()Sw1qFo`+PoUyWK{6 z2PTsVwOY-~-)J-QL70(Y)*K0-AbW|YuPgfwc6hbmm72G|21Ky{o<^dIc@^o{M*-o#PfHzm?Og)*F)-id`ll{;C zd_Hgc|ICDb-I&L+=XV^KW$)4)T{6*M(cqumAKwRj$~6mpU*%r0l9?j5Ud?*z%sT14 z+b11x7HFGlFT<9uKUZ>@q4bYcN(S$8`?zg&9&<2%DU?6;GD@}+ZrTynrNU#pE*$R>Dq< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Gnome15 + Logitech on Linux + + Version ${version} + + + diff --git a/data/themes/Makefile.am b/data/themes/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/data/themes/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/data/themes/default/Makefile.am b/data/themes/default/Makefile.am new file mode 100644 index 0000000..3f1d30b --- /dev/null +++ b/data/themes/default/Makefile.am @@ -0,0 +1,22 @@ +themedir = $(datadir)/gnome15/themes/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + default-menu-child-entry.svg \ + default-menu-separator.svg \ + default-confirmation-screen.svg \ + default-error-screen.svg \ + mx5500-menu-screen.svg \ + mx5500-menu-entry.svg \ + mx5500-menu-child-entry.svg \ + mx5500-menu-separator.svg \ + mx5500-confirmation-screen.svg \ + mx5500-error-screen.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-menu-child-entry.svg \ + g19-menu-separator.svg \ + g19-error-screen.svg \ + g19-confirmation-screen.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/data/themes/default/default-confirmation-screen.svg b/data/themes/default/default-confirmation-screen.svg new file mode 100644 index 0000000..b644c9c --- /dev/null +++ b/data/themes/default/default-confirmation-screen.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + L3 - No + L4 - Yes + + diff --git a/data/themes/default/default-error-screen.svg b/data/themes/default/default-error-screen.svg new file mode 100644 index 0000000..beecf00 --- /dev/null +++ b/data/themes/default/default-error-screen.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + L5 - Close + + diff --git a/data/themes/default/default-menu-child-entry.svg b/data/themes/default/default-menu-child-entry.svg new file mode 100644 index 0000000..9a9d8a8 --- /dev/null +++ b/data/themes/default/default-menu-child-entry.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + + diff --git a/data/themes/default/default-menu-entry.svg b/data/themes/default/default-menu-entry.svg new file mode 100644 index 0000000..0731610 --- /dev/null +++ b/data/themes/default/default-menu-entry.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + + diff --git a/data/themes/default/default-menu-screen.svg b/data/themes/default/default-menu-screen.svg new file mode 100644 index 0000000..a6262ba --- /dev/null +++ b/data/themes/default/default-menu-screen.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + ${title} + + + + + + + ${alt_title} + + diff --git a/data/themes/default/default-menu-separator.svg b/data/themes/default/default-menu-separator.svg new file mode 100644 index 0000000..e533f80 --- /dev/null +++ b/data/themes/default/default-menu-separator.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/themes/default/g19-confirmation-screen.svg b/data/themes/default/g19-confirmation-screen.svg new file mode 100644 index 0000000..6913b30 --- /dev/null +++ b/data/themes/default/g19-confirmation-screen.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + Yes + No + + + + + + ${text} + + diff --git a/data/themes/default/g19-error-screen.svg b/data/themes/default/g19-error-screen.svg new file mode 100644 index 0000000..3364eb1 --- /dev/null +++ b/data/themes/default/g19-error-screen.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${text} + Close + + Ok + + diff --git a/data/themes/default/g19-menu-child-entry.svg b/data/themes/default/g19-menu-child-entry.svg new file mode 100644 index 0000000..6366146 --- /dev/null +++ b/data/themes/default/g19-menu-child-entry.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + ${item_alt} + + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/data/themes/default/g19-menu-entry.svg b/data/themes/default/g19-menu-entry.svg new file mode 100644 index 0000000..e52a959 --- /dev/null +++ b/data/themes/default/g19-menu-entry.svg @@ -0,0 +1,407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + + + ${item_name} + + + + + + + ${item_alt} + + diff --git a/data/themes/default/g19-menu-screen.svg b/data/themes/default/g19-menu-screen.svg new file mode 100644 index 0000000..8a1b8f7 --- /dev/null +++ b/data/themes/default/g19-menu-screen.svg @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + ${alt_title} + + + + + + + diff --git a/data/themes/default/g19-menu-separator.svg b/data/themes/default/g19-menu-separator.svg new file mode 100644 index 0000000..d820d13 --- /dev/null +++ b/data/themes/default/g19-menu-separator.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/themes/default/mx5500-confirmation-screen.svg b/data/themes/default/mx5500-confirmation-screen.svg new file mode 100644 index 0000000..5a41194 --- /dev/null +++ b/data/themes/default/mx5500-confirmation-screen.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + No + Yes + + + + diff --git a/data/themes/default/mx5500-error-screen.svg b/data/themes/default/mx5500-error-screen.svg new file mode 100644 index 0000000..390ae42 --- /dev/null +++ b/data/themes/default/mx5500-error-screen.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${text} + ${title} + + Hold + + Close + + diff --git a/data/themes/default/mx5500-menu-child-entry.svg b/data/themes/default/mx5500-menu-child-entry.svg new file mode 100644 index 0000000..9f76490 --- /dev/null +++ b/data/themes/default/mx5500-menu-child-entry.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + ${item_name} + ${item_alt} + + diff --git a/data/themes/default/mx5500-menu-entry.svg b/data/themes/default/mx5500-menu-entry.svg new file mode 100644 index 0000000..9b4a30b --- /dev/null +++ b/data/themes/default/mx5500-menu-entry.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + ${item_name} + ${item_alt} + + diff --git a/data/themes/default/mx5500-menu-screen.svg b/data/themes/default/mx5500-menu-screen.svg new file mode 100644 index 0000000..82429a5 --- /dev/null +++ b/data/themes/default/mx5500-menu-screen.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + ${alt_title} + + diff --git a/data/themes/default/mx5500-menu-separator.svg b/data/themes/default/mx5500-menu-separator.svg new file mode 100644 index 0000000..50784d9 --- /dev/null +++ b/data/themes/default/mx5500-menu-separator.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/udev/98-gnome15.rules.in b/data/udev/98-gnome15.rules.in new file mode 100644 index 0000000..01fa9b9 --- /dev/null +++ b/data/udev/98-gnome15.rules.in @@ -0,0 +1,8 @@ +# This file maintains permissions for Gnome15 input devices +# + +# Make uinput load +SUBSYSTEM=="input", RUN+="/sbin/modprobe uinput" + +# Set permissions for uinput devices +KERNEL=="event*|uinput", GROUP="@DEVICEGROUP@", MODE="@DEVICEMODE@" \ No newline at end of file diff --git a/data/udev/99-gnome15-g15direct.rules.in b/data/udev/99-gnome15-g15direct.rules.in new file mode 100644 index 0000000..65a58d7 --- /dev/null +++ b/data/udev/99-gnome15-g15direct.rules.in @@ -0,0 +1,30 @@ +# This file maintains permissions for Gnome15's "G15 Direct" driver. These are the +# defaults, you might want to tighten them up a bit. +# +# See udev(7) for syntax. +# + +# Logitech G15 Gaming Keyboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G15 Gaming Keyboard (version 2) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G13 Advanced Gameboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech Z10 Speakers +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G11 Keyboard (no LCD) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c225", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G110 Keyboard (no LCD) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22b", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech Game Panel (Dell XPS 1730?) +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" + +# Logitech G510 +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" diff --git a/data/udev/99-gnome15-g19direct.rules.in b/data/udev/99-gnome15-g19direct.rules.in new file mode 100644 index 0000000..d1ebeb4 --- /dev/null +++ b/data/udev/99-gnome15-g19direct.rules.in @@ -0,0 +1,8 @@ +# This file maintains permissions for Gnome15's "G19 Direct" driver. These are the +# defaults, you might want to tighten them up a bit. +# +# See udev(7) for syntax. +# + +# Logitech G19 Gaming Keyboard +SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@" diff --git a/data/udev/99-gnome15-g930.rules.in b/data/udev/99-gnome15-g930.rules.in new file mode 100644 index 0000000..6e319df --- /dev/null +++ b/data/udev/99-gnome15-g930.rules.in @@ -0,0 +1,6 @@ +# This file maintains permissions for Logitech G keyboard devices +# See udev(7) for syntax. +# + +# Logitech G930 Headset +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0xa1f", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g930-extra-keys" diff --git a/data/udev/99-gnome15-kernel.rules.in b/data/udev/99-gnome15-kernel.rules.in new file mode 100644 index 0000000..3a70cd7 --- /dev/null +++ b/data/udev/99-gnome15-kernel.rules.in @@ -0,0 +1,39 @@ +# This file maintains permissions for Logitech G keyboard devices +# See udev(7) for syntax. +# + +# Logitech G19 Gaming Keyboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g19-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c229", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g19-lcd" + +# Logitech G15 Gaming Keyboard (version 1) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v1-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c222", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v1-lcd" + +# Logitech G15 Gaming Keyboard (version 2) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v2-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c227", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g15v2-lcd" + +# Logitech G13 Advanced Gameboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g13-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c21c", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g13-lcd" + +# Logitech G510 Keyboard +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22d", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-lcd" +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22e", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g510-lcd" + +# Logitech Z10 Speakers +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="z10-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a07", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="z10-lcd" + +# Logitech G11 Keyboard (no LCD) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c225", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g11-extra-keys" + +# Logitech G110 Keyboard (no LCD) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c22b", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="g110-extra-keys" + +# Logitech Game Panel (Dell XPS 1730?) +SUBSYSTEM=="input", ACTION=="add", KERNEL=="event*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="gamepanel-extra-keys" +SUBSYSTEMS=="usb", ACTION=="add", KERNEL=="fb[0-9]*", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c251", MODE="@DEVICEMODE@", GROUP="@DEVICEGROUP@", SYMLINK+="gamepanel-lcd" diff --git a/data/udev/Makefile.am b/data/udev/Makefile.am new file mode 100644 index 0000000..1aed22f --- /dev/null +++ b/data/udev/Makefile.am @@ -0,0 +1,20 @@ +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = 99-gnome15-kernel.rules +endif + +if ENABLE_DRIVER_G19DIRECT + MAYBE_G19DIRECT = 99-gnome15-g19direct.rules +endif + +if ENABLE_DRIVER_G15DIRECT + MAYBE_G15DIRECT = 99-gnome15-g15direct.rules +endif + +if ENABLE_DRIVER_G930 + MAYBE_G930 = 99-gnome15-g930.rules +endif + +udevdir = @UDEV_RULES_PATH@ +udev_DATA = 98-gnome15.rules $(MAYBE_KERNEL) $(MAYBE_G19DIRECT) $(MAYBE_G15DIRECT) $(MAYBE_G930) + +EXTRA_DIST = 98-gnome15.rules 99-gnome15-g19direct.rules 99-gnome15-kernel.rules 99-gnome15-g15direct.rules 99-gnome15-g930.rules \ No newline at end of file diff --git a/data/ui/Makefile.am b/data/ui/Makefile.am new file mode 100644 index 0000000..0dbfc30 --- /dev/null +++ b/data/ui/Makefile.am @@ -0,0 +1,16 @@ +uidir = $(datadir)/gnome15/ui +ui_DATA = driver_gtk.ui \ + driver_kernel.ui \ + driver_g19direct.ui \ + driver_g15direct.ui \ + driver_g930.ui \ + g15-config.ui \ + password.ui \ + macro-editor.ui \ + script-editor.ui \ + colorpicker.ui \ + accounts.ui \ + redblue.png + +EXTRA_DIST = \ + $(ui_DATA) diff --git a/data/ui/accounts.ui b/data/ui/accounts.ui new file mode 100644 index 0000000..391913f --- /dev/null +++ b/data/ui/accounts.ui @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + False + 5 + Accounts + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + 150 + True + True + AccountModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Accounts</b> + True + + + + + True + True + 0 + + + + + False + True + + + + + 300 + True + False + 4 + + + True + False + 4 + + + True + False + Type: + + + False + False + 0 + + + + + True + False + TypeModel + + + + 0 + + + + + True + True + 1 + + + + + False + True + 0 + + + + + 200 + True + False + 0 + none + + + True + False + 12 + + + True + False + + + + + + + + + + True + False + <b>Account Options</b> + True + + + + + True + True + 1 + + + + + True + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + + + True + False + 0 + Check every + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + True + True + 0 + + + + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + pop3 + POP3 + + + imap + IMAP + + + + + 1 + 9999 + 1 + 1 + 1 + + diff --git a/data/ui/colorpicker.ui b/data/ui/colorpicker.ui new file mode 100644 index 0000000..67eb8d2 --- /dev/null +++ b/data/ui/colorpicker.ui @@ -0,0 +1,232 @@ + + + + + + 255 + 1 + 10 + + + 255 + 1 + 10 + + + False + 5 + Pick Colour + True + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 8 + + + True + False + + + True + False + redblue.png + + + + + + + True + True + 0 + + + + + True + False + 2 + 2 + 34 + 8 + + + True + False + 0 + Red: + + + + + + + + True + False + 0 + Blue: + + + 1 + 2 + + + + + + True + True + + False + False + True + True + RAdjustment + + + 1 + 2 + + + + + + True + True + + False + False + True + True + BAdjustment + + + 1 + 2 + 1 + 2 + + + + + + False + False + 1 + + + + + False + False + 1 + + + + + + + + + button1 + + + + False + 5 + Pick Colour + True + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + #000000000000 + + + True + True + 1 + + + + + + + + + button2 + + + diff --git a/data/ui/driver_g15.ui b/data/ui/driver_g15.ui new file mode 100644 index 0000000..1a64c20 --- /dev/null +++ b/data/ui/driver_g15.ui @@ -0,0 +1,61 @@ + + + + + + 65535 + 1 + 10 + + + + True + False + + + True + False + + + 128 + True + False + 0 + Port + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + PortAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + False + False + 4 + 0 + + + + diff --git a/data/ui/driver_g15direct.ui b/data/ui/driver_g15direct.ui new file mode 100644 index 0000000..7977df5 --- /dev/null +++ b/data/ui/driver_g15direct.ui @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + macro + Emit macro keys + + + joystick + Analogue Joystick + + + mouse + Mouse + + + digital-joystick + Digital Joystick + + + + + 100 + 1 + 10 + + + 1000000 + 1 + 10 + + + 1000000 + 1 + 10 + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 3 + 3 + 4 + 4 + + + + + + True + True + + True + False + False + True + True + TimeoutAdjustment + 0.029999999999999999 + + + 1 + 2 + + + + + 48 + True + False + 0 + 8 + ms + + + 2 + 3 + + + + + 128 + True + False + 0 + Timeout + + + + + True + False + 0 + Joystick mode + + + 1 + 2 + + + + + True + False + JoyModeModel + + + + 1 + + + + + 1 + 2 + 1 + 2 + + + + + True + False + 0 + Center Offset + + + 2 + 3 + + + + + True + True + + True + False + False + True + True + OffsetAdjustment + + + 1 + 2 + 2 + 3 + + + + + True + False + 0 + 0 + 8 + 8 + How far off-center the joystick must +be before registering movement. +Too low, and the joystick will "stick" +in one direction. Too high and you +lose accuracy. + + + 2 + 3 + 2 + 3 + + + + + False + True + 1 + + + + + + button1 + + + diff --git a/data/ui/driver_g19.ui b/data/ui/driver_g19.ui new file mode 100644 index 0000000..1a64c20 --- /dev/null +++ b/data/ui/driver_g19.ui @@ -0,0 +1,61 @@ + + + + + + 65535 + 1 + 10 + + + + True + False + + + True + False + + + 128 + True + False + 0 + Port + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + PortAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + False + False + 4 + 0 + + + + diff --git a/data/ui/driver_g19direct.ui b/data/ui/driver_g19direct.ui new file mode 100644 index 0000000..cd2851d --- /dev/null +++ b/data/ui/driver_g19direct.ui @@ -0,0 +1,205 @@ + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + 128 + True + False + 0 + Timeout + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + TimeoutAdjustment + 0.029999999999999999 + + + True + True + 4 + 1 + + + + + 48 + True + False + 0.10000000149011612 + ms + + + False + False + 2 + + + + + False + False + 4 + 0 + + + + + True + False + + + 128 + True + False + then wait + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + ResetWaitAdjustment + + + True + True + 1 + + + + + 48 + True + False + 0.10000000149011612 + ms + + + False + False + 2 + + + + + False + False + 1 + + + + + Reset device before use + True + True + False + True + + + False + False + 2 + + + + + True + True + 1 + + + + + + + + + button1 + + + + 1000000 + 1 + 10 + + + 1000000 + 1 + 10 + + diff --git a/data/ui/driver_g930.ui b/data/ui/driver_g930.ui new file mode 100644 index 0000000..638064c --- /dev/null +++ b/data/ui/driver_g930.ui @@ -0,0 +1,80 @@ + + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + Grab multimedia keys + True + True + False + True + + + False + True + 0 + + + + + True + True + 1 + + + + + + + + + button1 + + + diff --git a/data/ui/driver_gtk.ui b/data/ui/driver_gtk.ui new file mode 100644 index 0000000..c5099ba --- /dev/null +++ b/data/ui/driver_gtk.ui @@ -0,0 +1,118 @@ + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + 128 + True + False + 0 + Mode: + + + False + False + 0 + + + + + True + False + ModeModel + + + + 0 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + + + + button1 + + + + + + + + + + g19 + + + g15v1 + + + g15v2 + + + g13 + + + + + diff --git a/data/ui/driver_kernel.ui b/data/ui/driver_kernel.ui new file mode 100644 index 0000000..3745f6c --- /dev/null +++ b/data/ui/driver_kernel.ui @@ -0,0 +1,206 @@ + + + + + + + + + + + + g19 + + + g15v1 + + + g15v2 + + + g13 + + + auto + + + + + + + + + + + + + macro + Emit Macro Keys + + + joystick + Analogue Joystick + + + mouse + Mouse + + + digital-joystick + Digital Joystick + + + + + + False + 5 + Driver Settings + dialog + + + True + False + 2 + + + True + False + end + + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 3 + 3 + 4 + 4 + + + 128 + True + False + 0 + Device: + + + + + True + False + DeviceModel + + + + 0 + + + + + 1 + 3 + + + + + Calibrate + True + True + True + + + 2 + 3 + 1 + 2 + + + + + + Grab multimedia keys + True + True + False + True + + + 3 + 2 + 3 + + + + + 128 + True + False + 0 + Joystick mode: + + + 1 + 2 + + + + + True + False + JoyModeModel + + + + 1 + + + + + 1 + 2 + 1 + 2 + + + + + False + True + 1 + + + + + + + + + button1 + + + diff --git a/data/ui/g15-config.ui b/data/ui/g15-config.ui new file mode 100644 index 0000000..b6c7af4 --- /dev/null +++ b/data/ui/g15-config.ui @@ -0,0 +1,3393 @@ + + + + + + False + 5 + About Plugin + center + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + False + end + 0 + + + + + True + False + 4 + + + True + False + + + True + False + 0 + Author: + + + + + + False + False + 0 + + + + + True + False + 0 + A. Author <a_user@company.com> + + + False + False + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 0 + Copyright: + + + + + + False + False + 0 + + + + + True + False + 0 + Copyright © 2006 A. Author + + + False + False + 1 + + + + + True + True + 1 + + + + + True + False + + + True + False + 0 + Site: + + + + + + False + False + 0 + + + + + True + True + True + none + 0 + http://glade.gnome.org + + + False + False + 1 + + + + + True + True + 2 + + + + + False + True + 1 + + + + + + CancelMacroButton + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + question + Remove Macro + Are you sure you wish to remove this macro? + True + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + button10 + button11 + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + question + Remove Profile + Are you sure you wish to remove this profile? + True + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + button6 + button7 + + + + False + 5 + Copy Existing Profile + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-ok + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + Copied Profile Name: + + + False + False + 0 + + + + + True + True + + True + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 1 + + + + + + button21 + button41 + + + + 100 + 1 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 540 + 440 + False + 5 + Global Options + center + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + False + end + 0 + + + + + True + False + 4 + + + Only Show Indicator On Error + True + True + False + 0.54000002145767212 + True + + + False + False + 0 + + + + + Start Desktop Service On Login + True + True + False + True + + + False + False + 1 + + + + + Start Indicator On Login + True + True + False + True + + + False + False + 2 + + + + + Start System Tray Icon On Login + True + True + False + True + + + False + False + 3 + + + + + Enable GNOME Shell extension (must restart GNOME Shell) + True + True + False + True + + + False + False + 4 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + 220 + True + True + + + True + True + GlobalPluginModel + + + Enabled + + + + 1 + 0 + + + + + + + Name + + + + 1 + 2 + + + + + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + 0 + Plugin Name + + + + + + False + True + 4 + 0 + + + + + True + False + + + True + False + 0 + Description: + + + + + + False + False + 0 + + + + + True + True + + + True + False + + + True + False + 8 + 8 + 8 + 8 + + + True + False + 0 + 0 + Description of plugin. <b>bold</b> + True + True + word-char + 60 + + + + + + + + + True + True + 4 + 1 + + + + + True + True + 1 + + + + + True + False + 8 + + + gtk-preferences + True + True + True + True + + + False + False + 0 + + + + + gtk-about + True + True + True + True + + + False + False + 1 + + + + + False + False + 2 + + + + + True + True + 1 + + + + + + + + + True + False + 4 + <b>Global Plugins</b> + True + + + + + True + True + 5 + + + + + True + True + 1 + + + + + + CancelMacroButton1 + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + error + Error importing profile + Error message to display + True + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + + ImportProfileErrorCloseButton + + + + + + + + + + + + + + + + + + + + False + 5 + Logitech G Keyboard Configuration + center + normal + + + True + False + 2 + + + True + False + end + + + Stop Service + True + True + True + + + False + False + 0 + True + + + + + gtk-close + True + True + True + True + True + True + + + False + False + 1 + + + + + Configure + True + True + True + + + False + False + 2 + True + + + + + False + True + end + 0 + + + + + True + True + + + True + True + never + automatic + in + + + True + True + DeviceModel + 1 + 0 + + + + 0 + + + + + + 4 + 1 + 3 + 2 + + + + + + + False + True + + + + + True + False + 4 + + + True + False + + + True + False + + + True + False + + + 0 + 0 + True + False + 0 + Logitech Keyboard Device + + + + + + True + True + 0 + + + + + Enabled + True + True + False + True + + + False + False + 1 + + + + + False + False + 8 + 0 + + + + + True + True + + + True + False + 4 + + + True + False + 18 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + + + + + + + + True + False + <b>Controls</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 0 + 24 + + + True + False + + + + + + + + + + True + False + <b>Switches</b> + True + + + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Cycle screens + True + True + False + True + + + False + False + 0 + + + + + True + False + 32 + + + True + False + + + True + False + 0 + Every + + + False + False + 4 + 0 + + + + + True + True + + True + False + False + True + True + CycleAdjustment + + + False + False + 1 + + + + + True + False + seconds + + + False + False + 4 + 2 + + + + + + + False + False + 4 + 1 + + + + + + + + + True + False + <b>Options</b> + True + + + + + False + False + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + 240 + True + False + DriverModel + + + + 1 + + + + + False + True + 0 + + + + + Options + True + True + True + + + False + True + 4 + 1 + + + + + + + + + True + False + <b>Driver</b> + True + + + + + False + True + 2 + + + + + + + True + False + Keyboard + + + False + + + + + True + False + + + True + True + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + 4 + + + gtk-new + True + True + True + True + + + False + False + 0 + + + + + Import + True + True + True + + + True + True + 4 + 1 + + + + + False + True + 4 + 0 + + + + + True + True + automatic + automatic + in + + + 300 + True + True + ProfileModel + False + False + 0 + + + column + + + + 5 + + + + + + + Name + True + + + + 4 + 3 + 0 + 1 + + + + + + + Id + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Profiles</b> + True + + + + + False + True + + + + + True + True + 3 + + + True + False + 2 + 4 + + + True + False + + + True + False + toolbutton2 + True + gtk-new + + + False + True + + + + + True + False + toolbutton1 + True + gtk-delete + + + False + True + + + + + True + False + toolbutton2 + True + gtk-properties + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + + + True + True + MacroModel + False + 1 + + + On + + + + 5 + + + + + + + 0 + Key + True + True + True + 2 + + + + 0 + + + + + + + Name + + + + 3 + 1 + + + + + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + 4 + + + M1 + True + True + False + True + True + + + False + False + 0 + + + + + M2 + True + True + False + True + M1 + + + False + False + 1 + + + + + M3 + True + True + False + True + M1 + + + False + False + 2 + + + + + True + True + 0 + + + + + + + + + + + + True + False + <b>Memory Bank</b> + True + + + + + False + False + 2 + + + + + + + True + False + Macros + + + False + + + + + True + False + 4 + 4 + 4 + + + True + False + + + Activate this profile when no others are +active + True + True + False + 0 + True + + + False + False + 4 + 0 + + + + + Activate this profile when a window with +the following title has focus + True + True + False + 0 + True + + + False + False + 4 + 1 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Window + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + 24 + True + True + True + Select a window from those active + WindowSelectImage + + + True + True + 2 + + + + + False + True + 4 + 2 + + + + + Activate when a command launched through +g15-launch matches the following pattern + True + True + False + True + + + False + False + 3 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Pattern + + + False + False + 0 + + + + + True + False + True + + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 4 + 4 + + + + + True + False + 0 + 4 + 4 + Use macros from another profile when they are not set in this one + True + True + + + False + False + 5 + + + + + 120 + True + False + 1 + + + 120 + True + False + 0 + 30 + Profile + + + False + False + 0 + + + + + True + False + ParentProfileModel + + + + 1 + + + + + True + True + 1 + + + + + False + False + 8 + 6 + + + + + + + + + + 1 + + + + + True + False + Activation + + + 1 + False + + + + + True + False + 4 + 4 + 4 + 4 + + + True + False + 4 + + + Send delays with keystrokes + True + True + False + 0 + True + + + False + False + 0 + + + + + Use fixed delay + True + True + False + 0 + True + + + False + True + 4 + 1 + + + + + True + False + + + 150 + True + False + 0 + Press for + + + True + False + 0 + + + + + True + True + + True + False + False + True + True + PressDelayAdjustment + 2 + + + True + True + 1 + + + + + False + False + 4 + 2 + + + + + True + False + + + 150 + True + False + 0 + Release, then wait + + + True + False + 0 + + + + + True + True + + True + False + False + True + True + ReleaseDelayAdjustment + 2 + + + True + True + 1 + + + + + False + False + 4 + 3 + + + + + + + 2 + + + + + True + False + Delays + + + 2 + False + + + + + True + False + 4 + 4 + 4 + 4 + + + True + False + + + True + False + 2 + 2 + + + + + + + + + True + False + 0 + Author + + + + + True + True + + True + False + False + True + True + + + 1 + 2 + + + + + False + False + 4 + 0 + + + + + 120 + True + False + 0 + none + + + 120 + True + False + 12 + + + True + False + 4 + + + True + False + 2 + 3 + 8 + 4 + + + True + True + True + + + 48 + 48 + True + False + 1 + gtk-missing-image + + + + + 1 + 2 + + + + + + + True + True + True + + + 48 + 48 + True + False + 1 + gtk-missing-image + + + + + 1 + 2 + 1 + 2 + + + + + + + Clear + True + True + True + 0 + + + 2 + 3 + + + + + + + Clear + True + True + True + 0 + + + 2 + 3 + 1 + 2 + + + + + + + True + False + 0 + Icon + + + GTK_FILL + + + + + + True + False + 0 + Background + + + 1 + 2 + GTK_FILL + + + + + + False + True + 0 + + + + + + + + + True + False + 24 + <b>Images</b> + True + center + + + + + True + True + 1 + + + + + True + False + 4 + + + True + False + 8 + + + Get more profiles or upload yours + True + True + True + none + http://www.russo79.com/forum/gnome-15/macro-profiles + + + True + True + 0 + + + + + True + True + 0 + + + + + Export + True + True + True + + + False + False + 1 + + + + + False + False + 4 + 2 + + + + + + + 3 + + + + + True + False + Information + + + 3 + False + + + + + True + False + 8 + + + True + False + + + True + False + ProfilePluginsModeModel + + + + 1 + + + + + False + False + 4 + 0 + + + + + True + True + automatic + automatic + in + + + True + True + EnabledProfilePluginsModel + False + False + 0 + + + Enabled + + + + 0 + + + + + + + Plugin + + + + 1 + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + 4 + + + + + True + False + Plugins + + + 4 + False + + + + + + + + + + + True + True + + + + + True + True + 4 + 0 + + + + + 1 + + + + + True + False + Macros + + + 1 + False + + + + + True + False + + + 200 + True + True + 1 + in + + + True + True + PluginModel + False + False + 0 + + + Enabled + + + + 0 + + + + + + + Plugin + + + + 1 + + + + + + + False + Id + + + + 2 + + + + + + + + + False + False + 0 + + + + + True + False + 8 + 8 + 8 + + + True + False + + + True + False + 0 + 0 + Plugin Name + + + + + + False + False + 10 + 0 + + + + + True + False + 4 + + + False + True + + + True + False + 0 + Supported Models: + + + + + + False + False + 0 + + + + + True + False + 0 + 0 + Models supported. + True + True + word-char + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + Description: + + + + + + False + False + 0 + + + + + True + True + + + True + False + + + True + False + 8 + 8 + 8 + 8 + + + True + False + 0 + 0 + Description of plugin. <b>bold</b> + True + True + word-char + 60 + + + + + + + + + True + True + 4 + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 3 + 4 + + + True + False + gtk-save-as + + + 1 + 2 + + + + + + True + False + 0 + label + + + 2 + 3 + + + + + True + False + label + + + + + + + + + + + + True + False + <b>Keys</b> + True + + + + + False + False + 2 + + + + + True + False + 8 + + + gtk-preferences + True + True + False + True + + + False + False + 0 + + + + + gtk-about + True + True + True + True + + + False + False + 1 + + + + + True + False + 1 + Theme: + + + True + True + 2 + + + + + True + False + ThemeModel + + + + 1 + + + + + True + True + 3 + + + + + False + False + 3 + + + + + True + True + 1 + + + + + + + True + True + 1 + + + + + 2 + + + + + True + False + Plugins + + + 2 + False + + + + + True + True + 1 + + + + + False + True + There is no appropriate driver for the device <b>DeviceName</b>. +Do you have all the required packages installed? + + True + center + + + True + True + 2 + + + + + True + True + 0 + + + + + False + True + No device selected. + True + center + + + True + True + end + 1 + + + + + True + True + 0 + + + + + True + True + + + + + True + True + 1 + + + + + + StopServiceButton + button1 + GlobalOptionsButton + + + + False + 5 + New Profile + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-ok + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + Profile Name: + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 1 + + + + + + button2 + button4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100 + 0.050000000000000003 + 1 + + + True + False + + + gtk-remove + True + False + True + True + + + + + Export + True + False + Export this profile as an .mzip archive. + image2 + False + + + + + Duplicate + True + False + image3 + False + + + + + True + False + + + + + True + False + Make this profile the currently active one + Activate + True + + + + + True + False + Lock + True + + + + + True + False + Unlock + True + + + + + False + 5 + True + center-on-parent + normal + True + MainWindow + error + Profile already exists + The profile name you have supplied already exists, please +choose another name. + True + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + + button5 + + + + + + + + + + + + all + Enable All Available Plugins + + + none + Disable All Available Plugins + + + selected + Plugins Selected Below + + + + + 100 + 0.050000000000000003 + 1 + + + False + 5 + Select Window + True + center-on-parent + 320 + 98 + normal + MainWindow + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + right + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + 120 + True + False + Window + + + False + False + 0 + + + + + True + False + WindowModel + + + + 0 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button8 + + + + + + + + + + + + True + False + gtk-zoom-in + + + True + False + gtk-zoom-in + + + True + False + gtk-save-as + + + True + False + gtk-copy + + diff --git a/data/ui/macro-editor.ui b/data/ui/macro-editor.ui new file mode 100644 index 0000000..58886bf --- /dev/null +++ b/data/ui/macro-editor.ui @@ -0,0 +1,901 @@ + + + + + + + + + + + + + + + + + + + + + + 0 + When Released + + + 1 + When Pressed + + + 2 + When Held + + + + + True + False + gtk-clear + + + + + + + + + + + mouse + Mouse Button + + + keyboard + Keyboard Key + + + joystick + Joystick Button + + + digital-joystick + Digital Joystick + + + command + Run Command + + + simple + Simple Macro + + + script + Macro Script + + + action + Action + + + + + + + + + + + + + 580 + False + 5 + Edit Macro + center-on-parent + normal + + + True + False + 2 + + + True + False + + + + + + False + True + 4 + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + + + + + + True + False + <b>Keys Pressed</b> + True + + + + + False + False + 0 + + + + + Allow combination of keys + True + True + False + True + + + + False + False + 4 + 1 + + + + + True + True + 0 + + + + + True + False + 4 + + + True + False + 4 + 2 + 16 + 8 + + + True + False + 0 + <b>Memory Bank:</b> + True + + + GTK_FILL + + + + + True + False + 0 + <b>Name:</b> + True + + + 1 + 2 + GTK_FILL + + + + + True + True + True + + 20 + False + False + True + True + + + + 1 + 2 + 1 + 2 + + + + + True + False + 0 + label + + + 1 + 2 + GTK_FILL + + + + + True + False + MapTypeModel + + + + + 1 + + + + + 1 + 2 + 2 + 3 + + + + + True + False + 0 + <b>Target:</b> + True + + + 2 + 3 + GTK_FILL + + + + + True + False + 0 + <b>Activation:</b> + True + + + 3 + 4 + + + + + True + False + ActivateOnModel + + + + + 1 + + + + + 1 + 2 + 3 + 4 + + + + + False + False + 0 + + + + + True + False + + + False + True + 1 + + + + + True + False + + + True + False + + + True + False + Filter: + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + + True + True + 1 + + + + + True + True + True + ClearImage + + + + False + False + 2 + + + + + False + False + 4 + 0 + + + + + True + True + automatic + automatic + in + + + True + True + MappedKeyModel + False + False + 0 + + + + column + + + + 0 + + + + + + + + + True + True + 1 + + + + + True + True + 2 + + + + + True + False + + + True + False + 8 + + + True + True + + True + False + False + True + True + + + + True + True + 0 + + + + + _Browse + True + True + True + True + + + + False + False + 1 + + + + + False + False + 0 + + + + + Run command in background + True + True + False + True + + + + False + True + 4 + 1 + + + + + True + True + 4 + 3 + + + + + True + False + + + True + True + + True + False + False + True + True + + + + False + False + 0 + + + + + True + False + 0 + \r for Return, \e for escape, \b for backspace, \t for tab +\p for pause and \\ for backslash + True + word-char + + + + + + False + False + 1 + + + + + False + False + 4 + 4 + + + + + True + False + + + True + True + etched-in + + + 128 + True + True + + + + + True + True + 0 + + + + + True + False + + + + + + Editor + True + True + True + + + + False + False + 1 + + + + + False + False + 4 + 1 + + + + + True + True + 5 + + + + + True + False + + + True + True + automatic + automatic + in + + + True + True + ActionModel + False + False + 0 + + + + column + + + + 1 + + + + + + + + + True + True + 0 + + + + + True + True + 4 + 6 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + 4 + + + True + False + 0 + Mode: + + + False + False + 0 + + + + + True + False + RepeatModel + + + + + 1 + + + + + True + True + 1 + + + + + False + False + 0 + + + + + Override default repeat delay + True + True + False + True + + + + True + True + 1 + + + + + True + False + 4 + + + True + False + Delay between repeats + + + True + True + 0 + + + + + True + True + + False + False + True + True + TurboAdjustment + 2 + + + + True + True + 1 + + + + + False + False + 2 + + + + + + + + + True + False + <b>Repetition</b> + True + + + + + False + False + 4 + 7 + + + + + False + True + 1 + + + + + True + True + 1 + + + + + + MacroEditCloseButton + + + + + + + + + + + + none + None + + + toggle + Toggle + + + held + When held + + + + + 0.050000000000000003 + 30 + 0.02 + 1 + + + + + + + + + + + BTN_TEST + 100 + + + KEY_TEST + 101 + + + + diff --git a/data/ui/password.ui b/data/ui/password.ui new file mode 100644 index 0000000..a333a01 --- /dev/null +++ b/data/ui/password.ui @@ -0,0 +1,161 @@ + + + + + + False + 5 + Password Required + True + center + dialog-password + normal + True + + + True + False + 2 + + + True + False + end + + + Ok + True + True + True + True + True + True + True + + + False + False + 0 + + + + + Cancel + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 64 + dialog-password + 6 + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Password Text + True + + + False + False + 0 + + + + + True + False + + + True + False + Password + + + True + False + 0 + + + + + True + True + False + + True + False + False + True + True + + + True + False + 1 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + True + True + 1 + + + + + + togglebutton1 + button1 + + + diff --git a/data/ui/redblue.png b/data/ui/redblue.png new file mode 100644 index 0000000000000000000000000000000000000000..bd9be8f4a920df8485adaccf909366229991a14f GIT binary patch literal 15433 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 literal 0 HcmV?d00001 diff --git a/data/ui/script-editor.ui b/data/ui/script-editor.ui new file mode 100644 index 0000000..7576935 --- /dev/null +++ b/data/ui/script-editor.ui @@ -0,0 +1,1508 @@ + + + + + + False + 5 + dialog + True + question + Add Delay + Provide the number of milliseconds to wait +for on this delay. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Delay + + + True + True + 0 + + + + + True + True + + False + False + True + True + DelayAdjustment + + + True + True + 1 + + + + + True + False + ms + + + False + False + 4 + 2 + + + + + True + True + 2 + + + + + + button6 + button7 + + + + False + 5 + dialog + True + question + Execute Command + Enter the name or path of a command to +execute. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Command + + + True + True + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + Browse + True + True + True + + + + True + True + 2 + + + + + True + True + 2 + + + + + + button8 + button9 + + + + False + 5 + dialog + True + question + Add Goto + Choose the label to jump to. The label must +already exist. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Goto + + + True + True + 0 + + + + + True + False + GotoLabelModel + + + + 0 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + + button10 + button13 + + + + False + 5 + dialog + True + question + Add Label + Enter the name of the label in this script. +You may then use Goto operations to +jump to this label. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 2 + + + + + + button11 + button12 + + + + False + 5 + dialog + True + question + Add Wait + You can halt the execution of the script either until all the +keys that trigger this macro are held, or until all the keys +the trigger this macro are released (depending on the +trigger type) + +This allows you to have up to 3 stages in your macro when +activated by press. The 2nd stage would start when the +key is held, and the 3rd and final stage when the keys are +released. + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + Wait + + + True + True + 0 + + + + + True + False + WaitModel + + + + 1 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + + button16 + button17 + + + + 99999 + 1 + 10 + + + 480 + 540 + False + 5 + Script Editor + dialog + + + True + False + 2 + + + True + False + + + True + False + _Edit + True + + + True + False + + + gtk-cut + True + False + True + True + + + + + + gtk-copy + True + False + True + True + + + + + + gtk-paste + True + False + True + True + + + + + + gtk-delete + True + False + True + True + + + + + + True + False + + + + + True + False + Select All + True + + + + + + True + False + Select All Delays + True + + + + + + True + False + Select All Key Operations + True + + + + + + True + False + Select All Key Presses + True + + + + + + True + False + Select All Key Releases + True + + + + + + True + False + Select All Commands + True + + + + + + True + False + Deselect All + True + + + + + + True + False + + + + + True + False + Edit Selected Values + True + + + + + + + + + + True + False + _Help + True + + + True + False + + + gtk-help + True + False + True + True + + + + + + + + + False + True + 0 + + + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Record keystrokes and insert them into the macro script + Record + True + gtk-media-record + + + + False + True + + + + + True + False + + + False + + + + + True + False + Remove selected macro operations + Remove + True + gtk-delete + + + + False + True + + + + + True + False + + + False + + + + + True + False + Delay + Delay + True + gtk-media-pause + + + + False + True + + + + + True + False + Execute command + Command + True + gtk-execute + + + + False + True + + + + + True + False + Add a script label + Label + True + gtk-underline + + + + False + True + + + + + True + False + Goto a script label + Goto + True + gtk-media-previous + + + + False + True + + + + + True + False + Wait + True + gtk-stop + + + + False + True + + + + + False + True + 2 + + + + + True + True + automatic + automatic + in + + + True + True + ScriptModel + True + + + + + + 16 + + + + 0 + + + + + + + Op + + + + 2 + + + + + + + Value + + + + + + 3 + 1 + + + + + + + + + True + True + 3 + + + + + True + False + + + + + + False + False + 4 + + + + + + + + + SaveButton + button1 + + + + + + + + + + + + + + + + True + False + gtk-media-record + 6 + + + 420 + False + 5 + dialog + True + other + Record + Record a key sequence and insert it into +this macro. + RecordImage + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + Output delays + True + True + False + True + + + + True + True + 0 + + + + + Emit UInput codes instead of X + True + True + False + True + + + + True + True + 1 + + + + + + + + + True + False + <b>Output</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + 8 + + + gtk-media-record + True + True + True + True + + + + True + False + 0 + + + + + gtk-media-stop + True + True + True + True + + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + 0 + Recording status text ... + + + True + True + 1 + + + + + + + + + True + False + <b>Recording</b> + True + + + + + True + True + 1 + + + + + + + + True + True + 2 + + + + + + button14 + button15 + + + + False + 5 + dialog + True + question + Remove Macro Operations + Are you sure you wish to remove the selected macro +operations? + + + True + False + 2 + + + True + False + end + + + gtk-remove + True + True + True + True + + + False + False + 0 + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + + + + button4 + button5 + + + + True + False + + + gtk-cut + True + False + True + True + + + + + + gtk-copy + True + False + True + True + + + + + + gtk-paste + True + False + True + True + + + + + + gtk-delete + True + False + True + True + + + + + + + + + + + + + + + + + + + False + 5 + dialog + True + Set Value + Set the value on all selected operations + + + True + False + 2 + + + True + False + end + + + gtk-save + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + Value + + + True + True + 0 + + + + + True + True + + False + False + True + True + + + True + True + 1 + + + + + True + True + 2 + + + + + + button2 + button3 + + + + + + + + + + + + Hold + Until All Keys Held + + + Release + Until All Keys Released + + + + diff --git a/data/ui/turbo.png b/data/ui/turbo.png new file mode 100644 index 0000000000000000000000000000000000000000..834668ad50453a6be12f95ade8a9ea0c668c34b2 GIT binary patch literal 781 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 literal 0 HcmV?d00001 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..bbf6ff7 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +gnome15 (0~git-1) unstable; urgency=medium + + * Initial debian package (git commit abfdec9016768e1c31beda29b25568eed6f7dfe3) + + -- Dmitry Yu Okunev Sun, 15 Jan 2017 20:25:19 +0300 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..0e1674c --- /dev/null +++ b/debian/control @@ -0,0 +1,22 @@ +Source: gnome15 +Section: extra +Priority: optional +Maintainer: Dmitry Yu Okunev +Build-Depends: debhelper (>=9), python-dev, python-gtk2-dev, python-keyring, + python-virtkey, python-pyinotify, python-usb, python-gconf, python-rsvg, + python-uinput, python-xlib +Standards-Version: 3.9.8 +Homepage: https://gnome15.org/ +#Vcs-Git: git://anonscm.debian.org/collab-maint/gnome15.git +#Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/gnome15.git + +Package: gnome15 +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, python-wnck +Description: Tools and libs for Logithech G series keyboards + Gnome15 provides a panel indicator (or applet), configuration tool, + macro system and plugin framework for the Logitech G series keyboards + and headsets, including the G15, G19, G13, G930, G35, G510, G11, G110 + and the Z-10 speakers. The intention is to provide the best + integration with the Linux desktop possible, using the standard + protocols and libraries where appropriate. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..2b37e16 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,56 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: gnome15 +Source: https://github.com/Huskynarr/gnome15 + +Files: * +Copyright: 2010-2016 Brett Smith + 2010-2016 Nuno Araujo + 2010-2016 NoXPhasma + 2010-2016 Huskynarr +License: GPL-3+ + This program is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later + version. + . + This program 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 General Public License for more + details. + . + You should have received a copy of the GNU General Public + License along with this package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + +# If you want to use GPL v2 or later for the /debian/* files use +# the following clauses, or change it to suit. Delete these two lines +Files: debian/* +Copyright: 2017 Dmitry Yu Okunev +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package 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 General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +# Please avoid picking licenses with terms that are more restrictive than the +# packaged work, as it may make Debian's contributions unacceptable upstream. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..31f06c9 --- /dev/null +++ b/debian/rules @@ -0,0 +1,25 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#export DH_VERBOSE = 1 + + +# see FEATURE AREAS in dpkg-buildflags(1) +#export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +# see ENVIRONMENT in dpkg-buildflags(1) +# package maintainers to append CFLAGS +#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +# package maintainers to append LDFLAGS +#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed + + +%: + dh $@ --with autoreconf + + +# dh_make generated override targets +# This is example for Cmake (See https://bugs.debian.org/641051 ) +#override_dh_auto_configure: +# dh_auto_configure -- # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) + diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debug-g15-config.sh b/debug-g15-config.sh new file mode 100755 index 0000000..09fb01c --- /dev/null +++ b/debug-g15-config.sh @@ -0,0 +1,5 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +src/scripts/g15-config diff --git a/debug-g15-systemtray.sh b/debug-g15-systemtray.sh new file mode 100755 index 0000000..e25d21a --- /dev/null +++ b/debug-g15-systemtray.sh @@ -0,0 +1,5 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +src/scripts/g15-systemtray diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..e622179 --- /dev/null +++ b/debug.sh @@ -0,0 +1,10 @@ +#!/bin/bash +DIR=$(dirname $0) +cd "${DIR}" +#export G15_PLUGINS=./src/plugins +if [ $# -eq 0 ]; then + args=restart +else + args=$@ +fi +src/scripts/g15-desktop-service -f -l INFO $args diff --git a/docs/Gnome15.dia b/docs/Gnome15.dia new file mode 100644 index 0000000000000000000000000000000000000000..0e75569e0e8f94d36f600ea1ce3b971780fc406a GIT binary patch literal 18338 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 g)~3@FHN-0IUF$`i3x&9+iB5j^1k0IwoZf&c&j literal 0 HcmV?d00001 diff --git a/docs/style_guide.md b/docs/style_guide.md new file mode 100644 index 0000000..f377b86 --- /dev/null +++ b/docs/style_guide.md @@ -0,0 +1,84 @@ +# Style Guide + +The style guide can be summed up as 'clang-format with the Google style set'. +In addition, the [Google Style Guide](https://google.github.io/styleguide/cppguide.html) +is followed and cpplint is the source of truth. When in doubt, defer to what +code in the project already does. + +Base rules: + +* 80 column line length max +* LF (Unix-style) line endings +* 2-space soft tabs, no TABs! +* [Google Style Guide](https://google.github.io/styleguide/cppguide.html) for naming/casing/etc +* Sort includes according to the [style guide rules](https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes) +* Comments are properly punctuated (that means capitalization and periods, etc) +* TODO's must be attributed like `// TODO(yourgithubname): foo.` + +Code that really breaks from the formatting rules will not be accepted, as then +no one else can use clang-format on the code without also touching all your +lines. + +### Why? + +To quote the [Google Style Guide](https://google.github.io/styleguide/cppguide.html): + +``` +One way in which we keep the code base manageable is by enforcing consistency. +It is very important that any programmer be able to look at another's code and +quickly understand it. Maintaining a uniform style and following conventions +means that we can more easily use "pattern-matching" to infer what various +symbols are and what invariants are true about them. Creating common, required +idioms and patterns makes code much easier to understand. In some cases there +might be good arguments for changing certain style rules, but we nonetheless +keep things as they are in order to preserve consistency. +``` + +## Buildbot Verification + +The buildbot runs `xb lint --all` on the master branch, and will run +`xb lint --origin` on pull requests. Run `xb format` before you commit each +local change so that you are consistently clean, otherwise you may have to +rebase. If you forget, run `xb format --origin` and rebase your changes (so you +don't end up with 5 changes and then a 6th 'whoops' one - that's nasty). + +The buildbot is running LLVM 3.8.0. If you are noticing style differences +between your local lint/format and the buildbot, ensure you are running that +version. + +## Tools + +### clang-format + +clang-format with the Google style is used to format all files. I recommend +installing/wiring it up to your editor of choice so that you don't even have to +think about tabs and wrapping and such. + +#### Command Line + +To use the `xb format` auto-formatter, you need to have a `clang-format` on your +PATH. If you're on Windows you can do this by installing an LLVM binary package +from [the LLVM downloads page](http://llvm.org/releases/download.html). If you +install it to the default location the `xb format` command will find it +automatically even if you don't choose to put all of LLVM onto your PATH. + +#### Visual Studio + +Grab the official [experimental Visual Studio plugin](http://llvm.org/builds/). +To switch to the Google style go Tools -> Options -> LLVM/Clang -> ClangFormat +and set Style to Google. Then use ctrl-r/ctrl-f to trigger the formatting. +Unfortunately it only does the cursor by default, so you'll have to select the +whole doc and invoke it to get it all done. + +If you have a better option, let me know! + +#### Xcode + +Install [Alcatraz](http://alcatraz.io/) to get the [ClangFormat](https://github.com/travisjeffery/ClangFormat-Xcode) +package. Set it to use the Google style and format on save. Never think about +tabs or linefeeds or whatever again. + +### cpplint + +TODO: write a cool script to do this/editor plugins. +In the future, the linter will run as a git commit hook and on travis. diff --git a/example.svg b/example.svg new file mode 100644 index 0000000..7cbb55a --- /dev/null +++ b/example.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/i18n/Makefile.am b/i18n/Makefile.am new file mode 100644 index 0000000..da1475b --- /dev/null +++ b/i18n/Makefile.am @@ -0,0 +1,30 @@ +EXTRA_DIST = colorpicker.en_GB.po \ + gnome15.en_GB.po \ + gnome15-drivers.en_GB.po \ + driver_g15.en_GB.po \ + driver_g15direct.en_GB.po \ + driver_g19.en_GB.po \ + driver_g930.en_GB.po \ + driver_g19direct.en_GB.po \ + driver_gtk.en_GB.po \ + driver_kernel.en_GB.po \ + g15-config.en_GB.po \ + macro-editor.en_GB.po + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $$M_LOCALE/LC_MESSAGES ; \ + for M_PO in "$(abs_srcdir)"/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file $$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/i18n; \ + cp -pR $$M_LOCALE $(DESTDIR)$(datadir)/gnome15/i18n; \ + done + \ No newline at end of file diff --git a/i18n/build-po.sh b/i18n/build-po.sh new file mode 100755 index 0000000..1f45291 --- /dev/null +++ b/i18n/build-po.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo -e "Locale: \c" +read locale +if [ -n "${locale}" ]; then + for i in *.pot; do + bn=$(basename $i .pot).${locale}.po + msginit --input=${i} --output=${bn} --locale=${locale} + done +fi \ No newline at end of file diff --git a/i18n/build-pot.sh b/i18n/build-pot.sh new file mode 100755 index 0000000..a665e88 --- /dev/null +++ b/i18n/build-pot.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +cd $(dirname $0) + +# Python +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15-drivers.pot ../src/gnome15/util/*.py +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15-drivers.pot ../src/gnome15/drivers/*.py +xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=gnome15.pot ../src/gnome15/*.py + +# .ui files +for i in ../data/ui/*.ui; do + intltool-extract --type=gettext/glade $i + mv ${i}.h . +done +for i in *.h; do + bn=$(basename ${i} .h) + bn=$(basename ${bn} .ui).pot + xgettext --from-code=UTF-8 --language=Python --keyword=N_ --keyword=_ --output=${bn} ${i} +done diff --git a/i18n/colorpicker.en_GB.po b/i18n/colorpicker.en_GB.po new file mode 100644 index 0000000..671a95f --- /dev/null +++ b/i18n/colorpicker.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: colorpicker.glade.h:1 +msgid "Blue:" +msgstr "Blue:" + +#: colorpicker.glade.h:2 +msgid "Pick Colour" +msgstr "Pick Colour" + +#: colorpicker.glade.h:3 +msgid "Red:" +msgstr "Red:" diff --git a/i18n/colorpicker.glade.h b/i18n/colorpicker.glade.h new file mode 100644 index 0000000..7274581 --- /dev/null +++ b/i18n/colorpicker.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Blue:"); +char *s = N_("Pick Colour"); +char *s = N_("Red:"); diff --git a/i18n/colorpicker.pot b/i18n/colorpicker.pot new file mode 100644 index 0000000..71766bf --- /dev/null +++ b/i18n/colorpicker.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: colorpicker.glade.h:1 +msgid "Blue:" +msgstr "" + +#: colorpicker.glade.h:2 +msgid "Pick Colour" +msgstr "" + +#: colorpicker.glade.h:3 +msgid "Red:" +msgstr "" diff --git a/i18n/driver_g15.en_GB.po b/i18n/driver_g15.en_GB.po new file mode 100644 index 0000000..7d8cec3 --- /dev/null +++ b/i18n/driver_g15.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g15.glade.h:1 +msgid "Port" +msgstr "Port" diff --git a/i18n/driver_g15.glade.h b/i18n/driver_g15.glade.h new file mode 100644 index 0000000..1cac7b9 --- /dev/null +++ b/i18n/driver_g15.glade.h @@ -0,0 +1 @@ +char *s = N_("Port"); diff --git a/i18n/driver_g15.pot b/i18n/driver_g15.pot new file mode 100644 index 0000000..35a3ad1 --- /dev/null +++ b/i18n/driver_g15.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g15.glade.h:1 +msgid "Port" +msgstr "" diff --git a/i18n/driver_g15direct.en_GB.po b/i18n/driver_g15direct.en_GB.po new file mode 100644 index 0000000..ed48411 --- /dev/null +++ b/i18n/driver_g15direct.en_GB.po @@ -0,0 +1,42 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g15direct.glade.h:1 +msgid "Analogue Joystick" +msgstr "Analogue Joystick" + +#: driver_g15direct.glade.h:2 +msgid "Emit macro keys" +msgstr "Emit macro keys" + +#: driver_g15direct.glade.h:3 +msgid "Joystick mode" +msgstr "Joystick mode" + +#: driver_g15direct.glade.h:4 +msgid "Mouse" +msgstr "Mouse" + +#: driver_g15direct.glade.h:5 +msgid "Timeout" +msgstr "Timeout" + +#: driver_g15direct.glade.h:6 +msgid "ms" +msgstr "ms" diff --git a/i18n/driver_g15direct.glade.h b/i18n/driver_g15direct.glade.h new file mode 100644 index 0000000..9778364 --- /dev/null +++ b/i18n/driver_g15direct.glade.h @@ -0,0 +1,6 @@ +char *s = N_("Analogue Joystick"); +char *s = N_("Emit macro keys"); +char *s = N_("Joystick mode"); +char *s = N_("Mouse"); +char *s = N_("Timeout"); +char *s = N_("ms"); diff --git a/i18n/driver_g15direct.pot b/i18n/driver_g15direct.pot new file mode 100644 index 0000000..1f9faa6 --- /dev/null +++ b/i18n/driver_g15direct.pot @@ -0,0 +1,42 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g15direct.glade.h:1 +msgid "Analogue Joystick" +msgstr "" + +#: driver_g15direct.glade.h:2 +msgid "Emit macro keys" +msgstr "" + +#: driver_g15direct.glade.h:3 +msgid "Joystick mode" +msgstr "" + +#: driver_g15direct.glade.h:4 +msgid "Mouse" +msgstr "" + +#: driver_g15direct.glade.h:5 +msgid "Timeout" +msgstr "" + +#: driver_g15direct.glade.h:6 +msgid "ms" +msgstr "" diff --git a/i18n/driver_g19.en_GB.po b/i18n/driver_g19.en_GB.po new file mode 100644 index 0000000..8f7de2e --- /dev/null +++ b/i18n/driver_g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g19.glade.h:1 +msgid "Port" +msgstr "Port" diff --git a/i18n/driver_g19.glade.h b/i18n/driver_g19.glade.h new file mode 100644 index 0000000..1cac7b9 --- /dev/null +++ b/i18n/driver_g19.glade.h @@ -0,0 +1 @@ +char *s = N_("Port"); diff --git a/i18n/driver_g19.pot b/i18n/driver_g19.pot new file mode 100644 index 0000000..888c241 --- /dev/null +++ b/i18n/driver_g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g19.glade.h:1 +msgid "Port" +msgstr "" diff --git a/i18n/driver_g19direct.en_GB.po b/i18n/driver_g19direct.en_GB.po new file mode 100644 index 0000000..03d7ecf --- /dev/null +++ b/i18n/driver_g19direct.en_GB.po @@ -0,0 +1,34 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g19direct.glade.h:1 +msgid "Reset device before use" +msgstr "Reset device before use" + +#: driver_g19direct.glade.h:2 +msgid "Timeout" +msgstr "Timeout" + +#: driver_g19direct.glade.h:3 +msgid "ms" +msgstr "ms" + +#: driver_g19direct.glade.h:4 +msgid "then wait" +msgstr "then wait" diff --git a/i18n/driver_g19direct.glade.h b/i18n/driver_g19direct.glade.h new file mode 100644 index 0000000..619ce16 --- /dev/null +++ b/i18n/driver_g19direct.glade.h @@ -0,0 +1,4 @@ +char *s = N_("Reset device before use"); +char *s = N_("Timeout"); +char *s = N_("ms"); +char *s = N_("then wait"); diff --git a/i18n/driver_g19direct.pot b/i18n/driver_g19direct.pot new file mode 100644 index 0000000..d4e87b0 --- /dev/null +++ b/i18n/driver_g19direct.pot @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_g19direct.glade.h:1 +msgid "Reset device before use" +msgstr "" + +#: driver_g19direct.glade.h:2 +msgid "Timeout" +msgstr "" + +#: driver_g19direct.glade.h:3 +msgid "ms" +msgstr "" + +#: driver_g19direct.glade.h:4 +msgid "then wait" +msgstr "" diff --git a/i18n/driver_g930.en_GB.po b/i18n/driver_g930.en_GB.po new file mode 100644 index 0000000..98ed86a --- /dev/null +++ b/i18n/driver_g930.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_g930.glade.h:1 +msgid "Emit Macro Keys" +msgstr "Emit Macro Keys" diff --git a/i18n/driver_g930.glade.h b/i18n/driver_g930.glade.h new file mode 100644 index 0000000..9e095dd --- /dev/null +++ b/i18n/driver_g930.glade.h @@ -0,0 +1 @@ +char *s = N_("Emit Macro Keys"); diff --git a/i18n/driver_g930.pot b/i18n/driver_g930.pot new file mode 100644 index 0000000..11f884a --- /dev/null +++ b/i18n/driver_g930.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_kernel.glade.h:1 +msgid "Emit Macro Keys" +msgstr "" diff --git a/i18n/driver_gtk.en_GB.po b/i18n/driver_gtk.en_GB.po new file mode 100644 index 0000000..1d30ebb --- /dev/null +++ b/i18n/driver_gtk.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_gtk.glade.h:1 +msgid "Mode:" +msgstr "Mode:" diff --git a/i18n/driver_gtk.glade.h b/i18n/driver_gtk.glade.h new file mode 100644 index 0000000..6d8497a --- /dev/null +++ b/i18n/driver_gtk.glade.h @@ -0,0 +1 @@ +char *s = N_("Mode:"); diff --git a/i18n/driver_gtk.pot b/i18n/driver_gtk.pot new file mode 100644 index 0000000..fa0a94b --- /dev/null +++ b/i18n/driver_gtk.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_gtk.glade.h:1 +msgid "Mode:" +msgstr "" diff --git a/i18n/driver_kernel.en_GB.po b/i18n/driver_kernel.en_GB.po new file mode 100644 index 0000000..436ea9c --- /dev/null +++ b/i18n/driver_kernel.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: driver_kernel.glade.h:1 +msgid "Analogue Joystick" +msgstr "Analogue Joystick" + +#: driver_kernel.glade.h:2 +msgid "Device:" +msgstr "Device:" + +#: driver_kernel.glade.h:3 +msgid "Digital Joystick" +msgstr "Digital Joystick" + +#: driver_kernel.glade.h:4 +msgid "Emit Macro Keys" +msgstr "Emit Macro Keys" + +#: driver_kernel.glade.h:5 +msgid "Joystick mode:" +msgstr "Joystick mode:" + +#: driver_kernel.glade.h:6 +msgid "Mouse" +msgstr "Mouse" + +#: driver_kernel.glade.h:7 +msgid "auto" +msgstr "auto" + +#: driver_kernel.glade.h:8 +msgid "g13" +msgstr "g13" + +#: driver_kernel.glade.h:9 +msgid "g15v1" +msgstr "g15v1" + +#: driver_kernel.glade.h:10 +msgid "g15v2" +msgstr "g15v2" + +#: driver_kernel.glade.h:11 +msgid "g19" +msgstr "g19" diff --git a/i18n/driver_kernel.glade.h b/i18n/driver_kernel.glade.h new file mode 100644 index 0000000..cd25e52 --- /dev/null +++ b/i18n/driver_kernel.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Analogue Joystick"); +char *s = N_("Device:"); +char *s = N_("Digital Joystick"); +char *s = N_("Emit Macro Keys"); +char *s = N_("Joystick mode:"); +char *s = N_("Mouse"); +char *s = N_("auto"); +char *s = N_("g13"); +char *s = N_("g15v1"); +char *s = N_("g15v2"); +char *s = N_("g19"); diff --git a/i18n/driver_kernel.pot b/i18n/driver_kernel.pot new file mode 100644 index 0000000..0f62039 --- /dev/null +++ b/i18n/driver_kernel.pot @@ -0,0 +1,47 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: driver_kernel.glade.h:1 +msgid "Emit Macro Keys" +msgstr "" + + +#: driver_kernel.glade.h:6 +msgid "Mouse" +msgstr "" + +#: driver_kernel.glade.h:7 +msgid "auto" +msgstr "" + +#: driver_kernel.glade.h:8 +msgid "g13" +msgstr "" + +#: driver_kernel.glade.h:9 +msgid "g15v1" +msgstr "" + +#: driver_kernel.glade.h:10 +msgid "g15v2" +msgstr "" + +#: driver_kernel.glade.h:11 +msgid "g19" +msgstr "" diff --git a/i18n/g15-config.en_GB.po b/i18n/g15-config.en_GB.po new file mode 100644 index 0000000..230a409 --- /dev/null +++ b/i18n/g15-config.en_GB.po @@ -0,0 +1,330 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: g15-config.glade.h:1 +msgid "Controls" +msgstr "Controls" + +#: g15-config.glade.h:2 +msgid "Global Plugins" +msgstr "Global Plugins" + +#: g15-config.glade.h:3 +msgid "Images" +msgstr "Images" + +#: g15-config.glade.h:4 +msgid "Keys" +msgstr "Keys" + +#: g15-config.glade.h:5 +msgid "Memory Bank" +msgstr "Memory Bank" + +#: g15-config.glade.h:6 +msgid "Options" +msgstr "Options" + +#: g15-config.glade.h:7 +msgid "Profiles" +msgstr "Profiles" + +#: g15-config.glade.h:8 +msgid "Switches" +msgstr "Switches" + +#: g15-config.glade.h:9 +msgid "A. Author " +msgstr "A. Author " + +#: g15-config.glade.h:10 +msgid "About Plugin" +msgstr "About Plugin" + +#: g15-config.glade.h:11 +msgid "Activate" +msgstr "Activate" + +#: g15-config.glade.h:12 +msgid "" +"Activate this profile when a window with\n" +"the following title has focus" +msgstr "" +"Activate this profile when a window with\n" +"the following title has focus" + +#: g15-config.glade.h:14 +msgid "" +"Activate this profile when no others are\n" +"active" +msgstr "" +"Activate this profile when no others are\n" +"active" + +#: g15-config.glade.h:16 +msgid "Activation" +msgstr "Activation" + +#: g15-config.glade.h:17 +msgid "Add Profile" +msgstr "Add Profile" + +#: g15-config.glade.h:18 +msgid "Are you sure you wish to remove this macro?" +msgstr "Are you sure you wish to remove this macro?" + +#: g15-config.glade.h:19 +msgid "Are you sure you wish to remove this profile?" +msgstr "Are you sure you wish to remove this profile?" + +#: g15-config.glade.h:20 +msgid "Author" +msgstr "Author" + +#: g15-config.glade.h:21 +msgid "Author:" +msgstr "Author:" + +#: g15-config.glade.h:22 +msgid "Background" +msgstr "Background" + +#: g15-config.glade.h:23 +msgid "Balh" +msgstr "Balh" + +#: g15-config.glade.h:24 +msgid "Clear" +msgstr "Clear" + +#: g15-config.glade.h:25 +msgid "Configure" +msgstr "Configure" + +#: g15-config.glade.h:26 +msgid "Copyright © 2006 A. Author" +msgstr "Copyright © 2006 A. Author" + +#: g15-config.glade.h:27 +msgid "Copyright:" +msgstr "Copyright:" + +#: g15-config.glade.h:28 +msgid "Cycle screens" +msgstr "Cycle screens" + +#: g15-config.glade.h:29 +msgid "Delays" +msgstr "Delays" + +#: g15-config.glade.h:30 +msgid "Description of plugin. bold" +msgstr "Description of plugin. bold" + +#: g15-config.glade.h:31 +msgid "Description:" +msgstr "Description:" + +#: g15-config.glade.h:32 +msgid "Driver" +msgstr "Driver" + +#: g15-config.glade.h:33 +msgid "Enabled" +msgstr "Enabled" + +#: g15-config.glade.h:34 +msgid "Every" +msgstr "Every" + +#: g15-config.glade.h:35 +msgid "Export" +msgstr "Export" + +#: g15-config.glade.h:36 +msgid "Get more profiles or upload yours" +msgstr "Get more profiles or upload yours" + +#: g15-config.glade.h:37 +msgid "Global Options" +msgstr "Global Options" + +#: g15-config.glade.h:38 +msgid "Icon" +msgstr "Icon" + +#: g15-config.glade.h:39 +msgid "Import" +msgstr "Import" + +#: g15-config.glade.h:40 +msgid "Information" +msgstr "Information" + +#: g15-config.glade.h:41 +msgid "Keyboard" +msgstr "Keyboard" + +#: g15-config.glade.h:42 +msgid "Logitech G Keyboard Configuration" +msgstr "Logitech G Keyboard Configuration" + +#: g15-config.glade.h:43 +msgid "Logitech Keyboard Device" +msgstr "Logitech Keyboard Device" + +#: g15-config.glade.h:44 +msgid "M1" +msgstr "M1" + +#: g15-config.glade.h:45 +msgid "M2" +msgstr "M2" + +#: g15-config.glade.h:46 +msgid "M3" +msgstr "M3" + +#: g15-config.glade.h:47 +msgid "Macros" +msgstr "Macros" + +#: g15-config.glade.h:48 +msgid "Models supported." +msgstr "Models supported." + +#: g15-config.glade.h:49 +msgid "Only Show Indicator On Error" +msgstr "Only Show Indicator On Error" + +#: g15-config.glade.h:50 +msgid "Plugin Name" +msgstr "Plugin Name" + +#: g15-config.glade.h:51 +msgid "Plugins" +msgstr "Plugins" + +#: g15-config.glade.h:52 +msgid "Press for" +msgstr "Press for" + +#: g15-config.glade.h:53 +msgid "Profile" +msgstr "Profile" + +#: g15-config.glade.h:54 +msgid "Profile Name:" +msgstr "Profile Name:" + +#: g15-config.glade.h:55 +msgid "Profile already exists" +msgstr "Profile already exists" + +#: g15-config.glade.h:56 +msgid "Release, then wait" +msgstr "Release, then wait" + +#: g15-config.glade.h:57 +msgid "Remove Macro" +msgstr "Remove Macro" + +#: g15-config.glade.h:58 +msgid "Remove Profile" +msgstr "Remove Profile" + +#: g15-config.glade.h:59 +msgid "Select Window" +msgstr "Select Window" + +#: g15-config.glade.h:60 +msgid "Select a window from those active" +msgstr "Select a window from those active" + +#: g15-config.glade.h:61 +msgid "Send delays with keystrokes" +msgstr "Send delays with keystrokes" + +#: g15-config.glade.h:62 +msgid "Site:" +msgstr "Site:" + +#: g15-config.glade.h:63 +msgid "Start Desktop Service On Login" +msgstr "Start Desktop Service On Login" + +#: g15-config.glade.h:64 +msgid "Start Indicator On Login" +msgstr "Start Indicator On Login" + +#: g15-config.glade.h:65 +msgid "Start System Tray Icon On Login" +msgstr "Start System Tray Icon On Login" + +#: g15-config.glade.h:66 +msgid "Stop Service" +msgstr "Stop Service" + +#: g15-config.glade.h:67 +msgid "Supported Models:" +msgstr "Supported Models:" + +#: g15-config.glade.h:68 +msgid "Test" +msgstr "Test" + +#: g15-config.glade.h:69 +msgid "" +"The profile name you have supplied already exists, please\n" +"choose another name." +msgstr "" +"The profile name you have supplied already exists, please\n" +"choose another name." + +#: g15-config.glade.h:71 +msgid "Use" +msgstr "Use" + +#: g15-config.glade.h:72 +msgid "Use fixed delay" +msgstr "Use fixed delay" + +#: g15-config.glade.h:73 +msgid "Use macros from another profile when they are not set in this one" +msgstr "Use macros from another profile when they are not set in this one" + +#: g15-config.glade.h:74 +msgid "Window" +msgstr "Window" + +#: g15-config.glade.h:75 +msgid "label" +msgstr "label" + +#: g15-config.glade.h:76 +msgid "seconds" +msgstr "seconds" + +#: g15-config.glade.h:77 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: g15-config.glade.h:78 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/i18n/g15-config.glade.h b/i18n/g15-config.glade.h new file mode 100644 index 0000000..3a0915c --- /dev/null +++ b/i18n/g15-config.glade.h @@ -0,0 +1,78 @@ +char *s = N_("Controls"); +char *s = N_("Global Plugins"); +char *s = N_("Images"); +char *s = N_("Keys"); +char *s = N_("Memory Bank"); +char *s = N_("Options"); +char *s = N_("Profiles"); +char *s = N_("Switches"); +char *s = N_("A. Author "); +char *s = N_("About Plugin"); +char *s = N_("Activate"); +char *s = N_("Activate this profile when a window with\n" + "the following title has focus"); +char *s = N_("Activate this profile when no others are\n" + "active"); +char *s = N_("Activation"); +char *s = N_("Add Profile"); +char *s = N_("Are you sure you wish to remove this macro?"); +char *s = N_("Are you sure you wish to remove this profile?"); +char *s = N_("Author"); +char *s = N_("Author:"); +char *s = N_("Background"); +char *s = N_("Balh"); +char *s = N_("Clear"); +char *s = N_("Configure"); +char *s = N_("Copyright © 2006 A. Author"); +char *s = N_("Copyright:"); +char *s = N_("Cycle screens"); +char *s = N_("Delays"); +char *s = N_("Description of plugin. bold"); +char *s = N_("Description:"); +char *s = N_("Driver"); +char *s = N_("Enabled"); +char *s = N_("Every"); +char *s = N_("Export"); +char *s = N_("Get more profiles or upload yours"); +char *s = N_("Global Options"); +char *s = N_("Icon"); +char *s = N_("Import"); +char *s = N_("Information"); +char *s = N_("Keyboard"); +char *s = N_("Logitech G Keyboard Configuration"); +char *s = N_("Logitech Keyboard Device"); +char *s = N_("M1"); +char *s = N_("M2"); +char *s = N_("M3"); +char *s = N_("Macros"); +char *s = N_("Models supported."); +char *s = N_("Only Show Indicator On Error"); +char *s = N_("Plugin Name"); +char *s = N_("Plugins"); +char *s = N_("Press for"); +char *s = N_("Profile"); +char *s = N_("Profile Name:"); +char *s = N_("Profile already exists"); +char *s = N_("Release, then wait"); +char *s = N_("Remove Macro"); +char *s = N_("Remove Profile"); +char *s = N_("Select Window"); +char *s = N_("Select a window from those active"); +char *s = N_("Send delays with keystrokes"); +char *s = N_("Site:"); +char *s = N_("Start Desktop Service On Login"); +char *s = N_("Start Indicator On Login"); +char *s = N_("Start System Tray Icon On Login"); +char *s = N_("Stop Service"); +char *s = N_("Supported Models:"); +char *s = N_("Test"); +char *s = N_("The profile name you have supplied already exists, please\n" + "choose another name."); +char *s = N_("Use"); +char *s = N_("Use fixed delay"); +char *s = N_("Use macros from another profile when they are not set in this one"); +char *s = N_("Window"); +char *s = N_("label"); +char *s = N_("seconds"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/i18n/g15-config.pot b/i18n/g15-config.pot new file mode 100644 index 0000000..948914b --- /dev/null +++ b/i18n/g15-config.pot @@ -0,0 +1,324 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: g15-config.glade.h:1 +msgid "Controls" +msgstr "" + +#: g15-config.glade.h:2 +msgid "Global Plugins" +msgstr "" + +#: g15-config.glade.h:3 +msgid "Images" +msgstr "" + +#: g15-config.glade.h:4 +msgid "Keys" +msgstr "" + +#: g15-config.glade.h:5 +msgid "Memory Bank" +msgstr "" + +#: g15-config.glade.h:6 +msgid "Options" +msgstr "" + +#: g15-config.glade.h:7 +msgid "Profiles" +msgstr "" + +#: g15-config.glade.h:8 +msgid "Switches" +msgstr "" + +#: g15-config.glade.h:9 +msgid "A. Author " +msgstr "" + +#: g15-config.glade.h:10 +msgid "About Plugin" +msgstr "" + +#: g15-config.glade.h:11 +msgid "Activate" +msgstr "" + +#: g15-config.glade.h:12 +msgid "" +"Activate this profile when a window with\n" +"the following title has focus" +msgstr "" + +#: g15-config.glade.h:14 +msgid "" +"Activate this profile when no others are\n" +"active" +msgstr "" + +#: g15-config.glade.h:16 +msgid "Activation" +msgstr "" + +#: g15-config.glade.h:17 +msgid "Add Profile" +msgstr "" + +#: g15-config.glade.h:18 +msgid "Are you sure you wish to remove this macro?" +msgstr "" + +#: g15-config.glade.h:19 +msgid "Are you sure you wish to remove this profile?" +msgstr "" + +#: g15-config.glade.h:20 +msgid "Author" +msgstr "" + +#: g15-config.glade.h:21 +msgid "Author:" +msgstr "" + +#: g15-config.glade.h:22 +msgid "Background" +msgstr "" + +#: g15-config.glade.h:23 +msgid "Balh" +msgstr "" + +#: g15-config.glade.h:24 +msgid "Clear" +msgstr "" + +#: g15-config.glade.h:25 +msgid "Configure" +msgstr "" + +#: g15-config.glade.h:26 +msgid "Copyright © 2006 A. Author" +msgstr "" + +#: g15-config.glade.h:27 +msgid "Copyright:" +msgstr "" + +#: g15-config.glade.h:28 +msgid "Cycle screens" +msgstr "" + +#: g15-config.glade.h:29 +msgid "Delays" +msgstr "" + +#: g15-config.glade.h:30 +msgid "Description of plugin. bold" +msgstr "" + +#: g15-config.glade.h:31 +msgid "Description:" +msgstr "" + +#: g15-config.glade.h:32 +msgid "Driver" +msgstr "" + +#: g15-config.glade.h:33 +msgid "Enabled" +msgstr "" + +#: g15-config.glade.h:34 +msgid "Every" +msgstr "" + +#: g15-config.glade.h:35 +msgid "Export" +msgstr "" + +#: g15-config.glade.h:36 +msgid "Get more profiles or upload yours" +msgstr "" + +#: g15-config.glade.h:37 +msgid "Global Options" +msgstr "" + +#: g15-config.glade.h:38 +msgid "Icon" +msgstr "" + +#: g15-config.glade.h:39 +msgid "Import" +msgstr "" + +#: g15-config.glade.h:40 +msgid "Information" +msgstr "" + +#: g15-config.glade.h:41 +msgid "Keyboard" +msgstr "" + +#: g15-config.glade.h:42 +msgid "Logitech G Keyboard Configuration" +msgstr "" + +#: g15-config.glade.h:43 +msgid "Logitech Keyboard Device" +msgstr "" + +#: g15-config.glade.h:44 +msgid "M1" +msgstr "" + +#: g15-config.glade.h:45 +msgid "M2" +msgstr "" + +#: g15-config.glade.h:46 +msgid "M3" +msgstr "" + +#: g15-config.glade.h:47 +msgid "Macros" +msgstr "" + +#: g15-config.glade.h:48 +msgid "Models supported." +msgstr "" + +#: g15-config.glade.h:49 +msgid "Only Show Indicator On Error" +msgstr "" + +#: g15-config.glade.h:50 +msgid "Plugin Name" +msgstr "" + +#: g15-config.glade.h:51 +msgid "Plugins" +msgstr "" + +#: g15-config.glade.h:52 +msgid "Press for" +msgstr "" + +#: g15-config.glade.h:53 +msgid "Profile" +msgstr "" + +#: g15-config.glade.h:54 +msgid "Profile Name:" +msgstr "" + +#: g15-config.glade.h:55 +msgid "Profile already exists" +msgstr "" + +#: g15-config.glade.h:56 +msgid "Release, then wait" +msgstr "" + +#: g15-config.glade.h:57 +msgid "Remove Macro" +msgstr "" + +#: g15-config.glade.h:58 +msgid "Remove Profile" +msgstr "" + +#: g15-config.glade.h:59 +msgid "Select Window" +msgstr "" + +#: g15-config.glade.h:60 +msgid "Select a window from those active" +msgstr "" + +#: g15-config.glade.h:61 +msgid "Send delays with keystrokes" +msgstr "" + +#: g15-config.glade.h:62 +msgid "Site:" +msgstr "" + +#: g15-config.glade.h:63 +msgid "Start Desktop Service On Login" +msgstr "" + +#: g15-config.glade.h:64 +msgid "Start Indicator On Login" +msgstr "" + +#: g15-config.glade.h:65 +msgid "Start System Tray Icon On Login" +msgstr "" + +#: g15-config.glade.h:66 +msgid "Stop Service" +msgstr "" + +#: g15-config.glade.h:67 +msgid "Supported Models:" +msgstr "" + +#: g15-config.glade.h:68 +msgid "Test" +msgstr "" + +#: g15-config.glade.h:69 +msgid "" +"The profile name you have supplied already exists, please\n" +"choose another name." +msgstr "" + +#: g15-config.glade.h:71 +msgid "Use" +msgstr "" + +#: g15-config.glade.h:72 +msgid "Use fixed delay" +msgstr "" + +#: g15-config.glade.h:73 +msgid "Use macros from another profile when they are not set in this one" +msgstr "" + +#: g15-config.glade.h:74 +msgid "Window" +msgstr "" + +#: g15-config.glade.h:75 +msgid "label" +msgstr "" + +#: g15-config.glade.h:76 +msgid "seconds" +msgstr "" + +#: g15-config.glade.h:77 +msgid "toolbutton1" +msgstr "" + +#: g15-config.glade.h:78 +msgid "toolbutton2" +msgstr "" diff --git a/i18n/gnome15-drivers.en_GB.po b/i18n/gnome15-drivers.en_GB.po new file mode 100644 index 0000000..3e75f0d --- /dev/null +++ b/i18n/gnome15-drivers.en_GB.po @@ -0,0 +1,177 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:57 +#: ../main/python/gnome15/drivers/driver_g15direct.py:236 +msgid "G15 Direct" +msgstr "G15 Direct" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:59 +msgid "" +"For use with the G15 based devices only, this driver communicates directly, " +msgstr "" +"For use with the G15 based devices only, this driver communicates directly, " + +#: ../main/python/gnome15/drivers/driver_g15direct.py:120 +#: ../main/python/gnome15/drivers/driver_g15.py:106 +#: ../main/python/gnome15/drivers/driver_g19direct.py:104 +#: ../main/python/gnome15/drivers/driver_g19.py:147 +#: ../main/python/gnome15/drivers/driver_gtk.py:46 +#: ../main/python/gnome15/drivers/driver_gtk.py:53 +#: ../main/python/gnome15/drivers/driver_kernel.py:194 +#: ../main/python/gnome15/drivers/driver_kernel.py:206 +msgid "Memory Bank Keys" +msgstr "Memory Bank Keys" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:121 +#: ../main/python/gnome15/drivers/driver_g15direct.py:122 +#: ../main/python/gnome15/drivers/driver_g15.py:107 +#: ../main/python/gnome15/drivers/driver_g15.py:108 +#: ../main/python/gnome15/drivers/driver_g19direct.py:105 +#: ../main/python/gnome15/drivers/driver_g19.py:148 +#: ../main/python/gnome15/drivers/driver_gtk.py:47 +#: ../main/python/gnome15/drivers/driver_gtk.py:57 +#: ../main/python/gnome15/drivers/driver_kernel.py:195 +#: ../main/python/gnome15/drivers/driver_kernel.py:203 +msgid "Keyboard Backlight Colour" +msgstr "Keyboard Backlight Colour" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:123 +#: ../main/python/gnome15/drivers/driver_g15.py:109 +#: ../main/python/gnome15/drivers/driver_gtk.py:54 +#: ../main/python/gnome15/drivers/driver_kernel.py:207 +msgid "Keyboard Backlight Level" +msgstr "Keyboard Backlight Level" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:124 +#: ../main/python/gnome15/drivers/driver_g15.py:110 +msgid "LCD Backlight Level" +msgstr "LCD Backlight Level" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:125 +#: ../main/python/gnome15/drivers/driver_g15.py:111 +#: ../main/python/gnome15/drivers/driver_kernel.py:209 +msgid "LCD Contrast" +msgstr "LCD Contrast" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:126 +#: ../main/python/gnome15/drivers/driver_g15.py:112 +#: ../main/python/gnome15/drivers/driver_gtk.py:55 +#: ../main/python/gnome15/drivers/driver_kernel.py:210 +msgid "Invert LCD" +msgstr "Invert LCD" + +#: ../main/python/gnome15/drivers/driver_g15.py:49 +msgid "G15Daemon" +msgstr "G15Daemon" + +#: ../main/python/gnome15/drivers/driver_g15.py:51 +msgid "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " +msgstr "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " + +#: ../main/python/gnome15/drivers/driver_g15.py:354 +msgid "g15daemon driver" +msgstr "g15daemon driver" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:53 +msgid "G19 Direct" +msgstr "G19 Direct" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:55 +msgid "For use with the Logitech G19 only, this driver communicates directly, " +msgstr "" +"For use with the Logitech G19 only, this driver communicates directly, " + +#: ../main/python/gnome15/drivers/driver_g19direct.py:106 +#: ../main/python/gnome15/drivers/driver_g19.py:149 +msgid "Boot Keyboard Backlight Colour" +msgstr "Boot Keyboard Backlight Colour" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:107 +#: ../main/python/gnome15/drivers/driver_g19.py:150 +#: ../main/python/gnome15/drivers/driver_gtk.py:48 +#: ../main/python/gnome15/drivers/driver_kernel.py:197 +msgid "LCD Brightness" +msgstr "LCD Brightness" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:108 +#: ../main/python/gnome15/drivers/driver_g19.py:151 +#: ../main/python/gnome15/drivers/driver_gtk.py:49 +#: ../main/python/gnome15/drivers/driver_kernel.py:198 +msgid "Default LCD Foreground" +msgstr "Default LCD Foreground" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:109 +#: ../main/python/gnome15/drivers/driver_g19.py:152 +#: ../main/python/gnome15/drivers/driver_gtk.py:50 +#: ../main/python/gnome15/drivers/driver_kernel.py:199 +msgid "Default LCD Background" +msgstr "Default LCD Background" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:110 +#: ../main/python/gnome15/drivers/driver_g19.py:153 +#: ../main/python/gnome15/drivers/driver_gtk.py:51 +#: ../main/python/gnome15/drivers/driver_kernel.py:200 +msgid "Default Highlight Color" +msgstr "Default Highlight Color" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:163 +#: ../main/python/gnome15/drivers/driver_g19.py:199 +msgid "G19D Network Daemon Driver" +msgstr "G19D Network Daemon Driver" + +#: ../main/python/gnome15/drivers/driver_g19.py:47 +msgid "G19D" +msgstr "G19D" + +#: ../main/python/gnome15/drivers/driver_g19.py:49 +msgid "For use with the Logitech G19 only, this driver uses G19D, " +msgstr "For use with the Logitech G19 only, this driver uses G19D, " + +#: ../main/python/gnome15/drivers/driver_gtk.py:38 +msgid "GTK Virtual Keyboard Driver" +msgstr "GTK Virtual Keyboard Driver" + +#: ../main/python/gnome15/drivers/driver_gtk.py:39 +msgid "A special development driver that emulates all supported, " +msgstr "A special development driver that emulates all supported, " + +#: ../main/python/gnome15/drivers/driver_gtk.py:136 +msgid "GTK Keyboard Emulator Driver" +msgstr "GTK Keyboard Emulator Driver" + +#: ../main/python/gnome15/drivers/driver_kernel.py:51 +msgid "Kernel Drivers" +msgstr "Kernel Drivers" + +#: ../main/python/gnome15/drivers/driver_kernel.py:52 +msgid "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " +msgstr "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " + +#: ../main/python/gnome15/drivers/driver_kernel.py:208 +msgid "LCD Backlight" +msgstr "LCD Backlight" diff --git a/i18n/gnome15-drivers.pot b/i18n/gnome15-drivers.pot new file mode 100644 index 0000000..dd33aa8 --- /dev/null +++ b/i18n/gnome15-drivers.pot @@ -0,0 +1,171 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:57 +#: ../main/python/gnome15/drivers/driver_g15direct.py:236 +msgid "G15 Direct" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:59 +msgid "" +"For use with the G15 based devices only, this driver communicates directly, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:120 +#: ../main/python/gnome15/drivers/driver_g15.py:106 +#: ../main/python/gnome15/drivers/driver_g19direct.py:104 +#: ../main/python/gnome15/drivers/driver_g19.py:147 +#: ../main/python/gnome15/drivers/driver_gtk.py:46 +#: ../main/python/gnome15/drivers/driver_gtk.py:53 +#: ../main/python/gnome15/drivers/driver_kernel.py:194 +#: ../main/python/gnome15/drivers/driver_kernel.py:206 +msgid "Memory Bank Keys" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:121 +#: ../main/python/gnome15/drivers/driver_g15direct.py:122 +#: ../main/python/gnome15/drivers/driver_g15.py:107 +#: ../main/python/gnome15/drivers/driver_g15.py:108 +#: ../main/python/gnome15/drivers/driver_g19direct.py:105 +#: ../main/python/gnome15/drivers/driver_g19.py:148 +#: ../main/python/gnome15/drivers/driver_gtk.py:47 +#: ../main/python/gnome15/drivers/driver_gtk.py:57 +#: ../main/python/gnome15/drivers/driver_kernel.py:195 +#: ../main/python/gnome15/drivers/driver_kernel.py:203 +msgid "Keyboard Backlight Colour" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:123 +#: ../main/python/gnome15/drivers/driver_g15.py:109 +#: ../main/python/gnome15/drivers/driver_gtk.py:54 +#: ../main/python/gnome15/drivers/driver_kernel.py:207 +msgid "Keyboard Backlight Level" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:124 +#: ../main/python/gnome15/drivers/driver_g15.py:110 +msgid "LCD Backlight Level" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:125 +#: ../main/python/gnome15/drivers/driver_g15.py:111 +#: ../main/python/gnome15/drivers/driver_kernel.py:209 +msgid "LCD Contrast" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15direct.py:126 +#: ../main/python/gnome15/drivers/driver_g15.py:112 +#: ../main/python/gnome15/drivers/driver_gtk.py:55 +#: ../main/python/gnome15/drivers/driver_kernel.py:210 +msgid "Invert LCD" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:49 +msgid "G15Daemon" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:51 +msgid "" +"For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses " +"g15daemon, available from " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g15.py:354 +msgid "g15daemon driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:53 +msgid "G19 Direct" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:55 +msgid "For use with the Logitech G19 only, this driver communicates directly, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:106 +#: ../main/python/gnome15/drivers/driver_g19.py:149 +msgid "Boot Keyboard Backlight Colour" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:107 +#: ../main/python/gnome15/drivers/driver_g19.py:150 +#: ../main/python/gnome15/drivers/driver_gtk.py:48 +#: ../main/python/gnome15/drivers/driver_kernel.py:197 +msgid "LCD Brightness" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:108 +#: ../main/python/gnome15/drivers/driver_g19.py:151 +#: ../main/python/gnome15/drivers/driver_gtk.py:49 +#: ../main/python/gnome15/drivers/driver_kernel.py:198 +msgid "Default LCD Foreground" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:109 +#: ../main/python/gnome15/drivers/driver_g19.py:152 +#: ../main/python/gnome15/drivers/driver_gtk.py:50 +#: ../main/python/gnome15/drivers/driver_kernel.py:199 +msgid "Default LCD Background" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:110 +#: ../main/python/gnome15/drivers/driver_g19.py:153 +#: ../main/python/gnome15/drivers/driver_gtk.py:51 +#: ../main/python/gnome15/drivers/driver_kernel.py:200 +msgid "Default Highlight Color" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19direct.py:163 +#: ../main/python/gnome15/drivers/driver_g19.py:199 +msgid "G19D Network Daemon Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19.py:47 +msgid "G19D" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_g19.py:49 +msgid "For use with the Logitech G19 only, this driver uses G19D, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:38 +msgid "GTK Virtual Keyboard Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:39 +msgid "A special development driver that emulates all supported, " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_gtk.py:136 +msgid "GTK Keyboard Emulator Driver" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:51 +msgid "Kernel Drivers" +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:52 +msgid "" +"Requires ali123's Logitech Kernel drivers. This method requires no other " +"daemons to be running, and works with the G13, G15, G19 and G110 keyboards. " +msgstr "" + +#: ../main/python/gnome15/drivers/driver_kernel.py:208 +msgid "LCD Backlight" +msgstr "" diff --git a/i18n/gnome15.en_GB.po b/i18n/gnome15.en_GB.po new file mode 100644 index 0000000..e14c53b --- /dev/null +++ b/i18n/gnome15.en_GB.po @@ -0,0 +1,187 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../main/python/gnome15/g15config.py:409 +msgid "Start Service" +msgstr "Start Service" + +#: ../main/python/gnome15/g15config.py:417 +msgid "No supported devices could be found. Is the " +msgstr "No supported devices could be found. Is the " + +#: ../main/python/gnome15/g15config.py:572 +msgid "The Gnome15 desktop service is not running. It is recommended " +msgstr "The Gnome15 desktop service is not running. It is recommended " + +#: ../main/python/gnome15/g15config.py:577 +msgid "The Gnome15 desktop service is starting up. Please wait" +msgstr "The Gnome15 desktop service is starting up. Please wait" + +#: ../main/python/gnome15/g15config.py:581 +msgid "The Gnome15 desktop service is stopping." +msgstr "The Gnome15 desktop service is stopping." + +#: ../main/python/gnome15/g15config.py:599 +msgid "The Gnome15 desktop service is running, but failed to connect " +msgstr "The Gnome15 desktop service is running, but failed to connect " + +#: ../main/python/gnome15/g15config.py:754 +msgid "Hold" +msgstr "Hold" + +#: ../main/python/gnome15/g15config.py:932 +msgid "There is no appropriate driver for the device " +msgstr "There is no appropriate driver for the device " + +#: ../main/python/gnome15/g15config.py:952 +msgid "This driver has no configuration options" +msgstr "This driver has no configuration options" + +#: ../main/python/gnome15/g15config.py:1243 +#: ../main/python/gnome15/g15macroeditor.py:311 +#, python-format +msgid "Macro %s" +msgstr "Macro %s" + +#: ../main/python/gnome15/g15config.py:1635 +msgid "Set backlight colour" +msgstr "Set backlight colour" + +#: ../main/python/gnome15/g15desktop.py:824 +msgid "Desktop integration for Logitech 'G' keyboards." +msgstr "Desktop integration for Logitech 'G' keyboards." + +#: ../main/python/gnome15/g15desktop.py:878 +msgid "Stop Desktop Service" +msgstr "Stop Desktop Service" + +#: ../main/python/gnome15/g15desktop.py:901 +msgid "Cycle screens automatically" +msgstr "Cycle screens automatically" + +#: ../main/python/gnome15/g15desktop.py:923 +#, python-format +msgid "Enable %s" +msgstr "Enable %s" + +#: ../main/python/gnome15/g15desktop.py:937 +msgid "Start Desktop Service" +msgstr "Start Desktop Service" + +#: ../main/python/gnome15/g15desktop.py:980 +#, python-format +msgid "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" +msgstr "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" + +#: ../main/python/gnome15/g15devices.py:310 +msgid "Virtual LCD Window" +msgstr "Virtual LCD Window" + +#: ../main/python/gnome15/g15devices.py:311 +msgid "Logitech G11 Keyboard" +msgstr "Logitech G11 Keyboard" + +#: ../main/python/gnome15/g15devices.py:312 +msgid "Logitech G19 Gaming Keyboard" +msgstr "Logitech G19 Gaming Keyboard" + +#: ../main/python/gnome15/g15devices.py:313 +msgid "Logitech G15 Gaming Keyboard (version 1)" +msgstr "Logitech G15 Gaming Keyboard (version 1)" + +#: ../main/python/gnome15/g15devices.py:314 +msgid "Logitech G15 Gaming Keyboard (version 2)" +msgstr "Logitech G15 Gaming Keyboard (version 2)" + +#: ../main/python/gnome15/g15devices.py:315 +msgid "Logitech G13 Advanced Gameboard" +msgstr "Logitech G13 Advanced Gameboard" + +#: ../main/python/gnome15/g15devices.py:316 +msgid "Logitech G510 Keyboard" +msgstr "Logitech G510 Keyboard" + +#: ../main/python/gnome15/g15devices.py:317 +msgid "Logitech G510 Keyboard (audio)" +msgstr "Logitech G510 Keyboard (audio)" + +#: ../main/python/gnome15/g15devices.py:318 +msgid "Logitech Z10 Speakers" +msgstr "Logitech Z10 Speakers" + +#: ../main/python/gnome15/g15devices.py:319 +msgid "Logitech G110 Keyboard" +msgstr "Logitech G110 Keyboard" + +#: ../main/python/gnome15/g15devices.py:320 +msgid "Logitech GamePanel" +msgstr "Logitech GamePanel" + +#: ../main/python/gnome15/g15devices.py:323 +msgid "Logitech MX5500" +msgstr "Logitech MX5500" + +#: ../main/python/gnome15/g15drivermanager.py:94 +#: ../main/python/gnome15/g15drivermanager.py:110 +#, python-format +msgid "No drivers support the model %s" +msgstr "No drivers support the model %s" + +#: ../main/python/gnome15/g15drivermanager.py:101 +#, python-format +msgid "" +"Driver %s is not available. Do you have to appropriate package installed?" +msgstr "" +"Driver %s is not available. Do you have to appropriate package installed?" + +#: ../main/python/gnome15/g15exceptions.py:25 +msgid "Failed to connect." +msgstr "Failed to connect." + +#: ../main/python/gnome15/g15macroeditor.py:257 +msgid "Open.." +msgstr "Open.." + +#: ../main/python/gnome15/g15macroeditor.py:264 +msgid "All files" +msgstr "All files" + +#: ../main/python/gnome15/g15macroeditor.py:466 +msgid "This key combination is already in use with " +msgstr "This key combination is already in use with " + +#: ../main/python/gnome15/g15macroeditor.py:476 +msgid "This key combination is reserved for use with an action. You " +msgstr "This key combination is reserved for use with an action. You " + +#: ../main/python/gnome15/g15macroeditor.py:485 +msgid "You have not chosen a macro key to assign the action to." +msgstr "You have not chosen a macro key to assign the action to." + +#: ../main/python/gnome15/g15screen.py:505 +msgid "The current device has no suitable output device" +msgstr "The current device has no suitable output device" + +#: ../main/python/gnome15/g15screen.py:1678 +msgid "Starting up .." +msgstr "Starting up .." diff --git a/i18n/gnome15.pot b/i18n/gnome15.pot new file mode 100644 index 0000000..98c724e --- /dev/null +++ b/i18n/gnome15.pot @@ -0,0 +1,184 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../main/python/gnome15/g15config.py:409 +msgid "Start Service" +msgstr "" + +#: ../main/python/gnome15/g15config.py:417 +msgid "No supported devices could be found. Is the " +msgstr "" + +#: ../main/python/gnome15/g15config.py:572 +msgid "The Gnome15 desktop service is not running. It is recommended " +msgstr "" + +#: ../main/python/gnome15/g15config.py:577 +msgid "The Gnome15 desktop service is starting up. Please wait" +msgstr "" + +#: ../main/python/gnome15/g15config.py:581 +msgid "The Gnome15 desktop service is stopping." +msgstr "" + +#: ../main/python/gnome15/g15config.py:599 +msgid "The Gnome15 desktop service is running, but failed to connect " +msgstr "" + +#: ../main/python/gnome15/g15config.py:754 +msgid "Hold" +msgstr "" + +#: ../main/python/gnome15/g15config.py:932 +msgid "There is no appropriate driver for the device " +msgstr "" + +#: ../main/python/gnome15/g15config.py:952 +msgid "This driver has no configuration options" +msgstr "" + +#: ../main/python/gnome15/g15config.py:1243 +#: ../main/python/gnome15/g15macroeditor.py:311 +#, python-format +msgid "Macro %s" +msgstr "" + +#: ../main/python/gnome15/g15config.py:1635 +msgid "Set backlight colour" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:824 +msgid "Desktop integration for Logitech 'G' keyboards." +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:878 +msgid "Stop Desktop Service" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:901 +msgid "Cycle screens automatically" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:923 +#, python-format +msgid "Enable %s" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:937 +msgid "Start Desktop Service" +msgstr "" + +#: ../main/python/gnome15/g15desktop.py:980 +#, python-format +msgid "" +"%s is now the active keyboard. Use mouse wheel up and down to cycle screens " +"on this device" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:310 +msgid "Virtual LCD Window" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:311 +msgid "Logitech G11 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:312 +msgid "Logitech G19 Gaming Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:313 +msgid "Logitech G15 Gaming Keyboard (version 1)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:314 +msgid "Logitech G15 Gaming Keyboard (version 2)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:315 +msgid "Logitech G13 Advanced Gameboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:316 +msgid "Logitech G510 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:317 +msgid "Logitech G510 Keyboard (audio)" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:318 +msgid "Logitech Z10 Speakers" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:319 +msgid "Logitech G110 Keyboard" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:320 +msgid "Logitech GamePanel" +msgstr "" + +#: ../main/python/gnome15/g15devices.py:323 +msgid "Logitech MX5500" +msgstr "" + +#: ../main/python/gnome15/g15drivermanager.py:94 +#: ../main/python/gnome15/g15drivermanager.py:110 +#, python-format +msgid "No drivers support the model %s" +msgstr "" + +#: ../main/python/gnome15/g15drivermanager.py:101 +#, python-format +msgid "" +"Driver %s is not available. Do you have to appropriate package installed?" +msgstr "" + +#: ../main/python/gnome15/g15exceptions.py:25 +msgid "Failed to connect." +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:257 +msgid "Open.." +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:264 +msgid "All files" +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:466 +msgid "This key combination is already in use with " +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:476 +msgid "This key combination is reserved for use with an action. You " +msgstr "" + +#: ../main/python/gnome15/g15macroeditor.py:485 +msgid "You have not chosen a macro key to assign the action to." +msgstr "" + +#: ../main/python/gnome15/g15screen.py:505 +msgid "The current device has no suitable output device" +msgstr "" + +#: ../main/python/gnome15/g15screen.py:1678 +msgid "Starting up .." +msgstr "" diff --git a/i18n/macro-editor.en_GB.po b/i18n/macro-editor.en_GB.po new file mode 100644 index 0000000..738ff64 --- /dev/null +++ b/i18n/macro-editor.en_GB.po @@ -0,0 +1,138 @@ +# English translations for gnome15 package. +# Copyright (C) 2011 THE gnome15'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15 package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: 2011-10-09 14:54+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: macro-editor.glade.h:1 +msgid "Keys Pressed" +msgstr "Keys Pressed" + +#: macro-editor.glade.h:2 +msgid "Memory Bank:" +msgstr "Memory Bank:" + +#: macro-editor.glade.h:3 +msgid "Name:" +msgstr "Name:" + +#: macro-editor.glade.h:4 +msgid "Repetition" +msgstr "Repetition" + +#: macro-editor.glade.h:5 +msgid "Target:" +msgstr "Target:" + +#: macro-editor.glade.h:6 +msgid "Action" +msgstr "Action" + +#: macro-editor.glade.h:7 +msgid "Allow combination of keys" +msgstr "Allow combination of keys" + +#: macro-editor.glade.h:8 +msgid "BTN_TEST" +msgstr "BTN_TEST" + +#: macro-editor.glade.h:9 +msgid "Delay between repeats" +msgstr "Delay between repeats" + +#: macro-editor.glade.h:10 +msgid "Digital Joystick" +msgstr "Digital Joystick" + +#: macro-editor.glade.h:11 +msgid "Edit Macro" +msgstr "Edit Macro" + +#: macro-editor.glade.h:12 +msgid "Filter:" +msgstr "Filter:" + +#: macro-editor.glade.h:13 +msgid "Joystick Button" +msgstr "Joystick Button" + +#: macro-editor.glade.h:14 +msgid "KEY_TEST" +msgstr "KEY_TEST" + +#: macro-editor.glade.h:15 +msgid "Keyboard Key" +msgstr "Keyboard Key" + +#: macro-editor.glade.h:16 +msgid "Macro Script" +msgstr "Macro Script" + +#: macro-editor.glade.h:17 +msgid "Mode:" +msgstr "Mode:" + +#: macro-editor.glade.h:18 +msgid "Mouse Button" +msgstr "Mouse Button" + +#: macro-editor.glade.h:19 +msgid "None" +msgstr "None" + +#: macro-editor.glade.h:20 +msgid "Override default repeat delay" +msgstr "Override default repeat delay" + +#: macro-editor.glade.h:21 +msgid "Run Command" +msgstr "Run Command" + +#: macro-editor.glade.h:22 +msgid "Simple Macro" +msgstr "Simple Macro" + +#: macro-editor.glade.h:23 +msgid "Toggle" +msgstr "Toggle" + +#: macro-editor.glade.h:24 +msgid "When held" +msgstr "When held" + +#: macro-editor.glade.h:25 +msgid "You can use & to run the command in the background" +msgstr "You can use & to run the command in the background" + +#: macro-editor.glade.h:26 +msgid "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" +msgstr "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" + +#: macro-editor.glade.h:28 +msgid "_Browse" +msgstr "_Browse" + +#: macro-editor.glade.h:29 +msgid "column" +msgstr "column" + +#: macro-editor.glade.h:30 +msgid "label" +msgstr "label" diff --git a/i18n/macro-editor.glade.h b/i18n/macro-editor.glade.h new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/i18n/macro-editor.glade.h @@ -0,0 +1,30 @@ +char *s = N_("Keys Pressed"); +char *s = N_("Memory Bank:"); +char *s = N_("Name:"); +char *s = N_("Repetition"); +char *s = N_("Target:"); +char *s = N_("Action"); +char *s = N_("Allow combination of keys"); +char *s = N_("BTN_TEST"); +char *s = N_("Delay between repeats"); +char *s = N_("Digital Joystick"); +char *s = N_("Edit Macro"); +char *s = N_("Filter:"); +char *s = N_("Joystick Button"); +char *s = N_("KEY_TEST"); +char *s = N_("Keyboard Key"); +char *s = N_("Macro Script"); +char *s = N_("Mode:"); +char *s = N_("Mouse Button"); +char *s = N_("None"); +char *s = N_("Override default repeat delay"); +char *s = N_("Run Command"); +char *s = N_("Simple Macro"); +char *s = N_("Toggle"); +char *s = N_("When held"); +char *s = N_("You can use & to run the command in the background"); +char *s = N_("\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" + "\\p for pause and \\\\ for backslash"); +char *s = N_("_Browse"); +char *s = N_("column"); +char *s = N_("label"); diff --git a/i18n/macro-editor.pot b/i18n/macro-editor.pot new file mode 100644 index 0000000..092137c --- /dev/null +++ b/i18n/macro-editor.pot @@ -0,0 +1,136 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 14:52+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: macro-editor.glade.h:1 +msgid "Keys Pressed" +msgstr "" + +#: macro-editor.glade.h:2 +msgid "Memory Bank:" +msgstr "" + +#: macro-editor.glade.h:3 +msgid "Name:" +msgstr "" + +#: macro-editor.glade.h:4 +msgid "Repetition" +msgstr "" + +#: macro-editor.glade.h:5 +msgid "Target:" +msgstr "" + +#: macro-editor.glade.h:6 +msgid "Action" +msgstr "" + +#: macro-editor.glade.h:7 +msgid "Allow combination of keys" +msgstr "" + +#: macro-editor.glade.h:8 +msgid "BTN_TEST" +msgstr "" + +#: macro-editor.glade.h:9 +msgid "Delay between repeats" +msgstr "" + +#: macro-editor.glade.h:10 +msgid "Digital Joystick" +msgstr "" + +#: macro-editor.glade.h:11 +msgid "Edit Macro" +msgstr "" + +#: macro-editor.glade.h:12 +msgid "Filter:" +msgstr "" + +#: macro-editor.glade.h:13 +msgid "Joystick Button" +msgstr "" + +#: macro-editor.glade.h:14 +msgid "KEY_TEST" +msgstr "" + +#: macro-editor.glade.h:15 +msgid "Keyboard Key" +msgstr "" + +#: macro-editor.glade.h:16 +msgid "Macro Script" +msgstr "" + +#: macro-editor.glade.h:17 +msgid "Mode:" +msgstr "" + +#: macro-editor.glade.h:18 +msgid "Mouse Button" +msgstr "" + +#: macro-editor.glade.h:19 +msgid "None" +msgstr "" + +#: macro-editor.glade.h:20 +msgid "Override default repeat delay" +msgstr "" + +#: macro-editor.glade.h:21 +msgid "Run Command" +msgstr "" + +#: macro-editor.glade.h:22 +msgid "Simple Macro" +msgstr "" + +#: macro-editor.glade.h:23 +msgid "Toggle" +msgstr "" + +#: macro-editor.glade.h:24 +msgid "When held" +msgstr "" + +#: macro-editor.glade.h:25 +msgid "You can use & to run the command in the background" +msgstr "" + +#: macro-editor.glade.h:26 +msgid "" +"\\r for Return, \\e for escape, \\b for backspace, \\t for tab\n" +"\\p for pause and \\\\ for backslash" +msgstr "" + +#: macro-editor.glade.h:28 +msgid "_Browse" +msgstr "" + +#: macro-editor.glade.h:29 +msgid "column" +msgstr "" + +#: macro-editor.glade.h:30 +msgid "label" +msgstr "" diff --git a/m4/ax_python_devel.m4 b/m4/ax_python_devel.m4 new file mode 100644 index 0000000..436a0bd --- /dev/null +++ b/m4/ax_python_devel.m4 @@ -0,0 +1,324 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_python_devel.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PYTHON_DEVEL([version]) +# +# DESCRIPTION +# +# Note: Defines as a precious variable "PYTHON_VERSION". Don't override it +# in your configure.ac. +# +# This macro checks for Python and tries to get the include path to +# 'Python.h'. It provides the $(PYTHON_CPPFLAGS) and $(PYTHON_LDFLAGS) +# output variables. It also exports $(PYTHON_EXTRA_LIBS) and +# $(PYTHON_EXTRA_LDFLAGS) for embedding Python in your code. +# +# You can search for some particular version of Python by passing a +# parameter to this macro, for example ">= '2.3.1'", or "== '2.4'". Please +# note that you *have* to pass also an operator along with the version to +# match, and pay special attention to the single quotes surrounding the +# version number. Don't use "PYTHON_VERSION" for this: that environment +# variable is declared as precious and thus reserved for the end-user. +# +# This macro should work for all versions of Python >= 2.1.0. As an end +# user, you can disable the check for the python version by setting the +# PYTHON_NOVERSIONCHECK environment variable to something else than the +# empty string. +# +# If you need to use this macro for an older Python version, please +# contact the authors. We're always open for feedback. +# +# LICENSE +# +# Copyright (c) 2009 Sebastian Huber +# Copyright (c) 2009 Alan W. Irwin +# Copyright (c) 2009 Rafael Laboissiere +# Copyright (c) 2009 Andrew Collier +# Copyright (c) 2009 Matteo Settenvini +# Copyright (c) 2009 Horst Knorr +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program 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 General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 8 + +AU_ALIAS([AC_PYTHON_DEVEL], [AX_PYTHON_DEVEL]) +AC_DEFUN([AX_PYTHON_DEVEL],[ + # + # Allow the use of a (user set) custom python version + # + AC_ARG_VAR([PYTHON_VERSION],[The installed Python + version to use, for example '2.3'. This string + will be appended to the Python interpreter + canonical name.]) + + AC_PATH_PROG([PYTHON],[python[$PYTHON_VERSION]]) + if test -z "$PYTHON"; then + AC_MSG_ERROR([Cannot find python$PYTHON_VERSION in your system path]) + PYTHON_VERSION="" + fi + + # + # Check for a version of Python >= 2.1.0 + # + AC_MSG_CHECKING([for a version of Python >= '2.1.0']) + ac_supports_python_ver=`$PYTHON -c "import sys; \ + ver = sys.version.split()[[0]]; \ + print (ver >= '2.1.0')"` + if test "$ac_supports_python_ver" != "True"; then + if test -z "$PYTHON_NOVERSIONCHECK"; then + AC_MSG_RESULT([no]) + AC_MSG_FAILURE([ +This version of the AC@&t@_PYTHON_DEVEL macro +doesn't work properly with versions of Python before +2.1.0. You may need to re-run configure, setting the +variables PYTHON_CPPFLAGS, PYTHON_LDFLAGS, PYTHON_SITE_PKG, +PYTHON_EXTRA_LIBS and PYTHON_EXTRA_LDFLAGS by hand. +Moreover, to disable this check, set PYTHON_NOVERSIONCHECK +to something else than an empty string. +]) + else + AC_MSG_RESULT([skip at user request]) + fi + else + AC_MSG_RESULT([yes]) + fi + + # + # if the macro parameter ``version'' is set, honour it + # + if test -n "$1"; then + AC_MSG_CHECKING([for a version of Python $1]) + ac_supports_python_ver=`$PYTHON -c "import sys; ver = sys.version.split()[[0]]; print (ver >= $1)"` + if test "$ac_supports_python_ver" = "True"; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([this package requires Python $1. +If you have it installed, but it isn't the default Python +interpreter in your system path, please pass the PYTHON_VERSION +variable to configure. See ``configure --help'' for reference. +]) + PYTHON_VERSION="" + fi + fi + + # + # Check if you have distutils, else fail + # + AC_MSG_CHECKING([for the distutils Python package]) + ac_distutils_result=`$PYTHON -c "import distutils" 2>&1` + if test -z "$ac_distutils_result"; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + AC_MSG_ERROR([cannot import Python module "distutils". +Please check your Python installation. The error was: +$ac_distutils_result]) + PYTHON_VERSION="" + fi + + # + # Check for Python include path + # + AC_MSG_CHECKING([for Python include path]) + if test -z "$PYTHON_CPPFLAGS"; then + python_path=`$PYTHON -c "import distutils.sysconfig; \ + print (distutils.sysconfig.get_python_inc ());"` + if test -n "${python_path}"; then + python_path="-I$python_path" + fi + PYTHON_CPPFLAGS=$python_path + fi + AC_MSG_RESULT([$PYTHON_CPPFLAGS]) + AC_SUBST([PYTHON_CPPFLAGS]) + + # + # Check for Python library path + # + AC_MSG_CHECKING([for Python library path]) + if test -z "$PYTHON_LDFLAGS"; then + # (makes two attempts to ensure we've got a version number + # from the interpreter) + ac_python_version=`cat<]], + [[Py_Initialize();]]) + ],[pythonexists=yes],[pythonexists=no]) + AC_LANG_POP([C]) + # turn back to default flags + CPPFLAGS="$ac_save_CPPFLAGS" + LIBS="$ac_save_LIBS" + + AC_MSG_RESULT([$pythonexists]) + + if test ! "x$pythonexists" = "xyes"; then + AC_MSG_FAILURE([ + Could not link test program to Python. Maybe the main Python library has been + installed in some non-standard library path. If so, pass it to configure, + via the LDFLAGS environment variable. + Example: ./configure LDFLAGS="-L/usr/non-standard-path/python/lib" + ============================================================================ + ERROR! + You probably have to install the development version of the Python package + for your distribution. The exact name of this package varies among them. + ============================================================================ + ]) + PYTHON_VERSION="" + fi + + # + # all done! + # +]) + diff --git a/m4/ax_python_module.m4 b/m4/ax_python_module.m4 new file mode 100644 index 0000000..bd70a06 --- /dev/null +++ b/m4/ax_python_module.m4 @@ -0,0 +1,49 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_python_module.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PYTHON_MODULE(modname[, fatal]) +# +# DESCRIPTION +# +# Checks for Python module. +# +# If fatal is non-empty then absence of a module will trigger an error. +# +# LICENSE +# +# Copyright (c) 2008 Andrew Collier +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 5 + +AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE]) +AC_DEFUN([AX_PYTHON_MODULE],[ + if test -z $PYTHON; + then + PYTHON="python" + fi + PYTHON_NAME=`basename $PYTHON` + AC_MSG_CHECKING($PYTHON_NAME module: $1) + $PYTHON -c "import $1" 2>/dev/null + if test $? -eq 0; + then + AC_MSG_RESULT(yes) + eval AS_TR_CPP(HAVE_PYMOD_$1)=yes + else + AC_MSG_RESULT(no) + eval AS_TR_CPP(HAVE_PYMOD_$1)=no + # + if test -n "$2" + then + AC_MSG_ERROR(failed to find required module $1) + exit 1 + fi + fi +]) diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..830583d --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,14 @@ +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray.1 +endif + +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator.1 +endif + +if ENABLE_DRIVER_KERNEL + MAYBE_SYSTEM_SERVICE = g15-system-service.1 +endif + +man1_MANS = g15-desktop-service.1 g15-config.1 $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) $(MAYBE_SYSTEM_SERVICE) +EXTRA_DIST = g15-desktop-service.1 g15-config.1 g15-systemtray.1 g15-indicator.1 g15-system-service.1 \ No newline at end of file diff --git a/man/g15-config.1 b/man/g15-config.1 new file mode 100644 index 0000000..f26d4dd --- /dev/null +++ b/man/g15-config.1 @@ -0,0 +1,33 @@ +.\" Process this file with +.\" groff -man -Tascii g15-config.1 +.\" +.TH g15-config 1 +.SH NAME +g15-config \- Configuration tool for Logitech G keyboards +.SH SYNOPSIS +.B g15-config [-c] +.SH DESCRIPTION +.B g15-config +Starts the +.B Gnome15 +Logitech G keyboard configuration tool. +It allows you to configure things such as the keyboard +backlight colour or level, screen cycling options and which +plugins are enabled. + +If the desktop service is not running, this tool will detect +that and offer advice as to what to do. + +The first time +.B g15-config +is used, you will be prompted for which driver to use. +.SH OPTIONS +.IP -c +Force the first-run configuration dialog to appear, allowing +choice of which driver to use (additional packages may be required +for your hardware). +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-indicator (1), +.BR g15-macros (1) \ No newline at end of file diff --git a/man/g15-desktop-service.1 b/man/g15-desktop-service.1 new file mode 100644 index 0000000..c173b61 --- /dev/null +++ b/man/g15-desktop-service.1 @@ -0,0 +1,44 @@ +.\" Process this file with +.\" groff -man -Tascii g15-desktop-service.1 +.\" +.TH g15-desktop-service 1 +.SH NAME +g15-desktop-service \- Starts Gnome15 Desktop service +.SH SYNOPSIS +.B g15-desktop-service [-c] +.SH DESCRIPTION +.B g15-desktop-service +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +This service is responsible for connecting to the underlying +service appropriate for the hardware (i.e. g15daemon or g19d), +managing and running the plugins, displaying the plugin's +output on the LCD, delivering key events to X when macro +keys are activated. + +It also provides the DBUS service used by various clients +such as the Panel Applet, Indicator or System Tray Icon, +or other external examples that want to draw on the +LCD. + +The first time Gnome15 is used, you will be prompted for +which driver to use (additional packages may be required +for your hardware). +.SH OPTIONS +.IP "-l DEBUG|INFO|WARNING|ERROR" +Set the log level. +.IP -f +Force the service to run in the foreground. +.IP start +Starts the service. +.IP stop +Stops the service. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-indicator (1) +.BR g15-systemtray (1) +.BR g15-system-service (1) \ No newline at end of file diff --git a/man/g15-indicator.1 b/man/g15-indicator.1 new file mode 100644 index 0000000..176894f --- /dev/null +++ b/man/g15-indicator.1 @@ -0,0 +1,27 @@ +.\" Process this file with +.\" groff -man -Tascii g15-indicator.1 +.\" +.TH g15-indicator 1 +.SH NAME +g15-indicator \- Starts an indicator for Gnome15 desktop service +.SH SYNOPSIS +.B g15-indicator +.SH DESCRIPTION +.B g15-indicator +Starts the Gnome15 +.I Indicator Applet +that may be used to quickly access configuration, select +the currently active screen, and start the +.B g15-desktop-service +when required. + +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-desktop-service (1), +.BR g15-systemtray (1) +.BR g15-system-service (1) \ No newline at end of file diff --git a/man/g15-system-service.1 b/man/g15-system-service.1 new file mode 100644 index 0000000..c1efb4a --- /dev/null +++ b/man/g15-system-service.1 @@ -0,0 +1,35 @@ +.\" Process this file with +.\" groff -man -Tascii g15-desktop-service.1 +.\" +.TH g15-system-service 1 +.SH NAME +g15-system-service \- Starts Gnome15 System service +.SH SYNOPSIS +.B g15-system-service +.SH DESCRIPTION +.B g15-system-service +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +This service is responsible for writing values to the +various LED device files exposed by the LG4L kernel drivers. + +Ordinarily, you should never need to start this service yourself, as +it will be started the first time it is needed. +.SH OPTIONS +.IP "-l DEBUG|INFO|WARNING|ERROR" +Set the log level. +.IP -f +Force the service to run in the foreground. +.IP start +Starts the service. +.IP stop +Stops the service. +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-indicator (1) +.BR g15-systemtray (1) +.BR g15-desktop-service (1) \ No newline at end of file diff --git a/man/g15-systemtray.1 b/man/g15-systemtray.1 new file mode 100644 index 0000000..b9feaa9 --- /dev/null +++ b/man/g15-systemtray.1 @@ -0,0 +1,28 @@ +.\" Process this file with +.\" groff -man -Tascii g15-indicator.1 +.\" +.TH g15-systemtray 1 +.SH NAME +g15-systemtray \- Adds an icon in the notification area allowing control of Gnome15 +.SH SYNOPSIS +.B g15-systemtray +.SH DESCRIPTION +.B g15-systemtray +Starts the Gnome15 +.I System Tray +that may be used to quickly access configuration, select +the currently active screen, and start the +.B g15-desktop-service +when required. + +Gnome15 is a suite of applications and plugins that provide +integration of the Logitech G series keyboards into the +GNOME desktop environment. + +.SH AUTHOR +Brett Smith +.SH "SEE ALSO" +.BR g15-config (1), +.BR g15-desktop-service (1), +.BR g15-system-service (1) +.BR g15-indicator (1) \ No newline at end of file diff --git a/mksvgheaders.py b/mksvgheaders.py new file mode 100755 index 0000000..bc5ef8f --- /dev/null +++ b/mksvgheaders.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python2 + +# +-----------------------------------------------------------------------------+ +# | GPL | +# +-----------------------------------------------------------------------------+ +# | Copyright (c) Brett Smith | +# | | +# | This program is free software; you can redistribute it and/or | +# | modify it under the terms of the GNU General Public License | +# | as published by the Free Software Foundation; either version 2 | +# | of the License, or (at your option) any later version. | +# | | +# | This program 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 General Public License for more details. | +# | | +# | You should have received a copy of the GNU General Public License | +# | along with this program; if not, write to the Free Software | +# | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | +# +-----------------------------------------------------------------------------+ + +from lxml import etree + +# Logging +import logging +logging.basicConfig(format='%(levelname)s:%(asctime)s:%(threadName)s:%(name)s:%(message)s', datefmt='%H:%M:%S') +logger = logging.getLogger() + +LEVELS = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + +nsmap = { + 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'cc': 'http://web.resource.org/cc/', + 'svg': 'http://www.w3.org/2000/svg', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'xlink': 'http://www.w3.org/1999/xlink', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', + } + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + level = logging.NOTSET + if options.log_level != None: + level = LEVELS.get(options.log_level.lower(), logging.NOTSET) + logger.setLevel(level = level) + + for f in args: + document = etree.parse(f) + root = document.getroot() + for text in root.xpath('//text()',namespaces=nsmap): + text = str(text).strip() + if len(text) > 0 and text.startswith("_("): + print "char *s = N_(\"%s\");" % text[2:-1] diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..1f595a8 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,94 @@ +SUBDIRS = scripts plugins gnome15 + +if ENABLE_DRIVER_G19DIRECT +SUBDIRS += pylibg19 +endif + +if ENABLE_PLUGIN_IMPULSE15 +SUBDIRS += libimpulse +endif + +if ENABLE_GNOME_SHELL_EXTENSION +SUBDIRS += gnome-shell-extension +endif + +all-local: + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $$PLUGIN_DIR ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR -a -d $$THEME_DIR/i18n ]; then \ + pushd $$THEME_DIR; \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + popd; \ + fi; \ + done; \ + popd; \ + fi; \ + done; + +clean-local: + find . -name '*.pyc' -exec rm {} \; ; \ + find . -name '*.pyo' -exec rm {} \; ; \ + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $$PLUGIN_DIR ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + if [ -d i18n/$$M_LOCALE ]; then \ + rm -fr i18n/$$M_LOCALE; \ + fi; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR/i18n/$$M_LOCALE ]; then \ + pushd $$THEME_DIR; \ + rm -fr i18n/$$M_LOCALE; \ + popd; \ + fi; \ + done; \ + done; \ + popd; \ + fi; \ + done; + +install-exec-hook: + for PLUGIN in `ls plugins`; do \ + PLUGIN_DIR=plugins/$$PLUGIN; \ + if [ -d $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN ]; then \ + pushd $$PLUGIN_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/i18n; \ + done; \ + for THEME_DIR in *; do \ + if [ -d $$THEME_DIR -a -d $$THEME_DIR/i18n ]; then \ + pushd $$THEME_DIR; \ + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/$$THEME_DIR/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/$$PLUGIN/$$THEME_DIR/i18n; \ + done; \ + popd; \ + fi; \ + done; \ + popd; \ + fi; \ + done; diff --git a/src/gamewrap/gamewrap b/src/gamewrap/gamewrap new file mode 100755 index 0000000..d719b5b --- /dev/null +++ b/src/gamewrap/gamewrap @@ -0,0 +1,101 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Wrapper to launch games (and other applications), monitor their output for +patterns and send events to interested DBUS clients +""" + + +import sys +import os +import glib + +# Logging +import logging +logging.basicConfig(format='%(threadName)s:%(name)s:%(message)s') +logger = logging.getLogger() + +LEVELS = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +# Server host class + +def check_service_status(system_dbus): + try : + system_bus.get_object('org.gnome15.GameWrap', '/org/gnome15/GameWrap').GetServerInformation() + return True + except Exception as e: + logger.debug("D-Bus service not available.", exc_info = e) + return False + +def start_service(args, bus, no_trap=False,): + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except ImportError as ie: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'", exc_info = ie) + + # Start the loop + try : + import gw + service = gw.G15GameWrapperServiceController(args, bus, no_trap=no_trap) + service.start_loop() + except dbus.exceptions.NameExistsException as nee: + logger.debug("D-Bus service already running", exc_info = nee) + print "GameWrap service is already running" + sys.exit(1) + +if __name__ == "__main__": + """ + Allow arguments to be passed to gamewrap itself. If the first command line + argument begins with - or --, then gamewrap options follow until the first + argument that doesn't start with - or -- + """ + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + (options, args) = parser.parse_args() + + level = logging.NOTSET + if options.log_level != None: + level = LEVELS.get(options.log_level.lower(), logging.NOTSET) + logger.setLevel(level = level) + + the_bus = dbus.SessionBus() + if check_service_status(the_bus): + print "GameWrap service already running" + else: + start_service(args, the_bus, options.no_trap) diff --git a/src/gamewrap/gw/__init__.py b/src/gamewrap/gw/__init__.py new file mode 100644 index 0000000..9e3d2fa --- /dev/null +++ b/src/gamewrap/gw/__init__.py @@ -0,0 +1,143 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject +import subprocess +import signal +import sys +import dbus.service +import threading +import re + +# Logging +import logging +logger = logging.getLogger(__name__) + +NAME = "GameWrap" +VERSION = "0.1" +BUS_NAME = "org.gnome15.GameWrap" +OBJECT_PATH = "/org/gnome15/GameWrap" +IF_NAME = "org.gnome15.GameWrap" + +class RunThread(threading.Thread): + + def __init__(self, controller): + threading.Thread.__init__(self, name = "ExecCommand") + self.controller = controller + + def run(self): + logger.info("Running '%s'", str(self.controller.args)) + self.process = subprocess.Popen(self.controller.args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) + logger.info("Process started OK") + while True: + line = self.process.stdout.readline(1024) + if line: + logger.info(">%s<", line) + for pattern_id in self.controller.patterns: + pattern = self.controller.patterns[pattern_id] + match = re.search(pattern, line) + if match: + logger.info("Match! %s", str(match)) + gobject.idle_add(self.controller.PatternMatched(patter_id, line)) + else: + break + logger.info("Waiting for process to complete") + self.controller.status = self.process.wait() + logger.info("Process complete with %s", self.controller.status) + self.controller.Stop() + +class G15GameWrapperServiceController(dbus.service.Object): + + def __init__(self, args, bus, no_trap=False): + bus_name = dbus.service.BusName(BUS_NAME, bus=bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, None, OBJECT_PATH, bus_name) + + self._page_sequence_number = 1 + self._bus = bus + self.args = args + self.status = 0 + self.patterns = {} + + logger.info("Exposing service for '%s'. Wait for signal to wait", str(args)) + + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + + self._loop = gobject.MainLoop() + + def start_loop(self): + logger.info("Starting GLib loop") + self._loop.run() + logger.debug("Exited GLib loop") + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal, shutting down") + self.shutdown() + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal, shutting down") + self.shutdown() + + """ + DBUS API + """ + @dbus.service.method(IF_NAME) + def Start(self): + RunThread(self).start() + + @dbus.service.method(IF_NAME) + def Stop(self): + gobject.idle_add(self._shutdown()) + + @dbus.service.method(IF_NAME, in_signature='ss') + def AddPattern(self, pattern_id, pattern): + logger.info("Adding pattern '%s' with id '%s'", pattern, pattern_id) + if pattern_id in self.patterns: + raise Exception("Pattern with ID %s already registered." % pattern_id) + self.patterns[pattern_id] = pattern + + @dbus.service.method(IF_NAME, in_signature='s') + def RemovePattern(self, pattern_id): + logger.info("Removing pattern with id '%s'", pattern_id) + if not pattern_id in self.patterns: + raise Exception("Pattern with ID %s not registered." % pattern_id) + del self.patterns[id] + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssssas') + def GetInformation(self): + return ("GameWrapper Service", "Gnome15 Project", VERSION, "1.0", self.args) + + + """ + Signals + """ + + """ + DBUS Signals + """ + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PatternMatch(self, pattern_id, line): + pass + + """ + Private + """ + + def _shutdown(self): + logger.info("Shutting down") + self._loop.quit() + sys.exit(self.status) \ No newline at end of file diff --git a/src/gamewrap/gw/wraplet.py b/src/gamewrap/gw/wraplet.py new file mode 100644 index 0000000..f219abf --- /dev/null +++ b/src/gamewrap/gw/wraplet.py @@ -0,0 +1,31 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import shlex + +class Wraplet(): + + def __init__(self, filename): + self.filename = filename + + fd = open(filename, "r") + try: + lexr = shlex.shlex() + lexr.wordchars = "._" + finally: + fd.close() + + \ No newline at end of file diff --git a/src/gamewrap/ut2004.wlet b/src/gamewrap/ut2004.wlet new file mode 100644 index 0000000..8b0b9a8 --- /dev/null +++ b/src/gamewrap/ut2004.wlet @@ -0,0 +1,58 @@ +# GameWrap config for Unreal Tourname 2004 + +# activate defines a list of patterns to match against the command line +# arguments. When a game is launched with parameters that match these, +# this config file will be activated. + +activate "ut2004", ".*/ut2004" + +# +# Map the internal terminology to the game terminology +# + +terminology { + energy: "Health", + game: "Round", + set: "Game", + match: "Tournament", + shield: "Shield", + boost: "Adrenalin" +} + +# +# Defines which patterns will activate which events +# + +events { + # Game + new-game: "NewGame", + lost-game: "ASGameInfo::EndRound", + won-game: "ASGameInfo::EndRound", + game-over: "ASGameInfo::EndRound", + + # Round + new-round: "NewRound", + lost-round: "LostRound", + won-round: "LostRound", + round-over: "LostRound", + + # Lives + life-lost: "Lifelost", + life-gained: "newlist", + + # Energy + energy-low: "Lifelost", + energy-boost: "newlist", + energy-max: "max-energy", + energy-level: "energy-level", + + # Shield + shield-full: "", + shield-empty: "", + shield-level: "", + + # Boost + boost-available: "", + boost-empty: "", + +} \ No newline at end of file diff --git a/src/gnome-shell-extension/Makefile.am b/src/gnome-shell-extension/Makefile.am new file mode 100644 index 0000000..0d627fe --- /dev/null +++ b/src/gnome-shell-extension/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = icons + +extensiondir = $(datadir)/gnome-shell/extensions/gnome15-shell-extension@gnome15.org +extension_DATA = extension.js \ + metadata.json \ + stylesheet.css + +EXTRA_DIST = \ + $(extension_DATA) diff --git a/src/gnome-shell-extension/extension.js b/src/gnome-shell-extension/extension.js new file mode 100644 index 0000000..fa8dd8d --- /dev/null +++ b/src/gnome-shell-extension/extension.js @@ -0,0 +1,750 @@ +// Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +// Copyright (C) 2012 Brett Smith +// Copyright (C) 2013 Brett Smith +// Nuno Araujo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* + * Gnome15 - The Logitech Keyboard Manager for Linux + * + * This GNOME Shell extension allows control of all supported and connected + * Logitech devices from the shell's top panel. A menu button is added for + * each device, providing options to enable/disable the device, enable/disable + * screen cycling, and make a particular page the currently visible one. + */ + +const St = imports.gi.St; +const Main = imports.ui.main; +const Tweener = imports.ui.tweener; +const Lang = imports.lang; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Clutter = imports.gi.Clutter; +const Config = imports.misc.config; + +let currNotification, gnome15Service, devices, dbus_watch_id; + +/* + * Remote object definitions. This is just a sub-set of the full API + * available, just enough to do the job + */ + +const Gnome15ServiceInterface = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +const Gnome15ScreenInterface = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* No idea why, but Cycle and CycleKeyboard signatures argument type are actually 'n', + * but this causes an exception when calling with JavaScript integer. + */ + +const Gnome15DeviceInterface = + + + + + + + + + + + + + + + + + + + + + + +const Gnome15PageInterface = + + + + + + + + + +/** + * Instances of this class are responsible for managing a single device. + * A single button is created and added to the top panel, various attributes + * about the device attached are read, and if the device currently has + * a screen (i.e. is enabled), the initial page list is loaded. + * + * Signals are also setup to watch for the screen being enable / disabled + * externally. + */ +const DeviceItem = new Lang.Class({ + Name: 'DeviceItem', + _init : function(key) { + this.parent(); + this._buttonSignals = new Array(); + let gnome15Device = _createDevice(key); + gnome15Device.GetModelFullNameRemote(Lang.bind(this, function(result) { + let [modelFullName] = result; + gnome15Device.GetModelIdRemote(Lang.bind(this, function(result) { + let [uid] = result; + gnome15Device.GetScreenRemote(Lang.bind(this, function(result) { + let [screen] = result; + gnome15Device.connectSignal("ScreenAdded", Lang.bind(this, function(src, senderName, args) { + let [screenPath] = args; + _log("Screen added " + screenPath); + this._getPages(screenPath); + })); + gnome15Device.connectSignal("ScreenRemoved", Lang.bind(this, function(src, senderName, args) { + let [screenPath] = args; + _log("Screen removed " + screenPath); + this._cleanUp(); + this._gnome15Button.clearPages(); + })); + this._addButton(key, modelFullName, uid, screen); + })); + })); + })); + }, + + _addButton: function(key, modelFullName, modelId, screen) { + let hasScreen = screen != null && screen.length > 0; + this._gnome15Button = new DeviceButton(key, modelId, modelFullName, hasScreen); + + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + Main.panel._rightBox.insert_child_at_index(this._gnome15Button.actor, 1); + Main.panel._rightBox.child_set(this._gnome15Button.actor, { + y_fill : true + }); + Main.panel._menus.addMenu(this._gnome15Button.menu); + } + else if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + Main.panel.addToStatusArea('gnome15-' + modelId, this._gnome15Button); + } + else { + Main.panel.addToStatusArea('gnome15-' + modelId, this._gnome15Button); + Main.panel.menuManager.addMenu(this._gnome15Button.menu); + } + + if(hasScreen) { + /* If this device already has a screen (i.e. is enabled, load the + * pages now). Otherwise, we wait for ScreenAdded to come in + * in extension itself + */ + this._getPages(screen); + } + else { + this._gnome15Button.reset(); + } + }, + + /** + * Removes the signals that are being watched for this device and + * mark the button so that the enabled switch is turned off when + * the menu is reset + */ + _cleanUp: function() { + if(this._gnome15Button._screen != null) { + for(let key in this._buttonSignals) { + this._gnome15Button._screen.disconnectSignal(this._buttonSignals[key]); + } + this._buttonSignals.splice(0, this._buttonSignals.length); + this._gnome15Button._screen = null; + } + }, + + /** + * Callback that receives the full list of pages currently showing on + * this device and adds them to the button. It then starts watching for + * new pages appearing, or pages being deleted and acts accordingly. + */ + _getPages: function(screen) { + this._cleanUp(); + this._gnome15Button._screen = _createScreen(screen); + this._gnome15Button._screen.GetPagesRemote(Lang.bind(this, function(result) { + let [pages] = result; + this._gnome15Button.clearPages(); + for(let key in pages) { + this._gnome15Button.addPage(pages[key]); + } + this._gnome15Button._screen.IsCyclingEnabledRemote(Lang.bind(this, function(result) { + let [cyclingEnabled] = result; + this._gnome15Button.setCyclingEnabled(cyclingEnabled); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("PageCreated", Lang.bind(this, function(src, senderName, args) { + let pagePath = args[0]; + this._gnome15Button.addPage(pagePath); + }))); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("PageDeleting", Lang.bind(this, function(src, senderName, args) { + let pagePath = args[0]; + this._gnome15Button.deletePage(pagePath); + }))); + this._buttonSignals.push(this._gnome15Button._screen.connectSignal("CyclingChanged", Lang.bind(this, function(src, senderName, args) { + let [cycle] = args; + this._gnome15Button.setCyclingEnabled(cycle); + }))); + })); + })); + + + }, + + /** + * Called as a result of the service disappearing or the extension being + * disabled. The button is removed from the top panel. + */ + close : function(pages) { + this._gnome15Button.destroy(); + } +}); + +/** + * A switch menu item that allows a single device to be enabled or disabled. + */ +const EnableDisableMenuItem = new Lang.Class({ + Name: 'EnableDisableMenuItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init : function(devicePath, modelName, screen) { + this.parent(modelName); + this.setToggleState(screen != null); + this.connect('toggled', Lang.bind(this, function() { + if(this.state) { + _createDevice(devicePath).EnableRemote(); + } + else { + _createDevice(devicePath).DisableRemote(); + } + })); + }, + + activate : function(event) { + this.parent(event); + }, +}); + +/** + * A switch menu item that allows automatic page cycling to be enabled or + * disabled. + */ +const CyclePagesMenuItem = new Lang.Class({ + Name: 'CyclePagesMenuItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init : function(selected, screen) { + this.parent("Cycle pages automatically"); + this.setToggleState(selected); + this._screen = screen; + }, + + activate : function(event) { + this._screen.SetCyclingEnabledRemote(!this.state); + this.parent(event); + }, +}); + +/** + * A menu item that represents a single page on a single device. Activating + * this item causes the page to be displayed. + */ +const PageMenuItem = new Lang.Class({ + Name: 'PageMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init : function(lblText, lblId, page_proxy) { + this.parent(); + this.label = new St.Label({ + text : lblText + }); + if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this.actor.add_child(this.label); + } + else { + this.addActor(this.label); + } + this._pageProxy = page_proxy; + this._text = lblText; + this._idTxt = lblId; + }, + + activate : function(event) { + this._pageProxy.CycleToRemote(); + this.parent(event); + }, +}); + +/** + * A menu item that that activates g15-config. It will open with provided + * device UID open (via the -d option of g15-config). + */ +const PreferencesMenuItem = new Lang.Class({ + Name: 'PreferencesMenuItem', + Extends: PopupMenu.PopupMenuItem, + + _init : function(deviceUid) { + this.parent("Configuration"); + this._deviceUid = deviceUid + }, + + activate : function(event) { + GLib.spawn_command_line_async('g15-config -d ' + this._deviceUid); + this.parent(event); + }, +}); + +/** + * Shell top panel button that represents a single Gnome15 device. + */ +const DeviceButton = new Lang.Class({ + Name: 'DeviceButton', + Extends: Config.PACKAGE_VERSION.indexOf("3.10") == 0 ? PanelMenu.Button : PanelMenu.SystemStatusButton, + NUMBER_OF_FIXED_MENU_ITEMS: 4, + + _init : function(devicePath, modelId, modelName) { + this._deviceUid = devicePath.substring(devicePath.lastIndexOf('/') + 1); + this._itemMap = {}; + + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + this.parent('logitech-' + modelId); + } + else if(Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this.parent(0.0, 'logitech-' + modelId + '-symbolic'); + } + else { + this.parent('logitech-' + modelId + '-symbolic'); + } + + this._cyclingEnabled = false; + this._devicePath = devicePath; + this._itemList = new Array(); + this._modelId = modelId; + this._modelName = modelName; + this._screen = null; + if(Config.PACKAGE_VERSION.indexOf("3.4") == 0) { + this._iconActor.add_style_class_name('device-icon'); + this._iconActor.set_icon_size(20); + this._iconActor.add_style_class_name('device-button'); + } + else if (Config.PACKAGE_VERSION.indexOf("3.10") == 0) { + this._icon = new St.Icon({ + icon_name: 'logitech-' + modelId + '-symbolic', + style_class: 'device-icon', + reactive: true, + track_hover: true + }); + this._icon.set_icon_size(20); + this._icon.add_style_class_name('device-button'); + this.actor.add_actor(this._icon); + } + else { + this.mainIcon.add_style_class_name('device-icon'); + this.mainIcon.set_icon_size(20); + this.mainIcon.add_style_class_name('device-button'); + } + + // Mouse whell events + this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); + }, + + /** + * Set whether cycling is enabled for this device + * + * @param cycle enable cycling + */ + setCyclingEnabled: function(cycle) { + this._cyclingEnabled = cycle; + this.reset(); + }, + + /** + * Remove the menu item for a page given it's path. + * + * @param pagePath path of page + */ + deletePage: function(pagePath) { + let idx = this._itemList.indexOf(pagePath); + if(idx != -1) { + this._itemList.splice(idx, 1); + this._itemMap[pagePath].destroy(); + delete this._itemMap[pagePath]; + } + }, + + /** + * Clear all pages from this menu. + */ + clearPages : function() { + this._itemList = new Array(); + this.reset(); + }, + + /** + * Add a new page to the menu given it's path. + * + * @param pagePath page of page to add + */ + addPage : function(pagePath) { + this._addPage(pagePath, true); + }, + + /** + * Rebuild the entire menu. + */ + reset : function() { + this.menu.removeAll(); + this.menu.addMenuItem(new EnableDisableMenuItem(this._devicePath, this._modelName, this._screen)); + this.menu.addMenuItem(new CyclePagesMenuItem(this._cyclingEnabled, this._screen)); + this.menu.addMenuItem(new PreferencesMenuItem(this._deviceUid)); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + for (let key in this._itemList) { + this._addPage(this._itemList[key]); + } + }, + + /** + * Add the menu items for a single page given it's page. Various attributes + * about the page are read via dbus and the menu item constructed and + * added to the menu component. + * + * @param pagePath page of page. + * @param insertPagePathInItemList flag that specifies if the pagePath should be inserted in + * the _itemList. Should be set to true when adding a page + * for the first time. + */ + _addPage : function(pagePath, insertPagePathInItemList) { + let Gnome15PageProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15PageInterface); + let pageProxy = new Gnome15PageProxy(Gio.DBus.session, 'org.gnome15.Gnome15', pagePath); + pageProxy.GetTitleRemote(Lang.bind(this, function(result) { + let [title] = result; + let item = new PageMenuItem(title, title, pageProxy); + let position = this._findMenuPositionFor(item); + if(insertPagePathInItemList == true) + this._itemList.splice(position, 0, pagePath); + this._itemMap[pagePath] = item; + this.menu.addMenuItem(item, position + this.NUMBER_OF_FIXED_MENU_ITEMS); + })); + }, + + /** + * Find the position where a given menu item must be inserted in the menu so that + * all the items are alphabetically ordered. + * + * @param item item that will be inserted in the menu + */ + _findMenuPositionFor : function(item) { + let i = 0; + for(let key in this._itemList) { + let pagePath = this._itemList[key]; + if(this._itemMap[pagePath]._text > item._text) + return i; + i++; + } + return this._itemList.length; + }, + + /** + * Handle mouse wheel events by cycling pages. + * + * @param actor source of event + * @param event event + */ + _onScrollEvent: function(actor, event) { + let direction = event.get_scroll_direction(); + if(this._screen != null) { + if (direction == Clutter.ScrollDirection.DOWN) { + this._screen.CycleRemote(-1); + } + else if (direction == Clutter.ScrollDirection.UP) { + this._screen.CycleRemote(1); + } + if (direction == Clutter.ScrollDirection.LEFT) { + this._screen.CycleKeyboardRemote(-1); + } + else if (direction == Clutter.ScrollDirection.RIGHT) { + this._screen.CycleKeyboardRemote(1); + } + } + }, +}); + +/* + * GNOME Shell Extension API functions + */ + +function init() { + _log('Loading Gnome15 Gnome Shell Extension') + devices = {} + let Gnome15ServiceProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15ServiceInterface); + + /* The "Service" is the core of Gnome, so connect to it and watch for some + * signals + */ + gnome15Service = new Gnome15ServiceProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', + '/org/gnome15/Service'); + + gnome15Service.connectSignal("Started", _onDesktopServiceStarted); + gnome15Service.connectSignal("Stopping", _onDesktopServiceStopping); + gnome15Service.connectSignal("DeviceAdded", _deviceAdded); + gnome15Service.connectSignal("DeviceRemoved", _deviceRemoved); +} + +function enable() { + _log('Enabling Gnome15 Gnome Shell Extension') + dbus_watch_id = Gio.bus_watch_name(Gio.BusType.SESSION, + 'org.gnome15.Gnome15', + Gio.BusNameWatcherFlags.NONE, + _onDesktopServiceAppeared, + _onDesktopServiceVanished); + + gnome15Service.IsStartedRemote(_onStarted); +} + +function disable() { + _log('Disabling Gnome15 Gnome Shell Extension') + for(let key in devices) { + _removeDevice(key); + } + Gio.bus_unwatch_name(dbus_watch_id); +} + +/* + * Private functions + */ + +/** + * Callback invoked when the DBus name owner changes (added). We don't actually care + * about this one as we load pages on other signals + */ +function _onDesktopServiceAppeared() { +} + +/** + * Callback invoked when the DBus name owner changes (removed). This occurs + * when the service disappears, even when it dies unexpectedly. + */ +function _onDesktopServiceVanished() { + _log('Desktop service vanished'); + _onDesktopServiceStopping(); +} + +/** + * Callback invoked when the Gnome15 service starts. We get the initial device + * list at this point. + */ +function _onDesktopServiceStarted() { + _log('Desktop service started'); + gnome15Service.GetDevicesRemote(_refreshDeviceList); +} + +/** + * Invoked when the Gnome15 desktop service starts shutting down (as a result + * of user selecting "Stop Service" most probably). + */ +function _onDesktopServiceStopping() { + _log('Desktop service stopping'); + for(let key in devices) { + _removeDevice(key); + } +} + +/** + * Callback from IsStarted called during initialisation. + */ +function _onStarted(result, excp) { + /* If there was an exception (e.g. g15-desktop-service isn't running) we + return. started value is null in this case */ + if(excp) { + return; + } + + let [started] = result; + if(started) { + gnome15Service.GetDevicesRemote(_refreshDeviceList); + } +} + +/** + * Callback from GetDevicesRemote that reads the returned device list and + * creates a button for each one. + */ +function _refreshDeviceList(result) { + let [devices] = result; + for (let key in devices) { + _addDevice(devices[key]); + } +} + +/** + * Gnome15 doesn't yet send DBus events when devices are hot-plugged, but it + * soon will and this function will add new device when they appear. + * + * @param source device source (may be null) + * @param key device DBUS object path + */ +function _deviceAdded(source, senderName, args) { + let [key] = args; + _addDevice(key); +} + + +function _addDevice(key) { + _log('Added device ' + key); + devices[key] = new DeviceItem(key); +} + +/** + * Gnome15 doesn't yet send DBus events when devices are hot-plugged, but it + * soon will and this function will add new device when they are removed. + * + * @param source device source (may be null) + * @param key device DBUS object path + */ +function _deviceRemoved(source, senderName, args) { + let [key] = args; + _removeDevice(key); +} + +function _removeDevice(key) { + _log('Removed device ' + key); + devices[key].close(); + delete devices[key]; +} + +/** + * Utility for creating a org.gnome15.Screen instance given it's path. + * + * @param path + * @returns {Gnome15ScreenProxy} + */ +function _createScreen(path) { + let Gnome15ScreenProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15ScreenInterface); + return new Gnome15ScreenProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', path); +} + +/** + * Utility for creating an org.gnome15.Device instance given it's path. + * + * @param path + * @returns {Gnome15DeviceProxy} + */ +function _createDevice(path) { + let Gnome15DeviceProxy = Gio.DBusProxy.makeProxyWrapper(Gnome15DeviceInterface); + return new Gnome15DeviceProxy(Gio.DBus.session, + 'org.gnome15.Gnome15', path); +} + +/** + * Utility for logging messages + * + * @param message + */ +function _log(message) { + global.log('gnome15-gnome-shell: ' + message) +} diff --git a/src/gnome-shell-extension/icons/Makefile.am b/src/gnome-shell-extension/icons/Makefile.am new file mode 100644 index 0000000..2de0d3c --- /dev/null +++ b/src/gnome-shell-extension/icons/Makefile.am @@ -0,0 +1,17 @@ +imagesdir = $(datadir)/icons/gnome/scalable/status +images_DATA = logitech-g110-symbolic.svg \ + logitech-g11-symbolic.svg \ + logitech-g13-symbolic.svg \ + logitech-g15v2-symbolic.svg \ + logitech-g15v1-symbolic.svg \ + logitech-g19-symbolic.svg \ + logitech-g35-symbolic.svg \ + logitech-g510-symbolic.svg \ + logitech-g930-symbolic.svg \ + logitech-gamepanel-symbolic.svg \ + logitech-mx5500-symbolic.svg \ + logitech-virtual-symbolic.svg \ + logitech-z10-symbolic.svg + +EXTRA_DIST = \ + $(images_DATA) diff --git a/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg new file mode 100644 index 0000000..f45f71d --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g11-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg new file mode 100644 index 0000000..aefc9de --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g110-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg new file mode 100644 index 0000000..7a6b9a6 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g13-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg new file mode 100644 index 0000000..abe6601 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g15v1-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg new file mode 100644 index 0000000..5229499 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g15v2-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg new file mode 100644 index 0000000..acd5c75 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g19-symbolic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + diff --git a/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg new file mode 100644 index 0000000..f209ddf --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g35-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg new file mode 100644 index 0000000..4841d1f --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g510-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg b/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg new file mode 100644 index 0000000..89c1c4b --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-g930-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg b/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg new file mode 100644 index 0000000..5cf9edc --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-gamepanel-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg b/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg new file mode 100644 index 0000000..382b603 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-mx5500-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-source.svg b/src/gnome-shell-extension/icons/logitech-source.svg new file mode 100644 index 0000000..617b0d4 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-source.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + GTK + diff --git a/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg b/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg new file mode 100644 index 0000000..91599df --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-virtual-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg b/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg new file mode 100644 index 0000000..75dff68 --- /dev/null +++ b/src/gnome-shell-extension/icons/logitech-z10-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + diff --git a/src/gnome-shell-extension/metadata.json b/src/gnome-shell-extension/metadata.json new file mode 100644 index 0000000..9adead5 --- /dev/null +++ b/src/gnome-shell-extension/metadata.json @@ -0,0 +1,13 @@ +{ + "shell-version": [ + "3.4", + "3.6", + "3.8", + "3.10" + ], + "uuid": "gnome15-shell-extension@gnome15.org", + "name": "Logitech Keyboard", + "description": "Configure and control Logitech Keyboards such as G15, G19, G13 as well as other devices such as G35 headphones, Z10 speakers and moref", + "url": "http://www.russo79.com/gnome15", + "extension-id": "gnome15-shell-extension" +} diff --git a/src/gnome-shell-extension/stylesheet.css b/src/gnome-shell-extension/stylesheet.css new file mode 100644 index 0000000..f117709 --- /dev/null +++ b/src/gnome-shell-extension/stylesheet.css @@ -0,0 +1,14 @@ +.device-button { +} + +.device-icon { +} + +.helloworld-label { + font-size: 36px; + font-weight: bold; + color: #ffffff; + background-color: rgba(10,10,10,0.7); + border-radius: 5px; + padding: .5em; +} diff --git a/src/gnome15/Makefile.am b/src/gnome15/Makefile.am new file mode 100644 index 0000000..e034f80 --- /dev/null +++ b/src/gnome15/Makefile.am @@ -0,0 +1,43 @@ +SUBDIRS = drivers util +gnome15dir = $(pkgpythondir) +gnome15_PYTHON = \ + __init__.py \ + g15accounts.py \ + g15service.py \ + g15config.py \ + g15macroeditor.py \ + g15actions.py \ + g15devices.py \ + g15desktop.py \ + g15dbus.py \ + g15debug.py \ + g15dconf.py \ + g15gtk.py \ + g15driver.py \ + g15notify.py \ + g15network.py \ + g15drivermanager.py \ + g15globals.py \ + g15plugin.py \ + g15top.py \ + g15locale.py \ + g15keyboard.py \ + g15keyio.py \ + g15pluginmanager.py \ + g15profile.py \ + g15exceptions.py \ + g15screen.py \ + g15system.py \ + g15theme.py \ + g15text.py \ + g15util.py \ + g15upgrade.py \ + g15uinput.py \ + g15logging.py \ + objgraph.py \ + dbusmenu.py \ + colorpicker.py \ + lcdsink.py + +EXTRA_DIST = \ + $(gnome15_PYTHON) \ No newline at end of file diff --git a/src/gnome15/__init__.py b/src/gnome15/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/colorpicker.py b/src/gnome15/colorpicker.py new file mode 100644 index 0000000..8d7f5c3 --- /dev/null +++ b/src/gnome15/colorpicker.py @@ -0,0 +1,253 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import cairo +from gtk import gdk +import gobject +import g15globals +import util.g15convert as g15convert +import os + +COLORS_REDBLUE = [(0, 0, 0, 1), (255, 0, 0, 1), (255, 0, 255, 1), (0, 0, 255, 1) ] +COLORS_FULL = [(0, 0, 0, 1), (255, 0, 0, 1), (0, 255, 0, 1), (0, 0, 255, 1), (255, 255, 0, 1), (0, 255, 255, 1), (255, 0, 255, 1), (255, 255, 255, 1) ] +COLORS_NAMES = ["Black", "Red", "Green", "Blue", "Yellow", "Cyan", "Indigo", "White" ] + +CELL_HEIGHT = 12 +CELL_WIDTH = 24 + +def _get_color_at( buffer, x, y): + x = int(x) + y = int(y) + data = buffer.get_data() + w = buffer.get_width() + s = ( buffer.get_stride() / w ) * ( w * y + x ) + s = max(0, min(s, len(data) - 3)) + b = ord(data[s]) + g = ord(data[s + 1]) + r = ord(data[s + 2]) + return (r, g, b) + +def _rounded_rectangle(cr, x, y, w, h, r=20): + cr.move_to(x+r,y) # Move to A + cr.line_to(x+w-r,y) # Straight line to B + cr.curve_to(x+w,y,x+w,y,x+w,y+r) # Curve to C, Control points are both at Q + cr.line_to(x+w,y+h-r) # Move to D + cr.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h) # Curve to E + cr.line_to(x+r,y+h) # Line to F + cr.curve_to(x,y+h,x,y+h,x,y+h-r) # Curve to G + cr.line_to(x,y+r) # Line to H + cr.curve_to(x,y,x,y,x+r,y) # Curve to A + +class ColorPreview(gtk.DrawingArea): + + def __init__(self, picker): + self.__gobject_init__() + self.picker = picker + super(ColorPreview, self).__init__() + self.set_size_request(CELL_WIDTH, CELL_HEIGHT) + self.connect("expose-event", self._expose) + self.connect("button-press-event", self._button_press) + self.down = False + self.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK) + + def _show_redblue_picker(self, widget_tree): + main_window = widget_tree.get_object("RBPicker") + c_widget = widget_tree.get_object("RBImageEvents") + img_surface = cairo.ImageSurface.create_from_png(os.path.join(g15globals.ui_dir, 'redblue.png')) + + r_adjustment = widget_tree.get_object("RAdjustment") + r_adjustment.set_value(self.picker.color[0]) + b_adjustment = widget_tree.get_object("BAdjustment") + b_adjustment.set_value(self.picker.color[2]) + self.adjusting_rb = False + self.picker_down = False + + def _update_adj(c): + self.picker._select_color((int(r_adjustment.get_value()), \ + 0, int(b_adjustment.get_value()))) + + def _set_color(c): + r_adjustment.set_value(c[0]) + b_adjustment.set_value(c[2]) + + def _button_release(widget, event): + self.picker_down = False + + def _button_press( widget, event): + _set_color(_get_color_at(img_surface, event.x, event.y)) + self.picker_down = True + + def _mouse_motion(widget, event): + if self.picker_down: + _set_color(_get_color_at(img_surface, event.x, event.y)) + + r_adjustment.connect("value-changed", _update_adj) + b_adjustment.connect("value-changed", _update_adj) + c_widget.connect("button-press-event", _button_press) + c_widget.connect("button-release-event", _button_release) + c_widget.connect("motion-notify-event", _mouse_motion) + c_widget.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) + + main_window.set_transient_for(self.get_toplevel()) + main_window.run() + main_window.hide() + + def _show_picker(self, widget_tree): + main_window = widget_tree.get_object("RGBPicker") + c_widget = widget_tree.get_object("RGBColour") + c_widget.set_current_color(g15convert.to_color(self.picker.color)) + def colour_picked(arg): + self.picker._select_color(g15convert.color_to_rgb(c_widget.get_current_color())) + c_widget.connect("color-changed", colour_picked) + main_window.set_transient_for(self.get_toplevel()) + main_window.run() + main_window.hide() + + def _button_press(self, widget, event): + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("colorpicker") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, 'colorpicker.ui')) + if self.picker.redblue: + self._show_redblue_picker(widget_tree) + else: + self._show_picker(widget_tree) + + def _expose(self, widget, event): + size = self.size_request() + cell_height = self.allocation[3] + cell_width = self.allocation[2] + + ctx = widget.window.cairo_create() + ctx.set_line_width(1.0) + + # Draw to a back buffer so we can get the color at the point + ctx.set_source_rgb(float(self.picker.color[0]) / 255.0, float(self.picker.color[1]) / 255.0, float(self.picker.color[2]) / 255.0) + _rounded_rectangle(ctx, 0, 0, cell_width, cell_height, 16) + ctx.fill() + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgb(0.5, 0.5, 0.5) + _rounded_rectangle(ctx, 0, 0, cell_width, cell_height, 16) + ctx.stroke() + +class ColorBar(gtk.DrawingArea): + + def __init__(self, picker): + self.__gobject_init__() + super(ColorBar, self).__init__() + self.picker = picker + self.set_size_request(len(self.picker.colors) * CELL_WIDTH, CELL_HEIGHT) + self.connect("expose-event", self._expose) + self.connect("button-press-event", self._button_press) + self.connect("button-release-event", self._button_release) + self.connect("motion-notify-event", self._mouse_motion) + self.down = False + self.add_events(gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) + self.picker_image_surface = None + + def _mouse_motion(self, widget, event): + if self.picker_image_surface is not None and self.down: + self.picker._select_color(_get_color_at(self.picker_image_surface, event.x, event.y)) + + def _button_press(self, widget, event): + if self.picker_image_surface is not None: + self.picker._select_color(_get_color_at(self.picker_image_surface, event.x, event.y)) + self.down = True + + def _button_release(self, widget, event): + self.down = False + + def _do_small_bar(self, ctx, cell_height, cell_width, c): + ctx.set_source_rgb(float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0) + ctx.rectangle(0, 0, cell_width, cell_height) + ctx.fill() + ctx.translate(cell_width, 0) + + def _do_bar(self, ctx, cell_height, cell_width, p, c): + ctx.set_source_rgb(float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0) + lg1 = cairo.LinearGradient(0.0, 0.0, cell_width, 0) + lg1.add_color_stop_rgba(0.0, float(p[0]) / 255.0, float(p[1]) / 255.0, float(p[2]) / 255.0, float(p[3])) + lg1.add_color_stop_rgba(0.5, float(c[0]) / 255.0, float(c[1]) / 255.0, float(c[2]) / 255.0, float(c[3])) + ctx.rectangle(0, 0, cell_width, cell_height) + ctx.set_source(lg1) + ctx.fill() + ctx.translate(cell_width, 0) + + def _expose(self, widget, event): + cr = widget.window.cairo_create() + cr.set_line_width(1.0) + size = (self.allocation[2],self.allocation[3]) + cell_height = size[1] + tc = len(self.picker.colors) + cell_width = size[0] / tc + main_width = cell_width * tc + + # Draw to a back buffer so we can get the color at the point + picker_image_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + ctx = cairo.Context(picker_image_surface) + ctx.save() + ctx.translate(1, 1) + colors = self.picker.colors + lc = colors[0] + self._do_small_bar(ctx, cell_height, cell_width, lc) + for i in range(0, len(colors) - 1): + c = colors[i] + self._do_bar(ctx, cell_height, cell_width, c, colors[i + 1]) + self._do_small_bar(ctx, cell_height, cell_width / 2, lc) + ctx.restore() + _rounded_rectangle(ctx, 0, 0, main_width, cell_height, 16) + ctx.set_operator(cairo.OPERATOR_DEST_IN) + ctx.fill() + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgb(0.5, 0.5, 0.5) + _rounded_rectangle(ctx, 0, 0, main_width, cell_height, 16) + ctx.stroke() + + # Paint + cr.set_source_surface(picker_image_surface) + cr.paint() + self.picker_image_surface = picker_image_surface + +class ColorPicker(gtk.HBox): + + def __init__(self, colors = None, redblue = False): + self.__gobject_init__() + gtk.HBox.__init__(self, spacing = 8) + self.colors = colors if colors is not None else ( COLORS_REDBLUE if redblue else COLORS_FULL ) + self.redblue = redblue + self.color = (0,0,0) + super(ColorPicker, self).__init__() + + bar = ColorBar(self) + preview = ColorPreview(self) + + self.pack_start(bar, True, True) + self.pack_start(preview, False, True) + + def set_color(self, color): + self.color = color + self.queue_draw() + + def _select_color(self, color): + self.color = color + self.queue_draw() + self.emit("color-chosen") + +gobject.type_register(ColorPicker) +gobject.type_register(ColorBar) +gobject.type_register(ColorPreview) +gobject.signal_new("color-chosen", ColorPicker, gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ()) \ No newline at end of file diff --git a/src/gnome15/dbusmenu.py b/src/gnome15/dbusmenu.py new file mode 100644 index 0000000..91406d9 --- /dev/null +++ b/src/gnome15/dbusmenu.py @@ -0,0 +1,198 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import dbus + +from lxml import etree +import time +import logging +logger = logging.getLogger(__name__) + + +''' +DBUSMenu property names +''' +VISIBLE = "visible" +ICON_NAME = "icon-name" +TYPE = "type" +LABEL = "label" +TOGGLE_TYPE = "toggle-type" +TOGGLE_STATE = "toggle-state" +ENABLED = "enabled" + +TYPE_SEPARATOR = "separator" +TYPE_ROOT = "root" + +TOGGLE_TYPE_NONE = "none" +TOGGLE_TYPE_RADIO = "radio" + +class DBUSMenuEntry(): + def __init__(self, id, properties, menu): + self.id = id + self.menu = menu + self.set_properties(properties) + self.children = [] + + def set_properties(self, properties): + self.properties = properties + if not VISIBLE in self.properties: + self.properties[VISIBLE] = True + self.label = self.properties[LABEL] if LABEL in self.properties else None + self.icon = None + self.type = self.properties[TYPE] if TYPE in self.properties else TYPE_ROOT + self.toggle_type = self.properties[TOGGLE_TYPE] if TOGGLE_TYPE in self.properties else TOGGLE_TYPE_NONE + self.toggle_state = self.properties[TOGGLE_STATE] if TOGGLE_STATE in self.properties else 0 + self.enabled = not ENABLED in self.properties or self.properties[ENABLED] + + def flatten(self, include_self = False): + flat_list = [] + if include_self: + self._flatten(self, flat_list) + else: + for c in self.children: + self._flatten(c, flat_list) + return flat_list + + def about_to_show(self): + return self.menu.dbus_menu.AboutToShow(self.id) + + def activate(self, variant = 0): + self.menu.dbus_menu.Event(self.id, "clicked", variant, int(time.time())) + + def hover(self, variant = 0): + self.menu.dbus_menu.Event(self.id, "hovered", variant, int(time.time())) + + def _flatten(self, element, flat_list): + flat_list.append(element) + for c in element.children: + self._flatten(c, flat_list) + + def is_visible(self): + return VISIBLE in self.properties and self.properties[VISIBLE] + + def get_label(self): + return self.label + + def get_icon(self): + return self.icon + + def get_alt_label(self): + return "" + + def get_icon_name(self): + return self.properties[ICON_NAME] if ICON_NAME in self.properties else None + +class DBUSMenu(): + + def __init__(self, session_bus, object_name, path, interface, on_change = None, natty = False): + self.natty = natty + self.session_bus = session_bus + self.on_change = on_change + self.messages_menu = self.session_bus.get_object(object_name, path) + self.dbus_menu = dbus.Interface(self.messages_menu, interface) + + self.dbus_menu.connect_to_signal("ItemUpdated", self._item_updated) + self.dbus_menu.connect_to_signal("ItemPropertyUpdated", self._item_property_updated) + self.dbus_menu.connect_to_signal("LayoutUpdated", self._layout_updated) + self.dbus_menu.connect_to_signal("ItemActivationRequested", self._item_activation_requested) + + # From Natty onwards + if self.natty: + self.dbus_menu.connect_to_signal("ItemsPropertiesUpdated", self._item_properties_updated) + + self._get_layout() + + def create_entry(self, id, properties): + return DBUSMenuEntry(id, properties, self) + + ''' + Private + ''' + + def _item_activation_requested(self, id, timestamp): + logger.warning("TODO - implement item activation request for %s on %d", id, timestamp) + + def _layout_updated(self, revision, parent): + self._get_layout() + if self.on_change != None: + self.on_change() + + def _item_updated(self, id): + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + menu.set_properties(self.dbus_menu.GetProperties(id, [])) + if self.on_change != None: + self.on_change(menu) + else: + logger.warning("Update request for item not in map") + + def _item_properties_updated(self, updated_properties, removed_properties): + for id, properties in updated_properties: + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + for prop in properties: + value = properties[prop] + if not prop in menu.properties or value != menu.properties[prop]: + menu.properties[prop] = value + menu.set_properties(menu.properties) + if self.on_change != None: + self.on_change(menu, prop, value) + else: + logger.warning("Update request for item not in map") + +# for id, properties in removed_properties: +# print "Removed: ",str(id),str(properties) + + def _item_property_updated(self, id, prop, value): + if str(id) in self.menu_map: + menu = self.menu_map[str(id)] + if not prop in menu.properties or value != menu.properties[prop]: + menu.properties[prop] = value + menu.set_properties(menu.properties) + if self.on_change != None: + self.on_change(menu, prop, value) + else: + logger.warning("Update request for item not in map") + + def _get_layout(self): + self.menu_map = {} + if self.natty: + revision, layout = self.dbus_menu.GetLayout(0, 3, []) + self.root_item = self._load_menu_struct(layout, self.menu_map) + else: + revision, menu_xml = self.dbus_menu.GetLayout(0) + self.root_item = self._load_xml_menu(etree.fromstring(menu_xml), self.menu_map) + + def _load_menu_struct(self, layout, map): + id = layout[0] + properties = layout[1] + menu = self.create_entry(id, dict(properties)) + map[str(id)] = menu + children = layout[2] + for item in children: + menu.children.append(self._load_menu_struct(item, map)) + return menu + + def _load_xml_menu(self, element, map): + id = int(element.get("id")) + menu = self.create_entry(id, dict(self.dbus_menu.GetProperties(id, []))) + map[str(id)] = menu + for child in element: + try : + menu.children.append(self._load_xml_menu(child, map)) + except dbus.DBUSException as e: + logger.warning("Failed to get child menu.", exc_info = e) + return menu diff --git a/src/gnome15/drivers/Makefile.am b/src/gnome15/drivers/Makefile.am new file mode 100644 index 0000000..cecd0d3 --- /dev/null +++ b/src/gnome15/drivers/Makefile.am @@ -0,0 +1,28 @@ +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = driver_kernel.py fb.py +endif +if ENABLE_DRIVER_G19DIRECT + MAYBE_G19DIRECT = driver_g19direct.py +endif +if ENABLE_DRIVER_G15DIRECT + MAYBE_G15DIRECT = driver_g15direct.py pylibg15.py +endif +if ENABLE_DRIVER_G930 + MAYBE_G930 = driver_g930.py +endif + +driversdir = $(pkgpythondir)/drivers + +drivers_PYTHON = __init__.py \ + driver_gtk.py \ + $(MAYBE_KERNEL) $(MAYBE_G19DIRECT) $(MAYBE_G15DIRECT) $(MAYBE_G930) + +EXTRA_DIST = __init__.py \ + driver_g15direct.py \ + driver_g930.py \ + driver_gtk.py \ + driver_kernel.py \ + fb.py \ + pylibg15.py + + \ No newline at end of file diff --git a/src/gnome15/drivers/__init__.py b/src/gnome15/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/drivers/driver_g15direct.py b/src/gnome15/drivers/driver_g15direct.py new file mode 100644 index 0000000..f410def --- /dev/null +++ b/src/gnome15/drivers/driver_g15direct.py @@ -0,0 +1,691 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Alternative implementation of a G19 Driver that uses pylibg19 to communicate directly +with the keyboard +""" +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from threading import RLock +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.g15uinput as g15uinput +import gnome15.g15exceptions as g15exceptions +import sys +import os +import gconf +import gtk +import logging +from PIL import ImageMath +from PIL import Image +import array +logger = logging.getLogger(__name__) +load_error = None +try : + import pylibg15 +except Exception as a: + logger.debug("Could not import pylibg15 module", exc_info = a) + load_error = a + +# Import from local version of pylibg19 if available +if g15globals.dev: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "pylibg19")) + +# Driver information (used by driver selection UI) +name=_("G15 Direct") +id="g15direct" +description=_("For use with the G15 based devices only, this driver communicates directly, " + \ + "with the keyboard and so is more efficient than the g15daemon driver. Note, " + \ + "you will have to ensure the permissions of the USB devices are read/write " + \ + "for your user.") +has_preferences=True + + +DEBUG_LIBG15="DEBUG_LIBG15" in os.environ +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + g15driver.G_KEY_G13 : 1<<12, + g15driver.G_KEY_G14 : 1<<13, + g15driver.G_KEY_G15 : 1<<14, + g15driver.G_KEY_G16 : 1<<15, + g15driver.G_KEY_G17 : 1<<16, + g15driver.G_KEY_G18 : 1<<17, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 +} + +EXT_KEY_MAP = { + g15driver.G_KEY_G19 : 1<<0, + g15driver.G_KEY_G20 : 1<<1, + g15driver.G_KEY_G21 : 1<<2, + g15driver.G_KEY_G22 : 1<<3, + + g15driver.G_KEY_JOY_LEFT : 1<<4, + g15driver.G_KEY_JOY_DOWN : 1<<5, + g15driver.G_KEY_JOY_CENTER : 1<<6, + g15driver.G_KEY_JOY : 1<<7 + } + +REVERSE_KEY_MAP = {} +for k in KEY_MAP.keys(): + REVERSE_KEY_MAP[KEY_MAP[k]] = k +EXT_REVERSE_KEY_MAP = {} +for k in EXT_KEY_MAP.keys(): + EXT_REVERSE_KEY_MAP[EXT_KEY_MAP[k]] = k + +mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +color_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +red_blue_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) +backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +lcd_backlight_control = g15driver.Control("lcd_backlight", _("LCD Backlight Level"), 2, 0, 2, hint = g15driver.HINT_SHADEABLE) +lcd_contrast_control = g15driver.Control("lcd_contrast", _("LCD Contrast"), 22, 0, 2) +invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +controls = { + g15driver.MODEL_G11 : [ mkeys_control, backlight_control ], + g15driver.MODEL_G15_V1 : [ mkeys_control, backlight_control, lcd_contrast_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G15_V2 : [ mkeys_control, backlight_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G13 : [ mkeys_control, color_backlight_control, invert_control ], + g15driver.MODEL_G510 : [ mkeys_control, color_backlight_control, invert_control ], + g15driver.MODEL_Z10 : [ backlight_control, lcd_backlight_control, invert_control ], + g15driver.MODEL_G110 : [ mkeys_control, red_blue_backlight_control ], + } + +# Default offsets +ANALOGUE_OFFSET = 20 +DIGITAL_OFFSET = 64 + +def show_preferences(device, parent, gconf_client): + prefs = G15DirectDriverPreferences(device, parent, gconf_client) + prefs.run() + +class G15DirectDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.gconf_client = gconf_client + self.device = device + + g15locale.get_translation("driver_g15direct") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_g15direct") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g15direct.ui")) + self.window = widget_tree.get_object("G15DirectDriverSettings") + self.window.set_transient_for(parent) + + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/timeout" % device.uid, + "Timeout", + 10000, + widget_tree, + False) + if not device.model_id == g15driver.MODEL_G13: + widget_tree.get_object("JoyModeCombo").destroy() + widget_tree.get_object("JoyModeLabel").destroy() + widget_tree.get_object("Offset").destroy() + widget_tree.get_object("OffsetLabel").destroy() + widget_tree.get_object("OffsetDescription").destroy() + else: + g15uigconf.configure_combo_from_gconf(gconf_client, + "/apps/gnome15/%s/joymode" % device.uid, + "JoyModeCombo", + "macro", + widget_tree) + # We have separate offset values for digital / analogue, + # so swap between them based on configuration + self.offset_widget = widget_tree.get_object("Offset") + self._set_offset_depending_on_mode(None) + widget_tree.get_object("JoyModeCombo").connect("changed", + self._set_offset_depending_on_mode) + self.offset_widget.connect("value-changed", self._spinner_changed) + + def run(self): + self.window.run() + self.window.hide() + + def _set_offset_depending_on_mode(self, widget): + mode = self.gconf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + offset_model = self.offset_widget.get_adjustment() + if mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE]: + val = g15gconf.get_int_or_default(self.gconf_client, + "/apps/gnome15/%s/analogue_offset" % self.device.uid, + ANALOGUE_OFFSET) + else: + val = g15gconf.get_int_or_default(self.gconf_client, + "/apps/gnome15/%s/digital_offset" % self.device.uid, + DIGITAL_OFFSET) + offset_model.set_value(val) + + def _spinner_changed(self, widget): + mode = self.gconf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + if mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE]: + self.gconf_client.set_int("/apps/gnome15/%s/analogue_offset" % self.device.uid, + int(widget.get_value())) + else: + self.gconf_client.set_int("/apps/gnome15/%s/digital_offset" % self.device.uid, + int(widget.get_value())) + +def fix_sans_style(root): + for element in root.iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + if load_error is not None: + raise load_error + g15driver.AbstractDriver.__init__(self, "g15direct") + self.on_close = on_close + self.device = device + self.timer = None + self.joy_mode = None + self.lock = RLock() + self.down = [] + self.move_x = 0 + self.move_y = 0 + self.connected = False + self.conf_client = gconf.client_get_default() + self.last_keys = None + self.last_ext_keys = None + + # We can only have one instance of this driver active in a single runtime + self.allow_multiple = False + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return controls[self.device.model_id] + + def get_key_layout(self): + if self.get_model_name() == g15driver.MODEL_G13 and "macro" == self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid): + """ + This driver with the G13 supports some additional keys + """ + l = list(self.device.key_layout) + l.append([ g15driver.G_KEY_UP ]) + l.append([ g15driver.G_KEY_JOY_LEFT, g15driver.G_KEY_LEFT, g15driver.G_KEY_JOY_CENTER, g15driver.G_KEY_RIGHT ]) + l.append([ g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_DOWN ]) + return l + else: + return self.device.key_layout + + def get_action_keys(self): + return self.device.action_keys + + def process_svg(self, document): + fix_sans_style(document.getroot()) + + def on_update_control(self, control): + self.lock.acquire() + try : + self._do_update_control(control) + finally: + self.lock.release() + + def get_name(self): + return _("G15 Direct") + + def get_model_names(self): + return [ g15driver.MODEL_G11, g15driver.MODEL_G15_V1, \ + g15driver.MODEL_G15_V2, g15driver.MODEL_G110, \ + g15driver.MODEL_G510, g15driver.MODEL_Z10, \ + g15driver.MODEL_G13 ] + + def get_model_name(self): + return self.device.model_id + + def grab_keyboard(self, callback): + self.callback = callback + self.last_keys = None + self.last_ext_keys = None + self.thread = pylibg15.grab_keyboard(self._handle_key_event, \ + g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/usb_key_read_timeout", 100), + self._on_error) + self.thread.on_unplug = self._keyboard_unplugged + + def is_connected(self): + return self.connected + + def paint(self, img): + if not self.is_connected(): + return + + # Just return if the device has no LCD + if self.device.bpp == 0: + return None + + self.lock.acquire() + try : + size = self.get_size() + + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Convert image buffer to string + buf = "" + for x in list(pil_img.getdata()): + buf += chr(x) + + if len(buf) != self.device.lcd_size[0] * self.device.lcd_size[1]: + logger.warning("Invalid buffer size") + else: + # TODO Replace with C routine + arrbuf = array.array('B', self.empty_buf) + width, height = self.get_size() + max_byte_offset = 0 + for x in range(0, width): + for y in range(0, height): + pixel_offset = y * width + x; + byte_offset = pixel_offset / 8; + max_byte_offset = max(max_byte_offset, byte_offset) + bit_offset = 7-(pixel_offset % 8); + val = ord(buf[x+(y*160)]) + pv = arrbuf[byte_offset] + if val > 0: + arrbuf[byte_offset] = pv | 1 << bit_offset + else: + arrbuf[byte_offset] = pv & ~(1 << bit_offset) + buf = arrbuf.tostring() + try : + logger.debug("Writing buffer of %d bytes", len(buf)) + pylibg15.write_pixmap(buf) + except IOError as e: + logger.error("Failed to send buffer.", exc_info = e) + self.disconnect() + finally: + self.lock.release() + + """ + Private + """ + def _on_error(self, code): + logger.info("Disconnected due to error %d", code) + self.disconnect() + + def _on_connect(self): + self.thread = None + self.callback = None + self.notify_handles = [] + + # Create an empty string buffer for use with monochrome LCD + self.empty_buf = "" + for _ in range(0, 861): + self.empty_buf += chr(0) + + # TODO Enable UINPUT if multimedia key support is required? + self.timeout = 10000 + e = self.conf_client.get("/apps/gnome15/%s/timeout" % self.device.uid) + if e: + self.timeout = e.get_int() + + logger.info("Initialising pylibg15, looking for %s:%s", + hex(self.device.controls_usb_id[0]), + hex(self.device.controls_usb_id[1])) + if DEBUG_LIBG15 or ( logger.level < logging.WARN and logger.level != logging.NOTSET ): + pylibg15.set_debug(pylibg15.G15_LOG_INFO) + err = pylibg15.init(False, self.device.controls_usb_id[0], self.device.controls_usb_id[1]) + if err != pylibg15.G15_NO_ERROR: + raise g15exceptions.NotConnectedException("libg15 returned error %d " % err) + logger.info("Initialised pylibg15") + self.connected = True + + for control in self.get_controls(): + self._do_update_control(control) + + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/joymode" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/timeout" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/digital_offset" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/analogue_offset" % self.device.uid, self._config_changed, None)) + + self._load_configuration() + + def _load_configuration(self): + self.joy_mode = self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + self.digital_calibration = g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/%s/digital_offset" % self.device.uid, 63) + self.analogue_calibration = g15gconf.get_int_or_default(self.conf_client, "/apps/gnome15/%s/analogue_offset" % self.device.uid, 20) + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + + def _on_disconnect(self): + if self.is_connected(): + for h in self.notify_handles: + self.conf_client.notify_remove(h) + logger.info("Exiting pylibg15") + self.connected = False + if self.thread is not None: + self.thread.on_exit = pylibg15.exit + self.thread.deactivate() + else: + pylibg15.exit() + if self.on_close != None: + self.on_close(self) + else: + raise Exception("Not connected") + + def _keyboard_unplugged(self): + logger.info("Keyboard has been unplugged.") + self.disconnect() + + def _get_g510_multimedia_keys(self, code): + keys = [] + code &= ~(1<<28) + if code & 1 << 0 != 0: + keys.append(g15uinput.KEY_PLAYPAUSE) + elif code & 1 << 1 != 0: + keys.append(g15uinput.KEY_STOPCD) + elif code & 1 << 2 != 0: + keys.append(g15uinput.KEY_PREVIOUSSONG) + elif code & 1 << 3 != 0: + keys.append(g15uinput.KEY_NEXTSONG) + elif code & 1 << 4 != 0: + keys.append(g15uinput.KEY_MUTE) + elif code & 1 << 5 != 0: + keys.append(g15uinput.KEY_VOLUMEUP) + elif code & 1 << 6 != 0: + keys.append(g15uinput.KEY_VOLUMEDOWN) + elif code & 1 << 7 != 0: + # Hmm .. whats the proper mute key? There doesn't appear to be + # a headset mute, perhaps we should expose as a macro key + keys.append(g15uinput.KEY_SOUND) + elif code & 1 << 8 != 0: + keys.append(g15uinput.KEY_MICMUTE) + return keys + + def _convert_ext_g15daemon_code(self, code): + keys = [] + for key in EXT_REVERSE_KEY_MAP: + if code & key != 0: + keys.append(EXT_REVERSE_KEY_MAP[key]) + return keys + + def _convert_from_g15daemon_code(self, code): + keys = [] + for key in REVERSE_KEY_MAP: + if code & key != 0: + keys.append(REVERSE_KEY_MAP[key]) + return keys + + + def _handle_key_event(self, code, ext_code): + if not self.is_connected() or self.disconnecting: + return + logger.debug("Key code %d", code) + + has_js = ext_code & EXT_KEY_MAP[g15driver.G_KEY_JOY] > 0 + if has_js: + ext_code -= EXT_KEY_MAP[g15driver.G_KEY_JOY] + + this_keys = [] if code == 0 else self._convert_from_g15daemon_code(code) + if ext_code > 0 and self.get_model_name() == g15driver.MODEL_G510: + this_keys += self._get_g510_multimedia_keys(ext_code) + elif ext_code > 0: + this_keys += self._convert_ext_g15daemon_code(ext_code) + + if self.get_model_name() == g15driver.MODEL_G13: + c = self.analogue_calibration if self.joy_mode in [ g15uinput.JOYSTICK, g15uinput.MOUSE ] else self.digital_calibration + + low_val = g15uinput.JOYSTICK_CENTER - c + high_val = g15uinput.JOYSTICK_CENTER + c + max_step = 5 + + pos = pylibg15.get_joystick_position() + """ + The device itself gives us joystick position values between 0 and 255. + The center is at 128. + The virtual joysticks are set to give values between -127 and 127. + The center is at 0. + So we adapt the received values. + """ + pos = (pos[0] - g15uinput.DEVICE_JOYSTICK_CENTER, + pos[1] - g15uinput.DEVICE_JOYSTICK_CENTER) + + logger.debug("Joystick at %s", str(pos)) + + if self.joy_mode == g15uinput.JOYSTICK: + if has_js: + self._abs_joystick(this_keys, pos) + elif self.joy_mode == g15uinput.DIGITAL_JOYSTICK: + if has_js: + self._digital_joystick(this_keys, pos, low_val, high_val) + elif self.joy_mode == g15uinput.MOUSE: + if has_js: + self._rel_mouse(this_keys, pos, low_val, high_val, max_step) + else: + self._emit_macro_keys(this_keys, pos, low_val, high_val) + + up = [] + down = [] + + last_keys = self.last_keys + + for k in this_keys: + if last_keys is None or not k in last_keys: + down.append(k) + + """ + This is a work around for the G510. Sometimes the key up + events are lost, leaving keys stuck down in Gnome15. Instead, + we just react on key down and ignore the key up if one + actually occurs. + """ + # Work around for the G510 and the volume wheel missing key up events and audio input events + if isinstance(k, tuple) and self.get_model_name() == g15driver.MODEL_G510: + up.append(k) + this_keys.remove(k) + + if last_keys is not None: + for k in last_keys: + if not k in this_keys and not k in down and not k in up: + up.append(k) + + if ( ext_code > 0 ) and self.get_model_name() == g15driver.MODEL_G510: + self._do_macro_keys(self._filter_macro_keys(down), self._filter_macro_keys(up)) + self._do_uinput_keys(self._filter_uinput_keys(down), self._filter_uinput_keys(up)) + else: + self._do_macro_keys(down, up) + + self.last_keys = this_keys + + def has_joystick_key(self, keys): + for k in keys: + if k in [ g15driver.G_KEY_JOY, g15driver.G_KEY_JOY_CENTER, \ + g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_JOY_LEFT ]: + return True + + def _do_uinput_keys(self, down, up): + if len(down) > 0: + for uinput_code in down: + g15uinput.emit(g15uinput.KEYBOARD, uinput_code, 1, False) + g15uinput.syn(g15uinput.KEYBOARD) + if len(up) > 0: + for uinput_code in up: + g15uinput.emit(g15uinput.KEYBOARD, uinput_code, 0, False) + g15uinput.syn(g15uinput.KEYBOARD) + + def _do_macro_keys(self, down, up): + if len(down) > 0: + self.callback(down, g15driver.KEY_STATE_DOWN) + if len(up) > 0: + self.callback(up, g15driver.KEY_STATE_UP) + + def _filter_macro_keys(self, keys): + m = [] + for c in keys: + if isinstance(c, str): + m.append(c) + return m + + def _filter_uinput_keys(self, keys): + m = [] + for c in keys: + if isinstance(c, tuple): + m.append(c) + return m + + def _emit_macro_keys(self, this_keys, pos, low_val, high_val): + if pos[0] < low_val: + this_keys.append(g15driver.G_KEY_LEFT) + elif pos[0] > high_val: + this_keys.append(g15driver.G_KEY_RIGHT) + if pos[1] < low_val: + this_keys.append(g15driver.G_KEY_UP) + elif pos[1] > high_val: + this_keys.append(g15driver.G_KEY_DOWN) + + def _check_js_buttons(self, joystick_type, this_keys): + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_LEFT, g15uinput.BTN_1) + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_DOWN, g15uinput.BTN_2) + self._check_buttons(joystick_type, this_keys, g15driver.G_KEY_JOY_CENTER, g15uinput.BTN_3) + + def _check_mouse_buttons(self, this_keys): + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_LEFT, g15uinput.BTN_MOUSE) + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_DOWN, g15uinput.BTN_RIGHT) + self._check_buttons(g15uinput.MOUSE, this_keys, g15driver.G_KEY_JOY_CENTER, g15uinput.BTN_MIDDLE) + + def _rel_mouse(self, this_keys, pos, low_val, high_val, max_step): + self._check_mouse_buttons(this_keys) + + relx = 0 + rely = 0 + + if pos[0] < low_val: + relx = ( low_val - pos[0] ) * -1 + elif pos[0] > high_val: + relx = pos[0] - high_val + if pos[1] < low_val: + rely = ( low_val - pos[1] ) * -1 + elif pos[1] > high_val: + rely = pos[1] - high_val + + relx = -max_step if relx < -max_step else ( max_step if relx > max_step else relx) + rely = -max_step if rely < -max_step else ( max_step if rely > max_step else rely) + + self.move_x = relx + self.move_y = rely + if relx != 0 or rely != 0: + self._mouse_move() + else: + if self.timer is not None: + self.timer.cancel() + + def _abs_joystick(self, this_keys, pos): + self._check_js_buttons(g15uinput.JOYSTICK, this_keys) + g15uinput.emit(g15uinput.JOYSTICK, g15uinput.ABS_X, pos[0], syn=False) + g15uinput.emit(g15uinput.JOYSTICK, g15uinput.ABS_Y, pos[1]) + + def _digital_joystick(self, this_keys, pos, low_val, high_val): + self._check_js_buttons(g15uinput.DIGITAL_JOYSTICK, this_keys) + pos_x = g15uinput.JOYSTICK_CENTER + pos_y = g15uinput.JOYSTICK_CENTER + + if pos[0] < low_val: + pos_x = g15uinput.JOYSTICK_MIN + elif pos[0] > high_val: + pos_x = g15uinput.JOYSTICK_MAX + if pos[1] < low_val: + pos_y = g15uinput.JOYSTICK_MIN + elif pos[1] > high_val: + pos_y = g15uinput.JOYSTICK_MAX + + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, g15uinput.ABS_X, pos_x, syn=False) + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, g15uinput.ABS_Y, pos_y) + + def _check_buttons(self, target, this_keys, key, button): + if key in this_keys: + this_keys.remove(key) + if not key in self.down: + g15uinput.emit(target, button, 1) + self.down.append(key) + elif key in self.down: + g15uinput.emit(target, button, 0) + self.down.remove(key) + + def _mouse_move(self): + if self.move_x != 0 or self.move_y != 0: + if self.move_x != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_X, self.move_x) + if self.move_y != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_Y, self.move_y) + self.timer = g15scheduler.schedule("MouseMove", 0.05, self._mouse_move) + + def _do_update_control(self, control): + level = control.value + logger.debug("Updating control %s to %s", str(control.id), str(control.value)) + if control.id == backlight_control.id: + self.check_control(control) + pylibg15.set_keyboard_brightness(level) + elif control.id == lcd_backlight_control.id: + self.check_control(control) + pylibg15.set_lcd_brightness(level) + elif control.id == lcd_contrast_control.id: + self.check_control(control) + pylibg15.set_contrast(level) + elif control.id == color_backlight_control.id or control.id == red_blue_backlight_control.id: + pylibg15.set_keyboard_color(level) + elif control.id == mkeys_control.id: + pylibg15.set_leds(level) \ No newline at end of file diff --git a/src/gnome15/drivers/driver_g19direct.py b/src/gnome15/drivers/driver_g19direct.py new file mode 100644 index 0000000..5da0116 --- /dev/null +++ b/src/gnome15/drivers/driver_g19direct.py @@ -0,0 +1,367 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Alternative implementation of a G19 Driver that uses pylibg19 to communicate directly +with the keyboard +""" +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from cStringIO import StringIO +from threading import RLock +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15exceptions as g15exceptions +import sys +import os +import gconf +import gtk +import usb +import logging +import array +logger = logging.getLogger(__name__) + +# Import from local version of pylibg19 if available +if g15globals.dev: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "pylibg19")) + +from g19.g19 import G19 + + + +# Driver information (used by driver selection UI) +name=_("G19 Direct") +id="g19direct" +description=_("For use with the Logitech G19 only, this driver communicates directly, " + \ + "with the keyboard and so is more efficient than the G19D driver. Note, " + \ + "you will have to ensure the permissions of the USB devices are read/write " + \ + "for your user.") +has_preferences=True + +MAX_X=320 +MAX_Y=240 + +CLIENT_CMD_KB_BACKLIGHT = "BL" + +KEY_MAP = { + 0: g15driver.G_KEY_LIGHT, + 1: g15driver.G_KEY_M1, + 2: g15driver.G_KEY_M2, + 3: g15driver.G_KEY_M3, + 4: g15driver.G_KEY_MR, + 5: g15driver.G_KEY_G1, + 6: g15driver.G_KEY_G2, + 7: g15driver.G_KEY_G3, + 8: g15driver.G_KEY_G4, + 9: g15driver.G_KEY_G5, + 10: g15driver.G_KEY_G6, + 11: g15driver.G_KEY_G7, + 12: g15driver.G_KEY_G8, + 13: g15driver.G_KEY_G9, + 14: g15driver.G_KEY_G10, + 15: g15driver.G_KEY_G11, + 16: g15driver.G_KEY_G12, + 17: g15driver.G_KEY_BACK, + 18: g15driver.G_KEY_DOWN, + 19: g15driver.G_KEY_LEFT, + 20: g15driver.G_KEY_MENU, + 21: g15driver.G_KEY_OK, + 22: g15driver.G_KEY_RIGHT, + 23: g15driver.G_KEY_SETTINGS, + 24: g15driver.G_KEY_UP, + 25: g15driver.G_KEY_WINKEY_SWITCH, + 26: g15driver.G_KEY_NEXT, + 27: g15driver.G_KEY_PREV, + 28: g15driver.G_KEY_STOP, + 29: g15driver.G_KEY_PLAY, + 30: g15driver.G_KEY_MUTE, + 31: g15driver.G_KEY_VOL_UP, + 32: g15driver.G_KEY_VOL_DOWN + } + + +# Controls +mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +default_keyboard_backlight_control = g15driver.Control("default_backlight_colour", _("Boot Keyboard Backlight Colour"), (0, 255, 0)) +lcd_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint = g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint = g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) +controls = [ mkeys_control, keyboard_backlight_control, default_keyboard_backlight_control, lcd_brightness_control, foreground_control, background_control, highlight_control ] + +def show_preferences(device, parent, gconf_client): + prefs = G19DriverPreferences(device, parent, gconf_client) + prefs.run() + +class G19DriverPreferences(): + + def __init__(self, device, parent, gconf_client): + g15locale.get_translation("driver_g19direct") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_g19direct") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g19direct.ui")) + self.window = widget_tree.get_object("G19DirectDriverSettings") + self.window.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, + "/apps/gnome15/%s/reset_usb" % device.uid, + "Reset", + False, + widget_tree, + True) + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/timeout" % device.uid, + "Timeout", + 10000, + widget_tree, + False) + g15uigconf.configure_spinner_from_gconf(gconf_client, + "/apps/gnome15/%s/reset_wait" % device.uid, + "ResetWait", + 0, + widget_tree, + False) + + def run(self): + self.window.run() + self.window.hide() + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "g19direct") + self.on_close = on_close + self.device = device + self.lock = RLock() + self.connected = False + self.conf_client = gconf.client_get_default() + + def get_antialias(self): + return cairo.ANTIALIAS_SUBPIXEL + + def get_size(self): + return (MAX_X, MAX_Y) + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return controls + + def get_key_layout(self): + return self.device.key_layout + + def get_action_keys(self): + return self.device.action_keys + + def process_svg(self, document): + pass + + def on_update_control(self, control): + self.lock.acquire() + try : + self._do_update_control(control) + finally: + self.lock.release() + + def get_name(self): + return name + + def get_model_names(self): + return [ g15driver.MODEL_G19 ] + + def get_model_name(self): + return self.device.model_id + + def grab_keyboard(self, callback): + self.callback = callback + self.lg19.start_event_handling() + + def is_connected(self): + return self.connected + + def paint(self, img): + if not self.is_connected(): + return + + width = img.get_width() + height = img.get_height() + + # Create a new flipped, rotated image. The G19 expects the image to scan vertically, but + # the cairo image surface will be horizontal. Rotating then flipping the image is the + # quickest way to convert this. 16 bit color (5-6-5) is also required. Unfortunately this format + # was disabled for a long time, as was only re-enabled in version 1.8.6. + try: + back_surface = cairo.ImageSurface (4, height, width) + except Exception as e: + logger.debug('Could not create ImageSurface. Trying earlier API.', exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, height, width) + + back_context = cairo.Context (back_surface) + g15cairo.rotate_around_center(back_context, width, height, 270) + g15cairo.flip_horizontal(back_context, width, height) + back_context.set_source_surface(img, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(self._rgb_to_uint16(r, g, b)) + buf = array.array('B', file_str.getvalue()) + else: + buf = array.array('B', str(back_surface.get_data())) + + expected_size = MAX_X * MAX_Y * ( self.get_bpp() / 8 ) + if len(buf) != expected_size: + logger.warning("Invalid buffer size, expected %d, got %d", expected_size, len(buf)) + else: + try: + self.lg19.send_frame(buf) + except usb.USBError as e: + logger.debug("Failed to send buffer.", exc_info = e) + self._on_receive_error(e) + + def process_input(self, event): + if self.callback == None: + logger.debug("Ignoring key input, keyboard not grabbed") + return + + keys_down = event.keysDown + keys_up = event.keysUp + + logger.debug("Processing input, keys_down = %d, keys_up = %d", + len(keys_down), + len(keys_up)) + + if len(keys_up) > 0: + c = [] + for key in keys_up: + c.append(KEY_MAP[key]) + self.callback(c, g15driver.KEY_STATE_UP) + + if len(keys_down) > 0: + c = [] + for key in keys_down: + c.append(KEY_MAP[key]) + self.callback(c, g15driver.KEY_STATE_DOWN) + + """ + Private + """ + def _on_connect(self): + # Detect what version of pyusb we are using + pyusb = self._get_usb_lib_version() + + self.callback = None + + reset = self.conf_client.get_bool("/apps/gnome15/%s/reset_usb" % self.device.uid) + timeout = 10000 + reset_wait = 0 + e = self.conf_client.get("/apps/gnome15/%s/timeout" % self.device.uid) + if e: + timeout = e.get_int() + e = self.conf_client.get("/apps/gnome15/%s/reset_wait" % self.device.uid) + if e: + reset_wait = e.get_int() + if reset and pyusb == 1: + logger.warning("Using pyusb 1.0. Resetting device causes crash in this version, no reset will be done") + reset = False + + try: + self.lg19 = G19(reset, False, timeout, reset_wait) + self.connected = True + except usb.USBError as e: + logger.error("Failed to connect.", exc_info = e) + raise g15exceptions.NotConnectedException() + + # Start listening for keys + self.lg19.add_input_processor(self) + + for control in self.get_controls(): + self._do_update_control(control) + + def _on_disconnect(self): + if self.is_connected(): + self.lg19.close() + self.connected = False + if self.on_close != None: + self.on_close(self) + else: + raise Exception("Not connected") + + def _on_receive_error(self, exception): + if self.is_connected(): + self.disconnect() + + def _get_usb_lib_version(self): + try: + import usb.core + return 1 + except Exception as e: + logger.debug('pyusb version 1 not available.', exc_info = e) + return 0 + + def _set_mkey_lights(self, lights): + val = 0 + if lights & g15driver.MKEY_LIGHT_1 != 0: + val += 0x80 + if lights & g15driver.MKEY_LIGHT_2 != 0: + val += 0x40 + if lights & g15driver.MKEY_LIGHT_3 != 0: + val += 0x20 + if lights & g15driver.MKEY_LIGHT_MR != 0: + val += 0x10 + self.lg19.set_enabled_m_keys(val) + + def _do_update_control(self, control): + try: + if control == keyboard_backlight_control: + self.lg19.set_bg_color(control.value[0], control.value[1], control.value[2]) + elif control == default_keyboard_backlight_control: + self.lg19.save_default_bg_color(control.value[0], control.value[1], control.value[2]) + elif control == lcd_brightness_control: + self.lg19.set_display_brightness(control.value) + elif control == mkeys_control: + self._set_mkey_lights(control.value) + except usb.USBError as e: + logger.debug('Error updating control.', exc_info = e) + self._on_receive_error(e) + + def _rgb_to_uint16(self, r, g, b): + rBits = r * 32 / 255 + gBits = g * 64 / 255 + bBits = b * 32 / 255 + + rBits = rBits if rBits <= 31 else 31 + gBits = gBits if gBits <= 63 else 63 + bBits = bBits if bBits <= 31 else 31 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + + return chr(valueL & 0xff) + chr(valueH & 0xff) diff --git a/src/gnome15/drivers/driver_g930.py b/src/gnome15/drivers/driver_g930.py new file mode 100644 index 0000000..0ad9fa1 --- /dev/null +++ b/src/gnome15/drivers/driver_g930.py @@ -0,0 +1,291 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from threading import Thread +from pyinputevent.pyinputevent import SimpleDevice + +import select +import pyinputevent.scancodes as S +import gnome15.g15driver as g15driver +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15globals as g15globals +import gnome15.g15uinput as g15uinput +import gconf +import fcntl +import os +import gtk +import cairo +import re +import usb + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id = "g930" +name = _("G930 Driver") +description = _("Simple driver that supports the keys on the G930/G35 headset. ") +has_preferences = True + + +""" +This dictionaries map the default codes emitted by the input system to the +Gnome15 codes. +""" +g930_key_map = { + S.KEY_PREVIOUSSONG : g15driver.G_KEY_G1, + S.KEY_PLAYPAUSE : g15driver.G_KEY_G2, + S.KEY_NEXTSONG : g15driver.G_KEY_G3, + S.KEY_MUTE : g15driver.G_KEY_MUTE, + S.KEY_VOLUMEDOWN : g15driver.G_KEY_VOL_DOWN, + S.KEY_VOLUMEUP : g15driver.G_KEY_VOL_UP + } + +# Other constants +EVIOCGRAB = 0x40044590 + +def show_preferences(device, parent, gconf_client): + prefs = G930DriverPreferences(device, parent, gconf_client) + prefs.run() + +class G930DriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.device = device + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g930.ui")) + self.window = widget_tree.get_object("G930DriverSettings") + self.window.set_transient_for(parent) + + self.grab_multimedia = widget_tree.get_object("GrabMultimedia") + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/%s/grab_multimedia" % device.uid, "GrabMultimedia", False, widget_tree) + + def run(self): + self.window.run() + self.window.hide() + +class KeyboardReceiveThread(Thread): + def __init__(self, device): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread-%s" % device.uid + self.setDaemon(True) + self.devices = [] + + def deactivate(self): + self._run = False + for dev in self.devices: + logger.info("Ungrabbing %d", dev.fileno()) + try : + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 0) + except Exception as e: + logger.info("Failed ungrab.", exc_info = e) + logger.info("Closing %d", dev.fileno()) + try : + self.fds[dev.fileno()].close() + except Exception as e: + logger.info("Failed close.", exc_info = e) + logger.info("Stopped %d", dev.fileno()) + logger.info("Stopped all input devices") + + def run(self): + self.poll = select.poll() + self.fds = {} + for dev in self.devices: + self.poll.register(dev, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 1) + self.fds[dev.fileno()] = dev + while self._run: + for x, e in self.poll.poll(1000): + dev = self.fds[x] + try : + if dev: + dev.read() + except OSError as e: + logger.debug('Could not read device file.', exc_info = e) + # Ignore this error if deactivated + if self._run: + raise e + logger.info("Thread left") + + +''' +Abstract input device +''' +class AbstractInputDevice(SimpleDevice): + def __init__(self, callback, key_map, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.callback = callback + self.key_map = key_map + + def _event(self, event_code, state): + if event_code in self.key_map: + key = self.key_map[event_code] + self.callback([key], state) + else: + logger.warning("Unmapped key for event: %s", event_code) + +''' +SimpleDevice implementation for handling multi-media keys. +''' +class MultiMediaDevice(AbstractInputDevice): + def __init__(self, grab_multimedia, callback, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, g930_key_map, *args, **kwargs) + self._grab_multimedia = grab_multimedia + + def receive(self, event): + if event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + return + elif event.etype == 4 and event.evalue == 786666: + # Hack for Volume down on G930 + if not self._grab_multimedia: + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEDOWN, 1, True) + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEDOWN, 0, True) + elif event.etype == 4 and event.evalue == 786665: + # Hack for Volume down on G930 + if not self._grab_multimedia: + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEUP, 1, True) + g15uinput.emit(g15uinput.KEYBOARD, g15uinput.KEY_VOLUMEUP, 0, True) + else: + logger.warning("Unhandled event: %s", str(event)) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close=None): + g15driver.AbstractDriver.__init__(self, "g510") + self.notify_handles = [] + self.on_close = on_close + self.key_thread = None + self.device = device + self.connected = False + self.conf_client = gconf.client_get_default() + self._init_device() + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/grab_multimedia" % self.device.uid, self._config_changed, None)) + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def is_connected(self): + return self.connected + + def get_model_names(self): + return [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + + def get_name(self): + return "Gnome15 G930/G35 Driver" + + def get_model_name(self): + return self.device.model_id if self.device != None else None + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + if self.grab_multimedia: + l = list(self.device.key_layout) + l.append([]) + l.append([ g15driver.G_KEY_VOL_UP, g15driver.G_KEY_VOL_DOWN, g15driver.G_KEY_MUTE ]) + return l + else: + return self.device.key_layout + + def _load_configuration(self): + self.grab_multimedia = self.conf_client.get_bool("/apps/gnome15/%s/grab_multimedia" % self.device.uid) + + def _config_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return [] + + def paint(self, img): + pass + + def on_update_control(self, control): + pass + + def grab_keyboard(self, callback): + if self.key_thread != None: + raise Exception("Keyboard already grabbed") + + self.key_thread = KeyboardReceiveThread(self.device) + for devpath in self.mm_devices: + logger.info("Adding input multi-media device %s", devpath) + self.key_thread.devices.append(MultiMediaDevice(self.grab_multimedia, callback, devpath, devpath)) + + self.key_thread.start() + + ''' + Private + ''' + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Not connected") + self._stop_receiving_keys() + if self.on_close != None: + g15scheduler.schedule("Close", 0, self.on_close, self) + + def _on_connect(self): + self.notify_handles = [] + self._init_driver() + if not self.device: + raise usb.USBError("No supported logitech headphones found on USB bus") + if self.device == None: + raise usb.USBError("WARNING: Found no " + self.model + " Logitech headphone, Giving up") + + def _reload_and_reconnect(self): + self._load_configuration() + if self.is_connected(): + self.disconnect() + + def _stop_receiving_keys(self): + if self.key_thread != None: + self.key_thread.deactivate() + self.key_thread = None + + def _init_device(self): + self._load_configuration() + self.device_name = None + + def _init_driver(self): + self._init_device() + self.mm_devices = [] + dir_path = "/dev/input/by-id" + for p in os.listdir(dir_path): + # TODO - not sure about the G35 - feedback needed + if re.search(r"usb-Logitech_Logitech_G930_Headset-event-if.*", p) or re.search(r"usb-Logitech_Logitech_G35_Headset-event-if.*", p): + logger.info("Input multi-media device %s matches", p) + self.mm_devices.append(dir_path + "/" + p) + + def __del__(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) \ No newline at end of file diff --git a/src/gnome15/drivers/driver_gtk.py b/src/gnome15/drivers/driver_gtk.py new file mode 100644 index 0000000..6fcd337 --- /dev/null +++ b/src/gnome15/drivers/driver_gtk.py @@ -0,0 +1,428 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15globals as g15globals + +import gconf + +import os +import gtk.gdk +import gobject +import cairo + +from PIL import Image +from PIL import ImageMath +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id="gtk" +name=_("GTK Virtual Keyboard Driver") +description=_("A special development driver that emulates all supported, " + \ + "models as a window on your desktop. This allows " + \ + "you to develop plugins without having access to a real Logitech hardward ") +has_preferences=True + +# Controls + +g19_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +g19_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +g19_lcd_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +g19_foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint = g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +g19_background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint = g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +g19_highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) + +g15_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +g15_backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) +g15_invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +g110_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint = g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) + +controls = { + g15driver.MODEL_G11 : [ g15_mkeys_control, g15_backlight_control ], + g15driver.MODEL_G19 : [ g19_mkeys_control, g19_keyboard_backlight_control, g19_lcd_brightness_control, g19_foreground_control, g19_background_control, g19_highlight_control ], + g15driver.MODEL_G15_V1 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G15_V2 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G13 : [ g15_mkeys_control, g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G510 : [ g15_mkeys_control, g19_keyboard_backlight_control, g15_invert_control ], + g15driver.MODEL_Z10 : [ g15_backlight_control, g15_invert_control ], + g15driver.MODEL_GAMEPANEL : [ g15_backlight_control, g15_invert_control ], + g15driver.MODEL_G110 : [ g19_mkeys_control, g110_keyboard_backlight_control ], + g15driver.MODEL_MX5500 : [ g15_invert_control ], + g15driver.MODEL_G930 : [ ], + g15driver.MODEL_G35 : [ ], + } + +def show_preferences(device, parent, gconf_client): + if device.model_id != 'virtual': + return + + prefs = GtkDriverPreferences(device, parent, gconf_client) + prefs.run() + +class GtkDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + g15locale.get_translation("driver_gtk") + widget_tree = gtk.Builder() + widget_tree.set_translation_domain("driver_gtk") + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_gtk.ui")) + self.window = widget_tree.get_object("GtkDriverSettings") + self.window.set_transient_for(parent) + + mode_model = widget_tree.get_object("ModeModel") + mode_model.clear() + for mode in g15driver.MODELS: + mode_model.append([mode]) + g15uigconf.configure_combo_from_gconf(gconf_client, + "/apps/gnome15/%s/gtk_mode" % device.uid, + "ModeCombo", + g15driver.MODEL_G19, + widget_tree) + + def run(self): + self.window.run() + self.window.hide() + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "gtk") + self.lights = 0 + self.main_window = None + self.connected = False + self.bpp = device.bpp + self.lcd_size = device.lcd_size + self.callback = None + self.action_keys = None + self.device = device + self.area = None + self.image = None + self.buttons = {} + self.event_box = None + self.on_close = on_close + self.conf_client = gconf.client_get_default() + self.notify_handle = self.conf_client.notify_add("/apps/gnome15/%s/gtk_mode" % self.device.uid, self.config_changed) + self._init_driver() + + def get_antialias(self): + if self.mode == g15driver.MODEL_G19: + return cairo.ANTIALIAS_DEFAULT + else: + return cairo.ANTIALIAS_NONE + + def config_changed(self, client, connection_id, entry, args): + self._init_driver() + if self.on_driver_options_change: + self.on_driver_options_change() + + def is_connected(self): + return self.connected + + def get_model_names(self): + return [ 'virtual' ] + + def get_name(self): + return _("GTK Keyboard Emulator Driver") + + def get_model_name(self): + return self.mode + + def get_action_keys(self): + return self.action_keys + + def get_key_layout(self): + return self.key_layout + + def get_zoomed_size(self): + zoom = self.get_zoom() + return ( self.lcd_size[0] * zoom, self.lcd_size[1] * zoom ) + + def get_zoom(self): + if self.bpp == 16: + return 1 + else: + return 3 + + def get_size(self): + return self.lcd_size + + def get_bpp(self): + return self.bpp + + def get_controls(self): + return self.controls + + def paint(self, image): + + if self.bpp != 0: + width = self.lcd_size[0] + height = self.lcd_size[1] + + if self.bpp == 1: + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(image) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", self.lcd_size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control and invert_control.value == 1: + pil_img = pil_img.point(lambda i: 1^i) + + # Create drawable message + pil_img = pil_img.convert("RGB") + self.image = pil_img + else: + # Take a copy of the image to prevent flickering + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(image) + argb_context.paint() + self.image = argb_surface + gobject.timeout_add(0, self.redraw) + + def process_svg(self, document): + if self.bpp == 1: + for element in document.getroot().iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + + def redraw(self): + if self.image != None and self.main_window is not None: + if isinstance(self.image, cairo.Surface): + self._draw_surface() + else: + self._draw_pixbuf() + self.area.queue_draw() + + def on_update_control(self, control): + gobject.idle_add(self._do_update_control, control) + + def grab_keyboard(self, callback): + self.callback = callback; + + ''' + Private + ''' + def _on_connect(self): + self._init_driver() + logger.info("Starting GTK driver") + gobject.idle_add(self._init_ui) + + def _on_disconnect(self): + logger.info("Disconnecting GTK driver") + if not self.is_connected(): + raise Exception("Not connected") + self.connected = False + if self.on_close != None: + self.on_close(self, retry=False) + gobject.idle_add(self._close_window) + + def _simulate_key(self, widget, key, state): + if self.callback != None: + keys = [] + keys.append(key) + self.callback(keys, state) + + def _do_update_control(self, control): + if self.connected: + if control == self.get_control_for_hint(g15driver.HINT_MKEYS): + self._do_set_mkey_lights() + elif control == self.get_control_for_hint(g15driver.HINT_DIMMABLE): + if isinstance(control.value, int): + v = ( 65535 / control.upper ) * control.value + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(v, v, v)) + else: + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(control.value[0] << 8, control.value[1] << 8, control.value[2] << 8)) + + def _window_closed(self, window, evt): + if self.main_window != None: + self.conf_client.set_bool("/apps/gnome15/%s/enabled" % self.device.uid, False) + + def _do_set_mkey_lights(self): + c = self.get_control_for_hint(g15driver.HINT_MKEYS) + if c is not None and c.value is not None: + if g15driver.G_KEY_M1 in self.buttons: + self._modify_button(g15driver.G_KEY_M1, c.value, g15driver.MKEY_LIGHT_1) + if g15driver.G_KEY_M2 in self.buttons: + self._modify_button(g15driver.G_KEY_M2, c.value, g15driver.MKEY_LIGHT_2) + if g15driver.G_KEY_M3 in self.buttons: + self._modify_button(g15driver.G_KEY_M3, c.value, g15driver.MKEY_LIGHT_3) + if g15driver.G_KEY_MR in self.buttons: + self._modify_button(g15driver.G_KEY_MR, c.value, g15driver.MKEY_LIGHT_MR) + + def _modify_button(self, id, lights, mask): + on = lights & mask != 0 + c = self.buttons[id] + key_text = " ".join(g15driver.get_key_names(list(id))) + c.set_label("*%s" % key_text if on else "%s" % key_text) + + def _close_window(self): + if self.main_window != None: + w = self.main_window + self.main_window = None + w.hide() + w.destroy() + self.area = None + + def _mode_changed(self, client, connection_id, entry, args): + if self.is_connected(): + gobject.idle_add(self.disconnect) + else: + logger.warning("Mode change would cause disconnect when already connected. %s", + str(entry)) + + def _draw_surface(self): + # Finally paint the Cairo surface on the GTK widget + zoom = self.get_zoom() + width = self.lcd_size[0] + height = self.lcd_size[1] + if self.area != None and self.area.window != None: + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, zoom * width, zoom * height) + context = cairo.Context(surface) + context.set_antialias(self.get_antialias()) + context.scale(zoom, zoom) + context.set_source_surface(self.image) + context.paint() + self.area.set_surface(surface) + + def _draw_pixbuf(self): + width = self.lcd_size[0] + height = self.lcd_size[1] + zoom = self.get_zoom() + pixbuf = g15cairo.image_to_pixbuf(self.image) + pixbuf = pixbuf.scale_simple(zoom * width, zoom * height, 0) + if self.area != None: + self.area.set_pixbuf(pixbuf) + + def _init_driver(self): + logger.info("Initialising GTK driver") + if self.device.model_id == 'virtual': + self.mode = self.conf_client.get_string("/apps/gnome15/%s/gtk_mode" % self.device.uid) + else: + self.mode = self.device.model_id + if self.mode == None or self.mode == "": + self.mode = g15driver.MODEL_G19 + logger.info("Mode is now %s", self.mode) + self.controls = controls[self.mode] + import gnome15.g15devices as g15devices + device_info = g15devices.get_device_info(self.mode) + self.bpp = device_info.bpp + self.action_keys = device_info.action_keys + self.lcd_size = device_info.lcd_size + self.key_layout = device_info.key_layout + logger.info("Initialised GTK driver") + + def _init_ui(self): + logger.info("Initialising GTK UI") + self.area = VirtualLCD(self) + #self.area.connect("expose_event", self._expose) + self.hboxes = [] + self.buttons = {} + zoomed_size = self.get_zoomed_size() + self.area.set_size_request(zoomed_size[0], zoomed_size[1]) + self.vbox = gtk.VBox () + self.vbox.add(self.area) + rows = gtk.VBox() + for row in self.get_key_layout(): + hbox = gtk.HBox() + for key in row: + key_text = " ".join(g15driver.get_key_names(list(key))) + g_button = gtk.Button(key_text) + g_button.connect("pressed", self._simulate_key, key, g15driver.KEY_STATE_DOWN) + g_button.connect("released", self._simulate_key, key, g15driver.KEY_STATE_UP) + hbox.add(g_button) + self.buttons[key] = g_button + rows.add(hbox) + + self.event_box = gtk.EventBox() + self.event_box.add(rows) + self.vbox.add(self.event_box) + + self.main_window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.main_window.set_title("Gnome15") + self.main_window.set_icon_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15")) + self.main_window.add(self.vbox) + self.main_window.connect("delete-event", self._window_closed) + + control = self.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control: + if isinstance(control.value, int): + v = ( 65535 / control.upper ) * control.value + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(v, v, v)) + else: + self.event_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(control.value[0] << 8, control.value[1] << 8, control.value[2] << 8)) + + self.main_window.show_all() + logger.info("Initialised GTK UI") + self.connected = True + logger.info("Connected") + + def __del__(self): + self.conf_client.notify_remove(self.notify_handle) + +class VirtualLCD(gtk.DrawingArea): + + def __init__(self, driver): + self.__gobject_init__() + self.driver = driver + self.set_double_buffered(True) + super(VirtualLCD, self).__init__() + self.connect("expose-event", self._expose) + self.buffer = None + + def _expose(self, widget, event): + if not self.driver.is_connected(): + return + cr = widget.window.cairo_create() + cr.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + cr.clip() + + # Paint + if self.buffer: + cr.set_source_surface(self.buffer) + cr.paint() + + def set_pixbuf(self, pixbuf): + self.buffer = g15cairo.pixbuf_to_surface(pixbuf) + + def set_surface(self, surface): + self.buffer = surface +# self.window.begin_paint_rect((0, 0, zoom * width, zoom * height)) +# context = self.window.cairo_create() +# context.set_source_surface(surface) +# context.paint() +# self.window.end_paint() + +gobject.type_register(VirtualLCD) + diff --git a/src/gnome15/drivers/driver_kernel.py b/src/gnome15/drivers/driver_kernel.py new file mode 100644 index 0000000..dfd6212 --- /dev/null +++ b/src/gnome15/drivers/driver_kernel.py @@ -0,0 +1,1432 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15-drivers").ugettext + +from cStringIO import StringIO +from pyinputevent.uinput import UInputDevice +from pyinputevent.pyinputevent import InputEvent, SimpleDevice +from pyinputevent.keytrans import * +from threading import Thread + +import select +import pyinputevent.scancodes as S +import gnome15.g15driver as g15driver +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15globals as g15globals +import gnome15.g15uinput as g15uinput +import gconf +import fcntl +import os +import gtk +import cairo +import re +import usb +import fb +from PIL import Image +from PIL import ImageMath +import array +import struct +import dbus +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +id = "kernel" +name = _("Kernel Drivers") +description = _("Requires ali123's Logitech Kernel drivers. This method requires no other \ +daemons to be running, and works with the G13, G15, G19 and G110 keyboards. ") +has_preferences = True + + +""" +This dictionaries map the default codes emitted by the input system to the +Gnome15 codes. +""" +g19_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_MENU : g15driver.G_KEY_MENU, + S.KEY_UP : g15driver.G_KEY_UP, + S.KEY_DOWN : g15driver.G_KEY_DOWN, + S.KEY_LEFT : g15driver.G_KEY_LEFT, + S.KEY_RIGHT : g15driver.G_KEY_RIGHT, + S.KEY_OK : g15driver.G_KEY_OK, + S.KEY_BACK : g15driver.G_KEY_BACK, + S.KEY_FORWARD : g15driver.G_KEY_SETTINGS, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_MUTE : g15driver.G_KEY_MUTE, + S.KEY_VOLUMEDOWN : g15driver.G_KEY_VOL_DOWN, + S.KEY_VOLUMEUP : g15driver.G_KEY_VOL_UP, + S.KEY_NEXTSONG : g15driver.G_KEY_NEXT, + S.KEY_PREVIOUSSONG : g15driver.G_KEY_PREV, + S.KEY_PLAYPAUSE : g15driver.G_KEY_PLAY, + S.KEY_STOPCD : g15driver.G_KEY_STOP, + } +g15_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18 + } + +g15v2_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6 + } +g13_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18, + S.KEY_F19 : g15driver.G_KEY_G19, + S.KEY_F20 : g15driver.G_KEY_G20, + S.KEY_F21 : g15driver.G_KEY_G21, + S.KEY_F22 : g15driver.G_KEY_G22, + S.BTN_X: g15driver.G_KEY_JOY_LEFT, + S.BTN_Y: g15driver.G_KEY_JOY_DOWN, + S.BTN_Z: g15driver.G_KEY_JOY_CENTER, + } +g110_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12 + } +g510_key_map = { + S.KEY_PROG1 : g15driver.G_KEY_M1, + S.KEY_PROG2 : g15driver.G_KEY_M2, + S.KEY_PROG3 : g15driver.G_KEY_M3, + S.KEY_RECORD : g15driver.G_KEY_MR, + S.KEY_OK : g15driver.G_KEY_L1, + S.KEY_LEFT : g15driver.G_KEY_L2, + S.KEY_UP : g15driver.G_KEY_L3, + S.KEY_DOWN : g15driver.G_KEY_L4, + S.KEY_RIGHT : g15driver.G_KEY_L5, + 228 : g15driver.G_KEY_LIGHT, + S.KEY_F1 : g15driver.G_KEY_G1, + S.KEY_F2 : g15driver.G_KEY_G2, + S.KEY_F3 : g15driver.G_KEY_G3, + S.KEY_F4 : g15driver.G_KEY_G4, + S.KEY_F5 : g15driver.G_KEY_G5, + S.KEY_F6 : g15driver.G_KEY_G6, + S.KEY_F7 : g15driver.G_KEY_G7, + S.KEY_F8 : g15driver.G_KEY_G8, + S.KEY_F9 : g15driver.G_KEY_G9, + S.KEY_F10 : g15driver.G_KEY_G10, + S.KEY_F11 : g15driver.G_KEY_G11, + S.KEY_F12 : g15driver.G_KEY_G12, + S.KEY_F13 : g15driver.G_KEY_G13, + S.KEY_F14 : g15driver.G_KEY_G14, + S.KEY_F15 : g15driver.G_KEY_G15, + S.KEY_F16 : g15driver.G_KEY_G16, + S.KEY_F17 : g15driver.G_KEY_G17, + S.KEY_F18 : g15driver.G_KEY_G18 + } + +g19_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 0, 0, 15, hint=g15driver.HINT_MKEYS) +g19_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (0, 255, 0), hint=g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE) + +g19_brightness_control = g15driver.Control("lcd_brightness", _("LCD Brightness"), 100, 0, 100, hint = g15driver.HINT_SHADEABLE) +g19_foreground_control = g15driver.Control("foreground", _("Default LCD Foreground"), (255, 255, 255), hint=g15driver.HINT_FOREGROUND | g15driver.HINT_VIRTUAL) +g19_background_control = g15driver.Control("background", _("Default LCD Background"), (0, 0, 0), hint=g15driver.HINT_BACKGROUND | g15driver.HINT_VIRTUAL) +g19_highlight_control = g15driver.Control("highlight", _("Default Highlight Color"), (255, 0, 0), hint=g15driver.HINT_HIGHLIGHT | g15driver.HINT_VIRTUAL) +g19_controls = [ g19_brightness_control, g19_keyboard_backlight_control, g19_foreground_control, g19_background_control, g19_highlight_control, g19_mkeys_control ] + +g110_keyboard_backlight_control = g15driver.Control("backlight_colour", _("Keyboard Backlight Colour"), (255, 0, 0), hint=g15driver.HINT_DIMMABLE | g15driver.HINT_SHADEABLE | g15driver.HINT_RED_BLUE_LED) +g110_controls = [ g110_keyboard_backlight_control, g19_mkeys_control ] + +g15_mkeys_control = g15driver.Control("mkeys", _("Memory Bank Keys"), 1, 0, 15, hint=g15driver.HINT_MKEYS) +g15_backlight_control = g15driver.Control("keyboard_backlight", _("Keyboard Backlight Level"), 2, 0, 2, hint=g15driver.HINT_DIMMABLE) +g15_lcd_backlight_control = g15driver.Control("lcd_backlight", _("LCD Backlight"), 2, 0, 2, g15driver.HINT_SHADEABLE) +g15_lcd_contrast_control = g15driver.Control("lcd_contrast", _("LCD Contrast"), 22, 0, 48, 0) +g15_invert_control = g15driver.Control("invert_lcd", _("Invert LCD"), 0, 0, 1, hint=g15driver.HINT_SWITCH | g15driver.HINT_VIRTUAL) +g15_controls = [ g15_mkeys_control, g15_backlight_control, g15_invert_control, g15_lcd_backlight_control, g15_lcd_contrast_control ] +g11_controls = [ g15_mkeys_control, g15_backlight_control ] +g13_controls = [ g19_keyboard_backlight_control, g15_mkeys_control, g15_invert_control, g15_mkeys_control ] + +""" +Keymaps that are sent to the kernel driver. These are the codes the driver +will emit. + +""" +K_KEYMAPS = { + g15driver.MODEL_G19: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_PROG1, + 0x000D : S.KEY_PROG2, + 0x000E : S.KEY_PROG3, + 0x000F : S.KEY_RECORD, + 0x0013 : 228, + 0x0018 : S.KEY_FORWARD, + 0x0019 : S.KEY_BACK, + 0x001A : S.KEY_MENU, + 0x001B : S.KEY_OK, + 0x001C : S.KEY_RIGHT, + 0x001D : S.KEY_LEFT, + 0x001E : S.KEY_DOWN, + 0x001F : S.KEY_UP + }, + g15driver.MODEL_G15_V1: { + 0x00 : S.KEY_F1, + 0x02 : S.KEY_F13, + 0x07 : 228, + 0x08 : S.KEY_F7, + 0x09 : S.KEY_F2, + 0x0b : S.KEY_F14, + 0x0f : S.KEY_LEFT, + 0x11 : S.KEY_F8, + 0x12 : S.KEY_F3, + 0x14 : S.KEY_F15, + 0x17 : S.KEY_UP, + 0x1a : S.KEY_F9, + 0x1b : S.KEY_F4, + 0x1d : S.KEY_F16, + 0x1f : S.KEY_DOWN, + 0x23 : S.KEY_F10, + 0x24 : S.KEY_F5, + 0x26 : S.KEY_F17, + 0x27 : S.KEY_RIGHT, + 0x28 : S.KEY_PROG1, + 0x2c : S.KEY_F11, + 0x2d : S.KEY_F6, + 0x31 : S.KEY_PROG2, + 0x35 : S.KEY_F12, + 0x36 : S.KEY_RECORD, + 0x3a : S.KEY_PROG3, + 0x3e : S.KEY_F18, + 0x3f : S.KEY_OK + }, + g15driver.MODEL_G15_V2: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_PROG1, + 0x0007 : S.KEY_PROG2, + 0x0008 : 228, + 0x0009 : S.KEY_LEFT, + 0x000a : S.KEY_UP, + 0x000b : S.KEY_DOWN, + 0x000c : S.KEY_RIGHT, + 0x000d : S.KEY_PROG3, + 0x000e : S.KEY_RECORD, + 0x000f : S.KEY_OK, + }, + g15driver.MODEL_G13: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_F13, + 0x000D : S.KEY_F14, + 0x000E : S.KEY_F15, + 0x000F : S.KEY_F16, + 0x0010 : S.KEY_F17, + 0x0011 : S.KEY_F18, + 0x0012 : S.KEY_F19, + 0x0013 : S.KEY_F20, + 0x0014 : S.KEY_F21, + 0x0015 : S.KEY_F22, + 0x0016 : S.KEY_OK, + 0x0017 : S.KEY_LEFT, + 0x0018 : S.KEY_UP, + 0x0019 : S.KEY_DOWN, + 0x001A : S.KEY_RIGHT, + 0x001B : S.KEY_PROG1, + 0x001C : S.KEY_PROG2, + 0x001D : S.KEY_PROG3, + 0x001E : S.KEY_RECORD, + 0x001F : S.BTN_X, + 0x0020 : S.BTN_Y, + 0x0021 : S.BTN_Z, + 0x0022 : 228, + }, + g15driver.MODEL_G110: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_PROG1, + 0x000D : S.KEY_PROG2, + 0x000E : S.KEY_PROG3, + 0x000F : S.KEY_RECORD, + 0x0010 : 228, + }, + g15driver.MODEL_G510: { + 0x0000 : S.KEY_F1, + 0x0001 : S.KEY_F2, + 0x0002 : S.KEY_F3, + 0x0003 : S.KEY_F4, + 0x0004 : S.KEY_F5, + 0x0005 : S.KEY_F6, + 0x0006 : S.KEY_F7, + 0x0007 : S.KEY_F8, + + 0x0008 : S.KEY_F9, + 0x0009 : S.KEY_F10, + 0x000A : S.KEY_F11, + 0x000B : S.KEY_F12, + 0x000C : S.KEY_F13, + 0x000D : S.KEY_F14, + 0x000E : S.KEY_F15, + 0x000F : S.KEY_F16, + + 0x0010 : S.KEY_F17, + 0x0011 : S.KEY_F18, + 0x0013 : 228, + 0x0014 : S.KEY_PROG1, + 0x0015 : S.KEY_PROG2, + 0x0016 : S.KEY_PROG3, + 0x0017 : S.KEY_RECORD, + + 0x0018 : S.KEY_OK, + 0x0019 : S.KEY_LEFT, + 0x001A : S.KEY_UP, + 0x001B : S.KEY_DOWN, + 0x001C : S.KEY_RIGHT + + }, + } + +# from https://chromium.googlesource.com/chromiumos/third_party/autotest/+/master/client/bin/input/linux_ioctl.py + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRMASK = ((1 << _IOC_NRBITS) - 1) +_IOC_TYPEMASK = ((1 << _IOC_TYPEBITS) - 1) +_IOC_SIZEMASK = ((1 << _IOC_SIZEBITS) - 1) +_IOC_DIRMASK = ((1 << _IOC_DIRBITS) - 1) + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS) +_IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS) +_IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS) + +IOC_NONE = 0 +IOC_WRITE = 1 +IOC_READ = 2 + +# Return the byte size of a python struct format string +def sizeof(t): + return struct.calcsize(t) + +def IOC(d, t, nr, size): + return ((d << _IOC_DIRSHIFT) | (ord(t) << _IOC_TYPESHIFT) | + (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT)) + +# used to create numbers +def IO(t, nr, t_format): + return IOC(IOC_NONE, t, nr, 0) + +def IOW(t, nr, t_format): + return IOC(IOC_WRITE, t, nr, sizeof(t_format)) + +def IOR(t, nr, t_format): + return IOC(IOC_READ, t, nr, sizeof(t_format)) + +# from https://chromium.googlesource.com/chromiumos/third_party/autotest/+/master/client/bin/input/linux_input.py + +# struct input_keymap_entry { +# __u8 flags; +# __u8 len; +# __u16 index; +# __u32 keycode; +# __u8 scancode[32]; +# }; +input_keymap_entry_t = 'BBHI32B' +input_keymap_entry_scancode_offset = 'BBHI' + +EVIOCGKEYCODE = IOR('E', 0x04, '2I') # get keycode +EVIOCGKEYCODE_V2 = IOR('E', 0x04, input_keymap_entry_t) +EVIOCSKEYCODE = IOW('E', 0x04, '2I') # set keycode +EVIOCSKEYCODE_V2 = IOW('E', 0x04, input_keymap_entry_t) + + +class DeviceInfo: + def __init__(self, leds, controls, key_map, led_prefix, keydev_pattern, sink_pattern = None, mm_pattern = None): + + """ + This object describes the device specific details this driver needs to know. Mainly + the file names that are created by the kernel driver and the maps for converting the + uinput keys codes from the driver into Gnome15 key code. + + Keyword arguments: + leds -- a list containing the files names of the 'Memory' keys (M1, M2, M3 and MR). + These names match files in /sys/class/leds, prefixed with the value of + led_prefix (see below) + controls -- a list g15driver.Control objects supported by this device + key_map -- a dictionary of UINPUT -> Gnome15 key codes + led_prefix -- Each LED file in /sys/class/leds is prefixed by this short model name. + keydev_pattern -- A regular expression that matches the filename of the 'if01' device in + /dev/input/by-id. 'G' Keys, 'M' Keys and 'D' Pad Keys are read from this device. + sink_pattern -- Optional. When specified, is a regular expression that matches the filename + of the 'keyboard' device in /dev/input/by-id. When specified, the driver + will open the device and just ignore any events from it. This fixes the problem + of 'F' keys being emitted when keys are pressed. + mm_pattern -- Optional. When specified, is a regular expression that matches the filename + of the 'multimedia keys' device in /dev/input/by-id. When specified, the driver + can open the device to intercept the multimedia keys and interpret them as + Gnome15 macro keys. + """ + + self.leds = leds + self.controls = controls + self.key_map = key_map + self.led_prefix = led_prefix + self.sink_pattern = sink_pattern + self.keydev_pattern = keydev_pattern + self.mm_pattern = mm_pattern + +""" +This dictionary keeps all the device specific details this +drivers needs to know. The key is the model code, the +value is a DeviceInfo object that contains the actual +details +""" +device_info = { + g15driver.MODEL_G19: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g19_controls, g19_key_map, "g19", + r"usb-Logitech_G19_Gaming_Keyboard-event-if.*", + r"usb-Logitech_G19_Gaming_Keyboard-.*event-kbd.*", + r"usb-046d_G19_Gaming_Keyboard-event-if.*"), + + g15driver.MODEL_G11: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "blue:mr" ], + g11_controls, g15_key_map, "g15", + r"G15_Keyboard_G15.*if"), + + g15driver.MODEL_G15_V1: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "blue:mr" ], + g15_controls, g15_key_map, "g15", + r"G15_Keyboard_G15.*if", + r"G15_Keyboard_G15.*kbd", + r"usb-Logitech_Logitech_Gaming_Keyboard-event-if.*"), + + g15driver.MODEL_G15_V2: DeviceInfo( + ["red:m1", "red:m2", "red:m3", "blue:mr" ], + g15_controls, g15v2_key_map, "g15v2", + r"G15_GamePanel_LCD-event-if.*", + r"G15_GamePanel_LCD-event-kdb.*"), + + g15driver.MODEL_G13: DeviceInfo( + ["red:m1", "red:m2", "red:m3", "red:mr" ], + g13_controls, g13_key_map, "g13", + r"_G13-event-mouse"), + + g15driver.MODEL_G110: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g110_controls, g110_key_map, "g110", + r"usb-LOGITECH_G110_G-keys-event-if.*", + r"usb-LOGITECH_G110_G-keys-event-kbd.*"), + + g15driver.MODEL_G510: DeviceInfo( + ["orange:m1", "orange:m2", "orange:m3", "red:mr" ], + g13_controls, g510_key_map, "g510", + r"G510_Gaming_Keyboard.*event-if.*", + r"G510_Gaming_Keyboard.*event.*kbd.*"), + } + + +# Other constants +EVIOCGRAB = 0x40044590 + +def show_preferences(device, parent, gconf_client): + prefs = KernelDriverPreferences(device, parent, gconf_client) + prefs.run() + +class KernelDriverPreferences(): + + def __init__(self, device, parent, gconf_client): + self.device = device + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_kernel.ui")) + self.window = widget_tree.get_object("KernelDriverSettings") + self.window.set_transient_for(parent) + + self.joy_mode_label = widget_tree.get_object("JoyModeLabel") + self.joy_mode_combo = widget_tree.get_object("JoyModeCombo") + self.joy_calibrate = widget_tree.get_object("JoyCalibrate") + self.grab_multimedia = widget_tree.get_object("GrabMultimedia") + + device_model = widget_tree.get_object("DeviceModel") + device_model.clear() + device_model.append(["auto"]) + for dev_file in os.listdir("/dev"): + if dev_file.startswith("fb"): + device_model.append(["/dev/%s" % dev_file]) + + g15uigconf.configure_combo_from_gconf(gconf_client, "/apps/gnome15/%s/fb_device" % device.uid, "DeviceCombo", "auto", widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, "/apps/gnome15/%s/joymode" % device.uid, "JoyModeCombo", "macro", widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/%s/grab_multimedia" % device.uid, "GrabMultimedia", False, widget_tree) + + self.grab_multimedia.set_sensitive(device_info[device.model_id].mm_pattern is not None) + + # See if jstest-gtk is available to do the calibration + self.calibrate_available = g15uinput.are_calibration_tools_available() + + self.joy_mode_combo.connect("changed", self._set_available_options) + self.joy_calibrate.connect("clicked", self._do_calibrate) + + self._set_available_options() + + def run(self): + self.window.run() + self.window.hide() + + def _set_available_options(self, widget = None): + self.joy_mode_label.set_sensitive(self.device.model_id == g15driver.MODEL_G13) + self.joy_mode_combo.set_sensitive(self.device.model_id == g15driver.MODEL_G13) + self.joy_calibrate.set_sensitive(g15uinput.get_device(self._get_device_type()) is not None and \ + self.device.model_id == g15driver.MODEL_G13 and \ + self.calibrate_available and \ + self.joy_mode_combo.get_active() in [1, 3]) + + def _get_device_type(self): + return g15uinput.JOYSTICK if self.joy_mode_combo.get_active() == 1 \ + else g15uinput.DIGITAL_JOYSTICK + + def _do_calibrate(self, widget): + g15uinput.calibrate(self._get_device_type()) + +class KeyboardReceiveThread(Thread): + def __init__(self, device): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread-%s" % device.uid + self.setDaemon(True) + self.devices = [] + + def deactivate(self): + self._run = False + for dev in self.devices: + logger.info("Ungrabbing %d", dev.fileno()) + try : + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 0) + except Exception as e: + logger.info("Failed ungrab.", exc_info = e) + logger.info("Closing %d", dev.fileno()) + try : + self.fds[dev.fileno()].close() + except Exception as e: + logger.info("Failed close.", exc_info = e) + logger.info("Stopped %d", dev.fileno()) + logger.info("Stopped all input devices") + + def run(self): + self.poll = select.poll() + self.fds = {} + for dev in self.devices: + self.poll.register(dev, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fcntl.ioctl(dev.fileno(), EVIOCGRAB, 1) + self.fds[dev.fileno()] = dev + while self._run: + for x, e in self.poll.poll(1000): + dev = self.fds[x] + try : + if dev: + dev.read() + except OSError as e: + logger.debug('Could not read device file', exc_info = e) + # Ignore this error if deactivated + if self._run: + raise e + logger.info("Thread left") + +''' +SimpleDevice implementation that does nothing with events. This is used to +work-around a problem where X ends up getting the G19 F-key events +''' +class SinkDevice(SimpleDevice): + def __init__(self, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + + def receive(self, event): + logger.debug("Sunk event %s", str(event)) + +''' +Abstract input device +''' +class AbstractInputDevice(SimpleDevice): + def __init__(self, callback, key_map, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.callback = callback + self.key_map = key_map + + def _event(self, event_code, state): + if event_code in self.key_map: + key = self.key_map[event_code] + self.callback([key], state) + else: + logger.warning("Unmapped key for event: %s", event_code) + +''' +SimpleDevice implementation for handling multi-media keys. +''' +class MultiMediaDevice(AbstractInputDevice): + def __init__(self, callback, key_map, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, key_map, *args, **kwargs) + + def receive(self, event): + if event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + return + else: + logger.warning("Unhandled event: %s", str(event)) + +''' +SimpleDevice implementation that translates kernel input events +into Gnome15 key events and forwards them to the registered +Gnome15 keyboard callback. +''' +class ForwardDevice(AbstractInputDevice): + def __init__(self, driver, callback, key_map, *args, **kwargs): + AbstractInputDevice.__init__(self, callback, key_map, *args, **kwargs) + self.driver = driver + self.ctrl = False + self.held_keys = [] + self.alt = False + self.shift = False + self.current_x = g15uinput.JOYSTICK_CENTER + self.digital_down = [] + self.current_y = g15uinput.JOYSTICK_CENTER + self.last_x = g15uinput.JOYSTICK_CENTER + self.last_y = g15uinput.JOYSTICK_CENTER + self.move_timer = None + + def send_all(self, events): + for event in events: + logger.debug(" --> %r", event) + self.udev.send_event(event) + + @property + def modcode(self): + code = 0 + if self.shift: + code += 1 + if self.ctrl: + code += 2 + if self.alt: + code += 4 + return code + + def receive(self, event): + if event.etype == S.EV_ABS: + if self.driver.joy_mode == g15uinput.JOYSTICK: + """ + The kernel modules give a joystick position values between 0 and 255. + The center is at 128. + The virtual joysticks are set to give values between -127 and 127. + The center is at 0. + So we adapt the received values. + """ + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + g15uinput.emit(self.driver.joy_mode, (event.etype, event.ecode), val, False) + else: + self._update_joystick(event) + elif event.etype == S.EV_KEY: + state = g15driver.KEY_STATE_DOWN if event.evalue == 1 else g15driver.KEY_STATE_UP + if event.ecode in [ S.BTN_X, S.BTN_Y, S.BTN_Z ]: + if self.driver.joy_mode ==g15uinput.MOUSE: + g15uinput.emit(g15uinput.MOUSE, self._translate_mouse_buttons(event.ecode), event.evalue, syn=True) + elif self.driver.joy_mode == g15uinput.DIGITAL_JOYSTICK: + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, event.ecode, event.evalue, syn=True) + elif self.driver.joy_mode == g15uinput.JOYSTICK: + g15uinput.emit(g15uinput.JOYSTICK, event.ecode, event.evalue, syn=True) + else: + if event.evalue != 2: + self._event(event.ecode, state) + else: + if event.evalue != 2: + self._event(event.ecode, state) + elif event.etype == 0: + if self.driver.joy_mode == g15uinput.JOYSTICK: + # Just pass-through when in analogue joystick mode + g15uinput.emit(self.driver.joy_mode, ( event.etype, event.ecode ), event.evalue, False) + else: + logger.warning("Unhandled event: %s", str(event)) + + """ + Private + """ + + def _record_current_absolute_position(self, event): + """ + Update the current_x and current_y positions if this is an + absolute movement event + """ + if event.ecode == S.ABS_X: + self.current_x = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_Y: + self.current_y = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + + def _update_joystick(self, event): + """ + Handle a position update event from the joystick, either by translating + it to mouse movements, digitising it, or emiting macros + + Keyword arguments: + event -- event + """ + if self.driver.joy_mode == g15uinput.DIGITAL_JOYSTICK: + self._record_current_absolute_position(event) + self._digital_joystick(event) + elif self.driver.joy_mode == g15uinput.MOUSE: + low_val = g15uinput.JOYSTICK_CENTER - self.driver.calibration + high_val = g15uinput.JOYSTICK_CENTER + self.driver.calibration + + if event.ecode == S.REL_X: + self.current_x = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.REL_Y: + self.current_y = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + + # Get the amount between the current value and the centre to move + move_x = 0 + move_y = 0 + if self.current_x >= high_val: + move_x = self.current_x - high_val + elif self.current_x <= low_val: + move_x = self.current_x - low_val + if self.current_y >= high_val: + move_y = self.current_y - high_val + elif self.current_y <= low_val: + move_y = self.current_y - low_val + + if self.current_x != self.last_x or self.current_y != self.last_y: + self.last_x = self.current_x + self.last_y = self.current_y + self.move_x = self._clamp(-3, move_x / 8, 3) + self.move_y = self._clamp(-3, move_y / 8, 3) + self._mouse_move() + else: + if self.move_timer is not None: + self.move_timer.cancel() + else: + self._emit_macro(event) + + def _translate_mouse_buttons(self, ecode): + """ + Translate the default joystick event codes to default mouse + event codes + + Keyword arguments: + ecode -- event code to translate + """ + if ecode == S.BTN_X: + return g15uinput.BTN_LEFT + elif ecode == S.BTN_Y: + return g15uinput.BTN_RIGHT + elif ecode == S.BTN_Z: + return g15uinput.BTN_MIDDLE + + def _compute_bounds(self): + """ + Calculate the distances from the (rough) centre position to the position + when movement each axis will start emiting events based on the + current calibration value. + + """ + return (g15uinput.JOYSTICK_CENTER - (self.driver.calibration), + g15uinput.JOYSTICK_CENTER + (self.driver.calibration)) + + def _emit_macro(self, event): + """ + Emit macro keys for joystick positions, so they can be processed as all + other macro keys are (i.e. assigned to a macro, script, or a different + uinput key) + + Keyword arguments: + event -- event + """ + low_val, high_val = self._compute_bounds() + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_X: + if val < low_val: + self._release_keys([g15driver.G_KEY_RIGHT]) + if not g15driver.G_KEY_LEFT in self.held_keys: + self.callback([g15driver.G_KEY_LEFT], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_LEFT) + elif val > high_val: + self._release_keys([g15driver.G_KEY_LEFT]) + if not g15driver.G_KEY_RIGHT in self.held_keys: + self.callback([g15driver.G_KEY_RIGHT], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_RIGHT) + else: + self._release_keys([g15driver.G_KEY_LEFT,g15driver.G_KEY_RIGHT]) + if event.ecode == S.ABS_Y: + if val < low_val: + self._release_keys([g15driver.G_KEY_DOWN]) + if not g15driver.G_KEY_UP in self.held_keys: + self.callback([g15driver.G_KEY_UP], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_UP) + elif val > high_val: + self._release_keys([g15driver.G_KEY_UP]) + if not g15driver.G_KEY_DOWN in self.held_keys: + self.callback([g15driver.G_KEY_DOWN], g15driver.KEY_STATE_DOWN) + self.held_keys.append(g15driver.G_KEY_DOWN) + else: + self._release_keys([g15driver.G_KEY_UP,g15driver.G_KEY_DOWN]) + + def _release_keys(self, keys): + for k in keys: + if k in self.held_keys: + self.callback([k], g15driver.KEY_STATE_UP) + self.held_keys.remove(k) + + def _clamp(self, minimum, x, maximum): + return max(minimum, min(x, maximum)) + + def _mouse_move(self): + if self.move_x != 0 or self.move_y != 0: + if self.move_x != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_X, self.move_x) + if self.move_y != 0: + g15uinput.emit(g15uinput.MOUSE, g15uinput.REL_Y, self.move_y) + self.move_timer = g15scheduler.schedule("MouseMove", 0.1, self._mouse_move) + + def _digital_joystick(self, event): + low_val, high_val = self._compute_bounds() + val = event.evalue - g15uinput.DEVICE_JOYSTICK_CENTER + if event.ecode == S.ABS_X: + if val < low_val and not "l" in self.digital_down: + self.digital_down.append("l") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_MIN) + elif val > high_val and not "r" in self.digital_down: + self.digital_down.append("r") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_MAX) + elif val >= low_val and val <= high_val and "l" in self.digital_down: + self.digital_down.remove("l") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_CENTER) + elif val >= low_val and val <= high_val and "r" in self.digital_down: + self.digital_down.remove("r") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_X, + g15uinput.JOYSTICK_CENTER) + if event.ecode == S.ABS_Y: + if val < low_val and not "u" in self.digital_down: + self.digital_down.append("u") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_MIN) + elif val > high_val and not "d" in self.digital_down: + self.digital_down.append("d") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_MAX) + if val >= low_val and val <= high_val and "u" in self.digital_down: + self.digital_down.remove("u") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_CENTER) + elif val >= low_val and val <= high_val and "d" in self.digital_down: + self.digital_down.remove("d") + g15uinput.emit(g15uinput.DIGITAL_JOYSTICK, + g15uinput.ABS_Y, + g15uinput.JOYSTICK_CENTER) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close=None): + g15driver.AbstractDriver.__init__(self, "kernel") + self.notify_handles = [] + self.fb = None + self.var_info = None + self.on_close = on_close + self.key_thread = None + self.device = device + self.device_info = None + self.system_service = None + self.conf_client = gconf.client_get_default() + + try: + self._init_device() + except Exception as e: + # Reset the framebuffer choice back to auto if the requested device does not exist + if self.device_name != None and self.device_name != "" or self.device_name != "auto": + self.conf_client.set_string("/apps/gnome15/%s/fb_device" % self.device.uid, "auto") + self._init_device() + else: + logger.warning("Could not open %s.", self.device_name, exc_info = e) + + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/joymode" % self.device.uid, self._config_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/fb_device" % self.device.uid, self._framebuffer_device_changed, None)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/grab_multimedia" % self.device.uid, self._config_changed, None)) + + def get_antialias(self): + if self.device.bpp != 1: + return cairo.ANTIALIAS_DEFAULT + else: + return cairo.ANTIALIAS_NONE + + def is_connected(self): + return self.system_service != None + + def get_model_names(self): + return device_info.keys() + + def get_name(self): + return "Linux Logitech Kernel Driver" + + def get_model_name(self): + return self.device.model_id if self.device != None else None + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + if self.get_model_name() == g15driver.MODEL_G13 and "macro" == self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid): + """ + This driver with the G13 supports some additional keys + """ + l = list(self.device.key_layout) + l.append([ g15driver.G_KEY_UP ]) + l.append([ g15driver.G_KEY_JOY_LEFT, g15driver.G_KEY_LEFT, g15driver.G_KEY_JOY_CENTER, g15driver.G_KEY_RIGHT ]) + l.append([ g15driver.G_KEY_JOY_DOWN, g15driver.G_KEY_DOWN ]) + return l + elif self.device_info.mm_pattern is not None and self.grab_multimedia: + l = list(self.device.key_layout) + l.append([]) + l.append([ g15driver.G_KEY_VOL_UP, g15driver.G_KEY_VOL_DOWN, g15driver.G_KEY_MUTE ]) + l.append([ g15driver.G_KEY_PREV, g15driver.G_KEY_PLAY, g15driver.G_KEY_STOP, g15driver.G_KEY_NEXT ]) + return l + else: + return self.device.key_layout + + def _load_configuration(self): + self.joy_mode = self.conf_client.get_string("/apps/gnome15/%s/joymode" % self.device.uid) + self.grab_multimedia = self.conf_client.get_bool("/apps/gnome15/%s/grab_multimedia" % self.device.uid) + if self.joy_mode == g15uinput.MOUSE: + logger.info("Enabling mouse emulation") + self.calibration = 20 + elif self.joy_mode == g15uinput.JOYSTICK: + logger.info("Enabling analogue joystick emulation") + self.calibration = 20 + elif self.joy_mode == g15uinput.DIGITAL_JOYSTICK: + logger.info("Enabling digital joystick emulation") + self.calibration = 64 + else: + logger.info("Enabling macro keys for joystick") + self.calibration = 64 + + def _config_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def _framebuffer_device_changed(self, client, connection_id, entry, args): + self._reload_and_reconnect() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return self.device_info.controls if self.device_info != None else None + + def paint(self, img): + if not self.fb: + return + width = img.get_width() + height = img.get_height() + character_width = width / 8 + fixed = self.fb.get_fixed_info() + padding = fixed.line_length - character_width + file_str = StringIO() + + if self.get_model_name() == g15driver.MODEL_G19: + try: + back_surface = cairo.ImageSurface (4, width, height) + except Exception as e: + logger.debug("Could not create ImageSurface. Trying earlier API.", exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height) + back_context = cairo.Context (back_surface) + back_context.set_source_surface(img, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + """ + If the creation of the type 4 image failed (i.e. earlier version of Cairo) + then we have to convert it ourselves. This is slow. + + TODO Replace with C routine + """ + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(self.rgb_to_uint16(r, g, b)) + buf = file_str.getvalue() + else: + buf = str(back_surface.get_data()) + else: + width, height = self.get_size() + arrbuf = array.array('B', self.empty_buf) + + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + ''' + Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + colours dithered. It would be nice if Cairo could do this :( Any suggestions? + ''' + pil_img = Image.frombuffer("RGBA", (width, height), argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + # Invert the screen if required + if g15_invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Data is 160x43, 1 byte per pixel. Will have value of 0 or 1. + width, height = self.get_size() + data = list(pil_img.getdata()) + fixed = self.fb.get_fixed_info() + v = 0 + b = 1 + + # TODO Replace with C routine + for row in range(0, height): + for col in range(0, width): + if data[( row * width ) + col]: + v += b + b = b << 1 + if b == 256: + # Full byte + b = 1 + i = row * fixed.line_length + col / 8 + arrbuf[i] = v + v = 0 + buf = arrbuf.tostring() + + if self.fb and self.fb.buffer: + self.fb.buffer[0:len(buf)] = buf + + def process_svg(self, document): + if self.get_bpp() == 1: + for element in document.getroot().iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans", "font-family:%s" % g15globals.fixed_size_font_name)) + + def on_update_control(self, control): + if control == g19_keyboard_backlight_control or control == g110_keyboard_backlight_control: + self._write_to_led("red:bl", control.value[0]) + if control.hint & g15driver.HINT_RED_BLUE_LED == 0: + self._write_to_led("green:bl", control.value[1]) + self._write_to_led("blue:bl", control.value[2]) + else: + # The G110 only has red and blue LEDs + self._write_to_led("blue:bl", control.value[2]) + elif control == g15_backlight_control: + if self.get_model_name() == g15driver.MODEL_G15_V2: + # G15v2 has different coloured backlight + self._write_to_led("orange:keys", control.value) + else: + self._write_to_led("blue:keys", control.value) + elif control == g15_lcd_backlight_control or control == g19_brightness_control: + self._write_to_led("white:screen", control.value) + elif control == g15_lcd_contrast_control: + self._write_to_led("contrast:screen", control.value) + elif control == g15_mkeys_control or control == g19_mkeys_control: + self._set_mkey_lights(control.value) + else: + if control.hint & g15driver.HINT_VIRTUAL == 0: + logger.warning("Setting the control " + control.id + " is not yet supported on this model. " + \ + "Please report this as a bug, providing the contents of your /sys/class/led" + \ + "directory and the keyboard model you use.") + + def grab_keyboard(self, callback): + if self.key_thread != None: + raise Exception("Keyboard already grabbed") + + # Configure the keymap + logger.info("Grabbing current keymap settings") + self.original_keymap = self._get_keymap() + kernel_keymap_replacement = K_KEYMAPS[self.device.model_id] + self._set_keymap(kernel_keymap_replacement) + + self.key_thread = KeyboardReceiveThread(self.device) + for devpath in self.keyboard_devices: + logger.info("Adding input device %s", devpath) + self.key_thread.devices.append(ForwardDevice(self, callback, self.device_info.key_map, devpath, devpath)) + for devpath in self.sink_devices: + logger.info("Adding input sink device %s", devpath) + self.key_thread.devices.append(SinkDevice(devpath, devpath)) + for devpath in self.mm_devices: + logger.info("Adding input multi-media device %s", devpath) + self.key_thread.devices.append(MultiMediaDevice(callback, self.device_info.key_map, devpath, devpath)) + self.key_thread.start() + + ''' + Private + ''' + + def _on_connect(self): + self.notify_handles = [] + # Check hardware again + self._init_driver() + + # Sanity check + if not self.device: + raise usb.USBError("No supported logitech keyboards found on USB bus") + if self.device == None: + raise usb.USBError("WARNING: Found no " + self.model + " Logitech keyboard, Giving up") + + # If there is no LCD for this device, don't open the framebuffer + if self.device.bpp != 0: + if self.fb_mode == None or self.device_name == None: + raise usb.USBError("No matching framebuffer device found") + if self.fb_mode != self.framebuffer_mode: + raise usb.USBError("Unexpected framebuffer mode %s, expected %s for device %s" % (self.fb_mode, self.framebuffer_mode, self.device_name)) + + # Open framebuffer + logger.info("Using framebuffer %s", self.device_name) + self.fb = fb.fb_device(self.device_name) + if logger.isEnabledFor(logging.DEBUG): + self.fb.dump() + self.var_info = self.fb.get_var_info() + + # Create an empty string buffer for use with monochrome LCD + self.empty_buf = "" + for i in range(0, self.fb.get_fixed_info().smem_len): + self.empty_buf += chr(0) + + # Connect to DBUS + system_bus = dbus.SystemBus() + try: + system_service_object = system_bus.get_object('org.gnome15.SystemService', '/org/gnome15/SystemService') + except dbus.DBusException as e: + logger.debug('D-Bus service not available.', exc_info = e) + raise Exception("Failed to connect to Gnome15 system service. Is g15-system-service running (as root). \ +It should be launched automatically if Gnome15 is installed correctly.") + self.system_service = dbus.Interface(system_service_object, 'org.gnome15.SystemService') + + # Centre the joystick by default + if self.joy_mode in [ g15uinput.JOYSTICK, g15uinput.DIGITAL_JOYSTICK ]: + g15uinput.emit(self.joy_mode, g15uinput.ABS_X, g15uinput.JOYSTICK_CENTER, False) + g15uinput.emit(self.joy_mode, g15uinput.ABS_Y, g15uinput.JOYSTICK_CENTER, False) + g15uinput.syn(self.joy_mode) + + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Not connected") + self._stop_receiving_keys() + if self.fb is not None: + self.fb.__del__() + self.fb = None + if self.on_close != None: + g15scheduler.schedule("Close", 0, self.on_close, self) + self.system_service = None + + def _reload_and_reconnect(self): + self._load_configuration() + if self.is_connected(): + self.disconnect() + + def _set_mkey_lights(self, lights): + if self.device_info.leds: + leds = self.device_info.leds + self._write_to_led(leds[0], lights & g15driver.MKEY_LIGHT_1 != 0) + self._write_to_led(leds[1], lights & g15driver.MKEY_LIGHT_2 != 0) + self._write_to_led(leds[2], lights & g15driver.MKEY_LIGHT_3 != 0) + self._write_to_led(leds[3], lights & g15driver.MKEY_LIGHT_MR != 0) + else: + logger.warning(" Setting MKey lights on " + self.device.model_id + " not yet supported. " + \ + "Please report this as a bug, providing the contents of your /sys/class/led" + \ + "directory and the keyboard model you use.") + + def _stop_receiving_keys(self): + if self.key_thread != None: + # Configure the keymap + logger.info("Resetting keymap settings back to the way they were") + self._set_keymap(self.original_keymap) + + self.key_thread.deactivate() + self.key_thread = None + + def _do_write_to_led(self, name, value): + if not self.system_service: + logger.warning("Attempt to write to LED when not connected") + else: + logger.debug("Writing %s to LED %s", value, name) + self.system_service.SetLight(self.device.uid, name, value) + + def _write_to_led(self, name, value): + gobject.idle_add(self._do_write_to_led, name, value) + + def _set_keymap(self, keymap): + for devpath in self.keyboard_devices: + logger.debug("Setting keymap on device %s", devpath) + fd = open(devpath, "rw") + try: + buf = array.array('B', [0] * sizeof(input_keymap_entry_t)) + for scancode, keycode in keymap.items(): + struct.pack_into(input_keymap_entry_t, buf, 0, + 0, # flags + sizeof('I'), # len + 0, # index + keycode, # keycode + *([0] * 32)) + struct.pack_into('I', buf, sizeof(input_keymap_entry_scancode_offset), scancode) + + fcntl.ioctl(fd, EVIOCSKEYCODE_V2, buf) + logger.debug(" key %d := %d", scancode, keycode) + finally: + fd.close() + + def _get_keymap(self): + for devpath in self.keyboard_devices: + logger.debug("Getting keymap from device %s", devpath) + fd = open(devpath, "rw") + try: + keymap = K_KEYMAPS[self.device.model_id].copy() + buf = array.array('B', [0] * sizeof(input_keymap_entry_t)) + for scancode in keymap: + struct.pack_into(input_keymap_entry_t, buf, 0, + 0, # flags + sizeof('I'), # len + 0, # index + S.KEY_RESERVED, # keycode + *([0] * 32)) + struct.pack_into('I', buf, sizeof(input_keymap_entry_scancode_offset), scancode) + + fcntl.ioctl(fd, EVIOCGKEYCODE_V2, buf) + keymap[scancode] = struct.unpack(input_keymap_entry_t, buf)[3] + logger.debug(" key %d = %d", scancode, keymap[scancode]) + return keymap + finally: + fd.close() + return None + + def _handle_bound_key(self, key): + logger.info("G key - %d", key) + return True + + def _mode_changed(self, client, connection_id, entry, args): + if self.is_connected(): + self.disconnect() + + def _init_device(self): + self._load_configuration() + if not self.device.model_id in device_info: + return + + self.device_info = device_info[self.device.model_id] + self.fb_mode = None + self.device_name = None + self.framebuffer_mode = "NONE" + + if self.device.bpp == 0: + logger.info("Device %s has no framebuffer", self.device.model_id) + else: + if self.device.bpp == 1: + self.framebuffer_mode = "GFB_MONO" + else: + self.framebuffer_mode = "GFB_QVGA" + logger.info("Using %s frame buffer mode", self.framebuffer_mode) + + # Determine the framebuffer device to use + self.device_name = self.conf_client.get_string("/apps/gnome15/%s/fb_device" % self.device.uid) + if self.device_name == None or self.device_name == "" or self.device_name == "auto": + for fb in os.listdir("/sys/class/graphics"): + if fb != "fbcon": + logger.info("Trying %s", fb) + device_file = "/sys/class/graphics/%s/device" % fb + if os.path.exists(device_file): + usb_id = os.path.basename(os.path.realpath(device_file)).split(".")[0].split(":") + if len(usb_id) > 2: + if usb_id[1].lower() == "%04x" % self.device.controls_usb_id[0] and usb_id[2].lower() == "%04x" % self.device.controls_usb_id[1]: + self.device_name = "/dev/%s" % fb + break + + # If still no device name, give up + if self.device_name == None or self.device_name == "" or self.device_name == "auto": + raise Exception("No frame buffer device specified, and none could be found automatically. Are the kernel modules loaded?") + + # Get the mode of the device + f = open("/sys/class/graphics/" + os.path.basename(self.device_name) + "/name", "r") + try : + self.fb_mode = f.readline().replace("\n", "") + finally : + f.close() + + def _init_driver(self): + self._init_device() + + # Try and find the paths for the keyboard devices + self.keyboard_devices = [] + self.sink_devices = [] + self.mm_devices = [] + + dir = "/dev/input/by-id" + for p in os.listdir(dir): + if re.search(self.device_info.keydev_pattern, p): + logger.info("Input device %s matches %s", p, self.device_info.keydev_pattern) + self.keyboard_devices.append(dir + "/" + p) + if self.device_info.sink_pattern is not None and re.search(self.device_info.sink_pattern, p): + logger.info("Input sink device %s matches %s", p, self.device_info.sink_pattern) + self.sink_devices.append(dir + "/" + p) + if self.grab_multimedia and self.device_info.mm_pattern is not None and re.search(self.device_info.mm_pattern, p): + logger.info("Input multi-media device %s matches %s", p, self.device_info.mm_pattern) + self.mm_devices.append(dir + "/" + p) + + def __del__(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) diff --git a/src/gnome15/drivers/driver_mx5500.py b/src/gnome15/drivers/driver_mx5500.py new file mode 100644 index 0000000..d9e2252 --- /dev/null +++ b/src/gnome15/drivers/driver_mx5500.py @@ -0,0 +1,406 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Main implementation of a G15Driver that uses g15daemon to control and query the +keyboard +""" + +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15scheduler as g15scheduler +import gtk +import os.path +import socket +import cairo +import gconf +from PIL import ImageMath +from PIL import Image +from threading import Thread +from threading import Lock +import struct +import time +import logging +import asyncore +import sys +logger = logging.getLogger(__name__) + +# Driver information (used by driver selection UI) +name="MX5500" +id="mx5500" +description="For use with the Logitech G15v1, G15v2, G13, G510 and G110. This driver uses mx5500tools, available from " + \ + "mx5500tools. The mx5500d service " + \ + "must be installed and running when starting Gnome15." +has_preferences=True + +DEFAULT_PORT=15550 + +CLIENT_CMD_KB_BACKLIGHT = 0x08 +CLIENT_CMD_CONTRAST = 0x40 +CLIENT_CMD_BACKLIGHT = 0x80 +CLIENT_CMD_GET_KEYSTATE = ord('k') +CLIENT_CMD_KEY_HANDLER = 0x10 +CLIENT_CMD_MKEY_LIGHTS = 0x20 +CLIENT_CMD_SWITCH_PRIORITIES = ord('p') +CLIENT_CMD_NEVER_SELECT = ord('n') +CLIENT_CMD_IS_FOREGROUND = ord('v') +CLIENT_CMD_IS_USER_SELECTED = ord('u') +CLIENT_CMD_KB_BACKLIGHT_COLOR = ord('r') + +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + g15driver.G_KEY_G13 : 1<<12, + g15driver.G_KEY_G14 : 1<<13, + g15driver.G_KEY_G15 : 1<<14, + g15driver.G_KEY_G16 : 1<<15, + g15driver.G_KEY_G17 : 1<<16, + g15driver.G_KEY_G18 : 1<<17, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 + } + + +invert_control = g15driver.Control("invert_lcd", "Invert LCD", 0, 0, 1, hint = g15driver.HINT_SWITCH ) + +def show_preferences(device, parent, gconf_client): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "driver_g15.ui")) + g15uigconf.configure_spinner_from_gconf(gconf_client, "/apps/gnome15/%s/g15daemon_port" % device.uid, "Port", DEFAULT_PORT, widget_tree, False) + return widget_tree.get_object("DriverComponent") + +def fix_sans_style(root): + for element in root.iter(): + style = element.get("style") + if style != None: + element.set("style", style.replace("font-family:Sans","font-family:%s" % g15globals.fixed_size_font_name)) + +class G15Dispatcher(asyncore.dispatcher): + def __init__(self, map, conn, callback = None): + self.key_stage = 0 + self.out_buffer = "" + self.oob_buffer = "" + self.recv_buffer = "" + self.callback = callback; + self.reverse_map = {} + for k in KEY_MAP.keys(): + self.reverse_map[KEY_MAP[k]] = k + self.received_handshake = False + asyncore.dispatcher.__init__(self, sock=conn, map = map) + + def wait_for_handshake(self): + while not self.received_handshake: + time.sleep(0.5) + + def handle_close(self): + self.received_handshake = True + + def handle_expt(self): + data = self.socket.recv(1, socket.MSG_OOB) + if len(data) > 0: + val = ord(data[0]) + if val & CLIENT_CMD_BACKLIGHT: + level = val - CLIENT_CMD_BACKLIGHT + elif val & CLIENT_CMD_KB_BACKLIGHT: + level = val - CLIENT_CMD_KB_BACKLIGHT + elif val & CLIENT_CMD_CONTRAST: + logger.warning("Ignoring contrast command") + else: + logger.warning("Ignoring unknown OOB command") + + def handle_key(self, data): + if self.key_stage == 0: + self.last_key = struct.unpack("0: + self.recv_buffer += received + + # Have we collected enough for a key? + # TODO is this even neccesary, will we always get those 4 bytes when they are available + if self.received_handshake: + while len(self.recv_buffer) > 3: + data = self.get_data(4) + if data: + self.handle_key(data) + else: + data = self.get_data(16) + if data: + if data != "G15 daemon HELLO": + raise Exception("Excepted G15 daemon handshake.") + self.out_buffer = "GBUF" + self.received_handshake = True + except Exception as e: + self.oob_buffer = "" + self.out_buffer = "" + logger.debug("Error reading data from G15 daemon", exc_info = e) + raise e + + def get_data(self, required_length): + if len(self.recv_buffer) >= required_length: + data = self.recv_buffer[0:required_length] + self.recv_buffer = self.recv_buffer[required_length:] + return data + + def writable(self): + return len(self.oob_buffer) > 0 or len(self.out_buffer) > 0 + + def send_with_options(self, buffer, options = 0): + try: + return self.socket.send(buffer, options) + except socket.error, why: + self.oob_buffer = "" + if why.args[0] == EWOULDBLOCK: + return 0 + elif why.args[0] in (ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED): + self.handle_close() + return 0 + else: + raise + + def handle_write(self): + if len(self.out_buffer) > 0: + sent = self.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] + return sent + elif len(self.oob_buffer) > 0: + s = 0 + for c in self.oob_buffer: + s += self.send_with_options(c, socket.MSG_OOB) + self.oob_buffer = self.oob_buffer[s:] + + def convert_from_g15daemon_code(self, code): + keys = [] + for key in self.reverse_map: + if code & key != 0: + keys.append(self.reverse_map[key]) + return keys + +class G15Async(Thread): + def __init__(self, map): + Thread.__init__(self) + self.name = "G15Async" + self.setDaemon(True) + self.map = map + + def run(self): + asyncore.loop(timeout = 0.01, map = self.map) + +class Driver(g15driver.AbstractDriver): + + def __init__(self, device, on_close = None): + g15driver.AbstractDriver.__init__(self, "g15") + self.device = device + self.lock = Lock() + self.dispatcher = None + self.on_close = on_close + self.socket = None + self.connected = False + self.async = None + self.change_timer = None + self.conf_client = gconf.client_get_default() + + def get_size(self): + return self.device.lcd_size + + def get_bpp(self): + return self.device.bpp + + def get_controls(self): + return [ invert_control ] + + def get_antialias(self): + return cairo.ANTIALIAS_NONE + + def get_action_keys(self): + return self.device.action_keys + + def get_key_layout(self): + return self.device.key_layout + + def send(self, data, opt = None): + if opt == socket.MSG_OOB: + self.dispatcher.oob_buffer += data + else: + self.dispatcher.out_buffer += data + + def on_update_control(self, control): + pass + + def get_name(self): + return "mx5500tools driver" + + def get_model_names(self): + return [ g15driver.MODEL_MX5500 ] + + def get_model_name(self): + return self.device.model_id + + + def is_connected(self): + return self.connected + + def config_changed(self, client, connection_id, entry, args): + if self.change_timer != None: + self.change_timer.cancel() + self.change_timer = g15scheduler.schedule("ChangeG15DaemonConfiguration", 3.0, self.update_conf) + + def update_conf(self): + logger.info("Configuration changed") + if self.connected: + logger.info("Reconnecting") + self.disconnect() + self.connect() + + def grab_keyboard(self, callback): + self.dispatcher.callback = callback + self.send(chr(CLIENT_CMD_KEY_HANDLER),socket.MSG_OOB) + + def process_svg(self, document): + fix_sans_style(document.getroot()) + + def paint(self, img): + if not self.is_connected(): + return + + # Just return if the device has no LCD + if self.device.bpp == 0: + return None + + self.lock.acquire() + try : + size = self.get_size() + + # Paint to 565 image provided into an ARGB image surface for PIL's benefit. PIL doesn't support 565? + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(img) + argb_context.paint() + + # Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + # colours dithered. It would be nice if Cairo could do this :( Any suggestions? + pil_img = Image.frombuffer("RGBA", size, argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + invert_control = self.get_control("invert_lcd") + if invert_control.value == 0: + pil_img = pil_img.point(lambda i: 1^i) + + # Covert image buffer to string + buf = "" + for x in list(pil_img.getdata()): + buf += chr(x) + + if len(buf) != self.device.lcd_size[0] * self.device.lcd_size[1]: + logger.warning("Invalid buffer size") + else: + try : + self.send(buf) + except IOError as e: + logger.error("Failed to send buffer.", exc_info = e) + self.disconnect() + finally: + self.lock.release() + + def _on_disconnect(self): + if not self.is_connected(): + raise Exception("Already disconnected") + self.conf_client.notify_remove(self.notify_handle) + self.connected = False + if self.dispatcher != None: + self.dispatcher.running = False + self.socket.close() + self.socket = None + self.dispatcher = None + if self.on_close != None: + self.on_close(self) + + def _on_connect(self): + port = 15550 + e = self.conf_client.get("/apps/gnome15/%s/g15daemon_port" % self.device.uid) + if e: + port = e.get_int() + + map = {} + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(5.0) + self.socket.connect(("127.0.0.1", port)) + + self.dispatcher = G15Dispatcher(map, self.socket) + self.async = G15Async(map).start() + self.dispatcher.wait_for_handshake() + self.connected = True + + self.notify_handle = self.conf_client.notify_add("/apps/gnome15/%s/g15daemon_port" % self.device.uid, self.config_changed, None) \ No newline at end of file diff --git a/src/gnome15/drivers/fb.py b/src/gnome15/drivers/fb.py new file mode 100644 index 0000000..32016cb --- /dev/null +++ b/src/gnome15/drivers/fb.py @@ -0,0 +1,191 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ctypes import * +from fcntl import ioctl +import mmap +import os + +FBIOGET_VSCREENINFO=0x4600 +FBIOPUT_VSCREENINFO=0x4601 +FBIOGET_FSCREENINFO=0x4602 + +class fb_fix_screeninfo(Structure): + _fields_ = [ + ("id", c_char * 16), + ("smem_start", c_ulong), + ("smem_len", c_int, 32), + ("type", c_int, 32), + ("type_aux", c_int, 32), + ("visual", c_int, 32), + ("xpanstep", c_int, 16), + ("ypanstep", c_int, 16), + ("ywrapstep", c_int, 16), + ("line_length", c_int, 32), + ("mmio_start", c_ulong), + ("mmio_len", c_int, 32), + ("accel", c_int, 32), + ("reserved", c_ushort * 3), + ] + +class fb_bitfield(Structure): + _fields_ = [ + ("offset", c_int, 32), + ("length", c_int, 32), + ("msb_right", c_int, 32), + ] + + def __repr__(self): + return "bitfield [ offset = %d, length = %d, msb_right = %d ]" % ( self.offset, self.length, self.msb_right ) + +class fb_var_screeninfo(Structure): + _fields_ = [ + ( "xres", c_int, 32), + ( "yres", c_int, 32), + ( "xres_virtual", c_int, 32), + ( "yres_virtual", c_int, 32), + ( "xoffset", c_int, 32), + ( "yoffset", c_int, 32), + ( "bits_per_pixel", c_int, 32), + ( "grayscale", c_int, 32), + ( "red", fb_bitfield), + ( "green", fb_bitfield), + ( "blue", fb_bitfield), + ( "transp", fb_bitfield), + ( "nonstd", c_int, 32), + ( "activate", c_int, 32), + ( "height", c_int, 32), + ( "width", c_int, 32), + ( "accel_flags", c_int, 32), + ( "pixclock", c_int, 32), + ( "left_margin", c_int, 32), + ( "right_margin", c_int, 32), + ( "upper_margin", c_int, 32), + ( "lower_margin", c_int, 32), + ( "hsync_len", c_int, 32), + ( "vsync_len", c_int, 32), + ( "sync", c_int, 32), + ( "vmode", c_int, 32), + ( "rotate", c_int, 32), + ( "reserved", c_ulong * 5), + ] + +class fb_device(): + def __init__(self, device_name, mode = os.O_RDWR): + self.device_file = os.open(device_name, os.O_RDWR) + self.buffer = None + self.invalidate() + + def invalidate(self): + if self.buffer != None: + self.buffer().close() + self.buffer = mmap.mmap(self.device_file, self.get_screen_size(), mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + + def get_fixed_info(self): + fixed_info = fb_fix_screeninfo() + if ioctl(self.device_file, FBIOGET_FSCREENINFO, fixed_info): + raise Exception("Error reading fixed information.\n") + return fixed_info + + def get_var_info(self): + variable_info = fb_var_screeninfo() + if ioctl(self.device_file, FBIOGET_VSCREENINFO, variable_info): + raise Exception("Error reading variable information.\n") + return variable_info + + def close(self): + if self.buffer != None: + self.buffer.close() + self.buffer = None + os.close(self.device_file) + + def __del__(self): + try: + self.close() + except Exception as e: + logger.debug('Error destroying fb_device.', exc_info = e) + pass + + def get_screen_size(self): + # fb_sys_write() in linux kernel 2.6.36 relies on this value + return self.get_fixed_info().smem_len + + # variable_info = self.get_var_info() + # return variable_info.xres * variable_info.yres * variable_info.bits_per_pixel / 8 + + + def dump(self): + + fixed_info = self.get_fixed_info() + + print "--------------" + print "Fixed" + print "--------------" + print "id:", fixed_info.id + print "smem_start:", fixed_info.smem_start + print "smem_len:", fixed_info.smem_len + print "type:", fixed_info.type + print "type_aux:", fixed_info.type_aux + print "visual:", fixed_info.visual + print "xpanstep:", fixed_info.xpanstep + print "ypanstep:", fixed_info.ypanstep + print "ywrapstep:", fixed_info.ywrapstep + print "line_length:", fixed_info.line_length + print "mmio_start:", fixed_info.mmio_start + print "mmio_len:", fixed_info.mmio_len + print "accel:", fixed_info.accel + + + variable_info = self.get_var_info() + + print "--------------" + print "Variable" + print "--------------" + print "xres:",variable_info.xres + print "yres:",variable_info.yres + print "xres_virtual:",variable_info.xres_virtual + print "yres_virtual:",variable_info.yres_virtual + print "xoffset:",variable_info.xoffset + print "yoffset:",variable_info.yoffset + print "bits_per_pixel:",variable_info.bits_per_pixel + print "grayscale:",variable_info.grayscale + print "red:",variable_info.red + print "green:",variable_info.green + print "blue:",variable_info.blue + print "transp:",variable_info.transp + print "activate:",variable_info.activate + print "height:",variable_info.height + print "width:",variable_info.width + print "accel_flags:",variable_info.accel_flags + print "pixclock:",variable_info.pixclock + print "left_margin:",variable_info.left_margin + print "right_margin:",variable_info.right_margin + print "update_margin:",variable_info.upper_margin + print "lower_margin:",variable_info.lower_margin + print "hsync_len:",variable_info.hsync_len + print "vsync_len:",variable_info.vsync_len + print "sync:",variable_info.sync + print "vmode:",variable_info.vmode + print "rotate:",variable_info.rotate + + +if __name__ == "__main__": + for d in os.listdir("/dev"): + if d.startswith("fb"): + print "---------",d + device = fb_device("/dev/%s" %d) + print "Screen bytes: " + str(device.get_screen_size()) + device.dump() diff --git a/src/gnome15/drivers/pylibg15.py b/src/gnome15/drivers/pylibg15.py new file mode 100644 index 0000000..bc9f7ff --- /dev/null +++ b/src/gnome15/drivers/pylibg15.py @@ -0,0 +1,187 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +from ctypes import * +from threading import Thread + +libg15 = cdll.LoadLibrary("libg15.so.1") + +# Default key read timeout. Too low and keys will be missed (very obvious in a VM) +KEY_READ_TIMEOUT = 100 + +G15_LCD = 1 +G15_KEYS = 2 +G15_DEVICE_IS_SHARED = 4 +G15_DEVICE_5BYTE_RETURN = 8 +G15_DEVICE_G13 = 16 +G15_DEVICE_G510 = 32 + +G15_KEY_READ_LENGTH = 9 +G510_STANDARD_KEYBOARD_INTERFACE = 0x0 + +# Error codes +G15_NO_ERROR = 0 +G15_ERROR_READING_USB_DEVICE=4 +G15_TRY_AGAIN = 5 +G15_ERROR_NOENT = -2 +G15_ERROR_NODEV = -19 + +# Debug levels +G15_LOG_INFO = 1 +G15_LOG_WARN = 0 + +class KeyboardReceiveThread(Thread): + def __init__(self, callback, key_read_timeout, on_error): + Thread.__init__(self) + self._run = True + self.name = "KeyboardReceiveThread" + self.callback = callback + self.on_exit = None + self.on_unplug = None + self.key_read_timeout = key_read_timeout + self.on_error = on_error + + def deactivate(self): + if self._run: + self._run = False + + def run(self): + try: + pressed_keys = c_int(0) + while self._run: + err = libg15.getPressedKeys(byref(pressed_keys), 10) + code = 0 + ext_code = 0 + if err == G15_NO_ERROR: + if is_ext_key(pressed_keys.value): + ext_code = int(pressed_keys.value) + ext_code &= ~(1<<28) + err = libg15.getPressedKeys(byref(pressed_keys), 10) + if err == G15_NO_ERROR: + code = pressed_keys.value + elif err in [ G15_TRY_AGAIN, G15_ERROR_READING_USB_DEVICE ]: + pass + elif err == G15_ERROR_NODEV: + # Device unplugged + self._run = False + if self.on_unplug is not None: + self.on_unplug() + else: + if self.on_error is not None: + self.on_error(err) + break + else: + code = pressed_keys.value + + self.callback(code, ext_code) + elif err in [ G15_TRY_AGAIN, G15_ERROR_READING_USB_DEVICE ] : + continue + elif err == G15_ERROR_NODEV: + # Device unplugged + self._run = False + if self.on_unplug is not None: + self.on_unplug() + else: + if self.on_error is not None: + self.on_error(err) + break + + finally: + if self.on_exit is not None: + self.on_exit() + self._run = True + +class libg15_devices_t(Structure): + _fields_ = [ ("name", c_char_p), + ("vendorid", c_int), + ("productid", c_int), + ("caps", c_int) ] + +def is_ext_key(code): + """ + Get if the key code provide is an "Extended Key". Extended keys are used + to cope with libg15's restriction on the number of available codes, + which the G13 exceeds. + + Keyword arguments: + code -- code to test if extended + """ + return code & (1<<28) != 0 + +def grab_keyboard(callback, key_read_timeout = KEY_READ_TIMEOUT, on_error = None): + """ + Start polling for keyboard events. Device must be initialised. The thread + returned can be stopped by calling deactivate(). + + The callback is invoked with two arguments. The first being the bit mask + of any pressed non-extended codes. The second is the mask of any extended + key presses. + + Keyword arguments: + callback -- function to call on key event + key_read_timeout -- timeout for reading key presses. too low and keys will be missed + """ + t = KeyboardReceiveThread(callback, key_read_timeout, on_error) + t.start() + return t + +def init(init_usb = True, vendor_id = 0, product_id = 0): + """ + This one return G15_NO_ERROR on success, something + else otherwise (for instance G15_ERROR_OPENING_USB_DEVICE + """ + return libg15.setupLibG15(vendor_id, product_id, 1 if init_usb else 0) + +def reinit(): + """ re-initialise a previously unplugged keyboard ie ENODEV was returned at some point """ + return libg15.re_initLibG15() + + +def exit(): + return libg15.exitLibG15() + +def set_debug(level): + """ + Keyword arguments: + level -- level, one of G15_LOG_INFO or G15_LOG_WARN + """ + libg15.libg15Debug(level) + +def write_pixmap(data): + libg15.writePixmapToLCD(data) + +def set_contrast(level): + return libg15.setLCDContrast(level) + +def set_leds(leds): + return libg15.setLEDs(leds) + +def set_lcd_brightness(level): + return libg15.setLCDBrightness(level) + +def set_keyboard_brightness(level): + return libg15.setKBBrightness(level) + +def set_keyboard_color(color): + val = libg15.setG510LEDColor(color[0], color[1], color[2]) + return val + +def get_joystick_position(): + return ( libg15.getJoystickX(), libg15.getJoystickY() ) + +def __handle_key(code): + print "Got %d" %code \ No newline at end of file diff --git a/src/gnome15/g15accounts.py b/src/gnome15/g15accounts.py new file mode 100644 index 0000000..d0b5805 --- /dev/null +++ b/src/gnome15/g15accounts.py @@ -0,0 +1,538 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Classes that may be used by plugins that require account management, usually +to access some kind of network server such as Email, Calendar or Feeds. + +A GTK UI is also provided that subclasses may plug-in their own protocol specific +configuration widgets. + +Accounts are stored as simple XML files in .g +""" + +import os +from lxml import etree +import gtk +import g15globals +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15pythonlang as g15pythonlang +import pyinotify +import pwd +from threading import Lock +import gobject +import keyring + +import logging +logger = logging.getLogger(__name__) + +""" +Functions +""" + +""" +Configure monitoring of account files. This allows plugins to get notified +when accounts they are using change +""" +account_listeners = [] + +watch_manager = pyinotify.WatchManager() +mask = pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_CREATE | pyinotify.IN_ATTRIB # watched events + +class EventHandler(pyinotify.ProcessEvent): + + def _notify(self, event): + for a in account_listeners: + a(event) + + def process_IN_MODIFY(self, event): + self._notify(event) + + def process_IN_CREATE(self, event): + self._notify(event) + + def process_IN_ATTRIB(self, event): + self._notify(event) + + def process_IN_DELETE(self, event): + self._notify(event) + +notifier = pyinotify.ThreadedNotifier(watch_manager, EventHandler()) +notifier.name = "AccountsPyInotify" +notifier.setDaemon(True) +notifier.start() + +CURRENT_USERNAME=pwd.getpwuid(os.getuid())[0] + +class Status(): + def __init__(self): + self.stopping = False + +STATUS = Status() + +''' +Helper classes for getting a secret from the keyring +''' +class G15Keyring(): + + def __init__(self): + self.lock = Lock() + self.password = None + + def get_username(self, account): + username = account.get_property("username", "") + return username if username != "" else CURRENT_USERNAME + + def get_uri_and_props(self, account, hostname = None, port = None): + + username = self.get_username(account) + name = account.type + "://" + username + + props = { + 'service':account.type, + 'username':username + } + + if hostname is not None: + name = name + "@" + hostname + props['server'] = hostname + if port is not None: + props['port'] = port + name = name + ":" + str(port) + + return props, name + + def store_password(self, account, password, hostname = None, port = None): + _, name = self.get_uri_and_props(account, hostname, port) + keyring.set_password(name, self.get_username(account), password) + + def find_secret(self, account, name, release_lock = True): + username = self.get_username(account) + try : + if STATUS.stopping: + self.password = None + return + + pw = keyring.get_password(name, username) + if pw is not None: + self.password = pw + return + + # Ask for the password + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "password.ui")) + dialog = widget_tree.get_object("PasswordDialog") + text_widget = widget_tree.get_object("Text") + text_widget.set_text(_("The account %s for the user %s.\n\ +requires a password, This will be stored in the Gnome Keyring and \n\ +and will not be asked for again unless there is some later problem\n\ +problem authentication (for example as the result of\n\ +a password change).") % (account.name, username)) + text_widget.set_use_markup(True) + password_widget = widget_tree.get_object("Password") + dialog.show_all() + + response = dialog.run() + try : + if response == 1: + self.password = password_widget.get_text() + finally : + dialog.destroy() + + return + finally: + if release_lock: + self.lock.release() + + + def retrieve_password(self, account, hostname = None, port = None, force_dialog = False): + + _, name = self.get_uri_and_props(account, hostname, port) + + ''' + Find the item. It appears gnome keyring access must be run on the gobject loop? I don't + really understand the problem, but doing this seems to fix it + + TODO find out what is actually happening + ''' + if g15pythonlang.is_gobject_thread(): + self.find_secret(account, name, False) + else: + self.lock.acquire() + self.password = None + gobject.idle_add(self.find_secret, account, name) + self.lock.acquire() + self.lock.release() + if self.password != None: + return self.password + + +class G15AccountManager(G15Keyring): + """ + Manages the storage and loading of an account list. This is + stored as an XML file in the Gnome configuration directory + """ + + def __init__(self, file_path, item_name): + """ + Constructor + + Keyword arguments: + file_path -- location accounts are stored. Directory will be created if it does not exist + item_name -- name of item in XML file + """ + G15Keyring.__init__(self) + + self._conf_file = os.path.expanduser(file_path) + self.item_name = item_name + self.load() + self.listeners = {} + self.listener_functions = {} + + def add_change_listener(self, listener): + self.listeners[listener] = watch_manager.add_watch(os.path.dirname(self._conf_file), mask, rec=True) + def a(event): + self.load() + if event.pathname == self._conf_file: + listener(self) + self.listener_functions[listener] = a + account_listeners.append(a) + + def remove_change_listener(self, listener): + wdd = self.listeners[listener] + account_listeners.remove(self.listener_functions[listener]) + del self.listener_functions[listener] + for k in wdd: + try: + watch_manager.rm_watch(wdd[k],quiet = False) + except Exception as e: + logger.debug("Error removing change listener '%s'", str(k), exc_info = e) + pass + + def load(self): + accounts = [] + if not os.path.exists(self._conf_file): + dir_path = os.path.dirname(self._conf_file) + g15os.mkdir_p(dir_path) + else: + document = etree.parse(self._conf_file) + for element in document.getroot().xpath('//%s' % self.item_name): + acc = G15Account(element.get("name"), element.get("type")) + for property_element in element: + acc.properties[property_element.get("name")] = property_element.get("value") + accounts.append(acc) + + self.accounts = accounts + + + def by_name(self, name): + """ + Get an account given its name. + + Keyword arguments: + name -- account name + """ + for acc in self.accounts: + if acc.name == name: + return acc + + + def save(self): + """ + Save all accounts. + """ + root = etree.Element("xml") + document = etree.ElementTree(root) + for acc in self.accounts: + acc_el = etree.SubElement(root, self.item_name, type=acc.type, name=acc.name) + for key in acc.properties: + etree.SubElement(acc_el, "property", name=key, value=acc.properties[key]) + xml = etree.tostring(document) + fh = open(self._conf_file, "w") + try : + fh.write(xml) + finally : + fh.close() + +class G15Account(): + """ + A single account. An account has two main attributes, + a name and a type. All protocol specific details are + stored in the properties map. + """ + + def __init__(self, name, account_type): + """ + Constructor + + Keyword arguments: + name -- account name + account_type -- account type + """ + self.name = name + self.type = account_type + self.properties = {} + + def get_property(self, key, default_value=None): + return self.properties[key] if key in self.properties else default_value + + +class G15AccountOptions(): + """ + Superclass of the UI protocol specific configuration. + """ + + def __init__(self, account, account_ui): + """ + Constructor + + Keyword arguments: + account -- account + account_ui -- instance of G15AccountPreferences that contains the options widget + """ + self.account = account + self.account_ui = account_ui + +class G15AccountPreferences(): + """ + Configuration UI + """ + + + def __init__(self, parent, gconf_client, gconf_key, file_path, item_name, default_refresh = 60): + """ + Constructor + + Keyword arguments: + parent -- parent GTK component (for modality) + gconf_client -- gconf client + gconf_key -- gconf key prefix for this plugin + file_path -- location of accounts file + item_name -- name of item in XML files + """ + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.visible_options = None + self._save_timer = None + self._adjusting = False + + self.account_mgr = G15AccountManager(file_path, item_name) + + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "accounts.ui")) + + # Models + self.type_model = self.widget_tree.get_object("TypeModel") + self.account_model = self.widget_tree.get_object("AccountModel") + self.type_model.clear() + for t in self.get_account_types(): + self.type_model.append([ t, self.get_account_type_name(t) ]) + + # Widgets + self.account_type = self.widget_tree.get_object("TypeCombo") + self.account_list = self.widget_tree.get_object("AccountList") + self.url_renderer = self.widget_tree.get_object("URLRenderer") + + # Updates + self.update_adjustment = self.widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(g15gconf.get_int_or_default(gconf_client, gconf_key + "/update_time", default_refresh)) + + # Connect to events + self.account_list.connect("cursor-changed", self._select_account) + self.account_type.connect("changed", self._type_changed) + self.update_adjustment.connect("value-changed", self._update_time_changed) + self.url_renderer.connect("edited", self._url_edited) + self.widget_tree.get_object("NewAccount").connect("clicked", self._new_url) + self.widget_tree.get_object("RemoveAccount").connect("clicked", self._remove_url) + + # Configure widgets + self._reload_model() + self._select_account() + + # Hide non-relevant stuff + self.widget_tree.get_object("TypeContainer").set_visible(len(self.get_account_types()) > 1) + + # Additional options + place_holder = self.widget_tree.get_object("OptionsContainer") + opts = self.create_general_options() + if opts: + opts.reparent(place_holder) + + # Show dialog + dialog = self.widget_tree.get_object("AccountDialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self._urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah) + + """ + Implement + """ + def create_general_options(self): + """ + Create general options for the dialog. These are added to the area + beneath the refresh interval spinner + """ + pass + + def get_account_type_name(self, account_type): + """ + Get the localized account type name + + Keyword arguments: + account_type -- account type (always provided, will be same as account.type if exists) + """ + raise Exception("Not implemented") + + def get_account_types(self): + """ + Get the account types that are available + """ + raise Exception("Not implemented") + + def create_options_for_type(self, account, account_type): + """ + Create the concrete G15AccountOptions object given the account type name. + + Keyword arguments: + account -- account object (will be None if this is for a new account) + account_type -- account type (always provided, will be same as account.type if exists) + """ + raise Exception("Not implemented") + + """ + Private + """ + def save_accounts(self): + if not self._adjusting: + if self._save_timer is not None: + self._save_timer.cancel() + self._save_timer = g15scheduler.schedule("SaveAccounts", 2, self._do_save_accounts) + + def _do_save_accounts(self): + self.account_mgr.save() + + def _update_time_changed(self, widget): + self.gconf_client.set_int(self.gconf_key + "/update_time", int(widget.get_value())) + + def _url_edited(self, widget, row_index, value): + row = self.account_model[row_index] + if value != "": + acc = self.account_mgr.by_name(row[0]) + if acc == None: + acc = G15Account(value, self.get_account_types()[0]) + self.account_mgr.accounts.append(acc) + else: + acc.name = value + self.save_accounts() + self._reload_model() + self.account_list.get_selection().select_path(row_index) + self._select_account() + else: + acc = self.account_mgr.by_name(row[0]) + if acc is not None: + self.account_mgr.accounts.remove(acc) + self._reload_model() + + def _urls_changed(self, client, connection_id, entry, args): + self._reload_model() + + def _reload_model(self): + acc = self._get_selected_account() + self.account_model.clear() + for i in range(0, len(self.account_mgr.accounts)): + account = self.account_mgr.accounts[i] + row = [ account.name, True ] + self.account_model.append(row) + if account == acc: + self.account_list.get_selection().select_path(i) + + (model, sel) = self.account_list.get_selection().get_selected() + if sel == None: + self.account_list.get_selection().select_path(0) + + def _new_url(self, widget): + self.account_model.append(["", True]) + self.account_list.set_cursor_on_cell(str(len(self.account_model) - 1), focus_column=self.account_list.get_column(0), focus_cell=self.url_renderer, start_editing=True) +# self.account_list.grab_focus() + + def _remove_url(self, widget): + (model, path) = self.account_list.get_selection().get_selected() + url = model[path][0] + acc = self.account_mgr.by_name(url) + if acc is not None: + self.account_mgr.accounts.remove(acc) + self.save_accounts() + self._reload_model() + self._load_options_for_type() + + def _type_changed(self, widget): + sel = self._get_selected_type() + acc = self._get_selected_account() + if acc.type != sel: + acc.type = sel + self.save_accounts() + self._load_options_for_type() + + def _load_options_for_type(self): + account_type = self._get_selected_type() + acc = self._get_selected_account() + options = self.create_options_for_type(acc, account_type) if acc is not None else None + if self.visible_options != None: + self.visible_options.component.destroy() + self.visible_options = options + place_holder = self.widget_tree.get_object("PlaceHolder") + for c in place_holder.get_children(): + place_holder.remove(c) + if self.visible_options is not None: + self.visible_options.component.reparent(place_holder) + else: + l = gtk.Label("No options found for this account\ntype. Do you have all the required\nplugins installed?") + l.xalign = 0.5 + l.show() + place_holder.add(l) + + def _select_account(self, widget=None): + account = self._get_selected_account() + self._adjusting = True + if account != None: + self.account_type.set_sensitive(True) + self.widget_tree.get_object("PlaceHolder").set_visible(True) + for i in range(0, len(self.type_model)): + if self.type_model[i][0] == account.type: + self.account_type.set_active(i) + if self.account_type.get_active() == -1: + self.account_type.set_active(0) + self._load_options_for_type() + else: + self.account_type.set_sensitive(False) + self.widget_tree.get_object("PlaceHolder").set_visible(False) + self._adjusting = False + + def _get_selected_type(self): + active = self.account_type.get_active() + return None if active == -1 else self.type_model[active][0] + + def _get_selected_account(self): + (model, path) = self.account_list.get_selection().get_selected() + if path != None: + return self.account_mgr.by_name(model[path][0]) \ No newline at end of file diff --git a/src/gnome15/g15actions.py b/src/gnome15/g15actions.py new file mode 100644 index 0000000..979bb2a --- /dev/null +++ b/src/gnome15/g15actions.py @@ -0,0 +1,72 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Manages registration of 'actions'. Each device will support default bindings +to these actions based on the keys they have available. + +Additionally, plugins may register new actions that may be bound to macro +keys. +""" + +import g15driver + +""" +Some screen related actions that may be mapped to additional keys +""" +NEXT_SCREEN = "next-screen" +PREVIOUS_SCREEN = "previous-screen" +NEXT_BACKLIGHT = "next-backlight" +PREVIOUS_BACKLIGHT = "previous-backlight" +CANCEL_MACRO = "cancel-macro" + +""" +Global the plugins and other subsystems may add new actions too. The list +here is the minimum a device must support to be useful. +""" +actions = [ + g15driver.NEXT_SELECTION, + g15driver.PREVIOUS_SELECTION, + g15driver.NEXT_PAGE, + g15driver.PREVIOUS_PAGE, + g15driver.SELECT, + g15driver.VIEW, + g15driver.CLEAR, + g15driver.MENU, + g15driver.MEMORY_1, + g15driver.MEMORY_2, + g15driver.MEMORY_3, + NEXT_SCREEN, + PREVIOUS_SCREEN, + NEXT_BACKLIGHT, + PREVIOUS_BACKLIGHT, + CANCEL_MACRO + ] + + +class ActionBinding(): + """ + Created when an action is invoked and contains the keys that activated + the action (if any), the state they were in and the action ID + """ + def __init__(self, action, keys, state): + self.action = action + self.state = state + self.keys = keys + + def __cmp__(self, other): + f = cmp(self.keys, other.keys) + return f if f != 0 else cmp(self.state, other.state) \ No newline at end of file diff --git a/src/gnome15/g15config.py b/src/gnome15/g15config.py new file mode 100644 index 0000000..02da5d4 --- /dev/null +++ b/src/gnome15/g15config.py @@ -0,0 +1,1993 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2009-2012 Brett Smith +# Copyright (C) 2013 Gnome15 authors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import pango +import dbus.service +import os +import sys +import g15globals +import g15profile +import gconf +import g15pluginmanager +import g15driver +import g15desktop +import g15drivermanager +import g15macroeditor +import g15devices +import util.g15convert as g15convert +import util.g15scheduler as g15scheduler +import util.g15uigconf as g15uigconf +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15icontools as g15icontools +import g15theme +import colorpicker +import subprocess +import shutil +import zipfile +import time + +import logging +logger = logging.getLogger(__name__) + +# Upgrade +import g15upgrade +g15upgrade.upgrade() + +# Determine if appindicator is available, this decides the nature +# of the message displayed when the Gnome15 service is not running +HAS_APPINDICATOR=False +try : + import appindicator + appindicator.__path__ + HAS_APPINDICATOR=True +except Exception as e: + logger.debug('Could not load appindicator module', exc_info = e) + pass + +# Store the temporary profile icons here (for when the icon comes from a window, the filename is not known +icons_dir = os.path.join(g15globals.user_cache_dir, "macro_profiles") +g15os.mkdir_p(icons_dir) + +PALE_RED = gtk.gdk.Color(213, 65, 54) + + +BUS_NAME="org.gnome15.Configuration" +NAME="/org/gnome15/Config" +IF_NAME="org.gnome15.Config" + +STOPPED = 0 +STARTING = 1 +STARTED = 2 +STOPPING = 3 + +class G15ConfigService(dbus.service.Object): + """ + DBUS Service used to prevent g15-config from running more than once. Each run will + test if this service is available, if it is, then the Present function will be + called and the runtime exited. + """ + + def __init__(self, config): + self._config = config + bus_name = dbus.service.BusName(BUS_NAME, bus=config.session_bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, bus_name, NAME) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='') + def Present(self): + self._config.main_window.present() + + @dbus.service.method(IF_NAME, in_signature='s', out_signature='') + def PresentWithDeviceUID(self, device_uid): + self._config._default_device_name = device_uid + self._config._load_devices() + self._config.main_window.present() + +class G15GlobalConfig: + + def __init__(self, parent, widget_tree, conf_client): + self.widget_tree = widget_tree + self.conf_client = conf_client + self.selected_id = None + + only_show_indicator_on_error = self.widget_tree.get_object("OnlyShowIndicatorOnError") + start_desktop_service_on_login = self.widget_tree.get_object("StartDesktopServiceOnLogin") + start_indicator_on_login = self.widget_tree.get_object("StartIndicatorOnLogin") + start_system_tray_on_login = self.widget_tree.get_object("StartSystemTrayOnLogin") + global_plugin_enabled_renderer = self.widget_tree.get_object("GlobalPluginEnabledRenderer") + enable_gnome_shell_extension = self.widget_tree.get_object("EnableGnomeShellExtension") + + self.dialog = self.widget_tree.get_object("GlobalOptionsDialog") + self.global_plugin_model = self.widget_tree.get_object("GlobalPluginModel") + self.global_plugin_tree = self.widget_tree.get_object("GlobalPluginTree") + self.global_plugin_tree.connect("cursor-changed", self._select_plugin) + + self.widget_tree.get_object("GlobalPreferencesButton").connect("clicked", self._show_preferences) + self.widget_tree.get_object("GlobalAboutPluginButton").connect("clicked", self._show_about_plugin) + start_desktop_service_on_login.connect("toggled", self._change_desktop_service, "gnome15") + start_indicator_on_login.connect("toggled", self._change_desktop_service, "g15-indicator") + start_system_tray_on_login.connect("toggled", self._change_desktop_service, "g15-systemtray") + enable_gnome_shell_extension.connect("toggled", self._change_gnome_shell_extension) + global_plugin_enabled_renderer.connect("toggled", self._toggle_plugin) + + # Service options + gnome_shell = g15desktop.get_desktop() == "gnome-shell" + shell_extension_installed = g15desktop.is_shell_extension_installed("gnome15-shell-extension@gnome15.org") + only_show_indicator_on_error.set_visible(g15desktop.is_desktop_application_installed("g15-indicator") and not gnome_shell) + start_indicator_on_login.set_visible(g15desktop.is_desktop_application_installed("g15-indicator") and not gnome_shell) + start_system_tray_on_login.set_visible(g15desktop.is_desktop_application_installed("g15-systemtray") and not gnome_shell) + enable_gnome_shell_extension.set_visible(gnome_shell and shell_extension_installed) + start_desktop_service_on_login.set_active(g15desktop.is_autostart_application("gnome15")) + start_indicator_on_login.set_active(g15desktop.is_autostart_application("g15-indicator")) + start_system_tray_on_login.set_active(g15desktop.is_autostart_application("g15-systemtray")) + enable_gnome_shell_extension.set_active(g15desktop.is_gnome_shell_extension_enabled("gnome15-shell-extension@gnome15.org")) + + self.dialog.set_transient_for(parent) + + def run(self): + notify_h = self.conf_client.notify_add("/apps/gnome15/global/plugins", self._plugins_changed) + # Plugins + self._load_plugins() + + if len(self.global_plugin_model) == 0: + self.widget_tree.get_object("GlobalPluginsFrame").set_visible(False) + self.dialog.set_size_request(-1, -1) + elif self._get_selected_plugin() == None: + self.global_plugin_tree.get_selection().select_path(self.global_plugin_model.get_path(self.global_plugin_model.get_iter(0))) + self._select_plugin() + + self.dialog.run() + self.dialog.hide() + self.conf_client.notify_remove(notify_h) + + def _show_about_plugin(self, widget): + plugin = self._get_selected_plugin() + dialog = self.widget_tree.get_object("AboutPluginDialog") + dialog.set_title("About %s" % plugin.name) + dialog.run() + dialog.hide() + + def _show_preferences(self, widget): + plugin = self._get_selected_plugin() + plugin.show_preferences(self.dialog, None, self.conf_client, "/apps/gnome15/global/plugins/%s" % plugin.id) + + def _get_selected_plugin(self): + (model, path) = self.global_plugin_tree.get_selection().get_selected() + if path != None: + return g15pluginmanager.get_module_for_id(model[path][3]) + + def _select_plugin(self, widget = None): + plugin = self._get_selected_plugin() + if plugin != None: + self.selected_id = plugin.id + self.widget_tree.get_object("GlobalPluginNameLabel").set_text(plugin.name) + self.widget_tree.get_object("GlobalDescriptionLabel").set_text(plugin.description) + self.widget_tree.get_object("GlobalDescriptionLabel").set_use_markup(True) + self.widget_tree.get_object("AuthorLabel").set_text(plugin.author) + self.widget_tree.get_object("SupportedLabel").set_text(", ".join(g15pluginmanager.get_supported_models(plugin)).upper()) + self.widget_tree.get_object("CopyrightLabel").set_text(plugin.copyright) + self.widget_tree.get_object("SiteLabel").set_uri(plugin.site) + self.widget_tree.get_object("SiteLabel").set_label(plugin.site) + self.widget_tree.get_object("GlobalPreferencesButton").set_sensitive(plugin.has_preferences) + self.widget_tree.get_object("GlobalPluginDetails").set_visible(True) + else: + self.widget_tree.get_object("GlobalPluginDetails").set_visible(False) + + def _load_plugins(self): + self.global_plugin_model.clear() + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + if g15pluginmanager.is_global_plugin(mod): + passive = g15pluginmanager.is_passive_plugin(mod) + enabled = passive or self.conf_client.get_bool("/apps/gnome15/global/plugins/%s/enabled" % mod.id ) + self.global_plugin_model.append([enabled, not passive, mod.name, mod.id]) + if mod.id == self.selected_id: + self.global_plugin_tree.get_selection().select_path(self.global_plugin_model.get_path(self.global_plugin_model.get_iter(len(self.global_plugin_model) - 1))) + + def _plugins_changed(self, client, connection_id, entry, args): + self._load_plugins() + + def _change_gnome_shell_extension(self, widget): + g15desktop.set_gnome_shell_extension_enabled("gnome15-shell-extension@gnome15.org", widget.get_active()) + + def _change_desktop_service(self, widget, application_name): + g15desktop.set_autostart_application(application_name, widget.get_active()) + + def _toggle_plugin(self, widget, path): + plugin = g15pluginmanager.get_module_for_id(self.global_plugin_model[path][3]) + if plugin != None: + key = "/apps/gnome15/global/plugins/%s/enabled" % plugin.id + self.conf_client.set_bool(key, not self.conf_client.get_bool(key)) + + +class G15Config: + + """ + Configuration user interface for Gnome15. Allows selection and configuration + of the device, macros and enabled plugins. + """ + + adjusting = False + + def __init__(self, parent_window=None, service=None, options=None): + self.parent_window = parent_window + self._options = options + self._controls_visible = False + self.profile_save_timer = None + self._signal_handles = [] + self.notify_handles = [] + self.control_notify_handles = [] + self.selected_id = None + self.service = service + self.conf_client = gconf.client_get_default() + self.rows = None + self.adjusting = False + self.gnome15_service = None + self.connected = False + self.color_button = None + self.screen_services = {} + self.state = STOPPED + self.driver = None + self.selected_device = None + self._last_no_devices = -1 + + # Load main Glade file + g15locale.get_translation("g15-config") + g15Config = os.path.join(g15globals.ui_dir, 'g15-config.ui') + self.widget_tree = gtk.Builder() + self.widget_tree.set_translation_domain("g15-config") + self.widget_tree.add_from_file(g15Config) + self.main_window = self.widget_tree.get_object("MainWindow") + + # Make sure there is only one g15config running + self.session_bus = dbus.SessionBus() + try : + G15ConfigService(self) + except dbus.exceptions.NameExistsException as e: + logger.debug("D-Bus service already running", exc_info = e) + if self._options is not None and self._options.device_uid != "": + self.session_bus.get_object(BUS_NAME, NAME).PresentWithDeviceUID(self._options.device_uid) + else: + self.session_bus.get_object(BUS_NAME, NAME).Present() + self.session_bus.close() + g15profile.notifier.stop() + sys.exit() + + # Get the initially selected device + self._default_device_name = self.conf_client.get_string("/apps/gnome15/config_device_name") \ + if self._options is None or self._options.device_uid == "" else self._options.device_uid + + # Widgets + self.site_label = self.widget_tree.get_object("SiteLabel") + self.cycle_screens = self.widget_tree.get_object("CycleScreens") + self.cycle_screens_options = self.widget_tree.get_object("CycleScreensOptions") + self.cycle_seconds = self.widget_tree.get_object("CycleAdjustment") + self.cycle_seconds_widget = self.widget_tree.get_object("CycleSeconds") + self.plugin_model = self.widget_tree.get_object("PluginModel") + self.plugin_tree = self.widget_tree.get_object("PluginTree") + self.plugin_enabled_renderer = self.widget_tree.get_object("PluginEnabledRenderer") + self.main_vbox = self.widget_tree.get_object("MainVBox") + self.profiles_tree = self.widget_tree.get_object("ProfilesTree") + self.profileNameColumn = self.widget_tree.get_object("ProfileName") + self.keyNameColumn = self.widget_tree.get_object("KeyName") + self.macroNameColumn = self.widget_tree.get_object("MacroName") + self.macro_list = self.widget_tree.get_object("MacroList") + self.application = self.widget_tree.get_object("ApplicationLocation") + self.m1 = self.widget_tree.get_object("M1") + self.m2 = self.widget_tree.get_object("M2") + self.m3 = self.widget_tree.get_object("M3") + self.window_model = self.widget_tree.get_object("WindowModel") + self.window_combo = self.widget_tree.get_object("WindowCombo") + self.window_entry = self.widget_tree.get_object("WindowEntry") + self.window_name = self.widget_tree.get_object("WindowName") + self.window_select = self.widget_tree.get_object("WindowSelect") + self.context_remove_profile = self.widget_tree.get_object("ContextRemoveProfile") + self.context_activate_profile = self.widget_tree.get_object("ContextActivateProfile") + self.context_lock_profile = self.widget_tree.get_object("LockProfile") + self.context_unlock_profile = self.widget_tree.get_object("UnlockProfile") + self.activate_on_focus = self.widget_tree.get_object("ActivateProfileOnFocusCheckbox") + self.macro_name_renderer = self.widget_tree.get_object("MacroNameRenderer") + self.profile_name_renderer = self.widget_tree.get_object("ProfileNameRenderer") + self.window_label = self.widget_tree.get_object("WindowLabel") + self.activate_by_default = self.widget_tree.get_object("ActivateByDefaultCheckbox") + self.send_delays = self.widget_tree.get_object("SendDelaysCheckbox") + self.fixed_delays = self.widget_tree.get_object("FixedDelaysCheckbox") + self.release_delay = self.widget_tree.get_object("ReleaseDelay") + self.press_delay = self.widget_tree.get_object("PressDelay") + self.press_delay_adjustment = self.widget_tree.get_object("PressDelayAdjustment") + self.release_delay_adjustment = self.widget_tree.get_object("ReleaseDelayAdjustment") + self.profile_icon = self.widget_tree.get_object("ProfileIcon") + self.background = self.widget_tree.get_object("Background") + self.background_label = self.widget_tree.get_object("BackgroundLabel") + self.icon_browse_button = self.widget_tree.get_object("BrowseForIcon") + self.background_browse_button = self.widget_tree.get_object("BrowseForBackground") + self.clear_icon_button = self.widget_tree.get_object("ClearIcon") + self.clear_background_button = self.widget_tree.get_object("ClearBackground") + self.macro_properties_button = self.widget_tree.get_object("MacroPropertiesButton") + self.new_macro_button = self.widget_tree.get_object("NewMacroButton") + self.delete_macro_button = self.widget_tree.get_object("DeleteMacroButton") + self.memory_bank_vbox = self.widget_tree.get_object("MemoryBankVBox") + self.macros_model = self.widget_tree.get_object("MacroModel") + self.mapped_key_model = self.widget_tree.get_object("MappedKeyModel") + self.profiles_model = self.widget_tree.get_object("ProfileModel") + self.profiles_context_menu = self.widget_tree.get_object("ProfileContextMenu") + self.device_model = self.widget_tree.get_object("DeviceModel") + self.device_view = self.widget_tree.get_object("DeviceView") + self.main_pane = self.widget_tree.get_object("MainPane") + self.main_parent = self.widget_tree.get_object("MainParent") + self.device_title = self.widget_tree.get_object("DeviceTitle") + self.device_enabled = self.widget_tree.get_object("DeviceEnabled") + self.tabs = self.widget_tree.get_object("Tabs") + self.stop_service_button = self.widget_tree.get_object("StopServiceButton") + self.driver_model = self.widget_tree.get_object("DriverModel") + self.driver_combo = self.widget_tree.get_object("DriverCombo") + self.global_options_button = self.widget_tree.get_object("GlobalOptionsButton") + self.macro_edit_close_button = self.widget_tree.get_object("MacroEditCloseButton") + self.key_table = self.widget_tree.get_object("KeyTable") + self.key_frame = self.widget_tree.get_object("KeyFrame") + self.memory_bank = self.widget_tree.get_object("MemoryBank") + self.macros_tab = self.widget_tree.get_object("MacrosTab") + self.macros_tab_label = self.widget_tree.get_object("MacrosTabLabel") + self.keyboard_tab = self.widget_tree.get_object("KeyboardTab") + self.plugins_tab = self.widget_tree.get_object("PluginsTab") + self.profile_plugins_tab = self.widget_tree.get_object("ProfilePluginsTab") + self.parent_profile_box = self.widget_tree.get_object("ParentProfileBox") + self.parent_profile_label = self.widget_tree.get_object("ParentProfileLabel") + self.parent_profile_model = self.widget_tree.get_object("ParentProfileModel") + self.parent_profile_combo = self.widget_tree.get_object("ParentProfileCombo") + self.profile_author = self.widget_tree.get_object("ProfileAuthor") + self.export_profile = self.widget_tree.get_object("Export") + self.import_profile = self.widget_tree.get_object("ImportButton") + self.information_content = self.widget_tree.get_object("InformationContent") + self.delays_content = self.widget_tree.get_object("DelaysContent") + self.activation_content = self.widget_tree.get_object("ActivationContent") + self.launch_pattern_box = self.widget_tree.get_object("LaunchPatternBox") + self.activate_on_launch = self.widget_tree.get_object("ActivateOnLaunch") + self.launch_pattern = self.widget_tree.get_object("LaunchPattern") + self.theme_model = self.widget_tree.get_object("ThemeModel") + self.theme_label = self.widget_tree.get_object("ThemeLabel") + self.theme_combo = self.widget_tree.get_object("ThemeCombo") + self.profile_plugins_mode_model = self.widget_tree.get_object("ProfilePluginsModeModel") + self.profile_plugins_mode = self.widget_tree.get_object("ProfilePluginsMode") + self.enabled_profile_plugins_model = self.widget_tree.get_object("EnabledProfilePluginsModel") + self.enabled_profile_plugins = self.widget_tree.get_object("EnabledProfilePlugins") + self.enabled_profile_plugins_renderer = self.widget_tree.get_object("EnabledProfilePluginsRenderer") + self.device_settings = self.widget_tree.get_object("DeviceSettings") + self.no_device_selected = self.widget_tree.get_object("NoDeviceSelected") + self.no_driver_available = self.widget_tree.get_object("NoDriverAvailable") + self.driver_options = self.widget_tree.get_object("DriverOptions") + + # Window + self.main_window.set_transient_for(self.parent_window) + self.main_window.set_icon_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15")) + + # Monitor gconf + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + + # Monitor macro profiles changing + g15profile.profile_listeners.append(self._profiles_changed) + + # Configure widgets + self.profiles_tree.get_selection().set_mode(gtk.SELECTION_SINGLE) + self.macro_list.get_selection().set_mode(gtk.SELECTION_SINGLE) + + # Indicator options + # TODO move this out of here + g15uigconf.configure_checkbox_from_gconf(self.conf_client, "/apps/gnome15/indicate_only_on_error", "OnlyShowIndicatorOnError", False, self.widget_tree, True) + + # Bind to events + self.cycle_seconds.connect("value-changed", self._cycle_seconds_changed) + self.cycle_screens.connect("toggled", self._cycle_screens_changed) + self.site_label.connect("activate", self._open_site) + self.plugin_tree.connect("cursor-changed", self._select_plugin) + self.plugin_enabled_renderer.connect("toggled", self._toggle_plugin) + self.enabled_profile_plugins_renderer.connect("toggled", self._toggle_enabled_profile_plugins) + self.widget_tree.get_object("PreferencesButton").connect("clicked", self._show_preferences) + self.widget_tree.get_object("AboutPluginButton").connect("clicked", self._show_about_plugin) + self.widget_tree.get_object("AddButton").connect("clicked", self._add_profile) + self.widget_tree.get_object("ContextDuplicateProfile").connect("activate", self._copy_profile) + self.context_activate_profile.connect("activate", self._activate) + self.widget_tree.get_object("ContextExportProfile").connect("activate", self._export) + self.context_unlock_profile.connect("activate", self._unlock_profile) + self.context_lock_profile.connect("activate", self._lock_profile) + self.activate_on_focus.connect("toggled", self._activate_on_focus_changed) + self.activate_by_default.connect("toggled", self._activate_on_focus_changed) + self.clear_icon_button.connect("clicked", self._clear_icon) + self.clear_background_button.connect("clicked", self._clear_icon) + self.delete_macro_button.connect("clicked", self._remove_macro) + self.icon_browse_button.connect("clicked", self._browse_for_icon) + self.background_browse_button.connect("clicked", self._browse_for_icon) + self.macro_properties_button.connect("clicked", self._macro_properties) + self.new_macro_button.connect("clicked", self._new_macro) + self.macro_list.connect("cursor-changed", self._select_macro) + self.macro_name_renderer.connect("edited", self._macro_name_edited) + self.profile_name_renderer.connect("edited", self._profile_name_edited) + self.m1.connect("toggled", self._memory_changed) + self.m2.connect("toggled", self._memory_changed) + self.m3.connect("toggled", self._memory_changed) + self.profiles_tree.connect("cursor-changed", self._select_profile) + self.profiles_tree.connect("button-press-event", self._show_profile_list_context) + self.context_remove_profile.connect("activate", self._remove_profile) + self.send_delays.connect("toggled", self._send_delays_changed) + self.fixed_delays.connect("toggled", self._send_delays_changed) + self.press_delay_adjustment.connect("value-changed", self._send_delays_changed) + self.release_delay_adjustment.connect("value-changed", self._send_delays_changed) + self.window_select.connect("clicked", self._select_window) + self.window_name.connect("changed", self._window_name_changed) + self.window_combo.connect("changed", self._window_name_changed) + self.parent_profile_combo.connect("changed", self._parent_profile_changed) + self.m1.connect("toggled", self._memory_changed) + self.profile_author.connect("changed", self._profile_author_changed) + self.stop_service_button.connect("clicked", self._stop_service) + self.export_profile.connect("clicked", self._export) + self.device_view.connect("selection-changed", self._device_selection_changed) + self.device_enabled.connect("toggled", self._device_enabled_changed) + self.driver_combo.connect("changed", self._driver_changed) + self.theme_combo.connect("changed", self._theme_changed) + self.profile_plugins_mode.connect("changed", self._profile_plugins_mode_changed) + self.global_options_button.connect("clicked", self._show_global_options) + self.macro_list.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.macro_list.connect("button_press_event", self._macro_list_clicked) + self.import_profile.connect("clicked", self._import_profile) + self.driver_options.connect('clicked', self._show_driver_options) + + # Enable profiles to be dropped onto the list + self.macro_list.enable_model_drag_dest([('text/plain', 0, 0)], + gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY) + self.macro_list.connect("drag-data-received", self._macro_profile_dropped) + + # Connection to BAMF for running applications list + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + except Exception as e: + logger.warning("BAMF not available, falling back to WNCK", exc_info = e) + self.bamf_matcher = None + import wnck + self.screen = wnck.screen_get_default() + + # Show infobar component to start desktop service if it is not running + self.infobar = gtk.InfoBar() + self.infobar.set_size_request(-1, 64) + self.warning_label = gtk.Label() + self.warning_label.set_size_request(400, -1) + self.warning_label.set_line_wrap(True) + self.warning_label.set_alignment(0.0, 0.0) + self.warning_image = gtk.Image() + + # Start button + self.stop_service_button.set_sensitive(False) + button_vbox = gtk.VBox() + self.start_button = None + self.start_button = gtk.Button(_("Start Service")) + self.start_button.connect("clicked", self._start_service) + self.start_button.show() + button_vbox.pack_start(self.start_button, False, False) + + # Populate model and configure other components + self._load_devices() + if len(self.device_model) == 0: + raise Exception(_("No supported devices could be found. Is the " + \ + "device correctly plugged in and powered and " + \ + "do you have all the required drivers installed?")) + else: + if len(self.device_model) == 1 and not g15devices.is_enabled(self.conf_client, self.selected_device): + self.device_enabled.set_active(True) + + # Build the infobar content + content = self.infobar.get_content_area() + content.pack_start(self.warning_image, False, False) + content.pack_start(self.warning_label, True, True) + content.pack_start(button_vbox, False, False) + + # Add the bar to the glade built UI + self.main_vbox.pack_start(self.infobar, False, False) + self.warning_box_shown = False + self.infobar.hide_all() + + self.gnome15_service = None + + # Watch for Gnome15 starting and stopping + try : + self._connect() + except dbus.exceptions.DBusException as e: + logger.debug("Failed to connect to service.", exc_info = e) + self._disconnect() + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + # Watch for new devices (if pyudev is installed) + g15devices.device_added_listeners.append(self._devices_changed) + g15devices.device_removed_listeners.append(self._devices_changed) + + def run(self): + ''' Set up everything and display the window + ''' + if len(self.devices) > 1: + self.main_window.set_size_request(800, 600) + else: + self.main_window.set_size_request(640, 600) + self.id = None + while True: + opt = self.main_window.run() + logger.debug("Option %s", str(opt)) + if opt != 1 and opt != 2: + break + + self.main_window.hide() + g15profile.notifier.stop() + + ''' + Private + ''' + def _devices_changed(self, device = None): + self._load_devices() + + def _name_owner_changed(self, name, old_owner, new_owner): + if name == "org.gnome15.Gnome15": + if old_owner == "" and not self.connected: + self._connect() + elif old_owner != "" and self.connected: + self._disconnect() + + def __del__(self): + self._remove_notify_handles() + + def _remove_notify_handles(self): + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + def _stop_service(self, event = None): + self.gnome15_service.Stop(reply_handler = self._general_dbus_reply, error_handler = self._general_dbus_error) + + def _general_dbus_reply(self, *args): + logger.info("DBUS reply %s", str(args)) + + def _general_dbus_error(self, *args): + logger.error("DBUS error %s", str(args)) + + def _starting(self): + logger.debug("Got starting signal") + self.state = STARTING + self._status_change() + + def _started(self): + logger.debug("Got started signal") + self.state = STARTED + self._status_change() + + def _stopping(self): + logger.debug("Got stopping signal") + self.state = STOPPING + self._status_change() + + def _stopped(self): + logger.debug("Got stopped signal") + self.state = STOPPED + self._status_change() + + def _disconnect(self): + for sig in self._signal_handles: + self.session_bus.remove_signal_receiver(sig) + self._signal_handles = [] + self.screen_services = {} + self.state = STOPPED + self._do_status_change() + self.connected = False + + def _connect(self): + self.gnome15_service = self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + + # Set initial status + logger.debug("Getting state") + if self.gnome15_service.IsStarting(): + logger.debug("State is starting") + self.state = STARTING + elif self.gnome15_service.IsStopping(): + logger.debug("State is stopping") + self.state = STOPPING + else: + logger.debug("State is started") + self.state = STARTED + for screen_name in self.gnome15_service.GetScreens(): + logger.debug("Screen added %s", screen_name) + screen_service = self.session_bus.get_object('org.gnome15.Gnome15', screen_name) + self.screen_services[screen_name] = screen_service + + # Watch for changes + self._signal_handles.append(self.session_bus.add_signal_receiver(self._starting, dbus_interface="org.gnome15.Service", signal_name='Starting')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._started, dbus_interface="org.gnome15.Service", signal_name='Started')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._stopping, dbus_interface="org.gnome15.Service", signal_name='Stopping')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._stopped, dbus_interface="org.gnome15.Service", signal_name='Stopped')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._screen_added, dbus_interface="org.gnome15.Service", signal_name='ScreenAdded')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._screen_removed, dbus_interface="org.gnome15.Service", signal_name='ScreenRemoved')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='Connected')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='ConnectionFailed')) + self._signal_handles.append(self.session_bus.add_signal_receiver(self._status_change, dbus_interface="org.gnome15.Screen", signal_name='Disconnected')) + self.connected = True + self._do_status_change() + + def _screen_added(self, screen_name): + logger.debug("Screen added %s", screen_name) + screen_service = self.session_bus.get_object('org.gnome15.Gnome15', screen_name) + self.screen_services[screen_name] = screen_service + gobject.idle_add(self._do_status_change) + + def _screen_removed(self, screen_name): + logger.debug("Screen removed %s", screen_name) + if screen_name in self.screen_services: + del self.screen_services[screen_name] + self._do_status_change() + + def _status_change(self, arg1 = None, arg2 = None, arg3 = None): + gobject.idle_add(self._do_status_change) + + def _do_status_change(self): + if not self.gnome15_service or self.state == STOPPED: + self.stop_service_button.set_sensitive(False) + logger.debug("Stopped") + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is not running. It is recommended " + \ + "you add g15-desktop-service as a Startup Application.")) + elif self.state == STARTING: + logger.debug("Starting up") + self.stop_service_button.set_sensitive(False) + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is starting up. Please wait"), False) + elif self.state == STOPPING: + logger.debug("Stopping") + self.stop_service_button.set_sensitive(False) + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is stopping."), False) + else: + logger.debug("Started - Checking status") + connected = 0 + first_error = "" + for screen in self.screen_services: + try: + if self.screen_services[screen].IsConnected(): + connected += 1 + else: + first_error = self.screen_services[screen].GetLastError() + except dbus.DBusException as e: + logger.debug("D-Bus communication error", exc_info = e) + pass + + logger.debug("Found %d of %d connected", connected, len(self.screen_services)) + screen_count = len(self.screen_services) + if connected != screen_count and first_error is not None and first_error != "": + if len(self.screen_services) == 1: + self._show_message(gtk.MESSAGE_WARNING, _("The Gnome15 desktop service is running, but failed to connect " + \ + "to the keyboard driver. The error message given was %s") % first_error, False) + else: + mesg = ("The Gnome15 desktop service is running, but only %d out of %d keyboards are connected. The first error message given was %s") % ( connected, screen_count, first_error ) + self._show_message(gtk.MESSAGE_WARNING, mesg, False) + else: + self._hide_warning() + self.stop_service_button.set_sensitive(True) + + def _hide_warning(self): + self.warning_box_shown = False + self.infobar.hide_all() + self.main_window.check_resize() + + def _start_service(self, widget): + widget.set_sensitive(False) + g15os.run_script("g15-desktop-service", ["-f"]) + + def _show_message(self, type, text, start_service_button = True): + self.infobar.set_message_type(type) + if self.start_button != None: + self.start_button.set_sensitive(True) + self.start_button.set_visible(start_service_button) + self.warning_label.set_text(text) + self.warning_label.set_use_markup(True) + + if type == gtk.MESSAGE_WARNING: + self.warning_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) +# self.warning_label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(0, 0, 0)) + + self.main_window.check_resize() + self.infobar.show_all() + if self.start_button != None and not start_service_button: + self.start_button.hide() + self.warning_box_shown = True + + def _open_site(self, widget): + subprocess.Popen(['xdg-open',widget.get_uri()]) + + def _to_rgb(self, string_rgb): + rgb = string_rgb.split(",") + return (int(rgb[0]), int(rgb[1]), int(rgb[2])) + + def _to_color(self, rgb): + return gtk.gdk.Color(rgb[0] <<8, rgb[1] <<8,rgb[2] <<8) + + def _color_chosen(self, widget, control): + color = widget.color + self.conf_client.set_string(self._get_full_key(control.id), "%d,%d,%d" % ( color[0],color[1],color[2])) + + def _control_changed(self, widget, control): + if control.hint & g15driver.HINT_SWITCH != 0: + val = 0 + if widget.get_active(): + val = 1 + self.conf_client.set_int(self._get_full_key(control.id), val) + else: + self.conf_client.set_int(self._get_full_key(control.id), int(widget.get_value())) + + def _show_preferences(self, widget): + plugin = self._get_selected_plugin() + plugin.show_preferences(self.main_window, self.driver, self.conf_client, self._get_full_key("plugins/%s" % plugin.id)) + + def _show_about_plugin(self, widget): + plugin = self._get_selected_plugin() + dialog = self.widget_tree.get_object("AboutPluginDialog") + dialog.set_title("About %s" % plugin.name) + dialog.run() + dialog.hide() + + def _load_macro_state(self): + device_info = g15devices.get_device_info(self.driver.get_model_name()) if self.driver is not None else None + self.macros_tab.set_visible(device_info is not None and device_info.macros) + self.macros_tab_label.set_visible(device_info is not None and device_info.macros) + + # Hide memory bank if there are no M-Keys + self.memory_bank.set_visible(self.driver != None and self.driver.has_memory_bank()) + + def _load_plugins(self): + """ + Loads what drivers and plugins are appropriate for the selected + device + """ + self.plugin_model.clear() + if self.selected_device: + # Plugins appropriate + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + key = self._get_full_key("plugins/%s/enabled" % mod.id ) + if self.driver and self.driver.get_model_name() in g15pluginmanager.get_supported_models(mod) and not g15pluginmanager.is_global_plugin(mod): + enabled = self.conf_client.get_bool(key) + self.plugin_model.append([enabled, mod.name, mod.id]) + if mod.id == self.selected_id: + self.plugin_tree.get_selection().select_path(self.plugin_model.get_path(self.plugin_model.get_iter(len(self.plugin_model) - 1))) + if len(self.plugin_model) > 0 and self._get_selected_plugin() == None: + self.plugin_tree.get_selection().select_path(self.plugin_model.get_path(self.plugin_model.get_iter(0))) + + self._select_plugin(None) + self._set_tab_status() + + def _load_drivers(self): + self.driver_model.clear() + if self.selected_device: + for driver_mod_key in list(g15drivermanager.imported_drivers): + driver_mod = g15drivermanager.imported_drivers[driver_mod_key] + try: + driver = driver_mod.Driver(self.selected_device) + if self.selected_device.model_id in driver.get_model_names(): + self.driver_model.append((driver_mod.id, driver_mod.name)) + except Exception as e: + logger.info("Failed to load driver.", exc_info = e) + + self.driver_combo.set_sensitive(len(self.driver_model) > 1) + self._set_driver_from_configuration() + + def _get_selected_plugin(self): + (model, path) = self.plugin_tree.get_selection().get_selected() + if path != None: + return g15pluginmanager.get_module_for_id(model[path][2]) + + def _toggle_enabled_profile_plugins(self, widget, path): + row = self.enabled_profile_plugins_model[path] + plugin_id = row[2] + plugin = g15pluginmanager.get_module_for_id(plugin_id) + if plugin != None: + if plugin.id in self.selected_profile.selected_plugins: + self.selected_profile.selected_plugins.remove(plugin.id) + else: + self.selected_profile.selected_plugins.append(plugin.id) + self._load_enabled_profile_plugins() + self._save_profile(self.selected_profile) + + def _toggle_plugin(self, widget, path): + plugin = g15pluginmanager.get_module_for_id(self.plugin_model[path][2]) + if plugin != None: + key = self._get_full_key("plugins/%s/enabled" % plugin.id ) + self.conf_client.set_bool(key, not self.conf_client.get_bool(key)) + + def _select_plugin(self, widget): + plugin = self._get_selected_plugin() + if plugin != None: + self.selected_id = plugin.id + self.widget_tree.get_object("PluginNameLabel").set_text(plugin.name) + self.widget_tree.get_object("DescriptionLabel").set_text(plugin.description) + self.widget_tree.get_object("DescriptionLabel").set_use_markup(True) + self.widget_tree.get_object("AuthorLabel").set_text(plugin.author) + self.widget_tree.get_object("SupportedLabel").set_text(", ".join(g15pluginmanager.get_supported_models(plugin)).upper()) + self.widget_tree.get_object("CopyrightLabel").set_text(plugin.copyright) + self.widget_tree.get_object("SiteLabel").set_uri(plugin.site) + self.widget_tree.get_object("SiteLabel").set_label(plugin.site) + self.widget_tree.get_object("PreferencesButton").set_sensitive(plugin.has_preferences and self.driver is not None) + self.widget_tree.get_object("PluginDetails").set_visible(True) + + themes = g15theme.get_themes(self.selected_device.model_id, plugin) + self.theme_model.clear() + if len(themes) > 1: + key = self._get_full_key("plugins/%s/theme" % plugin.id ) + plugin_theme = self.conf_client.get_string(key) + if plugin_theme is None: + plugin_theme = "default" + for i, t in enumerate(themes): + self.theme_model.append([t.theme_id,t.name]) + if t.theme_id == plugin_theme: + self.theme_combo.set_active(i) + self.theme_label.set_visible(True) + self.theme_combo.set_visible(True) + else: + self.theme_label.set_visible(False) + self.theme_combo.set_visible(False) + else: + self.widget_tree.get_object("PluginDetails").set_visible(False) + + # List the keys that are required for each action + for c in self.key_table.get_children(): + self.key_table.remove(c) + actions = g15pluginmanager.get_actions(plugin, self.selected_device) + rows = len(actions) + if rows > 0: + self.key_table.set_property("n-rows", rows) + row = 0 + active_profile = g15profile.get_active_profile(self.driver.device) if self.driver is not None else None + if active_profile is None: + logger.warning("No active profile found. It's possible the profile no longer exists, or is supplied with a plugin that cannot be found.") + else: + bindings = [] + for action_id in actions: + # First try the active profile to see if the action has been re-mapped + action_binding = None + for state in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + action_binding = active_profile.get_binding_for_action(state, action_id) + if action_binding is None: + # No other keys bound to action, try the device defaults + device_info = g15devices.get_device_info(self.driver.get_model_name()) + if action_id in device_info.action_keys: + action_binding = device_info.action_keys[action_id] + break + else: + break + + if action_binding is not None: + bindings.append(action_binding) + else: + logger.warning("Plugin %s requires an action that is not available (%s)", + plugin.id, action_id) + + bindings = sorted(bindings) + + for action_binding in bindings: + # If hold + label = gtk.Label("") + label.set_size_request(40, -1) + if action_binding.state == g15driver.KEY_STATE_HELD: + label.set_text(_("Hold")) + label.set_use_markup(True) + label.set_alignment(0.0, 0.5) + self.key_table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2); + label.show() + + # Keys + keys = gtk.HBox(spacing = 4) + for k in action_binding.keys: + fname = os.path.abspath("%s/key-%s.png" % (g15globals.image_dir, k)) + pixbuf = gtk.gdk.pixbuf_new_from_file(fname) + pixbuf = pixbuf.scale_simple(22, 14, gtk.gdk.INTERP_BILINEAR) + img = gtk.image_new_from_pixbuf(pixbuf) + img.show() + keys.add(img) + keys.show() + self.key_table.attach(keys, 1, 2, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2) + + # Text + label = gtk.Label(actions[action_binding.action]) + label.set_alignment(0.0, 0.5) + label.show() + self.key_table.attach(label, 2, 3, row, row + 1, xoptions = gtk.FILL, xpadding = 4, ypadding = 2) + row += 1 + + + if row > 0: + self.key_frame.set_visible(True) + else: + self.key_frame.set_visible(False) + + def _macro_profile_dropped(self, widget, context, x, y, selection, info, timestamp): +# print '\n'.join([str(t) for t in context.targets]) + return True + + def _set_cycle_seconds_value_from_configuration(self): + val = self.conf_client.get(self._get_full_key("cycle_seconds")) + time = 10 + if val != None: + time = val.get_int() + if time != self.cycle_seconds.get_value(): + self.cycle_seconds.set_value(time) + + def _set_cycle_screens_value_from_configuration(self): + val = g15gconf.get_bool_or_default(self.conf_client, self._get_full_key("cycle_screens"), True) + self.cycle_seconds_widget.set_sensitive(val) + if val != self.cycle_screens.get_active(): + self.cycle_screens.set_active(val) + + def _control_configuration_changed(self, client, connection_id, entry, args): + widget = args[1] + control = args[0] + if isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + widget.set_active(entry.value.get_int() == 1) + else: + widget.set_value(entry.value.get_int()) + else: + widget.set_color(self._to_rgb(entry.value.get_string())) + + def _cycle_screens_configuration_changed(self, client, connection_id, entry, args): + self._set_cycle_screens_value_from_configuration() + + def _cycle_seconds_configuration_changed(self, client, connection_id, entry, args): + self._set_cycle_seconds_value_from_configuration() + + def _plugins_changed(self, client, connection_id, entry, args): + self._load_plugins() + self._load_macro_state() + self._load_drivers() + self._load_enabled_profile_plugins() + + def _cycle_screens_changed(self, widget=None): + self.conf_client.set_bool(self._get_full_key("cycle_screens"), self.cycle_screens.get_active()) + + def _cycle_seconds_changed(self, widget): + val = int(self.cycle_seconds.get_value()) + self.conf_client.set_int(self._get_full_key("cycle_seconds"), val) + + def _create_color_icon(self, color): + draw = gtk.Image() + pixmap = gtk.gdk.Pixmap(None, 16, 16, 24) + cr = pixmap.cairo_create() + cr.set_source_rgb(float(color[0]) / 255.0, float(color[1]) / 255.0, float(color[2]) / 255.0) + cr.rectangle(0, 0, 16, 16) + cr.fill() + draw.set_from_pixmap(pixmap, None) + return draw + + def _active_profile_changed(self, client, connection_id, entry, args): + self._load_profile_list() + + def _send_delays_changed(self, widget=None): + if not self.adjusting: + self.selected_profile.send_delays = self.send_delays.get_active() + self.selected_profile.fixed_delays = self.fixed_delays.get_active() + self.selected_profile.press_delay = int(self.press_delay_adjustment.get_value() * 1000) + self.selected_profile.release_delay = int(self.release_delay_adjustment.get_value() * 1000) + self._save_profile(self.selected_profile) + self._set_delay_state() + + def _set_delay_state(self): + self.fixed_delays.set_sensitive(self.selected_profile.send_delays) + self.press_delay.set_sensitive(self.selected_profile.fixed_delays and self.selected_profile.send_delays) + self.release_delay.set_sensitive(self.selected_profile.fixed_delays and self.selected_profile.send_delays) + + def _activate_on_focus_changed(self, widget=None): + if not self.adjusting: + self.selected_profile.activate_on_focus = widget.get_active() + self._set_available_profile_actions() + self._save_profile(self.selected_profile) + + def _parent_profile_changed(self, widget): + if not self.adjusting: + sel = self.parent_profile_combo.get_active() + self.selected_profile.base_profile = self.parent_profile_model[sel][0] if sel > 0 else None + self._save_profile(self.selected_profile) + + def _window_name_changed(self, widget): + if isinstance(widget, gtk.ComboBox): + active = widget.get_active() + if active >= 0: + self.window_name.set_text(self.window_model[active][0]) + else: + if widget.get_text() != self.selected_profile.window_name: + self.selected_profile.window_name = widget.get_text() + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + if view.Name() == self.selected_profile.window_name: + icon = view.Icon() + if icon != None: + icon_path = g15icontools.get_icon_path(icon) + if icon_path != None: + # We need to copy the icon as it may be temporary + copy_path = os.path.join(icons_dir, os.path.basename(icon_path)) + shutil.copy(icon_path, copy_path) + self.selected_profile.icon = copy_path + self._set_image(self.profile_icon, copy_path) + else: + import wnck + for window in wnck.screen_get_default().get_windows(): + if window.get_name() == self.selected_profile.window_name: + icon = window.get_icon() + if icon != None: + filename = os.path.join(icons_dir,"%d.png" % self.selected_profile.id) + icon.save(filename, "png") + self.selected_profile.icon = filename + self._set_image(self.profile_icon, filename) + + self._save_profile(self.selected_profile) + + def _driver_configuration_changed(self, *args): + self._set_driver_from_configuration() + self._load_plugins() + self._add_controls() + + def _set_driver_from_configuration(self): + selected_driver = self.conf_client.get_string(self._get_full_key("driver")) + i = 0 + sel = False + for ( driver_id, driver_name ) in self.driver_model: + if driver_id == selected_driver: + self.driver_combo.set_active(i) + sel = True + i += 1 + if len(self.driver_model) > 0 and not sel: + self.conf_client.set_string(self._get_full_key("driver"), self.driver_model[0][0]) + else: + driver_mod = g15drivermanager.get_driver_mod(selected_driver) + + # Show or hide the Keyboard / Plugins tab depending on if there is a driver that matches + if not driver_mod: + self.no_driver_available.set_label(_("There is no appropriate driver for the " + \ + "device %s.\nDo you have all the " + \ + "required packages installed?") \ + % self.selected_device.model_fullname) + self.tabs.set_visible(False) + self.no_driver_available.set_visible(True) + else: + self.driver_options.set_sensitive(driver_mod.has_preferences) + self.tabs.set_visible(True) + self.no_driver_available.set_visible(False) + + def _show_driver_options(self, widget): + selected_driver = self.conf_client.get_string(self._get_full_key("driver")) + driver_mod = g15drivermanager.get_driver_mod(selected_driver) + driver_mod.show_preferences(self.selected_device, + self.main_window, + self.conf_client) + + def _set_tab_status(self): + self.keyboard_tab.set_visible(self._controls_visible) + self.plugins_tab.set_visible(len(self.plugin_model) > 0) + self.profile_plugins_tab.set_visible(len(self.plugin_model) > 0) + + def _driver_options_changed(self): + self._add_controls() + self._load_plugins() + self._load_macro_state() + self._hide_warning() + + def _device_enabled_configuration_changed(self, client, connection_id, entry, args): + self._set_enabled_value_from_configuration() + + def _set_enabled_value_from_configuration(self): + enabled = g15devices.is_enabled(self.conf_client, self.selected_device) if self.selected_device != None else False + self.device_enabled.set_active(enabled) + self.device_enabled.set_sensitive(self.selected_device != None) + self.tabs.set_sensitive(enabled) + + def _device_enabled_changed(self, widget = None): + gobject.idle_add(self._set_device) + + def _theme_changed(self, widget = None): + if not self.adjusting: + sel = widget.get_active() + if sel >= 0: + key = self._get_full_key("plugins/%s/theme" % self._get_selected_plugin().id ) + path = self.theme_model.get_iter(sel) + self.conf_client.set_string(key, self.theme_model[path][0]) + + def _driver_changed(self, widget = None): + if len(self.driver_model) > 0: + sel = self.driver_combo.get_active() + if sel >= 0: + row = self.driver_model[sel] + current = self.conf_client.get_string(self._get_full_key("driver")) + if not current or row[0] != current: + self.conf_client.set_string(self._get_full_key("driver"), row[0]) + + def _set_device(self): + if self.selected_device: + g15devices.set_enabled(self.conf_client, self.selected_device, self.device_enabled.get_active()) + + def _memory_changed(self, widget): + self._load_profile(self.selected_profile) + + def _device_selection_changed(self, widget): + self._load_device() + if self.selected_device: + self.conf_client.set_string("/apps/gnome15/config_device_name", self.selected_device.uid) + self.device_settings.set_visible(True) + self.no_device_selected.set_visible(False) + else: + self.device_settings.set_visible(False) + self.no_device_selected.set_visible(True) + + def _load_device(self): + sel_items = self.device_view.get_selected_items() + sel_idx = sel_items[0][0] if len(sel_items) > 0 else -1 + self.selected_device = self.devices[sel_idx] if sel_idx > -1 and sel_idx < len(self.devices) else None + if self.selected_device: + self._load_drivers() + self._remove_notify_handles() + self.device_title.set_text(self.selected_device.model_fullname if self.selected_device else "") + self._set_enabled_value_from_configuration() + if self.selected_device != None: + self.conf_client.add_dir(self._get_device_conf_key(), gconf.CLIENT_PRELOAD_NONE) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("cycle_seconds"), self._cycle_seconds_configuration_changed)); + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("cycle_screens"), self._cycle_screens_configuration_changed)); + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("plugins"), self._plugins_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("active_profile"), self._active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("locked"), self._active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("enabled"), self._device_enabled_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key("driver"), self._driver_configuration_changed)) + self.selected_profile = g15profile.get_active_profile(self.selected_device) + self._set_cycle_seconds_value_from_configuration() + self._set_cycle_screens_value_from_configuration() + self.selected_profile = None + self._add_controls() + self.main_window.show_all() + self._load_profile_list() + self._load_plugins() + self._load_macro_state() + self._load_windows() + self._do_status_change() + self._set_tab_status() + + def _get_device_conf_key(self): + return "/apps/gnome15/%s" % self.selected_device.uid + + def _get_full_key(self, key): + return "%s/%s" % (self._get_device_conf_key(), key) + + def _select_profile(self, widget): + (model, path) = self.profiles_tree.get_selection().get_selected() + self.selected_profile = g15profile.get_profile(self.selected_device, model[path][2]) + self._load_profile(self.selected_profile) + + def _select_macro(self, widget): + self._set_available_actions() + + def _set_available_actions(self): + (_, path) = self.macro_list.get_selection().get_selected() + self.delete_macro_button.set_sensitive(path != None and not self.selected_profile.read_only) + self.macro_properties_button.set_sensitive(path != None) + + def _set_available_profile_actions(self): + sel = self.profile_plugins_mode.get_active() + path = self.profile_plugins_mode_model.get_iter(sel) + self.enabled_profile_plugins.set_sensitive(not self.selected_profile.read_only and self.profile_plugins_mode_model[path][0] == g15profile.SELECTED_PLUGINS) + self.window_name.set_sensitive(not self.selected_profile.read_only and self.selected_profile.activate_on_focus) + self.window_select.set_sensitive(not self.selected_profile.read_only and self.selected_profile.activate_on_focus) + + def _activate(self, widget): + (model, path) = self.profiles_tree.get_selection().get_selected() + self._make_active(g15profile.get_profile(self.selected_device, model[path][2])) + + def _make_active(self, profile): + profile.make_active() + self._load_profile_list() + + def _profile_plugins_mode_changed(self, widget = None): + if not self.adjusting: + sel = widget.get_active() + path = self.profile_plugins_mode_model.get_iter(sel) + self.selected_profile.plugins_mode = self.profile_plugins_mode_model[path][0] + self._set_available_profile_actions() + self._save_profile(self.selected_profile) + + def _clear_icon(self, widget): + if widget == self.clear_icon_button: + self.selected_profile.icon = "" + self._set_image(self.profile_icon, "") + else: + self.selected_profile.background = "" + self._set_image(self.background, "") + self._save_profile(self.selected_profile) + + def _add_macro_filters(self, dialog): + macros_filter = gtk.FileFilter() + macros_filter.set_name("Macro Archives") + macros_filter.add_pattern("*.mzip") + dialog.add_filter(macros_filter) + all_filter = gtk.FileFilter() + all_filter.set_name("All files") + all_filter.add_pattern("*") + dialog.add_filter(all_filter) + + def _import_profile(self, widget): + dialog = gtk.FileChooserDialog("Import..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + self._add_macro_filters(dialog) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + import_filename = dialog.get_filename() + profile_dir = g15profile.get_profile_dir(self.selected_device) + file = zipfile.ZipFile(import_filename, "r") + + profile_id = g15profile.generate_profile_id() + + try: + everything_ok = False + error = "" + + zip_contents = file.namelist(); + + # Check if there is a macro file + macro_filename = "" + for filename in zip_contents: + if filename.endswith(".macros"): + everything_ok = True + macro_filename = filename + break + else: + error = "Invalid archive (missing .macros file)" + + if everything_ok: + # Parse and handle the macro file + file_split = macro_filename.split(".", 1) + + dest_name = "%d.%s" % ( profile_id, file_split[1]) + + # Read the profile so we can adjust for the new environment + profiles = g15profile.get_profiles(self.selected_device) + macro_file = file.open(macro_filename, 'r') + try: + imported_profile = g15profile.G15Profile(self.selected_device) + imported_profile.load(None, macro_file) + imported_profile.set_id(profile_id) + finally: + macro_file.close() + + if self.selected_device.model_id not in imported_profile.models: + everything_ok = False + error = "The profile you imported was made for another device." + + if everything_ok: + # Find the best new name for the profile + new_name = imported_profile.name + idx = 1 + while True: + found = False + for p in profiles: + if new_name == p.name: + found = True + break + if found: + idx += 1 + new_name = "%s (%d)" % (imported_profile.name, idx) + else: + break + imported_profile.name = new_name + + # Set the icons + if imported_profile.icon: + imported_profile.icon = "%s/%d.%s" % ( profile_dir, profile_id, imported_profile.icon.split(".", 1)[1] ) + if imported_profile.background: + imported_profile.background = "%s/%d.%s" % ( profile_dir, profile_id, imported_profile.background.split(".", 1)[1] ) + + # Actually save + g15profile.create_profile(imported_profile) + + # Import the other files + for filename in zip_contents: + file_split = filename.split(".", 1) + + dest_name = "%d.%s" % ( profile_id, file_split[1]) + + if not dest_name.endswith(".macros"): + # Just extract all other files + dest_dir = os.path.join(profile_dir, os.path.dirname(dest_name)) + g15os.mkdir_p(dest_dir) + macro_file = file.open(filename, 'r') + try: + out_file = open(os.path.join(dest_dir, os.path.basename(dest_name)), 'w') + try: + out_file.write(macro_file.read()) + finally: + out_file.close() + finally: + macro_file.close() + + # If there was an error when importing display an error message + if not everything_ok: + import_profile_error_dialog = self.widget_tree.get_object("ImportProfileError") + import_profile_error_dialog.set_transient_for(self.main_window) + import_profile_error_dialog.format_secondary_text(error) + import_profile_error_dialog_close_button = self.widget_tree.get_object("ImportProfileErrorCloseButton") + import_profile_error_dialog_close_button.connect("clicked", lambda x: import_profile_error_dialog.hide()) + import_profile_error_dialog.run() + + finally: + file.close() + + dialog.destroy() + + def _lock_profile(self, widget): + if g15profile.is_locked(self.selected_device): + g15profile.set_locked(self.selected_device, False) + if not self.selected_profile.is_active(): + self.selected_profile.make_active() + g15profile.set_locked(self.selected_device, True) + + def _unlock_profile(self, widget): + g15profile.set_locked(self.selected_device, False) + + def _export(self, widget): + dialog = gtk.FileChooserDialog("Export..", + None, + gtk.FILE_CHOOSER_ACTION_SAVE, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + dialog.set_filename(os.path.expanduser("~/%s.mzip" % self.selected_profile.name)) + self._add_macro_filters(dialog) + response = dialog.run() + if response == gtk.RESPONSE_OK: + export_file = dialog.get_filename() + if not export_file.lower().endswith(".mzip"): + export_file += ".mzip" + + self.selected_profile.export(export_file) + dialog.destroy() + + def _browse_for_icon(self, widget): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.main_window) + if widget == self.icon_browse_button: + dialog.set_filename(self.selected_profile.icon) + else: + dialog.set_filename(self.selected_profile.background) + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + dialog.add_filter(filter) + + response = dialog.run() + + if response == gtk.RESPONSE_OK: + if widget == self.icon_browse_button: + self.selected_profile.icon = dialog.get_filename() + self._set_image(self.profile_icon, self.selected_profile.icon) + else: + self.selected_profile.background = dialog.get_filename() + self._set_image(self.background, self.selected_profile.background) + self._save_profile(self.selected_profile) + + dialog.destroy() + + def _remove_profile(self, widget): + dialog = self.widget_tree.get_object("ConfirmRemoveProfileDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + active_profile = g15profile.get_active_profile(self.selected_device) + if active_profile is not None and self.selected_profile.id == active_profile.id: + if g15profile.is_locked(self.selected_device): + g15profile.set_locked(self.selected_device, False) + self._make_active(g15profile.get_profile(self.selected_device, 0)) + self.selected_profile.delete() + self.profiles.remove(self.selected_profile) + if len(self.profiles) > 0: + self.selected_profile = self.profiles[0] + self._load_profile_list() + + def _profile_author_changed(self, widget): + if not self.adjusting: + self.selected_profile.author = widget.get_text() + self._save_profile(self.selected_profile) + + def _new_macro(self, widget): + memory = self._get_memory_number() + + # Find the next free G-Key + use = None + for row in self.driver.get_key_layout(): + if not use: + for key in row: + reserved = g15devices.are_keys_reserved(self.driver.get_model_name(), list(key)) + in_use = self.selected_profile.are_keys_in_use(g15driver.KEY_STATE_UP, memory, [ key ]) + if not in_use and not reserved: + use = key + break + + if use: + macro = self.selected_profile.create_macro(memory, [use], + _("Macro %s") % " ".join(g15driver.get_key_names([use])), + g15profile.MACRO_SIMPLE, + "", + g15driver.KEY_STATE_UP) + self._edit_macro(macro) + else: + logger.warning("No free keys") + + def _macro_properties(self, widget): + self._edit_macro(self._get_selected_macro()) + + def _get_selected_macro(self): + (model, path) = self.macro_list.get_selection().get_selected() + if model and path: + row = model[path] + return self.selected_profile.get_macro(row[4], + self._get_memory_number(), + g15profile.get_keys_from_key(row[2])) + + def _select_window(self, widget): + dialog = self.widget_tree.get_object("SelectWindowDialog") + dialog.set_transient_for(self.main_window) + dialog.run() + dialog.hide() + + def _edit_macro(self, macro): + macro_editor = g15macroeditor.G15MacroEditor(self.main_window) + macro_editor.set_driver(self.driver) + macro_editor.set_macro(macro) + macro_editor.run() + + def _remove_macro(self, widget): + memory = self._get_memory_number() + (model, path) = self.macro_list.get_selection().get_selected() + key_list_key = model[path][2] + activate_on = model[path][4] + dialog = self.widget_tree.get_object("ConfirmRemoveMacroDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + keys = g15profile.get_keys_from_key(key_list_key) + self.selected_profile.delete_macro(activate_on, memory, keys) + self._load_profile_list() + + def _save_profile(self, profile): + if not self.adjusting: + if self.profile_save_timer is not None: + self.profile_save_timer.cancel() + self.profile_save_timer = g15scheduler.schedule("SaveProfile", 2, self._do_save_profile, profile) + + def _do_save_profile(self, profile): + logger.info("Saving profile %s", profile.name) + profile.save() + + global_config = None + def _show_global_options(self, widget): + if self.global_config is None: + self.global_config = G15GlobalConfig(self.main_window, self.widget_tree, self.conf_client) + self.global_config.run() + + def _add_profile(self, widget): + dialog = self.widget_tree.get_object("NewProfileDialog") + dialog.set_transient_for(self.main_window) + response = dialog.run() + dialog.hide() + if response == 1: + new_profile_name = self.widget_tree.get_object("NewProfileName").get_text() + new_profile = g15profile.G15Profile(self.selected_device, g15profile.generate_profile_id()) + new_profile.name = new_profile_name + g15profile.create_profile(new_profile) + self.selected_profile = g15profile.get_profile(self.selected_device, new_profile.id) + self._load_profile_list() + + def _copy_profile(self, widget): + dupe_profile = g15profile.get_profile(self.selected_device, self.selected_profile.id) + dialog = self.widget_tree.get_object("CopyProfileDialog") + dialog.set_transient_for(self.main_window) + + # Choose a default name for the copy + default_name = self.selected_profile.name + last_cb = default_name.rfind(")") + last_ob = default_name.rfind("(") + i = 0 + if last_cb >=0 and last_ob >0: + i = int(default_name[last_ob + 1:last_cb]) + default_name = default_name[:last_ob].strip() + new_name = default_name + while True: + p = g15profile.get_profile_by_name(self.selected_device, new_name) + if p is None: + break + i += 1 + new_name = "%s (%i)" % ( default_name, i ) + + self.widget_tree.get_object("CopiedProfileName").set_text(new_name) + response = dialog.run() + dialog.hide() + if response == 1: + dupe_profile.set_id(g15profile.generate_profile_id()) + dupe_profile.name = self.widget_tree.get_object("CopiedProfileName").get_text() + dupe_profile.save() + self.selected_profile = dupe_profile + self._load_profile_list() + + def _get_memory_number(self): + if self.m1.get_active(): + return 1 + elif self.m2.get_active(): + return 2 + elif self.m3.get_active(): + return 3 + + def _load_devices(self): + self.device_model.clear() + self.selected_device = None + self.devices = g15devices.find_all_devices() + previous_sel_device_name = self._default_device_name + sel_device_name = None + idx = 0 + for device in self.devices: + if device.model_id == 'virtual': + icon_file = g15icontools.get_icon_path(["preferences-system-window", "preferences-system-windows", "gnome-window-manager", "window_fullscreen"]) + else: + icon_file = g15icontools.get_app_icon(self.conf_client, device.model_id) + pixb = gtk.gdk.pixbuf_new_from_file(icon_file) + self.device_model.append([pixb.scale_simple(96, 96, gtk.gdk.INTERP_BILINEAR), device.model_fullname, 96, gtk.WRAP_WORD, pango.ALIGN_CENTER]) + if previous_sel_device_name is not None and device.uid == previous_sel_device_name: + sel_device_name = device.uid + self.device_view.select_path((idx,)) + idx += 1 + if sel_device_name is None and len(self.devices) > 0: + sel_device_name = self.devices[0].uid + self.device_view.select_path((0,)) + + if idx != self._last_no_devices: + if idx == 1: + self.widget_tree.get_object("MainScrolledWindow").set_visible(False) + self.widget_tree.get_object("DeviceDetails").set_visible(False) + else: + self.widget_tree.get_object("MainScrolledWindow").set_visible(True) + self.widget_tree.get_object("DeviceDetails").set_visible(True) + # Hide the device settings if no device is selected + if sel_device_name is None: + self.device_settings.set_visible(False) + self.no_device_selected.set_visible(True) + + def _load_profile_list(self): + current_selection = self.selected_profile + self.profiles_model.clear() + if self.selected_device != None: + tree_selection = self.profiles_tree.get_selection() + active = g15profile.get_active_profile(self.selected_device) + active_id = "" + if active != None: + active_id = active.id + self.selected_profile = None + default_profile = g15profile.get_default_profile(self.selected_device) + self.profiles = g15profile.get_profiles(self.selected_device) + locked = g15profile.is_locked(self.selected_device) + for profile in self.profiles: + weight = 400 + selected = profile.id == active_id + if selected: + weight = 700 + lock_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(g15globals.image_dir, "locked.png")) if locked and selected else None + self.profiles_model.append([profile.name, weight, profile.id, profile == default_profile, not profile.read_only, lock_icon ]) + if current_selection != None and profile.id == current_selection.id: + tree_selection.select_path(self.profiles_model.get_path(self.profiles_model.get_iter(len(self.profiles_model) - 1))) + self.selected_profile = profile + if self.selected_profile == None: + tree_selection.select_path(self.profiles_model.get_path(self.profiles_model.get_iter(0))) + self.selected_profile = self.profiles[0] + if self.selected_profile != None: + self._load_profile(self.selected_profile) + + def _load_parent_profiles(self): + self.parent_profile_model.clear() + self.parent_profile_model.append([-1, "" ]) + if self.selected_device != None: + for profile in self.profiles: + if profile.id != self.selected_profile.id: + self.parent_profile_model.append([profile.id, profile.name ]) + + def _profiles_changed(self, device_uid, macro_profile_id): + gobject.idle_add(self._load_profile_list) + + def _profile_name_edited(self, widget, row, value): + profile = self.profiles[int(row)] + if value != profile.name and not profile.read_only: + profile.name = value + self._save_profile(profile) + + def _macro_list_clicked(self, widget, event): + if event.type == gtk.gdk._2BUTTON_PRESS: + self._macro_properties(event) + + def _macro_name_edited(self, widget, row, value): + macro = self._get_sorted_list()[int(row)] + if value != macro.name: + macro.name = value + macro.save() + self._load_profile(self.selected_profile) + + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _get_sorted_list(self): + sm = list(self.selected_profile.get_sorted_macros(None, self._get_memory_number())) + return sm + + def _load_profile(self, profile): + self.adjusting = True + try : + current_selection = self._get_selected_macro() + tree_selection = self.macro_list.get_selection() + name = profile.window_name + if name == None: + name = "" + self.macros_model.clear() + selected_macro = None + macros = self._get_sorted_list() + + # Build the macro model and set the initial selection + for macro in macros: + if macro.activate_on == g15driver.KEY_STATE_HELD: + on_name = _("Hold") + elif macro.activate_on == g15driver.KEY_STATE_DOWN: + on_name = _("Press") + else: + on_name = _("Release") + row = [", ".join(g15driver.get_key_names(macro.keys)), + macro.name, + macro.key_list_key, + not profile.read_only, + macro.activate_on, + on_name ] + self.macros_model.append(row) + if current_selection != None and macro.key_list_key == current_selection.key_list_key: + tree_selection.select_path(self.macros_model.get_path(self.macros_model.get_iter(len(self.macros_model) - 1))) + selected_macro = macro + if selected_macro == None and len(macros) > 0: + tree_selection.select_path(self.macros_model.get_path(self.macros_model.get_iter(0))) + + + # Various enabled / disabled and visible / invisible states are + # adjusted depending on the selected profile + self.new_macro_button.set_sensitive(not profile.read_only) + self.delete_macro_button.set_sensitive(not profile.read_only) + self.information_content.set_sensitive(not profile.read_only) + self.delays_content.set_sensitive(not profile.read_only) + self.activation_content.set_sensitive(not profile.read_only) + self.profile_plugins_mode.set_sensitive(not profile.read_only) + + if profile.get_default(): + self.activate_on_focus.set_visible(False) + self.launch_pattern_box.set_visible(False) + self.activate_on_launch.set_visible(False) + self.window_label.set_visible(False) + self.window_select.set_visible(False) + self.parent_profile_label.set_visible(False) + self.parent_profile_box.set_visible(False) + self.window_name.set_visible(False) + self.activate_by_default.set_visible(True) + self.context_remove_profile.set_sensitive(False) + else: + self._load_windows() +# self.launch_pattern_box.set_visible(True) +# self.activate_on_launch.set_visible(True) + self.launch_pattern_box.set_visible(False) + self.activate_on_launch.set_visible(False) + + self.window_name.set_visible(True) + self.parent_profile_label.set_visible(True) + self.parent_profile_box.set_visible(True) + self.window_select.set_visible(True) + self.activate_on_focus.set_visible(True) + self.window_label.set_visible(True) + self.activate_by_default.set_visible(False) + self.context_remove_profile.set_sensitive(not profile.read_only) + + # Set actions available based on locked state + locked = g15profile.is_locked(self.selected_device) + self.context_activate_profile.set_sensitive(not locked and not profile.is_active()) + self.context_unlock_profile.set_sensitive(profile.is_active() and locked) + self.context_lock_profile.set_sensitive(not profile.is_active() or ( profile.is_active() and not locked ) ) + self.activate_on_launch.set_active(profile.activate_on_launch) + self.launch_pattern.set_sensitive(self.activate_on_launch.get_active()) + + # Background button state + self.background_browse_button.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + self.background_label.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + self.clear_background_button.set_visible(self.driver is not None and self.driver.get_bpp() > 1) + + # Set the values of the widgets + self.launch_pattern.set_text("" if profile.launch_pattern is None else profile.launch_pattern) + self.profile_author.set_text(profile.author) + self.activate_by_default.set_active(profile.activate_on_focus) + if profile.window_name != None: + self.window_name.set_text(profile.window_name) + else: + self.window_name.set_text("") + self.send_delays.set_active(profile.send_delays) + self.fixed_delays.set_active(profile.fixed_delays) + self._set_delay_state() + self.press_delay_adjustment.set_value(float(profile.press_delay) / 1000.0) + self.release_delay_adjustment.set_value(float(profile.release_delay) / 1000.0) + self._set_image(self.profile_icon, profile.get_profile_icon_path(48)) + self._set_image(self.background, profile.get_resource_path(profile.background)) + self.activate_on_focus.set_active(profile.activate_on_focus) + self.window_combo.set_sensitive(self.activate_on_focus.get_active()) + + # Set up colors + if self.color_button != None: + rgb = profile.get_mkey_color(self._get_memory_number()) + if rgb == None: + self.enable_color_for_m_key.set_active(False) + self.color_button.set_sensitive(False) + self.color_button.set_color(g15convert.to_color((255, 255, 255))) + else: + self.color_button.set_sensitive(True and not profile.read_only) + self.color_button.set_color(g15convert.to_color(rgb)) + self.enable_color_for_m_key.set_active(True) + self.enable_color_for_m_key.set_sensitive(not profile.read_only) + + # Plugins + self._load_enabled_profile_plugins() + + # Parent profile + self._load_parent_profiles() + self.parent_profile_combo.set_active(0) + for i in range(0, len(self.parent_profile_model)): + if ( profile.base_profile == None and i == 0 ) or \ + ( i > 0 and profile.base_profile == self.parent_profile_model[i][0] ): + self.parent_profile_combo.set_active(i) + + # Inital state based on macro and profile selection + self._set_available_actions() + self._set_available_profile_actions() + finally: + self.adjusting = False + + def _load_enabled_profile_plugins(self): + for i in range(0, len(self.profile_plugins_mode_model)): + if self.selected_profile.plugins_mode == self.profile_plugins_mode_model[i][0]: + self.profile_plugins_mode.set_active(i) + self.enabled_profile_plugins_model.clear() + if self.selected_device: + for mod in sorted(g15pluginmanager.imported_plugins, key=lambda key: key.name): + key = self._get_full_key("plugins/%s/enabled" % mod.id ) + if self.driver and self.driver.get_model_name() in g15pluginmanager.get_supported_models(mod) and not g15pluginmanager.is_global_plugin(mod): + enabled = self.conf_client.get_bool(key) + if enabled: + self.enabled_profile_plugins_model.append([mod.id in self.selected_profile.selected_plugins, mod.name, mod.id]) + + def _set_image(self, widget, path): + if path == None or path == "" or not os.path.exists(path): + widget.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_DIALOG) + else: + widget.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(path, 48, 48)) + + def _load_windows(self): + self.window_model.clear() + window_name = self.window_name.get_text() + i = 0 + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + vn = view.Name() + self.window_model.append([vn, window]) + if window_name != None and vn == window_name: + self.window_combo.set_active(i) + i += 1 + else: + apps = {} + for window in self.screen.get_windows(): + if not window.is_skip_pager(): + app = window.get_application() + if app and not app.get_name() in apps: + apps[app.get_name()] = app + for app in apps: + self.window_model.append([app, app]) + if window_name != None and app == window_name: + self.window_combo.set_active(i) + i += 1 + + def _add_controls(self): + + self._controls_visible = False + + # Remove previous notify handles + for nh in self.control_notify_handles: + self.conf_client.notify_remove(nh) + + driver_controls = None + if self.selected_device != None: + # Driver. We only need this to get the controls. Perhaps they should be moved out of the driver + # class and the values stored separately + try : + self.driver = g15drivermanager.get_driver(self.conf_client, self.selected_device) + self.driver.on_driver_options_change = self._driver_options_changed + + # Controls + driver_controls = self.driver.get_controls() + for control in driver_controls: + control.set_from_configuration(self.driver.device, self.conf_client) + + except Exception as e: + logger.error("Failed to load driver to query controls.", exc_info = e) + + if not driver_controls: + driver_controls = [] + + # Remove current components + controls = self.widget_tree.get_object("ControlsBox") + for c in controls.get_children(): + controls.remove(c) + for c in self.memory_bank_vbox.get_children(): + self.memory_bank_vbox.remove(c) + self.memory_bank_vbox.add(self.widget_tree.get_object("MemoryBanks")) + + # Slider and Color controls + table = gtk.Table(rows = max(1, len(driver_controls)), columns = 2) + table.set_row_spacings(4) + row = 0 + for control in driver_controls: + val = control.value + if isinstance(val, int): + if ( control.hint & g15driver.HINT_SWITCH ) == 0 and ( control.hint & g15driver.HINT_MKEYS ) == 0: + label = gtk.Label(control.name) + label.set_alignment(0.0, 0.5) + label.show() + table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 8, ypadding = 4); + + hscale = gtk.HScale() + hscale.set_value_pos(gtk.POS_RIGHT) + hscale.set_digits(0) + hscale.set_range(control.lower,control.upper) + hscale.set_value(control.value) + hscale.connect("value-changed", self._control_changed, control) + hscale.show() + + halign = gtk.Alignment(0, 0, 1.0, 1.00) + halign.add(hscale) + + table.attach(halign, 1, 2, row, row + 1, xoptions = gtk.EXPAND | gtk.FILL) + self.control_notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, hscale ])) + else: + label = gtk.Label(control.name) + label.set_alignment(0.0, 0.5) + label.show() + table.attach(label, 0, 1, row, row + 1, xoptions = gtk.FILL, xpadding = 8, ypadding = 4); + + picker = colorpicker.ColorPicker(redblue = control.hint & g15driver.HINT_RED_BLUE_LED != 0) + picker.set_color(control.value) + picker.connect("color-chosen", self._color_chosen, control) + table.attach(picker, 1, 2, row, row + 1) + + self.control_notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, picker])); + + row += 1 + if row > 0: + self._controls_visible = True + controls.add(table) + controls.show_all() + + # Switch controls + controls = self.widget_tree.get_object("SwitchesBox") + for c in controls.get_children(): + controls.remove(c) + table.set_row_spacings(4) + row = 0 + for control in driver_controls: + val = control.value + if isinstance(val, int): + if control.hint & g15driver.HINT_SWITCH != 0: + check_button = gtk.CheckButton(control.name) + check_button.set_active(control.value == 1) + check_button.set_alignment(0.0, 0.0) + check_button.show() + controls.pack_start(check_button, False, False, 4) + check_button.connect("toggled", self._control_changed, control) + self.notify_handles.append(self.conf_client.notify_add(self._get_full_key(control.id), self._control_configuration_changed, [ control, check_button ])); + row += 1 + if row > 0: + self._controls_visible = True + + controls.show_all() + self.widget_tree.get_object("SwitchesFrame").set_child_visible(row > 0) + + # Hide the cycle screens if the device has no screen + if self.driver != None and self.driver.get_bpp() == 0: + self.cycle_screens.hide() + self.cycle_screens_options.hide() + else: + self._controls_visible = True + self.cycle_screens.show() + self.cycle_screens_options.show() + + # If the keyboard has a colour dimmer, allow colours to be assigned to memory banks + control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) if self.driver != None else None + if control != None and not isinstance(control.value, int): + self._controls_visible = True + hbox = gtk.HBox() + self.enable_color_for_m_key = gtk.CheckButton(_("Set backlight colour")) + self.enable_color_for_m_key.connect("toggled", self._color_for_mkey_enabled) + hbox.pack_start(self.enable_color_for_m_key, True, False) + self.color_button = gtk.ColorButton() + self.color_button.set_sensitive(False) + self.color_button.connect("color-set", self._profile_color_changed) + hbox.pack_start(self.color_button, True, False) + self.memory_bank_vbox.add(hbox) + hbox.show_all() + else: + self.color_button = None + self.enable_color_for_m_key = None + + def _profile_color_changed(self, widget): + if not self.adjusting: + self.selected_profile.set_mkey_color(self._get_memory_number(), + g15convert.color_to_rgb(widget.get_color()) if self.enable_color_for_m_key.get_active() else None) + self._save_profile(self.selected_profile) + + def _color_for_mkey_enabled(self, widget): + self.color_button.set_sensitive(widget.get_active()) + self._profile_color_changed(self.color_button) + + def _show_profile_list_context(self, treeview, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + time = event.time + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, cellx, celly = pthinfo + treeview.grab_focus() + treeview.set_cursor( path, col, 0) + self.profiles_context_menu.popup( None, None, None, event.button, time) + return True \ No newline at end of file diff --git a/src/gnome15/g15dbus.py b/src/gnome15/g15dbus.py new file mode 100644 index 0000000..042227e --- /dev/null +++ b/src/gnome15/g15dbus.py @@ -0,0 +1,988 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import dbus.service +import g15globals +import g15theme +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15driver +import g15devices +import gobject + + +from cStringIO import StringIO + +BUS_NAME="org.gnome15.Gnome15" +NAME="/org/gnome15/Service" +PAGE_NAME="/org/gnome15/Page" +CONTROL_ACQUISITION_NAME="/org/gnome15/Control" +SCREEN_NAME="/org/gnome15/Screen" +DEVICE_NAME="/org/gnome15/Device" +IF_NAME="org.gnome15.Service" +PAGE_IF_NAME="org.gnome15.Page" +CONTROL_ACQUISITION_IF_NAME="org.gnome15.Control" +SCREEN_IF_NAME="org.gnome15.Screen" +DEVICE_IF_NAME="org.gnome15.Device" + +# Logging +import logging +logger = logging.getLogger(__name__) + +class AbstractG15DBUSService(dbus.service.Object): + + def __init__(self, conn=None, object_path=None, bus_name=None): + dbus.service.Object.__init__(self, conn, object_path, bus_name) + self._reserved_keys = [] + + def action_performed(self, binding): + self.Action(binding.action) + + def handle_key(self, keys, state, post): + if not post: + p = [] + for k in keys: + if k in self._reserved_keys: + p.append(k) + if len(p) > 0: + if state == g15driver.KEY_STATE_UP: + gobject.idle_add(self.KeysReleased,p) + elif state == g15driver.KEY_STATE_DOWN: + gobject.idle_add(self.KeysPressed, p) + return True + + def _set_receive_actions(self, enabled): + if enabled and self in self._screen.key_handler.action_listeners: + raise Exception("Already receiving actions") + elif not enabled and not self in self._screen.key_handler.action_listeners: + raise Exception("Not receiving actions") + if enabled: + self._screen.key_handler.action_listeners.append(self) + else: + self._screen.key_handler.action_listeners.remove(self) + +class G15DBUSDeviceService(AbstractG15DBUSService): + + def __init__(self, dbus_service, device): + AbstractG15DBUSService.__init__(self, dbus_service._bus_name, "%s/%s" % ( DEVICE_NAME, device.uid ) ) + self._dbus_service = dbus_service + self._service = dbus_service._service + self._device = device + + @dbus.service.signal(DEVICE_IF_NAME, signature='s') + def ScreenAdded(self, screen_name): + pass + + @dbus.service.signal(DEVICE_IF_NAME, signature='s') + def ScreenRemoved(self, screen_name): + pass + + @dbus.service.method(DEVICE_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + self._set_receive_actions(enabled) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetScreen(self): + for screen_path in self._dbus_service._dbus_screens: + screen = self._dbus_service._dbus_screens[screen_path] + if screen._screen.device.uid == self._device.uid: + return "%s/%s" % ( SCREEN_NAME, self._device.uid) + return "" + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='') + def Disable(self): + g15devices.set_enabled(self._service.conf_client, self._device, False) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='') + def Enable(self): + g15devices.set_enabled(self._service.conf_client, self._device, True) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetModelFullName(self): + return self._device.model_fullname + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetModelId(self): + return self._device.model_id + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetUID(self): + return self._device.uid + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='s') + def GetUsbID(self): + return "%s:%s" % ( hex(self._device.controls_usb_id[0]), hex(self._device.controls_usb_id[1]) ) + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='u') + def GetBPP(self): + return self._device.bpp + + @dbus.service.method(DEVICE_IF_NAME, in_signature='', out_signature='uu') + def GetSize(self): + return self._device.size + +class G15DBUSClient(): + + def __init__(self, bus_name): + self.bus_name = bus_name + self.pages = [] + self.acquisitions = [] + + def cleanup(self): + for p in list(self.pages): + p.delete() + for a in list(self.acquisitions): + a.release() + +class G15DBUSScreenService(AbstractG15DBUSService): + + def __init__(self, dbus_service, screen): + self._bus_name = "%s/%s" % ( SCREEN_NAME, screen.device.uid ) + AbstractG15DBUSService.__init__(self, dbus_service._bus_name, self._bus_name ) + self._dbus_service = dbus_service + self._service = dbus_service._service + self._screen = screen + self._screen.add_screen_change_listener(self) + self._screen.key_handler.key_handlers.append(self) + self._notify_handles = [] + self._dbus_pages = {} + self._clients = {} + + self._notify_handles.append(self._screen.conf_client.notify_add("/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, self._cycle_screens_option_changed)) + + ''' + screen change listener and action listener + ''' + + def memory_bank_changed(self, new_memory_bank): + if g15scheduler.run_on_gobject(self.memory_bank_changed, new_memory_bank): + return + logger.debug("Sending memory bank changed signel (%d)", new_memory_bank) + self.MemoryBankChanged(new_memory_bank) + + def attention_cleared(self): + if g15scheduler.run_on_gobject(self.attention_cleared): + return + logger.debug("Sending attention cleared signal") + self.AttentionCleared() + logger.debug("Sent attention cleared signal") + + def attention_requested(self, message): + if g15scheduler.run_on_gobject(self.attention_requested, message): + return + logger.debug("Sending attention requested signal") + self.AttentionRequested(message if message != None else "") + logger.debug("Sent attention requested signal") + + def driver_connected(self, driver): + if g15scheduler.run_on_gobject(self.driver_connected, driver): + return + logger.debug("Sending driver connected signal") + self.Connected(driver.get_name()) + logger.debug("Sent driver connected signal") + + def driver_connection_failed(self, driver, exception): + if g15scheduler.run_on_gobject(self.driver_connection_failed, driver, exception): + return + logger.debug("Sending driver connection failed signal") + self.ConnectionFailed(driver.get_name(), str(exception)) + logger.debug("Sent driver connection failed signal") + + def driver_disconnected(self, driver): + if g15scheduler.run_on_gobject(self.driver_disconnected, driver): + return + logger.debug("Sending driver disconnected signal") + self.Disconnected(driver.get_name()) + logger.debug("Sent driver disconnected signal") + + def page_changed(self, page): + if g15scheduler.run_on_gobject(self.page_changed, page): + return + logger.debug("Sending page changed signal for %s", page.id) + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + self.PageChanged(dbus_page._bus_name) + logger.debug("Sent page changed signal for %s", page.id) + else: + logger.warning("Got page_changed event when no such page (%s) exists", page.id) + + def new_page(self, page): + if g15scheduler.run_on_gobject(self.new_page, page): + return + logger.debug("Sending new page signal for %s", page.id) + if page.id in self._dbus_pages: + raise Exception("Page %s already in DBUS service.", page.id) + dbus_page = G15DBUSPageService(self, page, self._dbus_service._page_sequence_number) + self._dbus_pages[page.id] = dbus_page + self.PageCreated(dbus_page._bus_name, page.title) + self._dbus_service._page_sequence_number += 1 + logger.debug("Sent new page signal for %s" % page.id) + + def title_changed(self, page, title): + if g15scheduler.run_on_gobject(self.title_changed, page, title): + return + logger.debug("Sending title changed signal for %s", page.id) + dbus_page = self._dbus_pages[page.id] + self.PageTitleChanged(dbus_page._bus_name, title) + logger.debug("Sent title changed signal for %s", page.id) + + def deleting_page(self, page): + if g15scheduler.run_on_gobject(self.deleting_page, page): + return + logger.debug("Sending page deleting signal for %s", page.id) + + for client_bus_name in self._clients: + client = self._clients[client_bus_name] + if page in client.pages: + client.pages.remove(page) + + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + if dbus_page in page.key_handlers: + page.key_handlers.remove(dbus_page) + self.PageDeleting(dbus_page._bus_name, ) + else: + logger.warning("DBUS Page %s is deleting, but it never existed. Huh? %s", + page.id, + str(self._dbus_pages)) + logger.debug("Sent page deleting signal for %s", page.id) + + def deleted_page(self, page): + if g15scheduler.run_on_gobject(self.deleted_page, page): + return + logger.debug("Sending page deleted signal for %s", page.id) + if page.id in self._dbus_pages: + dbus_page = self._dbus_pages[page.id] + self.PageDeleted(dbus_page._bus_name) + dbus_page.remove_from_connection() + del self._dbus_pages[page.id] + else: + logger.warning("DBUS Page %s was deleted, but it never existed. Huh? %s", + page.id, + str(self._dbus_pages)) + logger.debug("Sent page deleted signal for %s", page.id) + + """ + DBUS Functions + """ + + @dbus.service.method(SCREEN_IF_NAME, in_signature='sds', out_signature='s', sender_keyword = "sender") + def AcquireControl(self, control_id, release_after, value, sender = None): + control = self._screen.driver.get_control(control_id) + if control is None: + raise Exception("No control with ID of %s" % control_id) + if value == "": + initial_value = None + elif isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + initial_value = 1 if value == "true" else 0 + else: + initial_value = int(value) + else: + sp = value.split(",") + initial_value = (int(sp[0]), int(sp[1]), int(sp[2])) + control_acquisition = self._screen.driver.acquire_control(control, None if release_after == 0 else release_after, initial_value) + dbus_control_acquisition = G15DBUSControlAcquisition(self, control_acquisition, self._dbus_service._acquire_sequence_number) + self._get_client(sender).acquisitions.append(control_acquisition) + control_acquisition.on_release = dbus_control_acquisition._notify_release + self._dbus_service._acquire_sequence_number += 1 + return dbus_control_acquisition._bus_name + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetMessage(self): + return self._screen.attention_message + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='') + def ClearAttention(self): + return self._screen.clear_attention() + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s', out_signature='') + def RequestAttention(self, message): + self._screen.request_attention(message) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='ssn', out_signature='s', sender_keyword = 'sender') + def CreatePage(self, page_id, title, priority, sender = None): + page = g15theme.G15Page(page_id, self._screen, priority = priority) + self._screen.add_page(page) + page.set_title(title) + self._get_client(sender).pages.append(page) + return self.GetPageForID(page_id) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsCyclingEnabled(self): + return g15gconf.get_bool_or_default(self._service.conf_client, "/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, True); + + @dbus.service.method(SCREEN_IF_NAME, in_signature='b', out_signature='') + def SetCyclingEnabled(self, enabled): + self._service.conf_client.set_bool("/apps/gnome15/%s/cycle_screens" % self._screen.device.uid, enabled); + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsReceiveActions(self): + return self in self._screen.key_handler.action_listeners + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s') + def ReserveKey(self, key_name): + if key_name in self._reserved_keys: + raise Exception("Already reserved") + self._reserved_keys.add(key_name) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s') + def UnreserveKey(self, key_name): + if not key_name in self._reserved_keys: + raise Exception("Not reserved") + self._reserved_keys.remove(key_name) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='ssss') + def GetDeviceInformation(self): + device = self._screen.device + return ( device.uid, device.model_id, "%s:%s" % ( hex(device.controls_usb_id[0]),hex(device.controls_usb_id[1]) ), device.model_fullname ) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='ssnnn') + def GetDriverInformation(self): + driver = self._screen.driver + return ( driver.get_name(), driver.get_model_name(), driver.get_size()[0], driver.get_size()[1], driver.get_bpp() ) if driver != None else None + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetDeviceUID(self): + return self._screen.device.uid + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='as') + def GetControlIds(self): + c = [] + for control in self._screen.driver.get_controls(): + c.append(control.id) + return c + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsConnected(self): + return self._screen.driver.is_connected() if self._screen.driver != None else False + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n') + def CycleKeyboard(self, value): + for c in self._get_dimmable_controls(): + if isinstance(c.value, int): + self._screen.cycle_level(value, c) + else: + self._screen.cycle_color(value, c) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='s', out_signature='s') + def GetPageForID(self, page_id): + return self._dbus_pages[page_id]._bus_name + + @dbus.service.method(SCREEN_IF_NAME, out_signature='s') + def GetVisiblePage(self): + return self.GetPageForID(self._screen.get_visible_page().id) + + @dbus.service.method(SCREEN_IF_NAME, out_signature='as') + def GetPages(self): + l = [] + for page in self._dbus_pages.values(): + l.append(page._bus_name) + return l + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n', out_signature='as') + def GetPagesBelowPriority(self, priority): + logger.warning("The GetPagesBelowPriority is deprecated. Use GetPages instead.") + l = [] + for page in self._dbus_pages.values(): + if page._page.priority >= priority: + l.append(page._bus_name) + return l + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='s') + def GetLastError(self): + err = self._screen.get_last_error() + if err is None: + return "" + return str(err) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='') + def ClearPopup(self): + return self._screen.clear_popup() + + @dbus.service.method(SCREEN_IF_NAME, in_signature='n', out_signature='') + def Cycle(self, cycle): + return self._screen.cycle(cycle) + + @dbus.service.method(SCREEN_IF_NAME, in_signature='', out_signature='b') + def IsAttentionRequested(self): + return self._screen.attention + + @dbus.service.method(SCREEN_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + self._set_receive_actions(enabled) + + """ + DBUS Signals + """ + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Disconnected(self, driver_name): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Connected(self, driver_name): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def ConnectionFailed(self, driver_name, exception_text): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='u') + def MemoryBankChanged(self, new_memory_bank): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageChanged(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PageCreated(self, page_path, title): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='ss') + def PageTitleChanged(self, page_path, new_title): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageDeleted(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def PageDeleting(self, page_path): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def AttentionRequested(self, message): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='') + def AttentionCleared(self): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='as') + def KeysPressed(self, keys): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='as') + def KeysReleased(self, keys): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='s') + def Action(self, binding): + pass + + @dbus.service.signal(SCREEN_IF_NAME, signature='b') + def CyclingChanged(self, cycle): + pass + + """ + Private + """ + def _removing(self): + for h in self._notify_handles: + self._service.conf_client.notify_remove(h) + + def _cycle_screens_option_changed(self, client, connection_id, entry, args): + self.CyclingChanged(entry.value.get_bool()) + + def _get_dimmable_controls(self): + controls = [] + for c in self._screen.driver.get_controls(): + if c.hint & g15driver.HINT_DIMMABLE != 0: + controls.append(c) + return controls + + def _get_dimmable_control_values(self): + values = [] + for c in self._get_dimmable_controls(): + values.append(c.value) + return values + + def _get_screen_path(self): + return "%s/%s" % ( SCREEN_NAME, self._screen.device.uid ) + + def _get_client(self, sender): + if sender in self._clients: + return self._clients[sender] + else: + c = G15DBUSClient(sender) + self._clients[sender] = c + return c + +class G15DBUSControlAcquisition(AbstractG15DBUSService): + + def __init__(self, screen_service, acquisition, sequence_number): + self._bus_name = "%s%s" % ( CONTROL_ACQUISITION_NAME , str( sequence_number ) ) + AbstractG15DBUSService.__init__(self, screen_service._dbus_service._bus_name, self._bus_name ) + self._screen_service = screen_service + self._sequence_number = sequence_number + self._acquisition = acquisition + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, out_signature='s') + def GetValue(self): + control = self._acquisition.control + value = self._acquisition.val + if isinstance(value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + return "true" if value else "false" + else: + return str(value) + else: + return "%d,%d,%d" % value + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, out_signature='t') + def GetHint(self): + return self._acquisition.control.hint + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='sd', out_signature='') + def SetValue(self, value, reset_after): + control = self._acquisition.control + reset_after = None if reset_after == 0.0 else reset_after + if isinstance(control.value, int): + if control.hint & g15driver.HINT_SWITCH != 0: + self._acquisition.set_value(1 if value == "true" else 0, reset_after) + else: + self._acquisition.set_value(int(value), reset_after) + else: + sp = value.split(",") + self._acquisition.set_value((int(sp[0]), int(sp[1]), int(sp[2])), reset_after) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='ddb', out_signature='') + def Fade(self, percentage, duration, release): + self._acquisition.fade(percentage, duration, release) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME, in_signature='ddd', out_signature='') + def Blink(self, off_val, delay, duration): + self._acquisition.blink(off_val, delay, None if duration == 0 else duration) + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def Reset(self): + self._acquisition.reset() + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def CancelReset(self): + self._acquisition.cancel_reset() + + @dbus.service.method(CONTROL_ACQUISITION_IF_NAME) + def Release(self): + self._screen_service._screen.driver.release_control(self._acquisition) + + """ + Private + """ + def _notify_release(self): + logger.info("Release acquisition of control %s", self._acquisition.control.id) + for client_bus_name in self._screen_service._clients: + client = self._screen_service._clients[client_bus_name] + if self._acquisition in client.acquisitions: + client.acquisitions.remove(self._acquisition) + self.remove_from_connection() + +class G15DBUSPageService(AbstractG15DBUSService): + + def __init__(self, screen_service, page, sequence_number): + self._bus_name = "%s%s" % ( PAGE_NAME , str( sequence_number ) ) + AbstractG15DBUSService.__init__(self, screen_service._dbus_service._bus_name, self._bus_name ) + self._screen_service = screen_service + self._screen = self._screen_service._screen + self._sequence_number = sequence_number + self._page = page + self._timer = None + self._page.key_handlers.append(self) + + @dbus.service.method(PAGE_IF_NAME, in_signature='b') + def SetReceiveActions(self, enabled): + if enabled and self in self._screen_service._screen.action_listeners: + raise Exception("Already receiving actions") + elif not enabled and not self in self._screen_service._screen.action_listeners: + raise Exception("Not receiving actions") + if enabled: + self._screen_service._screen.action_listeners.append(self) + else: + self._screen_service._screen.action_listeners.remove(self) + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='b') + def GetReceiveActions(self): + return self in self._screen_service._screen.action_listeners + + @dbus.service.method(PAGE_IF_NAME, out_signature='n') + def GetPriority(self): + return self._page.priority + + @dbus.service.method(PAGE_IF_NAME, out_signature='s') + def GetTitle(self): + return self._page.title + + @dbus.service.method(PAGE_IF_NAME, out_signature='s') + def GetId(self): + return self._page.id + + @dbus.service.method(PAGE_IF_NAME, out_signature='b') + def IsVisible(self): + return self._page.is_visible() + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='b') + def IsReceiveActions(self): + return self in self._screen.key_handler.action_listeners + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Delete(self): + self._screen_service._screen.del_page(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Raise(self): + self._screen_service._screen.raise_page(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='', out_signature='') + def CycleTo(self): + self._screen_service._screen.cycle_to(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def NewSurface(self): + self._page.new_surface() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Save(self): + self._page.save() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Restore(self): + self._page.restore() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def DrawSurface(self): + self._page.draw_surface() + + @dbus.service.method(PAGE_IF_NAME, in_signature='d') + def SetLineWidth(self, line_width): + self._page.set_line_width(line_width) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddd') + def Line(self, x1, y1, x2, y2): + self._page.line(x1, y1, x2, y2) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ddddb') + def Rectangle(self, x, y, width, height, fill): + self._page.rectangle(x, y, width, height, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddb') + def Circle(self, x, y, radius, fill): + self._page.arc(x, y, radius, 0, 360, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dddddb') + def Arc(self, x, y, radius, startAngle, endAngle, fill): + self._page.arc(x, y, radius, startAngle, endAngle, fill) + + @dbus.service.method(PAGE_IF_NAME, in_signature='nnnn') + def Foreground(self, r, g, b, a): + self._page.foreground(r, g, b, a) + + @dbus.service.method(PAGE_IF_NAME, in_signature='dsss') + def SetFont(self, font_size = 12.0, font_family = "Sans", font_style = "normal", font_weight = "normal"): + self._page.set_font(font_size, font_family, font_style, font_weight) + + @dbus.service.method(PAGE_IF_NAME, in_signature='sdddds') + def Text(self, text, x, y, width, height, contraints = "left"): + self._page.text(text, x, y, width, height, contraints) + + @dbus.service.method(PAGE_IF_NAME, in_signature='sdddd') + def Image(self, path, x, y, width, height): + if not "/" in path: + path = g15icontools.get_icon_path(path, width if width != 0 else 128) + + size = None if width == 0 or height == 0 else (width, height) + + img_surface = g15cairo.load_surface_from_file(path, size) + self._page.image(img_surface, x, y) + + @dbus.service.method(PAGE_IF_NAME, in_signature='aydd') + def ImageData(self, image_data, x, y): + file_str = StringIO(str(image_data)) + img_surface = g15cairo.load_surface_from_file(file_str, None) + file_str.close() + self._page.image(img_surface, x, y) + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def CancelTimer(self): + self._timer.cancel() + + @dbus.service.method(PAGE_IF_NAME, in_signature='') + def Redraw(self): + self._screen_service._screen.redraw(self._page) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ss') + def LoadTheme(self, theme_dir, variant): + self._page.set_theme(g15theme.G15Theme(theme_dir, variant)) + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def SetThemeSVG(self, svg_text): + self._page.set_theme(g15theme.G15Theme(None, None, svg_text = svg_text)) + + @dbus.service.method(PAGE_IF_NAME, in_signature='ss') + def SetThemeProperty(self, name, value): + self._page.theme_properties[name] = value + + @dbus.service.method(PAGE_IF_NAME, in_signature='a{ss}') + def SetThemeProperties(self, properties): + self._page.theme_properties = properties + + @dbus.service.method(PAGE_IF_NAME, in_signature='ndd') + def SetPriority(self, priority, revert_after, delete_after): + self._timer = self._screen_service._screen.set_priority(self._page, priority, revert_after, delete_after) + + @dbus.service.signal(PAGE_IF_NAME, signature='as') + def KeysPressed(self, keys): + pass + + @dbus.service.signal(PAGE_IF_NAME, signature='as') + def KeysReleased(self, keys): + pass + + @dbus.service.signal(PAGE_IF_NAME, signature='s') + def Action(self, binding): + pass + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def ReserveKey(self, key_name): + if key_name in self._reserved_keys: + raise Exception("Already reserved") + self._reserved_keys.add(key_name) + + @dbus.service.method(PAGE_IF_NAME, in_signature='s') + def UnreserveKey(self, key_name): + if not key_name in self._reserved_keys: + raise Exception("Not reserved") + self._reserved_keys.remove(key_name) + + """ + Callbacks + """ + def action_performed(self, binding): + if self.IsVisible(): + AbstractG15DBUSService.action_performed(self, binding) + +class G15DBUSService(AbstractG15DBUSService): + + def __init__(self, service): + AbstractG15DBUSService.__init__(self) + self._service = service + logger.debug("Getting Session DBUS") + self._bus = dbus.SessionBus() + self._page_sequence_number = 1 + self._acquire_sequence_number = 1 + logger.debug("Exposing service") + self._bus_name = dbus.service.BusName(BUS_NAME, bus=self._bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, self._bus_name, NAME) + self._service.service_listeners.append(self) + logger.debug("DBUS service ready") + self._dbus_screens = {} + self._dbus_devices = [] + self._dbus_device_map = {} + for device in g15devices.find_all_devices(): + dbus_device = G15DBUSDeviceService(self, device) + self._dbus_devices.append(dbus_device) + self._dbus_device_map[device.uid] = dbus_device + g15devices.device_added_listeners.append(self._device_added) + g15devices.device_removed_listeners.append(self._device_removed) + + self._bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def _device_removed(self, device): + if device.uid in self._dbus_device_map: + dbus_device = self._dbus_device_map[device.uid] + self._dbus_devices.remove(dbus_device) + del self._dbus_device_map[device.uid] + logger.info("Removed DBUS device %s/%s", DEVICE_NAME, device.uid) + self.DeviceRemoved("%s/%s" % ( DEVICE_NAME, device.uid )) + self._silently_remove_from_connector(dbus_device) + else: + logger.warning("DBUS service did not know about a device for some reason (%s)", device.uid) + + def _device_added(self, device): + dbus_device = G15DBUSDeviceService(self, device) + self._dbus_devices.append(dbus_device) + self._dbus_device_map[device.uid] = dbus_device + logger.info("Added DBUS device %s/%s", DEVICE_NAME, device.uid) + self.DeviceAdded("%s/%s" % ( DEVICE_NAME, device.uid )) + + def stop(self): + g15devices.device_added_listeners.remove(self._device_added) + g15devices.device_removed_listeners.remove(self._device_removed) + for dbus_device in self._dbus_devices: + self._silently_remove_from_connector(dbus_device) + for screen in self._dbus_screens: + self._silently_remove_from_connector(self._dbus_screens[screen]) + self._silently_remove_from_connector(self) + + def _silently_remove_from_connector(self, obj): + try: + obj.remove_from_connection() + except Exception as e: + logger.debug("Error silently removing obj from connection.", exc_info = e) + pass + + ''' + service listener + ''' + def screen_added(self, screen): + if g15scheduler.run_on_gobject(self.screen_added, screen): + return + logger.debug("Screen added for %s", screen.device.model_id) + screen_service = G15DBUSScreenService(self, screen) + self._dbus_screens[screen.device.uid] = screen_service + self.ScreenAdded("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + dbus_device = self._dbus_device_map[screen.device.uid] + dbus_device.ScreenAdded("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + + def screen_removed(self, screen): + if g15scheduler.run_on_gobject(self.screen_removed, screen): + return + logger.debug("Screen removed for %s", screen.device.model_id) + self.ScreenRemoved("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + if screen.device.uid in self._dbus_device_map: + dbus_device = self._dbus_device_map[screen.device.uid] + dbus_device.ScreenRemoved("%s/%s" % ( SCREEN_NAME, screen.device.uid )) + try: + screen_service = self._dbus_screens[screen.device.uid] + screen_service._removing() + screen_service.remove_from_connection() + except Exception as e: + logger.debug("Error removing screen object.", exc_info = e) + # May happen on shutdown + pass + del self._dbus_screens[screen.device.uid] + + def service_stopping(self): + if g15scheduler.run_on_gobject(self.service_stopping): + return + logger.debug("Sending stopping down signal") + self.Stopping() + + def service_stopped(self): + if g15scheduler.run_on_gobject(self.service_stopped): + return + logger.debug("Sending stopped down signal") + self.Stopped() + + def service_starting_up(self): + if g15scheduler.run_on_gobject(self.service_starting_up): + return + logger.debug("Sending starting up signal") + self.Starting() + + def service_started_up(self): + if g15scheduler.run_on_gobject(self.service_started_up): + return + logger.debug("Sending started up signal") + self.Started() + + ''' + DBUS Signals + ''' + + @dbus.service.signal(IF_NAME) + def Stopping(self): + pass + + @dbus.service.signal(IF_NAME) + def Stopped(self): + pass + + @dbus.service.signal(IF_NAME) + def Starting(self): + pass + + @dbus.service.signal(IF_NAME) + def Started(self): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def ScreenAdded(self, screen_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def ScreenRemoved(self, screen_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def DeviceAdded(self, device_name): + pass + + @dbus.service.signal(IF_NAME, signature='s') + def DeviceRemoved(self, device_name): + pass + + ''' + DBUS methods + ''' + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return ( g15globals.name, "Gnome15 Project", g15globals.version, "2.1" ) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='') + def Stop(self): + g15scheduler.queue("serviceQueue", "dbusShutdown", 0, self._service.shutdown) + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStarting(self): + return self._service.starting_up + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStarted(self): + started = not self._service.starting_up and not self._service.shutting_down + return started + + @dbus.service.method(IF_NAME, in_signature='', out_signature='b') + def IsStopping(self): + return self._service.shutting_down + + @dbus.service.method(IF_NAME, out_signature='as') + def GetDevices(self): + l = [] + for device in self._dbus_devices: + l.append("%s/%s" % (DEVICE_NAME, device._device.uid ) ) + return l + + @dbus.service.method(IF_NAME, out_signature='as') + def GetScreens(self): + l = [] + for screen in self._dbus_screens: + l.append("%s/%s" % (SCREEN_NAME, screen ) ) + return l + + @dbus.service.method(IF_NAME, in_signature='ssas') + def Launch(self, profile_name, screen_id, args): + logger.info("Launch under profile %s, screen %s, args = %s", + profile_name, + screen_id, + str(args)) + + """ + Private + """ + def _name_owner_changed(self, name, old_owner, new_owner): + for screen in self._dbus_screens.values(): + if name in screen._clients and old_owner and not new_owner: + logger.info("Cleaning up DBUS client %s", name) + client = screen._clients[name] + client.cleanup() + del screen._clients[name] + diff --git a/src/gnome15/g15dconf.py b/src/gnome15/g15dconf.py new file mode 100644 index 0000000..ecc47ea --- /dev/null +++ b/src/gnome15/g15dconf.py @@ -0,0 +1,122 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +This class provides a way of retrieving and monitoring dconf without using +the GI bindings, which may no longer be mixed with static bindings (that +Gnome15 uses). + +This class is stop gap until a better solution can be found +""" + +import dbus +import os +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +PASSIVE_MATCH_STRING="type='method_call',interface='ca.desrt.dconf.Writer',member='Change'" +EAVESDROP_MATCH_STRING="eavesdrop='true',%s" % PASSIVE_MATCH_STRING + +class GSettingsCallback(): + + def __init__(self, handle, key, callback): + self.handle = handle + self.key = key + self.callback = callback + +class GSettings(): + + def __init__(self, schema_id): + self.schema_id = schema_id + self._handle = 1 + # DBUS session instance must be private or monitoring will not work properly + self._session_bus = dbus.SessionBus(private=True) + self._writer = dbus.Interface(self._session_bus.get_object("ca.desrt.dconf", "/ca/desrt/dconf/Writer/user"), "ca.desrt.dconf.Writer") + self._monitors = {} + + self._match_string = EAVESDROP_MATCH_STRING + try: + self._session_bus.add_match_string(self._match_string) + except Exception as e: + logger.debug('Could not add EAVESDROP match rule. Trying PASSIVE', exc_info = e) + self._match_string = PASSIVE_MATCH_STRING + self._session_bus.add_match_string(self._match_string) + self._session_bus.add_message_filter(self._msg_cb) + + def connect(self, key, callback): + l = key.split(":") + if l[0] != "changed": + raise Exception("Only currently supported changed events") + key = l[2] + handle = self._handle + self._handle += 1 + self._monitors[handle] = GSettingsCallback(handle, key, callback) + return handle + + def disconnect(self, handle): + if handle in self._monitors: + del self._monitors[handle] + + def get_string(self, key): + _, result = self._get_status_output("gsettings get %s %s" % (self.schema_id, key)) + if len(result) > 0: + result = result.replace("\n", "") + if result.startswith("'"): + return result[1:-1] + return result + + def _get_status_output(self, cmd): + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + try: + text = pipe.read() + finally: + sts = pipe.close() + if sts is None: + sts = 0 + if text[-1:] == '\n': + text = text[:-1] + return sts, text + + def _changed(self, key): + s = "" + for b in key: + if b == 0: + break + else: + s += chr(b) + li = s.rfind("/") + if li > 0: + s_id = s[:li][1:].replace("/", ".") + k = s[li + 1:].replace("-", "_") + if s_id == self.schema_id: + for m in self._monitors: + mon = self._monitors[m] + if mon.key == k: + # Bit rubbish, but we need to give dconf time to update + gobject.timeout_add(1000, mon.callback) + + def _msg_cb(self, bus, msg): + # Only interested in method calls + if isinstance(msg, dbus.lowlevel.MethodCallMessage): + if msg.get_member() == "Change": + self._changed(*msg.get_args_list()) + + def __del__(self): + self._session_bus.remove_match_string(self._match_string) diff --git a/src/gnome15/g15debug.py b/src/gnome15/g15debug.py new file mode 100644 index 0000000..95fcfb2 --- /dev/null +++ b/src/gnome15/g15debug.py @@ -0,0 +1,34 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gc +import weakref +import objgraph + +#gc.set_debug(gc.DEBUG_LEAK) + + +import time +if __name__ == "__main__": + print "Creating snapshot1" + snapshot1 = take_snapshot() + print "Creating some objects" + l = [ "A", "B", "C", "D", "E" ] + print "Creating snapshot2" + snapshot2 = take_snapshot() + print "Comparing" + compare_snapshots(snapshot1, snapshot2) + \ No newline at end of file diff --git a/src/gnome15/g15desktop.py b/src/gnome15/g15desktop.py new file mode 100644 index 0000000..9d4e97f --- /dev/null +++ b/src/gnome15/g15desktop.py @@ -0,0 +1,1540 @@ +# coding: utf-8 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Helper classes for implementing desktop components that can monitor and control some functions +of the desktop service. It is used for Indicators, System tray icons and panel applets and +deals with all the hard work of connecting to DBus and monitoring events. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import subprocess +import gconf +import gobject +import shutil +import gnome15.g15globals as g15globals +import gnome15.g15screen as g15screen +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15notify as g15notify +import gnome15.util.g15icontools as g15icontools +import dbus +import os.path +import operator +import xdg.DesktopEntry +import xdg.BaseDirectory + +# Logging +import logging +logger = logging.getLogger(__name__) + +from threading import RLock +from threading import Thread + +icon_theme = gtk.icon_theme_get_default() +if g15globals.dev: + icon_theme.prepend_search_path(g15globals.icons_dir) + +# Private +__browsers = { } + +""" +Some constants +""" +AUTHORS=["Brett Smith ", "Nuno Araujo", "Ciprian Ciubotariu", "Andrea Calabrò" ] +GPL=""" + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. +""" + +def autostart_path_for(application_name): + """ + Returns the autostart path of the application_name desktop file + """ + return os.path.join(xdg.BaseDirectory.xdg_config_home, + "autostart", + "%s.desktop" % application_name) + +def is_desktop_application_installed(application_name): + """ + Get if a desktop file is installed for a particular application + + Keyword arguments: + application_name -- name of application + """ + for directory in xdg.BaseDirectory.xdg_config_dirs: + desktop_file = os.path.join(directory, + "autostart", + "%s.desktop" % application_name) + if os.path.exists(desktop_file): + return True + return False + +def is_autostart_application(application_name): + """ + Get whether the application is set to autostart + """ + installed = is_desktop_application_installed(application_name) + path = autostart_path_for(application_name) + if os.path.exists(path): + desktop_entry = xdg.DesktopEntry.DesktopEntry(path) + autostart = len(desktop_entry.get('X-GNOME-Autostart-enabled')) == 0 or desktop_entry.get('X-GNOME-Autostart-enabled', type="boolean") + hidden = desktop_entry.getHidden() + return autostart and not hidden + else: + # There is no config file, so enabled if installed + return installed + +def set_autostart_application(application_name, enabled): + """ + Set whether an application is set to autostart + + Keyword arguments: + application_name -- application name + enabled -- enabled or not + """ + path = autostart_path_for(application_name) + if enabled and os.path.exists(path): + os.remove(path) + elif not enabled: + app_path = "/etc/xdg/autostart/%s.desktop" % application_name + if not os.path.exists(path): + shutil.copy(app_path, path) + desktop_entry = xdg.DesktopEntry.DesktopEntry(path) + desktop_entry.set("X-GNOME-Autostart-enabled", "false") + desktop_entry.set("Hidden", "false") + desktop_entry.write() + +def get_desktop(): + ''' + Utility function to get the name of the current desktop environment. The list + of detectable desktop environments is not complete, but hopefully this will + improve over time. Currently no attempt is made to determine the version of + the desktop in use. + + Will return :- + + gnome GNOME Desktop + gnome-shell GNOME Shell Desktop + kde KDE + [None] No known desktop + ''' + + evars = os.environ + + # GNOME Shell (need a better way) + if ( "DESKTOP_SESSION" in evars and evars["DESKTOP_SESSION"] == "gnome-shell" ) or \ + ( "GJS_DEBUG_OUTPUT" in evars ): + return "gnome-shell" + + # XDG_CURRENT_DESKTOP + dt = { "LXDE" : "lxde", "GNOME" : "gnome"} + if "XDG_CURRENT_DESKTOP" in evars: + val = evars["XDG_CURRENT_DESKTOP"] + if val in dt: + return dt[val] + + # Environment variables that suggest the use of GNOME + for i in [ "GNOME_DESKTOP_SESSION_ID", "GNOME_KEYRING_CONTROL" ]: + if i in evars: + return "gnome" + + # Environment variables that suggest the use of KDE + for i in [ "KDE_FULL_SESSION", "KDE_SESSION_VERSION", "KDE_SESSION_UID" ]: + if i in evars: + return "kde" + + # Environment variables that suggest the use of LXDE + for i in [ "_LXSESSION_PID" ]: + if i in evars: + return "lxde" + +def is_shell_extension_installed(extension): + """ + Get whether a GNOME Shell extension is installed. + + Keyword arguments: + extension -- extension name + """ + for prefix in xdg.BaseDirectory.xdg_data_dirs: + extension_path = os.path.join(prefix, "gnome-shell", "extensions", extension) + if os.path.exists(extension_path): + return True + return False + +def is_gnome_shell_extension_enabled(extension): + """ + Get whether a GNOME Shell extension is enabled. This uses the + gsettings command. Python GSettings bindings (GObject introspected ones) + are not used, as well already use PyGTK and the two don't mix + + Keyword arguments: + extension -- extension name + """ + status, text = g15os.get_command_output("gsettings get org.gnome.shell enabled-extensions") + if status == 0: + try: + return extension in eval(text) + except Exception as e: + logger.debug("Failed testing if extension is enabled.", exc_info = e) + + return False + +def set_gnome_shell_extension_enabled(extension, enabled): + """ + Enable or disable a GNOME Shell extension is enabled. This uses the + gsettings command. Python GSettings bindings (GObject introspected ones) + are not used, as well already use PyGTK and the two don't mix + + Keyword arguments: + extension -- extension name + enabled -- enabled + """ + status, text = g15os.get_command_output("gsettings get org.gnome.shell enabled-extensions") + if status == 0: + try: + extensions = eval(text) + except Exception as e: + logger.debug('No gnome-shell extensions enabled.', exc_info = e) + # No extensions available, so init an empty array + extensions = [] + pass + contains = extension in extensions + if contains and not enabled: + extensions.remove(extension) + elif not contains and enabled: + extensions.append(extension) + s = "" + for c in extensions: + if len(s) >0: + s += "," + s += "'%s'" % c + try: + status, text = g15os.get_command_output("gsettings set org.gnome.shell enabled-extensions \"[%s]\"" % s) + except Exception as e: + logger.debug("Failed to set extension enabled.", exc_info = e) + +def browse(url): + """ + Open the configured browser + + Keyword arguments: + url -- URL + """ + b = g15gconf.get_string_or_default(gconf.client_get_default(), \ + "/apps/gnome15/browser", "default") + if not b in __browsers and not b == "default": + logger.warning("Could not find browser %s, falling back to default", b) + b = "default" + if not b in __browsers: + raise Exception("Could not find browser %s" % b) + __browsers[b].browse(url) + +def add_browser(browser): + """ + Register a new browser. The object must extend G15Browser + + Keyword arguments: + browser -- browser object. + """ + if browser.browser_id in __browsers: + raise Exception("Browser already registered") + if not isinstance(browser, G15Browser): + raise Exception("Not a G15Browser instance") + __browsers[browser.browser_id] = browser + +class G15Browser(): + def __init__(self, browser_id, name): + self.name = name + self.browser_id = browser_id + + def browse(self, url): + raise Exception("Not implemented") + +class G15DefaultBrowser(G15Browser): + def __init__(self): + G15Browser.__init__(self, "default", _("Default system browser")) + + def browse(self, url): + logger.info("xdg-open '%s'", url) + subprocess.Popen(['xdg-open', url]) + +add_browser(G15DefaultBrowser()) + +class G15AbstractService(Thread): + + def __init__(self): + Thread.__init__(self) + # Start this thread, which runs the gobject loop. This is + # run first, and in a thread, as starting the Gnome15 will send + # DBUS events (which are sent on the loop). + self.loop = gobject.MainLoop() + self.start() + + def start_loop(self): + logger.info("Starting GLib loop") + g15pythonlang.set_gobject_thread() + try: + self.loop.run() + except Exception as e: + logger.debug('Error while running GLib loop', exc_info = e) + logger.info("Exited GLib loop") + + def start_service(self): + raise Exception("Not implemented") + + def run(self): + # Now start the service, which will connect to all devices and + # start their plugins + self.start_service() + + +class G15Screen(): + """ + Client side representation of a remote screen. Holds general details such + as model name, UID and the pages that screen is currently showing. + """ + + def __init__(self, path, device_model_fullname, device_uid): + self.path = path + self.device_model_fullname = device_model_fullname + self.device_uid = device_uid + self.items = {} + self.message = None + +class G15DesktopComponent(): + """ + Helper class for implementing desktop components that can monitor and control some functions + of the desktop service. It is used for Indicators, System tray icons and panel applets and + deals with all the hard work of connecting to DBus and monitoring events. + """ + + def __init__(self): + self.screens = {} + self.service = None + self.start_service_item = None + self.attention_item = None + self.pages = [] + self.lock = RLock() + self.attention_messages = {} + self.connected = False + + # Connect to DBus and GConf + self.conf_client = gconf.client_get_default() + self.session_bus = dbus.SessionBus() + + # Enable monitoring of Gnome15 GConf settings + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + + # Initialise desktop component + self.initialise_desktop_component() + self.icons_changed() + + def start_service(self): + """ + Start the desktop component. An attempt will be made to connect to Gnome15 over + DBus. If this fails, the component should stay active until the service becomes + available. + """ + + # Try and connect to the service now + try : + self._connect() + except dbus.exceptions.DBusException as e: + logger.debug("Error while starting the service.", exc_info = e) + self._disconnect() + + # Start watching various events + self.conf_client.notify_add("/apps/gnome15/indicate_only_on_error", self._indicator_options_changed) + gtk_icon_theme = gtk.icon_theme_get_default() + gtk_icon_theme.connect("changed", self._theme_changed) + + # Watch for Gnome15 starting and stopping + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + """ + Pulic functions + """ + def is_attention(self): + return len(self.attention_messages) > 0 + + def get_icon_path(self, icon_name): + """ + Helper function to get an icon path or it's name, given the name. + """ + if g15globals.dev: + # Because the icons aren't installed in this mode, they must be provided + # using the full filename. Unfortunately this means scaling may be a bit + # blurry in the indicator applet + path = g15icontools.get_icon_path(icon_name, 128) + logger.debug("Dev mode icon %s is at %s", icon_name, path) + return path + else: + if not isinstance(icon_name, list): + icon_name = [ icon_name ] + for i in icon_name: + p = g15icontools.get_icon_path(i, -1) + if p is not None: + return i + + def show_configuration(self, arg = None): + """ + Show the configuration user interface + """ + g15os.run_script("g15-config") + + def stop_desktop_service(self, arg = None): + """ + Stop the desktop service + """ + self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').Stop() + + def start_desktop_service(self, arg = None): + """ + Start the desktop service + """ + g15os.run_script("g15-desktop-service", ["-f"]) + + def show_page(self, path): + """ + Show a page, given its path + """ + self.session_bus.get_object('org.gnome15.Gnome15', path).CycleTo() + + def check_attention(self): + """ + Check the current state of attention, either clearing it or setting it and displaying + a new message + """ + if len(self.attention_messages) == 0: + self.clear_attention() + else: + for i in self.attention_messages: + message = self.attention_messages[i] + self.attention(message) + break + + """ + Functions that must be implemented + """ + + def initialise_desktop_component(self): + """ + This function is called during construction and should create initial desktop component + """ + raise Exception("Not implemented") + + def rebuild_desktop_component(self): + """ + This function is called every time the list of screens or pages changes + in someway. The desktop component should be rebuilt to reflect the + new state + """ + raise Exception("Not implemented") + + def clear_attention(self): + """ + Clear any "Attention" state indicators + """ + raise Exception("Not implemented") + + def attention(self, message = None): + """ + Display an "Attention" state indicator with a message + + Keyword Arguments: + message -- message to display + """ + raise Exception("Not implemented") + + def icons_changed(self): + """ + Invoked once a start up, and then whenever the desktop icon theme changes. Implementations + should do whatever required to change any themed icons they are displayed + """ + raise Exception("Not implemented") + + def options_changed(self): + """ + Invoked when any global desktop component options change. + """ + raise Exception("Not implemented") + + ''' + DBUS Event Callbacks + ''' + def _name_owner_changed(self, name, old_owner, new_owner): + if name == "org.gnome15.Gnome15": + if old_owner == "": + if self.service == None: + self._connect() + else: + if self.service != None: + self.connected = False + self._disconnect() + + def _page_created(self, page_path, page_title, path = None): + screen_path = path + logger.debug("Page created (%s) %s = %s", screen_path, page_path, page_title) + page = self.session_bus.get_object('org.gnome15.Gnome15', page_path ) + self.lock.acquire() + try : + if page.GetPriority() >= g15screen.PRI_LOW: + self._add_page(screen_path, page_path, page) + finally : + self.lock.release() + + def _page_title_changed(self, page_path, title, path = None): + screen_path = path + self.lock.acquire() + try : + self.screens[screen_path].items[page_path] = title + self.rebuild_desktop_component() + finally : + self.lock.release() + + def _page_deleting(self, page_path, path = None): + screen_path = path + self.lock.acquire() + logger.debug("Destroying page (%s) %s", screen_path, page_path) + try : + items = self.screens[screen_path].items + if page_path in items: + del items[page_path] + self.rebuild_desktop_component() + finally : + self.lock.release() + + def _attention_cleared(self, path = None): + screen_path = path + if screen_path in self.attention_messages: + del self.attention_messages[screen_path] + self.rebuild_desktop_component() + + def _attention_requested(self, message = None, path = None): + screen_path = path + if not screen_path in self.attention_messages: + self.attention_messages[screen_path] = message + self.rebuild_desktop_component() + + """ + Private + """ + + def _enable(self, widget, device): + device.Enable() + + def _disable(self, widget, device): + device.Disable() + + def _cycle_screens_option_changed(self, client, connection_id, entry, args): + self.rebuild_desktop_component() + + def _remove_screen(self, screen_path): + print "*** removing %s from %s" % ( str(screen_path), str(self.screens)) + if screen_path in self.screens: + try : + del self.screens[screen_path] + except dbus.DBusException as e: + logger.debug("Error removing screen '%s'", screen_path, exc_info = e) + pass + self.rebuild_desktop_component() + + def _add_screen(self, screen_path): + logger.debug("Screen added %s", screen_path) + remote_screen = self.session_bus.get_object('org.gnome15.Gnome15', screen_path) + ( device_uid, device_model_name, device_usb_id, device_model_fullname ) = remote_screen.GetDeviceInformation() + screen = G15Screen(screen_path, device_model_fullname, device_uid) + self.screens[screen_path] = screen + if remote_screen.IsAttentionRequested(): + screen.message = remote_screen.GetMessage() + + def _device_added(self, screen_path): + self.rebuild_desktop_component() + + def _device_removed(self, screen_path): + self.rebuild_desktop_component() + + def _connect(self): + logger.debug("Connecting") + self._reset_attention() + self.service = self.session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + self.connected = True + logger.debug("Connected") + + # Load the initial screens + self.lock.acquire() + try : + for screen_path in self.service.GetScreens(): + logger.debug("Adding %s", screen_path) + self._add_screen(screen_path) + remote_screen = self.session_bus.get_object('org.gnome15.Gnome15', screen_path) + for page_path in remote_screen.GetPages(): + page = self.session_bus.get_object('org.gnome15.Gnome15', page_path) + if page.GetPriority() >= g15screen.PRI_LOW and page.GetPriority() < g15screen.PRI_HIGH: + self._add_page(screen_path, page_path, page) + finally : + self.lock.release() + + # Listen for events + self.session_bus.add_signal_receiver(self._device_added, dbus_interface = "org.gnome15.Service", signal_name = "DeviceAdded") + self.session_bus.add_signal_receiver(self._device_removed, dbus_interface = "org.gnome15.Service", signal_name = "DeviceRemoved") + self.session_bus.add_signal_receiver(self._add_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenAdded") + self.session_bus.add_signal_receiver(self._remove_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenRemoved") + self.session_bus.add_signal_receiver(self._page_created, dbus_interface = "org.gnome15.Screen", signal_name = "PageCreated", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._page_title_changed, dbus_interface = "org.gnome15.Screen", signal_name = "PageTitleChanged", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._page_deleting, dbus_interface = "org.gnome15.Screen", signal_name = "PageDeleting", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._attention_requested, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionRequested", path_keyword = 'path') + self.session_bus.add_signal_receiver(self._attention_cleared, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionCleared", path_keyword = 'path') + + # We are now connected, so remove the start service menu item and allow cycling + self.rebuild_desktop_component() + + def _disconnect(self): + logger.debug("Disconnecting") + self.session_bus.remove_signal_receiver(self._device_added, dbus_interface = "org.gnome15.Service", signal_name = "DeviceAdded") + self.session_bus.remove_signal_receiver(self._device_removed, dbus_interface = "org.gnome15.Service", signal_name = "DeviceRemoved") + self.session_bus.remove_signal_receiver(self._add_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenAdded") + self.session_bus.remove_signal_receiver(self._remove_screen, dbus_interface = "org.gnome15.Service", signal_name = "ScreenRemoved") + self.session_bus.remove_signal_receiver(self._page_created, dbus_interface = "org.gnome15.Screen", signal_name = "PageCreated") + self.session_bus.remove_signal_receiver(self._page_title_changed, dbus_interface = "org.gnome15.Screen", signal_name = "PageTitleChanged") + self.session_bus.remove_signal_receiver(self._page_deleting, dbus_interface = "org.gnome15.Screen", signal_name = "PageDeleting") + self.session_bus.remove_signal_receiver(self._attention_requested, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionRequested") + self.session_bus.remove_signal_receiver(self._attention_cleared, dbus_interface = "org.gnome15.Screen", signal_name = "AttentionCleared") + + if self.service != None and self.connected: + for screen_path in dict(self.screens): + self._remove_screen(screen_path) + + self._reset_attention() + self._attention_requested("service", "g15-desktop-service is not running.") + + self.service = None + self.connected = False + self.rebuild_desktop_component() + + def _reset_attention(self): + self.attention_messages = {} + self.rebuild_desktop_component() + + def _add_page(self, screen_path, page_path, page): + logger.debug("Adding page %s to %s", page_path, screen_path) + items = self.screens[screen_path].items + if not page_path in items: + items[page_path] = page.GetTitle() + self.rebuild_desktop_component() + + def _indicator_options_changed(self, client, connection_id, entry, args): + self.options_changed() + + def _theme_changed(self, theme): + self.icons_changed() + + +class G15GtkMenuPanelComponent(G15DesktopComponent): + + def __init__(self): + self.screen_number = 0 + self.devices = [] + self.notify_message = None + G15DesktopComponent.__init__(self) + + def about_info(self, widget): + about = gtk.AboutDialog() + about.set_name("Gnome15") + about.set_version(g15globals.version) + about.set_license(GPL) + about.set_authors(AUTHORS) + about.set_documenters(["Brett Smith "]) + about.set_logo(gtk.gdk.pixbuf_new_from_file(g15icontools.get_app_icon(self.conf_client, "gnome15", 128))) + about.set_comments(_("Desktop integration for Logitech 'G' keyboards.")) + about.run() + about.hide() + + def scroll_event(self, widget, event): + + direction = event.direction + if direction == gtk.gdk.SCROLL_UP: + screen = self._get_active_screen_object() + self._close_notify_message() + screen.ClearPopup() + screen.Cycle(1) + elif direction == gtk.gdk.SCROLL_DOWN: + screen = self._get_active_screen_object() + self._close_notify_message() + screen.ClearPopup() + screen.Cycle(-1) + else: + """ + If there is only one device, right scroll cycles the backlight color, + otherwise toggle between the devices (used to select what to scroll with up + and down) + """ + if direction == gtk.gdk.SCROLL_LEFT: + self._get_active_screen_object().CycleKeyboard(-1) + elif direction == gtk.gdk.SCROLL_RIGHT: + if len(self.screens) > 1: + if self.screen_number >= len(self.screens) - 1: + self.screen_number = 0 + else: + self.screen_number += 1 + + self._set_active_screen_number() + else: + self._get_active_screen_object().CycleKeyboard(1) + + def rebuild_desktop_component(self): + logger.debug("Removing old menu items") + for item in self.last_items: + item.get_parent().remove(item) + item.destroy() + + self.last_items = [] + i = 0 + + # Remove the notify handles used for the previous cycle components + logger.debug("Removing old notify handles") + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + logger.debug("Building new menu") + if self.service and self.connected: + + item = gtk.MenuItem(_("Stop Desktop Service")) + item.connect("activate", self.stop_desktop_service) + self.add_service_item(item) + self.add_service_item(gtk.MenuItem()) + + try: + devices = self.service.GetDevices() + for device_path in devices: + remote_device = self.session_bus.get_object('org.gnome15.Gnome15', device_path) + screen_path = remote_device.GetScreen() + + screen = self.screens[screen_path] if len(screen_path) > 0 and screen_path in self.screens else None + + if screen: + if i > 0: + logger.debug("Adding separator") + self._append_item(gtk.MenuItem()) + # Disable + if len(devices) > 1: + item = gtk.MenuItem("Disable %s" % screen.device_model_fullname) + item.connect("activate", self._disable, remote_device) + self.add_service_item(item) + + # Cycle screens + item = gtk.CheckMenuItem(_("Cycle screens automatically")) + item.set_active(g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/%s/cycle_screens" % screen.device_uid, True)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/cycle_screens" % screen.device_uid, self._cycle_screens_option_changed)) + item.connect("toggled", self._cycle_screens_changed, screen.device_uid) + self._append_item(item) + + # Alert message + if screen.message: + self._append_item(gtk.MenuItem(screen.message)) + + logger.debug("Adding items") + + + sorted_x = sorted(screen.items.iteritems(), key=operator.itemgetter(1)) + for item_key, text in sorted_x: + logger.debug("Adding item %s = %s ", item_key, text) + item = gtk.MenuItem(text) + item.connect("activate", self._show_page, item_key) + self._append_item(item) + else: + # Enable + if len(devices) > 1: + item = gtk.MenuItem(_("Enable %s") % remote_device.GetModelFullName()) + item.connect("activate", self._enable, remote_device) + self.add_service_item(item) + i += 1 + except Exception as e: + logger.debug("Failed to find devices, service probably stopped.", exc_info = e) + self.connected = False + self.rebuild_desktop_component() + + self.devices = devices + else: + self.devices = [] + self.add_start_desktop_service() + + self.menu.show_all() + self.check_attention() + + def add_start_desktop_service(self): + item = gtk.MenuItem(_("Start Desktop Service")) + item.connect("activate", self.start_desktop_service) + self.add_service_item(item) + + def add_service_item(self, item): + self._append_item(item) + + def initialise_desktop_component(self): + + self.last_items = [] + self.start_service_item = None + self.attention_item = None + self.notify_handles = [] + + # Indicator menu + self.menu = gtk.Menu() + self.create_component() + self.menu.show_all() + + def create_component(self): + raise Exception("Not implemented") + + def remove_attention_menu_item(self): + if self.attention_item != None: + self.menu.remove(self.attention_item) + self.attention_item.destroy() + self.menu.show_all() + self.attention_item = None + + def options_changed(self): + self.check_attention() + + """ + Private + """ + + def _get_active_screen_object(self): + screen = list(self.screens.values())[self.screen_number] + return self.session_bus.get_object('org.gnome15.Gnome15', screen.path) + + def _set_active_screen_number(self): + self._close_notify_message() + screen = list(self.screens.values())[self.screen_number] + body = _("%s is now the active keyboard. Use mouse wheel up and down to cycle screens on this device") % screen.device_model_fullname + self.notify_message = g15notify.notify(screen.device_model_fullname, body, "preferences-desktop-keyboard-shortcuts") + + def _close_notify_message(self): + if self.notify_message is not None: + try: + self.notify_message.close() + except Exception as e: + logger.debug("Failed to close message.", exc_info = e) + self.notify_message = None + + def _append_item(self, item, menu = None): + self.last_items.append(item) + if menu is None: + menu = self.menu + menu.append(item) + + def _show_page(self,event, page_path): + self.show_page(page_path) + + def _cycle_screens_changed(self, widget, device_uid): + self.conf_client.set_bool("/apps/gnome15/%s/cycle_screens" % device_uid, widget.get_active()) + +if __name__ == "__main__": + print "g15-systemtray installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-systemtray"), is_autostart_application("g15-systemtray") ) + print "g15-desktop-service installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-desktop-service"), is_autostart_application("g15-desktop-service") ) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) + print "dropbox installed = %s, enabled = %s" % ( is_desktop_application_installed("dropbox"), is_autostart_application("dropbox") ) + print "xdropbox installed = %s, enabled = %s" % ( is_desktop_application_installed("xdropbox"), is_autostart_application("xdropbox") ) + print "nepomukserver installed = %s, enabled = %s" % ( is_desktop_application_installed("nepomukserver"), is_autostart_application("nepomukserver") ) + set_autostart_application("g15-indicator", False) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) + set_autostart_application("g15-indicator", True) + print "g15-indicator installed = %s, enabled = %s" % ( is_desktop_application_installed("g15-indicator"), is_autostart_application("g15-indicator") ) diff --git a/src/gnome15/g15devices.py b/src/gnome15/g15devices.py new file mode 100644 index 0000000..1397d3d --- /dev/null +++ b/src/gnome15/g15devices.py @@ -0,0 +1,485 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import usb +import g15driver +import g15actions +import util.g15pythonlang as g15pythonlang +import g15drivermanager + +# Logging +import logging +logger = logging.getLogger(__name__) + +''' +Keyboard layouts +''' + +z10_key_layout = [ + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ] + ] + +z10_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_L5 ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_HELD), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_HELD) + } + +g11_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g11_action_keys = { + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +g15v1_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g510_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_G7, g15driver.G_KEY_G8, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_G13, g15driver.G_KEY_G14, g15driver.G_KEY_G15 ], + [ g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g15v2_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g13_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3, g15driver.G_KEY_G4, g15driver.G_KEY_G5, g15driver.G_KEY_G6, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G8, g15driver.G_KEY_G9, g15driver.G_KEY_G10, g15driver.G_KEY_G11, g15driver.G_KEY_G12, g15driver.G_KEY_G13, g15driver.G_KEY_G14 ], + [ g15driver.G_KEY_G15, g15driver.G_KEY_G16, g15driver.G_KEY_G17, g15driver.G_KEY_G18, g15driver.G_KEY_G19 ], + [ g15driver.G_KEY_G20, g15driver.G_KEY_G21, g15driver.G_KEY_G22 ], + [ g15driver.G_KEY_L1, g15driver.G_KEY_L2, g15driver.G_KEY_L3, g15driver.G_KEY_L4, g15driver.G_KEY_L5 ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +g930_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G2, g15driver.G_KEY_G3 ] + ] + +""" +Unfortunately we have to leave L1 clear for g15daemon for the moment +""" +g15_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_L5 ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_L2 ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_L4 ], g15driver.KEY_STATE_HELD), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_L3 ], g15driver.KEY_STATE_HELD), + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +""" +G110 - Only actions we need really are the memory bank ones +""" +g110_action_keys = { + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +g110_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G2, g15driver.G_KEY_G8 ], + [ g15driver.G_KEY_G3, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G10 ], + [ g15driver.G_KEY_G5, g15driver.G_KEY_G11], + [ g15driver.G_KEY_G6, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_MIC_MUTE, g15driver.G_KEY_HEADPHONES_MUTE ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ] + ] + +""" +G19 +""" +g19_key_layout = [ + [ g15driver.G_KEY_G1, g15driver.G_KEY_G7 ], + [ g15driver.G_KEY_G2, g15driver.G_KEY_G8 ], + [ g15driver.G_KEY_G3, g15driver.G_KEY_G9 ], + [ g15driver.G_KEY_G4, g15driver.G_KEY_G10 ], + [ g15driver.G_KEY_G5, g15driver.G_KEY_G11 ], + [ g15driver.G_KEY_G6, g15driver.G_KEY_G12 ], + [ g15driver.G_KEY_UP ], + [ g15driver.G_KEY_LEFT, g15driver.G_KEY_OK, g15driver.G_KEY_RIGHT ], + [ g15driver.G_KEY_DOWN ], + [ g15driver.G_KEY_MENU, g15driver.G_KEY_BACK, g15driver.G_KEY_SETTINGS ], + [ g15driver.G_KEY_M1, g15driver.G_KEY_M2, g15driver.G_KEY_M3, g15driver.G_KEY_MR ], + ] +g19_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_DOWN), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_DOWN), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_OK ], g15driver.KEY_STATE_UP), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_MENU ], g15driver.KEY_STATE_UP), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_UP), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_SETTINGS ], g15driver.KEY_STATE_UP), + g15driver.NEXT_PAGE: g15actions.ActionBinding(g15driver.NEXT_PAGE, [ g15driver.G_KEY_RIGHT ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_PAGE: g15actions.ActionBinding(g15driver.PREVIOUS_PAGE, [ g15driver.G_KEY_LEFT ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_1: g15actions.ActionBinding(g15driver.MEMORY_1, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_2: g15actions.ActionBinding(g15driver.MEMORY_2, [ g15driver.G_KEY_M2 ], g15driver.KEY_STATE_UP), + g15driver.MEMORY_3: g15actions.ActionBinding(g15driver.MEMORY_3, [ g15driver.G_KEY_M3 ], g15driver.KEY_STATE_UP) + } + +""" +MX5500 + +Only two keys near the LCD, so various combinations of keys and holding keys is used to +provide the 6 most basic actions +""" + +mx5500_key_layout = [ + [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ] + ] +mx5500_action_keys = { g15driver.NEXT_SELECTION: g15actions.ActionBinding(g15driver.NEXT_SELECTION, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_UP), + g15driver.PREVIOUS_SELECTION: g15actions.ActionBinding(g15driver.PREVIOUS_SELECTION, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_UP), + g15driver.SELECT: g15actions.ActionBinding(g15driver.SELECT, [ g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_HELD), + g15driver.MENU: g15actions.ActionBinding(g15driver.MENU, [ g15driver.G_KEY_UP ], g15driver.KEY_STATE_HELD), + g15driver.CLEAR: g15actions.ActionBinding(g15driver.CLEAR, [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_HELD), + g15driver.VIEW: g15actions.ActionBinding(g15driver.VIEW, [ g15driver.G_KEY_UP, g15driver.G_KEY_DOWN ], g15driver.KEY_STATE_UP) + } + +# Registered Logitech models +device_list = { } +device_by_usb_id = { } +__cached_devices = [] + +class DeviceInfo(): + """ + Represents the characteristics of a single model of a Logitech device. + """ + def __init__(self, model_id, usb_id_list, controls_usb_id_list, key_layout, bpp, lcd_size, macros, model_fullname, action_keys): + """ + Creates a new DeviceInfo and add's it to the registered device types + + Keyword arguments + model_id -- model ID (as found in g15driver constants) + usb_id_list -- list or single tuple with vendor and product codes (this is the device searched for). + controls_usb_id_list -- tuple with vendor and product codes for the controls device + key_layout -- keyboard layout + bpp -- the number of bits per pixel (or 0 for no LCD) + lcd_size -- the size of the LCD (or None for no LCD) + macros -- the model has macro keys (G-Keys) + model_fullname -- full name of the model + action_keys -- default keybinds to use for actions + """ + self.model_id = model_id + self.key_layout = key_layout + self.macros = macros + self.bpp = bpp + self.lcd_size = lcd_size + self.action_keys = action_keys + self.model_fullname = model_fullname + device_list[self.model_id] = self + + # Some devices (1 currently) use a different USBID for controls + if controls_usb_id_list is None: + controls_usb_id_list = usb_id_list + + # Some devices may have multiple usb ID's (for audio mode) + if not isinstance(usb_id_list, list): + usb_id_list = [usb_id_list] + if not isinstance(controls_usb_id_list, list): + controls_usb_id_list = [controls_usb_id_list] + if not len(usb_id_list) == len(controls_usb_id_list): + raise Exception("Controls USB ID list is not the same length as USB ID list") + self.usb_id_list = usb_id_list + self.controls_usb_id_list = controls_usb_id_list + + # Map all the devices + for c in self.usb_id_list: + device_by_usb_id[c] = self + + # Gather all keys in the layout + self.all_keys = [] + for row in self.key_layout: + for key in row: + self.all_keys.append(key) + + def matches(self, usb_id): + return usb_id in self.controls_usb_id_list or usb_id in self.usb_id_list + + def __repr__(self): + return "DeviceInfo [%s/%s] model: %s (%s). Has a %d BPP screen of %dx%d. " % \ + ( str(self.usb_id_list), str(self.controls_usb_id_list), self.model_id, self.model_fullname, self.bpp, self.lcd_size[0], self.lcd_size[1]) + +class Device(): + """ + Represents a single discovered device. + + Keyword arguments + usb_id -- the actual ID + usb_device -- the USB device + device_info -- DeviceInfo object containing static model information + """ + def __init__(self, usb_id, controls_usb_id, usb_device, index, device_info): + self.usb_id = usb_id + self.controls_usb_id = controls_usb_id + + self.usb_device = usb_device + self.index = index + self.uid = "%s_%d" % ( device_info.model_id, index ) + self.model_id = device_info.model_id + self.key_layout = device_info.key_layout + self.bpp = device_info.bpp + self.lcd_size = device_info.lcd_size + self.model_fullname = device_info.model_fullname + self.all_keys = device_info.all_keys + self.action_keys = device_info.action_keys + + def get_key_index(self, key): + if key in self.all_keys: + self.all_keys.index(key) + + def __hash__(self): + return self.ui.__hash() + + def __eq__(self, o): + try: + return o is not None and self.uid == o.uid + except AttributeError as e: + logger.debug('AttributeError when comparing Device', exc_info = e) + return False + + def __repr__(self): + usb_str = hex(self.usb_id[0]) if self.usb_id is not None and len(self.usb_id) > 0 else "Unknown" + usb_str2 = hex(self.usb_id[1]) if self.usb_id is not None and len(self.usb_id) > 1 else "Unknown" + sz1 = self.lcd_size[0] if self.lcd_size is not None and len(self.lcd_size) > 0 else "??" + sz2 = self.lcd_size[1] if self.lcd_size is not None and len(self.lcd_size) > 1 else "??" + return "Device [%s] %s model: %s (%s) on USB ID %s:%s. Has a %d BPP screen of %dx%d. " % \ + ( str(self.usb_device), self.uid, self.model_id, self.model_fullname, usb_str, usb_str2, self.bpp, sz1, sz2) + +def are_keys_reserved(model_id, keys): + if len(keys) < 1: + raise Exception("Empty key list provided") + device_info = get_device_info(model_id) + if device_info is None: + raise Exception("No device with ID of %s" % model_id) + for action_binding in device_info.action_keys.values(): + if sorted(keys) == sorted(action_binding.keys): + return True + return False + +def get_device_info(model_id): + return device_list[model_id] + +def is_enabled(conf_client, device): + val = conf_client.get("/apps/gnome15/%s/enabled" % device.uid) + return ( val == None and device.model_id != "virtual" ) or ( val is not None and val.get_bool() ) + +def set_enabled(conf_client, device, enabled): + conf_client.set_bool("/apps/gnome15/%s/enabled" % device.uid, enabled) + +def get_device(uid): + """ + Find the device with the specified UID. + """ + for d in find_all_devices(): + if d.uid == uid: + return d + +def find_all_devices(do_cache = True): + global __cached_devices + + """ + Get a list of Device objects, one for each supported device that is plugged in. + There may be more than one device of the same type. + """ + + # If we have pydev, we can cache the devices + if do_cache and have_udev and len(__cached_devices) != 0: + return __cached_devices + + device_map = {} + + # Find all supported devices plugged into USB + for bus in usb.busses(): + for usb_device in bus.devices: + key = ( usb_device.idVendor, usb_device.idProduct ) + # Is a supported device + if not key in device_map: + device_map[key] = [] + device_map[key].append(usb_device) + + # Turn the found USB devices into Device objects + devices = [] + indices = {} + + for device_key in device_map: + usb_devices = device_map[device_key] + for usb_device in usb_devices: + if device_key in device_by_usb_id: + device_info = device_by_usb_id[device_key] + """ + Take the quirk of the G11/G15 into account. This check means that only one of each + type can exist at a time, but any more is pretty unlikely + """ + if device_info.model_id == g15driver.MODEL_G15_V1 and not (0x046d, 0xc222) in device_map: + # Actually a G11, will be detected with id (0x046d, 0xc225) + continue + elif device_info.model_id == g15driver.MODEL_G11 and (0x046d, 0xc222) in device_map: + # Actually a G15v1 + device_info = device_list[g15driver.MODEL_G15_V1] + + """ + Now create the device instance that will be used by the caller + """ + index = 0 if not device_key in indices else indices[device_key] + controls_usb_id = device_info.controls_usb_id_list[device_info.usb_id_list.index(device_key)] + devices.append(Device(device_key, controls_usb_id, usb_device, index, device_info)) + indices[device_key] = index + 1 + + + """ + If the GTK driver is installed, add a virtual device as well + """ + if g15drivermanager.get_driver_mod("gtk"): + devices.append(Device((0x0000, 0x0000), (0x0000, 0x0000), None, 0, device_list['virtual'])) + + # If we have pydev, we can cache the devices + if have_udev and do_cache: + __cached_devices += devices + + return devices + +def find_device(models): + for lg_model in find_all_devices(): + for model in models: + if lg_model.model_name == model: + return lg_model + +def _get_cached_device_by_usb_id(usb_id): + for c in __cached_devices: + if c.usb_id == usb_id: + return c + +""" +Register all supported models +""" +if g15drivermanager.get_driver_mod("gtk"): + DeviceInfo('virtual', (0x0000, 0x0000), None, [], 0, ( 0, 0 ), False, _("Virtual LCD Window"), None) +DeviceInfo(g15driver.MODEL_G11, (0x046d, 0xc225), None, g11_key_layout, 0, ( 0, 0 ), True, _("Logitech G11 Keyboard"), g11_action_keys) +DeviceInfo(g15driver.MODEL_G19, (0x046d, 0xc229), None, g19_key_layout, 16, ( 320, 240 ), True, _("Logitech G19 Gaming Keyboard"), g19_action_keys) +DeviceInfo(g15driver.MODEL_G15_V1, (0x046d, 0xc221), (0x046d, 0xc222), g15v1_key_layout, 1, ( 160, 43 ), True, _("Logitech G15 Gaming Keyboard (version 1)"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G15_V2, (0x046d, 0xc227), None, g15v2_key_layout, 1, ( 160, 43 ), True, _("Logitech G15 Gaming Keyboard (version 2)"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G13, (0x046d, 0xc21c), None, g13_key_layout, 1, ( 160, 43 ), True, _("Logitech G13 Advanced Gameboard"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G510, [ (0x046d, 0xc22d), + (0x046d, 0xc22e) ], None, g510_key_layout, 1, ( 160, 43 ), True, _("Logitech G510 Keyboard"), g15_action_keys) +DeviceInfo(g15driver.MODEL_Z10, (0x046d, 0x0a07), None, z10_key_layout, 1, ( 160, 43 ), False, _("Logitech Z10 Speakers"), z10_action_keys) +DeviceInfo(g15driver.MODEL_G110, (0x046d, 0xc22b), None, g110_key_layout, 0, ( 0, 0 ), True, _("Logitech G110 Keyboard"), g110_action_keys) +DeviceInfo(g15driver.MODEL_GAMEPANEL, (0x046d, 0xc251), None, g15v1_key_layout, 1, ( 160, 43 ), True, _("Logitech GamePanel"), g15_action_keys) +DeviceInfo(g15driver.MODEL_G930, (0x046d, 0xa1f), None, g930_key_layout, 0, ( 0, 0 ), True, _("Logitech G930 Headphones"), {}) +DeviceInfo(g15driver.MODEL_G35, (0x046d, 0xa15), None, g930_key_layout, 0, ( 0, 0 ), True, _("Logitech G35 Headphones"), {}) + +# When I get hold of an MX5500, I will add Bluetooth detection as well +DeviceInfo(g15driver.MODEL_MX5500, (0x0000, 0x0000), (0x0000, 0x0000), mx5500_key_layout, 1, ( 136, 32 ), False, _("Logitech MX5500"), mx5500_action_keys) + +# If we have pyudev, we can monitor for devices being plugged in and unplugged +have_udev = False +device_added_listeners = [] +device_removed_listeners = [] + +def __device_added(observer, device): + uevent_attr = device.attributes.get('uevent', None) + if uevent_attr != None: + uevent = g15pythonlang.parse_as_properties(uevent_attr) + if "PRODUCT" in uevent: + if device.attributes.get("subsystem", None) == "usb": + major,minor,_ = uevent["PRODUCT"].split("/") + else: + _,major,minor,_ = uevent["PRODUCT"].split("/") + for c in device_list: + device_info = device_list[c] + usb_id = (int(major, 16), int(minor, 16)) + if device_info.matches(usb_id): + if not _get_cached_device_by_usb_id(usb_id): + del __cached_devices[:] + find_all_devices() + for r in reversed(__cached_devices): + if r.usb_id == usb_id: + logger.info("Added device %s", r) + for l in device_added_listeners: + l(r) + break + break + +def __device_removed(observer, device): + current_devices = list(__cached_devices) + new_devices = find_all_devices(do_cache = False) + found = False + for d in current_devices: + for e in new_devices: + if e.uid == d.uid: + found = True + break + if not found: + if d in __cached_devices: + __cached_devices.remove(d) + for l in device_removed_listeners: + l(d) + break + +try: + import pyudev.glib + __context = pyudev.Context() + __monitor = pyudev.Monitor.from_netlink(__context) + __observer = pyudev.glib.GUDevMonitorObserver(__monitor) + __observer.connect('device-added', __device_added) + __observer.connect('device-removed', __device_removed) + find_all_devices() + have_udev = True + __monitor.start() +except Exception as e: + logger.info("Failed to get PyUDev context, hot plugging support not available", exc_info = e) + +if __name__ == "__main__": + for device in find_all_devices(): + print str(device) + \ No newline at end of file diff --git a/src/gnome15/g15driver.py b/src/gnome15/g15driver.py new file mode 100644 index 0000000..cbc75d2 --- /dev/null +++ b/src/gnome15/g15driver.py @@ -0,0 +1,794 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Default actions + +TODO MOVE THESE TO G15ACTIONS +""" +NEXT_SELECTION = "next-sel" +PREVIOUS_SELECTION = "prev-sel" +NEXT_PAGE = "next-page" +PREVIOUS_PAGE = "prev-page" +SELECT = "select" +VIEW = "view" +CLEAR = "clear" +MENU = "menu" +MEMORY_1 = "memory-1" +MEMORY_2 = "memory-2" +MEMORY_3 = "memory-3" + +""" +Bitmask values for setting the M key LED lights. See set_mkey_lights() +""" +MKEY_LIGHT_1 = 1<<0 +MKEY_LIGHT_2 = 1<<1 +MKEY_LIGHT_3 = 1<<2 +MKEY_LIGHT_MR = 1<<3 + +""" +Constants for key codes +""" +KEY_STATE_UP = 0 +KEY_STATE_DOWN = 1 +KEY_STATE_HELD = 2 + +""" +G keys + +G15v1 - G1-G18 +G15v1 - G1-G18 +G13 - G1-22 +G19 - G1-G12 +""" +G_KEY_G1 = "g1" +G_KEY_G2 = "g2" +G_KEY_G3 = "g3" +G_KEY_G4 = "g4" +G_KEY_G5 = "g5" +G_KEY_G6 = "g6" +G_KEY_G7 = "g7" +G_KEY_G8 = "g8" +G_KEY_G9 = "g9" +G_KEY_G10 = "g10" +G_KEY_G11 = "g11" +G_KEY_G12 = "g12" +G_KEY_G13 = "g13" +G_KEY_G14 = "g14" +G_KEY_G15 = "g15" +G_KEY_G16 = "g16" +G_KEY_G17 = "g17" +G_KEY_G18 = "g18" +G_KEY_G19 = "g19" +G_KEY_G20 = "g20" +G_KEY_G21 = "g21" +G_KEY_G22 = "g22" + +""" +Joystick (G13) +""" +G_KEY_JOY_LEFT = "jl" +G_KEY_JOY_DOWN = "jd" +G_KEY_JOY_CENTER = "jc" +G_KEY_JOY = "js" + + +""" +Display keys +""" +G_KEY_BACK = "back" +G_KEY_DOWN = "down" +G_KEY_LEFT = "left" +G_KEY_MENU = "menu" +G_KEY_OK = "ok" +G_KEY_RIGHT = "right" +G_KEY_SETTINGS = "settings" +G_KEY_UP = "up" + +""" +M keys. On all models +""" +G_KEY_M1 = "m1" +G_KEY_M2 = "m2" +G_KEY_M3 = "m3" +G_KEY_MR = "mr" + +""" +L-Keys. On g15v1, v2, g13 and g19. NOT on g110 +""" +G_KEY_L1 = "l1" +G_KEY_L2 = "l2" +G_KEY_L3 = "l3" +G_KEY_L4 = "l4" +G_KEY_L5 = "l5" + +""" +Light key. On all models +""" +G_KEY_LIGHT = "light" + +""" +Multimedia keys +""" +G_KEY_WINKEY_SWITCH = "win" +G_KEY_NEXT = "next" +G_KEY_PREV = "prev" +G_KEY_STOP = "stop" +G_KEY_PLAY = "play" +G_KEY_MUTE = "mute" +G_KEY_VOL_UP = "vol-up" +G_KEY_VOL_DOWN = "vol-down" + +""" +G110 only +""" +G_KEY_MIC_MUTE = "mic-mute" +G_KEY_HEADPHONES_MUTE = "headphones-mute" + +""" +Models +""" +MODEL_G15_V1 = "g15v1" +MODEL_G15_V2 = "g15v2" +MODEL_G11 = "g11" +MODEL_G13 = "g13" +MODEL_G19 = "g19" +MODEL_G930 = "g930" +MODEL_G35 = "g35" +MODEL_G510 = "g510" +MODEL_G110 = "g110" +MODEL_Z10 = "z10" +MODEL_MX5500 = "mx5500" +MODEL_GAMEPANEL = "gamepanel" + +MODELS = [ MODEL_G15_V1, MODEL_G15_V2, MODEL_G11, MODEL_G13, MODEL_G19, MODEL_G510, MODEL_G110, MODEL_Z10, MODEL_MX5500, MODEL_GAMEPANEL, MODEL_G930, MODEL_G35 ] + +""" +Control hints +""" +HINT_DIMMABLE = 1 << 0 +HINT_SHADEABLE = 1 << 1 +HINT_FOREGROUND = 1 << 2 +HINT_BACKGROUND = 1 << 3 +HINT_HIGHLIGHT = 1 << 4 +HINT_SWITCH = 1 << 5 +HINT_MKEYS = 1 << 6 +HINT_VIRTUAL = 1 << 7 +HINT_RED_BLUE_LED = 1 << 8 + +# 16bit 565 +CAIRO_IMAGE_FORMAT=4 + +FX_QUEUE = "ControlEffects" + +import util.g15scheduler as g15scheduler +import time +import colorsys +from threading import Lock +from threading import Event +import logging +logger = logging.getLogger(__name__) + +seq_no = 0 + +def get_key_names(keys): + """ + Get the string name of the key given it's code + """ + key_names = [] + for key in keys: + key_names.append((key[:1].upper() + key[1:].lower()).replace('-',' ')) + return key_names + +def zeroize(val): + """ + Zeroise a control value (will be used for the fully off value). The type + returned will be the same as the type provided + + Keyword arguments: + val -- current value + """ + if isinstance(val, int): + return 0 + elif isinstance(val, tuple): + return (0, 0, 0) + else: + return False + +def get_mask_for_memory_bank(bank): + """ + Get the M_KEY_LIGHT_* mask given the memory bank number (1,2 or 3) + + Keyword arguments: + bank -- memory bank + """ + if bank == 1: + return MKEY_LIGHT_1 + elif bank == 2: + return MKEY_LIGHT_2 + elif bank == 3: + return MKEY_LIGHT_3 + +def get_memory_bank_for_mask(val): + """ + Get the memory bank that is activated given the M_KEY_LIGHT_* mask + + Keyword arguments: + val -- light key mask value + """ + if val & MKEY_LIGHT_3 != 0: + return 3 + elif val & MKEY_LIGHT_2 != 0: + return 2 + elif val & MKEY_LIGHT_1 != 0: + return 1 + return 0 + +class Control(): + """ + Represents a single "Control". Each control represents a feature of the + device that may be adjusted. For example, the red, green and blue LEDs of + the keyboard backlight on a G19. + + A control's value will be of different classes depending on the type of + control this instance represents. + + A On/Off or single value controls (such as brightness of a single LED is + represented as as int). + + A colour control is a tuple consisting of R,G and B intensity values. + + Other controls are boolean (currently not used) + """ + + def __init__(self, id, name, value = 0.0, lower = 0.0, upper = 255.0, hint = 0): + """ + Constructor + + Keyword arguments: + id -- unique control ID + name -- an english name for the control + value -- the initial value (int,tuple or bool) + lower -- minimum value the control may be + upper -- maximum value the control may be + hint -- bitmask of HINT_ consants describing the control + """ + self.id = id + self.hint = hint + self.name = name + self.lower = lower + self.upper = upper + self.value = value + self.default_value = value + + def set_from_configuration(self, device, conf_client): + """ + Configure this control's value from it's gconf entry for the provided device + + Keyword arguments: + device device the control is associated with + conf_client configuration client instance + """ + entry = conf_client.get("/apps/gnome15/%s/%s" % ( device.uid, self.id )) + if entry != None: + if isinstance(self.default_value, int): + self.value = entry.get_int() + elif isinstance(self.default_value, tuple): + rgb = entry.get_string().split(",") + self.value = (int(rgb[0]),int(rgb[1]),int(rgb[2])) + else: + self.value = entry.get_bool() + else: + # Use the default value + self.value = self.default_value + + def zeroize(self): + """ + Set the control to it's OFF value e.g no power to any LED (R, G or B) + for an RGB control + """ + self.value = zeroize(self.default_value) + +class AbstractControlAcquisition(object): + + def __init__(self, driver): + self.driver = driver + self.val = None + self.on_released = None + self.reset_timer = None + self.fade_timer = None + self.on = False + self._released = False + self._waiting = False + self._condition = Event() + + def wait(self): + if self._waiting: + raise Exception("Already waiting") + self._waiting = True + self._condition.wait() + self._waiting = False + + def fade(self, percentage = 100.0, duration = 1.0, release = False, step = 1): + target_val = self.get_target_value(self.val, percentage) + if self.val != target_val: + self.fade_cancelled = False + self._reduce( ( duration / float( self.val - target_val ) ) * step, target_val, release, step) + elif release: + self.driver.release_control(self) + + def get_target_value(self, val, percentage): + return val - int ( ( float(val) / 100.0 ) * percentage ) + + + def blink(self, off_val = 0, delay = 0.5, duration = None, blink_started = None): + if blink_started == None: + blink_started = time.time() + self.cancel_fade() + self.cancel_reset() + if self.on: + self.adjust(self.val) + else: + self.adjust(off_val if isinstance(off_val, int) or isinstance(off_val, tuple) else off_val()) + self.on = not self.on + if duration == None or time.time() < blink_started + duration: + self.reset_timer = g15scheduler.queue(FX_QUEUE, "Blink", delay, self.blink, off_val, delay, duration, blink_started) + return self.reset_timer + + def is_active(self): + raise Exception("Not implemented") + + def adjust(self, val): + raise Exception("Not implemented") + + def set_value(self, val, reset_after = None): + old_val = val + if self.val is None or val != self.val or reset_after is not None: + if self.val is None: + logger.debug("Initial value of control is %s", str(val)) + self.reset_val = val + + self.val = val + self.on = True + self.cancel_fade() + self.adjust(val) + self.cancel_reset() + + logger.debug("Set value of control to %s", str(val)) + + if reset_after: + self.reset_val = old_val + self.reset_timer = g15scheduler.queue(FX_QUEUE, "LEDReset", reset_after, self.reset) + return self.reset_timer + + def reset(self): + self.set_value(self.reset_val) + + def cancel_reset(self): + if self.reset_timer: + self.reset_timer.cancel() + self.reset_timer = None + + def get_value(self): + return self.val + + def cancel_fade(self): + if self.fade_timer is not None: + self.fade_cancelled = True + + def _cleanup(self): + self.cancel_reset() + self.cancel_fade() + + """ + Private + """ + def _reduce(self, interval, target_val, release, step): + if not self.fade_cancelled: + if self.val > target_val: + self.set_value(self.val - step) + if self.val == target_val: + if release: + self.driver.release_control(self) + else: + self.fade_timer = g15scheduler.queue(FX_QUEUE, "Fade", interval, self._reduce, interval, target_val, release, step) + + def _notify_released(self): + if self._released: + raise Exception("Already released") + if self.on_released: + self.on_released() + self._released = True + self._condition.set() + +class ControlAcquisition(AbstractControlAcquisition): + + def __init__(self, driver, control): + self.control = control + AbstractControlAcquisition.__init__(self, driver) + + def is_active(self): + if self.control.id in self.driver.acquired_controls: + ctrls = self.driver.acquired_controls[self.control.id] + return len(ctrls) > 0 and self in ctrls and ctrls.index(self) == len(ctrls) - 1 + + def blink(self, off_val = None, delay = 0.5, duration = None, blink_started = None): + AbstractControlAcquisition.blink(self, ( (0,0,0) if isinstance(self.control.value, tuple) else 0 ) if off_val is None else off_val , delay, duration, blink_started) + + def release(self): + self.driver.release_control(self) + + def adjust(self, val): + if self.is_active(): + self.control.value = val + self.driver.update_control(self.control) + + def fade(self, percentage = 100.0, duration = 1.0, release = False, step = 1): + if isinstance(self.val, int): + AbstractControlAcquisition.fade(self, percentage, duration, release) + else: + self.fade_cancelled = False + target_val = self.get_target_value(self.val, percentage) + h, s, v = self.rgb_to_hsv(self.val) + t_h, t_s, t_v = self.rgb_to_hsv(target_val) + diff = float( v - t_v ) + if diff > 0: + self._reduce( ( duration / diff ) * step, target_val, release, step) + elif release: + self.driver.release_control(self) + + def get_target_value(self, val, percentage): + if isinstance(self.val, int): + return AbstractControlAcquisition.get_target_value(self, val, percentage) + else: + h, s, v = self.rgb_to_hsv(val) + return self.hsv_to_rgb(( h, s, AbstractControlAcquisition.get_target_value(self, v, percentage) )) + + def rgb_to_hsv(self, val): + r, g, b = val + h, s, v = colorsys.rgb_to_hsv(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0) + return ( int(h * 255.0), int(s * 255.0), int(v * 255.0 )) + + def hsv_to_rgb(self, val): + h, s, v = val + r, g, b = colorsys.hsv_to_rgb(float(h) / 255.0, float(s) / 255.0, float(v) / 255.0) + return ( int(r * 255.0), int(g * 255.0), int(b * 255.0 )) + + """ + Private + """ + def _reduce(self, interval, target_val, release, step): + if not self.fade_cancelled: + if isinstance(self.val, int): + AbstractControlAcquisition._reduce(self, interval, target_val, release, step) + else: + if self.val > target_val: + h, s, v = self.rgb_to_hsv(self.val) + v = max(0, v - step) + new_rgb = self.hsv_to_rgb((h, s, v)) + if new_rgb == self.val: + new_rgb = target_val + self.set_value(new_rgb) + if self.val <= target_val: + if release and not self._released: + self.driver.release_control(self) + else: + g15scheduler.queue(FX_QUEUE, "Fade", interval, self._reduce, interval, target_val, release, step) + +class AbstractDriver(object): + + def __init__(self, id): + self.id = id + global seq_no + self.on_driver_options_change = None + seq_no += 1 + self.seq = seq_no + self.disconnecting = False + self.connecting = False + self.all_off_on_disconnect = True + self.allow_multiple = True + self._reset_state() + + def has_memory_bank(self): + for l in self.get_key_layout(): + if G_KEY_M1 in l or G_KEY_M2 in l or G_KEY_M3 in l: + return True + return False + + def release_all_acquisitions(self): + self.acquired_controls = {} + values = dict(self.initial_acquired_control_values) + for k in values: + c = self.get_control(k) + if c: + if k in self.acquired_controls: + self.acquired_controls[k]._cleanup() + c.value = values[k] + self.update_control(c) + + def zeroize_all_controls(self): + for c in self.get_controls(): + c.zeroize() + + def acquire_control(self, control, release_after = None, val = None, on_release = None): + control_acquisitions = self.acquired_controls[control.id] if control.id in self.acquired_controls else [] + self.acquired_controls[control.id] = control_acquisitions + if len(control_acquisitions) == 0: + self.initial_acquired_control_values[control.id] = control.value + + control_acquisition = ControlAcquisition(self, control) + control_acquisition.on_release = on_release + control_acquisitions.append(control_acquisition) + + # Only set the value when active (i.e. in the control_acquisitions list) + control_acquisition.set_value(val if val is not None else control.value) + + if release_after: + g15scheduler.queue(FX_QUEUE, "ReleaseControl", release_after, self._release_control, control_acquisition) + return control_acquisition + + def acquire_control_with_hint(self, hint, release_after = None, val = None): + control = self.get_control_for_hint(hint) + if control: + return self.acquire_control(control, release_after, val) + + def release_control(self, control_acquisition): + logger.info("Releasing %s", control_acquisition.control.id) + if control_acquisition.control.id in self.acquired_controls: + control_acquisitions = self.acquired_controls[control_acquisition.control.id] + control_acquisition._notify_released() + control_acquisition.cancel_reset() + control_acquisition.cancel_fade() + control_acquisitions.remove(control_acquisition) + ctrls = len(control_acquisitions) + if ctrls > 0: + control_acquisition.control.value = control_acquisitions[ctrls - 1].val + self.update_control(control_acquisition.control) + else: + control_acquisition.control.value = self.initial_acquired_control_values[control_acquisition.control.id] + self.update_control(control_acquisition.control) + + def release_mkey_lights(self, control_acquisition): + logger.warning("DEPRECATED call to release_mkey_lights, use release_control") + self.release_control(control_acquisition) + + def disconnect(self): + """ + Disconnect the driver. Callers should use this, and s + subclasses should override on_disconnect. + """ + try: + self.disconnecting = True + logger.info("Disconnecting driver") + if self.all_off_on_disconnect: + for c in self.initial_acquired_control_values: + self.initial_acquired_control_values[c] = zeroize(self.initial_acquired_control_values[c]) + self.release_all_acquisitions() + if self.all_off_on_disconnect: + for c in self.get_controls(): + if isinstance(c.value, int): + c.value = 0 + elif isinstance(c.value, tuple): + c.value = (0, 0, 0) + self.update_control(c) + self._on_disconnect() + finally: + self.disconnecting = False + self._reset_state() + + def _on_disconnect(self): + """ + For subclasses to implemented disconnection logic + """ + raise NotImplementedError( "Not implemented" ) + + def connect(self): + """ + Start the driver + """ + if self.is_connected(): + raise Exception("Already connected") + logger.info("Connecting driver %s", self.get_name()) + self.connecting = True + try: + self._on_connect() + finally: + self.connecting = False + + def _on_connect(self): + raise NotImplementedError( "Not implemented" ) + + def is_connected(self): + """ + Get if driver is connected + """ + raise NotImplementedError( "Not implemented" ) + + def reconnect(self): + """ + Disconnected (if connected), then reconnect + """ + if self.is_connected(): + self.disconnect() + self.connect() + + + def get_name(self): + """ + Get the name of the driver + """ + raise NotImplementedError( "Not implemented" ) + + + def get_model_names(self): + """ + Get a list of the model names this driver supports + """ + raise NotImplementedError( "Not implemented" ) + + + def get_model_name(self): + """ + Get the model name that this driver is connected to + """ + raise NotImplementedError( "Not implemented" ) + + + def get_size(self): + """ + Get the size of the screen. Returns a tuple of (width, height) + """ + raise NotImplementedError( "Not implemented" ) + + def get_key_layout(self): + """ + Get the grid the extra keys available on this keyboard. This is currently only a hint for the Gtk driver + """ + raise NotImplementedError( "Not implemented" ) + + + def get_bpp(self): + """ + Get the bits per pixel. 1 would be monochrome + """ + raise NotImplementedError( "Not implemented") + + + def get_controls(self): + """ + Get the all of the controls available. This would include things such as LCD contrast, LCD brightness, + keyboard colour, keyboard backlight etc + """ + raise NotImplementedError( "Not implemented") + + + def paint(self, image): + """ + Repaint the screen. + """ + raise NotImplementedError( "Not implemented" ) + + + def update_control(self, control): + """ + Synchronize a control with the keyboard. For example, if the control was for the + keyboard colour, the keyboard colour would actually change when this function + is invoked + + Subclasses should not override this function, instead they should implement + on_update_control() + + Keyword arguments: + control -- control to update + """ + if self.check_control(control): + for l in self.control_update_listeners: + l.control_updated(control) + self.on_update_control(control) + + def check_control(self, control): + if isinstance(control.value, int): + if control.value > control.upper: + control.value = control.upper + elif control.value < control.lower: + control.value = control.lower + return True + + def on_update_control(self, control): + raise NotImplementedError( "Not implemented" ) + + + + def grab_keyboard(self, callback): + """ + Start receiving events when the additional keys (G keys, L keys and M keys) + are pressed and released. The provided callback will be invoked with two + arguments, the first being the key code (see the constants G_KEY_xx) + and the second being the key state (KEY_STATE_DOWN or KEY_STATE_UP). + + Keyword arguments: + callback -- invoked when keys are pressed + """ + raise NotImplementedError( "Not implemented" ) + + + def process_svg(self, document): + """ + Give the driver a chance to alter a theme's SVG. This has been introduced to work + around a problem of Inkscape (the recommended 'IDE' for designing themes), + does not saving bitmap font names + """ + raise NotImplementedError( "Not implemented" ) + + def get_mkey_lights(self): + return self.lights + + def get_control(self, control_id): + controls = self.get_controls() + if controls: + for control in self.get_controls(): + if control_id == control.id: + return control + + def get_control_for_hint(self, hint): + controls = self.get_controls() + if controls: + for control in self.get_controls(): + if ( hint & control.hint ) == hint: + return control + + def update_controls(self): + for control in self.get_controls(): + if control.hint & HINT_VIRTUAL == 0: + self.update_control(control) + + def get_color_as_ratios(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return ( float(fg_rgb[0]) / 255.0,float(fg_rgb[1]) / 255.0,float(fg_rgb[2]) / 255.0 ) + + def get_color_as_hexrgb(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return rgb_to_hex(fg_rgb) + + def get_color(self, hint, default): + fg_control = self.get_control_for_hint(hint) + fg_rgb = default + if fg_control != None: + fg_rgb = fg_control.value + return fg_rgb + + """ + Private + """ + + def _release_control(self, control_acquisition): + if control_acquisition.is_active(): + self.release_control(control_acquisition) + + def _reset_state(self): + self.lights = 0 + self.control_update_listeners = [] + self.acquired_controls = {} + self.initial_acquired_control_values = {} + +def rgb_to_hex(rgb): + return '#%02x%02x%02x' % rgb diff --git a/src/gnome15/g15drivermanager.py b/src/gnome15/g15drivermanager.py new file mode 100644 index 0000000..7a08bd0 --- /dev/null +++ b/src/gnome15/g15drivermanager.py @@ -0,0 +1,118 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +This module is responsible for loading the available hardware drivers, and +choosing the best one to use. + +The best driver is always used when no driver is yet configured, other the +configured driver is always used if possible. If it is not available, +or no longer supports the model for the associated device, then it will revert +back to using the best driver. + +The "best driver" is simply the first driver that supports the associated +device. +''' + +import os +import logging +logger = logging.getLogger(__name__) + +# Find all drivers +drivers_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "drivers")) +logger.info("Loading drivers from %s", drivers_dir) +imported_drivers = {} + +driverfiles = [fname[:-3] for fname in os.listdir(drivers_dir) if fname.endswith(".py") and fname.startswith("driver_")] +for d in driverfiles: + try : + driver_mod = __import__("gnome15.drivers.%s" % d , fromlist=[]) + mod = getattr(getattr(driver_mod, "drivers"), d) + imported_drivers[d] = mod + except Exception as e: + logger.warning("Failed to load driver.", exc_info = e) + + +def get_driver_mod(driver_id): + ''' + Get a driver module given it's ID. + + Keyword arguments: + driver_id -- driver ID + ''' + for driver_mod in imported_drivers.values(): + if driver_mod.id == driver_id: + return driver_mod + +def get_driver(conf_client, device, on_close = None): + ''' + Called by clients to create the configured driver. If the configured + driver is not available, it will fallback to using the "best driver". + + Keyword arguments: + conf_client -- configuration client + device -- device to find driver for + on_close -- callback passed to driver that is executed when the driver closes. + ''' + driver_name = conf_client.get_string("/apps/gnome15/%s/driver" % device.uid) + if not driver_name: + # If no driver has yet been configured, always use the best driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("No drivers support the model %s") % device.model_id) + + logger.info("Using first available driver for %s, %s", device.model_id, driver.get_name()) + return driver + + driver_mod_key = "driver_" + driver_name + if not driver_mod_key in imported_drivers: + # If the previous driver is no longer installed, get the best remaining driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("Driver %s is not available. Do you have to appropriate package installed?") % driver_name) + else: + logger.info("Configured driver %s is not available, using %s instead", + driver_mod_key, + driver.get_name()) + else: + driver = imported_drivers[driver_mod_key].Driver(device, on_close = on_close) + + if not device.model_id in driver.get_model_names(): + # If the configured driver is now incorrect for the device model, just use the best driver + # If no driver has yet been configured, always use the best driver + driver = _get_best_driver(device, on_close) + if driver == None: + raise Exception(_("No drivers support the model %s") % device.model_id) + logger.warning("Ignoring configured driver %s, as the model is not supported by it." \ + "Looking for best driver", driver) + return driver + else: + # Configured driver is OK to use + return driver + +def _get_best_driver(device, on_close = None): + ''' + Get the "best driver" available. This will be the first driver that + supports the provided device. + + Keyword arguments: + device -- device to find driver for + on_close -- callback passed to driver that is executed when the driver closes. + ''' + for driver_mod_key in imported_drivers: + driver = imported_drivers[driver_mod_key].Driver(device, on_close = on_close) + if device.model_id in driver.get_model_names(): + return driver \ No newline at end of file diff --git a/src/gnome15/g15exceptions.py b/src/gnome15/g15exceptions.py new file mode 100644 index 0000000..e9b19e1 --- /dev/null +++ b/src/gnome15/g15exceptions.py @@ -0,0 +1,27 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +class NotConnectedException(Exception): + def __init__(self, message = _("Failed to connect.")): + Exception.__init__(self, message) + +class RetryException(Exception): + def __init__(self, message = _("Retry.")): + Exception.__init__(self, message) + \ No newline at end of file diff --git a/src/gnome15/g15globals.py.in b/src/gnome15/g15globals.py.in new file mode 100644 index 0000000..b0d077b --- /dev/null +++ b/src/gnome15/g15globals.py.in @@ -0,0 +1,53 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import xdg.BaseDirectory + +name = "@PACKAGE_NAME@" +version = "@PACKAGE_VERSION@" + +package_dir = os.path.abspath(os.path.dirname(__file__)) +image_dir = os.path.join(package_dir, "..", "..", "data", "images") +dev = False +if os.path.exists(image_dir): + dev = True + ui_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "ui")) + font_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "fonts")) + icons_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "icons")) + ukeys_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "ukeys")) + plugin_dir = os.path.realpath(os.path.join(package_dir, "..", "plugins")) + scripts_dir = os.path.realpath(os.path.join(package_dir, "..", "scripts")) + themes_dir = os.path.realpath(os.path.join(package_dir, "..", "..", "data", "themes")) + i18n_dir = os.path.realpath(os.path.join(package_dir, "..", "i18n")) +else: + image_dir = "@prefix@/share/gnome15/images" + ui_dir = "@prefix@/share/gnome15/ui" + font_dir = "@prefix@/share/gnome15" + plugin_dir = "@prefix@/share/gnome15/plugins" + themes_dir = "@prefix@/share/gnome15/themes" + ukeys_dir = "@prefix@/share/gnome15/ukeys" + i18n_dir = "@prefix@/share/gnome15/i18n" + icons_dir = "@prefix@/share/icons" + scripts_dir = "@prefix@/bin" + +user_config_dir = os.path.join(xdg.BaseDirectory.xdg_config_home, "gnome15") +user_data_dir = os.path.join(xdg.BaseDirectory.xdg_data_home, "gnome15") +user_cache_dir = os.path.join(xdg.BaseDirectory.xdg_cache_home, "gnome15") + +# Differs from distro to distro, so it can changed as a ./configure option +# by setting the FIXED_SIZE_FONT environment variable. +fixed_size_font_name = "@FIXED_SIZE_FONT@" \ No newline at end of file diff --git a/src/gnome15/g15gtk.py b/src/gnome15/g15gtk.py new file mode 100644 index 0000000..beb8b33 --- /dev/null +++ b/src/gnome15/g15gtk.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +A top level GTK windows that draws on the LCD +''' +import gtk +import gobject +import g15driver +import util.g15cairo as g15cairo +import util.g15pythonlang as g15pythonlang +from threading import Lock +from threading import Semaphore +import g15theme +import g15screen +import cairo +import ctypes + + +_initialized = False +def create_cairo_font_face_for_file (filename, faceindex=0, loadoptions=0): + global _initialized + global _freetype_so + global _cairo_so + global _ft_lib + global _surface + + CAIRO_STATUS_SUCCESS = 0 + FT_Err_Ok = 0 + + if not _initialized: + + # find shared objects + _freetype_so = ctypes.CDLL ("libfreetype.so.6") + _cairo_so = ctypes.CDLL ("libcairo.so.2") + + _cairo_so.cairo_ft_font_face_create_for_ft_face.restype = ctypes.c_void_p + _cairo_so.cairo_ft_font_face_create_for_ft_face.argtypes = [ ctypes.c_void_p, ctypes.c_int ] + _cairo_so.cairo_set_font_face.argtypes = [ ctypes.c_void_p, ctypes.c_void_p ] + _cairo_so.cairo_font_face_status.argtypes = [ ctypes.c_void_p ] + _cairo_so.cairo_status.argtypes = [ ctypes.c_void_p ] + + # initialize freetype + _ft_lib = ctypes.c_void_p () + if FT_Err_Ok != _freetype_so.FT_Init_FreeType (ctypes.byref (_ft_lib)): + raise "Error initialising FreeType library." + + class PycairoContext(ctypes.Structure): + _fields_ = [("PyObject_HEAD", ctypes.c_byte * object.__basicsize__), + ("ctx", ctypes.c_void_p), + ("base", ctypes.c_void_p)] + + _surface = cairo.ImageSurface (cairo.FORMAT_A8, 0, 0) + + _initialized = True + + # create freetype face + ft_face = ctypes.c_void_p() + cairo_ctx = cairo.Context (_surface) + cairo_t = PycairoContext.from_address(id(cairo_ctx)).ctx + + if FT_Err_Ok != _freetype_so.FT_New_Face (_ft_lib, filename, faceindex, ctypes.byref(ft_face)): + raise Exception("Error creating FreeType font face for " + filename) + + # create cairo font face for freetype face + cr_face = _cairo_so.cairo_ft_font_face_create_for_ft_face (ft_face, loadoptions) + if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_font_face_status (cr_face): + raise Exception("Error creating cairo font face for " + filename) + + _cairo_so.cairo_set_font_face (cairo_t, cr_face) + if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_status (cairo_t): + raise Exception("Error creating cairo font face for " + filename) + + face = cairo_ctx.get_font_face () + + return face + + +class G15OffscreenWindow(g15theme.Component): + + def __init__(self, component_id): + g15theme.Component.__init__(self, component_id) + self.window = None + self.content = None + + def on_configure(self): + g15theme.Component.on_configure(self) + gobject.idle_add(self._create_window) + self.get_screen().key_handler.action_listeners.append(self) + + def notify_remove(self): + g15theme.Component.notify_remove(self) + self.get_screen().key_handler.action_listeners.remove(self) + + def set_content(self, content): + self.content = content + if self.window is not None: + gobject.idle_add(self._do_set_content) + + def action_performed(self, binding): + if self.is_visible(): + if binding.action == g15driver.NEXT_SELECTION: + gobject.idle_add(self.window.focus_next) + elif binding.action == g15driver.PREVIOUS_SELECTION: + gobject.idle_add(self.window.focus_previous) + if binding.action == g15driver.NEXT_PAGE: + gobject.idle_add(self.window.change_widget) + elif binding.action == g15driver.PREVIOUS_PAGE: + gobject.idle_add(self.window.change_widget, None, True) + elif binding.action == g15driver.SELECT: + pass + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.window is not None: + self.window.paint(canvas) + + + """ + Private + """ + + def _do_set_content(self): + self.window.set_content(self.content) + + def _create_window(self): + screen = self.get_screen() + window = G15Window(screen, self.get_root(), self.view_bounds[0], self.view_bounds[1], \ + self.view_bounds[2], self.view_bounds[3]) + if self.content is not None: + self.window.set_content(self.content) + self.window = window + screen.redraw(self.get_root()) + +class G15Window(gtk.OffscreenWindow): + + def __init__(self, screen, page, area_x, area_y, area_width, area_height): + gtk.OffscreenWindow.__init__(self) + self.pixbuf = None + self.scroller = None + self.screen = screen + self.page = page + self.lock = None + self.area_x = int(area_x) + self.area_y = int(area_y) + self.area_width = int(area_width) + self.area_height = int(area_height) + self.surface = None + self.content = gtk.EventBox() + self.set_app_paintable(True) + self.content.set_app_paintable(True) + self.connect("screen-changed", self.screen_changed) + self.content.connect("expose-event", self._transparent_expose) + self.content.set_size_request(self.area_width, self.area_height) + self.add(self.content) + self.connect("damage_event", self._damage) + self.connect("expose_event", self._expose) + self.screen_changed(None, None) + self.lock = Semaphore() + + def set_content(self, content): + self.content.add(content) + self.show_all() + + # If the content window is a scroller, we send focus events to it + # moving the scroller position to the focussed component + if isinstance(content, gtk.ScrolledWindow): + self.scroller = content + + def paint(self, canvas): + if g15pythonlang.is_gobject_thread(): + raise Exception("Painting on mainloop") + self.start_for_capture() + gobject.idle_add(self._do_capture) + self.lock.acquire() + canvas.save() + canvas.translate(self.area_x, self.area_y) + canvas.set_source_surface(self.surface) + canvas.paint() + canvas.restore() + + def focus_next(self): + self.content.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD) + self.scroll_to_focussed() + self.screen.redraw(self.page) + + def focus_previous(self): + self.content.get_toplevel().child_focus(gtk.DIR_TAB_BACKWARD) + self.screen.redraw(self.page) + self.scroll_to_focussed() + + def change_widget(self, amount = None, reverse = False): + focussed = self.get_focus() + if focussed != None: + if isinstance(focussed, gtk.HScale): + adj = focussed.get_adjustment() + ps = adj.get_page_size() if amount is None else amount + if ps == 0: + ps = 10 + if reverse: + adj.set_value(adj.get_value() - ps) + else: + adj.set_value(adj.get_value() + ps) + self.screen.redraw(self.page) + + def show_all(self): + gtk.OffscreenWindow.show_all(self) + + def scroll_to_focussed(self): + if self.scroller is not None: + hadj = self.scroller.get_hadjustment() + vadj = self.scroller.get_vadjustment() + x, y = self.get_focus().translate_coordinates(self.scroller.get_children()[0], 0, 0) + max_x = hadj.upper - hadj.page_size + max_y = vadj.upper - vadj.page_size + hadj.set_value(min(x, max_x)) + vadj.set_value(min(y, max_y)) + + def screen_changed(self, widget, old_screen=None): + global supports_alpha + + # To check if the display supports alpha channels, get the colormap + screen = self.get_screen() + colormap = screen.get_rgba_colormap() + if colormap == None: + colormap = screen.get_rgb_colormap() + supports_alpha = False + else: + supports_alpha = True + + # Now we have a colormap appropriate for the screen, use it + self.set_colormap(colormap) + + return False + + def _transparent_expose(self, widget, event = None): + """ + To overcome inability to set a container component as transparent. I + cannot get compositing working (perhaps it just doesn't because we are + going to an offscreen window). So, to get pseudo-transparency, + we repaint the background the screen would normally paint, offset by + the position of this component + """ + cr = widget.window.cairo_create() + self.screen.clear_canvas(cr) + cr.save() + cr.translate(-self.area_x, -self.area_y) + for s in self.screen.painters: + if s.place == g15screen.BACKGROUND_PAINTER: + s.paint(cr) + cr.restore() + return False + + def _expose(self, widget, event): + self.screen.redraw(self.page) + return False + + def _damage(self, widget, event): +# print "Damage" +# self.screen.redraw(self.page) + return False + + def start_for_capture(self): + self.lock = Lock() + self.lock.acquire() + + def _do_capture(self): + self.content.window.invalidate_rect((0,0,self.area_width,self.area_height), True) + self.content.window.process_updates(True) + pixbuf = gtk.gdk.Pixbuf( gtk.gdk.COLORSPACE_RGB, False, 8, self.area_width, self.area_height) + pixbuf.get_from_drawable(self.content.window, self.content.get_colormap(), 0, 0, 0, 0, self.area_width, self.area_height) + self.surface = g15cairo.pixbuf_to_surface(pixbuf) + self.pixbuf = pixbuf + self.lock.release() \ No newline at end of file diff --git a/src/gnome15/g15keyboard.py b/src/gnome15/g15keyboard.py new file mode 100644 index 0000000..78974a6 --- /dev/null +++ b/src/gnome15/g15keyboard.py @@ -0,0 +1,661 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module deals with handling raw key presses from the driver, and turning them +into Macros or actions. The different types of macro are handled accordingly, as well +as the repetition functions. + +All key events are handled on a queue (one per instance of a key handler). + +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + +import g15profile +import util.g15scheduler as g15scheduler +import g15driver +import g15actions +import g15uinput +import g15screen + +import logging +logger = logging.getLogger(__name__) + +class KeyState(): + """ + Holds the current state of a single macro key + """ + def __init__(self, key): + self.key = key + self.state_id = None + self.timer = None + self.consumed = False + self.defeat_release = False + self.consume_until_release = False + + def is_consumed(self): + return self.consumed or self.consume_until_release + + def cancel_timer(self): + """ + Cancel the HELD timer if one exists. + """ + if self.timer is not None: + self.timer.cancel() + self.timer = None + + def __repr__(self): + return "%s = %s [consumed = %s]" % (self.key, g15profile.to_key_state_name(self.state_id), str(self.consumed) ) + +class G15KeyHandler(): + """ + Main class for handling key events. There should be one instance of this + per active G15Screen. + """ + + def __init__(self, screen): + # Public + self.queue_name = "MacroQueue-%s" % screen.device.uid + + """ + List of callbacks invoked when an action is activated by it's key combination + """ + self.action_listeners = [] + + """ + List of callbacks invoked for raw key handling. Normally plugins shouldn't + use this, use actions instead + """ + self.key_handlers = [] + + # Private + self.__screen = screen + self.__conf_client = self.__screen.conf_client + self.__repeat_macros = [] + self.__macro_repeat_timer = None + self.__uinput_macros = [] + self.__normal_macros = [] + self.__normal_held_macros = [] + self.__notify_handles = [] + self.__key_states = {} + + def get_key_states(self): + # Get the current state of the keys + return self.__key_states + + def start(self): + """ + Start handling keys + """ + screen_key = "/apps/gnome15/%s" % self.__screen.device.uid + logger.info("Starting %s's key handler.", self.__screen.device.uid) + g15profile.profile_listeners.append(self._profile_changed) + self.__screen.screen_change_listeners.append(self) + self.__notify_handles.append(self.__conf_client.notify_add("%s/active_profile" % screen_key, self._active_profile_changed)) + logger.info("Starting of %s's key handler is complete.", self.__screen.device.uid) + self._reload_active_macros() + + def stop(self): + """ + Stop handling keys + """ + logger.info("Stopping key handler for %s", self.__screen.device.uid) + g15scheduler.stop_queue(self.queue_name) + if self in self.__screen.screen_change_listeners: + self.__screen.screen_change_listeners.remove(self) + if self._profile_changed in g15profile.profile_listeners: + g15profile.profile_listeners.remove(self._profile_changed) + for h in self.__notify_handles: + self.__conf_client.notify_remove(h) + self.__notify_handles = [] + logger.info("Stopped key handler for %s", self.__screen.device.uid) + + def key_received(self, keys, state_id): + """ + This function starts processing of the provided keys, turning them + into macros, actions and handling repetition. The key event will be + placed on the queue, leaving this function to return immediately + + Keyword arguments: + keys -- list of keys to process + state_id -- key state ID (g15driver.KEY_STATE_UP, _DOWN and _HELD) + """ + g15scheduler.execute(self.queue_name, "KeyReceived", self._do_key_received, keys, state_id) + + def memory_bank_changed(self, bank): + self._reload_active_macros() + + """ + Callbacks + """ + + def _active_profile_changed(self, client, connection_id, entry, args): + self._reload_active_macros() + return 1 + + def _profile_changed(self, profile_id, device_uid): + self._reload_active_macros() + + """ + Private + """ + + def _reload_active_macros(self): + self.__normal_held_macros = [] + self.__normal_macros = [] + self.__uinput_macros = [] + self._build_macros() + + def _do_key_received(self, keys, state_id): + """ + Actual handling of key events. + + Keyword arguments: + keys -- list of keys + state_id -- key state (g15driver.KEY_STATE_UP, _DOWN and _HELD) + """ + + """ + See if the screen itself, or the plugins, want to handle the key. This + is the legacy method of key handling, the preferred method now is + actions which is handled below. However, this is still useful for plugins + that want to take over key handling, such as screensaver which + disables all keys while it is active. + """ + try: + if self._handle_key(keys, state_id, post=False): + return + + """ + Deal with each key separately, this keeps it simpler + """ + for key in keys: + + """ + Now set up the macro key state. This is where we decide what macros + and actions to activate. + """ + if self._configure_key_state(key, state_id): + + """ + Do uinput macros first. These are treated slightly differently, because + a press of the Macro key equals a "press" of the virtual key, + a release of the Macro key equals a "release" of the virtual key etc. + """ + self._handle_uinput_macros() + + """ + Now the ordinary macros, processed on key_up + """ + self._handle_normal_macros() + + """ + Now the actions + """ + self._handle_actions() + + """ + Now do the legacy 'post' handling. + """ + if not self._handle_key(keys, state_id, post=True): + pass + + """ + When ALL keys are UP, clear out the state + """ + up = 0 + for k, v in self.__key_states.items(): + if v.state_id == g15driver.KEY_STATE_UP: + up += 1 + if up > 0 and up == len(self.__key_states): + self.__key_states = {} + finally: + """ + Always redraw the current page on key presses + """ + self.__screen.redraw() + + def _handle_actions(self): + """ + This handles the default action bindings. The actions may have + already re-mapped as a macro, in which case they will be ignored + here. + """ + action_keys = self.__screen.driver.get_action_keys() + if action_keys: + for action in action_keys: + binding = action_keys[action] + f = 0 + for k in binding.keys: + if k in self.__key_states and \ + binding.state == self.__key_states[k].state_id and \ + not self.__key_states[k].is_consumed(): + f += 1 + if f == len(binding.keys): + self._action_performed(binding) + for k in binding.keys: + self.__key_states[k].consume_until_release = True + + def _handle_normal_macros(self): + """ + First check for any KEY_STATE_HELD macros. We do these first so KEY_STATE_UP + macros don't consume the key states + """ + for m in self.__normal_held_macros: + held = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(held) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_HELD, held) + + + """ + Search for all the non-uinput macros that would be activated by the + current key state. In this case, KEY_STATE_UP macros are looked for + """ + for m in self.__normal_macros: + up = [] + held = [] + down = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_DOWN: + down.append(key_state) + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_UP and not key_state.defeat_release: + up.append(key_state) + if not key_state.is_consumed() and key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(up) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_UP, up) + if len(down) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_DOWN, down) + if len(held) == len(m.keys): + self._handle_macro(m, g15driver.KEY_STATE_HELD, held) + + + def _handle_uinput_macros(self): + """ + Search for all the uinput macros that would be activated by the + current key state, and emit events of the same type. + """ + uinput_repeat = False + for m in self.__uinput_macros: + down = [] + up = [] + held = [] + for k in m.keys: + if k in self.__key_states: + key_state = self.__key_states[k] + if not key_state.is_consumed(): + if key_state.state_id == g15driver.KEY_STATE_UP and not key_state.defeat_release: + up.append(key_state) + if key_state.state_id == g15driver.KEY_STATE_DOWN: + down.append(key_state) + if key_state.state_id == g15driver.KEY_STATE_HELD: + held.append(key_state) + + if len(down) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_DOWN, down) + if len(up) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_UP, up) + if len(held) == len(m.keys): + self._handle_uinput_macro(m, g15driver.KEY_STATE_HELD, held) + uinput_repeat = True + + """ + Simulate a uinput repeat by just handling an empty key list. + No keys have changed state, so we should just keep hitting this + reschedule until they do + """ + if uinput_repeat: + g15scheduler.queue(self.queue_name, "RepeatUinput", \ + 0.1, \ + self._handle_uinput_macros) + + def _configure_key_state(self, key, state_id): + + """ + Maintains the "key state" table, which holds what state each key + is currently in. + + This function will return the number of state changes, so this key + event may be ignored if it is no longer appropriate (i.e. a hold + timer event for keys that are now released) + + Keyword arguments: + key -- single key + state_id -- state_id (g15driver.KEY_STATE_UP, _DOWN or _HELD) + """ + if state_id == g15driver.KEY_STATE_HELD and not key in self.__key_states: + """ + All keys were released before the HOLD timer kicked in, so we + totally ignore this key + """ + pass + else: + if not key in self.__key_states: + self.__key_states[key] = KeyState(key) + key_state = self.__key_states[key] + + # This is a new key press, so reset this key's consumed state + key_state.consumed = False + + # Check the sanity of the key press + self._check_key_state(state_id, key_state) + key_state.state_id = state_id + + if state_id == g15driver.KEY_STATE_DOWN: + """ + Key is now down, let's set up a timer to produce a held event + """ + key_state.timer = g15scheduler.queue(self.queue_name, + "HoldKey%s" % str(key), \ + self.__screen.service.key_hold_duration, \ + self._do_key_received, [ key ], \ + g15driver.KEY_STATE_HELD) + elif state_id == g15driver.KEY_STATE_UP: + """ + Now the key is up, cancel the HELD timer if one exists. + """ + key_state.cancel_timer() + + return True + + def _get_all_macros(self, profile = None, macro_list = None, macro_keys = None, mapped_to_key = False, state = None): + """ + Get all macros, including those in parent profiles. By default, the + "root" is the active profile + + Keyword argumentsL + profile -- root profile or None for active profile + macro_list -- list to append macros to. + mapped_to_key -- boolean indicator whether to only find UINPUT type macros + """ + if profile is None: + profile = g15profile.get_active_profile(self.__screen.device) + if macro_list is None: + macro_list = [] + if macro_keys is None: + macro_keys = [] + + if state == None: + state = g15driver.KEY_STATE_UP + + bank = self.__screen.get_memory_bank() + for m in profile.macros[state][bank - 1]: + if not m.key_list_key in macro_keys: + if ( not mapped_to_key and not m.is_uinput() ) or \ + ( mapped_to_key and m.is_uinput() ): + macro_list.append(m) + macro_keys.append(m.key_list_key) + if profile.base_profile is not None: + profile = g15profile.get_profile(self.__screen.device, profile.base_profile) + if profile is not None: + self._get_all_macros(profile, macro_list, macro_keys, mapped_to_key, state) + return macro_list + + def _build_macros(self, profile = None, macro_keys = None, held_macro_keys = None, down_macro_keys = None): + if profile is None: + profile = g15profile.get_active_profile(self.__screen.device) + if macro_keys is None: + macro_keys = [] + if held_macro_keys is None: + held_macro_keys = [] + if down_macro_keys is None: + down_macro_keys = [] + + bank = self.__screen.get_memory_bank() + for m in profile.macros[g15driver.KEY_STATE_UP][bank - 1]: + if not m.key_list_key in macro_keys: + if m.is_uinput(): + self.__uinput_macros.append(m) + else: + self.__normal_macros.append(m) + macro_keys.append(m.key_list_key) + + for m in profile.macros[g15driver.KEY_STATE_DOWN][bank - 1]: + if not m.key_list_key in down_macro_keys: + if m.is_uinput(): + self.__uinput_macros.append(m) + else: + self.__normal_macros.append(m) + down_macro_keys.append(m.key_list_key) + + for m in profile.macros[g15driver.KEY_STATE_HELD][bank - 1]: + if not m.key_list_key in held_macro_keys: + if not m.is_uinput(): + self.__normal_held_macros.append(m) + held_macro_keys.append(m.key_list_key) + + if profile.base_profile is not None: + profile = g15profile.get_profile(self.__screen.device, profile.base_profile) + if profile is not None: + self._build_macros(profile, macro_keys, held_macro_keys, down_macro_keys) + + def _check_key_state(self, new_state_id, key_state): + """ + Sanity check + + Keyword arguments: + new_state_id -- new state ID + key_state -- key state object + """ + if new_state_id == g15driver.KEY_STATE_UP and \ + key_state.state_id not in [ g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + logger.warning("Received key up state before receiving key down, indicates defeated key press.") + return False +# if new_state_id == g15driver.KEY_STATE_DOWN and \ +# key_state.state_id is not None: +# logger.warning("Received unexpected key down (key was in state %s).", +# g15profile.to_key_state_name(key_state.state_id)) +# return False + if new_state_id == g15driver.KEY_STATE_HELD and \ + key_state.state_id in [ g15driver.KEY_STATE_UP, None ]: + logger.warning("Received key held state before receiving key down.") + return False + + return True + + def _send_uinput_keypress(self, macro, uc, uinput_repeat = False): + g15uinput.locks[macro.type].acquire() + try: + if uinput_repeat: + g15uinput.emit(macro.type, uc, 2) + else: + g15uinput.emit(macro.type, uc, 1) + g15uinput.emit(macro.type, uc, 0) + finally: + g15uinput.locks[macro.type].release() + + def _repeat_uinput(self, macro, uc, uinput_repeat = False): + if macro in self.__repeat_macros: + self._send_uinput_keypress(macro, uc, uinput_repeat) + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "MacroRepeat", macro.repeat_delay, self._repeat_uinput, self._reload_macro_instance(macro), uc, uinput_repeat) + + def _reload_macro_instance(self, macro): + p = g15profile.get_profile(macro.profile.device, macro.profile.id) + if p: + return p.get_macro(macro.activate_on, macro.memory, macro.keys) + logger.warning("Could not reload macro %s, using old instance.", macro.name) + return macro + + def _handle_uinput_macro(self, macro, state, key_states): + uc = macro.get_uinput_code() + self._consume_keys(key_states) + if state == g15driver.KEY_STATE_UP: + if macro in self.__repeat_macros and macro.repeat_mode == g15profile.REPEAT_WHILE_HELD: + self.__repeat_macros.remove(macro) + g15uinput.emit(macro.type, uc, 0) + elif state == g15driver.KEY_STATE_DOWN: + if macro in self.__repeat_macros: + if macro.repeat_mode == g15profile.REPEAT_TOGGLE and macro.repeat_delay != -1: + """ + For REPEAT_TOGGLE mode with custom repeat rate, we now cancel + the repeat timer and defeat the key release. + """ + self.__repeat_macros.remove(macro) + else: + """ + For REPEAT_TOGGLE mode with default repeat rate, we will send a release if this + is the second press. We also defeat the 2nd release. + """ + g15uinput.emit(macro.type, uc, 0) + self.__repeat_macros.remove(macro) + self._defeat_release(key_states) + else: + if macro.repeat_mode == g15profile.REPEAT_TOGGLE: + """ + Start repeating + """ + if not macro in self.__repeat_macros: + self._defeat_release(key_states) + self.__repeat_macros.append(macro) + if macro.repeat_delay != -1: + """ + For the default delay, just let the OS handle the repeat + """ + self._repeat_uinput(macro, uc) + else: + """ + For the custom delay, send the key press now. We send + the first when it is actually released, then start + sending further repeats on a timer + """ + g15uinput.emit(macro.type, uc, 1) + self._defeat_release(key_states) + elif macro.repeat_mode == g15profile.NO_REPEAT: + """ + For NO_REPEAT macros we send the release now, and defeat the + actual key release that will come later. + """ + self._send_uinput_keypress(macro, uc) + elif macro.repeat_mode == g15profile.REPEAT_WHILE_HELD and macro.repeat_delay != -1: + self._send_uinput_keypress(macro, uc) + else: + g15uinput.emit(macro.type, uc, 1) + + elif state == g15driver.KEY_STATE_HELD: + if macro.repeat_mode == g15profile.REPEAT_WHILE_HELD: + if macro.repeat_delay != -1: + self.__repeat_macros.append(macro) + self._repeat_uinput(macro, uc, False) + + def _defeat_release(self, key_states): + for k in key_states: + k.defeat_release = True + k.cancel_timer() + + def _consume_keys(self, key_states): + """ + Mark as consumed so they don't get activated again if other key's are + pressed or released while this macro is active + + Keyword arguments: + key_states -- list of KeyState objects to mark as consumed + """ + for k in key_states: + k.consumed = True + + def _process_macro(self, macro, state, key_states): + if macro.type == g15profile.MACRO_ACTION: + binding = g15actions.ActionBinding(macro.macro, macro.keys, state) + if not self._action_performed(binding): + # Send it to the service for handling + self.__screen.service.macro_handler.handle_macro(macro) + else: + for k in key_states: + k.consume_until_released = True + else: + # Send it to the service for handling + self.__screen.service.macro_handler.handle_macro(macro) + + def _handle_macro(self, macro, state, key_states, repetition = False): + self._consume_keys(key_states) + delay = macro.repeat_delay if macro.repeat_delay != -1 else 0.1 + if macro.repeat_mode == g15profile.REPEAT_TOGGLE and state == g15driver.KEY_STATE_UP: + if macro in self.__repeat_macros and not repetition: + # Key pressed again, so stop repeating + self.__cancel_macro_repeat_timer() + self.__repeat_macros.remove(macro) + else: + if not macro in self.__repeat_macros and not repetition: + self.__repeat_macros.append(macro) + else: + self._process_macro(macro, state, key_states) + + # We test again because a toggle might have stopped the repeat + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "RepeatMacro", delay, self._handle_macro, macro, state, key_states, True) + elif macro.repeat_mode == g15profile.REPEAT_WHILE_HELD and not state == g15driver.KEY_STATE_DOWN: + if state == g15driver.KEY_STATE_UP and macro in self.__repeat_macros and not repetition: + # Key released again, so stop repeating + self.__cancel_macro_repeat_timer() + self.__repeat_macros.remove(macro) + else: + if state == g15driver.KEY_STATE_HELD and not macro in self.__repeat_macros and not repetition: + self.__repeat_macros.append(macro) + self._process_macro(macro, state, key_states) + + # We test again because a toggle might have stopped the repeat + if macro in self.__repeat_macros: + self.__macro_repeat_timer = g15scheduler.queue(self.queue_name, "RepeatMacro", delay, self._handle_macro, macro, g15driver.KEY_STATE_HELD, key_states, True) + elif state == g15driver.KEY_STATE_DOWN and macro.activate_on == g15driver.KEY_STATE_DOWN: + self._process_macro(macro, state, key_states) + elif state == g15driver.KEY_STATE_UP and macro.activate_on == g15driver.KEY_STATE_UP: + self._process_macro(macro, state, key_states) + elif state == g15driver.KEY_STATE_HELD and macro.activate_on == g15driver.KEY_STATE_HELD: + self._process_macro(macro, state, key_states) + + # Also defeat the key release so any normal KEY_STATE_UP macros don't get activated as well + self._defeat_release(key_states) + + def __cancel_macro_repeat_timer(self): + """ + Cancel the currently pending macro repeat + """ + if self.__macro_repeat_timer is not None: + self.__macro_repeat_timer.cancel() + self.__macro_repeat_timer = None + + def _handle_key(self, keys, state_id, post=False): + """ + Send the key press to various handlers. This is for plugins and other + code that needs to completely take over the macro keys, for general + key handling "Actions" should be used instead. + """ + + # Event first goes to this objects key handlers + for h in self.key_handlers: + if h.handle_key(keys, state_id, post): + return True + + return False + + def _action_performed(self, binding): + logger.info("Invoking action '%s'", binding.action) + + for l in self.action_listeners: + if l.action_performed(binding): + return True \ No newline at end of file diff --git a/src/gnome15/g15keyio.py b/src/gnome15/g15keyio.py new file mode 100644 index 0000000..bd5de79 --- /dev/null +++ b/src/gnome15/g15keyio.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Classes and functions for recording X key presses using either raw X events +or XTEST as well as injecting such keys +""" + +import gnome15.g15locale as g15locale +import gnome15.g15uinput as g15uinput +_ = g15locale.get_translation("macro-recorder", modfile = __file__).ugettext + +import time +import logging +logger = logging.getLogger(__name__) + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +from threading import Thread + +local_dpy = display.Display() +record_dpy = display.Display() + +def get_keysyms(): + l = [] + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_": + l.append(name[3:]) + return l + +class RecordThread(Thread): + def __init__(self, _record_callback): + Thread.__init__(self) + self.setDaemon(True) + self.name = "RecordThread" + self._record_callback = _record_callback + self.ctx = record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.MotionNotify), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + def disable_record_context(self): + if self.ctx != None: + local_dpy.record_disable_context(self.ctx) + local_dpy.flush() + + def run(self): + record_dpy.record_enable_context(self.ctx, self._record_callback) + record_dpy.record_free_context(self.ctx) + +class G15KeyRecorder(): + + def __init__(self, driver): + self._driver = driver + self._record_key = None + self._record_thread = None + self._last_keys = None + self._key_down = None + + self.script = [] + self.on_add = None + self.on_stop = None + self.single_key = False + self.output_delays = True + self.emit_uinput = False + + def clear(self): + del self.script[:] + + def is_recording(self): + return self._record_thread is not None + + def start_record(self): + if self._record_thread is None: + self._start_recording() + return True + else: + self._cancel_macro(None) + return True + + ''' + Private + ''' + + def _lookup_keysym(self, keysym): + logger.debug("Looking up %s", keysym) + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_" and getattr(XK, name) == keysym: + return name[3:] + return "[%d]" % keysym + + def _record_callback(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, record_dpy.display, None, None) + if event.type in [X.KeyPress, X.KeyRelease]: + pr = event.type == X.KeyPress and "Press" or "Release" + logger.debug("Event detail = %s", event.detail) + keysym = local_dpy.keycode_to_keysym(event.detail, 0) + if not keysym: + logger.debug("Recorded %s", event.detail) + self._record_key_callback(event, event.detail) + else: + logger.debug("Keysym = %s", str(keysym)) + s = self._lookup_keysym(keysym) + logger.debug("Recorded %s", s) + self._record_key_callback(event, s) + + def _record_key_callback(self, event, keyname): + if self._key_down == None: + self._key_down = time.time() + else: + now = time.time() + delay = time.time() - self._key_down + if self.output_delays: + self.script.append(["Delay", str(int(delay * 1000))]) + self._key_down = now + + if self.emit_uinput: + pr = event.type == X.KeyPress and "UPress" or "URelease" + keyname = g15uinput.get_keysym_to_uinput_mapping(keyname) + " " + g15uinput.KEYBOARD + if keyname: + for c in keyname.split(","): + self._add_key(pr, event, c) + else: + pr = event.type == X.KeyPress and "Press" or "Release" + self._add_key(pr, event, keyname) + + + def _add_key(self, pr, event, keyname): + keydown = self._key_state[keyname] if keyname in self._key_state else None + if keydown is None: + if event.type == X.KeyPress: + self._key_state[keyname] = True + self._add(pr, keyname) + else: + # Got a release without getting a press - ignore + pass + else: + if event.type == X.KeyRelease: + self._add(pr, keyname) + del self._key_state[keyname] + + if self.single_key: + self.stop_record() + + def _add(self, pr, keyname): + self.script.append([pr, keyname]) + if self.on_add: + self.on_add(pr, keyname) + + def _done_recording(self, state): + if self._record_keys != None: + self.stop_record() + + def stop_record(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + self._key_down = None + self._record_key = None + self._record_thread = None + if self.on_stop is not None: + self.on_stop(self) + + def _start_recording(self): + self.script = [] + self._key_state = {} + self._key_down = None + self._record_thread = RecordThread(self._record_callback) + self._record_thread.start() + diff --git a/src/gnome15/g15locale.py b/src/gnome15/g15locale.py new file mode 100644 index 0000000..7434333 --- /dev/null +++ b/src/gnome15/g15locale.py @@ -0,0 +1,241 @@ +# coding: utf-8 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Helpers for internationalisation. See http://wiki.maemo.org/How_to_Internationalize_python_apps. +Gnome15 has multiple translation domains that are loaded dynamically and then cached +in memory. + +Translations may be required for Python code, SVG or Glade files. +""" + +import os +import locale +import gettext +import g15globals +import util.g15gconf as g15gconf +import time +import datetime +import re + +import logging +logger = logging.getLogger(__name__) + +# Change this variable to your app name! +# The translation files will be under +# @LOCALE_DIR@/@LANGUAGE@/LC_MESSAGES/@APP_NAME@.mo +APP_NAME = "SleepAnalyser" + +LOCALE_DIR = g15globals.i18n_dir + +# Now we need to choose the language. We will provide a list, and gettext +# will use the first translation available in the list +# +# In maemo it is in the LANG environment variable +# (on desktop is usually LANGUAGES) +DEFAULT_LANGUAGES = [] +if 'LANG' in os.environ: + DEFAULT_LANGUAGES += os.environ.get('LANG', '').split(':') +if 'LANGUAGE' in os.environ: + DEFAULT_LANGUAGES += os.environ.get('LANGUAGE', '').split('.') +DEFAULT_LANGUAGES += ['en_GB'] + +lc, encoding = locale.getdefaultlocale() +if lc: + languages = [lc] +else: + languages = [] + +# Concat all languages (env + default locale), +# and here we have the languages and location of the translations +languages += DEFAULT_LANGUAGES +mo_location = LOCALE_DIR + +# Cached translations +__translations = {} + +# Replace these date/time formats to get a format without seconds +REPLACE_FORMATS = [ + (u'.%S', u''), + (u':%S', u''), + (u',%S', u''), + (u' %S', u''), + (u':%OS', ''), + (u'%r', '%I:%M %p'), + (u'%t', '%H:%M'), + (u'%T', '%H:%M') + ] + +def format_time(time_val, gconf_client, display_seconds = True, show_timezone = False, compact = True): + """ + Format a given time / datetime as a time in the 12hour format. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + time_val -- time / datetime object + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, + "/apps/gnome15/time_format", + locale.nl_langinfo(locale.T_FMT_AMPM)) + # For some locales T_FMT_AMPM is empty. + # Set the format to a default value if this is the case. + if fmt == "": + fmt = "%r" + + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(time_val, time.struct_time): + time_val = datetime.datetime(*time_val[:6]) + + if not show_timezone: + fmt = fmt.replace("%Z", "") + + if compact: + fmt = fmt.replace(" %p", "%p") + fmt = fmt.replace(" %P", "%P") + + fmt = fmt.strip() + + if isinstance(time_val, tuple): + return time.strftime(fmt, time_val) + else: + return time_val.strftime(fmt) + +def format_time_24hour(time_val, gconf_client, display_seconds = True, show_timezone = False): + """ + Format a given time / datetime as a time in the 24hour format. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + time_val -- time / datetime object / tuple + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/time_format_24hr", locale.nl_langinfo(locale.T_FMT)) + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(time_val, time.struct_time): + time_val = datetime.datetime(*time_val[:6]) + + if not show_timezone: + fmt = fmt.replace("%Z", "") + fmt = fmt.strip() + + if isinstance(time_val, tuple): + return time.strftime(fmt, time_val) + else: + return time_val.strftime(fmt) + +def format_date(date_val, gconf_client): + """ + Format a datetime as a date (without time). GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + date_val -- date / datetime object + gconf_client -- gconf client instance + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/date_format", locale.nl_langinfo(locale.D_FMT)) + if isinstance(date_val, tuple): + return datetime.date.strftime(fmt, date_val) + else: + return date_val.strftime(fmt) + +def format_date_time(date_val, gconf_client, display_seconds = True): + """ + Format a datetime as a date and a time. GConf + is checked for custom format, otherwise the default for the locale is + used. + + Keyword arguments: + date_val -- date / datetime object + gconf_client -- gconf client instance + display_seconds -- if false, seconds will be stripped from result + """ + fmt = g15gconf.get_string_or_default(gconf_client, "/apps/gnome15/date_time_format", locale.nl_langinfo(locale.D_T_FMT)) + if not display_seconds: + fmt = __strip_seconds(fmt) + if isinstance(date_val, tuple): + return datetime.datetime.strftime(fmt, date_val) + else: + return date_val.strftime(fmt) + +def get_translation(domain, modfile=None): + """ + Initialize a translation domain. Unless modfile is supplied, + the translation will be searched for in the default location. If it + is supplied, it's parent directory will be pre-pended to i18n to get + the location to use. + + Translation objects are cached. + + Keyword arguments: + domain -- translation domain + modfile -- module file location (search relative to this file + /i18n) + """ + if domain in __translations: + return __translations[domain] + gettext.install (True, localedir=None, unicode=1) + translation_location = mo_location + if modfile is not None: + translation_location = "%s/i18n" % os.path.dirname(modfile) + gettext.find(domain, translation_location) + locale.bindtextdomain(domain, translation_location) + gettext.bindtextdomain(domain, translation_location) + gettext.textdomain (domain) + gettext.bind_textdomain_codeset(domain, "UTF-8") + language = gettext.translation (domain, translation_location, languages=languages, fallback=True) + __translations[domain] = language + return language + +def parse_US_time(time_val): + """ + Parses a time in the US format (%I:%M %p) + This method assumes that the time_val value is valid. + It's behaviour is similar to a call to time.strptime + """ + parsed = re.match('(0?[1-9]|1[0-2]):([0-5][0-9]) (AM|am|PM|pm)', time_val) + hour, minute, ampm = parsed.group(1, 2, 3) + hour = int(hour) + minute = int(minute) + if ampm.lower() == 'pm': + hour = hour + 12 + return time.struct_time((1900, 1, 1, hour, minute, 0, 0, 1, -1)) + +def parse_US_time_or_none(time_val): + try: + return parse_US_time(time_val) + except Exception as e: + logger.debug("Invalid format for US time.", exc_info = e) + return None + +""" +Private +""" + +def __strip_seconds(fmt): + for f in REPLACE_FORMATS: + fmt = fmt.replace(*f) + return fmt \ No newline at end of file diff --git a/src/gnome15/g15logging.py b/src/gnome15/g15logging.py new file mode 100644 index 0000000..b54dbe9 --- /dev/null +++ b/src/gnome15/g15logging.py @@ -0,0 +1,51 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Initializes and configures the python logging system that is used in Gnome15 +""" +import logging + +DEFAULT_LEVEL = logging.NOTSET + +def configure(): + """ + Configures the logging python module with a basic configuration + and a logging format. + """ + logging.basicConfig(format='%(levelname)s\t%(asctime)s-%(threadName)s\t%(name)s - %(message)s', + datefmt='%H:%M:%S') + +def get_level(level): + """ + Returns the python logging module level matching the string passed + as parameter, or the default logging level if the string doesn't + match any level. + """ + result = logging.getLevelName(level.upper()) + if result == "Level %s" % level: + result = DEFAULT_LEVEL + return result + +def get_root_logger(): + """ + Initializes the logging system with a basic configuration, and + creates a root logger set to the default logging level. + """ + configure() + logger = logging.getLogger() + logger.setLevel(DEFAULT_LEVEL) + return logger diff --git a/src/gnome15/g15macroeditor.py b/src/gnome15/g15macroeditor.py new file mode 100644 index 0000000..9b3cfc7 --- /dev/null +++ b/src/gnome15/g15macroeditor.py @@ -0,0 +1,1104 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Manages the UI for editing a single macro. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("gnome15").ugettext + + +import g15globals +import g15profile +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15icontools as g15icontools +import g15uinput +import g15devices +import g15driver +import g15keyio +import g15actions +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import os +import pango +import gconf + +import logging +logger = logging.getLogger(__name__) + +# Key validation constants +IN_USE = "in-use" +RESERVED_FOR_ACTION = "reserved" +NO_KEYS = "no-keys" +OK = "ok" + +class G15MacroEditor(): + + def __init__(self, parent=None): + """ + Constructor. Create a new macro editor. You must call set_driver() + and set_macro() after constructions to populate the macro key buttons + and the other fields. + """ + self.__gconf_client = gconf.client_get_default() + self.__widget_tree = gtk.Builder() + self.__widget_tree.set_translation_domain("g15-macroeditor") + self.__widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "macro-editor.ui")) + self.__window = self.__widget_tree.get_object("EditMacroDialog") + if self.__window is not None and parent is not None: + self.__window.set_transient_for(parent) + + self.adjusting = False + self.editing_macro = None + self.selected_profile = None + self.memory_number = 1 + self.close_button = None + + # Private + self.__text_buffer = None + self.__rows = None + self.__driver = None + self.__key_buttons = None + self.__load_objects() + self.__load_actions() + self.__create_macro_info_bar() + self.__macro_save_timer = None + + # Connect signal handlers + self.__widget_tree.connect_signals(self) + + def run(self): + self.__window.run() + self.__window.hide() + + def set_driver(self, driver): + """ + Set the driver to use for this macro. This allows the full set of + available keys (and other capabilities) to determined. + + Keyword arguments: + driver -- driver + """ + self.__driver = driver + + def set_macro(self, macro): + """ + Set the macro to edit. Note, set_driver must have been called first + so it knows which macro keys are available for use for the model + in question. + + Keyword arguments: + macro -- macro to edit + """ + if self.__driver is None: + raise Exception("No driver set. Cannot set macro") + + self.adjusting = True + try: + self.editing_macro = macro + self.selected_profile = macro.profile + self.memory_number = macro.memory + self.__widget_tree.get_object("KeyBox").set_sensitive(not self.selected_profile.read_only) + keys_frame = self.__widget_tree.get_object("KeysFrame") + self.__allow_combination.set_active(len(self.editing_macro.keys) > 1) + + # Build the G-Key selection widget + if self.__rows: + keys_frame.remove(self.__rows) + self.__rows = gtk.VBox() + self.__rows.set_spacing(4) + self.__key_buttons = [] + for row in self.__driver.get_key_layout(): + hbox = gtk.HBox() + hbox.set_spacing(4) + for key in row: + key_name = g15driver.get_key_names([ key ]) + g_button = gtk.ToggleButton(" ".join(key_name)) + g_button.key = key + key_active = key in self.editing_macro.keys + g_button.set_active(key_active) + self.__set_button_style(g_button) + g_button.connect("toggled", self._toggle_key, key, self.editing_macro) + self.__key_buttons.append(g_button) + hbox.pack_start(g_button, True, True) + self.__rows.pack_start(hbox, False, False) + keys_frame.add(self.__rows) + keys_frame.show_all() + + # Set the activation mode + for index, (activate_on_id, activate_on_name) in enumerate(self.__activate_on_combo.get_model()): + if activate_on_id == self.editing_macro.activate_on: + self.__activate_on_combo.set_active(index) + + # Set the repeat mode + for index, (repeat_mode_id, repeat_mode_name) in enumerate(self.__repeat_mode_combo.get_model()): + if repeat_mode_id == self.editing_macro.repeat_mode: + self.__repeat_mode_combo.set_active(index) + + # Set the type of macro + for index, (macro_type, macro_type_name) in enumerate(self.__map_type_model): + if macro_type == self.editing_macro.type: + self.__mapped_key_type_combo.set_active(index) + self.__set_available_options() + + # Set the other details + for index, row in enumerate(self.__map_type_model): + if row[0] == self.editing_macro.type: + self.__mapped_key_type_combo.set_active(index) + break + self.__load_keys() + if self.editing_macro.type in [ g15profile.MACRO_MOUSE, g15profile.MACRO_JOYSTICK, g15profile.MACRO_DIGITAL_JOYSTICK, g15profile.MACRO_KEYBOARD ]: + for index, row in enumerate(self.__mapped_key_model): + if self.__mapped_key_model[index][0] == self.editing_macro.macro: + self.__select_tree_row(self.__uinput_tree, index) + break + elif self.editing_macro.type == g15profile.MACRO_ACTION: + for index, row in enumerate(self.__action_model): + if self.__action_model[index][0] == self.editing_macro.macro: + self.__select_tree_row(self.__action_tree, index) + break + + self.__text_buffer = gtk.TextBuffer() + self.__text_buffer.connect("changed", self._macro_script_changed) + self.__macro_script.set_buffer(self.__text_buffer) + + self.__turbo_rate.get_adjustment().set_value(self.editing_macro.repeat_delay) + self.__memory_bank_label.set_text("M%d" % self.memory_number) + self.__macro_name_field.set_text(self.editing_macro.name) + self.__override_default_repeat.set_active(self.editing_macro.repeat_delay != -1) + + if self.editing_macro.type == g15profile.MACRO_SIMPLE: + self.__simple_macro.set_text(self.editing_macro.macro) + else: + self.__simple_macro.set_text("") + if self.editing_macro.type == g15profile.MACRO_COMMAND: + cmd = self.editing_macro.macro + background = False + if cmd.endswith("&"): + cmd = cmd[:-1] + background = True + elif cmd == "": + background = True + self.__command.set_text(cmd) + self.__run_in_background.set_active(background) + else: + self.__run_in_background.set_active(False) + self.__command.set_text("") + if self.editing_macro.type == g15profile.MACRO_SCRIPT: + self.__text_buffer.set_text(self.editing_macro.macro) + else: + self.__text_buffer.set_text("") + + self.__check_macro(self.editing_macro.keys) + self.__macro_name_field.grab_focus() + + finally: + self.adjusting = False + self.editing_macro.name = self.__macro_name_field.get_text() + self.__set_available_options() + + """ + Event handlers + """ + def _override_default_repeat_changed(self, widget): + if not self.adjusting: + sel = widget.get_active() + if sel: + self.editing_macro.repeat_delay = 0.1 + self.__turbo_rate.get_adjustment().set_value(0.1) + self.__save_macro(self.editing_macro) + self.__set_available_options() + else: + self.editing_macro.repeat_delay = -1.0 + self.__set_available_options() + self.__save_macro(self.editing_macro) + + def _macro_script_changed(self, text_buffer): + self.editing_macro.macro = text_buffer.get_text(text_buffer.get_start_iter(), text_buffer.get_end_iter()) + self.__save_macro(self.editing_macro) + + def _show_script_editor(self, widget): + editor = G15MacroScriptEditor(self.__gconf_client, self.__driver, self.editing_macro, self.__window) + if editor.run(): + self.__text_buffer.set_text(self.editing_macro.macro) + self.__save_macro(self.editing_macro) + + def _turbo_changed(self, widget): + if not self.adjusting: + self.editing_macro.repeat_delay = widget.get_value() + self.__save_macro(self.editing_macro) + + def _repeat_mode_selected(self, widget): + if not self.adjusting: + self.editing_macro.repeat_mode = widget.get_model()[widget.get_active()][0] + self.__save_macro(self.editing_macro) + self.__set_available_options() + + def _mapped_key_type_changed(self, widget): + if not self.adjusting: + key = self.__map_type_model[widget.get_active()][0] + self.editing_macro.type = key + self.editing_macro.macro = "" + self.adjusting = True + try: + self.__load_keys() + finally: + self.adjusting = False + self.__select_tree_row(self.__uinput_tree, 0) + self.set_macro(self.editing_macro) + self.__set_available_options() + + def _clear_filter(self, widget): + self.__filter.set_text("") + + def _filter_changed(self, widget): + try: + self.adjusting = True + self.__load_keys() + finally: + self.adjusting = False + self._key_selected(None) + + def _simple_macro_changed(self, widget): + self.editing_macro.macro = widget.get_text() + self.__save_macro(self.editing_macro) + + def _command_changed(self, widget): + self.__save_command() + + def _browse_for_command(self, widget): + dialog = gtk.FileChooserDialog(_("Open.."), + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + file_filter = gtk.FileFilter() + file_filter.set_name(_("All files")) + file_filter.add_pattern("*") + dialog.add_filter(file_filter) + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + if response == gtk.RESPONSE_OK: + self.__command.set_text(dialog.get_filename()) + dialog.destroy() + return False + + def _run_in_background_changed(self, widget): + if not self.adjusting: + self.__save_command() + + def _allow_combination_changed(self, widget): + if not self.adjusting and not self.__allow_combination.get_active(): + for button in self.__key_buttons: + if len(self.editing_macro.keys) > 1: + button.set_active(False) + self.__check_macro(self.editing_macro.keys) + + def _macro_name_changed(self, widget): + self.editing_macro.name = widget.get_text() + self.__save_macro(self.editing_macro) + + def _toggle_key(self, widget, key, macro): + """ + Event handler invoked when one of the macro key buttons is pressed. + """ + keys = list(macro.keys) + + if key in keys: + keys.remove(key) + else: + if not self.adjusting and not self.__allow_combination.get_active(): + for button in self.__key_buttons: + if button != widget: + self.adjusting = True + try : + button.set_active(False) + finally: + self.adjusting = False + for ikey in keys: + if ikey != key: + keys.remove(ikey) + keys.append(key) + + if not self.selected_profile.are_keys_in_use(self.editing_macro.activate_on, + self.memory_number, keys, + exclude=[self.editing_macro]): + if self.__macro_name_field.get_text() == "" or self.__macro_name_field.get_text().startswith("Macro "): + new_name = " ".join(g15driver.get_key_names(keys)) + self.editing_macro.name = _("Macro %s") % new_name + self.__macro_name_field.set_text(self.editing_macro.name) + macro.set_keys(keys) + + self.__set_button_style(widget) + + if not self.adjusting: + self.__check_macro(keys) + self.__save_macro(self.editing_macro) + + def _key_selected(self, widget): + if not self.adjusting: + (model, path) = self.__uinput_tree.get_selection().get_selected() + if path is not None: + key = model[path][0] + self.editing_macro.macro = key + self.__save_macro(self.editing_macro) + + def _action_selected(self, widget): + if not self.adjusting: + (model, path) = self.__action_tree.get_selection().get_selected() + if path: + key = model[path][0] + self.editing_macro.macro = key + self.__save_macro(self.editing_macro) + + def _activate_on_changed(self, widget): + if not self.adjusting: + self.editing_macro.set_activate_on(widget.get_model()[widget.get_active()][0]) + self.__save_macro(self.editing_macro) + if self.editing_macro.activate_on == g15driver.KEY_STATE_HELD: + self.__repeat_mode_combo.set_active(0) + self.__set_available_options() + self.__check_macro(list(self.editing_macro.keys)) + + """ + Private + """ + + def __save_command(self): + macrotext = self.__command.get_text() + if self.__run_in_background.get_active(): + macrotext += "&" + self.editing_macro.macro = macrotext + self.__save_macro(self.editing_macro) + + def __select_tree_row(self, tree, row): + tree_iter = tree.get_model().iter_nth_child(None, row) + if tree_iter: + tree_path = tree.get_model().get_path(tree_iter) + tree.get_selection().select_path(tree_path) + tree.scroll_to_cell(tree_path) + + def __save_macro(self, macro): + """ + Schedule saving of the macro in 2 seconds. This may be called again + before the 2 seconds are up, in which case the timer will reset. + + Keyword arguments: + macro -- macro to save + """ + if not self.adjusting: + if self.__macro_save_timer is not None: + self.__macro_save_timer.cancel() + self.__macro_save_timer = g15scheduler.schedule("SaveMacro", 2, self.__do_save_macro, macro) + + def __do_save_macro(self, macro): + """ + Actually save the macro. This should not be called directly + + Keyword arguments: + macro -- macro to save + """ + if self.__validate_macro(macro.keys) in [ OK, RESERVED_FOR_ACTION ] : + logger.info("Saving macro %s", macro.name) + macro.save() + + def __load_actions(self): + self.__action_model.clear() + for action in g15actions.actions: + self.__action_model.append([action, action]) + + def __load_objects(self): + """ + Load references to the various components contain in the Glade file + """ + self.__macro_script = self.__widget_tree.get_object("MacroScript") + self.__map_type_model = self.__widget_tree.get_object("MapTypeModel") + self.__mapped_key_model = self.__widget_tree.get_object("MappedKeyModel") + self.__mapped_key_type_combo = self.__widget_tree.get_object("MappedKeyTypeCombo") + self.__map_type_model = self.__widget_tree.get_object("MapTypeModel") + self.__simple_macro = self.__widget_tree.get_object("SimpleMacro") + self.__command = self.__widget_tree.get_object("Command") + self.__run_in_background = self.__widget_tree.get_object("RunInBackground") + self.__browse_for_command = self.__widget_tree.get_object("BrowseForCommand") + self.__allow_combination = self.__widget_tree.get_object("AllowCombination") + self.__macro_name_field = self.__widget_tree.get_object("MacroNameField") + self.__macro_warning_box = self.__widget_tree.get_object("MacroWarningBox") + self.__memory_bank_label = self.__widget_tree.get_object("MemoryBankLabel") + self.__uinput_box = self.__widget_tree.get_object("UinputBox") + self.__command_box = self.__widget_tree.get_object("CommandBox") + self.__script_box = self.__widget_tree.get_object("ScriptBox") + self.__simple_box = self.__widget_tree.get_object("SimpleBox") + self.__action_box = self.__widget_tree.get_object("ActionBox") + self.__uinput_tree = self.__widget_tree.get_object("UinputTree") + self.__action_tree = self.__widget_tree.get_object("ActionTree") + self.__action_model = self.__widget_tree.get_object("ActionModel") + self.__repeat_mode_combo = self.__widget_tree.get_object("RepeatModeCombo") + self.__repetition_frame = self.__widget_tree.get_object("RepetitionFrame") + self.__turbo_rate = self.__widget_tree.get_object("TurboRate") + self.__turbo_box = self.__widget_tree.get_object("TurboBox") + self.__filter = self.__widget_tree.get_object("Filter") + self.__override_default_repeat = self.__widget_tree.get_object("OverrideDefaultRepeat") + self.__activate_on_combo = self.__widget_tree.get_object("ActivateOnCombo") + self.__show_script_editor = self.__widget_tree.get_object("ShowScriptEditor") + + def __load_keys(self): + """ + Load the available keys for the selected macro type + """ + sel_type = self.__get_selected_type() + filter_text = self.__filter.get_text().strip().lower() + if g15profile.is_uinput_type(sel_type): + (model, path) = self.__uinput_tree.get_selection().get_selected() + sel = None + if path: + sel = model[path][0] + model.clear() + found = False + for n, v in g15uinput.get_buttons(sel_type): + if len(filter_text) == 0 or filter_text in n.lower(): + model.append([n, v]) + if n == sel: + self.__select_tree_row(self.__uinput_tree, len(model)) + found = True + (model, path) = self.__uinput_tree.get_selection().get_selected() + if not found and len(model) > 0: + self.__select_tree_row(self.__uinput_tree, 0) + + def __get_selected_type(self): + """ + Get the selected macro type + """ + return self.__map_type_model[self.__mapped_key_type_combo.get_active()][0] + + def __set_available_options(self): + """ + Set the sensitive state of various components based on the current + selection of other components. + """ + + sel_type = self.__get_selected_type(); + uinput_type = g15profile.is_uinput_type(sel_type) + opposite_state = g15driver.KEY_STATE_UP if \ + self.editing_macro.activate_on == \ + g15driver.KEY_STATE_HELD else \ + g15driver.KEY_STATE_HELD + key_conflict = self.selected_profile.get_macro(opposite_state, \ + self.editing_macro.memory, + self.editing_macro.keys) is not None + + self.__uinput_tree.set_sensitive(uinput_type) + self.__run_in_background.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__command.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__browse_for_command.set_sensitive(sel_type == g15profile.MACRO_COMMAND) + self.__simple_macro.set_sensitive(sel_type == g15profile.MACRO_SIMPLE) + self.__macro_script.set_sensitive(sel_type == g15profile.MACRO_SCRIPT) + self.__action_tree.set_sensitive(sel_type == g15profile.MACRO_ACTION) + self.__activate_on_combo.set_sensitive(not uinput_type and not key_conflict) + self.__repeat_mode_combo.set_sensitive(self.__activate_on_combo.get_active() != 2) + self.__override_default_repeat.set_sensitive(self.editing_macro.repeat_mode != g15profile.NO_REPEAT) + self.__turbo_box.set_sensitive(self.editing_macro.repeat_mode != g15profile.NO_REPEAT and self.__override_default_repeat.get_active()) + + self.__simple_box.set_visible(sel_type == g15profile.MACRO_SIMPLE) + self.__command_box.set_visible(sel_type == g15profile.MACRO_COMMAND) + self.__action_box.set_visible(sel_type == g15profile.MACRO_ACTION) + self.__script_box.set_visible(sel_type == g15profile.MACRO_SCRIPT) + self.__show_script_editor.set_visible(sel_type == g15profile.MACRO_SCRIPT) + self.__uinput_box.set_visible(uinput_type) + + def __validate_macro(self, keys): + """ + Validate the list of keys, checking if they are in use, reserved + for an action, and that some have actually been supplier + + Keyword arguments: + keys -- list of keys to validate + """ + if len(keys) > 0: + reserved = g15devices.are_keys_reserved(self.__driver.get_model_name(), keys) + + in_use = self.selected_profile.are_keys_in_use(self.editing_macro.activate_on, + self.memory_number, + keys, + exclude=[self.editing_macro]) + if in_use: + return IN_USE + elif reserved: + return RESERVED_FOR_ACTION + else: + return OK + else: + return NO_KEYS + + def __check_macro(self, keys): + """ + Check with the keys provided are valid for the current state, e.g. + check if another macro or action is using them. Note, this still + allows the change to happen, it will just show a warning and prevent + the window from being closed if + """ + val = self.__validate_macro(keys) + if val == IN_USE: + self.__macro_infobar.set_message_type(gtk.MESSAGE_ERROR) + self.__macro_warning_label.set_text(_("This key combination is already in use with " + \ + "another macro. Please choose a different key or combination of keys")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + + if self.close_button is not None: + self.close_button.set_sensitive(False) + elif val == RESERVED_FOR_ACTION: + self.__macro_infobar.set_message_type(gtk.MESSAGE_WARNING) + self.__macro_warning_label.set_text(_("This key combination is reserved for use with an action. You " + \ + "may use it, but the results are undefined.")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + if self.close_button is not None: + self.close_button.set_sensitive(True) + elif val == NO_KEYS: + self.__macro_infobar.set_message_type(gtk.MESSAGE_WARNING) + self.__macro_warning_label.set_text(_("You have not chosen a macro key to assign the action to.")) + self.__macro_infobar.set_visible(True) + self.__macro_infobar.show_all() + if self.close_button is not None: + self.close_button.set_sensitive(False) + else: + self.__macro_infobar.set_visible(False) + if self.close_button is not None: + self.close_button.set_sensitive(True) + + def __create_macro_info_bar(self): + """ + Creates a component for display information about the current + macro, such as conflicts. The component is added to a placeholder in + the Glade file + """ + self.__macro_infobar = gtk.InfoBar() + self.__macro_infobar.set_size_request(-1, -1) + self.__macro_warning_label = gtk.Label() + self.__macro_warning_label.set_line_wrap(True) + self.__macro_warning_label.set_width_chars(60) + content = self.__macro_infobar.get_content_area() + content.pack_start(self.__macro_warning_label, True, True) + + self.__macro_warning_box.pack_start(self.__macro_infobar, True, True) + self.__macro_infobar.set_visible(False) + + def __set_button_style(self, button): + """ + Alter the button style based on whether it is active or not + + Keyword arguments: + button -- button widget + """ + font = pango.FontDescription("Sans 10") + if button.get_use_stock(): + label = button.child.get_children()[1] + elif isinstance(button.child, gtk.Label): + label = button.child + else: + raise ValueError("button does not have a label") + if button.get_active(): + font.set_weight(pango.WEIGHT_HEAVY) + else: + font.set_weight(pango.WEIGHT_MEDIUM) + label.modify_font(font) + +OP_ICONS = { 'delay' : 'gtk-media-pause', + 'press' : 'gtk-go-down', + 'upress' : 'gtk-go-down', + 'release' : 'gtk-go-up', + 'urelease' : 'gtk-go-up', + 'execute' : 'gtk-execute', + 'label' : 'gtk-underline', + 'wait' : 'gtk-stop', + 'goto' : [ 'stock_media-prev','media-skip-backward','gtk-media-previous' ] } + +class G15MacroScriptEditor(): + + def __init__(self, gconf_client, driver, editing_macro, parent = None): + + self.__gconf_client = gconf_client + self.__driver = driver + self.__clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD) + + self.__recorder = g15keyio.G15KeyRecorder(self.__driver) + self.__recorder.on_stop = self._on_stop_record + self.__recorder.on_add = self._on_record_add + + self.__widget_tree = gtk.Builder() + self.__widget_tree.set_translation_domain("g15-macroeditor") + self.__widget_tree.add_from_file(os.path.join(g15globals.ui_dir, "script-editor.ui")) + self._load_objects() + if parent is not None: + self.__window.set_transient_for(parent) + self._load_key_presses() + self._configure_widgets() + self._add_info_box() + self.set_macro(editing_macro) + self._set_available() + + # Connect signal handlers + self.__widget_tree.connect_signals(self) + + # Configure defaults + self.__output_delays.set_active(g15gconf.get_bool_or_default(self.__gconf_client, "/apps/gnome15/script_editor/record_delays", True)) + self.__emit_uinput.set_active(g15gconf.get_bool_or_default(self.__gconf_client, "/apps/gnome15/script_editor/emit_uinput", False)) + self.__recorder.output_delays = self.__output_delays.get_active() + self.__recorder.emit_uinput = self.__emit_uinput.get_active() + + def set_macro(self, macro): + self.__editing_macro = macro + self.__macros = self.__editing_macro.macro.split("\n") + self.__recorder.clear() + self._rebuild_model() + self._set_available() + + def _rebuild_model(self): + self.__script_model.clear() + for macro_text in self.__macros: + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = " ".join(split[1:]) + if op in OP_ICONS: + icon = OP_ICONS[op] + icon_path = g15icontools.get_icon_path(icon, 24) + self.__script_model.append([gtk.gdk.pixbuf_new_from_file(icon_path), val, op, True]) + + self._validate_script() + + def _validate_script(self): + msg = self._do_validate_script() + if msg: + self._show_message(gtk.MESSAGE_ERROR, msg) + self.__save_button.set_sensitive(False) + else: + self.__infobar.hide_all() + self.__save_button.set_sensitive(True) + + def _do_validate_script(self): + labels = [] + for _,val,op,_ in self.__script_model: + if op == "label": + if val in labels: + return "Label %s is defined more than once" % val + labels.append(val) + + pressed = {} + for _,val,op,_ in self.__script_model: + if op == "press" or op == "upress": + if val in pressed: + return "More than one key press of %s before a release" % val + pressed[val] = True + elif op == "release" or op == "urelease": + if not val in pressed: + return "Release of %s before it was pressed" % val + del pressed[val] + elif op == "goto": + if not val in labels: + return "Goto %s uses a label that doesn't exist" % val + + if len(pressed) > 0: + return "The script leaves %s pressed on completion" % ",".join(pressed.keys()) + + return None + + def run(self): + response = self.__window.run() + self.__window.hide() + if response == gtk.RESPONSE_OK: + buf = "" + for p in self.__macros: + if not buf == "": + buf += "\n" + buf += p + self.__editing_macro.macro = buf + return True + + def _add_info_box(self): + self.__infobar = gtk.InfoBar() + self.__infobar.set_size_request(-1, 32) + self.__warning_label = gtk.Label() + self.__warning_label.set_size_request(400, -1) + self.__warning_label.set_line_wrap(True) + self.__warning_label.set_alignment(0.0, 0.0) + self.__warning_image = gtk.Image() + content = self.__infobar.get_content_area() + content.pack_start(self.__warning_image, False, False) + content.pack_start(self.__warning_label, True, True) + self.__info_box_area.pack_start(self.__infobar, False, False) + self.__infobar.hide_all() + + def _show_message(self, message_type, text): + print "Showing message",text + self.__infobar.set_message_type(message_type) + self.__warning_label.set_text(text) + self.__warning_label.set_use_markup(True) + + if type == gtk.MESSAGE_WARNING: + self.__warning_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) + +# self.main_window.check_resize() + self.__infobar.show_all() + + def _load_objects(self): + self.__window = self.__widget_tree.get_object("EditScriptDialog") + self.__script_model = self.__widget_tree.get_object("ScriptModel") + self.__script_tree = self.__widget_tree.get_object("ScriptTree") + self.__set_value_dialog = self.__widget_tree.get_object("SetValueDialog") + self.__set_value = self.__widget_tree.get_object("SetValue") + self.__edit_selected_values = self.__widget_tree.get_object("EditSelectedValues") + self.__delay_adjustment = self.__widget_tree.get_object("DelayAdjustment") + self.__command = self.__widget_tree.get_object("Command") + self.__label = self.__widget_tree.get_object("Label") + self.__goto_label_model = self.__widget_tree.get_object("GotoLabelModel") + self.__goto_label = self.__widget_tree.get_object("GotoLabel") + self.__key_press_model = self.__widget_tree.get_object("KeyPressModel") + self.__record_key = self.__widget_tree.get_object("RecordKey") + self.__emit_uinput = self.__widget_tree.get_object("EmitUInput") + self.__output_delays = self.__widget_tree.get_object("OutputDelays") + self.__record_button = self.__widget_tree.get_object("RecordButton") + self.__stop_button = self.__widget_tree.get_object("StopButton") + self.__record_status = self.__widget_tree.get_object("RecordStatus") + self.__scrip_editor_popup = self.__widget_tree.get_object("ScriptEditorPopup") + self.__info_box_area = self.__widget_tree.get_object("InfoBoxArea") + self.__save_button = self.__widget_tree.get_object("SaveButton") + self.__wait_combo = self.__widget_tree.get_object("WaitCombo") + self.__wait_model = self.__widget_tree.get_object("WaitModel") + + def _load_key_presses(self): + self.__key_press_model.clear() + if self.__emit_uinput.get_active(): + for n, v in g15uinput.get_buttons(g15uinput.KEYBOARD): + self.__key_press_model.append([n]) + else: + for n in g15keyio.get_keysyms(): + self.__key_press_model.append([n]) + + def _configure_widgets(self): + self.__script_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + tree_selection = self.__script_tree.get_selection() + tree_selection.connect("changed", self._on_selection_changed) + + def _on_tree_button_press(self, treeview, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + time = event.time + tree_selection = self.__script_tree.get_selection() + if tree_selection.count_selected_rows() < 2: + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, _, _ = pthinfo + treeview.grab_focus() + treeview.set_cursor( path, col, 0) + self.__scrip_editor_popup.popup( None, None, None, event.button, time) + return True + + def _on_cut(self, widget): + self._on_copy(widget) + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + for p in reversed(selected_paths): + del self.__macros[p[0]] + self._rebuild_model() + + def _on_copy(self, widget): + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + buf = "" + for p in selected_paths: + if not buf == "": + buf += "\n" + buf += self._format_row(model[p]) + self.__clipboard.set_text(buf) + + def _on_paste(self, widget): + self.__clipboard.request_text(self._clipboard_text_received) + + def _clipboard_text_received(self, clipboard, text, data): + i = self._get_insert_index() + if text: + for macro_text in text.split("\n"): + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = split[1] + if op in OP_ICONS: + self.__macros.insert(i, macro_text) + i += 1 + self._rebuild_model() + + def _on_record_add(self, pr, key): + gobject.idle_add(self._set_available) + + def _on_selection_changed(self, widget): + self.__edit_selected_values.set_sensitive(self._unique_selected_types() == 1) + + def _unique_selected_types(self): + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + t = {} + for p in selected_paths: + op = model[p][2] + t[op] = ( t[op] if op in t else 0 ) + 1 + + return len(t) + + def _on_emit_uinput_toggled(self, widget): + self.__recorder.emit_uinput = widget.get_active() + self.__gconf_client.set_bool("/apps/gnome15/script_editor/emit_uinput", widget.get_active()) + + def _on_deselect_all(self, widget): + self.__script_tree.get_selection().unselect_all() + + def _on_edit_selected_values_activate(self, widget): + self.__set_value_dialog.set_transient_for(self.__window) + response = self.__set_value_dialog.run() + self.__set_value_dialog.hide() + if response == gtk.RESPONSE_OK: + tree_selection = self.__script_tree.get_selection() + model, selected_paths = tree_selection.get_selected_rows() + for p in selected_paths: + self.__macros[p[0]] = self._format_row(model[p], self.__set_value.get_text()) + self._rebuild_model() + + def _format_row(self, row, value = None): + return "%s %s" % (self._format_op(row[2]),value if value is not None else row[1]) + + def _format_op(self, op): + return op[:1].upper() + op[1:] + + def _on_browse_command(self, widget): + dialog = gtk.FileChooserDialog("Choose Command..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.__window) + dialog.set_filename(self.__command.get_text()) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self.__command.set_text(dialog.get_filename()) + + def _on_new_goto(self, widget): + self.__goto_label_model.clear() + for _,val,op,_ in self.__script_model: + if op == "label": + self.__goto_label_model.append([val]) + if not self.__goto_label.get_active() >= 0 and len(self.__goto_label_model) > 0: + self.__goto_label.set_active(0) + + dialog = self.__widget_tree.get_object("AddGotoDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("goto"), self.__goto_label_model[self.__goto_label.get_active()][0])) + + def _on_new_label(self, widget): + dialog = self.__widget_tree.get_object("AddLabelDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("label"), self.__label.get_text())) + + def _on_new_execute(self, widget): + dialog = self.__widget_tree.get_object("AddExecuteDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("execute"), self.__command.get_text())) + + + def _on_new_wait(self, widget): + dialog = self.__widget_tree.get_object("AddWaitDialog") + dialog.set_transient_for(self.__window) + if not self.__wait_combo.get_active() >= 0 and len(self.__wait_model) > 0: + self.__wait_combo.set_active(0) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("wait"), self.__wait_model[self.__wait_combo.get_active()][0])) + + def _on_add_delay(self, widget): + dialog = self.__widget_tree.get_object("AddDelayDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + self._stop_recorder() + if response == gtk.RESPONSE_OK: + self._insert_macro("%s %s" % ( self._format_op("delay"), int(self.__delay_adjustment.get_value())) ) + + def _on_rows_reordered(self, model, path, iter, new_order): + print "reorder" + # The model will have been updated, so update our text base list from that + for index,row in enumerate(self._script.model): + x = self._format_row(row) + print x + self.__macros[index] = x + self._rebuild_model() + + def _get_insert_index(self): + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + return len(self.__script_model) if len(selected_paths) == 0 else selected_paths[0][0] + 1 + + def _on_start_record_button(self, widget): + self.__recorder.start_record() + self._set_available() + + def _set_available(self): + self.__record_button.set_sensitive(not self.__recorder.is_recording()) + self.__stop_button.set_sensitive(self.__recorder.is_recording()) + ops = len(self.__recorder.script) + self.__record_status.set_text(_("Now recording (%d) operations" % ops) if self.__recorder.is_recording() else (_("Will insert %d operations" % ops) if ops > 0 else "")) + + def _on_stop_record_button(self, widget): + self.__recorder.stop_record() + + def _on_stop_record(self, recorder): + gobject.idle_add(self._set_available) + + def _stop_recorder(self): + if self.__recorder.is_recording(): + self.__recorder.stop_record() + + def _on_output_delays_changed(self, widget): + self.__recorder.output_delays = widget.get_active() + self.__gconf_client.set_bool("/apps/gnome15/script_editor/record_delays", self.__recorder.output_delays) + + def _on_record(self, widget): + self.__recorder.clear() + self._set_available() + dialog = self.__widget_tree.get_object("RecordDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if self.__recorder.is_recording(): + self.__recorder.stop_record() + if response == gtk.RESPONSE_OK: + i = self._get_insert_index() + for op, value in self.__recorder.script: + if len(self.__recorder.script) > 0: + macro_text = "%s %s" % ( self._format_op(op), value) + self.__macros.insert(i, macro_text) + i += 1 + self._rebuild_model() + + def _insert_macro(self, macro_text): + i = self._get_insert_index() + self.__macros.insert(i, macro_text) + self._rebuild_model() + + def _on_remove_macro_operations(self, widget): + dialog = self.__widget_tree.get_object("RemoveMacroOperationsDialog") + dialog.set_transient_for(self.__window) + response = dialog.run() + dialog.hide() + if response == gtk.RESPONSE_OK: + tree_selection = self.__script_tree.get_selection() + _, selected_paths = tree_selection.get_selected_rows() + for p in reversed(selected_paths): + del self.__macros[p[0]] + self._rebuild_model() + + def _on_value_edited(self, widget, path, value): + self.__macros[int(path)] = self._format_row(self.__script_model[int(path)], value) + self._rebuild_model() + + def _on_select_all_key_operations(self, widget): + self._select_by_op([ "press", "release", "upress", "urelease" ]) + + def _on_select_all_key_presses(self, widget): + self._select_by_op(["press", "upress" ]) + + def _on_select_all_key_releases(self, widget): + self._select_by_op(["release", "urelease"]) + + def _on_select_all_commands(self, widget): + self._select_by_op("execute") + + def _on_select_all(self, widget): + self.__script_tree.get_selection().select_all() + + def _on_select_all_delays(self, widget): + self._select_by_op("delay") + + def _on_macro_operation_cursor_changed(self, widget): + pass + +# tree_selection = self.__script_tree.get_selection() +# _, selected_path = tree_selection.get_selected_rows() +# if len(selected_path) == 1: +# selected_index = selected_path[0][0] +# _,val,op,_ = self.__script_model[selected_index] +# print op,val +# +# if op == "press": +# for i in range(selected_index + 1, len(self.__macros)): +# _,row_val,row_op,_ = self.__script_model[i] +# if row_op == "delay": +# self._select_row(i) +# elif row_op == "release" and val == row_val: +# self._select_row(i) +# +# if i + 1 < len(self.__script_model) and \ +# self.__script_model[i + 1][2] == "delay": +# self._select_row(i + 1) +# +# break +# elif op == "release": +# if selected_index + 1 < len(self.__script_model) and \ +# self.__script_model[selected_index + 1][2] == "delay": +# self._select_row(selected_index + 1) +# +# for i in range(selected_index - 1, 0, -1): +# _,row_val,row_op,_ = self.__script_model[i] +# if row_op == "delay": +# self._select_row(i) +# elif row_op == "press" and val == row_val: +# self._select_row(i) +# break + + + def _select_by_op(self, show_ops): + tree_selection = self.__script_tree.get_selection() + tree_selection.unselect_all() + for idx, row in enumerate(self.__script_model): + _,_,op,_ = row + if isinstance(show_ops, list) and op in show_ops or op == show_ops: + tree_selection.select_path(self.__script_model.get_path(self.__script_model.get_iter_from_string("%d" % idx))) + + def _select_row(self, row): + self.__script_tree.get_selection().select_path(self.__script_model.get_path(self.__script_model.get_iter_from_string("%d" % row))) + +if __name__ == "__main__": + me = G15MacroEditor() + if (me.window): + me.window.connect("destroy", gtk.main_quit) + me.window.run() + me.window.hide() diff --git a/src/gnome15/g15network.py b/src/gnome15/g15network.py new file mode 100644 index 0000000..89d631d --- /dev/null +++ b/src/gnome15/g15network.py @@ -0,0 +1,79 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Classes and utilities for monitoring the current state of the network, allowing +plugins that declare "needs_network" to be enabled or disabled depending on this +state. + +This is done using the NetworkManager DBUS interface, although the number of +states available is reduced to connected/disconnected +''' + +import dbus + +# Logging +import logging +logger = logging.getLogger(__name__) + +_system_bus = dbus.SystemBus() + +NM_BUS_NAME = 'org.freedesktop.NetworkManager' +NM_OBJECT_PATH = '/org/freedesktop/NetworkManager' +NM_INTERFACE_NAME = 'org.freedesktop.NetworkManager' +NM_STATE_INDEX = { 0: 'Unknown', + 10: 'Asleep', + 20: 'Disconnected', + 30: 'Disconnecting', + 40: 'Connecting', + 50: 'Connected (Local)', + 60: 'Connected (Site)', + 70: 'Connected (Global)' } + +class NetworkManager(): + def __init__(self, screen): + self._screen = self + self.listeners = [] + self._state = -1 + try: + _manager = _system_bus.get_object(NM_BUS_NAME, NM_OBJECT_PATH) + self._interface = dbus.Interface(_manager, NM_INTERFACE_NAME) + self._set_state(self._interface.state()) + self._handle = self._interface.connect_to_signal('StateChanged', self._set_state) + except dbus.DBusException as e: + logger.warning("NetworkManager DBUS interface could not be contacted. All plugins will assume the network is available, and may behave unexpectedly.") + logger.debug("NetworkManager connection attempt below :", exc_info = e) + + # Assume connected + self._state = 70 + + def _set_state(self, state): + if state in NM_STATE_INDEX: + logger.info("New network state is %s", NM_STATE_INDEX[state]) + s = state + else: + logger.info("New network state is unknown") + s = 0 + if s != self._state and s in [ 0, 20, 60, 70 ]: + self._state = s + for l in self.listeners: + l(self.is_network_available()) + + def is_network_available(self): + return self._state in [ 60, 70 ] + + def is_internet_available(self): + return self._state == 70 diff --git a/src/gnome15/g15notify.py b/src/gnome15/g15notify.py new file mode 100644 index 0000000..dbd6e06 --- /dev/null +++ b/src/gnome15/g15notify.py @@ -0,0 +1,62 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Notifications +''' +import dbus +import g15globals + +# Logging +import logging +logger = logging.getLogger(__name__) + +_session_bus = dbus.SessionBus() + +class NotifyMessage(): + def __init__(self): + self.id = 0 + + def close(self): + logger.info("Closing notification %s", str(self.id)) + _get_obj().CloseNotification(self.id) + + def handle_reply(self, e): + self.id = int(e) + logger.debug("Got message ID %d", self.id) + + def handle_error(self, e): + logger.error("Error getting notification message ID. %s", str(e)) + +def _get_obj(): + return _session_bus.get_object("org.freedesktop.Notifications", '/org/freedesktop/Notifications') + +def notify(summary, body, icon = "", actions = [], hints = {}, timeout = 10.0, replaces = 0): + actions_array = dbus.Array(actions, signature='s') + hints_dict = dbus.Dictionary(hints, signature='sv') + msg = NotifyMessage() + _get_obj().Notify(g15globals.name, + replaces, + icon, + summary, + body, + actions_array, + hints_dict, + int(timeout * 1000), + dbus_interface = 'org.freedesktop.Notifications', + reply_handler = msg.handle_reply, + error_handler = msg.handle_error) + return msg \ No newline at end of file diff --git a/src/gnome15/g15plugin.py b/src/gnome15/g15plugin.py new file mode 100644 index 0000000..b7bcffc --- /dev/null +++ b/src/gnome15/g15plugin.py @@ -0,0 +1,426 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import dbus +import util.g15scheduler as g15scheduler +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15theme +import g15screen +import sys +import gobject + +class G15Plugin(): + + """ + Generic base plugin class + """ + def __init__(self, gconf_client, gconf_key, screen): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + """ + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.active = False + self.__notify_handlers = [] + + def create_theme(self): + """ + Create a theme, using the currently selected theme for this plugin + if one is available. + """ + theme = self.gconf_client.get_string("%s/theme" % self.gconf_key) + new_theme = None + if theme: + theme_def = g15theme.get_theme(theme, sys.modules[self.__module__]) + if theme_def: + new_theme = g15theme.G15Theme(theme_def) + if not new_theme: + new_theme = g15theme.G15Theme(self) + new_theme.plugin = self + return new_theme + + def watch(self, key, callback): + """ + Watch for gconf changes for this plugin on a particular sub-key, calling + the callback when the value changes. All watches will be removed when + the plugin deactivates, so these should be added during the activate + phase. + + Keyword arguments: + key - sub-key (or None to monitor everything) + callback - function to call on change + """ + if isinstance(key, list): + for k in key: + self.watch(k, callback) + return + if key is not None and key.startswith("/"): + k = key + else: + k = "%s/%s" % (self.gconf_key, key) if key is not None else self.gconf_key + self.__notify_handlers.append(self.gconf_client.notify_add(k, callback)) + + def activate(self): + self.active = True + self.watch("theme", self._reactivate) + + def deactivate(self): + for h in self.__notify_handlers: + self.gconf_client.notify_remove(h); + self.active = False + + def destroy(self): + pass + + def _reactivate(self, client, connection_id, entry, args): + self.deactivate() + self.activate() + +class G15PagePlugin(G15Plugin): + + """ + Generic base plugin for plugins that want to contribute a page (most plugins + will extend this in some way) + """ + def __init__(self, gconf_client, gconf_key, screen, icon, page_id, title): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + icon - icon to use for thumbnail + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.page_id = page_id + self.hidden = False + self._icon_path = g15icontools.get_icon_path(icon) + self._title = title + self.page = None + self.thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + self.add_page_on_activate = True + + def activate(self): + G15Plugin.activate(self) + self.page = self.create_page() + self.populate_page() + if self.add_page_on_activate: + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def deactivate(self): + if self.page is not None: + self.screen.del_page(self.page) + self.page = None + G15Plugin.deactivate(self) + + def create_page(self): + return g15theme.G15Page(self.page_id, self.screen, + title = self._title, theme = self.create_theme(), + thumbnail_painter = self._paint_thumbnail, + theme_properties_callback = self.get_theme_properties, + theme_properties_attributes = self.get_theme_attributes, + painter = self._paint, + originating_plugin = self) + + def populate_page(self): + """ + Populate page. Subclasses may override to create or configure + additional components. + """ + pass + + def get_theme_properties(self): + """ + Get the properties to pass to the SVG theme file for rendering. Sub-classes + may override to provide more properties if needed. + + The subclass may return the same properties object with more properties added, + or a complete new one if the default properties are to be excluded. + + Keyword arguments: + properties -- properties + """ + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._title + properties["alt_title"] = "" + return properties + + def reload_theme(self): + """ + Reload the current theme + """ + self.page.set_theme(self.create_theme()) + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + + def _paint_panel(self, canvas, allocated_size, horizontal): + pass + + def _paint(self, canvas): + pass + +class G15RefreshingPlugin(G15PagePlugin): + + """ + Base plugin class that may be used for plugins that refresh at set intervals. This + abstract class will take care of disabling the refresh while the page is not + visible + """ + + def __init__(self, gconf_client, gconf_key, screen, icon, page_id, title, refresh_interval = 1.0): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + icon - icon to use for thumbnail + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15PagePlugin.__init__(self, gconf_client, gconf_key, screen, icon, page_id, title) + self.refresh_interval = refresh_interval + self.session_bus = dbus.SessionBus() + self.timer = None + self.schedule_on_gobject = False + self.only_refresh_when_visible = True + + def create_page(self): + return g15theme.G15Page(self.page_id, + self.screen, + title = self._title, + theme = self.create_theme(), + thumbnail_painter = self._paint_thumbnail, + painter = self._paint, + panel_painter = self._paint_panel, + theme_properties_callback = self.get_theme_properties, + originating_plugin = self) + + def activate(self): + G15PagePlugin.activate(self) + self._schedule_refresh() + + def deactivate(self): + self._cancel_refresh() + G15PagePlugin.deactivate(self) + + def populate_page(self): + G15PagePlugin.populate_page(self) + self.refresh() + + def refresh(self): + """ + Sub-classes should implement and perform the recurring actions. There is no need to + to redraw the page, it is done automatically. + """ + pass + + def get_next_tick(self): + """ + Get how long to wait before the next refresh. By default this uses the 'refresh + interval', but sub-classes may override to provide custom tick logic. + """ + return self.refresh_interval + + def do_refresh(self): + """ + Programatically refresh. The timer will be reset + """ + self._cancel_refresh() + self._do_refresh() + self._schedule_refresh() + + + ''' Private + ''' + + def _reschedule_refresh(self): + self._cancel_refresh() + self._schedule_refresh() + + def _cancel_refresh(self): + if self.timer != None: + if isinstance(self.timer, int): + gobject.source_remove(self.timer) + else: + self.timer.cancel() + self.timer = None + + def _schedule_refresh(self): + if self.schedule_on_gobject: + self.timer = gobject.timeout_add(int(self.get_next_tick() * 1000), self._refresh) + else: + self.timer = g15scheduler.schedule("%s-Redraw" % self.page_id, + self.get_next_tick(), + self._refresh) + + def _refresh(self): + if self.page and (not self.only_refresh_when_visible or self.screen.is_visible(self.page)): + self._do_refresh() + self._reschedule_refresh() + + def _do_refresh(self): + self.refresh() + self.screen.redraw(self.page) + +class G15MenuPlugin(G15Plugin): + ''' + Base plugin class that may be used when the plugin just displays a single + menu style component. + ''' + + def __init__(self, gconf_client, gconf_key, screen, menu_title_icon, page_id, title, show_on_activate = True): + """ + Constructor + + Keyword arguments: + gconf_client - gconf client + gconf_key - gconf key for plugin + screen - screen + menu_title_icon - icon to use for thumbnail and the menu title + title - title for page (displayed in menu etc) + refresh_interval - how often to refresh the page + """ + G15Plugin.__init__(self, gconf_client, gconf_key, screen) + + self.page_id = page_id + self.page = None + self.hidden = False + self.menu = None + self.session_bus = dbus.SessionBus() + self._title = title + self._show_on_activate = show_on_activate + self.set_icon(menu_title_icon) + + def set_icon(self, icon): + self._icon_path = g15icontools.get_icon_path(icon) + self.thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + + def activate(self): + G15Plugin.activate(self) + self.reload_theme() + if self._show_on_activate: + self.show_menu() + + def deactivate(self): + G15Plugin.deactivate(self) + if self.page != None: + self.hide_menu() + + def get_theme_properties(self): + """ + Get the properties to pass to the SVG theme file for rendering. Sub-classes + may override to provide more properties if needed. + + The subclass may return the same properties object with more properties added, + or a complete new one if the default properties are to be excluded. + + Keyword arguments: + properties -- properties + """ + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._title + properties["alt_title"] = "" + properties["no_items"] = self.menu.get_child_count() == 0 + return properties + + def reload_theme(self): + """ + Reload the SVG theme and configure it + """ + self.theme = g15theme.G15Theme(self, "menu-screen") + + def show_menu(self): + + """ + Create the component tree for the menu page and draw it + """ + if not self.active: + return + + self.page = self.create_page() + self.menu = self.create_menu() + self.page.set_focused_component(self.menu) + self.menu.focusable = True + self.page.on_deleted = self.page_deleted + self.menu.set_focused(True) + self.page.add_child(self.menu) + self.page.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + self.load_menu_items() + self.add_to_screen() + + def add_to_screen(self): + """ + Add the page to the screen + """ + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def create_page(self): + """ + Create the page. Subclasses may override. + """ + return g15theme.G15Page(self.page_id, self.screen, priority=g15screen.PRI_NORMAL, title = self._title, theme = self.theme, \ + theme_properties_callback = self.get_theme_properties, + thumbnail_painter = self.paint_thumbnail, + originating_plugin = self) + + def page_deleted(self): + """ + Invoked when the page is removed from the screen + """ + self.page = None + + def create_menu(self): + """ + Create the menu component. Subclasses may override to create or configure + different components. + """ + return g15theme.Menu("menu") + + def hide_menu(self): + """ + Delete the page + """ + self.screen.del_page(self.page) + self.page = None + + def load_menu_items(self): + """ + Subclasses should override to set the initial menu items + """ + pass + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + \ No newline at end of file diff --git a/src/gnome15/g15pluginmanager.py b/src/gnome15/g15pluginmanager.py new file mode 100644 index 0000000..ee464e6 --- /dev/null +++ b/src/gnome15/g15pluginmanager.py @@ -0,0 +1,574 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module handles the loading, starting, stopping and general management of +plugins. + +There are two types of plugins supported :- + +Device Plugins - These are plugins that require an actual keyboard device + to work. For example, they might add a new screen, + or listen for key events. There may be one instance of + each plugin per connected device. + +Global Plugins - These are plugins that are not tied to any specific device. + Only one instance may be running at a time. + +Plugins are looked for in a number of locations. + +* g15globals.plugin_dir - This is where official plugins installed with Gnome15 reside +* $XDG_DATA_HOME/gnome15/plugins - This is where users can put their own local plugins +* $XDG_CONFIG_HOME/gnome15/plugins - This is a deprecated place where users can put + their own local plugins. It will be removed on a future + version of Gnome15 +* $G15_PLUGINS_DIR - If it exists allows custom locations to be added +* g15pluginmanager.extra_plugin_dirs - Allows other plugins to dynamically register new plugin + locations + +The lifecycle of all plugins consists of 5 stages. + +1. Loading - When the python module is loaded. This happens to all plugins, +regardless of whether they are enabled or not. Any plugins that fail this +stage will not be visible. + +2. Initialise - This is when the plugin instance is created. All enabled +plugins will go through this stage *once*. If a plugin is de-activated, and +then re-activated, it will not be re-initialised unless the device it is +attached to is completely stopped (not necessarily because of shutdown). + +3. Activation - Occurs during start-up of all enabled plugins. If a plugin is +de-activated, and then re-activated. The activate() function is called again. + +4. De-activation - Occurs when the plugin is de-activated for some reason. +This may be because the user disabled it, or if the device is attached to is +stopped, or when Gnome15 itself is shutting down. + +5. Destruction - Occurs when the device the plugin is attached to is stopped, +or if Gnome15 itself is closing down. + +""" + +import os.path +import sys +import g15globals +import g15driver +import g15actions +import gconf +import threading + +# Logging +import logging +logger = logging.getLogger(__name__) + +imported_plugins = [] + +""" +This list may be added to dynamically to add new plugin locations +""" +extra_plugin_dirs = [] + +# Plugin manager states +UNINITIALISED = 0 +STARTING = 1 +STARTED = 2 +ACTIVATING = 3 +ACTIVATED = 4 +DEACTIVATING = 5 +DEACTIVATED = 6 +DESTROYING = 7 + +def list_plugin_dirs(path): + """ + List all plugin directories in a given path. + + Keyword arguments: + path -- path to look for plugins + """ + plugindirs = [] + if os.path.exists(path): + for p_dir in os.listdir(path): + plugin_path = os.path.join(path, p_dir) + if os.path.isdir(plugin_path): + plugindirs.append(os.path.realpath(plugin_path)) + else: + logger.debug("Plugin path %s does not exist.", path) + return plugindirs + +def get_extra_plugin_dirs(): + """ + Get a list of all directories plugin directories may be found in. This + will included any dynamically registered using the + g15pluginmanager.extra_plugin_dirs list, and all paths that are found + in the G15_PLUGINS environment variable. + """ + plugindirs = [] + plugindirs += extra_plugin_dirs + if "G15_PLUGINS" in os.environ: + for p_dir in os.environ["G15_PLUGINS"].split(":"): + plugindirs += list_plugin_dirs(p_dir) + return plugindirs + +def get_module_for_id(module_id): + """ + Get a plugin module given it's ID. + + Keyword arguments: + module_id -- plugin module ID + """ + for mod in imported_plugins: + if mod.id == module_id: + return mod + +def get_supported_models(plugin_module): + """ + Get a list of models that a plugin supports. This takes into account + the supported_models and unsupported_models attributes to provide a list + of the actual model ID's that can be used. See g15driver.MODLES and other + contants. + + Keyword arguments: + plugin_module -- plugin module instance + """ + supported_models = [] + getattr(plugin_module, 'supported_models', g15driver.MODELS) + unsupported_models = getattr(plugin_module, 'unsupported_models', []) + for p in unsupported_models: + if p in supported_models: + supported_models.remove(p) + else: + logger.debug("Tried to remove '%s' not in supported_models. Ignoring...", p) + return supported_models + +def is_needs_network(plugin_module): + """ + Get if the provided plugin_module instance requires the network to be available. + If the plugin doesn't declare this, it is assumed to be False + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'needs_network', False) + +def is_default_enabled(plugin_module): + """ + Get if the provided plugin_module instance should be enabled by default. + This is used to determine a basic list of plugins to get the user going + when Gnome15 is first installed. + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'default_enabled', False) + +def is_global_plugin(plugin_module): + """ + Get if the provided plugin_module instance should is a "Global Plugin". + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'global_plugin', False) + +def is_passive_plugin(plugin_module): + """ + Get if the provided plugin_module instance should is a "Passive Plugin". + Passive plugins just provide additional classes and functions, probably + for another plugin. They are always enabled. + + Keyword arguments: + plugin_module -- plugin module instance + """ + return getattr(plugin_module, 'passive', False) + +def get_actions(plugin_module, device): + """ + Get a dictionary of all the "Actions" this plugin uses. The key is + the action ID, and the value of a textual description of what the action + is used for in this plugin. + + Keyword arguments: + plugin_module -- plugin module instance + device -- device the plugins are for + """ + actions = {} + # First look for actions for the specific device + if device is not None: + actions = getattr(plugin_module, 'actions_%s' % device.model_id, {}) + + if actions == {}: + return getattr(plugin_module, 'actions', {}) + else: + return actions + + + +""" +Loads the python modules for all plugins for all known locations. This is done +in two phases. + +Firstly, the paths of all plugins are added to the python search path. + +Secondly, all of these directories are scanned for python files with the same +name as the directory they are in. Each one of these is the main plugin module. + +TODO - These should really be using __init__.py +""" +all_plugin_directories = get_extra_plugin_dirs() + \ + list_plugin_dirs(os.path.expanduser("~/.gnome15/plugins")) + \ + list_plugin_dirs(os.path.join(g15globals.user_config_dir, "plugins")) + \ + list_plugin_dirs(os.path.join(g15globals.user_data_dir, "plugins")) + \ + list_plugin_dirs(g15globals.plugin_dir) + +# Phase 1 +for plugindir in all_plugin_directories: + if not plugindir in sys.path: + sys.path.insert(0, plugindir) + +# Phase 2 +for plugindir in all_plugin_directories: + plugin_name = os.path.basename(plugindir) + pluginfiles = [fname[:-3] for fname in os.listdir(plugindir) if fname == plugin_name + ".py"] + if not plugindir in sys.path: + sys.path.insert(0, plugindir) + try : + for mod in ([__import__(fname) for fname in pluginfiles]): + imported_plugins.append(mod) + # TODO - we need to be registering actions for a particular device + actions = get_actions(mod, None) + for a in actions: + if not a in g15actions.actions: + g15actions.actions.append(a) + except Exception as e: + logger.error("Failed to load plugin module %s.", plugindir, exc_info = e) + + +class G15Plugins(): + """ + Managed a set of plugins for either the global set, or the per device + set (in this case the screen argument must be provided). + + In total there will be n+1 instances of this, where n is the number of + connected and enabled devices. + """ + def __init__(self, screen, service=None, network_manager = None): + """ + Create a new plugin manager either for the provided device (screen), + or globally (when screen is None) + + Keyword arguments: + screen -- screen this plugin managed is attached to, or None for global plugins + service -- the service this plugin manager is managed by + """ + self.network_manager = network_manager + self.lock = threading.RLock() + self.screen = screen + self.service = service if service is not None else screen.service + self.conf_client = self.service.conf_client + self.started = [] + self.activated = [] + self.conf_client.add_dir(self._get_plugin_key(), gconf.CLIENT_PRELOAD_NONE) + self.module_map = {} + self.plugin_map = {} + self.state = UNINITIALISED + + def is_activated(self): + """ + Get if the plugin manager is currently fully ACTIVATED. + """ + return self.state == ACTIVATED + + def is_started(self): + """ + Get if the plugin manager is currently fully STARTED or in any ACTIVE + state. + """ + return self.is_in_active_state() or self.state == STARTED + + def is_in_active_state(self): + """ + Get if the plugin manager is currently in a state where it is either + fully ACTIVATED, or partially activated (ACTIVATING, DEACTIVATING). + """ + return self.state in [ ACTIVATED, DEACTIVATING, ACTIVATING ] + + def is_in_started_state(self): + """ + Get if the plugin manager is currently in a state where it is either + fully STARTED (or any activated state) or partially STARTED (STARTING, STOPPING) + """ + return self.is_in_active_state() or self.state in [ STARTED, STARTING, DESTROYING ] + + def has_plugin(self, module_id): + """ + Get if the plugin manager contains a plugin install with the + given plugin module ID. + + Keyword arguments: + module_id -- plugin module ID to search for + """ + return module_id in self.module_map + + def get_plugin(self, module_id): + """ + Get the plugin instance given the plugin module's ID + + Keyword arguments: + id -- plugin module ID to search for + """ + if module_id in self.module_map: + return self.module_map[module_id] + + def start(self): + """ + Start all plugins that are currently enabled. + """ + self.lock.acquire() + try : + self.state = STARTING + self.started = [] + added = [] + for mod in imported_plugins: + plugin_dir_key = self._get_plugin_key(mod.id) + if mod.id in added: + logger.warning("Same plugin with ID of %s is already loaded." \ + "Only the first copy will be used.", mod.id) + else: + self.conf_client.add_dir(plugin_dir_key, gconf.CLIENT_PRELOAD_NONE) + key = "%s/enabled" % plugin_dir_key + self.conf_client.notify_add(key, self._plugin_changed) + if (self.screen is None and is_global_plugin(mod)) or \ + (self.screen is not None and not is_global_plugin(mod)): + + # If first use, set the default enabled state + if self.conf_client.get(key) == None: + self.conf_client.set_bool(key, is_default_enabled(mod)) + + # Only actually activate if the plugin is not passive and the network + # is in the right state + + if self.conf_client.get_bool(key) and \ + not is_passive_plugin(mod): + try : + instance = self._create_instance(mod, plugin_dir_key) + if self.screen is None or self.screen.driver.get_model_name() in get_supported_models(mod): + self.started.append(instance) + except Exception as e: + self.conf_client.set_bool(key, False) + logger.error("Failed to load plugin %s.", mod.id, exc_info = e) + self.state = STARTED + except Exception as a: + self.state = UNINITIALISED + logger.debug("Error when starting plugins", exc_info = a) + raise a + finally: + self.lock.release() + logger.info("Started plugin manager") + + def handle_key(self, key, state, post=False): + """ + Pass the provided key event to all plugins. For each key event, this + will be called twice, once with post=False, and once with post=True + + Keyword arguments: + key -- key name + state -- key state + post -- post processing stage + """ + for plugin in self.started: + can_handle_keys = hasattr(plugin, 'handle_key') + if can_handle_keys and plugin.handle_key(key, state, post): + logger.info("Plugin %s handled key %s (%d), %s", + str(plugin), + str(key), + state, + str(post)) + return True + return False + + def activate(self, callback=None, plugin=None): + """ + Activate all plugins that currently started. + + Keyword arguments: + callback -- callback function to invoke when each invididual plugin + is activated. This is used for the progress bar during initial startup. + plugin -- If None, all started are activated. If a list, + those plugins are activated. If a single plugin, + that plugin is activated. If plugin is either list + or None, the state of the plugin manager will also + be changed + """ + + if plugin is None or isinstance(plugin, list): + logger.info("Activating plugins") + self.lock.acquire() + try : + self.state = ACTIVATING + self.activated = [] + idx = 0 + for plugin in plugin if isinstance(plugin, list) else self.started: + mod = self.plugin_map[plugin] + + # Only actually activate if the plugin is not passive and the network + # is in the right state + + needs_net = is_needs_network(mod) + if not needs_net or ( needs_net and \ + self.network_manager.is_network_available() ): + self._activate_instance(plugin, callback, idx) + + idx += 1 + self.state = ACTIVATED + except Exception as e: + self.state = STARTED + logger.debug("Error while activating plugin", exc_info = e) + raise e + finally: + self.lock.release() + logger.debug("Activated plugins") + else: + self._activate_instance(plugin, callback, 0) + + + def deactivate(self, plugin=None): + """ + De-activate plugins that are currently activated. + + Keyword arguments: + plugin -- If None, all activated are deactivated. If a list, + those plugins are deactivated. If a single plugin, + that plugin is deactivated. If plugin is either list + or None, the state of the plugin manager will also + be changed + """ + if plugin is None or isinstance(plugin, list): + logger.info("De-activating plugins") + self.lock.acquire() + try : + self.state = DEACTIVATING + for plugin in plugin if isinstance(plugin, list) else list(self.activated): + self._deactivate_instance(plugin) + finally: + self.state = DEACTIVATED + self.lock.release() + logger.info("De-activated plugins") + else: + self._deactivate_instance(plugin) + + def destroy(self): + """ + Destroy all plugins that are currently started. + """ + try : + self.state = DESTROYING + for plugin in self.started: + self.state = DESTROYING + self.started.remove(plugin) + plugin.destroy() + finally: + self.state = UNINITIALISED + + ''' + Private + ''' + def _deactivate_instance(self, plugin): + mod = self.plugin_map[plugin] + logger.debug("De-activating %s", mod.id) + if not plugin in self.activated: + raise Exception("%s is not activated" % mod.id) + try : + plugin.deactivate() + except Exception as e: + logger.warning("Failed to deactive plugin properly.", exc_info = e) + finally: + mod_id = self.plugin_map[plugin].id + if mod_id in self.service.active_plugins: + del self.service.active_plugins[mod_id] + self.activated.remove(plugin) + + def _get_plugin_key(self, subkey=None): + folder = self.screen.device.uid if self.screen is not None else "global" + if subkey: + return "/apps/gnome15/%s/plugins/%s" % (folder, subkey) + else: + return "/apps/gnome15/%s/plugins" % folder + + def _plugin_changed(self, client, connection_id, entry, args): + self.lock.acquire() + if self.screen is not None: + self.screen._check_active_plugins() + try : + path = entry.key.split("/") + plugin_id = path[5] + now_enabled = entry.value.get_bool() + plugin = get_module_for_id(plugin_id) + + # Check network state, and prevent enable if not in right state + needs_net = is_needs_network(plugin) + if now_enabled and needs_net and not self.network_manager.is_network_available(): + now_enabled = False + + instance = None + if plugin_id in self.module_map: + instance = self.module_map[plugin_id] + + if not is_passive_plugin(plugin): + if now_enabled and instance == None: + instance = self._create_instance(plugin, self._get_plugin_key(plugin_id)) + self.started.append(instance) + if self.is_in_active_state() == True: + self._activate_instance(instance) + elif not now_enabled and instance != None: + if instance in self.activated: + self._deactivate_instance(instance) + if instance in self.started: + self.started.remove(instance) + del self.module_map[plugin_id] + instance.destroy() + finally: + self.lock.release() + + def _activate_instance(self, instance, callback=None, idx=0): + mod = self.plugin_map[instance] + logger.info("Activating %s", mod.id) + try : + if self._is_single_instance(mod): + logger.info("%s may only be run once, checking if there is another instance", mod.id) + if mod.id in self.service.active_plugins: + raise Exception("Plugin may %s only run on one device at a time." % mod.id) + if callback != None: + callback(idx, len(self.started), mod.name) + instance.activate() + self.service.active_plugins[mod.id] = True + self.activated.append(instance) + except Exception as e: + logger.error("Failed to activate plugin %s.", mod.id, exc_info = e) + self.conf_client.set_bool(self._get_plugin_key("%s/enabled" % mod.id), False) + + def _is_single_instance(self, module): + return getattr(module, 'single_instance', False) + + def _create_instance(self, module, key): + logger.info("Loading %s", module.id) + if self.screen is not None: + instance = module.create(key, self.conf_client, screen=self.screen) + else: + instance = module.create(key, self.conf_client, service=self.service) + self.module_map[module.id] = instance + self.plugin_map[instance] = module + logger.info("Loaded %s", module.id) + return instance + diff --git a/src/gnome15/g15profile.py b/src/gnome15/g15profile.py new file mode 100644 index 0000000..bedac16 --- /dev/null +++ b/src/gnome15/g15profile.py @@ -0,0 +1,1173 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module contains the classes required for accessing macro details, as +well as the functions to load and save new profiles. + +A number of utility functions are also supplied to do things such as +getting the default or active profile. +""" + +import gconf +import time +import util.g15convert as g15convert +import util.g15gconf as g15gconf +import util.g15os as g15os +import util.g15icontools as g15icontools +import g15globals +import g15actions +import g15devices +import g15uinput +import g15driver +import ConfigParser +import codecs +import os.path +import stat +import pyinotify +import logging +import re +import zipfile +from cStringIO import StringIO + +logger = logging.getLogger(__name__) +active_profile = None +conf_client = gconf.client_get_default() + +''' +Watch for changes in macro configuration directory. +Observers can add a callback function to profile_listeners +to be informed when macro profiles change +''' +profile_listeners = [] + +wm = pyinotify.WatchManager() +mask = pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_CREATE | pyinotify.IN_ATTRIB # watched events + +# Create macro profiles directory +conf_dir = os.path.join(g15globals.user_config_dir, "macro_profiles") +g15os.mkdir_p(conf_dir) + +class EventHandler(pyinotify.ProcessEvent): + """ + Event handle the listens for the inotify events and informs all callbacks + that are registered in the profile_listeners variable + """ + + def _get_profile_ids(self, event): + path = os.path.basename(event.pathname) + device_uid = os.path.basename(os.path.dirname(event.pathname)) + if path.endswith(".macros") and not path.startswith("."): + id_no = path.split(".")[0] + return ( id_no, device_uid ) + + def _notify(self, event): + ids = self._get_profile_ids(event) + if ids: + for profile_listener in profile_listeners: + profile_listener(ids[0], ids[1]) + + def process_IN_MODIFY(self, event): + self._notify(event) + + def process_IN_CREATE(self, event): + self._notify(event) + + def process_IN_ATTRIB(self, event): + self._notify(event) + + def process_IN_DELETE(self, event): + self._notify(event) + +notifier = pyinotify.ThreadedNotifier(wm, EventHandler()) +notifier.name = "ProfilePyInotify" +notifier.setDaemon(True) +notifier.start() +wdd = wm.add_watch(conf_dir, mask, rec=True) + + +''' +Macro types. +''' + +MACRO_COMMAND="command" +MACRO_SIMPLE="simple" +MACRO_SCRIPT="script" +MACRO_MOUSE=g15uinput.MOUSE +MACRO_JOYSTICK=g15uinput.JOYSTICK +MACRO_DIGITAL_JOYSTICK=g15uinput.DIGITAL_JOYSTICK +MACRO_KEYBOARD=g15uinput.KEYBOARD +MACRO_ACTION="action" + +''' +Repeat modes +''' +REPEAT_TOGGLE="toggle" +NO_REPEAT="none" +REPEAT_WHILE_HELD="held" + +''' +Plugin modes +''' +NO_PLUGINS = "none" +ALL_PLUGINS = "all" +SELECTED_PLUGINS = "selected" + +""" +Defaults +""" +DEFAULT_REPEAT_DELAY = -1.0 + + +__profile_dirs = [] + +def add_profile_dir(profile_dir): + ''' + Add a new location to search for macro profiles. This allows plugins to + register their own directories and contribute new profiles. + + profile_dir -- profile directory to register + ''' + __profile_dirs.append(profile_dir) + +def remove_profile_dir(profile_dir): + ''' + Remove a location being search for macro profiles. This allows plugins to + de-register their own directories and stop contributing new profiles. + + profile_dir -- profile directory to de-register + ''' + __profile_dirs.remove(profile_dir) + +def get_profile_by_name(device, name): + """ + Get a profile given it's name. If there is more than one profile with + the same name, the first will be return. If no profile is found, None + will be returned + + Keyword arguments: + device -- device associated with profile + name -- profile name to find + """ + for profile in get_profiles(device): + if profile.name == name: + return profile + +def get_profiles(device): + ''' + Get list of all configured macro profiles for the specified device. + + Keyword arguments: + device -- device associated with profiles + ''' + profiles = [] + for profile_dir in get_all_profile_dirs(device): + if os.path.exists(profile_dir): + for profile in os.listdir(profile_dir): + if not profile.startswith(".") and profile.endswith(".macros"): + profile_id = ".".join(profile.split(".")[:-1]) + profile_object = G15Profile(device, profile_id, \ + file_path = "%s/%s" % \ + ( profile_dir, profile )) + if device.model_id in profile_object.models: + profiles.append(profile_object) + + if len(profiles) == 0: + return [ create_default(device) ] + + return profiles + +def get_all_profile_dirs(device): + """ + Get a list of all the directories profiles are searched for in. + + Keyword arguments: + device -- device + """ + dirs = list(__profile_dirs) + dirs.append(get_profile_dir(device)) + return dirs + +def create_default(device): + """ + Create the default profile for the specified device. + + Keyword arguments: + device -- device associated with default profile + """ + if not get_default_profile(device): + logger.info("No default macro profile. Creating one") + default_profile = G15Profile(device, profile_id = "Default") + default_profile.name = "Default" + default_profile.device = device + default_profile.activate_on_focus = True + default_profile.activate_on_launch = False + create_profile(default_profile) + wdd = wm.add_watch(conf_dir, mask, rec=True) + return get_default_profile(device) + +def create_profile(profile): + """ + Assign a profile object an ID, and save it to disk + + Keyword arguments: + profile -- profile to save + """ + if profile.id == None or profile.id == -1: + profile.set_id(generate_profile_id()) + logger.info("Creating profile %s, %s", profile.id, profile.name) + profile.save() + + +def generate_profile_id(): + return long(time.time()) + +def get_profile(device, profile_id): + """ + Get a profile given the device it is associated with and it's ID. The + profile will be fully loaded on return. The object returned will be a + new instance. + + Keyword arguments: + device -- device associated with profile + profile_id -- ID of profile to load + """ + for profile_dir in get_all_profile_dirs(device): + path = "%s/%s.macros" % ( profile_dir, profile_id ) + if os.path.exists(path): + return G15Profile(device, profile_id, file_path = path); + +def get_active_profile(device): + """ + Get the currently active profile for the specified device. This will + be retrieved from the configuration backend. + + Keyword arguments: + device -- device associated with profile + """ + val= conf_client.get("/apps/gnome15/%s/active_profile" % device.uid) + profile = None + if val != None and val.type == gconf.VALUE_INT: + # This is just here for compatibility with <= 0.7.x + profile = get_profile(device, str(val.get_int())) + elif val != None and val.type == gconf.VALUE_STRING: + profile = get_profile(device, val.get_string()) + + if profile is None: + profile = get_default_profile(device) + + if profile is None: + profile = create_default(device) + profile.make_active() + + return profile + +def is_locked(device): + """ + Get if the active profile is "locked" or if it may be changed for the specified device. + + Keyword arguments: + device -- device associated with profile + """ + return g15gconf.get_bool_or_default(conf_client, "/apps/gnome15/%s/locked" % device.uid, False) + +def set_locked(device, locked): + """ + Set if the active profile is 'locked', or if it may be changed + for the specified device. + + Keyword arguments: + device -- device associated with profile + locked -- lock statue + """ + conf_client.set_bool("/apps/gnome15/%s/locked" % device.uid, locked) + +def get_default_profile(device): + """ + Get the default profile for the specified device. + + Keyword arguments: + device -- device associated with default profile + """ + old_default = get_profile(device, "0") + if old_default is not None: + return old_default + return get_profile(device, "Default") + +def get_keys_from_key(key_list_key): + """ + Utility function to convert the string format of the list of keys used + in profile storage into a list of key codes as defined in g15driver + + Keyword arguments: + key_list_key -- string of key sequence required to activate macro + """ + return key_list_key.split("_") + +def get_keys_key(keys): + """ + Utility function function to convert the list of key codes as defined + in g15driver into the key list string used in profile storage. + + Keyword arguments: + keys -- list of key codes to convert to string + """ + return "_".join(keys) + +def get_profile_dir(device): + """ + Get the directory profiles for a particular device are stored. + + Keyword arguments: + device -- device + """ + return "%s/%s" % (conf_dir, device.uid) + + +def is_uinput_type(macro_type): + """ + Get if the macro type is a uinput mapping type + + Keyword arguments: + macro_type -- macro type + """ + return macro_type in [ MACRO_MOUSE, \ + MACRO_KEYBOARD, \ + MACRO_JOYSTICK, \ + MACRO_DIGITAL_JOYSTICK ] + +def find_profile_for_command(args, device): + """ + Searchs for a profile that is associated with a particular command. When + the command is launched through g15-launch, the desktop service will + call the function to find the profile to launch the command under. See + G15Profile.launch() for details on launching applications through + Gnome15. + + Keyword arguments: + device -- device + args -- list of arguments the command was launched with. The + first argument is the either the executable name (when the + executable is on the PATH, or the full path) + full path to the executable + """ + + + """ + First reformat the arguments so they are all wrapped in single quotes. + They shell that called g15-launch would have already expanded any + variables or filepaths that exist, so let's use single quotes + """ + command_line = "" + for a in args: + if len(command_line) > 0: + command_line += " " + command_line += "'" + a + "'" + + logger.info("Processed command '%s'", command_line) + + for p in get_profiles(device): + if p.can_launch(command_line): + return p + +def to_key_state_name(key_state_id): + """ + Return an english representation of a key state code + + Keyword arguments: + key_state_id -- key state ID (g15driver.KEY_STATE_UP, .. DOWN and HELD) + """ + return "Up" if key_state_id == g15driver.KEY_STATE_UP else \ + ( "Down" if key_state_id == g15driver.KEY_STATE_DOWN else "Held" ) + +def clone_macro(macro): + """ + Clone a macro + + Keyword arguments: + macro -- macro to clone + """ + m = G15Macro(macro.profle, macro.memory, macro.key_list_key, macro.activate_on) + m.name = macro.name + m.macro = macro.macro + m.repeat_mode = macro.repeat_mode + m.type = macro.type + m.repeat_delay = macro.repeat_delay + return m + + +class G15Macro(object): + """ + Represents a single macro in a profile. A macro defines how it's used + using the 'type', which may be one of MACRO_COMMAND, MACRO_SIMPLE, + MACRO_SCRIPT, MACRO_MOUSE, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK, + MACRO_KEYBOARD or MACRO_ACTION + """ + def __init__(self, profile, memory, key_list_key, activate_on): + """ + Constructor + + Keyword arguments: + profile -- parent profile object + memory -- memory bank this macro exists in + key_list_key -- string representation of keys required to activate macro + activate_on -- whether to activate on RELEASE or when HELD + """ + if profile is None: + raise Exception("No profile provided") + + self.profile = profile + self.memory = memory + self.key_list_key = key_list_key + self.activate_on = activate_on + + self.keys = key_list_key.split("_") + self.name = "" + self.macro = "" + self.repeat_mode = REPEAT_WHILE_HELD + self.type = MACRO_SCRIPT + self.repeat_delay = DEFAULT_REPEAT_DELAY + section_name = "m%d" % self.memory + if not self.profile.parser.has_section(section_name): + self.profile.parser.add_section(section_name) + + def is_uinput(self): + """ + Get if the macro type is a uinput mapping type + + Keyword arguments: + macro_type -- macro type + """ + return is_uinput_type(self.type) + + def compare(self, o): + """ + Compare this macro with another for sorting purposes. Macros will + be ordered with the G keys being first in numeric order, followed by + the memory bank keys in number order (MR is last), followed by the + 'L1' - 'L5' keys and finally all other keys ordered alphabetically. + + Keyword arguments: + o -- macro to compare this macro to + """ + return self._get_total(self.keys) - self._get_total(o.keys) + + def get_uinput_code(self): + """ + Get the uinput code of the key this macro is mapped to. If this + macro is not of a type that maps to a uinput key, an exception + will be thrown + """ + if not self.type in [ MACRO_MOUSE, MACRO_KEYBOARD, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK ]: + raise Exception("Macro of type %s, is not a type that maps to a uinput code." % self.type) + return g15uinput.capabilities[self.macro][1] if self.macro in g15uinput.capabilities else 0 + + def set_keys(self, keys): + """ + Set the list of keys this macro requires to activate. + + Keyword arguments: + keys -- list of keys required to activate macro + """ + section_name = "m%d" % self.memory + self.profile._delete_key(section_name, self.key_list_key) + self.keys = keys + self.key_list_key = get_keys_key(keys) + + def save(self): + """ + Save this macro. This triggers the whole profile that contains the + macro to be saved as well. + """ + self._store() + self.profile.save() + + def delete(self): + """ + Delete this macro + """ + self.profile.delete_macro(self.memory, self.key_list_key) + + def set_activate_on(self, new_activate_on): + """ + Changes the Activate On mode (i.e. when released or when held). This + function should be used rather than just modifying the property, as + the parent profile needs to be adjusted as well + + Keyword arguments: + new_activate_on -- new activate on ID + """ + current_list = self.profile.macros[self.activate_on][self.memory - 1] + current_list.remove(self) + self.profile._delete_key(self._get_section_name(), self.key_list_key) + self.activate_on = new_activate_on + self.profile.macros[self.activate_on][self.memory - 1].append(self) + + + """ + Private + """ + + def _remove_option(self, section_name, option_key): + if self.profile.parser.has_option(section_name, option_key): + self.profile.parser.remove_option(section_name, option_key) + + def _get_section_name(self): + return self.profile._get_section_name(self.activate_on, self.memory) + + def _store(self): + section_name = self._get_section_name() + pk = "keys_%s" % self.key_list_key + self.profile.parser.set(section_name, "%s_name" % pk, self._encode_val(self.name)) + self.profile.parser.set(section_name, "%s_type" % pk, self.type) + + if self.repeat_mode == REPEAT_WHILE_HELD: + self.profile._remove_if_exists("%s_repeatmode" % pk, section_name) + else: + self.profile.parser.set(section_name, "%s_repeatmode" % pk, self.repeat_mode) + if self.repeat_delay == -1: + self.profile._remove_if_exists("%s_repeatdelay" % pk, section_name) + else: + self.profile.parser.set(section_name, "%s_repeatdelay" % pk, self.repeat_delay) + + if self.profile.version == 1.0: + + if self.type in [ MACRO_KEYBOARD, MACRO_JOYSTICK, MACRO_DIGITAL_JOYSTICK, MACRO_MOUSE ]: + self.profile.parser.set(section_name, "%s_type" % pk, "mapped-to-key") + self.profile.parser.set(section_name, "%s_maptype" % pk, self.type) + self.profile.parser.set(section_name, "%s_mappedkey" % pk, self.macro) + self.profile._remove_if_exists("%s_command" % pk, section_name) + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + self.profile._remove_if_exists("%s_macro" % pk, section_name) + self.profile._remove_if_exists("%s_action" % pk, section_name) + else: + self.profile._remove_if_exists("%s_mappedkey" % pk, section_name) + self.profile._remove_if_exists("%s_maptype" % pk, section_name) + if self.type == MACRO_COMMAND: + self.profile.parser.set(section_name, "%s_command" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_command" % pk, section_name) + if self.type == MACRO_SIMPLE: + self.profile.parser.set(section_name, "%s_simplemacro" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + if self.type == MACRO_SCRIPT: + self.profile.parser.set(section_name, "%s_macro" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_macro" % pk, section_name) + + """ + Actions aren't actually supported in < 0.8, but store it in it's + own field anyway. Earlier versions will just not support that + macro + """ + if self.type == MACRO_ACTION: + self.profile.parser.set(section_name, "%s_action" % pk, self._encode_val(self.macro)) + else: + self.profile._remove_if_exists("%s_action" % pk, section_name) + else: + """ + Store in the new more compact version 2.0 format + """ + self.profile.parser.set(section_name, "%s_macro" % pk, self._encode_val(self.macro)) + self.profile._remove_if_exists("%s_maptype" % pk, section_name) + self.profile._remove_if_exists("%s_mappedkey" % pk, section_name) + self.profile._remove_if_exists("%s_command" % pk, section_name) + self.profile._remove_if_exists("%s_simplemacro" % pk, section_name) + self.profile._remove_if_exists("%s_action" % pk, section_name) + + def _encode_val(self, val): + val = val.encode('utf8') + return val + + def _decode_val(self, val): + return val + + def _load(self): + self.type = self._get("type", MACRO_SCRIPT) + self.macro = self._decode_val(self._get("macro", "")) + self.name = self._decode_val(self._get("name", "")) + self.repeat_mode = self._decode_val(self._get("repeatmode", REPEAT_WHILE_HELD)) + self.repeat_delay = float(self._get("repeatdelay", DEFAULT_REPEAT_DELAY)) + if self.type == "mapped-to-key": + self.macro = self._get("mappedkey", "") + self.type = self._get("maptype", "") + elif self.profile.version == 1.0: + if self.type == MACRO_COMMAND: + self.macro = self._decode_val(self._get("command", "")) + elif self.type == MACRO_SIMPLE: + self.macro = self._decode_val(self._get("simplemacro", "")) + elif self.type == MACRO_ACTION: + """ + Actions aren't actually supported in < 0.8, but this is how + it's stored when the profile is in 1.0 mode. + """ + self.macro = self._decode_val(self._get("action", "")) + + def _get(self, key, default_value): + section_name = self._get_section_name() + option_key = "keys_" + self.key_list_key + "_" + key + return self.profile.parser.get(section_name, option_key) if self.profile.parser.has_option(section_name, option_key) else default_value + + def __ne__(self, macro): + return not self.__eq__(macro) + + def __eq__(self, macro): + try: + return macro is not None and self.profile.id == macro.profile.id and self.key_list_key == macro.key_list_key and self.activate_on == macro.activate_on + except AttributeError as e: + logger.debug("Error when reading a macro attribute", exc_info = e) + return False + + def _get_total(self, keys): + t = 0 + for i in range(0, len(keys)): + if keys[i] != "": + t += self._get_key_val(keys[i]) + return t + + def _get_key_val(self, key): + if(key == ""): + return 0 + elif re.match("g[0-9]+.*", key): + return int(key[1:]) + elif re.match("m[1-3]", key): + return 50 + int(key[1:]) + elif key == "mr": + return 55 + elif re.match("l[0-9]+.*", key): + return 100 + int(key[1:]) + else: + ki = self.profile.device.get_key_index(key) + if ki is None: + ki = 200 + return 200 + ki + + def __repr__(self): + return "[Macro %d/%s (%s) [%s]" % ( self.memory, self.name, self.key_list_key, to_key_state_name(self.activate_on) ) + +class G15Profile(object): + """ + Encapsulates a single macro profile with 3 memory banks. This object + contains all the general information about the profile, as well as the + list of macros themselves. + """ + + def __init__(self, device, profile_id=None, file_path = None): + """ + Constructor + + Keyword arguments: + device -- device the profile is associated with + id -- profile ID + """ + + + self.device = device + self.read_only = False + self.parser = ConfigParser.ConfigParser({ + }) + self.name = None + self.icon = None + self.background = None + self.filename = None + self.id = -1 + if profile_id is not None: + self.set_id(profile_id) + if file_path is not None: + self.filename = file_path + self.author = "" + self.macros = { g15driver.KEY_STATE_UP: [], + g15driver.KEY_STATE_DOWN: [], + g15driver.KEY_STATE_HELD: [] + } + self.mkey_color = {} + self.activate_on_focus = False + self.activate_on_launch = False + self.launch_pattern = None + self.monitor = [ "stdout" ] + self.models = [ device.model_id ] + self.window_name = "" + self.base_profile = None + self.version = 2.1 + self.plugins_mode = ALL_PLUGINS + self.selected_plugins = [] + + self.load(self.filename) + + def can_launch(self, command_line): + """ + Test if this profile can launch a command with the provided arguments, + monitoring it's output (or other log files) for output, and produce + events and extract information that may be used by a "Game Theme" or "Game Plugin" + + Keyword arguments: + command_line -- command line to match against. this should have + each argument wrapped in quotes for consistency. + """ + return re.search(self.launch_pattern, command_line) + + def export(self, filename): + """ + Save this profile in a format that may be transmitted to another + computer (as a zip file). All references to external images (for icon and background) + are made relative and added to the archive. + + Keyword arguments: + filename -- file to save copy to + """ + profile_copy = get_profile(self.device, self.id) + + archive_file = zipfile.ZipFile(filename, "w", compression = zipfile.ZIP_DEFLATED) + try: + # Icon + if profile_copy.icon and os.path.exists(profile_copy.icon): + base_path = "%s.resources/%s" % ( profile_copy.id, os.path.basename(profile_copy.icon) ) + archive_file.write(profile_copy.icon, base_path ) + profile_copy.icon = base_path + + # Background + if profile_copy.background and os.path.exists(profile_copy.background): + base_path = "%s.resources/%s" % ( profile_copy.id, os.path.basename(profile_copy.background) ) + archive_file.write(profile_copy.background, base_path) + profile_copy.background = base_path + + # Profile + profile_data = StringIO() + try: + profile_copy.save(profile_data) + archive_file.writestr("%s.macros" % profile_copy.id, profile_data.getvalue()) + finally: + profile_data.close() + finally: + archive_file.close() + + def are_keys_in_use(self, activate_on, memory, keys, exclude = None): + """ + Get if the specified keys are currently in use for a macro in the + supplied memory bank number. Optionally, a list of macros that + should be excluded from the search can be supplied (usually used + to exclude the current macro when checking if other macros currently + use a set of keys) + + Keyword arguments: + activate_on -- the key state to activate the macro on + memory -- memory bank number + keys -- keys to search for + exclude -- list of macro objects to exclude + """ + bank = self.macros[activate_on][memory - 1] + for macro in bank: + if ( exclude == None or ( exclude != None and not self._is_excluded(exclude, macro) ) ) and sorted(keys) == sorted(macro.keys): + return True + return False + + def get_default(self): + """ + Get if this profile is the default one + """ + return self == get_default_profile(self.device) + + def save(self, filename = None): + """ + Save this profile to disk + """ + if self.read_only: + raise Exception("Cannot write to read-only profile") + logger.info("Saving macro profile %s, %s", self.id, self.name) + if filename is None: + filename = self.filename + if self.window_name == None: + self.window_name = "" + if self.icon == None: + self.icon = "" + + # Set the profile options + self.parser.set("DEFAULT", "name", self.name) + self.parser.set("DEFAULT", "version", str(self.version)) + self.parser.set("DEFAULT", "icon", self.icon) + self.parser.set("DEFAULT", "window_name", self.window_name) + if self.version == 1.0: + self.parser.set("DEFAULT", "base_profile", str(self.base_profile) if self.base_profile is not None else "-1") + else: + self.parser.set("DEFAULT", "base_profile", str(self.base_profile) if self.base_profile is not None else "") + self.parser.set("DEFAULT", "icon", self.icon) + self.parser.set("DEFAULT", "background", self.background) + self.parser.set("DEFAULT", "author", self.author) + self.parser.set("DEFAULT", "activate_on_focus", str(self.activate_on_focus)) + self.parser.set("DEFAULT", "plugins_mode", str(self.plugins_mode)) + self.parser.set("DEFAULT", "selected_plugins", ",".join(self.selected_plugins)) + self.parser.set("DEFAULT", "send_delays", str(self.send_delays)) + self.parser.set("DEFAULT", "fixed_delays", str(self.fixed_delays)) + self.parser.set("DEFAULT", "press_delay", str(self.press_delay)) + self.parser.set("DEFAULT", "release_delay", str(self.release_delay)) + self.parser.set("DEFAULT", "models", ",".join(self.models)) + + # Set the launch options + if self.launch_pattern is not None: + self.parser.set("LAUNCH", "pattern", self.launch_pattern) + self.parser.set("LAUNCH", "monitor", ",".join(self.monitor)) + self.parser.set("LAUNCH", "activate_on_launch", str(self.activate_on_launch)) + else: + self._remove_if_exists("pattern", "LAUNCH") + self._remove_if_exists("monitor", "LAUNCH") + self._remove_if_exists("activate_on_launch", "LAUNCH") + + # Remove and re-add the bank sections + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + section_name = "m%d" % i + if activate_on != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, activate_on ) + if not self.parser.has_section(section_name): + self.parser.add_section(section_name) + col = self.mkey_color[i] if i in self.mkey_color else None + if col: + self.parser.set(section_name, "backlight_color", g15convert.rgb_to_string(col)) + elif self.parser.has_option(section_name, "backlight_color"): + self.parser.remove_option(section_name, "backlight_color") + + # Add the macros + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + for macro in self.get_sorted_macros(activate_on, i): + if len(macro.keys) > 0: + macro._store() + + self._write(filename) + + def set_id(self, profile_id): + self.id = str(profile_id) + self.read_only = False + self.filename = "%s/%s/%s.macros" % ( conf_dir, self.device.uid, self.id ) + + def get_binding_for_action(self, activate_on, action_name): + """ + Get an ActionBinding if this profile contains a map to the supplied + action name. + + Keyword arguments: + activate_on -- the key state to activate the macro on + action_name -- name of action + """ + for bank in self.macros[activate_on]: + for m in bank: + if m.type == MACRO_ACTION and m.macro == action_name and activate_on == m.activate_on: + # TODO held actions? + return g15actions.ActionBinding(action_name, m.keys, g15driver.KEY_STATE_UP) + + def set_mkey_color(self, memory, rgb): + """ + Set a tuple containing the red, green and blue values of the colour + to use when the specifed bank is active + + Keyword arguments: + memory -- memory bank number + rgb -- colour to assign to bank + """ + self.mkey_color[memory] = rgb + + def get_mkey_color(self, memory): + """ + Get a tuple contain the red, green and blue values of the colour + to use when the specifed bank is active + + Keyword arguments: + memory -- memory bank number + """ + return self.mkey_color[memory] if memory in self.mkey_color else None + + def delete(self): + """ + Delete this macro profile + """ + os.remove(self.filename) + + def delete_macro(self, activate_on, memory, keys): + """ + Delete the macro that is activated by the specified keys in the + supplied memory bank number + + Keyword arguments: + activate_on -- key state to activate the macro on + memory -- memory bank number (starts at 1) + keys -- keys that activate the macro + """ + section_name = self._get_section_name(activate_on, memory) + key_list_key = get_keys_key(keys) + logger.info("Deleting macro M%d, for %s", memory, key_list_key) + self._delete_key(section_name, key_list_key) + self._write(self.filename) + bank_macros = self.macros[activate_on][memory - 1] + for macro in bank_macros: + if macro.key_list_key == key_list_key and macro in bank_macros: + bank_macros.remove(macro) + + def get_profile_icon_path(self, height): + """ + Get the icon for the profile. This will either be a specific icon + path, or if none is available, the default profile icon. If the + icon is a themed icon name, then that icon will be searched for and + the full path returned + + Keyword arguments: + height -- preferred height + """ + icon = self.icon + if icon is not None and icon.startswith("/"): + return icon + + path = self.get_resource_path(icon) + if path is None: + if icon == None or icon == "": + icon = [ "preferences-desktop-keyboard-shortcuts", "preferences-desktop-keyboard" ] + + return g15icontools.get_icon_path(icon, height) + + return path + + def get_resource_path(self, resource_name): + """ + Get the full path of a resource (i.e. a path relative to the location + of the profile's file. None will be returned if no such resource exists + + Keyword arguments: + resource_name -- resource name + """ + if resource_name is not None and resource_name != "": + if resource_name.startswith("/"): + return resource_name + if self.filename is not None: + path = os.path.join(os.path.dirname(self.filename), resource_name) + if os.path.exists(path): + return path + + def create_macro(self, memory, keys, name, macro_type, macro, activate_on): + """ + Create a new macro + + Keyword arguments: + memory -- memory bank number (starts at 1) + keys -- list of keys that activate the macro + name -- name of macro + type -- macro type + macro -- content of macro + """ + key_list_key = get_keys_key(keys) + logger.info("Creating macro M%d, for %s", memory, key_list_key) + new_macro = G15Macro(self, memory, key_list_key, activate_on) + new_macro.name = name + new_macro.type = macro_type + new_macro.macro = macro + self.macros[activate_on][memory - 1].append(new_macro) + new_macro.save() + return new_macro + + def get_macro(self, activate_on, memory, keys): + """ + Get the macro given the memory bank number and the list of keys + the macro requires to activate + + Keyword arguments: + activate_on -- the key state to activate the macro on + memory -- memory bank number (starts at 1) + keys -- list of keys that activate the macro + """ + bank = self.macros[activate_on][memory - 1] + for macro in bank: + key_count = 0 + for k in macro.keys: + if k in keys: + key_count += 1 + if key_count == len(macro.keys) and key_count == len(keys): + return macro + + def is_active(self): + """ + Get if this profile is the currently active one + """ + active = get_active_profile(self.device) + return active is not None and self.id == active.id + + def make_active(self): + """ + Make this the currently active profile. An Exception will be raised + if the profile is currently locked for this device + """ + if is_locked(self.device): + raise Exception("Cannot change active profile when locked.") + + + conf_client.set_string("/apps/gnome15/%s/active_profile" % self.device.uid, str(self.id)) + + def load(self, filename = None, fd = None): + """ + Load the profile from disk + """ + + # Initial values + self.macros = { g15driver.KEY_STATE_UP: [], + g15driver.KEY_STATE_DOWN: [], + g15driver.KEY_STATE_HELD: [] + } + self.mkey_color = {} + + # Load macro file + if self.id != -1 or filename is not None or fd is not None: + if ( isinstance(filename, str) or isinstance(filename, unicode) ) and os.path.exists(filename): + self.read_only = not os.stat(filename)[0] & stat.S_IWRITE + self.parser.readfp(codecs.open(filename, "r", "utf8")) + elif fd is not None: + self.read_only = True + self.parser.readfp(fd) + else: + self.read_only = False + + # Macro file format version. Try to keep macro files backwardly and + # forwardly compatible + if self.parser.has_option("DEFAULT", "version"): + self.version = float(self.parser.get("DEFAULT", "version").strip()) + else: + self.version = 1.0 + + # Info section + self.name = self.parser.get("DEFAULT", "name").strip() if self.parser.has_option("DEFAULT", "name") else "" + self.icon = self.parser.get("DEFAULT", "icon").strip() if self.parser.has_option("DEFAULT", "icon") else "" + self.background = self.parser.get("DEFAULT", "background").strip() if self.parser.has_option("DEFAULT", "background") else "" + self.author = self.parser.get("DEFAULT", "author").strip() if self.parser.has_option("DEFAULT", "author") else "" + self.window_name = self.parser.get("DEFAULT", "window_name").strip() if self.parser.has_option("DEFAULT", "window_name") else "" + self.models = self.parser.get("DEFAULT", "models").strip().split(",") if self.parser.has_option("DEFAULT", "models") else [ self.device.model_id ] + self.plugins_mode = self.parser.get("DEFAULT", "plugins_mode").strip() if self.parser.has_option("DEFAULT", "plugins_mode") else ALL_PLUGINS + self.selected_plugins = self.parser.get("DEFAULT", "selected_plugins").strip().split(",") \ + if self.parser.has_option("DEFAULT", "selected_plugins") else [ ] + + self.activate_on_focus = self.parser.getboolean("DEFAULT", "activate_on_focus") if self.parser.has_option("DEFAULT", "activate_on_focus") else False + self.send_delays = self.parser.getboolean("DEFAULT", "send_delays") if self.parser.has_option("DEFAULT", "send_delays") else False + self.fixed_delays = self.parser.getboolean("DEFAULT", "fixed_delays") if self.parser.has_option("DEFAULT", "fixed_delays") else False + + self.base_profile = self.parser.get("DEFAULT", "base_profile").strip() if self.parser.has_option("DEFAULT", "base_profile") else "" + if self.base_profile == "-1": + # For version 1.0 profile format compatibility + self.base_profile = None + + self.press_delay = self._get_int("press_delay", 50) + self.release_delay = self._get_int("release_delay", 50) + + # Launch + self.launch_pattern = self.parser.get("LAUNCH", "pattern").strip() \ + if self.parser.has_option("LAUNCH", "pattern") else None + self.monitor = self.parser.get("LAUNCH", "monitor").strip().split(",") \ + if self.parser.has_option("LAUNCH", "monitor") else [ "stdout" ] + self.activate_on_launch = self.parser.getboolean("LAUNCH", "activate_on_launch") \ + if self.parser.has_option("LAUNCH", "activate_on_launch") else False + + # Bank sections + + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + for i in range(1, 4): + section_name = "m%d" % i + if activate_on != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, activate_on ) + if not self.parser.has_section(section_name): + self.parser.add_section(section_name) + self.mkey_color[i] = g15convert.to_rgb(self.parser.get(section_name, "backlight_color")) if self.parser.has_option(section_name, "backlight_color") else None + memory_macros = [] + self.macros[activate_on].append(memory_macros) + for option in self.parser.options(section_name): + if option.startswith("keys_") and option.endswith("_name"): + key_list_key = option[5:-5] + macro_obj = G15Macro(self, i, key_list_key, activate_on) + macro_obj._load() + memory_macros.append(macro_obj) + + def get_sorted_macros(self, activate_on, memory_number): + """ + Get the list of macros sorted + + Keyword arguments: + activate_on -- the state the macro is activated on + memory_number -- memory bank number to retrieve macros from (starts at 1) + """ + sm = [] + if activate_on is None: + for activate_on in [ g15driver.KEY_STATE_UP, g15driver.KEY_STATE_DOWN, g15driver.KEY_STATE_HELD ]: + if activate_on in self.macros and memory_number <= len(self.macros[activate_on]): + sm += self.macros[activate_on][memory_number - 1] + else: + if activate_on in self.macros and memory_number <= len(self.macros[activate_on]): + sm += self.macros[activate_on][memory_number - 1] + sm.sort(self._comparator) + return sm + + ''' + Private + ''' + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _remove_if_exists(self, name, section = "DEFAULT"): + if self.parser.has_option(section, name): + self.parser.remove_option(section, name) + + def _get_section_name(self, state, memory): + section_name = "m%d" % memory + if state != g15driver.KEY_STATE_UP: + section_name = "%s-%s" % ( section_name, state ) + return section_name + + def _get_int(self, name, default_value, section = "DEFAULT"): + try: + return self.parser.getint(section, name) if self.parser.has_option(section, name) else default_value + except ValueError as v: + logger.debug("Error when parsing a integer value", exc_info = v) + return default_value + + def __ne__(self, profile): + return not self.__eq__(profile) + + def __eq__(self, profile): + return profile is not None and self.id == profile.id + + def _write(self, save_file = None): + + if save_file is None or self.id == -1: + raise Exception("Cannot save a profile without a filename or an id.") + + if isinstance(save_file, str): + dir_name = os.path.dirname(save_file) + if not os.path.exists(dir_name): + os.mkdir(dir_name) + tmp_file = "%s.tmp" % save_file + with open(tmp_file, 'wb') as configfile: + self.parser.write(configfile) + os.rename(tmp_file, save_file) + fhandle = file(save_file, 'a') + try: + os.utime(save_file, None) + finally: + fhandle.close() + else: + self.parser.write(save_file) + + def _delete_key(self, section_name, key_list_key): + for option in self.parser.options(section_name): + if option.startswith("keys_" + key_list_key + "_"): + self.parser.remove_option(section_name, option) + + def _is_excluded(self, excluded, macro): + for e in excluded: + if e == macro: + return True \ No newline at end of file diff --git a/src/gnome15/g15screen.py b/src/gnome15/g15screen.py new file mode 100644 index 0000000..6bb8197 --- /dev/null +++ b/src/gnome15/g15screen.py @@ -0,0 +1,1593 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +import gnome15.g15devices as g15devices +#from gnome15 import g15pluginmanager +_ = g15locale.get_translation("gnome15").ugettext + +""" +Queues +""" +REDRAW_QUEUE = "redrawQueue" + +""" +Page priorities +""" +PRI_POPUP = 999 +PRI_EXCLUSIVE = 100 +PRI_HIGH = 99 +PRI_NORMAL = 50 +PRI_LOW = 20 +PRI_INVISIBLE = 0 + +""" +Paint stages +""" +BACKGROUND_PAINTER = 0 +FOREGROUND_PAINTER = 1 + +""" +Simple colors +""" +COLOURS = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255), (255, 255, 255)] + +import g15driver +import util.g15scheduler as g15scheduler +import util.g15pythonlang as g15pythonlang +import util.g15gconf as g15gconf +import util.g15cairo as g15cairo +import util.g15icontools as g15icontools +import g15profile +import g15globals +import g15drivermanager +import g15keyboard +import g15theme +import g15actions +import time +import threading +import cairo +import gconf +import os.path +import sys +import logging +from threading import RLock +from g15exceptions import NotConnectedException +from g15exceptions import RetryException +logger = logging.getLogger(__name__) + +""" +This module contains the root component for a single device (i.e. the 'screen'), and all +of the supporting classes. The screen object is responsible for maintaining the connection +to the driver, starting and stopping all the device's plugins, tracking the current memory +bank, performing the actual painting for the associated device and more. + +To this screen, 'pages' will be added by plugins and other subsystems. Only a single page +is ever visible at one time, and the screen is responsible for switching between them. +You can think of the screen as the window manager. +""" + +def check_on_redraw(): + """ + Helper to check the current thread is the redraw thread + """ +# if not jobqueue.is_on_queue(REDRAW_QUEUE): +# raise Exception("Illegal thread access (on queue %s)." % jobqueue.get_current_queue()) + pass + +def run_on_redraw(cb, *args): + """ + Helper to run a callback function on the redraw queue. + """ + g15scheduler.queue(REDRAW_QUEUE, "Redraw", 0, cb, *args) + +class ScreenChangeAdapter(): + """ + Adapter class for screen change listeners to save such listeners having to + implement all callbacks, just override the ones you want + """ + + def memory_bank_changed(self, new_bank_number): + """ + Call when the current memory bank changes + + Keyword arguments: + new_bank_number -- new memory bank number + """ + pass + + def attention_cleared(self): + """ + Called when the screen is no longer in attention state (i.e. the + error has been cleared) + """ + pass + + def attention_requested(self, message): + """ + Called when the screen has some problem and needs attention (e.g. + driver problem) + + Keyword arguments: + message -- message detailing problem + """ + pass + + def driver_disconnected(self, driver): + """ + Called when the underlying driver is disconnected. + + Keyword arguments: + driver -- driver that disconnected + """ + pass + + def driver_connected(self, driver): + """ + Called when the underlying driver is connected. + + Keyword arguments: + driver -- driver that connected + """ + pass + + def driver_connection_failed(self, driver): + """ + Called when the underlying driver connection fails. + + Keyword arguments: + driver -- driver that connected + exception -- exception + """ + pass + + def deleting_page(self, page): + """ + Called when a page is about to be removed from screen + + Keyword arguments: + page -- page that is to be removed + """ + pass + + def deleted_page(self, page): + """ + Called when a page has been removed from screen + + Keyword arguments: + page -- page that has been removed + """ + pass + + def new_page(self, page): + """ + Called when a page is added to the screen + + Keyword arguments: + page -- page that has been added + """ + pass + + def title_changed(self, page, title): + """ + Called when the title of page changes + + Keyword arguments: + page -- page that has changed + title -- title new title + """ + pass + + def page_changed(self, page): + """ + Called when the page changes in some other way (e.g. priority) + + Keyword arguments: + page -- page that has changed + """ + pass + + + +class KeyState(): + """ + Holds the current state of a single macro key + """ + def __init__(self, key): + self.key = key + self.state_id = None + self.timer = None + + def cancel_timer(self): + """ + Cancel the HELD timer if one exists. + """ + if self.timer is not None: + self.timer.cancel() + self.timer = None + + def __repr__(self): + return "%s = %s" % (self.key, g15profile.to_key_state_name(self.state_id)) + +class Painter(): + """ + Painters may be added to screens to draw stuff either beneath (BACKGROUND_PAINTER) + or above (FOREGROUND_PAINTER) the main component (i.e. the currently visible page). + Each painter also has z-order which determines when it is painted in relation to + other painters of the same place. + """ + + + def __init__(self, place=BACKGROUND_PAINTER, z_order=0): + """ + Constructor + + Keyword arguments: + place -- either BACKGROUND_PAINTER or FOREGROUND_PAINTER + z_order -- the order the painter is called within the place + """ + self.z_order = z_order + self.place = place + + def paint(self, canvas): + """ + Subclasses must override to do the actual painting + + Keyword arguments: + canvas -- canvas + """ + raise Exception("Not implemented") + + +class G15Screen(): + + def __init__(self, plugin_manager_module, service, device): + self.service = service + self.plugin_manager_module = plugin_manager_module + self.device = device + self.driver = None + self.screen_change_listeners = [] + self.local_data = threading.local() + self.local_data.surface = None + self.plugins = [] + self.conf_client = service.conf_client + self.notify_handles = [] + self.connection_lock = RLock() + self.draw_lock = RLock() + self.defeat_profile_change = 0 + self.first_page = None + self.attention_message = g15globals.name + self.attention = False + self.splash = None + self.reschedule_lock = RLock() + self.last_error = None + self.loading_complete = False + self.control_handles = [] + self.color_no = 1 + self.cycle_timer = None + self._started_plugins = False + self.stopping = False + self.reconnect_timer = None + self.plugins = self.plugin_manager_module.G15Plugins(self, network_manager = service.network_manager) + self.pages = [] + self.memory_bank_color_control = None + self.acquired_controls = {} + self.painters = [] + self.fader = None + self.mkey = 1 + self.temp_acquired_controls = {} + self.key_handler = g15keyboard.G15KeyHandler(self) + self.glass_pane = g15theme.Component("glasspane") + + if not self._load_driver(): + raise Exception("Driver failed to load") + + def set_active_application_name(self, application_name): + """ + Set the currently active application (may be a window name or a high + level application name). Returns a boolean indicating whether or not + a profile that matches was found + + Keyword arguments: + application_name -- application name + splash -- splash callback + startup -- True when this change is the result of startup + """ + if self.device is None: + return False + + found = False + if self.defeat_profile_change < 1 and not g15profile.is_locked(self.device): + choose_profile = None + # Active window has changed, see if we have a profile that matches it + if application_name is not None: + for profile in g15profile.get_profiles(self.device): + if not profile.get_default() and profile.activate_on_focus and len(profile.window_name) > 0 and application_name.lower().find(profile.window_name.lower()) != -1: + choose_profile = profile + break + + # No applicable profile found. Look for a default profile, and see if it is set to activate by default + active_profile = g15profile.get_active_profile(self.device) + if choose_profile == None: + default_profile = g15profile.get_default_profile(self.device) + + if (active_profile == None or active_profile.id != default_profile.id) and default_profile.activate_on_focus: + default_profile.make_active() + found = True + elif active_profile == None or choose_profile.id != active_profile.id: + choose_profile.make_active() + found = True + + return found + + def start(self): + logger.info("Starting %s.", self.device.uid) + + # Remove previous fader is it exists + if self.fader: + self.painters.remove(self.fader) + self.fader = None + + # Start the driver + self.attempt_connection() + + # Start handling keys + self.key_handler.start() + self.key_handler.action_listeners.append(self) + self.key_handler.action_listeners.append(self.service.macro_handler) + self.key_handler.key_handlers.append(self) + self.key_handler.key_handlers.append(self.service.macro_handler) + + # This is just here for backwards compatibility and may be removed at some point + self.action_listeners = self.key_handler.action_listeners + + # Monitor gconf + screen_key = "/apps/gnome15/%s" % self.device.uid + logger.info("Watching GConf settings in %s", screen_key) + self.conf_client.add_dir(screen_key, gconf.CLIENT_PRELOAD_NONE) + self.notify_handles.append(self.conf_client.notify_add("%s/cycle_screens" % screen_key, self.resched_cycle)) + self.notify_handles.append(self.conf_client.notify_add("%s/active_profile" % screen_key, self.active_profile_changed)) + self.notify_handles.append(self.conf_client.notify_add("%s/driver" % screen_key, self.driver_changed)) + for control in self.driver.get_controls(): + self.notify_handles.append(self.conf_client.notify_add("%s/%s" % (screen_key, control.id), self._control_changed)) + logger.info("Starting for %s is complete.", self.device.uid) + + g15profile.profile_listeners.append(self._profile_changed) + + # Start watching for network changes + self.service.network_manager.listeners.append(self._network_state_change) + + def stop(self, quickly=False): + logger.info("Stopping screen for %s", self.device.uid) + self.stopping = True + + # Stop attempting reconnection + if self.reconnect_timer is not None: + self.reconnect_timer.cancel() + + + # Stop watching for network changes + if self._network_state_change in self.service.network_manager.listeners: + self.service.network_manager.listeners.remove(self._network_state_change) + + # Clean up key handler + if self in self.key_handler.action_listeners: + self.key_handler.action_listeners.remove(self) + if self.service.macro_handler in self.key_handler.action_listeners: + self.key_handler.action_listeners.remove(self.service.macro_handler) + if self in self.key_handler.key_handlers: + self.key_handler.key_handlers.remove(self) + if self.service.macro_handler in self.key_handler.key_handlers: + self.key_handler.key_handlers.remove(self.service.macro_handler) + self.key_handler.stop() + + # Stop listening for profile changes + if self._profile_changed in g15profile.profile_listeners: + g15profile.profile_listeners.remove(self._profile_changed) + + # Stop listening for configuration changes + for h in self.notify_handles: + self.conf_client.notify_remove(h) + self.notify_handles = [] + + # Shutdown effects + if self.is_active() and not quickly and (self.service.fade_screen_on_close \ + or self.service.fade_keyboard_backlight_on_close): + # Start fading keyboard + acquisition = None + slow_shutdown_duration = 3.0 + if self.service.fade_keyboard_backlight_on_close: + bl_control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if bl_control: + current_val = bl_control.value + """ + First acquire, and turn all lights off, this is the state it will + return to before disconnecting (we never release it) + """ + self.driver.acquire_control(bl_control, val=0 if isinstance(current_val, int) else (0, 0, 0)) + + acquisition = self.driver.acquire_control(bl_control, val=current_val) + acquisition.fade(duration=slow_shutdown_duration, release=True, step=1 if isinstance(current_val, int) else 10) + + # Fade screen + if self.driver.get_bpp() > 0 and self.service.fade_screen_on_close: + self.fade(True, duration=slow_shutdown_duration, step=10) + + # Wait for keyboard fade to finish as well if it hasn't already + if acquisition: + acquisition.wait() + + # Stop the plugins + if self.plugins and self.plugins.is_activated(): + self.plugins.deactivate() + self.plugins.destroy() + + # Disconnect the driver + if self.driver and self.driver.is_connected(): + self.driver.all_off_on_disconnect = self.service.all_off_on_disconnect + self.driver.disconnect() + + def add_screen_change_listener(self, screen_change_listener): + if not screen_change_listener in self.screen_change_listeners: + self.screen_change_listeners.append(screen_change_listener) + + def remove_screen_change_listener(self, screen_change_listener): + if screen_change_listener in self.screen_change_listeners: + self.screen_change_listeners.remove(screen_change_listener) + + def set_available_size(self, size): + self.available_size = size + self.redraw() + + def get_memory_bank(self): + return self.mkey + + def set_memory_bank(self, bank): + logger.info("Setting memory bank to %d", bank) + self.mkey = bank + val = g15driver.get_mask_for_memory_bank(bank) + control = self.driver.get_control_for_hint(g15driver.HINT_MKEYS) + if control: + self.acquired_controls[control.id].set_value(val) + self.set_color_for_mkey() + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "memory_bank_changed", bank) + + def index(self, page): + """ + Returns the page index + + Keyword arguments: + page -- page object + """ + i = 0 + for p in self.pages: + if p == page: + return i + i = i + 1 + return i + + def get_page(self, page_id): + """ + Return a page object given it's ID + + Keyword arguments: + page_id -- page ID + """ + for page in self.pages: + if page.id == page_id: + return page + + def clear_popup(self): + """ + Clear any popup screens that are currently running + """ + for page in self.pages: + if page.priority == PRI_POPUP: + # Drop the priority of other popups + page.set_priority(PRI_LOW) + break + + def add_page(self, page): + """ + Add a new page. Returns the G15Page object + + Keyword arguments: + page -- page to add + """ + if self.driver.get_bpp() == 0: + raise Exception("The current device has no suitable output device") + + logger.info("Creating new page with %s of priority %d", page.id, page.priority) + self.page_model_lock.acquire() + try : + logger.info("Adding page %s", page.id) + self.clear_popup() + if page.priority == PRI_EXCLUSIVE: + for p in self.pages: + if p.priority == PRI_EXCLUSIVE: + logger.warning("Another page is already exclusive. Lowering %s to HIGH", id) + page.priority = PRI_HIGH + break + self.pages.append(page) + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "new_page", page) + return page + finally: + self.page_model_lock.release() + + def new_page(self, painter=None, priority=PRI_NORMAL, on_shown=None, on_hidden=None, on_deleted=None, + id="Unknown", thumbnail_painter=None, panel_painter=None, title=None, \ + theme_properties_callback=None, theme_attributes_callback=None, + originating_plugin = None): + logger.warning("DEPRECATED call to G15Screen.new_page, use G15Screen.add_page instead") + + """ + Create a new page. Returns the G15Page object + + Keyword arguments: + painter -- painter function. Will be called with a 'canvas' argument that is a cairo.Context + priority -- priority of screen, defaults to PRI_NORMAL + on_shown -- function to call when screen is show. Defaults to None + on_hidden -- function to call when screen is hidden. Defaults to None + on_deleted -- function to call when screen is deleted. Defaults to None + id -- id of screen + thumbnail_painter -- function to call to paint thumbnails for this page. Defaults to None + panel_painter -- function to call to paint panel graphics for this page. Defaults to None + theme_properties_callback -- function to call to get theme properties + theme_attributes_callback -- function to call to get theme attributes + """ + if self.driver.get_bpp() == 0: + raise Exception(_("The current device has no suitable output device")) + + logger.info("Creating new page with %s of priority %d", id, priority) + self.page_model_lock.acquire() + try : + self.clear_popup() + if priority == PRI_EXCLUSIVE: + for page in self.pages: + if page.priority == PRI_EXCLUSIVE: + logger.warning("Another page is already exclusive. Lowering %s to HIGH", id) + priority = PRI_HIGH + break + + page = g15theme.G15Page(id, self, painter, priority, on_shown, on_hidden, on_deleted, \ + thumbnail_painter, panel_painter, theme_properties_callback, \ + theme_attributes_callback, + originating_plugin = originating_plugin) + self.pages.append(page) + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "new_page", page) + if title: + page.set_title(title) + return page + finally: + self.page_model_lock.release() + + def delete_after(self, delete_after, page): + """ + Delete a page after a given time interval. Returns timer object used for deleting. May be canceled + + Keyword arguments: + delete_after -- interval in seconds (float) + page -- page object to hide + """ + self.page_model_lock.acquire() + try : + if page.id in self.deleting: + # If the page was already deleting, cancel previous timer + self.deleting[page.id].cancel() + del self.deleting[page.id] + + timer = g15scheduler.schedule("DeleteScreen", delete_after, self.del_page, page) + self.deleting[page.id] = timer + return timer + finally: + self.page_model_lock.release() + + def is_on_timer(self, page): + ''' + Get if the given page is currently on a revert or delete timer + + Keyword arugments: + page -- page object + ''' + return page.id in self.reverting or page.id in self.deleting + + def set_priority(self, page, priority, revert_after=0.0, delete_after=0.0, do_redraw=True): + """ + Change the priority of a page, optionally reverting or deleting after a specified time. Returns timer object used for reverting or deleting. May be canceled + + Keyword arguments: + page -- page object to change + priority -- new priority + revert_after -- revert the page priority to it's original value after specified number of seconds + delete_after -- delete the page after specified number of seconds + do_redraw -- redraw after changing priority. Defaults to True + """ + self.page_model_lock.acquire() + try : + if page != None: + old_priority = page.priority + page._do_set_priority(priority) + if do_redraw: + self.redraw() + if revert_after != 0.0: + # If the page was already reverting, restore the priority and cancel the timer + if page.id in self.reverting: + old_priority = self.reverting[page.id][0] + self.reverting[page.id][1].cancel() + del self.reverting[page.id] + + # Start a new timer to revert + timer = g15scheduler.schedule("Revert", revert_after, self.set_priority, page, old_priority) + self.reverting[page.id] = (old_priority, timer) + return timer + if delete_after != 0.0: + return self.delete_after(delete_after, page) + finally: + self.page_model_lock.release() + + def raise_page(self, page): + """ + Raise the page. If it is LOW priority, it will be turned into a POPUP. If it is any other priority, + it will be raised to the top of list of all pages that are of the same priority (effectively making + it visible) + + Keyword arguments: + page - page to raise + """ + if page.priority == PRI_LOW: + page.set_priority(PRI_POPUP) + else: + page.set_time(time.time()) + self.redraw() + + def del_page(self, page): + """ + Remove the page from the screen. The page will be hidden and the next highest priority page + displayed. + + Keyword arguments: + page -- page to remove + """ + self.page_model_lock.acquire() + try : + if page != None and page in self.pages: + logger.info("Deleting page %s", page.id) + + # Remove any timers that might be running on this page + if page.id in self.deleting: + self.deleting[page.id].cancel() + del self.deleting[page.id] + if page.id in self.reverting: + self.reverting[page.id][1].cancel() + del self.reverting[page.id] + + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "deleting_page", page) + + if page == self.visible_page: + self.visible_page = None + page._do_on_hidden() + + page.remove_all_children() + + self.pages.remove(page) + page._do_on_deleted() + self.redraw() + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "deleted_page", page) + finally: + self.page_model_lock.release() + + def get_last_error(self): + return self.last_error + + def should_reconnect(self, exception): + if isinstance(exception, RetryException): + return True + if g15devices.have_udev: + return False + return isinstance(exception, NotConnectedException) + + def complete_loading(self): + try : +# logger.info("Activating plugins") +# self.plugins.activate(self.splash.update_splash if self.splash else None) + + logger.info("Setting active profile and activating plugins") + self.set_active_application_name(self.service.get_active_application_name()) + self._check_active_plugins(splash=self.splash.update_splash if self.splash else None, \ + startup=True) + + if self.first_page != None: + page = self.get_page(self.first_page) + if page: + self.raise_page(page) + + logger.info("Grabbing keyboard") + self.driver.grab_keyboard(self.key_handler.key_received) + + logger.info("Grabbed keyboard") + self.clear_attention() + + if self.splash: + self.splash.complete() + self.loading_complete = True + logger.info("Loading complete") + except Exception as e: + logger.debug("Exception completing loading", exc_info = e) + if self._process_exception(e): + raise + + def screen_cycle(self): + page = self.get_visible_page() + if page != None and page.priority < PRI_HIGH: + self.cycle(1) + else: + self.resched_cycle() + + def resched_cycle(self, arg1=None, arg2=None, arg3=None, arg4=None): + + self.reschedule_lock.acquire() + try: + logger.debug("Rescheduling cycle") + self._cancel_timer() + cycle_screens = g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/%s/cycle_screens" % self.device.uid, True) + active = self.driver != None and self.driver.is_connected() and cycle_screens + if active and self.cycle_timer == None: + val = self.conf_client.get("/apps/gnome15/%s/cycle_seconds" % self.device.uid) + time = 10 + if val != None: + time = val.get_int() + self.cycle_timer = g15scheduler.schedule("CycleTimer", time, self.screen_cycle) + finally: + self.reschedule_lock.release() + + def cycle_backlight(self, val): + c = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if c: + if isinstance(c, int): + self.cycle_level(val, c) + else: + self.cycle_color(val, c) + + def cycle_color(self, val, control): + logger.debug("Cycling of %s color by %d", control.id, val) + self.color_no += val + if self.color_no < 0: + self.color_no = len(COLOURS) - 1 + if self.color_no >= len(COLOURS): + self.color_no = 0 + color = COLOURS[self.color_no] + self.conf_client.set_string("/apps/gnome15/%s/%s" % (self.device.uid, control.id), "%d,%d,%d" % (color[0], color[1], color[2])) + + + def cycle_level(self, val, control): + logger.debug("Cycling of %s level by %d", control.id, val) + level = self.conf_client.get_int("/apps/gnome15/%s/%s" % (self.device.uid, control.id)) + level += val + if level > control.upper - 1: + level = control.lower + if level < control.lower - 1: + level = control.upper + self.conf_client.set_int("/apps/gnome15/%s/%s" % (self.device.uid, control.id), level) + + def control_configuration_changed(self, client, connection_id, entry, args): + key = os.path.basename(entry.key) + logger.debug("Controls changed %s", str(key)) + if self.driver != None: + for control in self.driver.get_controls(): + if key == control.id and control.hint & g15driver.HINT_VIRTUAL == 0: + if isinstance(control.value, int): + value = entry.value.get_int() + else: + rgb = entry.value.get_string().split(",") + value = (int(rgb[0]), int(rgb[1]), int(rgb[2])) + + """ + This sets the "root" acquisition so the colour/level is + whatever it is when nothing else has acquired + """ + self.acquired_controls[control.id].set_value(value) + + """ + Also create a temporary acquisition to override any + other acquisitions, such as profile/bank levels + """ + if control.id in self.temp_acquired_controls: + acq = self.temp_acquired_controls[control.id] + if acq.is_active(): + self.driver.release_control(acq) + acq = self.driver.acquire_control(control, release_after=3.0, val = value) + self.temp_acquired_controls[control.id] = acq + acq.set_value(value) + + break + self.redraw() + + def request_defeat_profile_change(self): + self.defeat_profile_change += 1 + + def release_defeat_profile_change(self): + if self.defeat_profile_change < 1: + raise Exception("Cannot release defeat profile change if not requested") + self.defeat_profile_change -= 1 + + def driver_changed(self, client, connection_id, entry, args): + if self.reconnect_timer: + self.reconnect_timer.cancel() + if self.driver == None or self.driver.id != entry.value.get_string(): + g15scheduler.schedule("DriverChange", 1.0, self._reload_driver) + + def active_profile_changed(self, client, connection_id, entry, args): + # Check if the active profile has change) + new_profile = g15profile.get_active_profile(self.device) + if new_profile == None: + logger.info("No profile active") + self.deactivate_profile() + else: + logger.info("Active profile changed to %s", new_profile.name) + self.activate_profile() + self.set_color_for_mkey() + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + return 1 + + def activate_profile(self): + logger.debug("Activating profile") + + if self.driver and self.driver.is_connected(): + self.set_memory_bank(1) + + def _network_state_change(self, new_state): + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + def _profile_changed(self, profile_id, device_uid): + self.set_color_for_mkey() + g15scheduler.schedule("ProfileChange", 1.0, self._check_active_plugins) + + def deactivate_profile(self): + logger.debug("De-activating profile") + if self.driver and self.driver.is_connected(): + self.set_memory_bank(0) + + def clear_attention(self): + logger.debug("Clearing attention") + self.attention = False + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "attention_cleared") + + def request_attention(self, message=None): + logger.debug("Requesting attention '%s'", message) + self.attention = True + if message != None: + self.attention_message = message + + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "attention_requested", message) + + def handle_key(self, keys, state_id, post): + """ + Do not call. This is invoked by the key handler + + Keyword arguments: + keys -- list of keys + state_id -- key state ID (g15driver.KEY_STATED_UP, _DOWN and _HELD) + """ + + self.resched_cycle() + + # Next it goes to the visible page + visible = self.get_visible_page() + if visible != None: + for h in visible.key_handlers: + if h.handle_key(keys, state_id, post): + return True + + # Now to all the plugins + if self.plugins.handle_key(keys, state_id, post=post): + return True + + def action_performed(self, binding): + if binding.action == g15driver.MEMORY_1: + self.set_memory_bank(1) + return True + elif binding.action == g15driver.MEMORY_2: + self.set_memory_bank(2) + return True + elif binding.action == g15driver.MEMORY_3: + self.set_memory_bank(3) + return True + elif binding.action == g15actions.NEXT_SCREEN: + self.cycle(1, True) + return True + elif binding.action == g15actions.PREVIOUS_SCREEN: + self.cycle(-1, True) + return True + elif binding.action == g15actions.NEXT_BACKLIGHT: + self.cycle_backlight(1) + return True + elif binding.action == g15actions.PREVIOUS_BACKLIGHT: + self.cycle_backlight(-1) + return True + + ''' + Private + ''' + + + def _init_screen(self): + logger.info("Starting screen") + self.pages = [] + self.content_surface = None + self.width = self.driver.get_size()[0] + self.height = self.driver.get_size()[1] + + self.surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + self.size = (self.width, self.height) + self.available_size = (0, 0, self.size[0], self.size[1]) + + self.page_model_lock = threading.RLock() + self.draw_lock = threading.Lock() + self.visible_page = None + self.old_canvas = None + self.transition_function = None + self.painter_function = None + self.mkey = 1 + self.reverting = { } + self.deleting = { } + self._do_redraw() + + def _control_changed(self, client, connection_id, entry, args): + control_id = entry.get_key().split("/")[-1] + control = self.driver.get_control(control_id) + control.set_from_configuration(self.driver.device, self.conf_client) + if self.visible_page: + self.visible_page.mark_dirty() + + def _cancel_timer(self): + self.reschedule_lock.acquire() + try: + if self.cycle_timer: + self.cycle_timer.cancel() + self.cycle_timer = None + finally: + self.reschedule_lock.release() + + def _process_exception(self, exception): + self.last_error = exception + self.request_attention(str(exception)) + self.resched_cycle() + if self.driver is not None: + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_connection_failed", self.driver, exception) + self.driver = None + if self.should_reconnect(exception): + logger.debug("Could not gracefully process exception.", exc_info = exception) + self.attempt_connection(5.0) + else: + return True + + def _reload_driver(self): + logger.info("Reloading driver") + if self.driver and self.driver.is_connected() : + self.driver.disconnect() + # Let any clients receive their disconnecting. Driver changes should be rare so this is not a big deal + time.sleep(2.0) + self._load_driver() + if self.driver: + self.attempt_connection(0.0) + + def _load_driver(self): + # Get the driver. If it is not configured, configuration will be required at this point + try : + self.driver = g15drivermanager.get_driver(self.conf_client, self.device, on_close=self.on_driver_close) + self.driver.on_driver_options_change = self._reload_driver + return True + except Exception as e: + logger.debug("Error loading driver", exc_info = e) + self._process_exception(e) + self.driver = None + return False + + def _should_deactivate(self, profile, mod): + import g15pluginmanager + """ + Determine if a plugin should be deactivated based on the current profile + and the state of the network + + Keyword arguments: + profile -- profile to check + mod -- plugin module + """ + return profile.plugins_mode == g15profile.NO_PLUGINS or \ + ( profile.plugins_mode == g15profile.SELECTED_PLUGINS and not mod.id in profile.selected_plugins) or \ + ( g15pluginmanager.is_needs_network(mod) and not self.service.network_manager.is_network_available()) + + def _should_activate(self, profile, mod): + import g15pluginmanager + """ + Determine if a plugin should be activated based on the current profile + and the state of the network + + Keyword arguments: + profile -- profile to check + mod -- plugin module + """ + needs_net = g15pluginmanager.is_needs_network(mod) + return ( profile.plugins_mode == g15profile.ALL_PLUGINS or \ + ( profile.plugins_mode == g15profile.SELECTED_PLUGINS and mod.id in profile.selected_plugins) ) and \ + ( not needs_net or ( needs_net and self.service.network_manager.is_network_available() ) ) + + def _check_active_plugins(self, splash=None, startup=False): + if self.driver is None or not self.driver.is_connected(): + logger.info("Ignoring change in plugin state, not connected") + return + + to_activate = [] + choose_profile = g15profile.get_active_profile(self.device) + + """ + Decide what plugins should de-activated or activated + """ + + if not startup: + """ + We don't need to deactivate during startup, nothing will be activated + """ + l = [] + for plugin in self.plugins.activated: + mod = self.plugins.plugin_map[plugin] + if self._should_deactivate(choose_profile, mod): + l.append(plugin) + for plugin in l: + self.plugins.deactivate(plugin=plugin) + + for plugin in self.plugins.started: + if not plugin in self.plugins.activated: + mod = self.plugins.plugin_map[plugin] + if self._should_activate(choose_profile, mod): + to_activate.append(plugin) + + """ + If this is happening during startup, then activate the actual + plugin manager (i.e. a list of plugins or None). Otherwise, + only activate the individual plugins + """ + if startup: + self.plugins.activate(splash, plugin=to_activate) + else: + for plugin in to_activate: + self.plugins.activate(plugin=plugin) + + def error_on_keyboard_display(self, text, title="Error", icon="dialog-error"): + page = g15theme.ErrorScreen(self, title, text, icon) + return page + + def error(self, error_text=None): + self.attention(error_text) + + def on_driver_close(self, driver, retry=True): + logger.info("Driver closed") + + for handle in self.control_handles: + self.conf_client.notify_remove(handle); + self.control_handles = [] + self.acquired_controls = {} + self.memory_bank_color_control = None + if self.plugins.is_activated(): + self.plugins.deactivate() + + # Delete any remaining pages + if self.pages: + for page in list(self.pages): + self.del_page(page) + + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_disconnected", driver) + + if not self.service.shutting_down and not self.stopping: + if retry: + logger.info("Testing if connection should be retried") + self._process_exception(NotConnectedException("Keyboard driver disconnected.")) + + self.stopping = False + logger.info("Completed closing driver") + + def is_active(self): + """ + Get if the driver is active. + """ + return self.driver != None and self.driver.is_connected() + + def is_visible(self, page): + return self._get_next_page_to_display() == page + + def get_visible_page(self): + return self.visible_page + + def has_page(self, page): + return self.get_page(page.id) != None + + def set_painter(self, painter): + o_painter = self.painter_function + self.painter_function = painter + return o_painter + + def set_transition(self, transition): + o_transition = self.transition_function + self.transition_function = transition + return o_transition + + def cycle_to(self, page, transitions=True): + g15scheduler.clear_jobs(REDRAW_QUEUE) + g15scheduler.execute(REDRAW_QUEUE, "cycleTo", self._do_cycle_to, page, transitions) + + def cycle(self, number, transitions=True): + g15scheduler.clear_jobs(REDRAW_QUEUE) + g15scheduler.execute(REDRAW_QUEUE, "doCycle", self._do_cycle, number, transitions) + + def redraw(self, page=None, direction="up", transitions=True, redraw_content=True, queue=True): + if page: + logger.debug("Redrawing %s", page.id) + else: + logger.debug("Redrawing current page") + if queue: + g15scheduler.execute(REDRAW_QUEUE, "redraw", self._do_redraw, page, direction, transitions, redraw_content) + else: + self._do_redraw(page, direction, transitions, redraw_content) + + + def set_color_for_mkey(self): + control = self.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + rgb = None + if control != None and not isinstance(control.value, int): + profile = g15profile.get_active_profile(self.device) + if profile != None: + rgb = profile.get_mkey_color(self.mkey) + + if rgb is not None: + if self.memory_bank_color_control is None: + self.memory_bank_color_control = self.driver.acquire_control(control) + self.memory_bank_color_control.set_value(rgb) + elif self.memory_bank_color_control is not None: + self.driver.release_control(self.memory_bank_color_control) + self.memory_bank_color_control = None + + def get_current_surface(self): + return self.local_data.surface + + def get_desktop_scale(self): + sx = float(self.available_size[2]) / float(self.width) + sy = float(self.available_size[3]) / float(self.height) + return min(sx, sy) + + def fade(self, stay_faded=False, duration=4.0, step=1): + self.fader = Fader(self, stay_faded=stay_faded, duration=duration, step=step).run() + + def attempt_connection(self, delay=0.0): + logger.debug("Attempting connection in %f", delay) + self.connection_lock.acquire() + try : + if self.reconnect_timer is not None: + self.reconnect_timer.cancel() + + if not self.service.session_active: + logger.debug("Desktop session not active, will not connect to driver") + return + + # With a G510, it's possible the keyboard is now in audio mode, so + # the device we use has changed + new_device = g15devices.get_device(self.device.uid) + if new_device is not None and self.device.controls_usb_id != new_device.controls_usb_id: + logger.info("Device changed, probably a G510 switching to or from audio mode") + self.device = new_device + self.driver = None + + if self.driver == None: + if not self._load_driver(): + raise + + if self.driver.is_connected(): + logger.warning("WARN: Attempt to reconnect when already connected.") + return + + if not self._started_plugins: + self.plugins.start() + self._started_plugins = True + + self.loading_complete = False + self.first_page = self.conf_client.get_string("/apps/gnome15/%s/last_page" % self.device.uid) + + if delay != 0.0: + self.reconnect_timer = g15scheduler.schedule("ReconnectTimer", delay, self.attempt_connection) + return + + try : + + if not self.driver.allow_multiple: + # Look for other screens using the same driver + for s in self.service.screens: + if s.driver is not None and self.driver != s.driver and \ + ( s.driver.is_connected() or s.driver.connecting ) and \ + s.driver.get_name() == self.driver.get_name(): + raise RetryException("Driver %s only allows one device at a time" % s.driver.get_name()) + + self.acquired_controls = {} + self.driver.zeroize_all_controls() + self.driver.connect() + self.driver.release_all_acquisitions() + for control in self.driver.get_controls(): + control.set_from_configuration(self.driver.device, self.conf_client) + self.acquired_controls[control.id] = self.driver.acquire_control(control, val=control.value) + logger.info("Acquired control of %s with value of %s", + control.id, + str(control.value)) + self.control_handles.append(self.conf_client.notify_add("/apps/gnome15/%s/%s" % (self.device.uid, control.id), self.control_configuration_changed)); + self.driver.update_controls() + self._init_screen() + if self.splash == None: + if self.driver.get_bpp() > 0: + self.splash = G15Splash(self, self.conf_client) + else: + self.splash.update_splash(0, 100, "Starting up ..") + self.set_memory_bank(1) + self.activate_profile() + self.last_error = None + for listener in self.screen_change_listeners: + g15pythonlang.call_if_exists(listener, "driver_connected", self.driver) + + self.complete_loading() + + except Exception as e: + logger.debug("Error attenpting connection", exc_info = e) + if self._process_exception(e): + raise + finally: + self.connection_lock.release() + + logger.debug("Connection for %s is complete.", self.device.uid) + + def clear_canvas(self, canvas): + """ + Clears a canvas, filling it with the current background color, and setting the canvas + paint color to the current foreground color + """ + rgb = self.driver.get_color_as_ratios(g15driver.HINT_BACKGROUND, (255, 255, 255)) + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + canvas.rectangle(0, 0, self.width, self.height) + canvas.fill() + rgb = self.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (0, 0, 0)) + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + self.configure_canvas(canvas) + + def page_title_changed(self, page, title): + """ + Tell all screen listeners a page title has changed + + Keyword arguments: + page -- page object + title -- new title + """ + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "title_changed", page, title) + + ''' + Private functions + ''' + + def _draw_page(self, visible_page, direction="down", transitions=True, redraw_content=True): + self.draw_lock.acquire() + try: + if self.driver == None or not self.driver.is_connected(): + return + + # Do not paint if the device has no LCD (i.e. G110) + if self.driver.get_bpp() == 0: + return + + surface = self.surface + + painters = sorted(self.painters, key=lambda painter: painter.z_order) + + # If the visible page is changing, creating a new surface. Both surfaces are + # then passed to any transition functions registered + if visible_page != self.visible_page: + logger.debug("Page has changed, recreating surface") + if visible_page.priority == PRI_NORMAL and not self.stopping: + self.service.conf_client.set_string("/apps/gnome15/%s/last_page" % self.device.uid, visible_page.id) + surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + + self.local_data.surface = surface + canvas = cairo.Context (surface) + self.clear_canvas(canvas) + + # Background painters + for painter in painters: + if painter.place == BACKGROUND_PAINTER: + painter.paint(canvas) + + old_page = None + if visible_page != self.visible_page: + old_page = self.visible_page + redraw_content = True + if self.visible_page != None: + self.visible_page = visible_page + old_page._do_on_hidden() + else: + self.visible_page = visible_page + if self.visible_page != None: + self.visible_page._do_on_shown() + + self.resched_cycle() + for l in self.screen_change_listeners: + g15pythonlang.call_if_exists(l, "page_changed", self.visible_page) + + # Call the screen's painter + if self.visible_page != None: + logger.debug("Drawing page %s " \ + "(direction = %s, transitions = %s, redraw_content = %s", + self.visible_page.id, + direction, + str(transitions), + str(redraw_content)) + + + # Paint the content to a new surface so it can be cached + if self.content_surface == None or redraw_content: + self.content_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.width, self.height) + content_canvas = cairo.Context(self.content_surface) + self.configure_canvas(content_canvas) + self.visible_page.paint(content_canvas) + + tx = self.available_size[0] + ty = self.available_size[1] + + # Scale to the available space, and center + sx = float(self.available_size[2]) / float(self.width) + sy = float(self.available_size[3]) / float(self.height) + scale = min(sx, sy) + sx = scale + sy = scale + + if tx == 0 and self.available_size[3] != self.size[1]: + sx = 1 + + if ty == 0 and self.available_size[2] != self.size[0]: + sy = 1 + + canvas.save() + canvas.translate(tx, ty) + canvas.scale(sx, sy) + canvas.set_source_surface(self.content_surface) + canvas.paint() + canvas.restore() + + """ + Glass pane (components a bit like foreground painters in that + they paint over the top of pages + """ + self.glass_pane.paint(canvas) + + # Foreground painters + for painter in painters: + if painter.place == FOREGROUND_PAINTER: + painter.paint(canvas) + + # Run any transitions + if transitions and self.transition_function != None and self.old_canvas != None: + self.transition_function(self.old_surface, surface, old_page, self.visible_page, direction) + + # Now apply any global transformations and paint + if self.painter_function != None: + self.painter_function(surface) + else: + self.driver.paint(surface) + + self.old_canvas = canvas + self.old_surface = surface + finally: + self.draw_lock.release() + + def configure_canvas(self, canvas): + canvas.set_antialias(self.driver.get_antialias()) + fo = cairo.FontOptions() + fo.set_antialias(self.driver.get_antialias()) + if self.driver.get_antialias() == cairo.ANTIALIAS_NONE: + fo.set_hint_style(cairo.HINT_STYLE_NONE) + fo.set_hint_metrics(cairo.HINT_METRICS_OFF) + canvas.set_font_options(fo) + return fo + + def _do_cycle_to(self, page, transitions=True): + self.page_model_lock.acquire() + try : + if page.priority == PRI_LOW: + # Visible until the next popup, or it hides itself + self.set_priority(page, PRI_POPUP) + elif page.priority < PRI_LOW: + self.clear_popup() + # Up to the page to make itself stay visible + self._draw_page(page, "down", transitions) + else: + self.clear_popup() + self._flush_reverts_and_deletes() + # Cycle within pages of the same priority + page_list = self._get_pages_of_priority(page.priority) + direction = "up" + direction_val = 1 + diff = page_list.index(page) + if diff >= (len(page_list) / 2): + direction_val *= -1 + direction = "down" + self._cycle_pages(diff, page_list) + self._do_redraw(page, direction=direction, transitions=transitions) + finally: + self.page_model_lock.release() + + def _do_cycle(self, number, transitions=True): + self.page_model_lock.acquire() + try : + self._flush_reverts_and_deletes() + self._cycle(number, transitions) + direction = "up" + if number < 0: + direction = "down" + self._do_redraw(self._get_next_page_to_display(), direction=direction, transitions=transitions) + finally: + self.page_model_lock.release() + + def _get_pages_of_priority(self, priority): + p_pages = [] + for page in self._sort(): + if page.priority == PRI_NORMAL: + p_pages.append(page) + return p_pages + + def _cycle_pages(self, number, pages): + if len(pages) > 0: + if number < 0: + for _ in range(number, 0): + first_time = pages[0].time + for i in range(0, len(pages) - 1): + pages[i].set_time(pages[i + 1].time) + pages[len(pages) - 1].set_time(first_time) + else: + for _ in range(0, number): + last_time = pages[len(pages) - 1].time + for i in range(len(pages) - 1, 0, -1): + pages[i].set_time(pages[i - 1].time) + pages[0].set_time(last_time) + + def _cycle(self, number, transitions=True): + if len(self.pages) > 0: + self._cycle_pages(number, self._get_pages_of_priority(PRI_NORMAL)) + + def _do_redraw(self, page=None, direction="up", transitions=True, redraw_content=True): + self.page_model_lock.acquire() + try : + current_page = self._get_next_page_to_display() + if page == None or page == current_page: + self._draw_page(current_page, direction, transitions, redraw_content) + elif page != None and page.panel_painter != None: + self._draw_page(current_page, direction, transitions, False) + finally: + self.page_model_lock.release() + + def _flush_reverts_and_deletes(self): + self.page_model_lock.acquire() + try : + for page_id in self.reverting: + (old_priority, timer) = self.reverting[page_id] + timer.cancel() + self.set_priority(self.get_page(page_id), old_priority) + self.reverting = {} + for page_id in list(self.deleting.keys()): + timer = self.deleting[page_id] + timer.cancel() + self.del_page(self.get_page(page_id)) + self.deleting = {} + finally: + self.page_model_lock.release() + + def _sort(self): + return sorted(self.pages, key=lambda page: page.value, reverse=True) + + def _get_next_page_to_display(self): + self.page_model_lock.acquire() + try : + srt = sorted(self.pages, key=lambda key: key.value, reverse=True) + if len(srt) > 0 and srt[0].priority != PRI_INVISIBLE: + return srt[0] + finally: + self.page_model_lock.release() + +""" +Fades the screen by inserting a foreground painter that paints a transparent +black rectangle over the top of everything. The opacity is this gradually +increased, creating a fading effect +""" +class Fader(Painter): + + def __init__(self, screen, stay_faded=False, duration=3.0, step=1): + Painter.__init__(self, FOREGROUND_PAINTER, 9999) + self.screen = screen + self.duration = duration + self.opacity = 0 + self.step = step + self.stay_faded = stay_faded + self.interval = (duration / 255) * step + + def run(self): + self.screen.painters.append(self) + try: + while self.opacity <= 255: + self.screen.redraw(redraw_content = False) + time.sleep(self.interval) + finally: + if not self.stay_faded: + self.screen.painters.remove(self) + + def paint(self, canvas): + # Fade to black on the G19, or white on everything else + if self.screen.driver.get_bpp() == 1: + col = 1.0 + else: + col = 0.0 + canvas.set_source_rgba(col, col, col, float(self.opacity) / 255.0) + canvas.rectangle(0, 0, self.screen.width, self.screen.height) + canvas.fill() + self.opacity += self.step + +class G15Splash(): + + def __init__(self, screen, gconf_client): + self.screen = screen + self.progress = 0.0 + self.text = _("Starting up ..") + icon_path = g15icontools.get_icon_path("gnome15") + if icon_path == None: + icon_path = os.path.join(g15globals.icons_dir, "hicolor", "apps", "scalable", "gnome15.svg") + self.logo = g15cairo.load_surface_from_file(icon_path) + self.page = g15theme.G15Page("Splash", self.screen, priority=PRI_EXCLUSIVE, thumbnail_painter=self._paint_thumbnail, \ + theme_properties_callback=self._get_properties, theme=g15theme.G15Theme(g15globals.image_dir, "background")) + self.screen.add_page(self.page) + + def complete(self): + self.progress = 100 + self.screen.redraw(self.page) + g15scheduler.queue(REDRAW_QUEUE, "ClearSplash", 2.0, self._hide) + + def update_splash(self, value, max_value, text=None): + self.progress = (float(value) / float(max_value)) * 100.0 + self.screen.redraw(self.page) + if text != None: + self.text = text + + def _get_properties(self): + return { "version": g15globals.version, + "progress": self.progress, + "text": self.text + } + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + return g15cairo.paint_thumbnail_image(allocated_size, self.logo, canvas) + + def _hide(self): + self.screen.del_page(self.page) + self.screen.redraw() diff --git a/src/gnome15/g15service.py b/src/gnome15/g15service.py new file mode 100644 index 0000000..8630af0 --- /dev/null +++ b/src/gnome15/g15service.py @@ -0,0 +1,1138 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# NoXPhasma +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import pygtk +from gnome15 import g15accounts +pygtk.require('2.0') +import os +import gobject +import g15globals +import g15screen +import g15profile +import g15dbus +import g15devices +import g15desktop +import g15uinput +import g15network +import g15accounts +import g15driver +import gconf +import util.g15scheduler as g15scheduler +import util.g15gconf as g15gconf +import util.g15os as g15os +import Xlib.X +import Xlib.ext +import Xlib.XK +import Xlib.display +import Xlib.protocol +import time +import dbus +import signal +import g15pluginmanager +import g15actions +from threading import Thread +import gtk.gdk + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Used for getting logout / shutdown signals +master_client = None +if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + try: + import gnome.ui + master_client = gnome.ui.master_client() + except Exception as e: + logger.debug("Could not get gnome master client", exc_info = e) + pass + +# Upgrade +import g15upgrade +g15upgrade.upgrade() + + +NAME = "Gnome15" +VERSION = g15globals.version +SERVICE_QUEUE = "serviceQueue" +MACRO_HANDLER_QUEUE = "macroHandler" + +special_X_keysyms = { + ' ' : "space", + '\t' : "Tab", + '\n' : "Return", # for some reason this needs to be cr, not lf + '\r' : "Return", + '\e' : "Escape", + '\b' : "BackSpace", + '!' : "exclam", + '#' : "numbersign", + '%' : "percent", + '$' : "dollar", + '&' : "ampersand", + '"' : "quotedbl", + '\'' : "apostrophe", + '(' : "parenleft", + ')' : "parenright", + '*' : "asterisk", + '=' : "equal", + '+' : "plus", + ',' : "comma", + '-' : "minus", + '.' : "period", + '/' : "slash", + ':' : "colon", + ';' : "semicolon", + '<' : "less", + '>' : "greater", + '?' : "question", + '@' : "at", + '[' : "bracketleft", + ']' : "bracketright", + '\\' : "backslash", + '^' : "asciicircum", + '_' : "underscore", + '`' : "grave", + '{' : "braceleft", + '|' : "bar", + '}' : "braceright", + '~' : "asciitilde" + } + +class CheckThread(Thread): + def __init__(self, device, check_function, quickly): + Thread.__init__(self) + self.name = "CheckDeviceState%s" % device.uid + self.device = device + self.quickly = quickly + self.check_function = check_function + self.start() + + def run(self): + self.check_function(self.device, self.quickly) + +class StartThread(Thread): + def __init__(self, screen): + Thread.__init__(self) + self.name = "StartScreen%s" % screen.device.uid + self.screen = screen + self.error = None + + def run(self): + try: + self.screen.start() + except Exception as e: + logger.error("Failed to start screen.", exc_info = e) + self.error = e + +class MacroHandler(object): + + def __init__(self): + self.buffered_executions = [] + self.cancelled = False + self.use_x_test = None + self.x_test_available = None + self.window = None + + def cancel(self): + """ + Cancel the currently running macro script if any. This script may + not immediately be cancelled if there are un-interuptable tasks running. + """ + self.cancelled = True + + def handle_key(self, keys, state_id, post): + """ + Handle raw keys. We use this to complete any macros waiting for another + key events + """ + g15scheduler.queue(MACRO_HANDLER_QUEUE, "HandleMacro", 0, self._do_handle_key, keys, state_id, post) + + def handle_macro(self, macro): + """ + We want to return control immediately after asking for a macro + to be handled, but we only ever want one macro running at a time. + This means the macro action is put on it's own queue. This also + allows long running macros to be cancelled + + Keyword arguments: + macro -- macro to handle + """ + g15scheduler.queue(MACRO_HANDLER_QUEUE, "HandleMacro", 0, self._do_handle, macro) + + def get_x_display(self): + self.init_xtest() + return self.local_dpy + + def init_xtest(self): + """ + Initialise XTEST if it is available. + """ + if self.x_test_available == None: + logger.info("Initialising macro output system") + + # Load Python Virtkey if it is available + + # Use python-virtkey for preference + + self.virtual_keyboard = None + try: + import virtkey + self.virtual_keyboard = virtkey.virtkey() + self.x_test_available = False + except Exception as e: + logger.warning("No python-virtkey, macros may be weird. Trying XTest", exc_info = e) + + # Determine whether to use XTest for sending key events to X + self.x_test_available = True + try : + import Xlib.ext.xtest + except ImportError as e: + logger.warning("No XTest, falling back to raw X11 events", exc_info = e) + self.x_test_available = False + + self.local_dpy = Xlib.display.Display() + + if self.x_test_available and not self.local_dpy.query_extension("XTEST") : + logger.warning("Found XTEST module, but the X extension could not be found") + self.x_test_available = False + + def send_string(self, ch, press): + """ + Sends a string (character) to the X server as if it was typed. + Depending on the configuration virtkey, XTEST or raw events may be used + + Keyword arguments: + ch -- character to send + press -- boolean indicating if this is a PRESS or RELEASE + """ + logger.debug("Sending string %s", ch) + + if self.virtual_keyboard is not None: + keysym = self._get_keysym(ch) + if press: + logger.debug("Sending keychar %s press = %s, keysym = %d (%x)", + ch, + press, + keysym, + keysym) + self.virtual_keyboard.press_keysym(keysym) + else: + self.virtual_keyboard.release_keysym(self._get_keysym(ch)) + else: + keycode, shift_mask = self._char_to_keycodes(ch) + logger.debug("Sending keychar %s keycode %d, press = %s, shift = %d", + ch, + int(keycode), + str(press), + shift_mask) + if (self.x_test_available and self.use_x_test) : + if press: + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyPress, 62) + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyPress, keycode) + else: + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyRelease, keycode) + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(self.local_dpy, Xlib.X.KeyRelease, 62) + else : + if press: + event = Xlib.protocol.event.KeyPress( + time=int(time.time()), + root=self.local_dpy.screen().root, + window=self.window, + same_screen=0, child=Xlib.X.NONE, + root_x=0, root_y=0, event_x=0, event_y=0, + state=shift_mask, + detail=keycode + ) + self.window.send_event(event, propagate=True) + else: + event = Xlib.protocol.event.KeyRelease( + time=int(time.time()), + root=self.local_dpy.screen().root, + window=self.window, + same_screen=0, child=Xlib.X.NONE, + root_x=0, root_y=0, event_x=0, event_y=0, + state=shift_mask, + detail=keycode + ) + self.window.send_event(event, propagate=True) + self.local_dpy.sync() + + def send_simple_macro(self, macro): + logger.debug("Simple macro '%s'", macro.macro) + esc = False + i = 0 + + press_delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.press_delay) / 1000.0 ) + release_delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.release_delay) / 1000.0 ) + + for c in macro.macro: + if self.cancelled: + logger.warning("Macro cancelled.") + break + if c == '\\' and not esc: + esc = True + else: + if esc and c == 'p': + time.sleep(release_delay + press_delay) + else: + if i > 0: + logger.debug("Release delay of %f", release_delay) + time.sleep(release_delay) + + if esc and c == 't': + c = '\t' + elif esc and c == 'r': + c = '\r' + elif esc and c == 'n': + c = '\r' + elif esc and c == 'b': + c = '\b' + elif esc and c == 'e': + c = '\e' + elif esc and c == '\\': + c = '\\' + + if c in special_X_keysyms: + c = special_X_keysyms[c] + + self.send_string(c, True) + time.sleep(press_delay) + logger.debug("Press delay of %f", press_delay) + self.send_string(c, False) + + i += 1 + + esc = False + + def press_delay(self, macro): + delay = 0.0 if not macro.profile.fixed_delays else ( float(macro.profile.press_delay) / 1000.0 ) + logger.debug("Press delay of %f", delay) + time.sleep(delay) + + def release_delay(self, macro): + delay = 0 if not macro.profile.fixed_delays else ( float(macro.profile.release_delay) / 1000.0 ) + logger.debug("Release delay of %f", delay) + time.sleep(delay) + + + def action_performed(self, binding): + if binding.action == g15actions.CANCEL_MACRO: + self.cancel() + return True + + def _do_handle_key(self, keys, state_id, post): + for b in list(self.buffered_executions): + if b.handle_key(keys, state_id, post): + """ + The keys that activated this macro are now all in the required state, + so continue execution + """ + self.buffered_executions.remove(b) + wait_for_state = b.execute() + if wait_for_state: + self.buffered_executions.append(b) + + def _get_keysym(self, ch) : + keysym = Xlib.XK.string_to_keysym(ch) + if keysym == 0 : + # Unfortunately, although this works to get the correct keysym + # i.e. keysym for '#' is returned as "numbersign" + # the subsequent display.keysym_to_keycode("numbersign") is 0. + if ch in special_X_keysyms: + keysym_name = special_X_keysyms[ch] + keysym = Xlib.XK.string_to_keysym(keysym_name) + return keysym + + def _char_to_keycodes(self, ch): + """ + Convert a character from a string into an X11 keycode when possible. + + Keyword arguments: + ch -- character to convert + """ + self.init_xtest() + shift_mask = 0 + + if str(ch).startswith("["): + keysym_code = int(ch[1:-1]) + # AltGr + if keysym_code == 65027: + keycode = 108 + else: + logger.warning("Unknown keysym %d", keysym_code) + keycode = 0 + else: + + keysym = self._get_keysym(ch) + + x_keycodes = self.local_dpy.keysym_to_keycodes(keysym) + keycode = 0 if keysym == 0 else self.local_dpy.keysym_to_keycode(keysym) + + # I have no idea how accurate this is, but it seems more so that + # the is_shifted() function + if keysym < 256: + for x in x_keycodes: + if x[1] == 1: + shift_mask = Xlib.X.ShiftMask + + if keycode == 0 : + logger.warning("Sorry, can't map (character %d)", ord(ch)) + + return keycode, shift_mask + + def _do_handle(self, macro): + + # Get the latest focused window if not using XTest + self.cancelled = False + self.init_xtest() + if self.virtual_keyboard is None and ( not self.use_x_test or not self.x_test_available ): + self.window = self.local_dpy.get_input_focus()._data["focus"]; + + if macro.type == g15profile.MACRO_COMMAND: + logger.warning("Running external command '%s'", macro.macro) + os.system(macro.macro) + elif macro.type == g15profile.MACRO_SIMPLE: + self.send_simple_macro(macro) + else: + executor = MacroScriptExecution(macro, self) + wait_for_state = executor.execute() + if wait_for_state: + self.buffered_executions.append(executor) + +class MacroScriptExecution(object): + + def __init__(self, macro, handler): + self.macro = macro + self.handler = handler + self.l = -1 + self.macros = self.macro.macro.split("\n") + self.wait_for_state = -2 + self.wait_for_keys = [] + self.down = 0 + self.all_keys_up = False + self.cancelled = False + + # First parse to get where the labels are + self.labels = {} + for l in range(0, len(self.macros)): + macro_text = self.macros[l] + split = macro_text.split(" ") + op = split[0].lower() + if op == "label" and len(op) > 1: + self.labels[split[1].lower()] = l + + def handle_key(self, keys, state_id, post): + + """ + If we get the state we are waiting for, OR if we get an UP before + getting a HELD, we remove this key from this key from the list we are waiting for + """ + if state_id == self.wait_for_state or state_id == g15driver.KEY_STATE_UP and self.wait_for_state == g15driver.KEY_STATE_HELD: + for k in keys: + self.wait_for_keys.remove(k) + + if len(self.wait_for_keys) == 0: + # All keys are now in the required state + if state_id == g15driver.KEY_STATE_UP and self.wait_for_state == g15driver.KEY_STATE_HELD: + # We should cancel execution now + self.cancelled = True + if state_id == g15driver.KEY_STATE_UP: + # Make a note of the fact all triggering keys are now up + self.all_keys_up = True + return True + + def execute(self): + while True: + if self.down == 0 and ( self.handler.cancelled or self.cancelled ): + logger.warning("Macro cancelled") + break + self.l += 1 + if self.l == len(self.macros): + break + macro_text = self.macros[self.l] + split = macro_text.split(" ") + op = split[0].lower() + if len(split) > 1: + val = split[1] + if op == "goto": + val = val.lower() + if val in self.labels: + self.l = self.labels[val] + else: + logger.warning("Unknown goto label %s in macro script. Ignoring", val) + elif op == "delay": + if not self.handler.cancelled and self.macro.profile.send_delays and not self.macro.profile.fixed_delays: + time.sleep(float(val) / 1000.0 if not self.macro.profile.fixed_delays else self.macro.profile.delay_amount) + elif op == "press": + if self.down > 0: + self.handler.release_delay(self.macro) + self.handler.send_string(val, True) + self.down += 1 + self.handler.press_delay(self.macro) + elif op == "release": + self.handler.send_string(val, False) + self.down -= 1 + elif op == "upress": + if len(split) < 3: + logger.error("Invalid operation in macro script. '%s'", macro_text) + else: + if self.down > 0: + self.handler.release_delay(self.macro) + self.down += 1 + self._send_uinput(split[2], val, 1) + self.handler.press_delay(self.macro) + elif op == "urelease": + if len(split) < 3: + logger.error("Invalid operation in macro script. '%s'", macro_text) + else: + self.down -= 1 + self._send_uinput(split[2], val, 0) + elif op == "wait": + if self.all_keys_up: + logger.warning("All keys for the macro %s are already up, " \ + "the rest of the script will be ignored", self.macro.name) + return False + else: + val = val.lower() + if val == "release": + if self.macro.activate_on == g15driver.KEY_STATE_UP: + logger.error("WaitRelease cannot be used with macros that activate on release") + else: + self.wait_for_state = g15driver.KEY_STATE_UP + self.wait_for_keys = list(self.macro.keys) + return True + elif val == "hold": + if self.macro.activate_on == g15driver.KEY_STATE_DOWN: + self.wait_for_state = g15driver.KEY_STATE_HELD + self.wait_for_keys = list(self.macro.keys) + return True + else: + logger.error("WaitHold cannot be used with macros that activate on hold or release") + else: + logger.error("Wait may only have an argument of release or hold") + elif op == "label": + # Ignore label / comment + pass + else: + logger.error("Invalid operation in macro script. '%s'", macro_text) + + else: + if len(split) > 0: + logger.error("Insufficient arguments in macro script. '%s'", macro_text) + + + def _send_uinput(self, target, val, state): + if val in g15uinput.capabilities: + g15uinput.emit(target, g15uinput.capabilities[val], state, True) + else: + logger.error("Unknown uinput key %s.", val) + +class G15Service(g15desktop.G15AbstractService): + + def __init__(self, service_host, no_trap=False): + self.exit_on_no_devices = False + self.active_plugins = {} + self.session_active = True + self.service_host = service_host + self.active_window = None + self.shutting_down = False + self.starting_up = True + self.conf_client = gconf.client_get_default() + self.screens = [] + self.started = False + self.service_listeners = [] + self.notify_handles = [] + self.device_notify_handles = {} + self.font_faces = {} + self.stopping = False + self.window_title_listener = None + self.active_application_name = None + self.active_window_title = None + self.ignore_next_sigint = False + self.debug_svg = False + self.devices = g15devices.find_all_devices() + self.macro_handler = MacroHandler() + self.global_plugins = None + + # Expose Gnome15 functions via DBus + logger.debug("Starting the DBUS service") + self.dbus_service = g15dbus.G15DBUSService(self) + + # Watch for signals + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + signal.signal(signal.SIGUSR1, self.sigusr1_handler) + + g15desktop.G15AbstractService.__init__(self) + self.name = "DesktopService" + + def start_service(self): + try: + self._do_start_service() + except Exception as e: + self.shutdown(True) + logger.error("Failed to start service.", exc_info = e) + + def sigusr1_handler(self, signum, frame): + logger.info("Got SIGUSR1 signal from %s, restarting", str(frame)) + self.restart() + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal from %s, shutting down", str(frame)) + self.shutdown(True) + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal from %s, shutting down", str(frame)) + self.shutdown(True) + + def stop(self, quickly = False): + if self.started: + g15accounts.STATUS.stopping = True + self.stopping = True + + g15devices.device_added_listeners.remove(self._device_added) + g15devices.device_removed_listeners.remove(self._device_removed) + g15uinput.close_devices() + self.global_plugins.deactivate() + self.session_active = False + try : + for h in self.notify_handles: + self.conf_client.notify_remove(h) + for h in self.device_notify_handles: + self.conf_client.notify_remove(self.device_notify_handles[h]) + try : + logger.info("Stopping profile change notification") + g15profile.notifier.stop() + except Exception as e: + logger.debug("Error stopping profile change notification", exc_info = e) + pass + try : + logger.info("Stopping account change notification") + g15accounts.notifier.stop() + except Exception as e: + logger.debug("Error stopping account change notification", exc_info = e) + pass + logger.info("Informing listeners we are stopping") + for listener in self.service_listeners: + listener.service_stopping() + logger.info("Stopping screens") + self._check_state_of_all_devices_async(quickly) + logger.info("Screens stopped") + self.started = False + finally : + self.stopping = False + else: + logger.warning("Ignoring stop request, already stopped.") + + def restart(self): + g15os.run_script("g15-desktop-service", ["restart"], background = True) + + def shutdown(self, quickly = False): + logger.info("Shutting down") + self.shutting_down = True + if self.global_plugins is not None: + self.global_plugins.destroy() + self.stop(quickly) + g15scheduler.stop_queue(MACRO_HANDLER_QUEUE) + g15scheduler.stop_queue(SERVICE_QUEUE) + logger.info("Stopping all schedulers") + g15scheduler.stop_all_schedulers() + for listener in self.service_listeners: + listener.service_stopped() + logger.info("Quiting loop") + self.loop.quit() + logger.info("Stopping DBus service") + self.dbus_service.stop() + + def get_active_application_name(self): + return self.active_application_name + + """ + Private + """ + + def _active_window_changed(self, old, object_name): + if object_name != "": + app = self.session_bus.get_object("org.ayatana.bamf", object_name) + view = None + try: + view = dbus.Interface(app, 'org.ayatana.bamf.view') + self.active_application_name = view.Name() + except dbus.DBusException as e: + logger.debug("Could not get current application name", exc_info = e) + self.active_application_name = None + + if view is not None: + screens = list(self.screens) + for s in list(screens): + if self._check_active_application(s, app, view): + screens.remove(s) + + window = dbus.Interface(app, 'org.ayatana.bamf.window') + self.active_window_title = self._get_x_prop(window, '_NET_WM_VISIBLE_NAME') + if not self.active_window_title: + self.active_window_title = self._get_x_prop(window, '_NET_WM_NAME') + for s in list(screens): + if self._check_active_window(s, app, window): + screens.remove(s) + + """ + Start listening for name changes within the view as well + """ + if self.window_title_listener is not None: + self.window_title_listener.remove() + + def _window_title_changed(old_name, new_name): + self.active_window_title = new_name + for s in list(self.screens): + self._check_active_window(s, app, window) + +# self.window_title_listener = view.connect_to_signal('NameChanged', _window_title_changed, None) + + def _get_x_prop(self, window, key): + try : + return window.XProps(key) + except dbus.DBusException as e: + logger.debug("Could not get window XProps", exc_info = e) + return None + + def _check_active_window(self, screen, app, window): + try : + if screen.set_active_application_name(self.active_window_title): + return True + except dbus.DBusException as e: + logger.debug("Could not check active window", exc_info = e) + pass + + def _check_active_application(self, screen, app, view): + try : + if view is not None and view.IsActive() == 1: + vn = view.Name() + if screen.set_active_application_name(vn): + return True + else: + parents = view.Parents() + for parent in parents: + app = self.session_bus.get_object("org.ayatana.bamf", parent) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + if self._check_active_application(screen, app, view): + return True + except dbus.DBusException as e: + logger.debug("Could not check active application", exc_info = e) + pass + + def _check_active_application_with_wnck(self, event=None): + try: + import wnck + window = wnck.screen_get_default().get_active_window() + if window is not None and not window.is_skip_pager(): + app = window.get_application() + active_application_name = app.get_name() if app is not None else "" + if active_application_name != self.active_application_name: + self.active_application_name = active_application_name + self.active_window_title = active_application_name + logger.info("Active application is now %s", self.active_application_name) + for screen in self.screens: + screen.set_active_application_name(active_application_name) + except Exception as e: + logger.warning("Failed to activate profile for active window", exc_info = e) + + gobject.timeout_add(500, self._check_active_application_with_wnck) + + def _check_state_of_all_devices(self, quickly = False): + logger.info("Checking state of %d devices", len(self.devices)) + for d in self.devices: + self._check_device_state(d, quickly) + + def _check_state_of_all_devices_async(self, quickly = False): + logger.info("Checking state of %d devices", len(self.devices)) + t = [] + for d in self.devices: + t.append(CheckThread(d, self._check_device_state, quickly)) + self._join_all(t) + + def _do_start_service(self): + + # Network manager + self.network_manager = g15network.NetworkManager(self) + + # Global plugins + self.session_active = True + self.global_plugins = g15pluginmanager.G15Plugins(None, self, network_manager = self.network_manager) + self.global_plugins.start() + + for listener in self.service_listeners: + listener.service_starting_up() + + # UINPUT + try: + g15uinput.open_devices() + except OSError as e: + logger.debug("Error opening uinput devices", exc_info = e) + if e.errno == 13 or e.errno == 2: + raise Exception("Failed to open uinput devices. Do you have the uinput module loaded (try modprobe uinput), and are the permissions of /dev/uinput correct? If you have just installed Gnome15 for the first time, you may need to simply reboot.") + else: + raise + + self.session_bus = dbus.SessionBus() + self.system_bus = dbus.SystemBus() + + # Create a screen for each device + self.conf_client.add_dir("/apps/gnome15", gconf.CLIENT_PRELOAD_NONE) + logger.info("Looking for devices") + if len(self.devices) == 0: + if g15devices.have_udev and not self.exit_on_no_devices: + logger.error("No devices found yet, waiting for some to appear") + else: + logger.error("No devices found. Gnome15 will now exit") + self.shutdown() + return + else: + # Create the default profile for all devices + for device in self.devices: + g15profile.create_default(device) + + # If there is a single device, it is enabled by default + if len(self.devices) == 1: + self.conf_client.set_bool("/apps/gnome15/%s/enabled" % self.devices[0].uid, True) + + errors = 0 + for device in self.devices: + val = self.conf_client.get("/apps/gnome15/%s/enabled" % device.uid) + h = self.conf_client.notify_add("/apps/gnome15/%s/enabled" % device.uid, self._device_enabled_configuration_changed, device) + self.device_notify_handles[device.uid] = h + if ( val == None and device.model_id != "virtual" ) or ( val is not None and val.get_bool() ): + screen = self._add_screen(device) + if not screen: + errors += 1 + + if len(self.devices) == errors: + logger.error("All screens failed to load. Shutting down") + self.shutdown() + return + + if len(self.screens) == 0: + logger.warning("No screens found yet. Will stay running waiting for one to be enabled.") + + # Load hidden configuration and monitor for changes + self._load_hidden_configuration() + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/scroll_delay", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/scroll_amount", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/animated_menus", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/animation_delay", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/key_hold_duration", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/use_x_test", self._hidden_configuration_changed)) + self.notify_handles.append(self.conf_client.notify_add("/apps/gnome15/disable_svg_glow", self._hidden_configuration_changed)) + + + # Monitor active application + gobject.idle_add(self._configure_window_monitoring) + + # Activate global plugins + self.global_plugins.activate() + + # Start each screen's plugin manager + th = [] + for screen in self.screens: + t = StartThread(screen) + if self.start_in_threads: + t.start() + else: + t.run() + th.append(t) + if len(self.screens) == 1: + if th[0].error is not None: + raise th[0].error + + if self.start_in_threads: + for t in th: + t.join() + + self.starting_up = False + for listener in self.service_listeners: + listener.service_started_up() + self.started = True + + gobject.idle_add(self._monitor_session) + + # Watch for devices changing + g15devices.device_added_listeners.append(self._device_added) + g15devices.device_removed_listeners.append(self._device_removed) + + def _join_all(self, threads, timeout = 30): + for t in threads: + t.join(timeout) + + def _monitor_session(self): + # Monitor active session (we shut down the driver when becoming inactive) + if self.system_bus.name_has_owner('org.freedesktop.ConsoleKit'): + self._connect_to_consolekit() + elif self.system_bus.name_has_owner('org.freedesktop.login1'): + self._connect_to_logind() + else: + logger.warning("None of the supported system session manager available, will not track active desktop session.") + self.session_active = True + + connected_to_session_manager = False + # session manager stuff (watch for logout etc) + try : + logger.info("Connecting to GNOME session manager") + session_manager_object = self.session_bus.get_object("org.gnome.SessionManager", "/org/gnome/SessionManager", "org.gnome.SessionManager") + client_path = session_manager_object.RegisterClient('gnome15.desktop', '', dbus_interface="org.gnome.SessionManager") + + self.session_manager_client_object = self.session_bus.get_object("org.gnome.SessionManager", client_path, "org.gnome.SessionManager.ClientPrivate") + self.session_manager_client_object.connect_to_signal("QueryEndSession", self._sm_query_end_session) + self.session_manager_client_object.connect_to_signal("EndSession", self._sm_end_session) + self.session_manager_client_object.connect_to_signal("CancelEndSession", self._sm_cancel_end_session) + self.session_manager_client_object.connect_to_signal("Stop", self._sm_stop) + + session_manager_client_public_object = self.session_bus.get_object("org.gnome.SessionManager", client_path, "org.gnome.SessionManager.Client") + sm_client_id = session_manager_client_public_object.GetStartupId() + gtk.gdk.set_sm_client_id(sm_client_id) + connected_to_session_manager = True + logger.info("Connected to GNOME session manager") + except Exception as e: + logger.info("GNOME session manager not available.") + logger.debug("GNOME connection attempt below :", exc_info = e) + + if not connected_to_session_manager: + try : + logger.info("Connecting to MATE session manager") + session_manager_object = self.session_bus.get_object("org.mate.SessionManager", "/org/mate/SessionManager", "org.mate.SessionManager") + client_path = session_manager_object.RegisterClient('gnome15.desktop', '', dbus_interface="org.mate.SessionManager") + + self.session_manager_client_object = self.session_bus.get_object("org.mate.SessionManager", client_path, "org.mate.SessionManager.ClientPrivate") + self.session_manager_client_object.connect_to_signal("QueryEndSession", self._sm_query_end_session) + self.session_manager_client_object.connect_to_signal("EndSession", self._sm_end_session) + self.session_manager_client_object.connect_to_signal("CancelEndSession", self._sm_cancel_end_session) + self.session_manager_client_object.connect_to_signal("Stop", self._sm_stop) + + session_manager_client_public_object = self.session_bus.get_object("org.mate.SessionManager", client_path, "org.mate.SessionManager.Client") + sm_client_id = session_manager_client_public_object.GetStartupId() + gtk.gdk.set_sm_client_id(sm_client_id) + connected_to_session_manager = True + logger.info("Connected to MATE session manager") + except Exception as e: + logger.info("MATE session manager not available.") + logger.debug("MATE connection attempt below :", exc_info = e) + + if not connected_to_session_manager: + logger.warning("None of the supported session managers available, will not detect logout signal for clean shutdown.") + + + def _sm_query_end_session(self, flags): + if self._is_monitor_session(): + logger.info("Querying for end session") + self._sm_client_dbus_will_quit(True, "") + + def _sm_cancel_end_session(self): + if self._is_monitor_session(): + if not self.session_active: + logger.info("Cancelled session end, starting up again") + self.session_active = True + self.start_service() + else: + logger.info("Cancelled session end, but we haven't started shutdown yet") + + def _sm_end_session(self, flags): + if self._is_monitor_session(): + logger.info("Ending session") + def e(): + self.stop() + self._sm_client_dbus_will_quit(True, "") + g15scheduler.queue(SERVICE_QUEUE, "endSession", 0.0, e) + + def _sm_client_dbus_will_quit(self, can_quit=True, reason=""): + self.session_manager_client_object.EndSessionResponse(can_quit,reason) + + def _sm_stop(self): + logger.info("Shutdown quickly") + self.shutdown(True) + + def _is_monitor_session(self): + return g15gconf.get_bool_or_default(self.conf_client, "/apps/gnome15/monitor_desktop_session", True) + + def _connect_to_consolekit(self): + try : + logger.info("Connecting to ConsoleKit") + self.system_bus.add_signal_receiver(self._active_session_changed, dbus_interface="org.freedesktop.ConsoleKit.Seat", signal_name="ActiveSessionChanged") + console_kit_object = self.system_bus.get_object("org.freedesktop.ConsoleKit", '/org/freedesktop/ConsoleKit/Manager') + console_kit_manager = dbus.Interface(console_kit_object, 'org.freedesktop.ConsoleKit.Manager') + logger.info("Seats %s ", str(console_kit_manager.GetSeats())) + self.this_session_path = console_kit_manager.GetSessionForCookie (os.environ['XDG_SESSION_COOKIE']) + logger.info("This session %s", self.this_session_path) + + # TODO GetCurrentSession doesn't seem to work as i would expect. Investigate. For now, assume we are the active session +# current_session = console_kit_manager.GetCurrentSession() +# logger.info("Current session %s ", current_session) +# self.session_active = current_session == self.this_session_path + self.session_active = True + + logger.info("Connected to ConsoleKit") + connected_to_system_session_manager = True + except Exception as e: + logger.warning("ConsoleKit not available", exc_info = e) + + def _connect_to_logind(self): + try : + logger.info("Connecting to logind") + self.system_bus.add_signal_receiver(self._logind_seat0_property_changed, "PropertiesChanged", "org.freedesktop.DBus.Properties", "org.freedesktop.login1", "/org/freedesktop/login1/seat/seat0") + self.this_session_path = self._get_systemd_active_session_path() + logger.info("This session %s ", self.this_session_path) + + self.session_active = True + + logger.info("Connected to logind") + connected_to_system_session_manager = True + except Exception as e: + logger.warning("logind not available.", exc_info = e) + + def _get_systemd_active_session_path(self): + seat0_object = self.system_bus.get_object("org.freedesktop.login1", '/org/freedesktop/login1/seat/seat0') + seat0_properties_interface = dbus.Interface(seat0_object, 'org.freedesktop.DBus.Properties') + id, session_path = seat0_properties_interface.Get('org.freedesktop.login1.Seat', 'ActiveSession') + return session_path + + def _logind_seat0_property_changed(self, interface, dicto, properties): + if "ActiveSession" in properties: + if self._is_monitor_session(): + session_path = self._get_systemd_active_session_path() + logger.info("This session %s", session_path) + self.session_active = session_path == self.this_session_path + if self.session_active: + logger.info("g15-desktop service is running on the active session") + else: + logger.info("g15-desktop service is NOT running on the active session") + g15scheduler.queue(SERVICE_QUEUE, "activeSessionChanged", 0.0, self._check_state_of_all_devices) + + def _active_session_changed(self, object_path): + logger.debug("Adding seat %s", object_path) + if self._is_monitor_session(): + self.session_active = object_path == self.this_session_path + if self.session_active: + logger.info("g15-desktop service is running on the active session") + else: + logger.info("g15-desktop service is NOT running on the active session") + g15scheduler.queue(SERVICE_QUEUE, "activeSessionChanged", 0.0, self._check_state_of_all_devices) + + def _configure_window_monitoring(self): + logger.info("Attempting to set up BAMF") + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + self.session_bus.add_signal_receiver(self._active_window_changed, dbus_interface = 'org.ayatana.bamf.matcher', signal_name="ActiveWindowChanged") + active_window = self.bamf_matcher.ActiveWindow() + logger.info("Will be using BAMF for window matching") + if active_window: + self._active_window_changed("", active_window) + except Exception as e: + logger.info("BAMF not available, falling back to polling WNCK.") + logger.debug("BAMF attempt below :", exc_info = e) + try : + import wnck + wnck.__file__ + self._check_active_application_with_wnck() + except Exception as e: + logger.warning("Python Wnck not available either, no automatic profile switching", exc_info = e) + + def _add_screen(self, device): + try: + screen = g15screen.G15Screen(g15pluginmanager, self, device) + self.screens.append(screen) + for listener in self.service_listeners: + listener.screen_added(screen) + return screen + except Exception as e: + logger.error("Failed to load driver for device %s.", device.uid, exc_info = e) + + def _hidden_configuration_changed(self, client, connection_id, entry, device): + self._load_hidden_configuration() + + def _load_hidden_configuration(self): + self.scroll_delay = float(g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/scroll_delay', 500)) / 1000.0 + self.scroll_amount = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/scroll_amount', 5) + self.animated_menus = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/animated_menus', True) + self.animation_delay = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/animation_delay', 100) / 1000.0 + self.key_hold_duration = g15gconf.get_int_or_default(self.conf_client, '/apps/gnome15/key_hold_duration', 2000) / 1000.0 + self.macro_handler.use_x_test = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/use_x_test', True) + self.disable_svg_glow = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/disable_svg_glow', False) + self.fade_screen_on_close = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/fade_screen_on_close', True) + self.all_off_on_disconnect = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/all_off_on_disconnect', True) + self.fade_keyboard_backlight_on_close = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/fade_keyboard_backlight_on_close', True) + self.start_in_threads = g15gconf.get_bool_or_default(self.conf_client, '/apps/gnome15/start_in_threads', False) + self._mark_all_pages_dirty() + + def _mark_all_pages_dirty(self): + for screen in self.screens: + for page in screen.pages: + page.mark_dirty() + + def _device_enabled_configuration_changed(self, client, connection_id, entry, device): + g15scheduler.queue(SERVICE_QUEUE, "deviceStateChanged", 0, self._check_device_state, device) + + def _check_device_state(self, device, quickly = False): + enabled = device in self.devices and g15devices.is_enabled(self.conf_client, device) and self.session_active + screen = self._get_screen_for_device(device) + if enabled and not screen: + logger.info("Enabling device %s", device.uid) + # Enable screen + screen = self._add_screen(device) + if screen: + screen.start() + logger.info("Enabled device %s", device.uid) + elif not enabled and screen: + # Disable screen + logger.info("Disabling device %s", device.uid) + screen.stop(quickly) + self.screens.remove(screen) + for listener in self.service_listeners: + listener.screen_removed(screen) + logger.info("Disabled device %s", device.uid) + + # If there is a single device, stop the service as well + if len(self.devices) == 0 and ( not g15devices.have_udev or self.exit_on_no_devices ): + self.shutdown(False) + + def _device_added(self, device): + self.devices = g15devices.find_all_devices() + self._check_device_state(device) + self.device_notify_handles[device.uid] = self.conf_client.notify_add("/apps/gnome15/%s/enabled" % device.uid, self._device_enabled_configuration_changed, device) + + def _device_removed(self, device): + self.devices = g15devices.find_all_devices() + self._check_device_state(device) + self.conf_client.notify_remove(self.device_notify_handles[device.uid]) + del self.device_notify_handles[device.uid] + + def _get_screen_for_device(self, device): + for screen in self.screens: + if screen.device.uid == device.uid: + return screen + + def __del__(self): + for screen in self.screens: + if screen.plugins.is_active(): + screen.plugins.deactivate() + if screen.plugins.is_started(): + screen.plugins.destroy() + del self.screens diff --git a/src/gnome15/g15system.py b/src/gnome15/g15system.py new file mode 100644 index 0000000..66750de --- /dev/null +++ b/src/gnome15/g15system.py @@ -0,0 +1,229 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject +import g15globals +import signal +import dbus.service +import os.path +import g15devices +import g15driver +import util.g15scheduler as g15scheduler + +# Logging +import logging +logger = logging.getLogger(__name__) + +NAME = "Gnome15" +VERSION = g15globals.version +BUS_NAME="org.gnome15.SystemService" +OBJECT_PATH="/org/gnome15/SystemService" +IF_NAME="org.gnome15.SystemService" + +""" +Maps model id's to driver names +""" +driver_names = { + g15driver.MODEL_G15_V1: "g15", + g15driver.MODEL_G15_V2: "g15", + g15driver.MODEL_G19: "g19", + g15driver.MODEL_G110: "g110", + g15driver.MODEL_G13: "g13", + } + +class SystemService(dbus.service.Object): + + def __init__(self, bus, controller): + bus_name = dbus.service.BusName(BUS_NAME, bus=bus, replace_existing=False, allow_replacement=False, do_not_queue=True) + dbus.service.Object.__init__(self, None, OBJECT_PATH, bus_name) + self._controller = controller + + """ + DBUS API + """ + + @dbus.service.method(IF_NAME) + def Stop(self): + self._controller.stop() + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetInformation(self): + return ( "%s System Service" % g15globals.name, "Gnome15 Project", g15globals.version, "1.0" ) + + @dbus.service.method(IF_NAME, in_signature='ssn') + def SetLight(self, device, light, value): + self._controller.devices[device].leds[light].set_led_value(value); + + @dbus.service.method(IF_NAME, in_signature='ss', out_signature='n') + def GetLight(self, device, light): + return self._controller.devices[device].leds[light].get_value() + + @dbus.service.method(IF_NAME, out_signature='as') + def GetDevices(self): + c = [] + for l in self._controller.devices: + c.append(l) + return c + + @dbus.service.method(IF_NAME, in_signature='s', out_signature='as') + def GetLights(self, device): + return self._controller.devices[device].leds.keys() + + @dbus.service.method(IF_NAME, in_signature='ss', out_signature='n') + def GetMaxLight(self, device, light): + return self._controller.devices[device].leds[light].get_max() + + +def get_int_value(filename): + return int(get_value(filename)) + +def get_value(filename): + fd = open(filename, "r") + try : + return fd.read() + finally : + fd.close() + +def set_value(filename, value): + g15scheduler.execute("System", "setValue", _do_set_value, filename, value); + +def _do_set_value(filename, value): + logger.debug("Writing %s to %s", filename, value) + fd = open(filename, "w") + try : + fd.write("%s\n" % str(value)) + finally : + fd.close() + +class KeyboardDevice(): + def __init__(self, device, device_path, index): + self.leds = {} + self.device = device + self.device_path = device_path + self.minor = get_int_value(os.path.join(device_path, "minor")) + self.uid = "%s_%d" % ( device.model_id, index ) + leds_path = os.path.join(device_path, "leds") + for d in os.listdir(leds_path): + f = os.path.join(leds_path, d) + keyboard_device, color, control = d.split(":") + keyboard_device, index = keyboard_device.split("_") + light_key = "%s:%s" % ( color, control ) + self.leds[light_key] = LED(light_key, self, f) + + +class LED(): + """ + Represents a single LED, the keyboard it is linked to, + and the /sys filename for the LED + """ + def __init__(self, light_key, keyboard_device, filename): + self.light_key = light_key + self.keyboard_device = keyboard_device + self.filename = filename + + def set_led_value(self, val): + """ + Set the current brightness of the LED + + Keyword arguments: + val -- + """ + if val < 0 or val > self.get_max(): + raise Exception("LED value out of range") + set_value(os.path.join(self.filename, "brightness"), val) + + def get_value(self): + return get_int_value(os.path.join(self.filename, "brightness")) + + def get_max(self): + return get_int_value(os.path.join(self.filename, "max_brightness")) + +DEVICES_PATH="/sys/bus/hid/devices" + +class G15SystemServiceController(): + + def __init__(self, bus, no_trap=False): + self._page_sequence_number = 1 + self._bus = bus + self.devices = {} + logger.debug("Exposing service") + + if not no_trap: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + + self._loop = gobject.MainLoop() + gobject.idle_add(self._start_service) + + def stop(self): + self._loop.quit() + + def start_loop(self): + logger.info("Starting GLib loop") + self._loop.run() + logger.debug("Exited GLib loop") + + def sigint_handler(self, signum, frame): + logger.info("Got SIGINT signal, shutting down") + self.shutdown() + + def sigterm_handler(self, signum, frame): + logger.info("Got SIGTERM signal, shutting down") + self.shutdown() + + def shutdown(self): + logger.info("Shutting down") + self._loop.quit() + + """ + Private + """ + def _start_service(self): + self._scan_devices() + SystemService(self._bus, self) + + def _scan_devices(self): + self.devices = {} + indices = {} + if os.path.exists(DEVICES_PATH): + for device in os.listdir(DEVICES_PATH): + # Only want devices with leds + device_path = os.path.join(DEVICES_PATH, device) + leds_path = os.path.join(device_path, "leds") + if os.path.exists(leds_path): + # Extract the USB ID + a = device.split(":") + usb_id = ( int("0x%s" % a[1], 16), int("0x%s" % a[2].split(".")[0], 16) ) + logger.info("Testing if device %04x:%04x is supported by Gnome15", + usb_id[0], + usb_id[1]) + + # Look for a matching Gnome15 device + for device in g15devices.find_all_devices(): + if device.controls_usb_id == usb_id: + # Found a device we want + logger.info("Found device %s", str(device)) + + # Work out UID + # TODO this is not quite right - if there is more than one device of same type, indexs might not match + index = 0 if not device.model_id in indices else indices[device.model_id] + keyboard_device = KeyboardDevice(device, device_path, index) + self.devices[device.uid] = keyboard_device + indices[device.model_id] = index + 1 + + else: + logger.info("No devices found at %s", DEVICES_PATH) + diff --git a/src/gnome15/g15text.py b/src/gnome15/g15text.py new file mode 100644 index 0000000..65dd07f --- /dev/null +++ b/src/gnome15/g15text.py @@ -0,0 +1,161 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pango +import pangocairo +import cairo +import gobject +import logging +logger = logging.getLogger(__name__) + +# Shared pango context +pango_context = pangocairo.cairo_font_map_get_default().create_context() + +""" +Handles drawing and measuring of text on a screen. +""" + + +def new_text(screen = None): + """ + Create a new text handler. This should be used rather than directly constructing + the G15PangoText + """ + if screen: + return G15PangoText(screen.driver.get_antialias()) + else: + return G15PangoText(True) + +class G15Text(object): + + def __init__(self, antialias): + self.antialias = antialias + + def set_attributes(self, text, bounds): + self.text = text + self.bounds = bounds + + def measure(self): + raise Exception("Not implemented") + + def draw(self, x, y, clip = None): + raise Exception("Not implemented") + + def set_canvas(self, canvas): + self.canvas = canvas + + """ + Private + """ + def _create_font_options(self): + fo = cairo.FontOptions() + fo.set_antialias(self.antialias) + if self.antialias == cairo.ANTIALIAS_NONE: + fo.set_hint_style(cairo.HINT_STYLE_NONE) + fo.set_hint_metrics(cairo.HINT_METRICS_OFF) + +class G15PangoText(G15Text): + + def __init__(self, antialias): + G15Text.__init__(self, antialias) + pangocairo.context_set_font_options(pango_context, self._create_font_options()) + self.__pango_cairo_context = None + self.__layout = None + self.valign = pango.ALIGN_CENTER + self.__layout = pango.Layout(pango_context) + + def set_canvas(self, canvas): + G15Text.set_canvas(self, canvas) + self.__pango_cairo_context = pangocairo.CairoContext(self.canvas) + + def set_attributes(self, text, bounds = None, wrap = None, align = pango.ALIGN_LEFT, width = None, spacing = None, \ + font_desc = None, font_absolute_size = None, attributes = None, + weight = None, style = None, font_pt_size = None, + valign = None, pxwidth = None): + + logger.debug("Text: %s, bounds = %s, wrap = %s, align = %s, width = %s, " \ + "attributes = %s, spacing = %s, font_desc = %s, weight = %s, " \ + "style = %s, font_pt_size = %s", + str(text), + str(bounds), + str(wrap), + str(align), + str(width), + str(attributes), + str(spacing), + str(font_desc), + str(weight), + str(style), + str(font_pt_size)) + + G15Text.set_attributes(self, text, bounds) + self.valign = valign + + font_desc_name = "Sans" if font_desc == None else font_desc + if weight: + font_desc_name += " %s" % weight + if style: + font_desc_name += " %s" % style + if font_pt_size: + font_desc_name += " " + str(font_pt_size) + font_desc = pango.FontDescription(font_desc_name) + if font_absolute_size is not None: + font_desc.set_absolute_size(font_absolute_size) + self.__layout.set_font_description(font_desc) + + if align != None: + self.__layout.set_alignment(align) + if spacing != None: + self.__layout.set_spacing(spacing) + if width != None: + self.__layout.set_width(width) + if pxwidth != None: + self.__layout.set_width(int(pango.SCALE * pxwidth)) + if wrap: + self.__layout.set_wrap(wrap) + if attributes: + self.__layout.set_attributes(attributes) + + self.__layout.set_text(text) + self.metrics = pango_context.get_metrics(self.__layout.get_font_description()) + + def measure(self): + text_extents = self.__layout.get_extents()[1] + return text_extents[0] / pango.SCALE, text_extents[1] / pango.SCALE, text_extents[2] / pango.SCALE, text_extents[3] / pango.SCALE + + def draw(self, x = None, y = None): + self.__pango_cairo_context.save() + + if self.bounds is not None: + if x == None: + x = self.bounds[0] + if y == None: + y = self.bounds[1] + + self.__pango_cairo_context.rectangle(self.bounds[0] - 1, self.bounds[1] - 1, self.bounds[2] + 2, self.bounds[3] + 2) + self.__pango_cairo_context.clip() + + if self.valign == pango.ALIGN_RIGHT: + y += self.bounds[3] - ( self.metrics.get_ascent() / 1000.0 ) + elif self.valign == pango.ALIGN_CENTER: + y += ( self.bounds[3] - ( self.metrics.get_ascent() / 1000.0 ) ) / 2 + + if x is not None and y is not None: + self.__pango_cairo_context.move_to(x, y) + + self.__pango_cairo_context.show_layout(self.__layout) + self.__pango_cairo_context.restore() + diff --git a/src/gnome15/g15theme.py b/src/gnome15/g15theme.py new file mode 100644 index 0000000..a5f1cde --- /dev/null +++ b/src/gnome15/g15theme.py @@ -0,0 +1,2227 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module contains all the classes required for the Gnome15 component and themeing +system. + +The basis of this is a component hierarchy that starts with a G15Page (actually, the +'screen' is kind of the root component, but that anomaly will be fixed). + +All components may contain children. Whether or not they are rendered is down to +the individual parent component. There is currently a limited set of container type +components, including Component itself, Page and Menu. Menu's children must be MenuItem +objects (or a subclass of MenuItem). Page's may contain any type of child. + +Each component has a 'Theme' associated with it. This is an SVG file that is rendered +at painting time. A Page's theme will take up all available space and be rendered at +0,0, where as child components will be rendered at and within bounds defined in the +parent's theme file (by using an svg:rect with an ID that links the two), or at a +place calculated by the component itself (for example, MenuItem children). + +The Theme is also responsible for handling automatic text scrolling, as well as processing +the SVG by doing string replacements and other manipulations such as those required +for progress bars, scroll bars. +""" + +import os +import cairo +import rsvg +import sys +import pango +import g15driver +import g15globals +import g15screen +import util.g15convert as g15convert +import util.g15scheduler as g15scheduler +import g15text +import g15locale +import util.g15cairo as g15cairo +import util.g15svg as g15svg +import util.g15icontools as g15icontools +import xml.sax.saxutils as saxutils +import base64 +import dbusmenu +import logging +import time +logger = logging.getLogger(__name__) +from string import Template +from copy import deepcopy +from cStringIO import StringIO +from lxml import etree +from threading import RLock +import ConfigParser + +BASE_PX=18.0 +DEBUG_SVG=False + +# The color in SVG theme files that by default gets replaced with the current 'highlight' color +DEFAULT_HIGHLIGHT_COLOR="#ff0000" + +class ThemeDefinition(object): + def __init__(self, theme_id, directory, plugin_module = None): + self.theme_id = theme_id + self.plugin_module = plugin_module + self.directory = directory + self.supported = [] + self.unsupported = [] + filename = os.path.join(self.directory, "%s.theme" % theme_id) + if not os.path.exists(filename): + if theme_id == "default": + self.name = "Simple" + self.description = "Default theme supplied with Gnome15" + else: + raise Exception("No theme descriptor %s" % filename) + else: + parser = ConfigParser.ConfigParser({}) + parser.read(filename) + self.name = parser.get("theme", "name") + self.description = parser.get("theme", "description") + if parser.has_option("theme", "supported_models"): + self.supported = parser.get("theme", "supported_models").split(",") + if parser.has_option("theme", "unsupported_models"): + self.unsupported = parser.get("theme", "unsupported_models").split(",") + + # Load any translations for this theme + tdomain = "%s.%s" % ( plugin_module.id, theme_id ) + self.translation = g15locale.get_translation(tdomain, self.directory) + if self.translation: + logger.info("Found translation %s", tdomain) + + def supports(self, model_id): + return ( len(self.supported) == 0 or model_id in self.supported ) \ + and not model_id in self.unsupported + +def get_theme(theme_id, plugin_module): + """ + Get a theme definition give it's ID and the plugin that contains it + + Keyword arguments: + theme_id -- theme ID + plugin_module -- plugin + """ + module_dir = os.path.dirname(plugin_module.__file__) + theme_dir = os.path.join(module_dir, theme_id) + if os.path.isdir(theme_dir) and ( theme_id == "default" or \ + os.path.exists(os.path.join(theme_dir, "%s.theme" % theme_id))): + return ThemeDefinition(theme_id, theme_dir, plugin_module) + +def get_themes(model_id, plugin_module): + """ + Get a list of themes this plugin supports for the requested model + + Keyword arguments: + model_id -- model support is required for + plugin_module -- plugin + """ + themes = [] + module_dir = os.path.dirname(plugin_module.__file__) + for d in os.listdir(module_dir): + theme_dir = os.path.join(module_dir, d) + if os.path.isdir(theme_dir) and ( d == "default" or \ + os.path.exists(os.path.join(theme_dir, "%s.theme" % d))): + definition = ThemeDefinition(d, theme_dir, plugin_module) + if definition.supports(model_id): + themes.append(definition) + return themes + +class Render(object): + def __init__(self, document, properties, text_boxes, attributes, processing_result): + self.document = document + self.properties = properties + self.text_boxes = text_boxes + self.attributes = attributes + self.processing_result = processing_result + +class ScrollState(object): + + def __init__(self): + self.range = (0.0, 0.0) + self.adjust = 0.0 + self.reversed = True + self.step = 1.0 + self.alignment = pango.ALIGN_LEFT + self.val = 0 + self.original = 0 + + def reset(self): + self.adjust = 0.0 + self.do_transform() + + def next(self): + self.adjust += -self.step if self.reversed else self.step + if self.adjust < self.range[0] and self.reversed: + self.reversed = False + self.adjust = self.range[0] + elif self.adjust > self.range[1] and not self.reversed: + self.adjust = self.range[1] + self.reversed = True + self.do_transform() + + def do_transform(self): + self.val = self.adjust + self.original + self.transform_elements() + +class HorizontalScrollState(ScrollState): + + def __init__(self, element = None): + ScrollState.__init__(self) + self.element = element + self.other_elements = [] + + def transform_elements(self): + self.element.set("x", str(int(self.val))) + for e in self.other_elements: + e.set("x", str(int(self.val))) + + def next(self): + ScrollState.next(self) + +class VerticalWrapScrollState(ScrollState): + def __init__(self, text_box): + ScrollState.__init__(self) + self.text_box = text_box + + def transform_elements(self): + self.text_box.base = self.val + +class TextBox(object): + def __init__(self): + self.bounds = ( ) + self.clip = () + self.align = "start" + self.text = "" + self.wrap = False + self.css = { } + self.normal_shadow = False + self.reverse_shadow = False + self.transforms = [] + self.base = 0 + +class LayoutManager(object): + def __init__(self): + pass + + def layout(self, parent): + raise Exception("Not implemeted") + +class GridLayoutManager(LayoutManager): + + def __init__(self, columns, rows = -1): + self.rows = rows + self.columns = columns + + def layout(self, parent): + x = 0 + y = 0 + col = 0 + row_height = 0 + for c in parent.get_children(): + if c.is_showing(): + bounds = c.view_bounds + if bounds is None: + logger.warning("No bounds on component %s", c.id) + else: + c.view_bounds = ( x, y, bounds[2], bounds[3]) + x += bounds[2] + row_height = max(row_height, bounds[3]) + col += 1 + if col >= self.columns: + x = 0 + y += row_height + row_height = 0 + col = 0 + + +class Childmap(dict): + def __init__(self): + type(self).__name__ = "Childmap" + dict.__init__(self) + +class Childlist(list): + def __init__(self, l = []): + type(self).__name__ = "Childlist" + list.__init__(self, l) + +class Component(object): + + def __init__(self, id): + self.id = id + self.theme = None + self._children = Childlist() + self.child_map = Childmap() + self.parent = None + self.screen = None + self.enabled = True + self.theme_properties = {} + self.theme_attributes = {} + self.theme_properties_callback = None + self.theme_attributes_callback = None + self.view_bounds = None + self.view_element = None + self.layout_manager = None + self.base = 0 + self.focusable = False + self._tree_lock = RLock() + self.do_clip = False + self.allow_scrolling = None + self.showing = True + self.activatable = False + self.scrollbar = None + + def set_scrollbar(self, scrollbar): + self.scrollbar = scrollbar + scrollbar.viewport = self + + def is_enabled(self): + return self.enabled + + def get_tree_lock(self): +# if self.parent == None: +# return self._tree_lock +# else: +# return self.parent.get_tree_lock() + return self._tree_lock + + def clear_scroll(self): + if self.theme: + self.theme.clear_scroll() + + def is_showing(self): + return self.showing + + def set_showing(self, showing): + self.showing = showing + + def get_showing_count(self): + i = 0 + for c in self._children: + if c.is_showing(): + i += 1 + return i + + def is_focused(self): + return self.get_root().focused_component == self + + def set_focused_component(self, component): + self.focused_component = component + + def set_focused(self, focused): + if not self.focusable: + raise Exception("%s is not focusable" % self.id) + if focused: + self.get_root().set_focused_component(self) + elif self.get_root().focused == self: + self.get_root().set_focused_component(None) + self.get_root().next_focus() + + def set_theme(self, theme): + self.get_tree_lock().acquire() + try: + if self.theme is not None: + self.theme._set_component(None) + self.theme = theme + theme._set_component(self) + self.view_bounds = theme.bounds + for c in self.get_children(): + c.configure(self) + finally: + self.get_tree_lock().release() + + def mark_dirty(self): + if self.theme is not None: + self.theme.mark_dirty() + for c in self.get_children(): + c.mark_dirty() + if c.scrollbar is not None: + c.scrollbar.mark_dirty() + + def get_allow_scrolling(self): + c = self + while c is not None: + if c.allow_scrolling is not None: + return c.allow_scrolling + c = c.parent + return True + + def do_scroll(self): + for c in self._children: + c.do_scroll() + if self.theme and self.get_allow_scrolling(): + self.theme.do_scroll() + + def check_for_scroll(self): + scroll = False + for c in self._children: + if c.check_for_scroll(): + scroll = True + if self.theme and self.get_allow_scrolling() and self.theme.is_scroll_required(): + scroll = True + return scroll + + def get_theme(self): + c = self + while c is not None: + if c.theme: + return c.theme + c = c.parent + + def get_screen(self): + c = self + while c is not None: + if c.screen: + return c.screen + c = c.parent + + def get_root(self): + c = self + r = None + while c is not None: + r = c + c = c.parent + return r + + def index_of_child(self, child): + return self._children.index(child) + + def get_child(self, index): + return self._children[index] + + def get_child_by_id(self, id): + return self.child_map[id] if id in self.child_map else None + + def contains_child(self, child): + return child in self._children + + def get_child_count(self): + return len(self._children) + + def set_children(self, children): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + # Remove any children that we currently have, but are not in the new list + for c in list(set(self._children) - set(children)): + self.remove_child(c) + + # Add any new children + for c in list(set(children) - set(self._children)): + self.add_child(c) + + # Now just change out child list to the new one so the order is correct + self._children = Childlist(children) + finally: + self.get_tree_lock().release() + + def get_children(self): + return list(self._children) + + def add_child(self, child, index = -1): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + if child.parent: + raise Exception("Child %s already has a parent. Remove it from it's last parent first before adding to %s." % (child.id, self.id)) + if child.id in self.child_map: + raise Exception("Child with ID of %s already exists in component %s. Trying to add %s, but %s exists" % (child.id, self.id, str(child), str(self.child_map[child.id]))) + self._check_has_parent() + child.configure(self) + self.child_map[child.id] = child + if index == -1: + self._children.append(child) + else: + self._children.insert(index, child) + self.mark_dirty() + self.notify_add(child) + finally: + self.get_tree_lock().release() + + def remove_all_children(self): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + for c in list(self._children): + self.remove_child(c) + finally: + self.get_tree_lock().release() + + def remove_child(self, child): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + if not child in self._children: + raise Exception("Not a child of this component.") + child.notify_remove() + if child.theme: + child.theme._component_removed() + child.parent = None + del self.child_map[child.id] + self._children.remove(child) + finally: + self.get_tree_lock().release() + + def remove_child_at(self, index): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + self.remove_child(self._children[index]) + finally: + self.get_tree_lock().release() + + def remove_from_parent(self): + g15screen.check_on_redraw() + if not self.parent: + raise Exception("Not added to a parent.") + self.parent.remove_child(self) + + def configure(self, parent): + self.parent = parent + self.on_configure() + theme = self.get_theme() + if theme == None: + logger.warning("No theme for component with ID of %s", self.id) + else: + self.view_element = theme.get_element(self.id) + if self.view_element is None: + self.view_element = theme.get_element() + self.view_bounds = g15svg.get_actual_bounds(self.view_element) if self.view_element is not None else None + + def is_visible(self): + return self.parent != None and self.parent.is_visible() + + def on_configure(self): + pass + + def get_default_theme_dir(self): + return os.path.join(g15globals.themes_dir, "default") + + def draw(self, element, theme): + """ + Called by the theme for the component to adjust the SVG document ID if required + """ + pass + + def paint_theme(self, canvas, properties, attributes): + """ + Paint the theme. Do not call directly, instead call paint() + """ + self.theme.draw(canvas, properties, self.get_theme_attributes()) + + def paint(self, canvas): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + canvas.save() + + # Translate to the components bounds and clip to the size of the view + if self.view_bounds: + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.rectangle(0, 0, self.view_bounds[2], self.view_bounds[3]) + canvas.clip() + + # Translate against the base, this allows components to be scrolled within their viewport + canvas.translate(0, -self.base) + + # Draw any theme for this component + if self.theme is not None: + canvas.save() + properties = self.get_theme_properties() + + # Add some common properties + if self.get_root().focused_component is not None: + properties['%s_focused' % self.get_root().focused_component.id ] = "true" + + screen = self.get_screen() + if screen: + states = screen.key_handler.get_key_states() + for k in states: + ks = states[k] + if ks.state_id == g15driver.KEY_STATE_DOWN: + properties['key_%s' % k ] = True + elif ks.state_id == g15driver.KEY_STATE_HELD: + properties['key_%s_held' % k ] = True + + self.paint_theme(canvas, properties, self.get_theme_attributes()) + canvas.restore() + + # Layout any children + if self.layout_manager != None: + self.layout_manager.layout(self) + + # Paint children + for c in self._children: + if c.is_showing(): + canvas.save() + if not self.do_clip or c.view_bounds is None or self.overlaps(self.view_bounds, c.view_bounds): + c.paint(canvas) + canvas.restore() + + canvas.restore() + finally: + self.get_tree_lock().release() + + def overlaps(self, bounds1, bounds2): + return bounds2[1] >= ( self.base - bounds2[3] ) and bounds2[1] < ( self.base + bounds1[3] ) + + def get_theme_properties(self): + p = None + if self.theme_properties_callback is not None: + p = self.theme_properties_callback() + if p is None: + p = self.theme_properties + return p + + def get_theme_attributes(self): + p = None + if self.theme_attributes_callback is not None: + p = self.theme_attributes_callback() + if p is None: + p = self.theme_attributes + return p + + def notify_add(self, component): + if self.parent: + self.parent.notify_add(component) + + def notify_remove(self): + self.remove_all_children() + + ''' + Private + ''' + def _check_has_parent(self): +# if not self.parent: +# raise Exception("%s must be added to a parent before children can be added to it." % self.id) + pass + + +class G15Page(Component): + def __init__(self, page_id, screen, painter = None, priority = g15screen.PRI_NORMAL, on_shown=None, on_hidden=None, on_deleted=None, \ + thumbnail_painter = None, panel_painter = None, theme_properties_callback = None, \ + theme_attributes_callback = None, theme = None, title = None, + originating_plugin = None): + Component.__init__(self, page_id) + self.title = title if title else self.id + self.time = time.time() + self.originating_plugin = originating_plugin + self.thumbnail_painter = thumbnail_painter + self.panel_painter = panel_painter + self.on_shown = on_shown + self.on_hidden = on_hidden + self.on_deleted = on_deleted + self.priority = priority + self.value = self.priority * self.time + self.painter = painter + self.cairo = cairo + self.theme_scroll_timer = None + self.opacity = 0 + self.key_handlers = [] + self.properties = {} + self.attributes = {} + self.back_buffer = None + self.buffer = None + self.back_context = None + self.font_size = 12.0 + self.font_family = "Sans" + self.font_style = "normal" + self.font_weight = "normal" + self.on_shown_listeners = [] + self.on_hidden_listeners = [] + self.on_deleted_listeners = [] + self.theme_properties_callback = theme_properties_callback + self.theme_attributes_callback = theme_attributes_callback + self.screen = screen + self.scroll_lock = RLock() + self.focused_component = None + self.text_handler = g15text.new_text(screen) + if theme: + self.set_theme(theme) + + def set_focused_component(self, focused_component, redraw = True): + self.focused_component = focused_component + if redraw: + self.redraw() + + def notify_add(self, component): + Component.notify_add(self, component) + if not self.focused_component and component.focusable: + self.next_focus(False) + + def redraw(self, queue = True): + screen = self.get_screen() + if screen: + screen.redraw(self, queue) + + def next_focus(self, redraw = True): + focus_list = self._add_to_focus_list(self, []) + if len(focus_list) == 0: + self.focused_component = None + return + + if self.focused_component and self.focused_component in focus_list: + i = focus_list.index(self.focused_component) + i += 1 + if i >= len(focus_list): + i = 0 + self.focused_component = focus_list[i] + else: + self.focused_component = focus_list[0] + self.mark_dirty() + if redraw: + self.redraw() + + def _add_to_focus_list(self, component, focus_list = []): + if component.focusable: + focus_list.append(component) + for c in component.get_children(): + self._add_to_focus_list(c, focus_list) + return focus_list + + def is_visible(self): + screen = self.get_screen() + return screen and screen.get_visible_page() == self + + def set_title(self, title): + self.title = title + screen = self.get_screen() + if screen and screen.get_page(self.id) is not None: + screen.page_title_changed(self, title) + + def set_priority(self, priority): + screen = self.get_screen() + if not screen: + raise Exception("Cannot set priority, not added to screen") + screen.set_priority(self, priority) + + def set_time(self, time): + self.time = time + self.value = self.priority * self.time + + def get_val(self): + return self.time * self.priority + + def new_surface(self): + screen = self.get_screen() + if not screen: + raise Exception("Cannot create new surface, not added to screen") + sw = screen.driver.get_size()[0] + sh = screen.driver.get_size()[1] + self.back_buffer = cairo.ImageSurface (cairo.FORMAT_ARGB32,sw, sh) + self.back_context = cairo.Context(self.back_buffer) + self.text_handler = g15text.new_text(screen) + screen.configure_canvas(self.back_context) + self.text_handler.set_canvas(self.back_context) + self.set_line_width(1.0) + rgb = screen.driver.get_color(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + self.foreground(rgb[0],rgb[1],rgb[2], 255) + + def draw_surface(self): + self.buffer = self.back_buffer + + def foreground(self, r, g, b, a = 255): + self.foreground_rgb = (r, g, b, a) + self.back_context.set_source_rgba(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0, float(a) / 255.0) + + def save(self): + self.back_context.save() + + def delete(self): + self.screen.del_page(self) + + def restore(self): + self.back_context.restore() + + def set_line_width(self, line_width): + self.back_context.set_line_width(line_width) + + def arc(self, x, y, radius, angle1, angle2, fill = False): + self.back_context.arc(x, y, radius, g15convert.degrees_to_radians(angle1), g15convert.degrees_to_radians(angle2)) + if fill: + self.back_context.fill() + else: + self.back_context.stroke() + + def line(self, x1, y1, x2, y2): + self.back_context.line_to(x1, y1) + self.back_context.line_to(x2, y2) + self.back_context.stroke() + + def image(self, image, x, y): + self.back_context.translate(x, y) + self.back_context.set_source_surface(image) + self.back_context.paint() + self.back_context.translate(-x, -y) + + def rectangle(self, x, y, width, height, fill = False): + self.back_context.rectangle(x, y, width, height) + if fill: + self.back_context.fill() + else: + self.back_context.stroke() + + def paint(self, canvas): + self.text_handler.set_canvas(canvas) + if self.painter != None: + self.painter(canvas) + + Component.paint(self, canvas) + + # Paint the canvas + if self.buffer != None: + canvas.save() + canvas.set_source_surface(self.buffer) + canvas.paint() + canvas.restore() + + # Check the theme tree to see if anything needs scrolling + self.check_scroll_and_reschedule() + + def check_scroll_and_reschedule(self): + self.scroll_lock.acquire() + try: + scroll = self.check_for_scroll() + if scroll and self.theme_scroll_timer == None: + self.theme_scroll_timer = g15scheduler.schedule("ScrollRedraw", self.screen.service.scroll_delay, self.scroll_and_reschedule) + elif not scroll and self.theme_scroll_timer != None: + self.theme_scroll_timer.cancel() + self.theme_scroll_timer = None + finally: + self.scroll_lock.release() + + def scroll_and_reschedule(self): + self.scroll_lock.acquire() + try: + self.do_scroll() + self.theme_scroll_timer = None + self.redraw() + finally: + self.scroll_lock.release() + + def set_font(self, font_size = None, font_family = None, font_style = None, font_weight = None): + if font_size: + self.font_size = font_size + if font_family: + self.font_family = font_family + if font_style: + self.font_style = font_style + if font_weight: + self.font_weight = font_weight + + def text(self, text, x, y, width, height, constraints = ""): + bounds = None + if width > 0 and height > 0: + bounds = (x, y, width, height) + + al = constraints.split(",") + align = None + valign = None + wrap = None + wrap_width = None + for con in al: + if con == "wrapchar": + wrap = pango.WRAP_CHAR + elif con == "wrapword": + wrap = pango.WRAP_WORD + elif con == "wrapwordchar": + wrap = pango.WRAP_WORD_CHAR + else: + if align == None: + align = self._parse_align(con) + else: + valign = self._parse_align(con) + + wrap_width = int(pango.SCALE * width) if width > 0 and height > 0 else None + + self.text_handler.set_attributes(text, bounds, align = align, valign = valign, \ + font_desc = self.font_family, font_pt_size = self.font_size, \ + style = self.font_style, weight = self.font_weight, \ + width = wrap_width, wrap = wrap) + self.text_handler.draw(x, y) + + """ + Private + """ + def _parse_align(self, align): + if align == "center": + return pango.ALIGN_CENTER + elif align == "right" or align == "bottom": + return pango.ALIGN_RIGHT + else: + return pango.ALIGN_LEFT + + def _do_on_shown(self): + for l in self.on_shown_listeners: + l() + if self.on_shown: + self.on_shown() + + def _do_on_hidden(self): + for l in self.on_hidden_listeners: + l() + if self.on_hidden: + self.on_hidden() + + def _do_on_deleted(self): + if self.theme: + self.theme._component_removed() + for l in self.on_deleted_listeners: + l() + if self.on_deleted: + self.on_deleted() + + def _check_has_parent(self): + # Theme is the root, needs no parent + pass + + def _do_set_priority(self, priority): + self.priority = priority + self.value = self.priority * self.time + +class Scrollbar(Component): + + def __init__(self, id, values_callback = None): + Component.__init__(self, id) + self.values_callback = values_callback + + def on_configure(self): + Component.on_configure(self) + self._configure_track_and_bounds(self.get_theme(), self.get_theme().get_element(self.id)) + + def _configure_track_and_bounds(self, theme, element): + max_s, view_size, position = self.values_callback() + knob = element.xpath('svg:*[@class=\'knob\']',namespaces=theme.nsmap)[0] + track = element.xpath('svg:*[@class=\'track\']',namespaces=theme.nsmap)[0] + track_bounds = g15svg.get_bounds(track) + knob_bounds = g15svg.get_bounds(knob) + scale = max(1.0, max_s / view_size) + knob.set("y", str( int( knob_bounds[1] + ( position / max(scale, 0.01) ) ) ) ) + knob.set("height", str(int(track_bounds[3] / max(scale, 0.01) ))) + # TODO - don't destroy current styles + if scale == 1: + element.set("style", "visibility: hidden;") + else: + element.set("style", "") + + def draw(self, theme, element): + self._configure_track_and_bounds(theme, element) + +class Menu(Component): + def __init__(self, component_id): + Component.__init__(self, component_id) + self.selected = None + self.on_selected = None + self.on_move = None + self.i = 0 + self.do_clip = True + self.layout_manager = GridLayoutManager(1) + self.scroll_timer = None + + def set_scrollbar(self, scrollbar): + scrollbar.values_callback = self.get_scroll_values + Component.set_scrollbar(self, scrollbar) + + def select_last_item(self): + c = self.get_child_count() + if c > 0: + self.set_selected_item(self.get_children()[c - 1]) + + def set_selected_item(self, item): + i = self.index_of_child(item) + if i >= 0: + self.i = i + self._do_selected() + + def add_separator(self): + self.add_child(MenuSeparator()) + + def sort(self): + pass + + def on_configure(self): + menu_theme = self.load_theme() + if menu_theme: + self.set_theme(menu_theme) + + def configure(self, parent): + Component.configure(self, parent) + self._recalc_scroll_values() + if not self in self.get_screen().key_handler.action_listeners: + self.get_screen().key_handler.action_listeners.append(self) + + def notify_remove(self): + Component.notify_remove(self) + self.get_screen().key_handler.action_listeners.remove(self) + + def load_theme(self): + pass + + def add_child(self, child, index = -1): + Component.add_child(self, child, index) + self.select_first() + self._recalc_scroll_values() + self.centre_on_selected() + + def remove_child(self, child): + Component.remove_child(self, child) + self.select_first() + self._recalc_scroll_values() + self.centre_on_selected() + + def set_children(self, children): + was_selected = self.selected + Component.set_children(self, children) + if was_selected in self.get_children(): + self.selected = was_selected + else: + self.select_first() + self.centre_on_selected() + + def centre_on_selected(self): + y = 0 + c = self.get_children() + for r in range(0, self._get_selected_index()): + if c[r].is_showing(): + y += self.get_item_height(c[r], True) + self.base = max(0, y - ( self.view_bounds[3] / 2 )) + self._recalc_scroll_values() + self.get_root().redraw() + + def get_scroll_values(self): + return self.scroll_values + + def get_item_height(self, item, group = False): + if item.theme is None: + logger.warning("Component %s has no theme and so no height", item.id) + return 10 + else: + return item.theme.bounds[3] + + def paint(self, canvas): + g15screen.check_on_redraw() + self.get_tree_lock().acquire() + try: + + self.select_first() + + # Get the Y position of the selected item + y = 0 + selected_y = -1 + for item in self.get_children(): + # Only include items that are "showing" + if item.is_showing(): + ih = self.get_item_height(item, True) + if item == self.selected: + selected_y = y + y += ih + + new_base = self.base + + # How much vertical space there is + v_space = self.view_bounds[3] + + # If the position of the selected item is offscreen below, change the offset so it is just visible + if self.selected != None: + ih = self.get_item_height(self.selected, True) + if selected_y >= new_base + v_space - ih: + new_base = ( selected_y + ih ) - v_space + # If the position of the selected item is offscreen above base, change the offset so it is just visible + elif selected_y < new_base: + new_base = selected_y + + if new_base != self.base: + # Stop all of the children from scrolling horizontally while we scroll vertically + if self.get_screen().service.animated_menus: + if new_base < self.base: + self.base -= max(1, int(( self.base - new_base ) / 3)) + else: + self.base += max(1, int(( new_base - self.base ) / 3)) + else: + self.base = new_base + + self.get_root().mark_dirty() + self._recalc_scroll_values() + if self.scroll_timer is not None: + self.scroll_timer.cancel() + if self.get_screen().service.animated_menus: + self.scroll_timer = g15scheduler.schedule("ScrollTo", self.get_screen().service.animation_delay, self.get_root().redraw) + else: + self.get_root().redraw() + + Component.paint(self, canvas) + finally: + self.get_tree_lock().release() + + def get_items_per_page(self): + self.get_tree_lock().acquire() + try: + total_size = 0 + for item in self.get_children(): + total_size += self.get_item_height(item, True) + avg_size = total_size / self.get_child_count() + return int(self.view_bounds[3] / avg_size) + finally: + self.get_tree_lock().release() + + def action_performed(self, binding): + if self.is_visible(): + if binding.action == g15driver.NEXT_SELECTION: + self.get_screen().resched_cycle() + self._move_down(1) + return True + elif binding.action == g15driver.PREVIOUS_SELECTION: + self.get_screen().resched_cycle() + self._move_up(1) + return True + if binding.action == g15driver.NEXT_PAGE: + self.get_screen().resched_cycle() + self._move_down(10) + return True + elif binding.action == g15driver.PREVIOUS_PAGE: + self.get_screen().resched_cycle() + self._move_up(10) + return True + elif binding.action == g15driver.SELECT: + self.get_screen().resched_cycle() + if self.selected: + self.selected.activate() + return True + + def handle_key(self, keys, state, post): + self.select_first() + if not post and state == g15driver.KEY_STATE_DOWN: + if g15driver.G_KEY_UP in keys: + self._move_up(1) + return True + elif g15driver.G_KEY_DOWN in keys or g15driver.G_KEY_L4 in keys: + self._move_down(1) + return True + elif g15driver.G_KEY_RIGHT in keys: + self._move_down(self.get_items_per_page()) + return True + elif g15driver.G_KEY_LEFT in keys: + self._move_up(self.get_items_per_page()) + return True + elif g15driver.G_KEY_OK in keys or g15driver.G_KEY_L5 in keys: + if self.selected and self.selected.activate(): + return True + + + return False + + def select_first(self): + self.get_tree_lock().acquire() + try: + if not self.selected == None and not self.contains_child(self.selected): + self.selected = None + if self.selected == None: + cc = self.get_child_count() + if cc > 0: + for i in range(0, cc): + s = self.get_child(i) + if s.is_enabled() and not isinstance(s, MenuSeparator) : + self.selected = s + break + else: + self.selected = None + finally: + self.get_tree_lock().release() + + ''' + Private + ''' + + def _recalc_scroll_values(self): + max_val = 0 + for item in self.get_children(): + if item.is_showing(): + max_val += self.get_item_height(item, True) + + self.scroll_values = max(max_val, self.view_bounds[3]), self.view_bounds[3], self.base + + def _check_selected(self): + if not self.selected in self.get_children(): + if self.i >= self.get_child_count(): + return + self.selected = self.get_child(self.i) + + def _do_selected(self): + self.selected = self.get_child(self.i) + if self.on_selected: + self.on_selected() + self._recalc_scroll_values() + self.clear_scroll() + self.mark_dirty() + self.get_root().redraw() + + def _get_selected_index(self): + c = self.get_children() + if not self.selected in c: + return 0 if len(c) > 0 else -1 + else: + return self.index_of_child(self.selected) + + def _move_up(self, amount = 1): + self.get_tree_lock().acquire() + try: + if self.get_child_count() == 0: + return + if self.on_move: + self.on_move() + self._check_selected() + self.i = self._get_selected_index() + items = self.get_child_count() + try: + if self.i == 0: + self.i = items - 1 + return + + first_enabled = self._get_first_enabled() + if first_enabled > -1: + for a in range(0, abs(amount), 1): + while True: + self.i -= 1 + if self.i < first_enabled: + if a == 0: + self.i = self._get_last_enabled() + return + else: + self.i = first_enabled + c = self.get_child(self.i) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + break + finally: + self._do_selected() + finally: + self.get_tree_lock().release() + + def _get_first_enabled(self): + for ci in range(0, self.get_child_count()): + c = self.get_child(ci) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + return ci + return -1 + + def _get_last_enabled(self): + for ci in range(self.get_child_count() - 1, 0, -1): + c = self.get_child(ci) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + return ci + return -1 + + + def _move_down(self, amount = 1): + self.get_tree_lock().acquire() + try: + if self.get_child_count() == 0: + return + if self.on_move: + self.on_move() + self._check_selected() + self.i = self._get_selected_index() + + items = self.get_child_count() + try: + if self.i == items - 1: + self.i = 0 + return + + first_enabled = self._get_first_enabled() + + if first_enabled > -1: + for a in range(0, abs(amount), 1): + while True: + self.i += 1 + if self.i == items: + if a == 0: + self.i = first_enabled + return + else: + self.i = self._get_last_enabled() + c = self.get_child(self.i) + if not isinstance(c, MenuSeparator) and c.is_enabled() and c.is_showing() and c.activatable: + break + finally: + self._do_selected() + finally: + self.get_tree_lock().release() + +class MenuScrollbar(Scrollbar): + def __init__(self, id, menu): + Scrollbar.__init__(self, id) + menu.set_scrollbar(self) + +class MenuItem(Component): + def __init__(self, component_id="menu-entry", group = True, name = None, alt = "", activate = None, icon = None, activatable = True): + Component.__init__(self, component_id) + self.group = group + self.name = name if name is not None else component_id + self.alt = alt + if activate is not None: + self.activate = activate + self.icon = icon + self.activatable = activatable + + def on_configure(self): + self.set_theme(G15Theme(self.parent.get_theme().dir, "menu-entry" if self.group else "menu-child-entry")) + + def get_theme_properties(self): + return { + "item_selected" : self.parent is not None and self == self.parent.selected, + "item_name" : self.name, + "item_alt" : self.alt, + "item_icon": self.icon + } + + def get_allow_scrolling(self): + self.get_tree_lock().acquire() + try: + return self.parent is not None and self == self.parent.selected + finally: + self.get_tree_lock().release() + + def activate(self): + return False + +class MenuSeparator(MenuItem): + def __init__(self, id = "menu-separator"): + MenuItem.__init__(self, id) + + def on_configure(self): + self.set_theme(G15Theme(self.parent.get_theme().dir, "menu-separator")) + +class DBusMenuItem(MenuItem): + def __init__(self, id, dbus_menu_entry): + MenuItem.__init__(self, id) + self.dbus_menu_entry = dbus_menu_entry + + def activate(self): + self.dbus_menu_entry.activate() + + def is_enabled(self): + return self.dbus_menu_entry.enabled + + def get_theme_properties(self): + properties = MenuItem.get_theme_properties(self) + properties["item_name"] = self.dbus_menu_entry.get_label() + properties["item_type"] = self.dbus_menu_entry.type + properties["item_enabled"] = self.dbus_menu_entry.enabled + properties["item_radio"] = self.dbus_menu_entry.toggle_type == dbusmenu.TOGGLE_TYPE_RADIO + properties["item_radio_selected"] = self.dbus_menu_entry.toggle_state == 1 + properties["item_alt"] = self.dbus_menu_entry.get_alt_label() + icon_name = self.dbus_menu_entry.get_icon_name() + if icon_name != None: + properties["item_icon"] = g15cairo.load_surface_from_file(g15icontools.get_icon_path(icon_name), self.theme.bounds[3]) + else: + properties["item_icon"] = self.dbus_menu_entry.get_icon() + return properties + +class DBusMenu(Menu): + def __init__(self, dbus_menu): + Menu.__init__(self, "menu") + self.dbus_menu = dbus_menu + + def on_configure(self): + Menu.on_configure(self) + self.populate() + + def menu_changed(self, menu = None, property = None, value = None): + current_ids = [] + for item in self.get_children(): + current_ids.append(item.id) + + self.populate() + + # Scroll to item if it is newly visible + if menu != None: + if property != None and property == dbusmenu.VISIBLE and value and menu.type != "separator": + self.selected = menu + else: + # Layout change + + # See if the selected item is still there + if self.selected != None: + sel = self.selected + self.selected = None + for i in self.get_children(): + if i.id == sel.id: + self.selected = i + + # See if there are new items, make them selected + for item in self.get_children(): + if not item.id in current_ids: + self.selected = item + break + + self.select_first() + + def populate(self): + self.get_tree_lock().acquire() + try: + self.remove_all_children() + i = 0 + for item in self.dbus_menu.root_item.children: + if item.is_visible(): + if item.type == dbusmenu.TYPE_SEPARATOR: + self.add_child(MenuSeparator("dbus-menu-separator-%d" % i)) + else: + self.add_child(DBusMenuItem("dbus-menu-item-%d" % i, item)) + i += 1 + finally: + self.get_tree_lock().release() + +class ErrorScreen(G15Page): + + def __init__(self, screen, title, text, icon = "dialog-error"): + self.page = G15Page.__init__(self, title, screen, priority = g15screen.PRI_HIGH, \ + theme = G15Theme(os.path.join(g15globals.themes_dir, "default"), "error-screen")) + self.theme_properties = { + "title": title, + "text": text, + "icon": g15icontools.get_icon_path(icon) + } + self.get_screen().add_page(self) + self.redraw() + self.get_screen().key_handler.action_listeners.append(self) + + def action_performed(self, binding): + if binding.action == g15driver.SELECT: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + +class ConfirmationScreen(G15Page): + + def __init__(self, screen, title, text, icon, callback, arg, cancel_callback = None): + G15Page.__init__(self, title, screen, priority = g15screen.PRI_HIGH, \ + theme = G15Theme(os.path.join(g15globals.themes_dir, "default"), "confirmation-screen")) + self.theme_properties = { + "title": title, + "text": text, + "icon": icon + } + self.arg = arg + self.callback = callback + self.cancel_callback = cancel_callback + self.get_screen().add_page(self) + self.redraw() + self.get_screen().key_handler.action_listeners.append(self) + + def action_performed(self, binding): + if binding.action == g15driver.PREVIOUS_SELECTION: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + if self.cancel_callback is not None: + self.cancel_callback(self.arg) + elif binding.action == g15driver.NEXT_SELECTION: + self.get_screen().del_page(self) + self.get_screen().key_handler.action_listeners.remove(self) + self.callback(self.arg) + +class G15Theme(object): + def __init__(self, dir_path, variant = None, svg_text = None, prefix = None, auto_dirty = True, translation = None): + self.translation = translation + self.plugin = None + if isinstance(dir_path, ThemeDefinition): + self.dir = dir_path.directory + self.translation = dir_path.translation + self.plugin_module = dir_path.plugin_module + elif isinstance(dir_path, str): + self.dir = dir_path + elif dir is not None: + self.plugin = dir_path + self.dir = os.path.join(os.path.dirname(sys.modules[dir_path.__module__].__file__), "default") + else: + self.dir = None + self.document = None + self.variant = variant + self.page = None + self.instance = None + self.svg_processor = None + self.svg_text = svg_text + self.prefix = prefix + self.render_lock = RLock() + self.scroll_timer = None + self.dirty = True + self.component = None + self.auto_dirty = auto_dirty + self.render = None + self.scroll_state = {} + self.nsmap = { + 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'cc': 'http://web.resource.org/cc/', + 'svg': 'http://www.w3.org/2000/svg', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'xlink': 'http://www.w3.org/1999/xlink', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', + } + + def set_variant(self, variant): + self.variant = variant + self._set_component(self.component) + self.mark_dirty() + + def clear_scroll(self): + for s in self.scroll_state: + self.scroll_state[s].reset() + + def _set_component(self, component): + self.render_lock.acquire() + try: + if self.component is not None and component is None: + # Give the python portion of the theme chance to de-initialize + if self.instance is not None and hasattr(self.instance, 'destroy'): + try: + self.instance.destroy(self) + except Exception as e: + logger.debug("Error destroying instance", exc_info = e) + + + self.component = component + page = component.get_root() if component is not None else None + + if self.page is not None: + self.page.on_shown_listeners.remove(self._page_visibility_changed) + self.page.on_hidden_listeners.remove(self._page_visibility_changed) + self.page = page if page is not None and isinstance(page, G15Page) else None + if self.page is not None: + self.page.on_shown_listeners.append(self._page_visibility_changed) + self.page.on_hidden_listeners.append(self._page_visibility_changed) + + if self.page is None: + self.document = None + self.screen = None + self.text = None + self.driver = None + self.bounds = None + else: + self.screen = self.page.get_screen() + self.text = g15text.new_text(self.screen) + self.driver = self.screen.driver + if self.dir != None: + self.theme_name = os.path.basename(self.dir) + prefix_path = self.prefix if self.prefix != None else os.path.basename(os.path.dirname(self.dir)).replace("-", "_")+ "_" + self.theme_name + "_" + + # The theme may have a python portion + module_name = self.get_path_for_variant(self.dir, self.variant, "py", fatal = False, prefix = prefix_path) + module = None + if module_name != None: + if not dir in sys.path: + sys.path.insert(0, self.dir) + module = __import__(os.path.basename(module_name)[:-3]) + self.instance = module + + path = self.get_path_for_variant(self.dir, self.variant, "svg") + + # Load translation for this variant + actual_variant = os.path.splitext(os.path.basename(path))[0] + self.translation = g15locale.get_translation(actual_variant, self.dir) + + self.document = etree.parse(path) + + + # Give the python portion of the theme chance to initialize + if self.instance is not None and hasattr(self.instance, 'create'): + try: + self.instance.create(self) + except Exception as e: + logger.debug("Error creating instance", exc_info = e) + + elif self.svg_text != None: + self.document = etree.ElementTree(etree.fromstring(self.svg_text)) + else: + raise Exception("Must either supply theme directory or SVG text") + + self.process_svg() + self.bounds = g15svg.get_bounds(self.document.getroot()) + finally: + self.render_lock.release() + + def process_svg(self): + self.driver.process_svg(self.document) + root = self.document.getroot() + + # Remove glow effects + if self.screen.service.disable_svg_glow: + for element in root.xpath('//svg:filter[@inkscape:label="Glow"]',namespaces=self.nsmap): + element.getparent().remove(element) + + # Remove sodipodi attributes + self.del_namespace("sodipodi", "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd") + self.del_namespace("inkscape", "http://www.inkscape.org/namespaces/inkscape") + + # Translate text + if self.translation is not None: + for textel in root.xpath('//text()',namespaces=self.nsmap): + tpar = textel.getparent() + text = tpar.text + if text is not None and len(text) > 0 and text.startswith("_("): + tpar.text = self.translation.ugettext(text[2:-1].strip()) + + + def del_namespace(self, prefix, uri): + for e in self.document.getroot().xpath("//*[namespace-uri()='%s' or @*[namespace-uri()='%s']]" % ( uri, uri ) ,namespaces=self.nsmap): + attr = e.attrib + for k in list(attr.keys()): + if k.startswith("{%s}" % uri): + del attr[k] + if e.getparent() is not None and e.prefix == prefix: + e.getparent().remove(e) + + + def get_path_for_variant(self, dir, variant, extension, fatal = True, prefix = ""): + if variant == None: + variant = "" + elif variant != "": + variant = "-" + variant + + # First try the provided path (i.e the plugin directory) + path = os.path.join(dir, prefix + self.driver.get_model_name() + variant + "." + extension ) + if not os.path.exists(path): + # Next try the theme directory + path = os.path.join(dir, g15globals.themes_dir, "default", self.driver.get_model_name() + variant + "." + extension) + if not os.path.exists(path): + # Now look for a default theme file in the provided path (i.e the plugin directory) + path = os.path.join(dir, prefix + "default" + variant + "." + extension) + if not os.path.exists(path): + # Finally look for a default theme file in the theme directory + path = os.path.join(dir, g15globals.themes_dir, "default", "default" + variant + "." + extension) + if not os.path.exists(path): + if fatal: + raise Exception("Missing %s. No .%s file for model %s in %s for variant %s" % ( path, extension, self.driver.get_model_name(), dir, variant )) + else: + return None + return path + + def convert_css_size(self, css_size): + em = 1.0 + if css_size.endswith("px"): + # Get EM based on size of 10px (the default cairo context is 10 so this should be right?) + px = float(css_size[:len(css_size) - 2]) + em = px / BASE_PX + elif css_size.endswith("pt"): + # Convert to px first, then use same algorithm + pt = float(css_size[:len(css_size) - 2]) + px = ( pt * 96.0 ) / 72.0 + em = px / BASE_PX + elif css_size.endswith("%"): + em = float(css_size[:len(css_size) - 1]) / 100.0 + elif css_size.endswith("em"): + em = float(css_size) + else: + raise Exception("Unknown font size") + return em + + def get_string_width(self, text, canvas, css): + # Font family + font_family = css.get("font-family") + + # Font size (translate to 'em') + font_size_text = css.get("font-size") + em = self.convert_css_size(font_size_text) + + # Font weight + font_weight = cairo.FONT_WEIGHT_NORMAL + if css.get("font-weight") == "bold": + font_weight = cairo.FONT_WEIGHT_BOLD + + # Font style + font_slant = cairo.FONT_SLANT_NORMAL + if css.get("font-style") == "italic": + font_slant = cairo.FONT_SLANT_ITALIC + elif css.get("font-style") == "oblique": + font_slant = cairo.FONT_SLANT_OBLIQUE + + try : + canvas.save() + canvas.select_font_face(font_family, font_slant, font_weight) + canvas.set_font_size(em * 10.0 * ( 4 / 3) ) + return canvas.text_extents(text)[:4] + finally: + canvas.restore() + + def parse_css(self, styles_text): + # Parse CSS styles + styles = { } + for style in styles_text.split(";") : + style_args = style.lstrip().rstrip().split(":") + if len(style_args) > 1: + styles[style_args[0].rstrip()] = style_args[1].lstrip().rstrip() + if not "text-align" in styles: + styles["text-align"] = "start" + return styles + + def format_styles(self, styles): + buf = "" + for style in styles: + buf += style + ":" + styles[style] + ";" + return buf.rstrip(';') + + def get_element(self, element_id = None, root = None): + if root == None: + root = self.document.getroot() + if element_id is None: + return root + els = root.xpath('//svg:*[@id=\'%s\']' % str(element_id),namespaces=self.nsmap) + return els[0] if len(els) > 0 else None + + def get_element_by_tag(self, tag, root = None): + if root == None: + root = self.document.getroot() + els = root.xpath('svg:%s' % str(tag),namespaces=self.nsmap) + return els[0] if len(els) > 0 else None + + def mark_dirty(self): + self.dirty = True + + def draw(self, canvas, properties = {}, attributes = {}): + if self.render != None and self.auto_dirty: + if self.render.properties != properties or self.render.attributes != attributes or \ + self.render.properties.values() != properties.values() or self.render.attributes.values() != attributes.values(): + self.dirty = True + + if self.render == None or self.dirty: + self.render_lock.acquire() + + if self.document is None: + raise Exception("No document available! Paint called before component finished initialising") + + self.text.set_canvas(canvas) + + try: + document = deepcopy(self.document) + processing_result = None + + # Give the python portion of the theme chance to draw stuff under the SVG + if self.instance is not None and hasattr(self.instance, 'paint_background'): + try: + self.instance.paint_background(properties, attributes) + except Exception as e: + logger.debug("Error painting background", exc_info = e) + + root = document.getroot() + + # Process the SVG + self._process_deletes(root, properties) + self._process_components(root) + self._set_progress_bars(root, properties) + self._set_relative_image_paths(root) + self._convert_image_urls(root, properties) + self._do_shadow("shadow", self.screen.driver.get_color_as_hexrgb(g15driver.HINT_BACKGROUND, (255, 255,255)), root) + self._do_shadow("reverseshadow", self.screen.driver.get_color_as_hexrgb(g15driver.HINT_FOREGROUND, (0, 0, 0)), root) + self._set_highlight_color(root) + + text_boxes = [] + self._handle_text_boxes(root, text_boxes, properties, canvas) + + # Pass the SVG document to the SVG processor if there is one + if self.svg_processor != None: + self.svg_processor(document, properties, attributes) + + # Pass the SVG document to the theme's python code to manipulate the document if required + if self.instance is not None and hasattr(self.instance, 'process_svg'): + try: + processing_result = self.instance.process_svg(self.driver, + root, + properties, + self.nsmap) + except Exception as e: + logger.debug("Error processing SVG", exc_info = e) + + self._set_default_style(root) + + self.render = Render(document, properties, text_boxes, attributes, processing_result) + self.dirty = False + finally: + self.render_lock.release() + else: + self.text.set_canvas(canvas) + + self._render_document(canvas, self.render) + return self.render.document + + def is_scroll_required(self): + return len(self.scroll_state) > 0 + + def do_scroll(self): + try: + self.render_lock.acquire() + if len(self.scroll_state) > 0: + for key in self.scroll_state: + self.scroll_state[key].next() + return True + finally: + self.render_lock.release() + + """ + Private + """ + + def _process_components(self, root): + """ + Find all elements that are associated with child components in the component this + theme is attached to, and draw them too. + + Keyword arguments: + root -- root of document + """ + if self.component: + for component_id in self.component.child_map.keys(): + component_elements = root.xpath('//svg:*[@id=\'%s\']' % component_id,namespaces=self.nsmap) + if len(component_elements) > 0: + c = component_elements[0] + c_class = c.get("class") + if c_class and "hidden-root" in c_class: + c.getparent().remove(c) + self.component.child_map[component_id].draw(self, c) + else: + logger.warning("Cannot find SVG element for component %s", component_id) + + def _process_deletes(self, root, properties): + """ + Remove all elements that are dependent on properties having non blank values + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:*[@title]',namespaces=self.nsmap): + title = element.get("title") + if title != None: + args = title.split(" ") + if args[0] == "del": + var = args[1] + condition = True + if var.startswith("!"): + var = var[1:] + condition = False + if ( condition and var in properties and properties[var] != "" and properties[var] != False ) or \ + ( not condition and ( not var in properties or properties[var] == "" or properties[var] == False ) ): + element.getparent().remove(element) + + def _set_progress_bars(self, root, properties): + """ + Sets the width attribute for any elements that have a style of "progress" based on + the value in the theme properties (with a key that is equal to the ID of the + element, less the _progress suffix). + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:rect[@class=\'progress\']',namespaces=self.nsmap): + bounds = g15svg.get_bounds(element) + id = element.get("id") + if id.endswith("_progress"): + property_key = id[:-9] + if property_key in properties: + value = float(properties[property_key]) + if value == 0: + value = 0.1 + element.set("width", str(int((bounds[2] / 100.0) * value))) + else: + logger.warning("Found progress element with an ID that doesn't exist in " + \ + "theme properties. Theme directory is %s, variant is %s." % (self.dir, self.variant )) + else: + logger.warning("Found progress element with an ID that doesn't end in _progress") + + def _set_highlight_color(self, root): + """ + Replaces any elements that have a color equal to the "highlight" colour + default with the configured highlight color + + Keyword arguments: + root -- root of document + """ + if self.screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + for element in root.xpath('//svg:*[@style]',namespaces=self.nsmap): + element.set("style", element.get("style").replace(DEFAULT_HIGHLIGHT_COLOR, self.screen.driver.get_color_as_hexrgb(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )))) + + def _set_relative_image_paths(self, root): + for element in root.xpath('//svg:image[@xlink:href]',namespaces=self.nsmap): + href = element.get("{http://www.w3.org/1999/xlink}href") + is_data = href and href.startswith("data:") + is_abs = href and ( href.startswith("http:") or href.startswith("https:") or href.startswith("file:") or href.startswith("/")) + is_var = href and "${" in href + if not is_data and not is_abs and not is_var: + href = os.path.join(self.dir, href) + element.set("{http://www.w3.org/1999/xlink}href", href) + + def _convert_image_urls(self, root, properties): + """ + Inserts either a local file URL or an embedded image URL into all + elements that have 'title' attribute whose value exists as a property + in the theme properties. + + Keyword arguments: + root -- root of document + properties -- theme properties + """ + for element in root.xpath('//svg:image',namespaces=self.nsmap): + id = element.get("title") + if id != None and id in properties and properties[id] != None: + file_str = StringIO() + val = properties[id] + if isinstance(val, str) and str(val).startswith("file:"): + file_str.write(val[5:]) + elif isinstance(val, str) and str(val).startswith("/"): + file_str.write(val) + else: + file_str.write("data:image/png;base64,") + img_data = StringIO() + if isinstance(val, cairo.Surface): + val.write_to_png(img_data) + file_str.write(base64.b64encode(img_data.getvalue())) + else: + file_str.write(val) + element.set("{http://www.w3.org/1999/xlink}href", file_str.getvalue()) + + def _set_default_style(self, root): + """ + Set the default fill color to be the default foreground. If elements don't specify their + own colour, they will inherit this + + Keyword arguments: + root -- root document element + """ + root_style = root.get("style") + fg_c = self.screen.driver.get_control_for_hint(g15driver.HINT_FOREGROUND) + fg_h = None + if fg_c != None: + val = fg_c.value + fg_h = "#%02x%02x%02x" % ( val[0],val[1],val[2] ) + if root_style != None: + root_styles = self.parse_css(root_style) + else: + root_styles = { } + root_styles["fill"] = fg_h + root.set("style", self.format_styles(root_styles)) + + def _handle_text_boxes(self, root, text_boxes, properties, canvas): + + # Look for text elements that have a clip path. If the rendered text is wider than + # the clip path, then this element may be scrolled. This clipped text can also + # be used to wrap and scroll vertical text, replacing the old 'text box' mechanism + + for element in root.xpath('//svg:text[@clip-path]',namespaces=self.nsmap): + id = element.get("id") + clip_path_node = self._get_clip_path_element(element) + vertical_wrap = "vertical-wrap" == element.get("title") + if clip_path_node is not None: + + t_span_node = self.get_element_by_tag("tspan", root = element) + if t_span_node is None: + # Doesn't have t_span + t_span_node = element + + t_span_text = t_span_node.text + if not t_span_text: + raise Exception("Text node had clip path, but no text/tspan->text could be found") + + clip_path_rect_node = self.get_element_by_tag("rect", clip_path_node) + if clip_path_rect_node is None: + raise Exception("No svg:rect for clip %s" % str(clip_path_node)) + clip_path_bounds = g15svg.get_actual_bounds(clip_path_rect_node, element) + text_bounds = g15svg.get_actual_bounds(element) + + text_box = TextBox() + text_box.text = Template(t_span_text).safe_substitute(properties) + text_box.css = self.parse_css(element.get("style")) + text_class = element.get("class") + if text_class: + if "reverseshadow" in text_class: + text_box.reverse_shadow = True + elif "shadow" in text_class: + text_box.normal_shadow = True + text_box.clip = clip_path_bounds + + self._update_text(text_box, vertical_wrap) + tx, ty, text_width, text_height = self.text.measure() +# text_width, text_height = self._get_actual_size(element, text_width, text_height) + text_box.bounds = ( text_bounds[0], text_bounds[1], text_width, text_height ) + + self._scroll_text_boxes(vertical_wrap, text_box, text_boxes, t_span_node, element) + + # Find all of the text boxes. This is a hack to get around rsvg not supporting + # flowText completely. The SVG must contain two elements. The first must have + # a class attribute of 'textbox' and the ID must be the property key that it + # will contain. The next should be the text element (which defines style etc) + # and must have an id attribute of _text. The text layer is + # then rendered after the SVG using Pango. + for element in root.xpath('//svg:rect[@class=\'textbox\']',namespaces=self.nsmap): + id = element.get("id") + logger.warning("DEPRECATED Text box with ID %s in %s", id, self.dir) + text_node = root.xpath('//*[@id=\'' + id + '_text\']',namespaces=self.nsmap)[0] + if text_node != None: + styles = self.parse_css(text_node.get("style")) + + # Store the text box + text_box = TextBox() + text_box.text = properties[id] + text_box.css = styles + text_box.wrap = True + text_boxes.append(text_box) + text_box.bounds = g15svg.get_actual_bounds(element) + text_box.clip = text_box.bounds + + # Remove the textnod SVG element + text_node.getparent().remove(text_node) + element.getparent().remove(element) + + def _scroll_text_boxes(self, vertical_wrap, text_box, text_boxes, t_span_node, element): + id = element.get("id") + text_height = text_box.bounds[3] + text_width = text_box.bounds[2] + clip_path_bounds = text_box.clip + + if vertical_wrap: + text_box.wrap = True + text_boxes.append(text_box) + if self.screen.service.scroll_amount > 0 and text_height > clip_path_bounds[3]: + if id in self.scroll_state: + scroll_item = self.scroll_state[id] + scroll_item.text_box = text_box + text_box.base = scroll_item.val + else: + scroll_item = VerticalWrapScrollState(text_box) + scroll_item.vertical = True + self.scroll_state[id] = scroll_item + diff = text_height - clip_path_bounds[3] + scroll_item.range = ( 0, diff) + scroll_item.step = self.screen.service.scroll_amount + scroll_item.transform_elements() + elif id in self.scroll_state: + del self.scroll_state[id] + + element.getparent().remove(element) + else: +# text_boxes.append(text_box) + + # Enable or disable scrolling + if self.screen.service.scroll_amount > 0 and text_width > clip_path_bounds[2]: + if id in self.scroll_state: + scroll_item = self.scroll_state[id] + scroll_item.element = element + else: + scroll_item = HorizontalScrollState(element) + + self.scroll_state[id] = scroll_item + diff = text_width - clip_path_bounds[2] + + #+ ( clip_path_bounds[0] - text_box.bounds[0] ) + if diff < 0: + raise Exception("Negative diff!?") + scroll_item.alignment = text_box.css["text-align"] + scroll_item.original = float(element.get("x")) + if scroll_item.alignment == "center": + scroll_item.range = ( -(diff / 2), (diff / 2)) + elif scroll_item.alignment == "start": + scroll_item.range = ( -diff, 0) + elif scroll_item.alignment == "end": + scroll_item.range = ( 0, diff) + + scroll_item.reset() + + scroll_item.step = self.screen.service.scroll_amount + scroll_item.other_elements = [t_span_node] + scroll_item.transform_elements() + elif id in self.scroll_state: + del self.scroll_state[id] +# element.getparent().remove(element) + + def _get_clip_path_element(self, element): + clip_val = element.get("clip-path") + if clip_val and len(clip_val) > 0 and clip_val != "none": + id = clip_val[5:-1] + el = self.get_element(id, element.getroottree().getroot()) + if el is None: + raise Exception("Text node had clip path (%s), but no clip path element with matching ID of %s could be found" % ( id, element.get("clip-path") ) ) + return el + + def _component_removed(self): + self.scroll_state = {} + self._set_component(None) + + def _page_visibility_changed(self): + pass + + def _render_document(self, canvas, render): + + encoded_properties = {} + # Encode entities in all the property values + for key in render.properties.keys(): + encoded_properties[key] = saxutils.escape(str(render.properties[key])) + + xml = etree.tostring(render.document) + t = Template(xml) + xml = t.safe_substitute(encoded_properties) + svg = rsvg.Handle() + try : + svg.write(xml) + if DEBUG_SVG: + print "------------------------------------------------------" + print xml + print "------------------------------------------------------" + except Exception as e: + logger.debug("Could not write SVG", exc_info = e) + try : + svg.close() + except Exception as e: + logger.debug("Could not close SVG", exc_info = e) + + svg.render_cairo(canvas) + + if len(render.text_boxes) > 0: + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + bg_rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_BACKGROUND, ( 255, 255, 255 )) + for text_box in render.text_boxes: + self._render_text_box(canvas, text_box, rgb, bg_rgb) + + # Give the python portion of the theme chance to draw stuff over the SVG + if self.instance is not None and hasattr(self.instance, 'paint_foreground'): + try: + self.instance.paint_foreground(canvas, + render.properties, + render.attributes, + render.processing_result) + except Exception as e: + logger.debug("Error painting foreground", exc_info = e) + + def _render_text_box(self, canvas, text_box, rgb, bg_rgb): + self._update_text(text_box, text_box.wrap) + +# if "fill" in text_css: +# rgb = g15convert. css["fill"] +# else: +# foreground = None + + if text_box.normal_shadow or text_box.reverse_shadow: + if text_box.normal_shadow: + canvas.set_source_rgb(bg_rgb[0], bg_rgb[1], bg_rgb[2]) + else: + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + for x in range(-1, 2): + for y in range(-1, 2): + if x != 0 or y != 0: + self.text.draw(text_box.bounds[0] + x, text_box.bounds[1] + y - text_box.base) + + # Draw primary text to canvas + canvas.set_source_rgb(rgb[0], rgb[1], rgb[2]) + self.text.draw(text_box.bounds[0], text_box.bounds[1] - text_box.base) + + def _get_actual_size(self, element, width, height): + list_transforms = [ cairo.Matrix(width, 0.0, 0.0, height, float(element.get("x")), float(element.get("y"))) ] + el = element + while el != None: + list_transforms += g15svg.get_transforms(el) + el = el.getparent() + list_transforms.reverse() + t = list_transforms[0] + for i in range(1, len(list_transforms)): + t = t.multiply(list_transforms[i]) + xx, yx, xy, yy, x0, y0 = t + return ( xx, yy ) + + def _update_text(self, text_box, wrap = False): + + css = text_box.css + + font_size_css = css["font-size"] if "font-size" in css else None + font_pt_size = None + if font_size_css: + nw = "".join(font_size_css.split()).lower() + if nw.endswith("px"): + fs = float(font_size_css[:-2]) + font_pt_size = int(g15cairo.approx_px_to_pt(fs)) + elif nw.endswith("pt"): + font_pt_size = int(font_size_css[:-2]) + + + font_family = css["font-family"] if "font-family" in css else None + font_weight = css["font-weight"] if "font-weight" in css else None + font_style = css["font-style"] if "font-style" in css else None + if "text-align" in css: + text_align = css["text-align"] + else: + text_align = "start" + alignment = pango.ALIGN_LEFT + if text_align == "end": + alignment =pango.ALIGN_RIGHT + elif text_align == "center": + alignment = pango.ALIGN_CENTER + + # Determine wrap and width to use + if wrap: + width = int(pango.SCALE * text_box.clip[2]) + wrap = pango.WRAP_WORD_CHAR + else: + wrap = 0 + width = -1 + + # Update the text handler + self.text.set_attributes(text_box.text, bounds = text_box.clip, wrap = wrap, align = alignment, \ + width = width, spacing = 0, \ + style = font_style, weight = font_weight, \ + font_pt_size = font_pt_size, \ + font_desc = font_family) + + def _do_shadow(self, id, color, root): + """ + Shadow is a special text effect useful on the G15. It will take 8 copies of a text element, make + them the same color as the background, and render them under the original text element at x-1/y-1, + xy-1,x+1/y,x-1/y etc. This makes the text legible if it overlaps other text or an image ( + at the expense of losing some detail of whatever is underneath) + + Keyword arguments: + id -- id of element to shadow + color -- 3 element tuple for RGB values of colour to use for shadow + root -- SVG document root + """ + + + for element in root.xpath('//svg:*[@class=\'%s\']' % id,namespaces=self.nsmap): + clip_path_element = self._get_clip_path_element(element) + bounds = g15svg.get_bounds(element) + idx = 1 + for x in range(-1, 2): + for y in range(-1, 2): + if x != 0 or y != 0: + element_id = element.get("id") + shadowed_id = element_id + "_" + str(idx) if element_id else None + + # Copy the element itself + shadowed = deepcopy(element) + if shadowed_id: + shadowed.set("id", shadowed_id) + for bound_element in shadowed.iter(): + bound_element.set("x", str(bounds[0] + x)) + bound_element.set("y", str(bounds[1] + y)) + styles = self.parse_css(shadowed.get("style")) + if styles == None: + styles = {} + styles["fill"] = color + shadowed.set("style", self.format_styles(styles)) + element.addprevious(shadowed) + + # Copy the clip path + if clip_path_element is not None: + clip_copy = deepcopy(clip_path_element) + clip_id = clip_path_element.get("id") + new_clip_id = "%s_%d" % ( clip_id, idx ) + clip_copy.set("id", new_clip_id ) + shadowed.set("clip-path", "url(#%s)" % new_clip_id) + clip_path_element.addprevious(clip_copy) + + idx += 1 diff --git a/src/gnome15/g15top.py b/src/gnome15/g15top.py new file mode 100644 index 0000000..6b47305 --- /dev/null +++ b/src/gnome15/g15top.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module has been written to be API compatible with the python-gtop bindings, +which are no longer available as from Ubuntu 12.10. The suggested replacement +is the GObject based bindings, which would be great except it means converting +ALL of Gnome15 to use such bindings. + +This class is stop gap until a better solution can be found +""" + +import os +import time + +class CPU(): + def __init__(self, name): + self.name = name + self.user = 0 + self.nice = 0 + self.sys = 0 + self.idle = 0 + +class CPUS(CPU): + def __init__(self): + CPU.__init__(self, "CPUS") + cpudata = open('/proc/stat') + self.cpus = [] + try: + for line in cpudata: + if line.startswith("cpu"): + (name, cuse, cn, csys, idle, tail) = line.split(None, 5) + cpu = self if name == "cpu" else CPU(name) + self.user = int(cuse) + self.nice = int(cn) + self.sys = int(csys) + self.idle = int(idle) + self.cpus.append(cpu) + finally: + cpudata.close() + +class ProcState(): + + def __init__(self, pid): + self.uid = 0 + self.cmd = "" + memdata = open('/proc/%d/status' % pid) + try: + for line in memdata: + if line.startswith("Uid:"): + self.uid = int(self._get_value(line)[0]) + elif line.startswith("Name:"): + self.cmd = self._get_value(line)[0] + finally: + memdata.close() + + def _get_value(self, line): + return line[line.index(':') + 1:].strip().split() + +class NetworkLoad(): + + def __init__(self, net, bytes_in, bytes_out): + self.net = net + self.bytes_in = bytes_in + self.bytes_out = bytes_out + +class Mem(): + + def __init__(self): + self.total = 0 + self.free = 0 + self.cached = 0 + memdata = open('/proc/meminfo') + try: + for line in memdata: + if line.startswith("MemTotal"): + self.total = self._get_value(line) + elif line.startswith("MemFree"): + self.free = self._get_value(line) + elif line.startswith("Cached"): + self.cached = self._get_value(line) + finally: + memdata.close() + + def _get_value(self, line): + return int(line[line.index(':') + 1:line.index('kB')]) * 1024 + +def netload(net): + """ + Get the network load details for the network interface described by the + provided network interface name + + Keyword arguments: + net -- network interface name + """ + prefix = '%6s:' % net + netdata = open('/proc/net/dev') + try: + for line in netdata: + if line.startswith(prefix): + data = line[line.index(':') + 1:].split() + return NetworkLoad(net, int(data[0]), int(data[8])) + finally: + netdata.close() + + +def netlist(): + """ + Returns a list of Net objects, one for each available network interface + """ + nets = [] + f = open("/proc/net/dev", "r") + tmp = f.readlines(2000) + f.close() + for line in tmp: + line = line.strip() + line = line.split(' ') + if len(line) > 0 and line[0].endswith(":"): + nets.append(line[0][:-1]) + + return nets + +def cpu(): + """ + Return an object containing data about all available CPUS + """ + return CPUS() + +def mem(): + """ + Return an object containing data about all available CPUS + """ + return Mem() + +def proclist(): + """ + Get a list of all process IDs + """ + n = [] + for d in os.listdir("/proc"): + if os.path.isdir("/proc/%s" % d): + try: + n.append(int(d)) + except ValueError: + pass + return n + +def proc_state(pid): + """ + Get an object describing the state of the given process + + Keyword arguments: + pid -- process ID + """ + return ProcState(pid) + +def proc_args(pid): + """ + Get the arguments used to launch a process + + Keyword arguments: + pid -- process ID + """ + cmddata = open('/proc/%d/cmdline' % pid) + try: + for line in cmddata: + return line.split("\0") + finally: + cmddata.close() + +class Uptime: + def __init__(self, uptime, idletime): + self.uptime = uptime + self.idletime = idletime + self.boot_time = time.time() - self.uptime + +def uptime(): + """ + Get the uptime of the computer + """ + cmddata = open('/proc/uptime') + try: + for line in cmddata: + vals = line.strip('\n').split(' ') + finally: + cmddata.close() + + return Uptime(float(vals[0]), float(vals[1])) + +if __name__ == "__main__": + for d in proclist(): + ps = proc_state(d) + print d,ps.cmd,ps.uid,proc_args(d) \ No newline at end of file diff --git a/src/gnome15/g15uinput.py b/src/gnome15/g15uinput.py new file mode 100644 index 0000000..1e1de45 --- /dev/null +++ b/src/gnome15/g15uinput.py @@ -0,0 +1,403 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Manages the use of uinput to inject input events (key presses, mouse movement, +joystick events) into the kernel. +""" + +import logging +import uinput +import util.g15os as g15os +import os +import subprocess +from uinput.ev import * +from threading import RLock +from gnome15 import g15globals +logger = logging.getLogger(__name__) + +MOUSE = "mouse" +JOYSTICK = "joystick" +DIGITAL_JOYSTICK = "digital-joystick" +KEYBOARD = "keyboard" +DEVICE_TYPES = [ MOUSE, KEYBOARD, JOYSTICK, DIGITAL_JOYSTICK ] + +""" +Joystick calibration values +""" +JOYSTICK_MIN = -127 +JOYSTICK_MAX = 127 +JOYSTICK_CENTER = 0 + +""" +Value sent by the hardware when the joystick is at the center +""" +DEVICE_JOYSTICK_CENTER=128 + +#capabilities = uinput.capabilities.CAPABILITIES +capabilities = uinput.ev.__dict__ +registered_parameters = { MOUSE: {}, + JOYSTICK: { + uinput.ABS_X: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + uinput.ABS_Y: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + }, + DIGITAL_JOYSTICK: { + uinput.ABS_X: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + uinput.ABS_Y: (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0), + }, + KEYBOARD: {} } +uinput_devices = {} +locks = {} +for t in DEVICE_TYPES: + locks[t] = RLock() + +""" +These are the very unofficial vendor / produce codes used for the virtual +devices +""" +GNOME15_USB_VENDOR_ID = 0xdd55 +GNOME15_MOUSE_PRODUCT_ID = 0x0001 +GNOME15_JOYSTICK_PRODUCT_ID = 0x0002 +GNOME15_KEYBOARD_PRODUCT_ID = 0x0003 +GNOME15_DIGITAL_JOYSTICK_PRODUCT_ID = 0x0004 + +""" +python-uinput currently doesn't expose these constants +""" + +EV_KEY = 0x01 +EV_REL = 0x02 +EV_ABS = 0x03 + +""" +special virtual keys that are actually joystick movement. + +These 'virtual' uinput codes are created so that the user can assign macros to the left, right, up +and down directions of the joystick. +By default uinput only has two codes (ABS_X and ABS_Y) that specify the axis, the direction being +determinated by the value passed to uinput.emit. +""" +JS = 0x9999 +JS_LEFT = 0x9701 +JS_RIGHT = 0x9702 +JS_DOWN = 0x9703 +JS_UP = 0x9704 +JS_MOVEMENT = { + "X_LEFT" : (JS, JS_LEFT), + "X_RIGHT" : (JS, JS_RIGHT), + "Y_UP" : (JS, JS_UP), + "Y_DOWN" : (JS, JS_DOWN), +} +for k in JS_MOVEMENT: + capabilities[k] = JS_MOVEMENT[k] + +""" +Load the X Keysym to UInput map +""" +__keysym_map = {} +__keysym_map_path = "%s/keysym-to-uinput" % g15globals.ukeys_dir +if os.path.exists(__keysym_map_path): + f = open(__keysym_map_path, "r") + b = [] + for line in f.readlines(): + line = line.strip() + if not line == "" and not line.startswith("#"): + arr = line.split("=") + if len(arr) > 1: + __keysym_map[arr[0].lower()] = arr[1] +else: + logger.warning("Could not find keysym to uinput map %s", __keysym_map_path) + +def get_keysym_to_uinput_mapping(keysym): + """ + Get the mapping for the provided keysym. This is case insensitive. + + Keyword arguments: + keysym -- X keysym + """ + if keysym.lower() in __keysym_map: + return __keysym_map[keysym.lower()] + logger.warning("Failed to translate X keysym %s to UInput code. " \ + "You can add a mapping by editing %s. " \ + "Please also report this on the Gnome15 project forums.", + keysym, + __keysym_map_path) + +def are_calibration_tools_available(): + """ + Test for the existence of calibration tools 'jstest-gtk' and 'jscal'. + """ + return os.system("which jstest-gtk >/dev/null 2>&1") == 0 and os.system("which jscal >/dev/null 2>&1") == 0 + +def open_devices(): + """ + Initialize, opening all devices + """ + __check_devices() + +def close_devices(): + """ + Clean up, closing all the devices + """ + for device_type in DEVICE_TYPES: + if device_type in uinput_devices: + logger.debug("Closing UINPUT device %s", device_type) + del uinput_devices[device_type] + +def calibrate(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + load_calibration(device_type) + os.system("jstest-gtk '%s'" % (device_file)) + save_calibration(device_type) + +def save_calibration(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + proc = subprocess.Popen(["jscal", "-q", device_file ], stdout=subprocess.PIPE) + out = proc.communicate()[0] + js_config_file = _get_js_config_file(device_type) + f = open(js_config_file, "w") + try : + f.write(out) + finally : + f.close() + +def load_calibration(device_type): + """ + Run external joystick calibration utility + + Keyword arguments: + device_type -- device type + """ + if are_calibration_tools_available(): + if not device_type in [ JOYSTICK, DIGITAL_JOYSTICK ]: + raise Exception("Cannot calibrate this device type (%s)" % device_type) + device_file = get_device(device_type) + if device_file: + js_config_file = _get_js_config_file(device_type) + if os.path.exists(js_config_file): + f = open(js_config_file, "r") + try : + cal = f.readline().split() + logger.info("Calibrating using '%s'", cal) + proc = subprocess.Popen(cal, stdout=subprocess.PIPE) + logger.info("Calibrated. %s", proc.communicate()[0]) + except Exception as e: + logger.error("Failed to calibrate joystick device.", exc_info = e) + finally : + f.close() + else: + logger.warning("No joystick calibration available.") + +def _get_js_config_file(device_type): + """ + Returns the filename used for saving the joystick calibration file + + If the directory that should own the file doesn't exist, it will be + created. + + Keyword arguments: + device_type -- device_type + """ + g15os.mkdir_p(g15globals.user_config_dir) + return os.path.join(g15globals.user_config_dir, "%s.js" % device_type) + +def get_device(device_type): + """ + Find the actual input device given the virtual device type + + Keyword arguments: + device_type -- device type + """ + vi_path = "/sys/devices/virtual/input" + if os.path.exists(vi_path): + for p in os.listdir(vi_path): + dev_dir = "%s/%s" % (vi_path, p) + name_file = "%s/name" % (dev_dir) + if os.path.exists(name_file): + f = open(name_file, "r") + try : + device_name = f.readline().replace("\n", "") + if device_name == "gnome15-%s" % device_type: + dev_files = os.listdir(dev_dir) + for dp in dev_files: + if dp.startswith("js"): + return "/dev/input/%s" % dp + for dp in dev_files: + if dp.startswith("event"): + return "/dev/input/%s" % dp + finally : + f.close() + + +def syn(target): + """ + Emit the syn. + + Keyword arguments: + target -- target device type (MOUSE, KEYBOARD or JOYSTICK). + """ + uinput_devices[target].syn() + +def emit(target, code, value, syn=True): + """ + Emit an input event, optionally emit a SYN as well + + Keyword arguments: + target -- The target device type (MOUSE, KEYBOARD or JOYSTICK) + type code. + code -- uinput code (either single code, where type will be + determined by target or a tuple consisting of event + type and event code) + value -- uinput value + syn -- emit SYN (defaults to True) + """ + if not target in DEVICE_TYPES: + raise Exception("Invalid target. '%s' must be one of %s" % (target, str(DEVICE_TYPES))) + + if not isinstance(code, tuple): + if target == MOUSE and code in [ uinput.REL_X[1], uinput.REL_Y[1] ]: + logger.debug("UINPUT mouse event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + code = ( EV_REL, code ) + elif ( target == JOYSTICK or target == DIGITAL_JOYSTICK ): + """ We translate the 'virtual' uinput codes into real uinput ones """ + if code == JS_LEFT: + value = JOYSTICK_MIN if value > 0 else JOYSTICK_CENTER + code = ABS_X + elif code == JS_RIGHT: + value = JOYSTICK_MAX if value > 0 else JOYSTICK_CENTER + code = ABS_X + elif code == JS_UP: + value = JOYSTICK_MIN if value > 0 else JOYSTICK_CENTER + code = ABS_Y + elif code == JS_DOWN: + value = JOYSTICK_MAX if value > 0 else JOYSTICK_CENTER + code = ABS_Y + else: + """ If we are simulating a bouton press, then the event is of type EV_KEY """ + code = (EV_KEY, code) + logger.debug("UINPUT joystick event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + else: + code = ( EV_KEY, code ) + logger.debug("UINPUT uinput keyboard event at %s, code = %s, val = %d, syn = %s", + target, + code, + value, + str(syn)) + + locks[target].acquire() + try: + uinput_devices[target].emit( code, value, syn) + finally: + locks[target].release() + +def __get_keys(prefix, exclude = None): + l = [] + for k in sorted(capabilities.iterkeys()): + if k.startswith(prefix) and ( exclude == None or not k.startswith(exclude) ): + l.append(capabilities[k]) + return l + +def get_keys(device_type): + if device_type == MOUSE: + return __get_keys("BTN_", "BTN_TOOL_") + elif device_type == JOYSTICK: + return __get_keys("BTN_", "X_", "Y_") + else: + return __get_keys("KEY_") + +def get_buttons(device_type, real_uinput_only = False): + fname = os.path.join(g15globals.ukeys_dir, "%s.keys" % device_type) + f = open(fname, "r") + b = [] + for line in f.readlines(): + line = line.strip() + if not line == "" and not line.startswith("#"): + if line in capabilities: + b.append((line, capabilities[line][1])) + else: + logger.warning("Invalid key name '%s' in %s", line, fname) + return b + +def __check_devices(): + for device_type in DEVICE_TYPES: + if not device_type in uinput_devices: + logger.info("Opening uinput device for %s", device_type) + keys = [] + for b, _ in get_buttons(device_type, True): + if capabilities[b][0] < 0x9999: + keys.append(capabilities[b]) + + if device_type == MOUSE: + virtual_product_id = GNOME15_MOUSE_PRODUCT_ID + keys.append((REL_X[0], REL_X[1], 0, 255, 0, 0)) + keys.append((REL_Y[0], REL_Y[1], 0, 255, 0, 0)) + elif device_type == JOYSTICK: + virtual_product_id = GNOME15_JOYSTICK_PRODUCT_ID + keys.append(ABS_X + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + keys.append(ABS_Y + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + elif device_type == DIGITAL_JOYSTICK: + virtual_product_id = GNOME15_JOYSTICK_PRODUCT_ID + keys.append(ABS_X + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + keys.append(ABS_Y + (JOYSTICK_MIN, JOYSTICK_MAX, 0, 0)) + else: + virtual_product_id = GNOME15_KEYBOARD_PRODUCT_ID + + caps = tuple(keys) + uinput_device = uinput.Device(name="gnome15-%s" % device_type, + events = caps, + vendor = GNOME15_USB_VENDOR_ID, + product = virtual_product_id) + uinput_devices[device_type] = uinput_device + + # Centre the joystick by default + if device_type == JOYSTICK or device_type == DIGITAL_JOYSTICK: + syn(device_type) + load_calibration(device_type) + emit(device_type, ABS_X, JOYSTICK_CENTER, False) + emit(device_type, ABS_Y, JOYSTICK_CENTER, False) + syn(device_type) + else: + emit(device_type, 0, 0) + emit(device_type, 0, 1) + diff --git a/src/gnome15/g15upgrade.py b/src/gnome15/g15upgrade.py new file mode 100644 index 0000000..fc9f924 --- /dev/null +++ b/src/gnome15/g15upgrade.py @@ -0,0 +1,116 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Utility to upgrade from earlier versions of Gnome15. This should be called +upon startup of either g15-config or g15-desktop-service to check whether +any migration needs to take place. +""" + +import os.path +import g15devices +import g15globals +import util.g15pythonlang as g15pythonlang +import logging +import shutil +import sys +import subprocess + +logger = logging.getLogger(__name__) + +def upgrade(): + version_0_x_0_to_0_7_0() + version_0_x_0_to_0_8_5() + +def version_0_x_0_to_0_8_5(): + """ + Location of mail accounts moved + """ + old_path = os.path.expanduser("~/.gnome2/gnome15/lcdbiff/mailboxes.xml") + new_path = os.path.join(g15globals.user_config_dir, "plugin-data", "lcdbiff", "mailboxes.xml") + if os.path.exists(old_path) and not os.path.exists(new_path): + logger.warning("Upgrading to 0.8.5, moving mailboxes") + os.renames(old_path, new_path) + +def version_0_x_0_to_0_7_0(): + """ + First version to upgrade configuration. This is the version where + multiple device support was introduced, pushing configuration into + sub-directories + """ + macros_dir = os.path.join(g15globals.user_config_dir, "macro_profiles") + if os.path.exists(os.path.join(macros_dir, "0.macros")): + logger.info("Upgrading macros and configuration to 0.7.x format") + + """ + If the default macro profile exists at the root of the macro_profiles directory, + then conversion hasn't yet occurred. So, copy all profiles into all device + sub-directories + """ + devices = g15devices.find_all_devices() + for file in os.listdir(macros_dir): + if file.endswith(".macros"): + profile_file = os.path.join(macros_dir, file) + for device in devices: + device_dir = os.path.join(macros_dir, device.uid) + if not os.path.exists(device_dir): + logger.info("Creating macro_profile directory for %s", device.uid) + os.mkdir(device_dir) + logger.info("Copying macro_profile %s to %s ", file, device.uid) + shutil.copyfile(profile_file, os.path.join(device_dir, file)) + os.remove(profile_file) + + """ + Copy the GConf folders. + """ + gconf_dir = os.path.expanduser("~/.gconf/apps/gnome15") + gconf_file = os.path.join(gconf_dir, "%gconf.xml") + gconf_plugins_dir = os.path.join(gconf_dir, "plugins") + for device in devices: + device_dir = os.path.join(gconf_dir, device.uid) + if not os.path.exists(device_dir): + logger.info("Creating GConf directory for %s", device.uid) + os.mkdir(device_dir) + logger.info("Copying settings %s to %s", gconf_file, device.uid) + shutil.copyfile(gconf_file, os.path.join(device_dir, "%gconf.xml")) + logger.info("Copying plugin settings %s to %s", gconf_plugins_dir, device.uid) + target_plugins_path = os.path.join(device_dir, "plugins") + if not os.path.exists(target_plugins_path): + shutil.copytree(gconf_plugins_dir, target_plugins_path ) + logger.info("Clearing current settings root") + shutil.rmtree(gconf_plugins_dir) + f = open(gconf_file, 'w') + try: + f.write('\n') + f.write('\n') + f.write('\n') + finally: + f.close() + + + """ + Tell GConf to reload it caches by finding it's process ID and sending it + SIGHUP + """ + if sys.version_info > (2, 6): + process_info = subprocess.check_output(["sh", "-c", "ps -U %d|grep gconfd|head -1" % os.getuid()]) + else: + import commands + process_info = commands.getstatusoutput("sh -c \"ps -U %d|grep gconfd|head -1\"" % os.getuid()) + if process_info: + pid = g15pythonlang.split_args(process_info)[0] + logger.info("Sending process %s SIGHUP", pid) + subprocess.check_call([ "kill", "-SIGHUP", pid ]) diff --git a/src/gnome15/g15util.py b/src/gnome15/g15util.py new file mode 100644 index 0000000..21c32fb --- /dev/null +++ b/src/gnome15/g15util.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +This file only exists to keep compatibility with 3rd party plugins. +It has been splitted into several files +''' + +import util.g15cairo as g15cairo +import util.g15convert as g15convert +import util.g15gconf as g15gconf +import util.g15icontools as g15icontools +import util.g15markup as g15markup +import util.g15os as g15os +import util.g15pythonlang as g15pythonlang +import util.g15scheduler as g15scheduler +import util.g15svg as g15svg +import util.g15uigconf as g15uigconf +import g15notify +import g15driver + + +def execute_for_output(cmd): + return g15os.get_command_output(cmd) + +def run_script(script, args = None, background = True): + return g15os.run_script(script, args, background) + +def attr_exists(obj, attr_name): + return g15pythonlang.attr_exists(obj, attr_name) + +def call_if_exists(obj, function_name, *args): + g15pythonlang.call_if_exists(obj, function_name, args) + +def configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha = None): + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha) + +def to_cairo_rgba(gconf_client, key, default): + return g15gconf.get_cairo_rgba_or_default(gconf_client, key, default) + +def color_changed(widget, gconf_client, key): + g15uigconf.color_changed(widget, gconf_client, key) + +def rgb_to_string(rgb): + return g15convert.rgb_to_string(rgb) + +def get_alt_color(color): + return g15convert.get_alt_color(color) + +def color_to_rgb(color): + return g15convert.color_to_rgb(color) + +def to_rgb(string_rgb, default = None): + return g15convert.to_rgb(string_rgb, default) + +def to_pixel(rgb): + return g15convert.to_pixel(rgb) + +def to_color(rgb): + return g15convert.to_color(rgb) + +def spinner_changed(widget, gconf_client, key, model, decimal = False): + g15uigconf.spinner_changed(widget, gconf_client, key, model, decimal) + +def configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal = False): + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal) + +def configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree) + +def combo_box_changed(widget, gconf_client, key, model, default_value): + g15uigconf.combo_box_changed(widget, gconf_client, key, model, default_value) + +def boolean_conf_value_change(client, connection_id, entry, args): + g15uigconf.boolean_conf_value_change(client, connection_id, entry, args) + +def text_conf_value_change(client, connection_id, entry, args): + g15uigconf.text_conf_value_change(client, connection_id, entry, args) + +def radio_conf_value_change(client, connection_id, entry, args): + g15uigconf.radio_conf_value_change(client, connection_id, entry, args) + +def configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes) + +def configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes) + +def configure_radio_from_gconf(gconf_client, gconf_key, widget_ids , gconf_values, default_value, widget_tree, watch_changes = False): + return g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key, widget_ids , gconf_values, default_value, widget_tree, watch_changes) + +def configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree) + +def adjustment_changed(adjustment, key, gconf_client, integer = True): + g15uigconf.adjustment_changed(adjustment, key, gconf_client, integer) + +def checkbox_changed(widget, key, gconf_client): + g15uigconf.checkbox_changed(widget, key, gconf_client) + +def text_changed(widget, key, gconf_client): + g15uigconf.text_changed(widget, key, gconf_client) + +def radio_changed(widget, key, gconf_client, gconf_value): + g15uigconf.radio_changed(widget, key, gconf_client, gconf_value) + +def get_float_or_default(gconf_client, key, default = None): + return g15gconf.get_float_or_default(gconf_client, key, default) + +def get_string_or_default(gconf_client, key, default = None): + return g15gconf.get_string_or_default(gconf_client, key, default) + +def get_bool_or_default(gconf_client, key, default = None): + return g15gconf.get_bool_or_default(gconf_client, key, default) + +def get_int_or_default(gconf_client, key, default = None): + return g15gconf.get_int_or_default(gconf_client, key, default) + +def get_rgb_or_default(gconf_client, key, default = None): + return g15gconf.get_rgb_or_default(gconf_client, key, default) + +def is_gobject_thread(): + return g15pythonlang.is_gobject_thread() + +def set_gobject_thread(): + g15pythonlang.set_gobject_thread() + +def get_lsb_release(): + return g15os.get_lsb_release() + +def get_lsb_distributor(): + return g15os.get_lsb_distributor() + +def append_if_exists( el, key, val, formatter = "%s"): + return g15pythonlang.append_if_exists( el, key, val, formatter) + +def get_command_output( cmd): + return g15os.get_command_output( cmd) + +def module_exists(module_name): + return g15pythonlang.module_exists(module_name) + +def value_or_empty(d, key): + return g15pythonlang.value_or_empty(d, key) + +def value_or_blank(d, key): + return g15pythonlang.value_or_blank(d, key) + +def value_or_default(d, key, default_value): + return g15pythonlang.value_or_default(d, key, default_value) + +def find(f, seq): + return g15pythonlang.find(f, seq) + +def mkdir_p(path): + g15os.mkdir_p(path) + +def notify(summary, body = "", icon = "dialog-info", actions = [], hints = {}, timeout = 0): + return g15notify.notify(summary, body, icon, actions, hints, timeout, 0) + +def strip_tags(html): + return g15markup.strip_tags(html) + +def total_seconds(time_delta): + return g15pythonlang.total_seconds(time_delta) + +def rgb_to_uint16(r, g, b): + return g15convert.rgb_to_uint16(r, g, b) + +def rgb_to_hex(rgb): + return g15convert.rgb_to_hex(rgb) + +def degrees_to_radians(degrees): + return g15convert.degrees_to_radians(degrees) + +def rotate(context, degrees): + g15cairo.rotate(context, degrees) + +def rotate_around_center(context, width, height, degrees): + g15cairo.rotate_around_center(context, width, height, degrees) + +def flip_horizontal(context, width, height): + g15cairo.flip_horizontal(context, width, height) + +def flip_vertical(context, width, height): + g15cairo.flip_vertical(context, width, height) + +def flip_hv_centered_on(context, fx, fy, cx, cy): + g15cairo.flip_hv_centered_on(context, fx, fy, cx, cy) + +def get_cache_filename(filename, size = None): + return g15cairo.get_cache_filename(filename, size) + +def get_image_cache_file(filename, size = None): + return g15cairo.get_image_cache_file(filename, size) + +def is_url(path): + return g15cairo.is_url(path) + +def load_surface_from_file(filename, size = None): + return g15cairo.load_surface_from_file(filename, size) + +def load_svg_as_surface(filename, size): + return g15cairo.load_svg_as_surface(filename, size) + +def image_to_surface(image, type = "ppm"): + return g15cairo.image_to_surface(image, type) + +def pixbuf_to_surface(pixbuf, size = None): + return g15cairo.pixbuf_to_surface(pixbuf, size) + +def local_icon_or_default(icon_name, size = 128): + return g15icontools.local_icon_or_default(icon_name, size) + +def get_embedded_image_url(path): + return g15icontools.get_embedded_image_url(path) + +def get_icon_path(icon = None, size = 128, warning = True, include_missing = True): + return g15icontools.get_icon_path(icon, size, warning, include_missing) + +def get_app_icon(gconf_client, icon, size = 128): + return g15icontools.get_app_icon(gconf_client, icon, size) + +def get_icon(gconf_client, icon, size = None): + return g15icontools.get_icon(gconf_client, icon, size) + +def paint_thumbnail_image(allocated_size, image, canvas): + return g15cairo.paint_thumbnail_image(allocated_size, image, canvas) + +def get_scale(target, actual): + return g15cairo.get_scale(target, actual) + +def approx_px_to_pt(px): + return g15cairo.approx_px_to_pt(px) + +def rotate_element(element, degrees): + g15svg.rotate_element(element, degrees) + +def split_args(args): + return g15pythonlang.split_args(args) + +def get_transforms(element, position_only = False): + return g15svg.get_transforms(element, position_only) + +def get_location(element): + return g15svg.get_location(element) + +def get_actual_bounds(element, relative_to = None): + return g15svg.get_actual_bounds(element, relative_to) + +def get_bounds(element): + return g15svg.get_bounds(element) + +def image_to_pixbuf(im, type = "ppm"): + return g15cairo.image_to_pixbuf(im, type) + +def surface_to_pixbuf(surface): + return g15cairo.surface_to_pixbuf(surface) + +def get_key_names(keys): + return g15driver.get_key_names(keys) + +def html_escape(text): + return g15markup.html_escape(text) + +def parse_as_properties(properties_string): + return g15pythonlang.parse_as_properties(properties_string) + +def to_int_or_none(s): + return g15pythonlang.to_int_or_none(s) + +def to_float_or_none(s): + return g15pythonlang.to_float_or_none(s) diff --git a/src/gnome15/lcdsink.py b/src/gnome15/lcdsink.py new file mode 100644 index 0000000..432e9d4 --- /dev/null +++ b/src/gnome15/lcdsink.py @@ -0,0 +1,92 @@ +# PiTiVi , Non-linear video editor +# +# pitivi/elements/thumbnailsink.py +# +# Copyright (c) 2005, Edward Hervey +# +# This program 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 program 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 program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +""" +GdkPixbuf thumbnail sink +""" + +import gobject +import gst +import struct +import time + +big_to_cairo_alpha_mask = struct.unpack('=i', '\xFF\x00\x00\x00')[0] +big_to_cairo_red_mask = struct.unpack('=i', '\x00\xFF\x00\x00')[0] +big_to_cairo_green_mask = struct.unpack('=i', '\x00\x00\xFF\x00')[0] +big_to_cairo_blue_mask = struct.unpack('=i', '\x00\x00\x00\xFF')[0] + +class CairoSurfaceThumbnailSink(gst.BaseSink): + """ + GStreamer thumbnailing sink element. + + Can be used in pipelines to generates gtk.gdk.Pixbuf automatically. + """ + + __gsignals__ = { + "thumbnail": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ([gobject.TYPE_UINT64])) + } + + __gsttemplates__ = ( + gst.PadTemplate("sink", + gst.PAD_SINK, + gst.PAD_ALWAYS, + gst.Caps("video/x-raw-rgb," + "bpp = (int) 32, depth = (int) 32," + "endianness = (int) BIG_ENDIAN," + "alpha_mask = (int) %i, " + "red_mask = (int) %i, " + "green_mask = (int) %i, " + "blue_mask = (int) %i, " + "width = (int) [ 1, max ], " + "height = (int) [ 1, max ], " + "framerate = (fraction) [ 0, 25 ]" + % (big_to_cairo_alpha_mask, + big_to_cairo_red_mask, + big_to_cairo_green_mask, + big_to_cairo_blue_mask))) + ) + + def __init__(self): + gst.BaseSink.__init__(self) + self.width = 1 + self.height = 1 + self.set_sync(True) + self.data = None + + def do_set_caps(self, caps): + self.log("caps %s" % caps.to_string()) + self.log("padcaps %s" % self.get_pad("sink").get_caps().to_string()) + self.width = caps[0]["width"] + self.height = caps[0]["height"] + if not caps[0].get_name() == "video/x-raw-rgb": + return False + return True + + def do_render(self, buf): + self.data = str(buf.data) + self.emit('thumbnail', buf.timestamp) + return gst.FLOW_OK + + def do_preroll(self, buf): + return self.do_render(buf) + +gobject.type_register(CairoSurfaceThumbnailSink) diff --git a/src/gnome15/objgraph.py b/src/gnome15/objgraph.py new file mode 100644 index 0000000..398ab04 --- /dev/null +++ b/src/gnome15/objgraph.py @@ -0,0 +1,399 @@ +""" +Ad-hoc tools for drawing Python object reference graphs with graphviz. + +This module is more useful as a repository of sample code and ideas, than +as a finished product. For documentation and background, read + + http://mg.pov.lt/blog/hunting-python-memleaks.html + http://mg.pov.lt/blog/python-object-graphs.html + http://mg.pov.lt/blog/object-graphs-with-graphviz.html + +in that order. Then use pydoc to read the docstrings, as there were +improvements made since those blog posts. + +Copyright (c) 2008 Marius Gedminas + +Released under the MIT licence. + + +Changes +======= + +1.1dev (2008-09-05) +------------------- + +New function: show_refs() for showing forward references. + +New functions: typestats() and show_most_common_types(). + +Object boxes are less crammed with useless information (such as IDs). + +Spawns xdot if it is available. +""" +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__author__ = "Marius Gedminas (marius@gedmin.as)" +__copyright__ = "Copyright (c) 2008 Marius Gedminas" +__license__ = "MIT" +__version__ = "1.1dev" +__date__ = "2008-09-05" + + +import gc +import inspect +import types +import weakref +import operator +import os + + +def count(typename): + """Count objects tracked by the garbage collector with a given class name. + + Example: + + >>> count('dict') + 42 + >>> count('MyClass') + 3 + + Note that the GC does not track simple objects like int or str. + """ + return sum(1 for o in gc.get_objects() if type(o).__name__ == typename) + + +def typestats(): + """Count the number of instances for each type tracked by the GC. + + Note that the GC does not track simple objects like int or str. + + Note that classes with the same name but defined in different modules + will be lumped together. + """ + stats = {} + for o in gc.get_objects(): + stats.setdefault(type(o).__name__, 0) + stats[type(o).__name__] += 1 + return stats + + +def show_most_common_types(limit=10): + """Count the names of types with the most instances. + + Note that the GC does not track simple objects like int or str. + + Note that classes with the same name but defined in different modules + will be lumped together. + """ + stats = sorted(typestats().items(), key=operator.itemgetter(1), + reverse=True) + if limit: + stats = stats[:limit] + width = max(len(name) for name, count in stats) + for name, count in stats[:limit]: + print name.ljust(width), count + + +def by_type(typename): + """Return objects tracked by the garbage collector with a given class name. + + Example: + + >>> by_type('MyClass') + [] + + Note that the GC does not track simple objects like int or str. + """ + return [o for o in gc.get_objects() if type(o).__name__ == typename] + + +def at(addr): + """Return an object at a given memory address. + + The reverse of id(obj): + + >>> at(id(obj)) is obj + True + + Note that this function does not work on objects that are not tracked by + the GC (e.g. ints or strings). + """ + for o in gc.get_objects(): + if id(o) == addr: + return o + return None + + +def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()): + """Find a shortest chain of references leading to obj. + + The start of the chain will be some object that matches your predicate. + + ``max_depth`` limits the search depth. + + ``extra_ignore`` can be a list of object IDs to exclude those objects from + your search. + + Example: + + >>> find_backref_chain(obj, inspect.ismodule) + [, ..., obj] + + Returns None if such a chain could not be found. + """ + queue = [obj] + depth = {id(obj): 0} + parent = {id(obj): None} + ignore = set(extra_ignore) + ignore.add(id(extra_ignore)) + ignore.add(id(queue)) + ignore.add(id(depth)) + ignore.add(id(parent)) + ignore.add(id(ignore)) + gc.collect() + while queue: + target = queue.pop(0) + if predicate(target): + chain = [target] + while parent[id(target)] is not None: + target = parent[id(target)] + chain.append(target) + return chain + tdepth = depth[id(target)] + if tdepth < max_depth: + referrers = gc.get_referrers(target) + ignore.add(id(referrers)) + for source in referrers: + if inspect.isframe(source) or id(source) in ignore: + continue + if id(source) not in depth: + depth[id(source)] = tdepth + 1 + parent[id(source)] = target + queue.append(source) + return None # not found + + +def show_backrefs(objs, max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None, filename = 'objgraph'): + """Generate an object reference graph ending at ``objs`` + + The graph will show you what objects refer to ``objs``, directly and + indirectly. + + ``objs`` can be a single object, or it can be a list of objects. + + Produces a Graphviz .dot file and spawns a viewer (xdot) if one is + installed, otherwise converts the graph to a .png image. + + Use ``max_depth`` and ``too_many`` to limit the depth and breadth of the + graph. + + Use ``filter`` (a predicate) and ``extra_ignore`` (a list of object IDs) to + remove undesired objects from the graph. + + Use ``highlight`` (a predicate) to highlight certain graph nodes in blue. + + Examples: + + >>> show_backrefs(obj) + >>> show_backrefs([obj1, obj2]) + >>> show_backrefs(obj, max_depth=5) + >>> show_backrefs(obj, filter=lambda x: not inspect.isclass(x)) + >>> show_backrefs(obj, highlight=inspect.isclass) + >>> show_backrefs(obj, extra_ignore=[id(locals())]) + + """ + show_graph(objs, max_depth=max_depth, extra_ignore=extra_ignore, + filter=filter, too_many=too_many, highlight=highlight, + edge_func=gc.get_referrers, swap_source_target=False, filename = filename) + + +def show_refs(objs, max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None): + """Generate an object reference graph starting at ``objs`` + + The graph will show you what objects are reachable from ``objs``, directly + and indirectly. + + ``objs`` can be a single object, or it can be a list of objects. + + Produces a Graphviz .dot file and spawns a viewer (xdot) if one is + installed, otherwise converts the graph to a .png image. + + Use ``max_depth`` and ``too_many`` to limit the depth and breadth of the + graph. + + Use ``filter`` (a predicate) and ``extra_ignore`` (a list of object IDs) to + remove undesired objects from the graph. + + Use ``highlight`` (a predicate) to highlight certain graph nodes in blue. + + Examples: + + >>> show_refs(obj) + >>> show_refs([obj1, obj2]) + >>> show_refs(obj, max_depth=5) + >>> show_refs(obj, filter=lambda x: not inspect.isclass(x)) + >>> show_refs(obj, highlight=inspect.isclass) + >>> show_refs(obj, extra_ignore=[id(locals())]) + + """ + show_graph(objs, max_depth=max_depth, extra_ignore=extra_ignore, + filter=filter, too_many=too_many, highlight=highlight, + edge_func=gc.get_referents, swap_source_target=True) + +# +# Internal helpers +# + +def show_graph(objs, edge_func, swap_source_target, + max_depth=3, extra_ignore=(), filter=None, too_many=10, + highlight=None, filename = 'objects'): + if not isinstance(objs, (list, tuple)): + objs = [objs] + f = file('%s.dot' % filename, 'w') + print >> f, 'digraph ObjectGraph {' + print >> f, ' node[shape=box, style=filled, fillcolor=white];' + queue = [] + depth = {} + ignore = set(extra_ignore) + ignore.add(id(objs)) + ignore.add(id(extra_ignore)) + ignore.add(id(queue)) + ignore.add(id(depth)) + ignore.add(id(ignore)) + for obj in objs: + print >> f, ' %s[fontcolor=red];' % (obj_node_id(obj)) + depth[id(obj)] = 0 + queue.append(obj) + gc.collect() + nodes = 0 + while queue: + nodes += 1 + target = queue.pop(0) + tdepth = depth[id(target)] + print >> f, ' %s[label="%s"];' % (obj_node_id(target), obj_label(target, tdepth)) + h, s, v = gradient((0, 0, 1), (0, 0, .3), tdepth, max_depth) + if inspect.ismodule(target): + h = .3 + s = 1 + if highlight and highlight(target): + h = .6 + s = .6 + v = 0.5 + v * 0.5 + print >> f, ' %s[fillcolor="%g,%g,%g"];' % (obj_node_id(target), h, s, v) + if v < 0.5: + print >> f, ' %s[fontcolor=white];' % (obj_node_id(target)) + if inspect.ismodule(target) or tdepth >= max_depth: + continue + neighbours = edge_func(target) + ignore.add(id(neighbours)) + n = 0 + for source in neighbours: + if inspect.isframe(source) or id(source) in ignore: + continue + if filter and not filter(source): + continue + if swap_source_target: + srcnode, tgtnode = target, source + else: + srcnode, tgtnode = source, target + elabel = edge_label(srcnode, tgtnode) + print >> f, ' %s -> %s%s;' % (obj_node_id(srcnode), obj_node_id(tgtnode), elabel) + if id(source) not in depth: + depth[id(source)] = tdepth + 1 + queue.append(source) + n += 1 + if n >= too_many: + print >> f, ' %s[color=red];' % obj_node_id(target) + break + print >> f, "}" + f.close() + print "Graph written to objects.dot (%d nodes)" % nodes + if os.system('which xdot >/dev/null') == 0: + print "Spawning graph viewer (xdot)" + os.system("xdot %s.dot &" % filename) + else: + os.system("dot -Tpng %s.dot > %s.png" % (filename, filename)) + print "Image generated as objects.png" + + +def obj_node_id(obj): + if isinstance(obj, weakref.ref): + return 'all_weakrefs_are_one' + return ('o%d' % id(obj)).replace('-', '_') + + +def obj_label(obj, depth): + return quote(type(obj).__name__ + ':\n' + + safe_repr(obj)) + + +def quote(s): + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + + +def safe_repr(obj): + try: + return short_repr(obj) + except: + return '(unrepresentable)' + + +def short_repr(obj): + if isinstance(obj, (type, types.ModuleType, types.BuiltinMethodType, + types.BuiltinFunctionType)): + return obj.__name__ + if isinstance(obj, types.MethodType): + if obj.im_self is not None: + return obj.im_func.__name__ + ' (bound)' + else: + return obj.im_func.__name__ + if isinstance(obj, (tuple, list, dict, set)): + return '%d items' % len(obj) + if isinstance(obj, weakref.ref): + return 'all_weakrefs_are_one' + return repr(obj)[:40] + + +def gradient(start_color, end_color, depth, max_depth): + if max_depth == 0: + # avoid division by zero + return start_color + h1, s1, v1 = start_color + h2, s2, v2 = end_color + f = float(depth) / max_depth + h = h1 * (1-f) + h2 * f + s = s1 * (1-f) + s2 * f + v = v1 * (1-f) + v2 * f + return h, s, v + + +def edge_label(source, target): + if isinstance(target, dict) and target is getattr(source, '__dict__', None): + return ' [label="__dict__",weight=10]' + elif isinstance(source, dict): + for k, v in source.iteritems(): + if v is target: + if isinstance(k, basestring) and k: + return ' [label="%s",weight=2]' % quote(k) + else: + return ' [label="%s"]' % quote(safe_repr(k)) + return '' + diff --git a/src/gnome15/util/Makefile.am b/src/gnome15/util/Makefile.am new file mode 100644 index 0000000..b76f63e --- /dev/null +++ b/src/gnome15/util/Makefile.am @@ -0,0 +1,17 @@ +utildir = $(pkgpythondir)/util +util_PYTHON = \ + __init__.py \ + g15convert.py \ + g15scheduler.py \ + g15pythonlang.py \ + g15uigconf.py \ + g15gconf.py \ + g15os.py \ + g15cairo.py \ + g15svg.py \ + g15icontools.py \ + g15markup.py \ + jobqueue.py + +EXTRA_DIST = \ + $(util_PYTHON) \ No newline at end of file diff --git a/src/gnome15/util/__init__.py b/src/gnome15/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gnome15/util/g15cairo.py b/src/gnome15/util/g15cairo.py new file mode 100644 index 0000000..0e398c4 --- /dev/null +++ b/src/gnome15/util/g15cairo.py @@ -0,0 +1,289 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Cairo utilities +Has functions to transform, load and convert cairo surfaces +''' + +import gtk.gdk +import os, os.path +import cairo +import math +import rsvg +import urllib +import base64 +import xdg.Mime as mime +import g15convert +import g15os +import gnome15.g15globals + +# Logging +import logging +logger = logging.getLogger(__name__) + +from cStringIO import StringIO + +def rotate(context, degrees): + context.rotate(g15convert.degrees_to_radians(degrees)); + +def rotate_around_center(context, width, height, degrees): + context.translate (height * 0.5, width * 0.5); + context.rotate(degrees * (math.pi / 180)); + context.translate(-width * 0.5, -height * 0.5); + +def flip_horizontal(context, width, height): + flip_hv_centered_on(context, -1, 1, width / 2, height / 2) + +def flip_vertical(context, width, height): + # TODO - Should work according to http://cairographics.org/matrix_transform/, but doesn't + flip_hv_centered_on(context, -1, 1, width / 2, height / 2) + +def flip_hv_centered_on(context, fx, fy, cx, cy): + mtrx = cairo.Matrix(fx,0,0,fy,cx*(1-fx),cy*(fy-1)) + context.transform(mtrx) + +def get_cache_filename(filename, size = None): + cache_file = base64.urlsafe_b64encode("%s-%s" % ( filename, str(size if size is not None else "0,0") ) ) + g15os.mkdir_p(g15globals.user_cache_dir) + return os.path.join(g15globals.user_cache_dir, "%s.img" % cache_file) + +def get_image_cache_file(filename, size = None): + full_cache_path = get_cache_filename(filename, size) + if os.path.exists(full_cache_path): + return full_cache_path + +def is_url(path): + # TODO try harder + return "://" in path + +def load_surface_from_file(filename, size = None): + type = None + if filename == None: + logger.warning("Empty filename requested") + return None + + if filename.startswith("http:") or filename.startswith("https:"): + full_cache_path = get_image_cache_file(filename, size) + if full_cache_path: + meta_fileobj = open(full_cache_path + "m", "r") + type = meta_fileobj.readline() + meta_fileobj.close() + if type == "image/svg+xml" or filename.lower().endswith(".svg"): + return load_svg_as_surface(filename, size) + else: + return pixbuf_to_surface(gtk.gdk.pixbuf_new_from_file(full_cache_path), size) + + if is_url(filename): + type = None + try: + file = urllib.urlopen(filename) + data = file.read() + type = file.info().gettype() + + if filename.startswith("file://"): + type = str(mime.get_type(filename)) + + if filename.startswith("http:") or filename.startswith("https:"): + full_cache_path = get_cache_filename(filename, size) + cache_fileobj = open(full_cache_path, "w") + cache_fileobj.write(data) + cache_fileobj.close() + meta_fileobj = open(full_cache_path + "m", "w") + meta_fileobj.write(type + "\n") + meta_fileobj.close() + + if type == "image/svg+xml" or filename.lower().endswith(".svg"): + svg = rsvg.Handle() + try: + if not svg.write(data): + raise Exception("Failed to load SVG") + svg_size = svg.get_dimension_data()[2:4] + if size == None: + size = svg_size + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(size[0]) if not isinstance(size, int) else size, int(size[1]) if not isinstance(size, int) else size) + context = cairo.Context(surface) + if size != svg_size: + scale = get_scale(size, svg_size) + context.scale(scale, scale) + svg.render_cairo(context) + surface.flush() + return surface + finally: + svg.close() + else: + if type == "text/plain": + if filename.startswith("file://"): + pixbuf = gtk.gdk.pixbuf_new_from_file(filename[7:]) + return pixbuf_to_surface(pixbuf, size) + raise Exception("Could not determine type") + else: + pbl = gtk.gdk.pixbuf_loader_new_with_mime_type(type) + pbl.write(data) + pixbuf = pbl.get_pixbuf() + pbl.close() + return pixbuf_to_surface(pixbuf, size) + return None + except Exception as e: + logger.warning("Failed to get image %s (%s).", filename, type, exc_info = e) + return None + else: + if os.path.exists(filename): + try: + if filename.lower().endswith(".svg"): + if os.path.islink(filename): + filename = os.path.realpath(filename) + return load_svg_as_surface(filename, size) + else: + return pixbuf_to_surface(gtk.gdk.pixbuf_new_from_file(filename), size) + + except Exception as e: + logger.warning("Failed to get image %s (%s).", filename, type, exc_info = e) + return None + +def load_svg_as_surface(filename, size): + svg = rsvg.Handle(filename) + try: + svg_size = svg.get_dimension_data()[2:4] + if size == None: + size = svg_size + sx = int(size) if isinstance(size, int) or isinstance(size, float) else int(size[0]) + sy = int(size) if isinstance(size, int) or isinstance(size, float) else int(size[1]) + surface = cairo.ImageSurface(0, sx, sy) + context = cairo.Context(surface) + if size != svg_size: + scale = get_scale(size, svg_size) + context.scale(scale, scale) + svg.render_cairo(context) + return surface + finally: + svg.close() + +def image_to_surface(image, type = "ppm"): + # TODO make better + return pixbuf_to_surface(image_to_pixbuf(image, type)) + +def pixbuf_to_surface(pixbuf, size = None): + x = pixbuf.get_width() + y = pixbuf.get_height() + scale = get_scale(size, (x, y)) + surface = cairo.ImageSurface(0, int(x * scale), int(y * scale)) + context = cairo.Context(surface) + gdk_context = gtk.gdk.CairoContext(context) + if size != None: + gdk_context.scale(scale, scale) + gdk_context.set_source_pixbuf(pixbuf,0,0) + gdk_context.paint() + gdk_context.scale(1 / scale, 1 / scale) + return surface + + +''' +Convert a PIL image to a GDK pixbuf +''' +def image_to_pixbuf(im, type = "ppm"): + p_type = type + if type == "ppm": + p_type = "pnm" + file1 = StringIO() + try: + im.save(file1, type) + contents = file1.getvalue() + finally: + file1.close() + loader = gtk.gdk.PixbufLoader(p_type) + loader.write(contents, len(contents)) + pixbuf = loader.get_pixbuf() + loader.close() + return pixbuf + +def surface_to_pixbuf(surface): + try: + file1 = StringIO() + surface.write_to_png(file1) + contents = file1.getvalue() + finally: + file1.close() + loader = gtk.gdk.PixbufLoader("png") + loader.write(contents, len(contents)) + pixbuf = loader.get_pixbuf() + loader.close() + return pixbuf + +def paint_thumbnail_image(allocated_size, image, canvas): + s = float(allocated_size) / image.get_height() + canvas.save() + canvas.scale(s, s) + canvas.set_source_surface(image) + canvas.paint() + canvas.scale(1 / s, 1 / s) + canvas.restore() + return image.get_width() * s + +def get_scale(target, actual): + scale = 1.0 + if target != None: + if isinstance(target, int) or isinstance(target, float): + sx = float(target) / actual[0] + sy = float(target) / actual[1] + else: + sx = float(target[0]) / actual[0] + sy = float(target[1]) / actual[1] + scale = max(sx, sy) + return scale + +pt_to_px = { + 6.0: 8.0, + 7.0: 9, + 7.5: 10, + 8.0: 11, + 9.0: 12, + 10.0: 13, + 10.5: 14, + 11.0: 15, + 12.0: 16, + 13.0: 17, + 13.5: 18, + 14.0: 19, + 14.5: 20, + 15.0: 21, + 16.0: 22, + 17.0: 23, + 18.0: 24, + 20.0: 26, + 22.0: 29, + 24.0: 32, + 26.0: 35, + 27.0: 36, + 28.0: 37, + 29.0: 38, + 30.0: 40, + 32.0: 42, + 34.0: 45, + 36.0: 48 + } +px_to_pt = {} +for pt in pt_to_px: + px_to_pt[pt_to_px[pt]] = pt + +def approx_px_to_pt(px): + px = round(px) + if px in px_to_pt: + return px_to_pt[px] + else: + return int(px * 72.0 / 96) + diff --git a/src/gnome15/util/g15convert.py b/src/gnome15/util/g15convert.py new file mode 100644 index 0000000..a1924e4 --- /dev/null +++ b/src/gnome15/util/g15convert.py @@ -0,0 +1,85 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Various conversions +''' + +import gtk.gdk +import math + +def rgb_to_string(rgb): + if rgb == None: + return None + else: + return "%d,%d,%d" % rgb + +def get_alt_color(color): + if color[0] == color[1] == color[2]: + return (1.0-color[0], 1.0-color[1], 1.0-color[2], color[3]) + else: + return (color[1],color[2],color[0],color[3]) + +def color_to_rgb(color): + i = ( color.red >> 8, color.green >> 8, color.blue >> 8 ) + return ( i[0],i[1],i[2] ) + +def to_rgb(string_rgb, default = None): + # Currently this method is implemented in g15gconf so that it avoids + # a dependency on g15convert. + # g15convert depends on gtk, and when initializing the gtk module a + # DISPLAY needs to be available. + # Unfortunately, for running g15-system-service, there is no DISPLAY + # set in it's environment, so it would make it throw an error. + # See https://projects.russo79.com/issues/173 + import g15gconf + return g15gconf._to_rgb(string_rgb, default) + +def to_pixel(rgb): + return ( rgb[0] << 24 ) + ( rgb[1] << 16 ) + ( rgb[2] < 8 ) + 0 + +def to_color(rgb): + return gtk.gdk.Color(rgb[0] <<8, rgb[1] <<8,rgb[2] <<8) + +def rgb_to_uint16(r, g, b): + rBits = r * 32 / 255 + gBits = g * 64 / 255 + bBits = b * 32 / 255 + + rBits = rBits if rBits <= 31 else 31 + gBits = gBits if gBits <= 63 else 63 + bBits = bBits if bBits <= 31 else 31 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + + return chr(valueL & 0xff) + chr(valueH & 0xff) + +def rgb_to_hex(rgb): + # Currently this method is implemented in g15driver so that it avoids + # a dependency on g15convert. + # g15convert depends on gtk, and when initializing the gtk module a + # DISPLAY needs to be available. + # Unfortunately, for running g15-system-service, there is no DISPLAY + # set in it's environment, so it would make it throw an error. + # See https://projects.russo79.com/issues/173 + import g15driver + return g15driver.rgb_to_hex(rgb) + +def degrees_to_radians(degrees): + return degrees * (math.pi / 180.0) + diff --git a/src/gnome15/util/g15gconf.py b/src/gnome15/util/g15gconf.py new file mode 100644 index 0000000..6e03af3 --- /dev/null +++ b/src/gnome15/util/g15gconf.py @@ -0,0 +1,121 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Set of utility methods to read values stored in gconf +''' + +def get_float_or_default(gconf_client, key, default = None): + """ + Tries to read a float value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + float_val = gconf_client.get(key) + return default if float_val == None else float_val.get_float() + +def get_string_or_default(gconf_client, key, default = None): + """ + Tries to read a string value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + str_val = gconf_client.get(key) + return default if str_val == None else str_val.get_string() + +def get_bool_or_default(gconf_client, key, default = None): + """ + Tries to read a boolean value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + bool_val = gconf_client.get(key) + return default if bool_val == None else bool_val.get_bool() + +def get_int_or_default(gconf_client, key, default = None): + """ + Tries to read a integer value from GConf and return a default value it + doesn't exist. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + int_val = gconf_client.get(key) + return default if int_val == None else int_val.get_int() + +def get_rgb_or_default(gconf_client, key, default = None): + """ + Tries to read a "rgb" value from GConf and return a default value it + doesn't exist. + A "rgb" value is in fact a comma separated string with the Red, Green and + Blue components encoded from 0 to 255. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + val = gconf_client.get_string(key) + return default if val == None or val == "" else _to_rgb(val) + +def get_cairo_rgba_or_default(gconf_client, key, default): + """ + Tries to read a "rgba" value from GConf and return a default value if + it doesn't exist. + A "rgba" value is encoded as two key on gconf. The first one is similar to + the rgb value described in get_rgb_or_default. The second one is stored in + _opacity and it represents the alpha value. + The returned value is encoded in a float tuple with each component ranging + from 0.0 to 1. + + Keyword arguments: + gconf_client : GConf client instance that will read the value + key : full path of the key to be read + default : value to return if key was not found + """ + str_val = gconf_client.get_string(key) + if str_val == None or str_val == "": + val = default + else: + v = _to_rgb(str_val) + alpha = gconf_client.get_int(key + "_opacity") + val = ( v[0], v[1],v[2], alpha) + return (float(val[0]) / 255.0, float(val[1]) / 255.0, float(val[2]) / 255.0, float(val[3]) / 255.0) + +def _to_rgb(string_rgb, default = None): + #This method should be in g15convert. The thing is that + #g15convert depends on gtk and on Fedora it raises an error when launching + #g15-system-service. + #(See https://projects.russo79.com/issues/173) + if string_rgb == None or string_rgb == "": + return default + rgb = string_rgb.split(",") + return (int(rgb[0]), int(rgb[1]), int(rgb[2])) diff --git a/src/gnome15/util/g15icontools.py b/src/gnome15/util/g15icontools.py new file mode 100644 index 0000000..e2e71da --- /dev/null +++ b/src/gnome15/util/g15icontools.py @@ -0,0 +1,137 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Icon utilities +''' + +from gnome15 import g15globals +import g15cairo +import gtk.gdk +import os +import cairo +from PIL import Image +import urllib +import base64 + +# Logging +import logging +logger = logging.getLogger(__name__) + +from cStringIO import StringIO + +''' +Look for icons locally as well if running from source +''' +gtk_icon_theme = gtk.icon_theme_get_default() +if g15globals.dev: + gtk_icon_theme.prepend_search_path(g15globals.icons_dir) + +def local_icon_or_default(icon_name, size = 128): + return get_icon_path(icon_name, size) + +def get_embedded_image_url(path): + + file_str = StringIO() + try: + img_data = StringIO() + try: + file_str.write("data:") + + if isinstance(path, cairo.ImageSurface): + # Cairo canvas + file_str.write("image/png") + path.write_to_png(img_data) + else: + if not "://" in path: + # File + surface = load_surface_from_file(path) + file_str.write("image/png") + surface.write_to_png(img_data) + else: + # URL + pagehandler = urllib.urlopen(path) + file_str.write(pagehandler.info().gettype()) + while 1: + data = pagehandler.read(512) + if not data: + break + img_data.write(data) + + file_str.write(";base64,") + file_str.write(base64.b64encode(img_data.getvalue())) + return file_str.getvalue() + finally: + img_data.close() + finally: + file_str.close() + +def get_icon_path(icon = None, size = 128, warning = True, include_missing = True): + o_icon = icon + if isinstance(icon, list): + for i in icon: + p = get_icon_path(i, size, warning = False, include_missing = False) + if p != None: + return p + logger.warning("Icon %s (%d) not found", str(icon), size) + if include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + else: + if icon != None: + icon = gtk_icon_theme.lookup_icon(icon, size, 0) + if icon != None: + if icon.get_filename() == None and warning: + logger.warning("Found icon %s (%d), but no filename was available", o_icon, size) + fn = icon.get_filename() + if os.path.isfile(fn): + return fn + elif include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + if warning: + logger.warning("Icon %s (%d) not found, using missing image", o_icon, size) + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + else: + if os.path.isfile(o_icon): + return o_icon + else: + if warning: + logger.warning("Icon %s (%d) not found", o_icon, size) + if include_missing and not icon in [ "image-missing", "gtk-missing-image" ]: + return get_icon_path(["image-missing", "gtk-missing-image"], size, warning) + +def get_app_icon(gconf_client, icon, size = 128): + icon_path = get_icon_path(icon, size) + if icon_path == None: + icon_path = os.path.join(g15globals.icons_dir,"hicolor", "scalable", "apps", "%s.svg" % icon) + return icon_path + +def get_icon(gconf_client, icon, size = None): + real_icon_file = get_icon_path(icon, size) + if real_icon_file != None: + if real_icon_file.endswith(".svg"): + pixbuf = gtk.gdk.pixbuf_new_from_file(real_icon_file) + scale = g15cairo.get_scale(size, (pixbuf.get_width(), pixbuf.get_height())) + if scale != 1.0: + pixbuf = pixbuf.scale_simple(pixbuf.get_width() * scale, pixbuf.get_height() * scale, gtk.gdk.INTERP_BILINEAR) + img = Image.fromstring("RGBA", (pixbuf.get_width(), pixbuf.get_height()), pixbuf.get_pixels()) + else: + img = Image.open(real_icon_file) + scale = g15cairo.get_scale(size, img.size) + if scale != 1.0: + img = img.resize((img.size[0] * scale, img.size[1] * scale),Image.BILINEAR) + + return img + diff --git a/src/gnome15/util/g15markup.py b/src/gnome15/util/g15markup.py new file mode 100644 index 0000000..5c5c017 --- /dev/null +++ b/src/gnome15/util/g15markup.py @@ -0,0 +1,48 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Markup utilities +''' + +from HTMLParser import HTMLParser + +class MLStripper(HTMLParser): + def __init__(self): + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def get_data(self): + return ''.join(self.fed) + +def strip_tags(html): + s = MLStripper() + s.feed(html) + return s.get_data() + +html_escape_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + +def html_escape(text): + return "".join(html_escape_table.get(c,c) for c in text) + diff --git a/src/gnome15/util/g15os.py b/src/gnome15/util/g15os.py new file mode 100644 index 0000000..79f7474 --- /dev/null +++ b/src/gnome15/util/g15os.py @@ -0,0 +1,133 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Gnome15 utilities to work with the system (running commands, manipulating the +filesystem, getting OS information...) +''' + +from gnome15 import g15globals +import os + +# Logging +import logging +logger = logging.getLogger(__name__) + +def run_script(script, args = None, background = True): + """ + Runs a python script from the scripts directory. + + Keyword arguments: + script: the filename of the script to run + args: an array of arguments to pass to the script (optional, None by default) + background: Set to run the script in the background (optional, True by default) + """ + a = "" + if args: + for arg in args: + a += "\"%s\"" % arg + p = os.path.realpath(os.path.join(g15globals.scripts_dir,script)) + logger.info("Running '%s'", p) + return os.system("\"%s\" %s %s" % ( p, a, " &" if background else "" )) + +def get_command_output(cmd): + """ + Runs a command on the shell and returns it's status code and output + + Keyword arguments: + cmd: the command to run (either full path, or just the name if the command + is in the %PATH) + + Returns + A tuple with the exit code of the command and the output made on stdout by + the command. + Note: the last '\n' is stripped from the output. + """ + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text + +def mkdir_p(path): + """ + Creates a directory and it's parents if needed unless it already exists.. + + Keyword arguments: + path: the full path to the directory to create. + """ + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + logger.debug("Error when trying to create path %s", path, exc_info = exc) + import errno + if exc.errno == errno.EEXIST: + pass + else: raise + +def full_path_of_program(program_name): + """ + Search for program_name in all the directories declared in the PATH + environment variable + + Keyword arguments: + program_name: the name of the program to search for + + Returns: + Full path name of the program_name, None if program_name was not + found in PATH. + """ + for dir in os.environ['PATH'].split(':'): + full_path = os.path.join(dir, program_name) + if os.path.exists(full_path): + return full_path + return None + +def is_program_in_path(program_name): + """ + Checks if a program_name is available in PATH environment variable + + Keyword arguments: + program_name: the name of the program to check + + Returns True if program_name is in PATH, else False + """ + return full_path_of_program(program_name) != None + +def get_lsb_release(): + """ + Gets the release number of the distribution + + Return: + ret: Return code of the lsb_release command + r: The release number + """ + ret, r = get_command_output('lsb_release -rs') + return float(r) if ret == 0 else 0 + +def get_lsb_distributor(): + """ + Gets the Linux distribution distributor id + + Return: + ret: Return code of the lsb_release command + r: The distributor id or "Unknown" if an error occurred + """ + ret, r = get_command_output('lsb_release -is') + return r if ret == 0 else "Unknown" + diff --git a/src/gnome15/util/g15pythonlang.py b/src/gnome15/util/g15pythonlang.py new file mode 100644 index 0000000..2f5a6b1 --- /dev/null +++ b/src/gnome15/util/g15pythonlang.py @@ -0,0 +1,186 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +import threading + +import logging +logger = logging.getLogger(__name__) + +''' +Helper methods "extending" the python syntax +''' + +def attr_exists(obj, attr_name): + """ + Get if an attribute exists on an object + + Keyword arguments: + obj -- object + attr_name -- attribute name + """ + return getattr(obj, attr_name, None) is not None + +def call_if_exists(obj, function_name, *args): + """ + Call a function on an object if it exists, ignoring any errors if it doesn't + """ + func = getattr(obj, function_name, None) + if callable(func): + func(*args) + +def module_exists(module_name): + """ + Get if a module exists + + Keyword arguments: + module_name: the name of the module to check + """ + try: + __import__(module_name) + except ImportError as e: + logger.debug("Could not find module %s", module_name, exc_info = e) + return False + else: + return True + +def value_or_empty(d, key): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then an empty array is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + """ + return value_or_default(d, key, []) + +def value_or_blank(d, key): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then an empty string is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + """ + return value_or_default(d, key, "") + +def value_or_default(d, key, default_value): + """ + Returns the value corresponding to a given key in a dictionnary. + If no value is found, then a default value is returned. + + Keyword arguments: + d: The dictionnary where to search for the value + key: The key to use for the lookup + default_value: The default value to return if no value is found + """ + try : + return d[key] + except KeyError as ke: + logger.debug("Didn't found %s in %s", key, d, exc_info = ke) + return default_value + +def to_int_or_none(s): + """ + Converts a string to a int or returns None if there was an error converting + """ + try: + return int(s) + except (ValueError, TypeError) as e: + logger.debug("Error converting %s to int", s, exc_info = e) + return None + +def to_float_or_none(s): + """ + Converts a string to a float or returns None if there was an error converting + """ + try: + return float(s) + except (ValueError, TypeError) as e: + logger.debug("Error converting %s to float", s, exc_info = e) + return None + +def find(f, seq): + """Return first item in sequence where f(item) == True.""" + for item in seq: + if f(item): + return item + +def append_if_exists(el, key, val, formatter = "%s"): + """ + Appends a value from a dictionnary to a string applying a formatter. + The value is only appended if it exists in the dictionnary and it's value is not None + + Keyword arguments: + el: The dictionnary where to search for the value + key: The key to search of the dictionnary + val: The string to which the found value will be appended + formatter: A format string to apply when appending the found value + + Returns: A new string with the found value appended to (prefixed by a comma) + """ + if key in el and el[key] is not None and len(str(el[key])) > 0: + if len(val) > 0: + val += "," + val += formatter % el[key] + return val + +def parse_as_properties(properties_string): + """ + Create a dictionnary [key,value] from a string containing a set of + name=value pairs separated by '\n' + + Keyword elements: + properties_string: string containing a set of name=value fields + """ + d = {} + for l in properties_string.split("\n"): + a = l.split("=") + if len(a) > 1: + d[a[0]] = a[1] + return d + +def split_args(args): + return re.findall(r'\w+', args) + +''' +Date / time utilities +''' +def total_seconds(time_delta): + """ + Calculate the total of seconds ellapsed in a timedelta value + + Keyword arguments: + time_delta: The timedelta value for which the number of seconds should be + calculated. + """ + return (time_delta.microseconds + (time_delta.seconds + time_delta.days * 24.0 * 3600.0) * 10.0**6.0) / 10.0**6.0 + +''' +GObject thread. Hosting applications may set this so that is_gobject_thread() +function works +''' +gobject_thread = [ None ] + +def is_gobject_thread(): + return threading.currentThread() == gobject_thread[0] + +def set_gobject_thread(): + gobject_thread[0] = threading.currentThread() + diff --git a/src/gnome15/util/g15scheduler.py b/src/gnome15/util/g15scheduler.py new file mode 100644 index 0000000..8902ae0 --- /dev/null +++ b/src/gnome15/util/g15scheduler.py @@ -0,0 +1,61 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject +import g15pythonlang + +# Logging +import logging +logger = logging.getLogger(__name__) + +import jobqueue + +''' +Default scheduler +''' +scheduler = jobqueue.JobScheduler() + +''' +Task scheduler. Tasks may be added to the queue to execute +after a specified interval. The timer is done by the gobject +event loop, which then executes the job on a different thread +''' + +def clear_jobs(queue_name = None): + scheduler.clear_jobs(queue_name) + +def execute(queue_name, job_name, function, *args): + return scheduler.execute(queue_name, job_name, function, *args) + +def schedule(job_name, interval, function, *args): + return scheduler.schedule(job_name, interval, function, *args) + +def run_on_gobject(function, *args): + if g15pythonlang.is_gobject_thread(): + return False + else: + gobject.idle_add(function, *args) + return True + +def stop_queue(queue_name): + scheduler.stop_queue(queue_name) + +def queue(queue_name, job_name, interval, function, *args): + return scheduler.queue(queue_name, job_name, interval, function, *args) + +def stop_all_schedulers(): + scheduler.stop_all() diff --git a/src/gnome15/util/g15svg.py b/src/gnome15/util/g15svg.py new file mode 100644 index 0000000..5628e12 --- /dev/null +++ b/src/gnome15/util/g15svg.py @@ -0,0 +1,152 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +SVG utilities +''' + +import cairo +import g15pythonlang + +# Logging +import logging +logger = logging.getLogger(__name__) + + +def rotate_element(element, degrees): + transforms = get_transforms(element) + if len(transforms) > 0: + t = transforms[0] + for i in range(1, len(transforms)): + t = t.multiply(transforms[i]) + else: + t = cairo.Matrix() + + t.rotate(g15convert.degrees_to_radians(degrees)) + ts = "m" + str(t)[7:] + element.set("transform", ts) + +def get_transforms(element, position_only = False): + transform_val = element.get("transform") + list = [] + if transform_val != None: + start = 0 + while True: + start_args = transform_val.find("(", start) + if start_args == -1: + break + name = transform_val[:start_args].lstrip() + end_args = transform_val.find(")", start_args) + if end_args == -1: + break + args = transform_val[start_args + 1:end_args].split(",") + if name == "translate": + list.append(cairo.Matrix(1.0, 0.0, 0.0, 1.0, float(args[0]), float(args[1]))) + elif name == "matrix": + if position_only: + list.append(cairo.Matrix(float(args[0]), float(args[1]), float(args[2]), float(args[3]),float(args[4]),float(args[5]))) + else: + list.append(cairo.Matrix(1, 0, 0, 1, float(args[4]),float(args[5]))) + elif name == "scale": + list.append(cairo.Matrix(float(args[0]), 0.0, 0.0, float(args[1]), 0.0, 0.0)) + else: + logger.warning("Unsupported transform %s", name) + start = end_args + 1 + + return list + +def get_location(element): + list = [] + while element != None: + x = element.get("x") + y = element.get("y") + if x != None and y != None: + list.append((float(x), float(y))) + transform_val = element.get("transform") + if transform_val != None: + start = 0 + while True: + start_args = transform_val.find("(", start) + if start_args == -1: + break + name = transform_val[:start_args].lstrip() + end_args = transform_val.find(")", start_args) + if end_args == -1: + logger.warning("Unexpected end of transform arguments") + break + args = g15pythonlang.split_args(transform_val[start_args + 1:end_args]) + if name == "translate": + list.append((float(args[0]), float(args[1]))) + elif name == "matrix": + list.append((float(args[4]),float(args[5]))) + else: + logger.warning("WARNING: Unsupported transform %s", name) + start = end_args + 1 + element = element.getparent() + list.reverse() + x = 0 + y = 0 + for i in list: + x += i[0] + y += i[1] + return (x, y) + +def get_actual_bounds(element, relative_to = None): + id = element.get("id") + + bounds = get_bounds(element) + transforms = [] + t = cairo.Matrix() + t.translate(bounds[0],bounds[1]) + transforms.append(t) + + # If the element is a clip path and the associated clipped_node is provided, the work out the transforms from + # the parent of the clipped_node, not the clip itself + if relative_to is not None: + element = relative_to.getparent() + + while element != None: + transforms += get_transforms(element, position_only=True) + element = element.getparent() + transforms.reverse() + if len(transforms) > 0: + t = transforms[0] + for i in range(1, len(transforms)): + t = t.multiply(transforms[i]) + + xx, yx, xy, yy, x0, y0 = t + return x0, y0, bounds[2], bounds[3] + +def get_bounds(element): + x = 0.0 + y = 0.0 + w = 0.0 + h = 0.0 + v = element.get("x") + if v != None: + x = float(v) + v = element.get("y") + if v != None: + y = float(v) + v = element.get("width") + if v != None: + w = float(v) + v = element.get("height") + if v != None: + h = float(v) + return (x, y, w, h) + diff --git a/src/gnome15/util/g15uigconf.py b/src/gnome15/util/g15uigconf.py new file mode 100644 index 0000000..776cfd2 --- /dev/null +++ b/src/gnome15/util/g15uigconf.py @@ -0,0 +1,278 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import g15convert + +''' +Set of utility methods to ease the binding between UI widgets and gconf settings +''' + +def configure_colorchooser_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, default_alpha = None): + """ + Sets the color and alpha values of a colorchooser widget from a gconf key + and initialize an event to store the value set by the user in the same + gconf key + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to setup + default_alpha: If different than None, the alpha value for the color is read + from the gconf_key + "_opacity" key. + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + val = gconf_client.get_string(gconf_key) + if val == None or val == "": + col = g15convert.to_color(default_value) + else: + col = g15convert.to_color(g15convert.to_rgb(val)) + if default_alpha != None: + alpha = gconf_client.get_int(gconf_key + "_opacity") + widget.set_use_alpha(True) + widget.set_alpha(alpha << 8) + else: + widget.set_use_alpha(False) + widget.set_color(col) + handler_id = widget.connect("color-set", color_changed, gconf_client, gconf_key) + return handler_id + +def color_changed(widget, gconf_client, key): + val = widget.get_color() + gconf_client.set_string(key, "%d,%d,%d" % ( val.red >> 8, val.green >> 8, val.blue >> 8 )) + if widget.get_use_alpha(): + gconf_client.set_int(key + "_opacity", widget.get_alpha() >> 8) + +def configure_spinner_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, decimal = False): + """ + Sets the value of a spinner from a gconf key and initializes an event to + store the spinner value in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to set up + decimal: If True, then the spinner value is a float else an int + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + model = widget.get_adjustment() + entry = gconf_client.get(gconf_key) + val = default_value + if entry != None: + if decimal: + val = entry.get_float() + else: + val = entry.get_int() + model.set_value(val) + handler_id = widget.connect("value-changed", spinner_changed, gconf_client, gconf_key, model) + return handler_id + + +def spinner_changed(widget, gconf_client, key, model, decimal = False): + if decimal: + gconf_client.set_float(key, widget.get_value()) + else: + gconf_client.set_int(key, int(widget.get_value())) + +def configure_combo_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + """ + Selects an item of a combobox from a gconf key and initializes an event to + store the current selected item value in the same gconf key when changed by + the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + This value can either be an int or a string. + When an int, it represents the index of the combobox item to select. + When a string, it represents the value that must be selected. + widget_tree: Widget tree containing the widget to set up + """ + widget = widget_tree.get_object(widget_id) + if widget == None: + raise Exception("No widget with id %s." % widget_id) + model = widget.get_model() + handler_id = widget.connect("changed", combo_box_changed, gconf_client, gconf_key, model, default_value) + + if isinstance(default_value, int): + e = gconf_client.get(gconf_key) + if e: + val = e.get_int() + else: + val = default_value + else: + val = gconf_client.get_string(gconf_key) + if val == None or val == "": + val = default_value + idx = 0 + for row in model: + if isinstance(default_value, int): + row_val = int(row[0]) + else: + row_val = str(row[0]) + if row_val == val: + widget.set_active(idx) + idx += 1 + + return handler_id + +def combo_box_changed(widget, gconf_client, key, model, default_value): + if isinstance(default_value, int): + gconf_client.set_int(key, int(model[widget.get_active()][0])) + else: + gconf_client.set_string(key, model[widget.get_active()][0]) + +def configure_checkbox_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + """ + Sets the state of a checkbox from a gconf key and initializes an event to + store the current state in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the state of the checkbox when + the value of the gconf key changes. + """ + widget = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + connection_id = None + if entry != None: + widget.set_active(entry.get_bool()) + else: + widget.set_active(default_value) + handler_id = widget.connect("toggled", checkbox_changed, gconf_key, gconf_client) + if watch_changes: + connection_id = gconf_client.notify_add(gconf_key, boolean_conf_value_change,( widget, gconf_key )); + return (handler_id, connection_id) + +def boolean_conf_value_change(client, connection_id, entry, args): + widget, key = args + widget.set_active( entry.get_value().get_bool()) + +def checkbox_changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def configure_text_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree, watch_changes = False): + """ + Sets the text of a text entry widget from a gconf key and initializes an + event to store the text in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the value of the text entry when + the value of the gconf key changes. + """ + widget = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + connection_id = None + if entry != None: + widget.set_text(entry.get_string()) + else: + widget.set_text(default_value) + handler_id = widget.connect("changed", text_changed, gconf_key, gconf_client) + if watch_changes: + connection_id = gconf_client.notify_add(gconf_key, text_conf_value_change,( widget, gconf_key )); + return (handler_id, connection_id) + +def text_conf_value_change(client, connection_id, entry, args): + widget, key = args + widget.set_text( entry.get_value().get_string()) + +def text_changed(widget, key, gconf_client): + gconf_client.set_string(key, widget.get_text()) + +def configure_radio_from_gconf(gconf_client, gconf_key, widget_ids, gconf_values, default_value, widget_tree, watch_changes = False): + """ + Sets the checked state of a set of radioboxes from a gconf key and initializes + an event to store their state in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_ids: Ids of the widgets + gconf_values: The values that should be read from the gconf_key to activate the + radiobox + default_value: Default value to set if it isn't set in GConf. + widget_tree: Widget tree containing the widget to set up + watch_changes: If True, then keeps updating the value of the text entry when + the value of the gconf key changes. + """ + entry = gconf_client.get(gconf_key) + handler_ids = [] + connection_ids = [] + sel_entry = entry.get_string() if entry else None + for i in range(0, len(widget_ids)): + gconf_value = gconf_values[i] + active = ( entry != None and gconf_value == sel_entry ) or ( entry == None and default_value == gconf_value ) + widget_tree.get_object(widget_ids[i]).set_active(active) + + for i in range(0, len(widget_ids)): + widget = widget_tree.get_object(widget_ids[i]) + handler_ids.append(widget.connect("toggled", radio_changed, gconf_key, gconf_client, gconf_values[i])) + if watch_changes: + connection_ids.append(gconf_client.notify_add(gconf_key, radio_conf_value_change,( widget, gconf_key, gconf_values[i] ))) + else: + connection_ids.append(None) + return (handler_ids, connection_ids) + +def radio_conf_value_change(client, connection_id, entry, args): + widget, key, gconf_value = args + str_value = entry.get_value().get_string() + widget.set_active(str_value == gconf_value) + +def radio_changed(widget, key, gconf_client, gconf_value): + gconf_client.set_string(key, gconf_value) + +def configure_adjustment_from_gconf(gconf_client, gconf_key, widget_id, default_value, widget_tree): + """ + Sets the value of a adjustment from a gconf key and initializes an event to + store the value in the same gconf key when changed by the user. + + gconf_client: Instance of GConfClient to communicate with GConf + gconf_key: Name of the key having the value + widget_id: Id of the widget + default_value: Default value to set if it isn't set in GConf + widget_tree: Widget tree containing the widget to set up + """ + adj = widget_tree.get_object(widget_id) + entry = gconf_client.get(gconf_key) + if entry != None: + if isinstance(default_value, int): + adj.set_value(entry.get_int()) + else: + adj.set_value(entry.get_float()) + else: + adj.set_value(default_value) + handler_id = adj.connect("value-changed", adjustment_changed, gconf_key, gconf_client, isinstance(default_value, int)) + return handler_id + +def adjustment_changed(adjustment, key, gconf_client, integer = True): + if integer: + gconf_client.set_int(key, int(adjustment.get_value())) + else: + gconf_client.set_float(key, adjustment.get_value()) + diff --git a/src/gnome15/util/jobqueue.py b/src/gnome15/util/jobqueue.py new file mode 100644 index 0000000..6128df4 --- /dev/null +++ b/src/gnome15/util/jobqueue.py @@ -0,0 +1,287 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import Queue +import threading +import traceback +import sys +import gobject +import time +from threading import RLock +from threading import local + +# Can be adjusted to speed up time to aid debugging. +TIME_FACTOR=1 + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Thread local to allow threads to detect what queue they are on +queue_names = local() + +def get_current_queue(): + if hasattr(queue_names, 'queue_name'): + return queue_names.queue_name + return "None" + +def is_on_queue(queue_name): + """ + Get if the current thread came from the queue with the specified name + + Keyword arguments: + queue_name -- queue name + """ + if hasattr(queue_names, 'queue_name') and queue_names.queue_name == queue_name: + return True + return False + +class GTimer: + def __init__(self, scheduler, task_queue, task_name, interval, function, stack, *args): + self.function = function + if function == None: + logger.warning("Attempt to run empty job %s on %s", task_name, task_queue.name) + traceback.print_stack() + return + self.stack = stack + self.scheduler = scheduler + self.task_queue = task_queue + self.task_name = task_name + self.source = gobject.timeout_add(int(float(interval) * 1000.0 * TIME_FACTOR), self.exec_item, function, *args) + self.complete = False + self.scheduler.all_jobs.append(self) + + def exec_item(self, function, *args): + try: + logger.debug("Executing GTimer %s", str(self.task_name)) + ji = self.task_queue.run(self.stack, function, *args) + logger.debug("Executed GTimer %s", str(self.task_name)) + finally: + self.scheduler.all_jobs_lock.acquire() + try: + if self in self.scheduler.all_jobs: + self.scheduler.all_jobs.remove(self) + self.complete = True + finally: + self.scheduler.all_jobs_lock.release() + # Destroy the timeout, don't execute this function again. + return False + + def is_complete(self): + return self.complete + + def cancel(self, *args): + self.scheduler.all_jobs_lock.acquire() + try: + if self in self.scheduler.all_jobs: + self.scheduler.all_jobs.remove(self) + # Check if callback function was executed, if yes this means that the timeout + # was automatically destroyed since the callback function returns False. + # Avoid thousands of warnings from source_remove(). + if not self.is_complete(): + gobject.source_remove(self.source) + logger.debug("Cancelled GTimer %s", str(self.task_name)) + finally: + self.scheduler.all_jobs_lock.release() + +''' +Task scheduler. Tasks may be added to the queue to execute +after a specified interval. The timer is done by the gobject +event loop, which then executes the job on a different thread +''' + +class JobScheduler(): + + def __init__(self): + self.queues = {} + self.all_jobs = [] + self.all_jobs_lock = RLock() + + def print_all_jobs(self): + print "Scheduled" + print "------" + for j in self.all_jobs: + print " %s - %s" % ( j.task_name, str(j.function)) + print + print "Running" + print "-------" + for q in self.queues: + self.queues[q].print_all_jobs() + + def schedule(self, name, interval, function, *args): + return self.queue("default", name, interval, function, *args) + + def stop_all(self): + logger.info("Stopping all queues") + for queue_name in self.queues: + self.queues[queue_name].stop() + + def clear_jobs(self, queue_name): + if queue_name in self.queues: + self.queues[queue_name].clear() + + def stop_queue(self, queue_name): + if queue_name in self.queues: + self.queues[queue_name].stop() + del self.queues[queue_name] + + def execute(self, queue_name, name, function, *args): + logger.debug("Executing on queue %s", queue_name) + if not queue_name in self.queues: + self.queues[queue_name] = JobQueue(name=queue_name) + self.queues[queue_name].run(self._get_stack(), function, *args) + + def _get_stack(self): + try: 1/0 + except: + tb = sys.exc_info()[2] + return traceback.extract_stack()[:-5] + + def queue(self, queue_name, name, interval, function, *args): + if not hasattr(function, "__call__"): + raise Exception("Not a function") + logger.debug("Queueing %s on %s for execution in %f", name, queue_name, interval) + if not queue_name in self.queues: + self.queues[queue_name] = JobQueue(name=queue_name) + + if interval == 0: + # Optimisation, if this is un-timed, avoid putting on main loop + self.queues[queue_name].run(self._get_stack(), function, *args) + else: + timer = GTimer(self, self.queues[queue_name], name, interval, function, self._get_stack(), *args) + logger.debug("Queued %s", name) + return timer + + +class JobQueue(): + + class JobItem(): + def __init__(self, stack, item, args = None): + self.args = args + self.item = item + self.queued = time.time() + self.started = None + self.finished = None + self.stack = stack + + def __init__(self,number_of_workers=1, name="JobQueue"): + logger.debug("Creating job queue %s with %d workers", name, number_of_workers) + self.work_queue = Queue.Queue() + self.queued_jobs = [] + self.name = name + self.stopping = False + self.all_jobs_lock = threading.Lock() + self.number_of_workers = number_of_workers + self.threads = [] + for __ in range(number_of_workers): + t = threading.Thread(target = self.worker) + t.name = name + t.setDaemon(True) + t.start() + self.threads.append(t) + + def print_all_jobs(self): + print "Queue %s" % self.name + for s in self.queued_jobs: + print " %s - %s" % (str(s.item), str(s.queued)) + + def stop(self): + logger.info("Stopping queue %s", self.name) + self.stopping = True + self.clear() + for i in range(0, self.number_of_workers): + self.work_queue.put(self.JobItem("Stopping", self._dummy)) + logger.info("Stopped queue %s", self.name) + + def _dummy(self): + pass + + def clear(self): + jobs = self.work_queue.qsize() + if jobs > 0: + logger.info("Clearing queue %s as it has %d jobs", self.name, jobs) + try : + while True: + item = self.work_queue.get_nowait() + logger.debug("Removed func = %s, args = %s, queued = %s, " \ + "started = %s, finished = %s", + str(item.item), + str(item.args), + str(item.queued), + str(item.started), + str(item.finished)) + if item in self.queued_jobs: + self.queued_jobs.remove(item) + except Queue.Empty as e: + logger.debug("The queue is already empty", exc_info = e) + pass + logger.info("Cleared queue %s", self.name) + + def run(self, stack, item, *args): + if self.stopping: + return + if item == None: + logger.warning("Attempt to run empty job.") + traceback.print_stack() + return + self.all_jobs_lock.acquire() + try : + logger.debug("Queued task on %s", self.name) + ji = self.JobItem(stack, item, args) + self.queued_jobs.append(ji) + self.work_queue.put(ji) + jobs = self.work_queue.qsize() + if jobs > 1: + logger.debug("Queue %s filling, now at %d jobs.", self.name, jobs) + + finally : + self.all_jobs_lock.release() + return ji + + def worker(self): + queue_names.queue_name = self.name + while not self.stopping: + item = self.work_queue.get() + try: + if item != None: + try: + logger.debug("Running task on %s", self.name) + item.started = time.time() + if item.args and len(item.args) > 0: + item.item(*item.args) + else: + item.item() + item.finished = time.time() + logger.debug("Ran task on %s", self.name) + finally: + if item in self.queued_jobs: + self.queued_jobs.remove(item) + except Exception as a: + try: + logger.debug("Error on worker", exc_info = a) + logger.debug("Caused by job") + logger.debug("%s\n", item.stack) + except Exception as e: + logger.debug("Could not log error on worker", exc_info = e) + pass + self.work_queue.task_done() + + if logger: + try: + logger.info("Exited queue %s", self.name) + except Exception as e: + pass + diff --git a/src/libimpulse/Impulse.c b/src/libimpulse/Impulse.c new file mode 100644 index 0000000..9b5e8cd --- /dev/null +++ b/src/libimpulse/Impulse.c @@ -0,0 +1,285 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Impulse 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Impulse. If not, see . + */ + +#include +#include +#include +#include +#include + +#define CHUNK 1024 + +static const long fft_max[] = { 12317168L, 7693595L, 5863615L, 4082974L, 5836037L, 4550263L, 3377914L, 3085778L, 3636534L, 3751823L, 2660548L, 3313252L, 2698853L, 2186441L, 1697466L, 1960070L, 1286950L, 1252382L, 1313726L, 1140443L, 1345589L, 1269153L, 897605L, 900408L, 892528L, 587972L, 662925L, 668177L, 686784L, 656330L, 1580286L, 785491L, 761213L, 730185L, 851753L, 927848L, 891221L, 634291L, 833909L, 646617L, 804409L, 1015627L, 671714L, 813811L, 689614L, 727079L, 853936L, 819333L, 679111L, 730295L, 836287L, 1602396L, 990827L, 773609L, 733606L, 638993L, 604530L, 573002L, 634570L, 1015040L, 679452L, 672091L, 880370L, 1140558L, 1593324L, 686787L, 781368L, 605261L, 1190262L, 525205L, 393080L, 409546L, 436431L, 723744L, 765299L, 393927L, 322105L, 478074L, 458596L, 512763L, 381303L, 671156L, 1177206L, 476813L, 366285L, 436008L, 361763L, 252316L, 204433L, 291331L, 296950L, 329226L, 319209L, 258334L, 388701L, 543025L, 396709L, 296099L, 190213L, 167976L, 138928L, 116720L, 163538L, 331761L, 133932L, 187456L, 530630L, 131474L, 84888L, 82081L, 122379L, 82914L, 75510L, 62669L, 73492L, 68775L, 57121L, 94098L, 68262L, 68307L, 48801L, 46864L, 61480L, 46607L, 45974L, 45819L, 45306L, 45110L, 45175L, 44969L, 44615L, 44440L, 44066L, 43600L, 57117L, 43332L, 59980L, 55319L, 54385L, 81768L, 51165L, 54785L, 73248L, 52494L, 57252L, 61869L, 65900L, 75893L, 65152L, 108009L, 421578L, 152611L, 135307L, 254745L, 132834L, 169101L, 137571L, 141159L, 142151L, 211389L, 267869L, 367730L, 256726L, 185238L, 251197L, 204304L, 284443L, 258223L, 158730L, 228565L, 375950L, 294535L, 288708L, 351054L, 694353L, 477275L, 270576L, 426544L, 362456L, 441219L, 313264L, 300050L, 421051L, 414769L, 244296L, 292822L, 262203L, 418025L, 579471L, 418584L, 419449L, 405345L, 739170L, 488163L, 376361L, 339649L, 313814L, 430849L, 275287L, 382918L, 297214L, 286238L, 367684L, 303578L, 516246L, 654782L, 353370L, 417745L, 392892L, 418934L, 475608L, 284765L, 260639L, 288961L, 301438L, 301305L, 329190L, 252484L, 272364L, 261562L, 208419L, 203045L, 229716L, 191240L, 328251L, 267655L, 322116L, 509542L, 498288L, 341654L, 346341L, 451042L, 452194L, 467716L, 447635L, 644331L, 1231811L, 1181923L, 1043922L, 681166L, 1078456L, 1088757L, 1221378L, 1358397L, 1817252L, 1255182L, 1410357L, 2264454L, 1880361L, 1630934L, 1147988L, 1919954L, 1624734L, 1373554L, 1865118L, 2431931L }; + +static uint32_t source_index = 0; +static int context_ready = 0; +static int16_t buffer[ CHUNK / 2 ], snapshot[ CHUNK / 2 ]; +static size_t buffer_index = 0; + +static pa_context *context = NULL; +static pa_stream *stream = NULL; +static pa_threaded_mainloop* mainloop = NULL; +static pa_io_event* stdio_event = NULL; +static pa_mainloop_api *mainloop_api = NULL; +static char *stream_name = NULL, *client_name = NULL, *device = NULL; + +static pa_sample_spec sample_spec = { + .format = PA_SAMPLE_S16LE, + .rate = 44100, + .channels = 2 +}; + +static pa_stream_flags_t flags = 0; + +static pa_channel_map channel_map; +static int channel_map_set = 0; + +/* A shortcut for terminating the application */ +static void quit( int ret ) { + assert( mainloop_api ); + mainloop_api->quit( mainloop_api, ret ); +} + +static void unmute_source_success_cb( pa_context *c, int success, void *userdata ) { + printf("unmute: %d\n", success); +} + +static void get_source_info_callback( pa_context *c, const pa_source_info *i, int is_last, void *userdata ) { + + if ( !i ) + return; + + printf("source index: %u\n", i->index ); + + // snprintf(t, sizeof(t), "%u", i->monitor_of_sink); + +// if ( i->monitor_of_sink != PA_INVALID_INDEX ) { + puts( i->name ); + // if ( device && strcmp( device, i->name ) == 0 ) return; + + device = pa_xstrdup( i->name ); + + if ( ( pa_stream_connect_record( stream, device, NULL, flags ) ) < 0 ) { + fprintf(stderr, "pa_stream_connect_record() failed: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } +// } +} + +/* This is called whenever new data is available */ +static void stream_read_callback(pa_stream *s, size_t length, void *userdata) { + const void *data; + assert(s); + assert(length > 0); +// printf("stream index: %d\n", pa_stream_get_index( s ) ); + if (stdio_event) + mainloop_api->io_enable(stdio_event, PA_IO_EVENT_OUTPUT); + + if (pa_stream_peek(s, &data, &length) < 0) { + fprintf(stderr, "pa_stream_peek() failed: %s\n", pa_strerror(pa_context_errno(context))); + quit(1); + return; + } + + assert(data); + assert(length > 0); + + int excess = buffer_index * 2 + length - ( CHUNK ); + + if ( excess < 0 ) excess = 0; + + memcpy( buffer + buffer_index, data, length - excess ); + buffer_index += ( length - excess ) / 2; + + if ( excess ) { + memcpy( snapshot, buffer, buffer_index * 2 ); + buffer_index = 0; + } + + pa_stream_drop(s); +} + +static void stream_state_callback( pa_stream *s, void* userdata ); + +static void init_source_stream_for_recording() { + + if (!(stream = pa_stream_new( context, stream_name, &sample_spec, channel_map_set ? &channel_map : NULL))) { + fprintf(stderr, "pa_stream_new() failed: %s\n", pa_strerror(pa_context_errno(context))); + quit(1); + } + + pa_stream_set_read_callback(stream, stream_read_callback, NULL); + pa_stream_set_state_callback( stream, stream_state_callback, NULL ); + pa_operation_unref( pa_context_set_source_mute_by_index( context, source_index, 0, unmute_source_success_cb, NULL ) ); + pa_operation_unref( pa_context_get_source_info_by_index( context, source_index, get_source_info_callback, NULL ) ); +} + +static void stream_state_callback( pa_stream *s, void* userdata ) { + if ( pa_stream_get_state( s ) == PA_STREAM_TERMINATED ) { + pa_stream_unref( stream ); + init_source_stream_for_recording(); + } +} + +static void context_state_callback( pa_context *c, void *userdata ) { + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + case PA_CONTEXT_READY: + assert(c); + assert(!stream); + + /*if (!(stream = pa_stream_new(c, stream_name, &sample_spec, channel_map_set ? &channel_map : NULL))) { + fprintf(stderr, "pa_stream_new() failed: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } + + pa_stream_set_read_callback(stream, stream_read_callback, NULL);*/ + init_source_stream_for_recording(); + + break; + case PA_CONTEXT_TERMINATED: + quit(0); + break; + + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "Connection failure: %s\n", pa_strerror(pa_context_errno(c))); + quit(1); + } +} + +void im_stop (void) { + + pa_threaded_mainloop_stop( mainloop ); + + //printf( "exit\n" ); +} + +void im_setSourceIndex( uint32_t index ) { + source_index = index; + if ( !stream ) return; + + if ( pa_stream_get_state( stream ) != PA_STREAM_UNCONNECTED ) + pa_stream_disconnect( stream ); + else + init_source_stream_for_recording(); +} + +double *im_getSnapshot( int fft ) { + + static double magnitude[ CHUNK / 4 ]; + + if ( ! fft ) { + int i; + for ( i = 0; i < CHUNK / 2; i += sample_spec.channels ) { + magnitude[ i / sample_spec.channels ] = 0; + int j; + for ( j = 0; j < sample_spec.channels; j++ ) + magnitude[ i / sample_spec.channels ] += fabs( ( (double) snapshot[ i + j ] / ( pow( 2, 16 ) / 2 ) ) / sample_spec.channels ); + } + } else { + + double *in; + fftw_complex *out; + fftw_plan p; + + in = (double*) malloc( sizeof( double ) * ( CHUNK / 2 ) ); + out = (fftw_complex*) fftw_malloc( sizeof( fftw_complex ) * ( CHUNK / 2 ) ); + + if ( snapshot != NULL ) { + int i; + for ( i = 0; i < CHUNK / 2; i++ ) { + in[ i ] = (double) snapshot[ i ]; + } + } + + p = fftw_plan_dft_r2c_1d( CHUNK / 2, in, out, 0 ); + + fftw_execute( p ); + + fftw_destroy_plan( p ); + + if ( out != NULL ) { + int i; + for ( i = 0; i < CHUNK / 2 / sample_spec.channels; i++ ) { + magnitude[ i ] = (double) sqrt( pow( out[ i ][ 0 ], 2 ) + pow( out[ i ][ 1 ], 2 ) ) / (double) fft_max[ i ]; + if ( magnitude[ i ] > 1.0 ) magnitude[ i ] = 1.0; + } + } + + free( in ); + fftw_free(out); + } + + return magnitude; // PyString_FromStringAndSize( (char *) snapshot, CHUNK ); +} + + +void im_start ( void ) { + + // Pulseaudio + int r; + char *server = NULL; + + client_name = pa_xstrdup( "impulse" ); + stream_name = pa_xstrdup( "impulse" ); + + // Set up a new main loop + + if ( ! ( mainloop = pa_threaded_mainloop_new( ) ) ) { + fprintf( stderr, "pa_mainloop_new() failed.\n" ); + return; + } + + mainloop_api = pa_threaded_mainloop_get_api( mainloop ); + + r = pa_signal_init( mainloop_api ); + assert( r == 0 ); + + /*if (!(stdio_event = mainloop_api->io_new(mainloop_api, + STDOUT_FILENO, + PA_IO_EVENT_OUTPUT, + stdout_callback, NULL))) { + fprintf(stderr, "io_new() failed.\n"); + goto quit; + }*/ + + // create a new connection context + if ( ! ( context = pa_context_new( mainloop_api, client_name ) ) ) { + fprintf( stderr, "pa_context_new() failed.\n" ); + return; + } + + pa_context_set_state_callback( context, context_state_callback, NULL ); + + /* Connect the context */ + pa_context_connect( context, server, 0, NULL ); + + // pulseaudio thread + pa_threaded_mainloop_start( mainloop ); + + return; +} + diff --git a/src/libimpulse/Impulse.h b/src/libimpulse/Impulse.h new file mode 100644 index 0000000..ab5979b --- /dev/null +++ b/src/libimpulse/Impulse.h @@ -0,0 +1,32 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Impulse 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Impulse. If not, see . + */ + + +#define IM_NOFFT 0 +#define IM_FFT 1 + +double *im_getSnapshot( int fft ); + +void im_setSourceIndex( uint32_t index ); + +void im_start( void ); + +void im_stop( void ); diff --git a/src/libimpulse/Makefile.am b/src/libimpulse/Makefile.am new file mode 100644 index 0000000..caec52d --- /dev/null +++ b/src/libimpulse/Makefile.am @@ -0,0 +1,14 @@ +lib_LTLIBRARIES = libimpulse.la +libimpulse_la_SOURCES = Impulse.c +libimpulse_la_CFLAGS = ${PULSE_CFLAGS} ${FFTW_CFLAGS} +libimpulse_la_LDFLAGS = -version-info 1:0:1 -no-undefined -pthread -shared -Wl ${PULSE_LIBS} ${FFTW_LIBS} -fPIC + +pyexec_LTLIBRARIES = impulse.la +impulse_la_SOURCES = impulsemodule.c +impulse_la_CFLAGS = $(PYTHON_CPPFLAGS) +impulse_la_LDFLAGS = -avoid-version -module $(PYTHON_LDFLAGS) +impulse_la_LIBADD = libimpulse.la + +pkginclude_HEADERS = Impulse.h + +METASOURCES = AUTO diff --git a/src/libimpulse/impulsemodule.c b/src/libimpulse/impulsemodule.c new file mode 100644 index 0000000..6eb032a --- /dev/null +++ b/src/libimpulse/impulsemodule.c @@ -0,0 +1,88 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Impulse 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Impulse. If not, see . + */ + +#include +#include "Impulse.h" +#include +#include +#include +#include + +static PyObject * impulse_getSnapshot( PyObject *self, PyObject *args, PyObject *kwargs ) { + PyObject *magnitude; + + int fft = 0; + + static char *kwlist[] = { "fft", NULL }; + + if ( !PyArg_ParseTupleAndKeywords( args, kwargs, "b", kwlist, &fft ) ) + return NULL; + + magnitude = PyTuple_New( 256 ); + + double *m = im_getSnapshot( fft ); + + int i; + for ( i = 0; i < 256; i++ ) + PyTuple_SetItem( magnitude, i, PyFloat_FromDouble( m[ i ] ) ); + + return magnitude; // PyString_FromStringAndSize( (char *) snapshot, CHUNK ); +} + +static PyObject* impulse_setSourceIndex( PyObject* self, PyObject* args, PyObject* kwargs ) { + uint32_t index; + + static char *kwlist[] = { "index", NULL }; + + if ( !PyArg_ParseTupleAndKeywords( args, kwargs, "i", kwlist, &index ) ) + return NULL; + + im_setSourceIndex( index ); + + Py_RETURN_NONE; +} + +static PyObject *ImpulseError; + +static PyMethodDef ImpulseMethods[ ] = { + { "getSnapshot", (PyCFunction)impulse_getSnapshot, METH_VARARGS | METH_KEYWORDS, "Returns the current audio snapshot from Pulseaudio." }, + { "setSourceIndex", (PyCFunction)impulse_setSourceIndex, METH_VARARGS | METH_KEYWORDS, "Changes the Pulseaudio source by it's index." }, + { NULL } /* Sentinel */ +}; + +PyMODINIT_FUNC initimpulse ( void ) { + PyObject *m; + + m = Py_InitModule( "impulse", ImpulseMethods ); + if (m == NULL) + return; + + ImpulseError = PyErr_NewException( "impulse.error", NULL, NULL ); + Py_INCREF( ImpulseError ); + PyModule_AddObject( m, "error", ImpulseError ); + + Py_AtExit( &im_stop ); + + im_start( ); + + return; +} + diff --git a/src/libimpulse/test-libimpulse.c b/src/libimpulse/test-libimpulse.c new file mode 100644 index 0000000..1debec6 --- /dev/null +++ b/src/libimpulse/test-libimpulse.c @@ -0,0 +1,42 @@ +/* + * + *+ Copyright (c) 2009 Ian Halpern + *@ http://impulse.ian-halpern.com + * + * This file is part of Impulse. + * + * Impulse is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Impulse 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Impulse. If not, see . + */ + + +#include "Impulse.h" +#include + +int main( ) { + + im_start( ); + + while ( 1 ) { + usleep( 1000000 / 30 ); + double *array = im_getSnapshot( IM_FFT ); + int i; + for ( i = 0; i < 256; i+=32 ) + printf( " %.2f", array[ i ] ); + printf( "\n" ); + fflush( stdout ); + } + im_stop( ); + + return 0; +} diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am new file mode 100644 index 0000000..ef48dbe --- /dev/null +++ b/src/plugins/Makefile.am @@ -0,0 +1,163 @@ +if ENABLE_PLUGIN_VOLUME + MAYBE_VOLUME = volume +endif +if ENABLE_PLUGIN_RSS + MAYBE_RSS = rss +endif +if ENABLE_PLUGIN_SYSMON + MAYBE_SYSMON = sysmon +endif +if ENABLE_PLUGIN_PROCESSES + MAYBE_PROCESSES = processes +endif +if ENABLE_PLUGIN_CAL + MAYBE_CAL = cal +endif +if ENABLE_PLUGIN_CAL_EVOLUTION + MAYBE_CAL_EVOLUTION = cal-evolution +endif +if ENABLE_PLUGIN_CAL_GOOGLE + MAYBE_CAL_GOOGLE = cal-google +endif +if ENABLE_PLUGIN_LCDBIFF + MAYBE_LCDBIFF = lcdbiff +endif +if ENABLE_PLUGIN_BACKGROUND + MAYBE_BACKGROUND = background +endif +if ENABLE_PLUGIN_CAIRO_CLOCK + MAYBE_CAIRO_CLOCK = cairo-clock +endif +if ENABLE_PLUGIN_CLOCK + MAYBE_CLOCK = clock +endif +if ENABLE_PLUGIN_FX + MAYBE_FX = fx +endif +if ENABLE_PLUGIN_MACRO_RECORDER + MAYBE_MACRO_RECORDER = macro-recorder +endif +if ENABLE_PLUGIN_MACROS + MAYBE_MACROS = macros +endif +if ENABLE_PLUGIN_PROFILES + MAYBE_PROFILES = profiles +endif +if ENABLE_PLUGIN_MOUNTS + MAYBE_MOUNTS = mounts +endif +if ENABLE_PLUGIN_NOTIFY_LCD + MAYBE_NOTIFY_LCD = notify-lcd +endif +if ENABLE_PLUGIN_IM + MAYBE_IM = im +endif +if ENABLE_PLUGIN_WEATHER + MAYBE_WEATHER = weather +endif +if ENABLE_PLUGIN_WEATHER_NOAA + MAYBE_WEATHER_NOAA = weather-noaa +endif +if ENABLE_PLUGIN_WEATHER_YAHOO + MAYBE_WEATHER_YAHOO = weather-yahoo +endif +if ENABLE_PLUGIN_MPRIS + MAYBE_MPRIS = mpris +endif +if ENABLE_PLUGIN_MENU + MAYBE_MENU = menu +endif +if ENABLE_PLUGIN_PANEL + MAYBE_PANEL = panel +endif +if ENABLE_PLUGIN_MEDIAPLAYER + MAYBE_MEDIAPLAYER = mediaplayer +endif +if ENABLE_PLUGIN_G15DAEMON_SERVER + MAYBE_G15DAEMON_SERVER = g15daemon-server +endif +if ENABLE_PLUGIN_STOPWATCH + MAYBE_STOPWATCH = stopwatch +endif +if ENABLE_PLUGIN_SCREENSAVER + MAYBE_SCREENSAVER = screensaver +endif +if ENABLE_PLUGIN_INDICATOR_MESSAGES + MAYBE_INDICATOR_MESSAGES = indicator-messages +endif +if ENABLE_PLUGIN_SENSE + MAYBE_SENSE = sense +endif +if ENABLE_PLUGIN_LCDSHOT + MAYBE_LCDSHOT = lcdshot +endif +if ENABLE_PLUGIN_TWEAK + MAYBE_TWEAK = tweak +endif +if ENABLE_PLUGIN_TAILS + MAYBE_TAILS = tails +endif +if ENABLE_PLUGIN_DISPLAY + MAYBE_DISPLAY = display +endif +if ENABLE_PLUGIN_VOIP + MAYBE_VOIP = voip +endif +if ENABLE_PLUGIN_VOIP_TEAMSPEAK3 + MAYBE_VOIP_TEAMSPEAK3 = voip-teamspeak3 +endif +if ENABLE_PLUGIN_GOOGLE_ANALYTICS + MAYBE_GOOGLE_ANALYTICS = google-analytics +endif +if ENABLE_PLUGIN_DEBUG + MAYBE_DEBUG = debug +endif +if ENABLE_PLUGIN_TRAFFIC_STATS + MAYBE_TRAFFIC_STATS = trafficstats +endif +if ENABLE_PLUGIN_POMMODORO + MAYBE_POMMODORO = pommodoro +endif +if ENABLE_PLUGIN_GAME_NEXUIZ + MAYBE_GAME_NEXUIZ = game-nexuiz +endif +if ENABLE_PLUGIN_BACKLIGHT + MAYBE_BACKLIGHT = backlight +endif +if ENABLE_PLUGIN_NOTIFY_LCD2 + MAYBE_NOTIFY_LCD2 = notify-lcd2 +endif +if ENABLE_PLUGIN_PPASTATS + MAYBE_PPASTATS = ppastats +endif +if ENABLE_PLUGIN_NM + MAYBE_NM = nm +endif +if ENABLE_PLUGIN_LENS + MAYBE_LENS = lens +endif +if ENABLE_PLUGIN_WEBKIT_BROWSER + MAYBE_WEBKIT_BROWSER = webkitbrowser +endif +if ENABLE_PLUGIN_THINGS + MAYBE_THINGS = things +endif +if ENABLE_PLUGIN_IMPULSE15 + MAYBE_IMPULSE15 = impulse15 +endif + + +SUBDIRS = runapp $(MAYBE_BACKGROUND) $(MAYBE_CAIRO_CLOCK) $(MAYBE_CLOCK) $(MAYBE_FX) $(MAYBE_MACRO_RECORDER) $(MAYBE_MACROS) $(MAYBE_MOUNTS) \ + $(MAYBE_NOTIFY_LCD) $(MAYBE_IM) $(MAYBE_WEATHER) $(MAYBE_MPRIS) $(MAYBE_MENU) $(MAYBE_PANEL) $(MAYBE_MEDIAPLAYER) $(MAYBE_G15DAEMON_SERVER) \ + $(MAYBE_STOPWATCH) $(MAYBE_SCREENSAVER) $(MAYBE_INDICATOR_MESSAGES) $(MAYBE_PROFILES) \ + $(MAYBE_SYSMON) $(MAYBE_VOLUME) $(MAYBE_RSS) $(MAYBE_PROCESSES) $(MAYBE_CAL) $(MAYBE_CAL_EVOLUTION) $(MAYBE_CAL_GOOGLE) $(MAYBE_LCDBIFF) $(MAYBE_SENSE) \ + $(MAYBE_LCDSHOT) $(MAYBE_TWEAK) $(MAYBE_TAILS) $(MAYBE_DISPLAY) $(MAYBE_VOIP) $(MAYBE_VOIP_TEAMSPEAK3) $(MAYBE_WEATHER_NOAA) \ + $(MAYBE_WEATHER_YAHOO) $(MAYBE_GOOGLE_ANALYTICS) $(MAYBE_DEBUG) $(MAYBE_TRAFFIC_STATS) $(MAYBE_POMMODORO) $(MAYBE_GAME_NEXUIZ) \ + $(MAYBE_BACKLIGHT) $(MAYBE_NOTIFY_LCD2) $(MAYBE_PPASTATS) $(MAYBE_NM) $(MAYBE_LENS) \ + $(MAYBE_WEBKIT_BROWSER) $(MAYBE_THINGS) $(MAYBE_IMPULSE15) + +DIST_SUBDIRS = runapp background cairo-clock clock fx macro-recorder macros mounts notify-lcd im weather mpris menu panel mediaplayer g15daemon-server stopwatch \ + profiles sysmon volume rss processes screensaver cal cal-evolution cal-google lcdbiff indicator-messages sense lcdshot tweak tails display \ + voip voip-teamspeak3 google-analytics weather-noaa weather-yahoo debug trafficstats pommodoro \ + game-nexuiz backlight notify-lcd2 ppastats nm lens webkitbrowser things impulse15 + diff --git a/src/plugins/background/Makefile.am b/src/plugins/background/Makefile.am new file mode 100644 index 0000000..dec1fdf --- /dev/null +++ b/src/plugins/background/Makefile.am @@ -0,0 +1,8 @@ +plugindir = $(datadir)/gnome15/plugins/background +plugin_DATA = background.ui \ + background.py \ + background-160x43.png \ + background-320x240.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/background/background-160x43.png b/src/plugins/background/background-160x43.png new file mode 100644 index 0000000000000000000000000000000000000000..e965e7984f2941ffd1bf55842c644d972e392a3a GIT binary patch literal 4197 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 mS1a(Pb8P*$9N^5%%>N(dCkch^?d**J0000 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("background", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="background" +name=_("Wallpaper") +description=_("Use an image for the LCD background") +author=_("Brett Smith ") +copyright="Copyright (C)2010 Brett Smith" +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Background(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BackgroundPreferences(parent, driver, gconf_client, gconf_key) + +class G15BackgroundPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "background.ui")) + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + # Widgets + dialog = widget_tree.get_object("BackgroundDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key + "/type", [ "UseDesktop", "UseFile" ], [ "desktop", "file" ], "desktop", widget_tree, True) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/style", "StyleCombo", "zoom", widget_tree) + widget_tree.get_object("UseDesktop").connect("toggled", self.set_available, widget_tree) + widget_tree.get_object("UseFile").connect("toggled", self.set_available, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_profile_override", "AllowProfileOverride", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/brightness", "BrightnessAdjustment", 50, widget_tree) + + # Currently, only GNOME is supported for getting the desktop background + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + widget_tree.get_object("UseFile").set_active(True) + + # The file chooser + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + chooser.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + chooser.add_filter(filter) + + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self.file_set) + widget_tree.connect_signals(self) + bg_img = gconf_client.get_string(gconf_key + "/path") + if bg_img == None: + bg_img = "" + chooser_button.set_filename(bg_img) + self.set_available(None, widget_tree) + dialog.run() + dialog.hide() + + def set_available(self, widget, widget_tree): + widget_tree.get_object("FileChooserLabel").set_sensitive(widget_tree.get_object("UseFile").get_active()) + widget_tree.get_object("FileChooserButton").set_sensitive(widget_tree.get_object("UseFile").get_active()) + + def file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/path", widget.get_filename()) + + +class G15BackgroundPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G15Background(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15BackgroundPainter(self.screen) + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + self.gnome_dconf_settings = None + self.gnome_dconf_handle = None + + # Monitor desktop specific configuration for wallpaper changes + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + self.notify_handlers.append(self.gconf_client.notify_add("/desktop/gnome/background/picture_filename", self.config_changed)) + if os.path.exists("/usr/share/glib-2.0/schemas/org.gnome.desktop.background.gschema.xml"): + try: + from gi.repository import Gio + self.gnome_dconf_settings = Gio.Settings.new("org.gnome.desktop.background") + except Exception as e: + logger.debug("Could not get background with GI, falling back", exc_info = e) + # Work around on Ubuntu 12.10+ until Gnome15 is converted to GObject bindings + import gnome15.g15dconf as g15dconf + self.gnome_dconf_settings = g15dconf.GSettings("org.gnome.desktop.background") + + if self.gnome_dconf_settings is not None: + self.gnome_dconf_handle = self.gnome_dconf_settings.connect("changed::picture_uri", self._do_config_changed) + + # Listen for profile changes + g15profile.profile_listeners.append(self._profiles_changed) + + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + for h in self.notify_handlers: + self.gconf_client.notify_remove(h); + self.screen.redraw() + if self.gnome_dconf_handle is not None: + self.gnome_dconf_settings.disconnect(self.gnome_dconf_handle) + self.gnome_dconf_settings.__del__() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + # Get the configuration + screen_size = self.screen.size + self.bg_img = None + bg_type = self.gconf_client.get_string(self.gconf_key + "/type") + if bg_type == None: + bg_type = "desktop" + bg_style = self.gconf_client.get_string(self.gconf_key + "/style") + if bg_style == None: + bg_style = "zoom" + allow_profile_override = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/allow_profile_override", True) + + # See if the current profile has a background + if allow_profile_override: + active_profile = g15profile.get_active_profile(self.screen.device) + if active_profile is not None and active_profile.background is not None and active_profile.background != "": + self.bg_img = active_profile.background + + if self.bg_img == None and bg_type == "desktop": + # Get the current background the desktop is using if possible + desktop_env = g15desktop.get_desktop() + if desktop_env in [ "gnome", "gnome-shell" ]: + if self.gnome_dconf_settings is not None: + self.bg_img = self.gnome_dconf_settings.get_string("picture-uri") + else: + self.bg_img = self.gconf_client.get_string("/desktop/gnome/background/picture_filename") + else: + logger.warning("User request wallpaper from the desktop, but the desktop environment is unknown. Please report this bug to the Gnome15 project") + + if self.bg_img == None: + # Use the file + self.bg_img = self.gconf_client.get_string(self.gconf_key + "/path") + + # Fallback to the default provided image + if self.bg_img == None: + self.bg_img = os.path.join(os.path.dirname(__file__), "background-%dx%d.png" % ( screen_size[0], screen_size[1] ) ) + + # Load the image + if self.bg_img != self.this_image or bg_style != self.current_style: + self.this_image = self.bg_img + self.current_style = bg_style + if g15cairo.is_url(self.bg_img) or os.path.exists(self.bg_img): + + """ + TODO handle background themes and transitions from XML files properly + + For now, just get the first static image + """ + if self.bg_img.endswith(".xml"): + filet = etree.parse(self.bg_img).getroot().findtext('.//file') + if filet: + self.bg_img = filet + + img_surface = g15cairo.load_surface_from_file(self.bg_img) + if img_surface is not None: + sx = float(screen_size[0]) / img_surface.get_width() + sy = float(screen_size[1]) / img_surface.get_height() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.save() + if bg_style == "zoom": + scale = max(sx, sy) + context.scale(scale, scale) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "stretch": + context.scale(sx, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "scale": + x = ( screen_size[0] - img_surface.get_width() * sy ) / 2 + context.translate(x, 0) + context.scale(sy, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "center": + x = ( screen_size[0] - img_surface.get_width() ) / 2 + y = ( screen_size[1] - img_surface.get_height() ) / 2 + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "tile": + context.set_source_surface(img_surface) + context.paint() + y = 0 + x = img_surface.get_width() + while y < screen_size[1] + img_surface.get_height(): + if x >= screen_size[1] + img_surface.get_width(): + x = 0 + y += img_surface.get_height() + context.restore() + context.save() + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + x += img_surface.get_width() + + context.restore() + self.painter.background_image = surface + else: + self.painter.background_image = None + else: + self.painter.background_image = None + + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + + self.screen.redraw() diff --git a/src/plugins/background/background.ui b/src/plugins/background/background.ui new file mode 100644 index 0000000..2a23037 --- /dev/null +++ b/src/plugins/background/background.ui @@ -0,0 +1,268 @@ + + + + + + -100 + 100 + 1 + 10 + + + + + + + + + + + + zoom + Zoom + + + tile + Tile + + + center + Center + + + scale + Scale + + + stretch + Stretch + + + + + 460 + False + 5 + Wallpaper Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + _Same as desktop background + True + True + False + True + True + True + + + True + True + 0 + + + + + Use _image file + True + True + False + True + True + True + UseDesktop + + + True + True + 1 + + + + + True + False + 4 + + + 160 + True + False + 0 + 32 + Background Image + + + False + False + 0 + + + + + True + False + Select A Background + + + True + True + 1 + + + + + False + False + 2 + + + + + True + False + 8 + + + True + False + Style + + + False + True + 0 + + + + + True + False + StyleModel + + + + 1 + + + + + True + True + 1 + + + + + True + True + 8 + 3 + + + + + Allow macro profiles to override background + True + True + False + True + + + False + True + 4 + + + + + True + False + + + 160 + True + False + Brightness + + + False + False + 0 + + + + + True + True + BrightnessAdjustment + 1 + 0 + + + True + True + 1 + + + + + True + True + 5 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/background/background2.py b/src/plugins/background/background2.py new file mode 100644 index 0000000..88cded6 --- /dev/null +++ b/src/plugins/background/background2.py @@ -0,0 +1,284 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("background", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="background" +name=_("Wallpaper") +description=_("Use an image for the LCD background") +author=_("Brett Smith ") +copyright="Copyright (C)2010 Brett Smith" +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Background(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BackgroundPreferences(parent, driver, gconf_client, gconf_key) + +class G15BackgroundPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "background.ui")) + + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + # Widgets + dialog = widget_tree.get_object("BackgroundDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_radio_from_gconf(gconf_client, gconf_key + "/type", [ "UseDesktop", "UseFile" ], [ "desktop", "file" ], "desktop", widget_tree, True) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/style", "StyleCombo", "zoom", widget_tree) + widget_tree.get_object("UseDesktop").connect("toggled", self.set_available, widget_tree) + widget_tree.get_object("UseFile").connect("toggled", self.set_available, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_profile_override", "AllowProfileOverride", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/brightness", "BrightnessAdjustment", 50, widget_tree) + + # Currently, only GNOME is supported for getting the desktop background + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + widget_tree.get_object("UseFile").set_active(True) + + # The file chooser + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name("Images") + filter.add_mime_type("image/png") + filter.add_mime_type("image/jpeg") + filter.add_mime_type("image/gif") + filter.add_pattern("*.png") + filter.add_pattern("*.jpg") + filter.add_pattern("*.jpeg") + filter.add_pattern("*.gif") + chooser.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + chooser.add_filter(filter) + + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self.file_set) + widget_tree.connect_signals(self) + bg_img = gconf_client.get_string(gconf_key + "/path") + if bg_img == None: + bg_img = "" + chooser_button.set_filename(bg_img) + self.set_available(None, widget_tree) + dialog.run() + dialog.hide() + + def set_available(self, widget, widget_tree): + widget_tree.get_object("FileChooserLabel").set_sensitive(widget_tree.get_object("UseFile").get_active()) + widget_tree.get_object("FileChooserButton").set_sensitive(widget_tree.get_object("UseFile").get_active()) + + def file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/path", widget.get_filename()) + + +class G15BackgroundPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G15Background(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15BackgroundPainter(self.screen) + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + self.gnome_dconf_settings = None + self.gnome_dconf_handle = None + + # Monitor desktop specific configuration for wallpaper changes + if g15desktop.get_desktop() in [ "gnome", "gnome-shell" ]: + self.notify_handlers.append(self.gconf_client.notify_add("/desktop/gnome/background/picture_filename", self.config_changed)) + if os.path.exists("/usr/share/glib-2.0/schemas/org.gnome.desktop.background.gschema.xml"): + try: + from gi.repository import Gio + self.gnome_dconf_settings = Gio.Settings.new("org.gnome.desktop.background") + except Exception as e: + logger.debug("Could not get background with GI, falling back", exc_info = e) + # Work around on Ubuntu 12.10+ until Gnome15 is converted to GObject bindings + import gnome15.g15dconf as g15dconf + self.gnome_dconf_settings = g15dconf.GSettings("org.gnome.desktop.background") + + if self.gnome_dconf_settings is not None: + self.gnome_dconf_handle = self.gnome_dconf_settings.connect("changed::picture_uri", self._do_config_changed) + + # Listen for profile changes + g15profile.profile_listeners.append(self._profiles_changed) + + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + for h in self.notify_handlers: + self.gconf_client.notify_remove(h); + self.screen.redraw() + if self.gnome_dconf_handle is not None: + self.gnome_dconf_settings.disconnect(self.gnome_dconf_handle) + self.gnome_dconf_settings.__del__() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + # Get the configuration + screen_size = self.screen.size + self.bg_img = None + bg_type = self.gconf_client.get_string(self.gconf_key + "/type") + if bg_type == None: + bg_type = "desktop" + bg_style = self.gconf_client.get_string(self.gconf_key + "/style") + if bg_style == None: + bg_style = "zoom" + allow_profile_override = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/allow_profile_override", True) + + # See if the current profile has a background + if allow_profile_override: + active_profile = g15profile.get_active_profile(self.screen.device) + if active_profile is not None and active_profile.background is not None and active_profile.background != "": + self.bg_img = active_profile.background + + if self.bg_img == None and bg_type == "desktop": + # Get the current background the desktop is using if possible + desktop_env = g15desktop.get_desktop() + if desktop_env in [ "gnome", "gnome-shell" ]: + if self.gnome_dconf_settings is not None: + self.bg_img = self.gnome_dconf_settings.get_string("picture-uri") + else: + self.bg_img = self.gconf_client.get_string("/desktop/gnome/background/picture_filename") + else: + logger.warning("User request wallpaper from the desktop, but the desktop environment is unknown. Please report this bug to the Gnome15 project") + + if self.bg_img == None: + # Use the file + self.bg_img = self.gconf_client.get_string(self.gconf_key + "/path") + + # Fallback to the default provided image + if self.bg_img == None: + self.bg_img = os.path.join(os.path.dirname(__file__), "background-%dx%d.png" % ( screen_size[0], screen_size[1] ) ) + + # Load the image + if self.bg_img != self.this_image or bg_style != self.current_style: + self.this_image = self.bg_img + self.current_style = bg_style + if g15cairo.is_url(self.bg_img) or os.path.exists(self.bg_img): + + """ + TODO handle background themes and transitions from XML files properly + + For now, just get the first static image + """ + + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.scale(screen_size[0], screen_size[1]) + context.set_line_width(0.04) + context.move_to(0.1, 0.5) + context.curve_to(0.4, 0.9, 0.6, 0.1, 0.9, 0.5) + context.stroke() + context.set_source_rgba(1, 0.2, 0.2, 0.6) + context.set_line_width(0.02) + context.move_to(0.1, 0.5) + context.line_to(0.4, 0.9) + context.move_to(0.6, 0.1) + context.line_to(0.9, 0.5) + context.stroke() + context.save() + context.paint() + context.restore() + self.painter.background_image = surface + + else: + self.painter.background_image = None + + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + + self.screen.redraw() diff --git a/src/plugins/background/i18n/background.en_GB.po b/src/plugins/background/i18n/background.en_GB.po new file mode 100644 index 0000000..5db1e79 --- /dev/null +++ b/src/plugins/background/i18n/background.en_GB.po @@ -0,0 +1,66 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "Allow macro profiles to override background" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "Background Image" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "Center" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "Scale" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "Select A Background" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "Stretch" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "Style" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "Tile" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "Use _image file" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "Wallpaper Preferences" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "Zoom" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "_Same as desktop background" diff --git a/src/plugins/background/i18n/background.glade.h b/src/plugins/background/i18n/background.glade.h new file mode 100644 index 0000000..af34a46 --- /dev/null +++ b/src/plugins/background/i18n/background.glade.h @@ -0,0 +1,12 @@ +char *s = N_("Allow macro profiles to override background"); +char *s = N_("Background Image"); +char *s = N_("Center"); +char *s = N_("Scale"); +char *s = N_("Select A Background"); +char *s = N_("Stretch"); +char *s = N_("Style"); +char *s = N_("Tile"); +char *s = N_("Use _image file"); +char *s = N_("Wallpaper Preferences"); +char *s = N_("Zoom"); +char *s = N_("_Same as desktop background"); diff --git a/src/plugins/background/i18n/background.pot b/src/plugins/background/i18n/background.pot new file mode 100644 index 0000000..b945edf --- /dev/null +++ b/src/plugins/background/i18n/background.pot @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "" diff --git a/src/plugins/backlight/Makefile.am b/src/plugins/backlight/Makefile.am new file mode 100644 index 0000000..e1d220b --- /dev/null +++ b/src/plugins/backlight/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/backlight +plugin_DATA = backlight.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/backlight/backlight.py b/src/plugins/backlight/backlight.py new file mode 100644 index 0000000..d10bbf6 --- /dev/null +++ b/src/plugins/backlight/backlight.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15icontools as g15icontools +import gnome15.g15gtk as g15gtk +import gtk +import gobject + +# Plugin details - All of these must be provided +id="backlight" +name="Backlight" +description="Set the keyboard backlight color using the LCD screen and menu keys. " + \ + "This plugin demonstrates the use of ordinary GTK widgets on the LCD." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +supported_models = [ g15driver.MODEL_G19 ] + +def create(gconf_key, gconf_client, screen): + return G15Backlight(gconf_client, gconf_key, screen) + +class G15Backlight(): + + def __init__(self, gconf_client, gconf_key, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.page = g15theme.G15Page(id, self.screen, theme_properties_callback = self._get_theme_properties, priority = g15screen.PRI_LOW, title = name, theme = g15theme.G15Theme(self), + originating_plugin = plugin) + self.window = g15gtk.G15OffscreenWindow("offscreenWindow") + self.page.add_child(self.window) + gobject.idle_add(self._create_offscreen_window) + + def deactivate(self): + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def destroy(self): + pass + + ''' + Private + ''' + + def _get_theme_properties(self): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = backlight_control.value + properties = { + "title" : "Set Backlight", + "icon" : g15icontools.get_icon_path("system-config-display"), + "r" : color[0], + "g" : color[1], + "b" : color[2] + } + return properties + + def _create_offscreen_window(self): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = backlight_control.value + + vbox = gtk.VBox() + adjustment = gtk.Adjustment(color[0], 0, 255, 1, 10, 10) + red = gtk.HScale(adjustment) + red.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 0) + + vbox.add(red) + red.grab_focus() + adjustment = gtk.Adjustment(color[1], 0, 255, 1, 10, 10) + green = gtk.HScale(adjustment) + green.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 1) + green.set_range(0, 255) + green.set_increments(1, 10) + vbox.add(green) + adjustment = gtk.Adjustment(color[2], 0, 255, 1, 10, 10) + blue = gtk.HScale(adjustment) + blue.set_draw_value(False) + adjustment.connect("value-changed", self._value_changed, 2) + blue.set_range(0, 255) + blue.set_increments(1, 10) + vbox.add(blue) + + self.window.set_content(vbox) + self.screen.add_page(self.page) + self.screen.redraw(self.page) + + def _value_changed(self, widget, octet): + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + color = list(backlight_control.value) + color[octet] = int(widget.get_value()) + self.gconf_client.set_string("/apps/gnome15/" + backlight_control.id, "%d,%d,%d" % ( color[0],color[1],color[2])) + \ No newline at end of file diff --git a/src/plugins/backlight/default/Makefile.am b/src/plugins/backlight/default/Makefile.am new file mode 100644 index 0000000..65a6dcc --- /dev/null +++ b/src/plugins/backlight/default/Makefile.am @@ -0,0 +1,5 @@ +themedir = $(datadir)/gnome15/plugins/backlight/default +theme_DATA = g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/backlight/default/g19.svg b/src/plugins/backlight/default/g19.svg new file mode 100644 index 0000000..0203270 --- /dev/null +++ b/src/plugins/backlight/default/g19.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + + ${r} + ${g} + ${b} + + diff --git a/src/plugins/cairo-clock/Makefile.am b/src/plugins/cairo-clock/Makefile.am new file mode 100644 index 0000000..c92155f --- /dev/null +++ b/src/plugins/cairo-clock/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = g15 g19 mx5500 + +plugindir = $(datadir)/gnome15/plugins/cairo-clock +plugin_DATA = cairo-clock.ui \ + cairo-clock.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cairo-clock/cairo-clock.py b/src/plugins/cairo-clock/cairo-clock.py new file mode 100644 index 0000000..0b5da36 --- /dev/null +++ b/src/plugins/cairo-clock/cairo-clock.py @@ -0,0 +1,440 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cairo-clock", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import datetime +from threading import Timer +import time +import gtk +import os +import sys +import cairo +import rsvg +import pango +import locale +import xdg.BaseDirectory + +# Plugin details - All of these must be provided +id="cairo-clock" +name=_("Cairo Clock") +description=_("Port of MacSlow's SVG clock to Gnome15. Standard cairo-clock \ +themes may be used on a G19, however, for all other models \ +you must use specially crafted themes (using GIF files instead of SVG). \ +One default theme for low resolution screens is provided.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15CairoClock(gconf_key, gconf_client, screen) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15CairoClockPreferences(parent, driver, gconf_key, gconf_client) + +def get_theme_dir(model_name, gconf_key, gconf_client, theme_name): + for dir in get_theme_dirs(model_name, gconf_key, gconf_client): + full_path = "%s/%s" % ( dir, theme_name) + if os.path.exists(full_path): + return full_path + +def get_theme_dirs(model_name, gconf_key, gconf_client): + dirs = [] + model_dir = "g15" + if model_name == g15driver.MODEL_G19: + model_dir = "g19" + elif model_name == g15driver.MODEL_MX5500: + model_dir = "mx5500" + dirs.append(os.path.join(os.path.dirname(__file__), model_dir)) + dirs.append(os.path.join(g15globals.user_data_dir, "cairo-clock", model_dir)) + theme_dir = gconf_client.get(gconf_key + "/theme_dir") + if theme_dir != None: + dirs.append(theme_dir.get_string()) + if model_name == g15driver.MODEL_G19: + dirs.append(os.path.join(xdg.BaseDirectory.xdg_data_home, "cairo-clock")) + dirs.append("/usr/share/cairo-clock/themes") + return dirs + +class G15CairoClockPreferences(): + + def __init__(self, parent, driver, gconf_key, gconf_client): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cairo-clock.ui")) + + dialog = widget_tree.get_object("ClockDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_seconds" % gconf_key, "DisplaySecondsCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_date" % gconf_key, "DisplayDateCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour" % gconf_key, "TwentyFourHourCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_digital_time" % gconf_key, "DisplayDigitalTimeCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/display_year" % gconf_key, "DisplayYearCheckbox", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/second_sweep" % gconf_key, "SecondSweep", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_digital" % gconf_key, "TwentyFourHourDigitalCheckbox", True, widget_tree) + + e = gconf_client.get(gconf_key + "/theme") + theme_name = "default" + if e != None: + theme_name = e.get_string() + theme_model = widget_tree.get_object("ThemeModel") + theme = widget_tree.get_object("ThemeCombo") + + def _theme_changed(widget, key): + gconf_client.set_string(key, theme_model[widget.get_active()][0]) + + theme.connect("changed", _theme_changed, gconf_key + "/theme") + + theme_dirs = get_theme_dirs(driver.get_model_name(), gconf_key, gconf_client) + themes = {} + for d in theme_dirs: + if os.path.exists(d): + for fname in os.listdir(d): + if os.path.isdir(os.path.join(d, fname)) and not fname in themes and ( driver.get_bpp() == 16 or fname == "default" ) : + theme_model.append([fname]) + themes[fname] = True + if fname == theme_name: + theme.set_active(len(theme_model) - 1) + + dialog.run() + dialog.hide() + + +class G15CairoClock(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, [ "cairo-clock", "clock", "gnome-panel-clock", "time", "xfce4-clock", "rclock" ], id, name) + self.revert_timer = None + self.display_date = False + self.display_seconds = False + self.only_refresh_when_visible = False + + def activate(self): + self._load_surfaces() + self.panel_text = g15text.new_text(self.screen) + self.time_text = g15text.new_text(self.screen) + g15plugin.G15RefreshingPlugin.activate(self) + self.watch(None, self.config_changed) + self.do_refresh() + + def create_theme(self): + # Painting is done using cairo and cairo-clock themes, no need for a theme + return None + + def config_changed(self, client, connection_id, entry, args): + self._load_surfaces() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + self.do_refresh() + + def refresh(self): + pass + + def get_next_tick(self): + now = datetime.datetime.now() + + if self.second_sweep: + next_tick = now + datetime.timedelta(0, 0.1) + elif self.display_seconds: + next_tick = now + datetime.timedelta(0, 1.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, int(next_tick.second)) + else: + next_tick = now + datetime.timedelta(0, 60.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, 0) + + return g15pythonlang.total_seconds( next_tick - now ) + + + ''' + Private + ''' + + def _load_surfaces(self): + self.display_date = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_date" % self.gconf_key, True) + self.display_seconds = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_seconds" % self.gconf_key, True) + self.display_date = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_date" % self.gconf_key, True) + self.display_year = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_year" % self.gconf_key, True) + self.display_digital_time = g15gconf.get_bool_or_default(self.gconf_client, "%s/display_digital_time" % self.gconf_key, True) + self.second_sweep = g15gconf.get_bool_or_default(self.gconf_client, "%s/second_sweep" % self.gconf_key, False) + self.twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour" % self.gconf_key, False) + self.twenty_four_hour_digital = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_digital" % self.gconf_key, True) + + self.gconf_client.get_bool(self.gconf_key + "/twenty_four_hour") + + self.svg_size = None + self.width = self.screen.width + self.height = self.screen.height + + theme = self.gconf_client.get_string(self.gconf_key + "/theme") + if theme == None: + theme = "default" + + self.clock_theme_dir = get_theme_dir(self.screen.driver.get_model_name(), self.gconf_key, self.gconf_client, theme) + if not self.clock_theme_dir: + self.clock_theme_dir = get_theme_dir(self.screen.driver.get_model_name(), self.gconf_key, self.gconf_client, "default") + if not self.clock_theme_dir: + raise Exception("No themes could be found.") + self.behind_hands = self._load_surface_list(["clock-drop-shadow", "clock-face", "clock-marks"]) + self.hour_surfaces = self._load_surface_list(["clock-hour-hand-shadow", "clock-hour-hand"]) + self.minute_surfaces = self._load_surface_list(["clock-minute-hand-shadow", "clock-minute-hand"]) + self.second_surfaces = self._load_surface_list(["clock-secondhand-shadow", "clock-second-hand"]) + self.above_hands = self._load_surface_list([ "clock-face-shadow", "clock-glass", "clock-frame" ]) + + def _load_surface_list(self, names): + list = [] + for i in names: + path = self.clock_theme_dir + "/" + i + ".svg" + if os.path.exists(path): + svg = rsvg.Handle(path) + try: + if self.svg_size == None: + self.svg_size = svg.get_dimension_data()[2:4] + + svg_size = self.svg_size + + sx = self.width / svg_size[0] + sy = self.height / svg_size[1] + scale = min(sx, sy) + surface = cairo.SVGSurface(None, svg_size[0] * scale * 2,svg_size[1] * scale * 2) + context = cairo.Context(surface) + self.screen.configure_canvas(context) + context.scale(scale, scale) + context.translate(svg_size[0], svg_size[1]) + svg.render_cairo(context) + context.translate(-svg_size[0], -svg_size[1]) + list.append(((svg_size[0] * scale, svg_size[1] * scale), surface)) + finally: + svg.close() + + path = self.clock_theme_dir + "/" + i + ".gif" + if os.path.exists(path): + img_surface = g15cairo.load_surface_from_file(path, self.height) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, img_surface.get_width() * 2, img_surface.get_height() * 2) + context = cairo.Context(surface) + self.screen.configure_canvas(context) + context.translate(img_surface.get_width(), img_surface.get_height()) + context.set_source_surface(img_surface) + context.paint() + list.append(((img_surface.get_width(), img_surface.get_height()), surface)) + return list + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + scale = allocated_size / self.height + canvas.scale(scale, scale) + self._do_paint(canvas, self.width, self.height, False) + canvas.scale(1 / scale, 1 / scale) + return allocated_size + + def _paint_panel(self, canvas, allocated_size, horizontal): + if not self.screen.is_visible(self.page): + self.panel_text.set_canvas(canvas) + + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + text = self._get_time_text(False) + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + if self.display_date: + text = "%s\n%s" % ( self._get_time_text(), self._get_date_text() ) + font_size = allocated_size / 3 + else: + text = self._get_time_text() + font_size = allocated_size / 2 + x = 4 + gap = 8 + + self.panel_text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.panel_text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.panel_text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + def _paint(self, canvas, draw_date = True): + + width = float(self.screen.width) + height = float(self.screen.height) + + self._do_paint(canvas, width, height, self.display_date, self.display_digital_time) + + def _get_time_text(self, display_seconds = None): + if display_seconds == None: + display_seconds = self.display_seconds + if self.twenty_four_hour_digital: + return g15locale.format_time_24hour(datetime.datetime.now(), self.gconf_client, display_seconds) + else: + return g15locale.format_time(datetime.datetime.now(), self.gconf_client, display_seconds) + + def _get_date_text(self): + if self.display_year: + return datetime.datetime.now().strftime(locale.nl_langinfo(locale.D_FMT)) + else: + dformat = locale.nl_langinfo(locale.D_FMT) + for s in [ "/%y", "/%Y", ".%y", ".%Y", ".%y", ".%Y" ]: + dformat = dformat.replace(s, "") + return datetime.datetime.now().strftime(dformat) + + def _do_paint(self, canvas, width, height, draw_date = True, draw_time = True): + canvas.save() + self._do_paint_clock(canvas, width, height, draw_date and self.screen.driver.get_bpp() != 1, draw_time and self.screen.driver.get_bpp() != 1) + canvas.restore() + self.time_text.set_canvas(canvas) + + if self.screen.driver.get_bpp() == 1: + + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + canvas.set_source_rgb(rgb[0],rgb[1],rgb[2]) + + if draw_date: + date_text = self._get_date_text() + self.time_text.set_attributes(pxwidth = 56, text = date_text, align = pango.ALIGN_CENTER, font_desc = "Fixed", font_absolute_size = 12 * pango.SCALE) + self.time_text.draw(0, 15) + + if draw_time: + time_text = self._get_time_text() + self.time_text.set_attributes(pxwidth = 56, text = time_text, align = pango.ALIGN_CENTER, font_desc = "Fixed", font_absolute_size = 12 * pango.SCALE) + self.time_text.draw(self.width - 56, 15) + + def _do_paint_clock(self, canvas, width, height, draw_date = True, draw_time = True): + + now = datetime.datetime.now() + properties = { } + + time = self._get_time_text() + + clock_width = min(width, height) + clock_height = min(width, height) + + drawing_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(clock_width), int(clock_height)) + drawing_context = cairo.Context(drawing_surface) + self.screen.configure_canvas(drawing_context) + + # Below hands + for svg_size, surface in self.behind_hands: + drawing_context.save() + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() + + # Date + t_offset = 0 + if draw_date: + drawing_context.save() + date_text = self._get_date_text() + drawing_context.select_font_face("Liberation Sans", + cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + drawing_context.set_font_size(27.0) + x_bearing, y_bearing, text_width, text_height = drawing_context.text_extents(date_text)[:4] + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + drawing_context.set_source_rgb(rgb[0],rgb[1],rgb[2]) + tx = ( ( clock_width - text_width ) / 2 ) - x_bearing + ty = clock_height * 0.665 + t_offset += text_height + 4 + drawing_context.move_to( tx, ty ) + + drawing_context.show_text(date_text) + drawing_context.restore() + + # Date + if draw_time: + drawing_context.save() + time_text = self._get_time_text() + drawing_context.select_font_face("Liberation Sans", + cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + drawing_context.set_font_size(27.0) + x_bearing, y_bearing, text_width, text_height = drawing_context.text_extents(time_text)[:4] + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + drawing_context.set_source_rgb(rgb[0],rgb[1],rgb[2]) + tx = ( ( clock_width - text_width ) / 2 ) - x_bearing + ty = ( clock_height * 0.665 ) + t_offset + drawing_context.move_to( tx, ty ) + + drawing_context.show_text(time_text) + drawing_context.restore() + + # The hand + if self.second_sweep: + ms_deg = ( ( float(now.microsecond) / 10000.0 ) * ( 6.0 / 100.0 ) ) + s_deg = ( now.second * 6 ) + ms_deg + else: + s_deg = now.second * 6 + m_deg = now.minute * 6 + ( now.second * ( 6.0 / 60.0 ) ) + + if self.twenty_four_hour: + h_deg = float(now.hour) * 15.0 + ( float ( now.minute * 0.25 ) ) + else: + h_deg = float( now.hour % 12 ) * 30.0 + ( float ( now.minute * 0.5 ) ) + + self._draw_hand(drawing_context, self.hour_surfaces, clock_width, clock_height, h_deg) + self._draw_hand(drawing_context, self.minute_surfaces, clock_width, clock_height, m_deg) + if self.display_seconds: + self._draw_hand(drawing_context, self.second_surfaces, clock_width, clock_height, s_deg) + + # Above hands + for svg_size, surface in self.above_hands: + drawing_context.save() + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() + + # Paint to clock, centering it on the screen + canvas.translate( int(( width - height) / 2), 0) + canvas.set_source_surface(drawing_surface) + canvas.paint() + + + def _draw_hand(self, drawing_context, hand_surfaces, width, height, deg): + for svg_size, surface in hand_surfaces: + drawing_context.save() + drawing_context.translate(svg_size[0] / 2.0, svg_size[1] / 2.0) + g15cairo.rotate(drawing_context, -90) + g15cairo.rotate(drawing_context, deg) + drawing_context.translate(-svg_size[0], -svg_size[1]) + drawing_context.set_source_surface(surface) + drawing_context.paint() + drawing_context.restore() \ No newline at end of file diff --git a/src/plugins/cairo-clock/cairo-clock.ui b/src/plugins/cairo-clock/cairo-clock.ui new file mode 100644 index 0000000..20adbf3 --- /dev/null +++ b/src/plugins/cairo-clock/cairo-clock.ui @@ -0,0 +1,244 @@ + + + + + + 320 + False + 5 + Clock Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + ThemeModel + + + + 0 + + + + + + + + + True + False + <b>Theme</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + Sweeping second hand + True + True + False + 0.52999997138977051 + True + + + True + True + 0 + + + + + Show Seconds + True + True + False + True + + + True + True + 1 + + + + + Show Date + True + True + False + True + + + True + True + 2 + + + + + Display Digital Time + True + True + False + True + + + True + True + 3 + + + + + Show Year + True + True + False + True + + + True + True + 4 + + + + + 24hr Analogue Time (requires 24hr theme) + True + True + False + True + + + True + True + 5 + + + + + 24hr Digital Time + True + True + False + 0.54000002145767212 + True + + + True + True + 6 + + + + + + + + + True + False + <b>Display Options</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + + + + + + + diff --git a/src/plugins/cairo-clock/g15/Makefile.am b/src/plugins/cairo-clock/g15/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/g15/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/g15/default/Makefile.am b/src/plugins/cairo-clock/g15/default/Makefile.am new file mode 100644 index 0000000..51d4127 --- /dev/null +++ b/src/plugins/cairo-clock/g15/default/Makefile.am @@ -0,0 +1,10 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/g15/default +cairoclocktheme_DATA = clock-frame.gif \ + clock-hour-hand.gif \ + clock-marks.gif \ + clock-glass.gif \ + clock-minute-hand.gif \ + clock-second-hand.gif + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/g15/default/clock-frame.gif b/src/plugins/cairo-clock/g15/default/clock-frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..58d13ac99df03f124269ebe28e7749a8f0663a38 GIT binary patch literal 168 zcmZ?wbhEHb)Mn6TXkcUjg0~Dkia%KxxfmE3bU=KN3?y}{1IlU;ARqTe#)@vyzHf{GPDOz?y UnDddvrh^`t&lhM0GB8*J03Y*0x&QzG literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/g15/default/clock-glass.gif b/src/plugins/cairo-clock/g15/default/clock-glass.gif new file mode 100644 index 0000000000000000000000000000000000000000..b8461639729fd80fe6c37e46b41a2af1fb273cbf GIT binary patch literal 127 zcmZ?wbhEHb)Mn6TSjfZx1poj4f6LGVM2i1Jor_WvOHxx5$}>wc6hbmm72G|20~i#4 zvM_QnFfr(Wl!G)fFqu#3UwQg1|Kd4YZgp?Ix92y1+ar%@&pMaAIu&+cH`76$6-K$| c9di%A<>%pQKeFg!hmY3uGs`{)FfdpH024|vx&QzG literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/g15/default/clock-hour-hand.gif b/src/plugins/cairo-clock/g15/default/clock-hour-hand.gif new file mode 100644 index 0000000000000000000000000000000000000000..8f4b4456e5501d81fb4af0e26cd2adfee9158851 GIT binary patch literal 103 zcmZ?wbhEHb)Mn6TSjfZx1poj4f6LGVM2bII7`Ygj7<53QAbAERi_3m%Tc*?H%{=Pd?Yb^*;Y~?)yJ}9~keht2F>z C@hd9; literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/g15/default/clock-marks.gif b/src/plugins/cairo-clock/g15/default/clock-marks.gif new file mode 100644 index 0000000000000000000000000000000000000000..6527b2aa203eed5e3bb9b73913bc1c2a5988b884 GIT binary patch literal 135 zcmZ?wbhEHb)Mn6TXkcUjg0~Dkia%KxxfmE3bU=KN3wwM6 ze-Wq6^A@~V*l}y)l-Et4e6D}foxEe+hb{fwTwW&~-1mR{JY4NZ7JcmS(VBjUiNP8Gt#l~w literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/g15/default/clock-second-hand.gif b/src/plugins/cairo-clock/g15/default/clock-second-hand.gif new file mode 100644 index 0000000000000000000000000000000000000000..221940e72c2be12e33f588baeb40a4b6cdd48555 GIT binary patch literal 101 zcmZ?wbhEHb)Mn6TSjfZx1poj4f6LGVM2bII7`Ygj7<53QAbAERvzGo!#kc&6xwqWP zUV3lOZ~nGN9@Cz6E_-!q+dJ;#pM0)=>wW&~-1mR{JY4NZ7JcmS(VBinZ5;!HH2@cR BDnkGO literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/g19/Makefile.am b/src/plugins/cairo-clock/g19/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/g19/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/g19/default/Makefile.am b/src/plugins/cairo-clock/g19/default/Makefile.am new file mode 100644 index 0000000..d6cc664 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/Makefile.am @@ -0,0 +1,16 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/g19/default +cairoclocktheme_DATA = clock-drop-shadow.svg \ + clock-face.svg \ + clock-face-shadow.svg \ + clock-frame.svg \ + clock-glass.svg \ + clock-hour-hand.svg \ + clock-hour-hand-shadow.svg \ + clock-marks.svg \ + clock-minute-hand.svg \ + clock-minute-hand-shadow.svg \ + clock-second-hand.svg \ + clock-second-hand-shadow.svg + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg new file mode 100644 index 0000000..8c18129 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-drop-shadow.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg new file mode 100644 index 0000000..12e2937 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-face-shadow.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-face.svg b/src/plugins/cairo-clock/g19/default/clock-face.svg new file mode 100644 index 0000000..747321c --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-face.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-frame.svg b/src/plugins/cairo-clock/g19/default/clock-frame.svg new file mode 100644 index 0000000..b387b81 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-frame.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-glass.svg b/src/plugins/cairo-clock/g19/default/clock-glass.svg new file mode 100644 index 0000000..2ebcb51 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-glass.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg new file mode 100644 index 0000000..4da746b --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-hour-hand-shadow.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg b/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg new file mode 100644 index 0000000..e14dcb4 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-hour-hand.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-marks.svg b/src/plugins/cairo-clock/g19/default/clock-marks.svg new file mode 100644 index 0000000..17781c3 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-marks.svg @@ -0,0 +1,1163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg new file mode 100644 index 0000000..d361f0b --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-minute-hand-shadow.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg b/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg new file mode 100644 index 0000000..5d09dd4 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-minute-hand.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg b/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg new file mode 100644 index 0000000..f8b297c --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-second-hand-shadow.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/cairo-clock/g19/default/clock-second-hand.svg b/src/plugins/cairo-clock/g19/default/clock-second-hand.svg new file mode 100644 index 0000000..3cc63e1 --- /dev/null +++ b/src/plugins/cairo-clock/g19/default/clock-second-hand.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po b/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po new file mode 100644 index 0000000..a3a857e --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/cairo-clock.glade.h:1 +msgid "24hr Mode" +msgstr "24hr Mode" + +#: i18n/cairo-clock.glade.h:2 +msgid "Display Options" +msgstr "Display Options" + +#: i18n/cairo-clock.glade.h:3 +msgid "Theme" +msgstr "Theme" + +#: i18n/cairo-clock.glade.h:4 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/cairo-clock.glade.h:5 +msgid "Display Digital Time" +msgstr "Display Digital Time" + +#: i18n/cairo-clock.glade.h:6 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/cairo-clock.glade.h:7 +msgid "Show Seconds" +msgstr "Show Seconds" + +#: i18n/cairo-clock.glade.h:8 +msgid "Show Year" +msgstr "Show Year" + +#: i18n/cairo-clock.glade.h:9 +msgid "Sweeping second hand" +msgstr "Sweeping second hand" diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.glade.h b/src/plugins/cairo-clock/i18n/cairo-clock.glade.h new file mode 100644 index 0000000..eed9f16 --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.glade.h @@ -0,0 +1,9 @@ +char *s = N_("24hr Mode"); +char *s = N_("Display Options"); +char *s = N_("Theme"); +char *s = N_("Clock Preferences"); +char *s = N_("Display Digital Time"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); +char *s = N_("Show Year"); +char *s = N_("Sweeping second hand"); diff --git a/src/plugins/cairo-clock/i18n/cairo-clock.pot b/src/plugins/cairo-clock/i18n/cairo-clock.pot new file mode 100644 index 0000000..4695fad --- /dev/null +++ b/src/plugins/cairo-clock/i18n/cairo-clock.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/cairo-clock.glade.h:1 +msgid "24hr Mode" +msgstr "" + +#: i18n/cairo-clock.glade.h:2 +msgid "Display Options" +msgstr "" + +#: i18n/cairo-clock.glade.h:3 +msgid "Theme" +msgstr "" + +#: i18n/cairo-clock.glade.h:4 +msgid "Clock Preferences" +msgstr "" + +#: i18n/cairo-clock.glade.h:5 +msgid "Display Digital Time" +msgstr "" + +#: i18n/cairo-clock.glade.h:6 +msgid "Show Date" +msgstr "" + +#: i18n/cairo-clock.glade.h:7 +msgid "Show Seconds" +msgstr "" + +#: i18n/cairo-clock.glade.h:8 +msgid "Show Year" +msgstr "" + +#: i18n/cairo-clock.glade.h:9 +msgid "Sweeping second hand" +msgstr "" diff --git a/src/plugins/cairo-clock/mx5500/Makefile.am b/src/plugins/cairo-clock/mx5500/Makefile.am new file mode 100644 index 0000000..4977d17 --- /dev/null +++ b/src/plugins/cairo-clock/mx5500/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = default \ No newline at end of file diff --git a/src/plugins/cairo-clock/mx5500/default/Makefile.am b/src/plugins/cairo-clock/mx5500/default/Makefile.am new file mode 100644 index 0000000..5dba142 --- /dev/null +++ b/src/plugins/cairo-clock/mx5500/default/Makefile.am @@ -0,0 +1,10 @@ +cairoclockthemedir = $(datadir)/gnome15/plugins/cairo-clock/mx5500/default +cairoclocktheme_DATA = clock-frame.gif \ + clock-hour-hand.gif \ + clock-marks.gif \ + clock-glass.gif \ + clock-minute-hand.gif \ + clock-second-hand.gif + +EXTRA_DIST = \ + $(cairoclocktheme_DATA) diff --git a/src/plugins/cairo-clock/mx5500/default/clock-frame.gif b/src/plugins/cairo-clock/mx5500/default/clock-frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..b27d9bfb8a106eeb39ba6b04db97d6af69f1c03e GIT binary patch literal 151 zcmZ?wbhEHbRA5kGXkcUjg0~DkivL8Ni&7IyQd1PlGfOfQLNZbn+&z5*7!-f9Fmf?4 zGU$L5g0wI&h4%Ebou19l-)-!aE_lv*`M*lJf=(sLSuv~D1zGPakXf+V^_Ka%_u=n< zGc-?9+?b}))Yp|#aI!_N*r#`q*6PGkzlinOOilhv)!(aK?U@*tnR-2=gNwl$0Epx? A=>Px# literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/mx5500/default/clock-glass.gif b/src/plugins/cairo-clock/mx5500/default/clock-glass.gif new file mode 100644 index 0000000000000000000000000000000000000000..5d0a8f0c5ab4ce6ab89cb2148d4e60db21b36303 GIT binary patch literal 88 zcmZ?wbhEHbRA5kGSjfZx1poj4f6LGVM2bII7`Ygj7<53QAbAER%_;pWPrv0~JZH literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif b/src/plugins/cairo-clock/mx5500/default/clock-hour-hand.gif new file mode 100644 index 0000000000000000000000000000000000000000..d43414e357ecdfcbc3c545852d2cdc5b649465ae GIT binary patch literal 87 zcmZ?wbhEHbRA5kGSjfZx1poj4f6LGVM2bII7`Ygj7<53QAbAERjTU~z({ImT2;6Y3 m!Fb!A-~4TlJf=PCT=weJws+jeKlxn$*8BX|x$iZM4AubYrX$Dz literal 0 HcmV?d00001 diff --git a/src/plugins/cairo-clock/mx5500/default/clock-marks.gif b/src/plugins/cairo-clock/mx5500/default/clock-marks.gif new file mode 100644 index 0000000000000000000000000000000000000000..132c28eccde8ccb4f02649a011b6d98ef26dbb75 GIT binary patch literal 132 zcmZ?wbhEHbRA5kGXkcUjg0~DkivL8Ni&7IyQd1PlGfOfQLNZbn+&z5*7!-f9Fmf?4 zGU$L5g0wI&Ira3fJpGoRnPKC+Oi8!jm+pAZ7fgHBxh!mF9~bw*S3cLvOimV_`wW&~+;>(6YXH8=BPRd= literal 0 HcmV?d00001 diff --git a/src/plugins/cal-evolution/Makefile.am b/src/plugins/cal-evolution/Makefile.am new file mode 100644 index 0000000..854da95 --- /dev/null +++ b/src/plugins/cal-evolution/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/cal-evolution +plugin_DATA = cal-evolution.py \ + icon.png \ + cal-evolution.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cal-evolution/cal-evolution.py b/src/plugins/cal-evolution/cal-evolution.py new file mode 100644 index 0000000..de6f320 --- /dev/null +++ b/src/plugins/cal-evolution/cal-evolution.py @@ -0,0 +1,131 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Calendar backend that retrieves event data from Evolution +""" + +import gnome15.g15locale as g15locale +import gnome15.g15accounts as g15accounts +_ = g15locale.get_translation("cal-evolution", modfile = __file__).ugettext +import gtk +import urllib +import vobject +import datetime +import dateutil +import sys, os, os.path +import re +import cal +import xdg.BaseDirectory +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +id="cal-evolution" +name=_("Calendar (Evolution support)") +description=_("Calendar for Evolution. Adds Evolution as a source for calendars \ +to the Calendar plugin") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +global_plugin=True +passive=True +unsupported_models=cal.unsupported_models + +""" +Calendar Back-end module functions +""" +def create_options(account, account_ui): + return EvolutionCalendarOptions(account, account_ui) + +def create_backend(account, account_manager): + return EvolutionBackend() + +class EvolutionCalendarOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal-evolution.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + try : + self.event.valarm + self.alarm = True + except AttributeError as ae: + logger.debug("Could not set attribute", exc_info = ae) + pass + +class EvolutionEvent(cal.CalendarEvent): + + def __init__(self, parsed_event): + cal.CalendarEvent.__init__(self) + + self.start_date = parsed_event.dtstart.value + if parsed_event.dtend: + self.end_date = parsed_event.dtend.value + else: + self.end_date = datetime.datetime(self.start_date.year,self.start_date.month,self.start_date.day, 23, 59, 0) + + self.summary = parsed_event.summary.value + self.alt_icon = os.path.join(os.path.dirname(__file__), "icon.png") + +class EvolutionBackend(cal.CalendarBackend): + + def __init__(self): + cal.CalendarBackend.__init__(self) + + def get_events(self, now): + calendars = [] + event_days = {} + + # Find all the calendar files + cal_dir = os.path.join(xdg.BaseDirectory.xdg_data_home, "evolution", "calendar") + if not os.path.exists(cal_dir): + # Older versions of evolution store their data in ~/.evolution + cal_dir = os.path.expanduser("~/.evolution/calendar") + if os.path.exists(cal_dir): + for root, dirs, files in os.walk(cal_dir): + for _file in files: + if _file.endswith(".ics"): + calendars.append(os.path.join(root, _file)) + + for cal in calendars: + if not re.search("^webcal://", cal[1]): + f = open(cal) + calstring = ''.join(f.readlines()) + f.close() + try: + event_list = vobject.readOne(calstring).vevent_list + except AttributeError as ae: + logger.debug("Could not read attribute", exc_info = ae) + continue + else: # evolution library does not support webcal ics + webcal = urllib.urlopen('http://' + cal[1][9:]) + webcalstring = ''.join(webcal.readlines()) + webcal.close() + event_list = vobject.readOne(webcalstring).vevent_list + + for e in event_list: + if type(e) != vobject.icalendar.RecurringComponent: + parsed_event = vobject.readOne(e.get_as_string()) + else: + parsed_event = e + + self.check_and_add(EvolutionEvent(parsed_event), now, event_days) + + return event_days \ No newline at end of file diff --git a/src/plugins/cal-evolution/cal-evolution.ui b/src/plugins/cal-evolution/cal-evolution.ui new file mode 100644 index 0000000..b0b7e1c --- /dev/null +++ b/src/plugins/cal-evolution/cal-evolution.ui @@ -0,0 +1,26 @@ + + + + + + False + + + True + False + + + True + False + No configuration required + + + True + True + 0 + + + + + + diff --git a/src/plugins/cal-evolution/icon.png b/src/plugins/cal-evolution/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ba4f15d6942fdae976cb3231aba80cea48af06 GIT binary patch literal 665 zcmV;K0%rY*P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iyf1 z02C$&#?)K@00J3FL_t(I%axL`OIu+S#ees`FNsNXEaKF`r3EE})C_j$5J$y1OL3@$ zLZ>Y5{tHFBxQN=nz^AgJM0@#^vB32K;nyc(9I+-2_ixmW@IL~%nLuhcli0`WGoB#{fo>CuJgXZi+Y`>W@FvQam>l# zAs?{o+o0}{xEpdE&Ot07D?Cgy5^K-VgwwRrrrP*wb@TyAm?+f$u^OVbF z`u#qoQi;jQN#^F}n3w`)=;(-6tHt{II?g#RE-tvfzNS*C zFg-oZ>FMbsyi-*kd^k8bpxth>va&*wB;4NK((QK1<#JRi6?(lMtyT*&BZTmm;6YNi z+huBMie|G3K)qgPVPRq9L9tk5d3kx1n5y2#sH%hzXfzuCjcj9M +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal-evolution", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import cal +import gtk +import os +import datetime +import calendar +import gdata.calendar.data +import gdata.calendar.client +import gdata.acl.data +import gdata.service +import iso8601 +import subprocess +import socket + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +id="cal-google" +name=_("Calendar (Google support)") +description=_("Adds your Google Calendar as a source for the Gnome15 Calendar plugin") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="cal" +unsupported_models=cal.unsupported_models + +""" +Calendar Back-end module functions +""" +def create_options(account, account_ui): + return GoogleCalendarOptions(account, account_ui) + +def create_backend(account, account_manager): + return GoogleCalendarBackend(account, account_manager) + +# How often refresh from the evolution calendar. This can be a slow process, so not too often +REFRESH_INTERVAL = 15 * 60 + +class GoogleCalendarOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal-google.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + username = self.widget_tree.get_object("Username") + username.connect("changed", self._username_changed) + username.set_text(self.account.get_property("username", "")) + + calendar = self.widget_tree.get_object("Calendar") + calendar.connect("changed", self._calendar_changed) + calendar.set_text(self.account.get_property("calendar", "")) + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + + def _calendar_changed(self, widget): + self.account.properties["calendar"] = widget.get_text() + self.account_ui.save_accounts() + +class GoogleEvent(cal.CalendarEvent): + + def __init__(self, when, parsed_event, color, url): + cal.CalendarEvent.__init__(self) + self.start_date = iso8601.parse_date(when.start) + if when.end: + # Cal plugin dates are inclusive, but googles seem to be exclusive + d = iso8601.parse_date(when.end) + if d.hour == 0 and d.minute == 0 and d.second == 0: + d = d - datetime.timedelta(0, 1) + + self.end_date = d + else: + self.end_date = datetime.datetime(self.start_date.year,self.start_date.month,self.start_date.day, 23, 59, 0) + + self.link = url + self.color = color + self.summary = parsed_event.title.text + self.alarm = len(when.reminder) > 0 + self.alt_icon = os.path.join(os.path.dirname(__file__), "icon.png") + + def activate(self): + logger.info("xdg-open '%s'", self.link) + subprocess.Popen(['xdg-open', self.link]) + + +class GoogleCalendarBackend(cal.CalendarBackend): + + def __init__(self, account, account_manager): + cal.CalendarBackend.__init__(self) + self.account = account + self.account_manager = account_manager + + def get_events(self, now): + self.cal_client = gdata.calendar.client.CalendarClient(source='%s-%s' % ( g15globals.name, g15globals.version ) ) + + # Reload the account + self.account = self.account_manager.by_name(self.account.name) + + for i in range(0, 3): + for j in range(0, 2): + password = self.account_manager.retrieve_password(self.account, "www.google.com", None, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + return self._retrieve_events(now, password) + except gdata.client.BadAuthentication as e: + logger.debug("Error authenticating", exc_info = e) + pass + + raise Exception(_("Authentication attempted too many times")) + + + def _retrieve_events(self, now, password): + event_days = {} + self.cal_client.ClientLogin(self.account.get_property("username", ""), password, self.cal_client.source) + self.account_manager.store_password(self.account, password, "www.google.com", None) + start_date = datetime.date(now.year, now.month, 1) + end_date = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]) + feeds = self.cal_client.GetAllCalendarsFeed() + + for i, a_calendar in zip(xrange(len(feeds.entry)), feeds.entry): + query = gdata.calendar.client.CalendarEventQuery(start_min=start_date, start_max=end_date) + logger.info("Retrieving events from %s to %s", str(start_date), str(end_date)) + feed = self.cal_client.GetCalendarEventFeed(a_calendar.content.src, q = query) + + # TODO - Color doesn't seem to work + color = None + + for i, an_event in zip(xrange(len(feed.entry)), feed.entry): + logger.info('Adding event %s (%s)', an_event.title.text, str(an_event.when)) + + """ + An event may have multiple times. cal doesn't support multiple times, so we add multiple events instead + """ + for a_when in an_event.when: + self.check_and_add(GoogleEvent(a_when, an_event, color, a_calendar.content.src), now, event_days) + + return event_days + + diff --git a/src/plugins/cal-google/cal-google.ui b/src/plugins/cal-google/cal-google.ui new file mode 100644 index 0000000..4075098 --- /dev/null +++ b/src/plugins/cal-google/cal-google.ui @@ -0,0 +1,97 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Calendar: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:110 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/cal-google/icon.png b/src/plugins/cal-google/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..effd7a8730d1e068fdbee9bd45558231ca102c19 GIT binary patch literal 1258 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyf1 z02Bj5!T=ip00e4DL_t(I%cWF%OjCCh{_dsFB9AiMcor&FSwuwGWFQJ5hA?bxq=#B^3bRZfXF#z3yT2 zReSkO3#)GJQ>Q+Qc)KHO8?9{iV%YF_H-Hm_%3w_0OW2r(aY;u z833eS$~X{tB;jbF=fvYHLbL#|t%Jtrlf&xAw0a4*r}$1?Lw} zG=>D{QJtv`VXZMog0Y_J3LtMc*^{)Uw>b!xB`$7F005?@Tr*y_K0ojw_Ls)7-8p6C zPVqbdC_X^sd9PVDMHwV2yd!xleNcG_0CXG5^L%x+sQ^IREc|nZp)W5GLZB=d0RVCO z?m>g__WneRm8iV<0+Bl()S^R>r0&GsDLXZk`Gub*3|2K1kEm!tpfE2sQz805 zA)LX=ot9Vt5U0%RpC1^ymOavcm1l&<9N+3*5lp7r01zQKulsGxPR^y2>o3He7rjz6 z!r_IV$Rv|xxpms5zG1Pvb%kgJreYLTY8xOlV*mr-TQ* z&a8Pf#!mwPC1E()F=Y37X!>9tvX4(ORW%4)uSJteag09ctQxddAW1%DX;7+6_imgCH6!D;T_+Q6N%{Ia!$ zdZ1226h&NCfqhk$r~v?yv?7G|=sM3!UU&L}s<9W>wcHDH*yXIuI;N0w?7O^MeEFs- zC)=cO|Cq7O|Gv*AWa-XErs)h6TitHe%VG*@0`7w$jmPaqq4uMjL!T( z6ToT_t!R&3m=G+UU)y9U0PsYkt65dry%_*vi$}-9aageckpD>wJxQzLXB4>f)8P79 zb#6&%oTJ>%#<|K?RTzdMm!Hc50A+svCsFFJ;{l*;QOax`X$lx-=(BT*>7?d=b zyT{_!j=qtl^(>9a?cNbQ-2aM#76~Oi1c+!;&R^Zv9#_=+dW2?QIshcS@OO*<14-Bl UPuEhJT>t<807*qoM6N<$f>fkTiU0rr literal 0 HcmV?d00001 diff --git a/src/plugins/cal-google/iso8601.py b/src/plugins/cal-google/iso8601.py new file mode 100644 index 0000000..5230e01 --- /dev/null +++ b/src/plugins/cal-google/iso8601.py @@ -0,0 +1,121 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""ISO 8601 date time string parsing + +Basic usage: +>>> import iso8601 +>>> iso8601.parse_date("2007-01-25T12:00:00Z") +datetime.datetime(2007, 1, 25, 12, 0, tzinfo=) +>>> +""" + +from datetime import datetime, timedelta, tzinfo +import re + +__all__ = ["parse_date", "ParseError"] + +# Adapted from http://delete.me.uk/2005/03/iso8601.html +ISO8601_REGEX = re.compile(r"(?P[0-9]{4})(-(?P[0-9]{1,2})(-(?P[0-9]{1,2})" + r"((?P.)(?P[0-9]{2}):(?P[0-9]{2})(:(?P[0-9]{2})(\.(?P[0-9]+))?)?" + r"(?PZ|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" +) +TIMEZONE_REGEX = re.compile("(?P[+-])(?P[0-9]{2}).(?P[0-9]{2})") + +class ParseError(Exception): + """Raised when there is a problem parsing a date string""" + +# Yoinked from python docs +ZERO = timedelta(0) +class Utc(tzinfo): + """UTC + + """ + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO +UTC = Utc() + +class FixedOffset(tzinfo): + """Fixed offset in hours and minutes from UTC + + """ + def __init__(self, offset_hours, offset_minutes, name): + self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + + def __repr__(self): + return "" % self.__name + +def parse_timezone(tzstring, default_timezone=UTC): + """Parses ISO 8601 time zone specs into tzinfo offsets + + """ + if tzstring == "Z": + return default_timezone + # This isn't strictly correct, but it's common to encounter dates without + # timezones so I'll assume the default (which defaults to UTC). + # Addresses issue 4. + if tzstring is None: + return default_timezone + m = TIMEZONE_REGEX.match(tzstring) + prefix, hours, minutes = m.groups() + hours, minutes = int(hours), int(minutes) + if prefix == "-": + hours = -hours + minutes = -minutes + return FixedOffset(hours, minutes, tzstring) + +def parse_date(datestring, default_timezone=UTC): + """Parses ISO 8601 dates into datetime objects + + The timezone is parsed from the date string. However it is quite common to + have dates without a timezone (not strictly correct). In this case the + default timezone specified in default_timezone is used. This is UTC by + default. + """ + if not isinstance(datestring, basestring): + raise ParseError("Expecting a string %r" % datestring) + m = ISO8601_REGEX.match(datestring) + if not m: + raise ParseError("Unable to parse date string %r" % datestring) + groups = m.groupdict() + tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) + if groups["fraction"] is None: + groups["fraction"] = 0 + else: + groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) + return datetime(int(groups["year"] if "year" in groups else 0), + int(groups["month"] if "month" in groups else 1), + int(groups["day"] if "day" in groups else 1), + int(groups["hour"] if "hour" in groups and groups["hour"] is not None else 0), + int(groups["minute"] if "minute" in groups and groups["minute"] is not None else 0), + int(groups["second"] if "second" in groups and groups["second"] is not None else 0), + int(groups["fraction"] if "fraction" in groups and groups["fraction"] is not None else 0), tz) diff --git a/src/plugins/cal/Makefile.am b/src/plugins/cal/Makefile.am new file mode 100644 index 0000000..45d700f --- /dev/null +++ b/src/plugins/cal/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/cal +plugin_DATA = cal.py \ + cal.ui \ + bell.gif + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/cal/bell.gif b/src/plugins/cal/bell.gif new file mode 100644 index 0000000000000000000000000000000000000000..f32c902bc1676969872483ae93ee8a23c8aa47e4 GIT binary patch literal 59 zcmZ?wbhEHbWM^P!SjYeZ|Ns97(+r9~Ss1w(m>6_GT#!5i6OT_r8CTo;u-Ts?mvJ#z F0{~f346pzI literal 0 HcmV?d00001 diff --git a/src/plugins/cal/cal.py b/src/plugins/cal/cal.py new file mode 100644 index 0000000..ac3d9f1 --- /dev/null +++ b/src/plugins/cal/cal.py @@ -0,0 +1,516 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15screen as g15screen +import gnome15.g15accounts as g15accounts +import gnome15.g15plugin as g15plugin +import gnome15.g15globals as g15globals +import datetime +import time +import os, os.path +import gtk +import calendar + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin data +id="cal" +name=_("Calendar") +description=_("1Provides basic support for calendars. To make this\n\ +plugin work, you will also need a second plugin for your calendar\n\ +provider. Currently, Gnome15 supports Evolution and Google calendars.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous day/Event"), + g15driver.NEXT_SELECTION : _("Next day/Event"), + g15driver.VIEW : _("Return to today"), + g15driver.CLEAR : _("Toggle calendar/events"), + g15driver.NEXT_PAGE : _("Next week"), + g15driver.PREVIOUS_PAGE : _("Previous week") +} +actions_g19={ + g15driver.PREVIOUS_PAGE : _("Previous day/Event"), + g15driver.NEXT_PAGE : _("Next day/Event"), + g15driver.VIEW : _("Return to today"), + g15driver.CLEAR : _("Toggle calendar/events"), + g15driver.NEXT_SELECTION : _("Next week"), + g15driver.PREVIOUS_SELECTION : _("Previous week") +} +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, \ + g15driver.MODEL_MX5500, g15driver.MODEL_G930, \ + g15driver.MODEL_G35 ] + +# How often refresh from the evolution calendar. This can be a slow process, so not too often +REFRESH_INTERVAL = 15 * 60 + +# Configuration +CONFIG_PATH = os.path.join(g15globals.user_config_dir, "plugin-data", "cal", "calendars.xml") +CONFIG_ITEM_NAME = "calendar" + +""" +Functions +""" + +def create(gconf_key, gconf_client, screen): + return G15Cal(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15CalendarPreferences(parent, gconf_client, gconf_key) + +def get_backend(account_type): + """ + Get the backend plugin module, given the account_type + + Keyword arguments: + account_type -- account type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("cal-%s" % account_type) + +def get_available_backends(): + """ + Get the "account type" names that are available by listing all of the + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("cal-"): + l.append(p.id[4:]) + return l + +class CalendarEvent(): + + def __init__(self): + self.start_date = None + self.end_date = None + self.summary = None + self.color = None + self.alarm = False + self.alt_icon = None + + def activate(self): + raise Exception("Not implemented") + +class CalendarBackend(): + + def __init__(self): + self.start_date = None + self.end_date = None + + def check_and_add(self, ve, now, event_days): + if ve.start_date.month == now.month and ve.start_date.year == now.year: + day = ve.start_date.day + while day <= ve.end_date.day: + key = str(day) + day_event_list = event_days[key] if key is event_days else None + if day_event_list is None: + day_event_list = list() + event_days[key] = day_event_list + day_event_list.append(ve) + day += 1 + + def get_events(self, now): + raise Exception("Not implemented") + +class EventMenuItem(g15theme.MenuItem): + + def __init__(self, plugin, event, component_id): + g15theme.MenuItem.__init__(self, component_id) + self.event = event + self.plugin = plugin + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.event.summary + + start_str = self.event.start_date.strftime("%H:%M") + end_str = self.event.end_date.strftime("%H:%M") + + if not self._same_day(self.event.start_date, self.event.end_date): + if not self._same_day(self.plugin._calendar_date, self.event.end_date): + end_str = _(self.event.end_date.strftime("%m/%d")) + if not self._same_day(self.plugin._calendar_date, self.event.start_date): + start_str = _(self.event.start_date.strftime("%m/%d")) + + + if self._same_day(self.event.start_date, self.event.end_date) and \ + self.event.start_date.hour == 0 and self.event.start_date.minute == 0 and \ + self.event.end_date.hour == 23 and self.event.end_date.minute == 59: + item_properties["item_alt"] = _("All Day") + else: + item_properties["item_alt"] = "%s-%s" % ( start_str, end_str) + + item_properties["item_alarm"] = self.event.alarm + if self.event.alarm: + if self.get_screen().device.bpp > 1: + item_properties["item_icon"] = g15icontools.get_icon_path([ "stock_alarm", "alarm-clock", "alarm-timer", "dialog-warning" ]) + else: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), 'bell.gif') + if self.event.alt_icon: + item_properties["alt_icon"] = self.event.alt_icon + return item_properties + + def activate(self): + self.event.activate() + + def _same_day(self, date1, date2): + return date1.day == date2.day and date1.month == date2.month and date1.year == date2.year + +class Cell(g15theme.Component): + def __init__(self, day, now, event, component_id): + g15theme.Component.__init__(self, component_id) + self.day = day + self.now = now + self.event = event + + def on_configure(self): + self.set_theme(g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), "cell")) + + def get_theme_properties(self): + weekday = self.day.weekday() + properties = {} + properties["weekday"] = weekday + properties["day"] = self.day.day + properties["event"] = self.event.summary if self.event else "" + if self.now.day == self.day.day and self.now.month == self.day.month: + properties["today"] = True + return properties + + def get_item_attributes(self, selected): + return {} + +class Calendar(g15theme.Component): + + def __init__(self, component_id="calendar"): + g15theme.Component.__init__(self, component_id) + self.layout_manager = g15theme.GridLayoutManager(7) + self.focusable = True + + +class G15CalendarPreferences(g15accounts.G15AccountPreferences): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME) + + def get_account_types(self): + return get_available_backends() + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + backend = get_backend(account.type) + if backend is None: + logger.warning("No backend for account type %s", account_type) + return None + return backend.create_options(account, self) + + def create_general_options(self): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "cal.ui")) + g15uigconf.configure_checkbox_from_gconf(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, "TwentyFourHourTimes", True, widget_tree) + return widget_tree.get_object("OptionPanel") + +class G15Cal(g15plugin.G15Plugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self._timer = None + self._icon_path = g15icontools.get_icon_path(["calendar", "evolution-calendar", "office-calendar", "stock_calendar" ]) + self._thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + + def activate(self): + g15plugin.G15Plugin.activate(self) + + self._active = True + self._event_days = None + self._calendar_date = None + self._page = None + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), auto_dirty = False) + self._loaded = 0 + + # Backend + self._account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + + # Calendar + self._calendar = Calendar() + + # Menu + self._menu = g15theme.Menu("menu") + self._menu.focusable = True + self._menu.focused_component = True + + # Page + self._page = g15theme.G15Page(name, self.screen, on_shown = self._on_shown, \ + on_hidden = self._on_hidden, theme_properties_callback = self._get_properties, + thumbnail_painter = self._paint_thumbnail, + originating_plugin = self) + self._page.set_title(_("Calendar")) + self._page.set_theme(self._theme) + self._page.focused_component = self._calendar + self._calendar.set_focused(True) + + # List for account changes + self._account_manager.add_change_listener(self._accounts_changed) + self.screen.key_handler.action_listeners.append(self) + + # Run first load in thread + self._page.add_child(self._menu) + self._page.add_child(self._calendar) + self._page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self.screen.add_page(self._page) + g15scheduler.schedule("CalendarFirstLoad", 0, self._redraw) + + # Listen for changes in the network state + self.screen.service.network_manager.listeners.append(self._network_state_changed) + + # Config changes + self.watch("twenty_four_hour_times", self._config_changed) + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + self.screen.service.network_manager.listeners.append(self._network_state_changed) + self._account_manager.remove_change_listener(self._accounts_changed) + self.screen.key_handler.action_listeners.remove(self) + if self._timer != None: + self._timer.cancel() + if self._page != None: + g15screen.run_on_redraw(self.screen.del_page, self._page) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page and self._page.is_visible(): + if self._calendar.is_focused(): + if ( binding.action == g15driver.PREVIOUS_PAGE and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_SELECTION and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(-1) + return True + elif ( binding.action == g15driver.NEXT_PAGE and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_SELECTION and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(1) + return True + elif ( binding.action == g15driver.PREVIOUS_SELECTION and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_PAGE and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(-7) + return True + elif ( binding.action == g15driver.NEXT_SELECTION and self.screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_PAGE and self.screen.device.model_id != g15driver.MODEL_G19 ): + self._adjust_calendar_date(7) + return True + elif binding.action == g15driver.VIEW: + self._calendar_date = None + self._adjust_calendar_date(0) + return True + if binding.action == g15driver.CLEAR: + self._page.next_focus() + return True + + """ + Private + """ + + def _config_changed(self, client, connection_id, entry, args): + self._loaded = 0 + self._redraw() + + def _network_state_changed(self, state): + self._loaded = 0 + self._redraw() + + def _accounts_changed(self, account_manager): + self._loaded = 0 + self._redraw() + + def _adjust_calendar_date(self, amount): + o_date = self._get_calendar_date() + self._calendar_date = o_date + datetime.timedelta(amount) + if amount == 0 or o_date.month != self._calendar_date.month or o_date.year != self._calendar_date.year: + self._load_month_events(self._calendar_date) + else: + g15screen.run_on_redraw(self._rebuild_components, self._calendar_date) + + def _get_calendar_date(self): + now = datetime.datetime.now() + return self._calendar_date if self._calendar_date is not None else now + + def _get_properties(self): + now = datetime.datetime.now() + calendar_date = self._get_calendar_date() + properties = {} + properties["icon"] = self._icon_path + properties["title"] = _('Calendar') + if g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True): + properties["time"] = g15locale.format_time_24hour(now, self.gconf_client, False) + properties["full_time"] = g15locale.format_time_24hour(now, self.gconf_client, True) + else: + properties["full_time"] = g15locale.format_time(now, self.gconf_client, True) + properties["time"] = g15locale.format_time(now, self.gconf_client, False) + properties["time_24"] = now.strftime("%H:%M") + properties["full_time_24"] = now.strftime("%H:%M:%S") + properties["time_12"] = now.strftime("%I:%M %p") + properties["full_time_12"] = now.strftime("%I:%M:%S %p") + properties["short_date"] = now.strftime("%a %d %b") + properties["full_date"] = now.strftime("%A %d %B") + properties["date"] = g15locale.format_date(now, self.gconf_client) + properties["locale_date"] = now.strftime("%x") + properties["locale_time"] = now.strftime("%X") + properties["year"] = now.strftime("%Y") + properties["short_year"] = now.strftime("%y") + properties["week"] = now.strftime("%W") + properties["month"] = now.strftime("%m") + properties["month_name"] = now.strftime("%B") + properties["short_month_name"] = now.strftime("%b") + properties["day_name"] = now.strftime("%A") + properties["short_day_name"] = now.strftime("%a") + properties["day_of_year"] = now.strftime("%d") + properties["cal_year"] = calendar_date.strftime("%Y") + properties["cal_month"] = calendar_date.strftime("%m") + properties["cal_month_name"] = calendar_date.strftime("%B") + properties["cal_short_month_name"] = calendar_date.strftime("%b") + properties["cal_year"] = calendar_date.strftime("%Y") + properties["cal_short_year"] = calendar_date.strftime("%y") + properties["cal_locale_date"] = calendar_date.strftime("%x") + if self._event_days is None or not str(calendar_date.day) in self._event_days: + properties["message"] = "No events" + properties["events"] = False + else: + properties["events"] = True + properties["message"] = "" + return properties + + def _load_month_events(self, now): + self._event_days = {} + + for c in self._page.get_children(): + if isinstance(c, Cell): + pass + + # Get all the events for this month + for acc in self._account_manager.accounts: + try: + backend = get_backend(acc.type) + if backend is None: + logger.warning("Could not find a calendar backend for %s", acc.name) + else: + # Backends may specify if they need a network or not, so check the state + import gnome15.g15pluginmanager as g15pluginmanager + needs_net = g15pluginmanager.is_needs_network(backend) + if not needs_net or ( needs_net and self.screen.service.network_manager.is_network_available() ): + backend_events = backend.create_backend(acc, self._account_manager).get_events(now) + if backend_events is None: + logger.warning("Calendar returned no events, skipping") + else: + self._event_days = dict(self._event_days.items() + \ + backend_events.items()) + else: + logger.warning("Skipping backend %s because it requires the network, " \ + "and the network is not availabe", acc.type) + except Exception as e: + logger.warning("Failed to load events for account %s.", acc.name, exc_info = e) + + g15screen.run_on_redraw(self._rebuild_components, now) + self._page.mark_dirty() + + def _rebuild_components(self, now): + self._menu.remove_all_children() + if str(now.day) in self._event_days: + events = self._event_days[str(now.day)] + i = 0 + for event in events: + self._menu.add_child(EventMenuItem(self, event, "menuItem-%d" % i)) + i += 1 + + # Add the date cell components + self._calendar.remove_all_children() + cal = calendar.Calendar() + i = 0 + for day in cal.itermonthdates(now.year, now.month): + event = None + if str(day.day) in self._event_days: + event = self._event_days[str(day.day)][0] + self._calendar.add_child(Cell(day, now, event, "cell-%d" % i)) + i += 1 + + self._page.mark_dirty() + self._page.redraw() + + def _schedule_redraw(self): + if self.screen.is_visible(self._page): + if self._timer is not None: + self._timer.cancel() + + """ + Because the calendar page also displays a clock, we want to + redraw at second zero of every minute + """ + self._timer = g15scheduler.schedule("CalRedraw", 60 - time.gmtime().tm_sec, self._redraw) + + def _on_shown(self): + self._hidden = False + self._redraw() + + def _on_hidden(self): + if self._timer != None: + self._timer.cancel() + + def _redraw(self): + t = time.time() + if t > self._loaded + REFRESH_INTERVAL: + self._loaded = t + self._reload_events_now() + else: + self._page.mark_dirty() + self.screen.redraw(self._page) + self._schedule_redraw() + + def _reload_events_now(self): + self._load_month_events(self._get_calendar_date()) + self.screen.redraw(self._page) + self._schedule_redraw() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._page != None and self._thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + diff --git a/src/plugins/cal/cal.ui b/src/plugins/cal/cal.ui new file mode 100644 index 0000000..4dc12e8 --- /dev/null +++ b/src/plugins/cal/cal.ui @@ -0,0 +1,31 @@ + + + + + + False + + + True + False + + + Use 24hr format for times + True + True + False + True + + + False + True + 0 + + + + + + + + + diff --git a/src/plugins/cal/default/Makefile.am b/src/plugins/cal/default/Makefile.am new file mode 100644 index 0000000..7cf5275 --- /dev/null +++ b/src/plugins/cal/default/Makefile.am @@ -0,0 +1,29 @@ +themedir = $(datadir)/gnome15/plugins/cal/default +theme_DATA = default.svg \ + default-cell.svg \ + default-menu-entry.svg \ + g19.svg \ + g19-cell.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + done diff --git a/src/plugins/cal/default/default-cell.svg b/src/plugins/cal/default/default-cell.svg new file mode 100644 index 0000000..725094f --- /dev/null +++ b/src/plugins/cal/default/default-cell.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day} + ${day} + + diff --git a/src/plugins/cal/default/default-menu-entry.svg b/src/plugins/cal/default/default-menu-entry.svg new file mode 100644 index 0000000..870c7fd --- /dev/null +++ b/src/plugins/cal/default/default-menu-entry.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_alt} + ${item_name} + + + + ${item_alt} + + ${item_name} + + diff --git a/src/plugins/cal/default/default.svg b/src/plugins/cal/default/default.svg new file mode 100644 index 0000000..f45f9eb --- /dev/null +++ b/src/plugins/cal/default/default.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${cal_locale_date} + + + ${message} + + + M + + + + T + + + + W + + + + T + + + + F + + + + S + + + + S + + + + + + + + + diff --git a/src/plugins/cal/default/g19-cell.svg b/src/plugins/cal/default/g19-cell.svg new file mode 100644 index 0000000..7364818 --- /dev/null +++ b/src/plugins/cal/default/g19-cell.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${day} + + + diff --git a/src/plugins/cal/default/g19-menu-entry.svg b/src/plugins/cal/default/g19-menu-entry.svg new file mode 100644 index 0000000..649418a --- /dev/null +++ b/src/plugins/cal/default/g19-menu-entry.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + ${item_name} + ${item_alt} + + + + diff --git a/src/plugins/cal/default/g19.svg b/src/plugins/cal/default/g19.svg new file mode 100644 index 0000000..70bf768 --- /dev/null +++ b/src/plugins/cal/default/g19.svg @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${time} + ${date} + + + + Sa + + Su + Mo + + Tu + + We + + Th + + Fr + + _(Events) + + + + + + + + + + + + + ${title} + + ${cal_month_name} ${cal_year} + ${message} + + + + + + + + _(Today) + + + + + _(Events/Calendar) + + diff --git a/src/plugins/cal/i18n/cal.en_GB.po b/src/plugins/cal/i18n/cal.en_GB.po new file mode 100644 index 0000000..eff8634 --- /dev/null +++ b/src/plugins/cal/i18n/cal.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: cal.py:37 +msgid "Calendar" +msgstr "Calendar" + +#: cal.py:38 +msgid "Calendar. Integrates with Evolution calendar." +msgstr "Calendar. Integrates with Evolution calendar." + +#: cal.py:40 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: cal.py:44 +msgid "Previous day/Event" +msgstr "Previous day/Event" + +#: cal.py:45 +msgid "Next day/Event" +msgstr "Next day/Event" + +#: cal.py:46 +msgid "" +"Toggle between calendar\n" +"and events" +msgstr "" +"Toggle between calendar\n" +"and events" + +#: cal.py:47 +msgid "Next week" +msgstr "Next week" + +#: cal.py:48 +msgid "Previous week" +msgstr "Previous week" diff --git a/src/plugins/cal/i18n/cal.pot b/src/plugins/cal/i18n/cal.pot new file mode 100644 index 0000000..fa5b148 --- /dev/null +++ b/src/plugins/cal/i18n/cal.pot @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: cal.py:37 +msgid "Calendar" +msgstr "" + +#: cal.py:38 +msgid "Calendar. Integrates with Evolution calendar." +msgstr "" + +#: cal.py:40 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: cal.py:44 +msgid "Previous day/Event" +msgstr "" + +#: cal.py:45 +msgid "Next day/Event" +msgstr "" + +#: cal.py:46 +msgid "" +"Toggle between calendar\n" +"and events" +msgstr "" + +#: cal.py:47 +msgid "Next week" +msgstr "" + +#: cal.py:48 +msgid "Previous week" +msgstr "" diff --git a/src/plugins/clock/Makefile.am b/src/plugins/clock/Makefile.am new file mode 100644 index 0000000..1bec669 --- /dev/null +++ b/src/plugins/clock/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/clock +plugin_DATA = clock.ui \ + clock.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/clock/clock.py b/src/plugins/clock/clock.py new file mode 100644 index 0000000..6ea893f --- /dev/null +++ b/src/plugins/clock/clock.py @@ -0,0 +1,366 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("clock", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import datetime +import gtk +import pango +import os +import locale + +# Plugin details - All of these must be provided +id="clock" +name=_("Clock") +description=_("Just displays a simple clock. This is the plugin used in \ +the tutorial at the Gnome15 site.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# +# This simple plugin displays a digital clock. It also demonstrates +# how to add a preferences dialog for your plugin +# + +''' +This function must create your plugin instance. You are provided with +a GConf client and a Key prefix to use if your plugin has preferences +''' +def create(gconf_key, gconf_client, screen): + return G15Clock(gconf_key, gconf_client, screen) + +''' +This function must be provided if you set has_preferences to True. You +should display a dialog for editing the plugins preferences +''' +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "clock.ui")) + + dialog = widget_tree.get_object("ClockDialog") + dialog.set_transient_for(parent) + + display_seconds = widget_tree.get_object("DisplaySecondsCheckbox") + display_seconds.set_active(gconf_client.get_bool(gconf_key + "/display_seconds")) + display_seconds.connect("toggled", _changed, gconf_key + "/display_seconds", gconf_client) + + display_date = widget_tree.get_object("DisplayDateCheckbox") + display_date.set_active(gconf_client.get_bool(gconf_key + "/display_date")) + display_date.connect("toggled", _changed, gconf_key + "/display_date", gconf_client) + + use_24hr_format = widget_tree.get_object("TwentFourHourCheckbox") + use_24hr_format.set_active(gconf_client.get_bool(gconf_key + "/use_24hr_format")) + use_24hr_format.connect("toggled", _changed, gconf_key + "/use_24hr_format", gconf_client) + + dialog.run() + dialog.hide() + +def _changed(widget, key, gconf_client): + ''' + gconf configuration has changed, redraw our canvas + ''' + gconf_client.set_bool(key, widget.get_active()) + +class G15Clock(g15plugin.G15Plugin): + ''' + You would normally want to extend at least g15plugin.G15Plugin as it + provides basic plugin functions. + + There are also further specialisations, such as g15plugin.G15PagePlugin + for plugins that have display a page, or g15plugin.G15MenuPlugin for + menu like plugins, or g15plugin.G15RefreshingPlugin for plugins that + refresh their view based on a timer. + + This example uses the most basic type to demonstrate how plugins are put + together, but it could easily use G15RefreshingPlugin and cut out a lot + of code. + + ''' + + + ''' + ****************************************************************** + * Lifecycle functions. You must provide activate and deactivate, * + * the constructor and destroy function are optional * + ****************************************************************** + ''' + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.hidden = False + self.page = None + + def activate(self): + ''' + The activate function is invoked when gnome15 starts up, or the plugin is re-enabled + after it has been disabled. When extending any of the provided base plugin classes, + you nearly always want to call the function in the supoer class as well + ''' + g15plugin.G15Plugin.activate(self) + + + ''' + Load our configuration + ''' + self.timer = None + self._load_configuration() + + ''' + We will be drawing text manually in the thumbnail, so it is recommended you use the + G15Text class which simplifies drawing and measuring text in an efficient manner + ''' + self.text = g15text.new_text(self.screen) + + ''' + Most plugins will delegate their drawing to a 'Theme'. A theme usually consists of an SVG file, one + for each model that is supported, and optionally a fragment of Python for anything that can't + be done with SVG and the built in theme facilities + ''' + self._reload_theme() + + ''' + Most plugins will usually want to draw on the screen. To do so, a 'page' is created. We also supply a callback here to + perform the painting. You can also supply 'on_shown' and 'on_hidden' callbacks here to be notified when your + page actually gets shown and hidden. + + A thumbnail painter function is also provided. This is used by other plugins want a thumbnail representation + of the current screen. For example, this could be used in the 'panel', or the 'menu' plugins + ''' + self.page = g15theme.G15Page("Clock", self.screen, + theme_properties_callback = self._get_properties, + thumbnail_painter = self.paint_thumbnail, panel_painter = self.paint_thumbnail, + theme = self.theme, + originating_plugin = self) + self.page.title = "Simple Clock" + + ''' + Add the page to the screen + ''' + self.screen.add_page(self.page) + + ''' + Once created, we should always ask for the screen to be drawn (even if another higher + priority screen is actually active. If the canvas is not displayed immediately, + the on_shown function will be invoked when it finally is. + ''' + self.screen.redraw(self.page) + + ''' + As this is a Clock, we want to redraw at fixed intervals. So, schedule another redraw + if appropriate + ''' + self._schedule_redraw() + + ''' + We want to be notified when the plugin configuration changed, so watch for gconf events. + The watch function is used, as this will automatically track the monitor handles + and clean them up when the plugin is deactivated + ''' + self.watch(None, self._config_changed) + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + + ''' + Stop updating + ''' + if self.timer != None: + self.timer.cancel() + self.timer = None + + ''' + Deactivation occurs when either the plugin is disabled, or the applet is stopped + On deactivate, we must remove our canvas. + ''' + self.screen.del_page(self.page) + + def destroy(self): + ''' + Invoked when the plugin is disabled or the applet is stopped + ''' + pass + + ''' + ************************************************************** + * Common callback functions. For example, your plugin is more* + * than likely to want to draw something on the LCD. Naming * + * the function paint() is the convention * + ************************************************************** + ''' + + ''' + Paint the thumbnail. You are given the MAXIMUM amount of space that is allocated for + the thumbnail, and you must return the amount of space actually take up. Thumbnails + can be used for example by the panel plugin, or the menu plugin. If you want to + support monochrome devices such as the G15, you will have to take into account + the amount of space you have (i.e. 6 pixels high maximum and limited width) + ''' + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page and not self.screen.is_visible(self.page): + properties = self._get_properties() + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + text = properties["time"] + if self.display_seconds: + text = text[:-3] + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + if self.display_date: + text = "%s\n%s" % ( properties["time"],properties["date"] ) + font_size = allocated_size / 3 + else: + text = properties["time"] + font_size = allocated_size / 2 + x = 4 + gap = 8 + + self.text.set_canvas(canvas) + self.text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, \ + font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _config_changed(self, client, connection_id, entry, args): + + ''' + Load the gconf configuration + ''' + self._load_configuration() + + ''' + This is called when the gconf configuration changes. See add_notify and remove_notify in + the plugin's activate and deactive functions. + ''' + + ''' + Reload the theme as the layout required may have changed (i.e. with the 'show date' + option has been change) + ''' + self._reload_theme() + self.page.set_theme(self.theme) + + ''' + In this case, we temporarily raise the priority of the page. This will force + the page to be painted (i.e. the paint function invoked). After the specified time, + the page will revert it's priority. Only one revert timer is active at any one time, + so it is safe to call this function in quick succession + ''' + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + + ''' + Schedule a redraw as well + ''' + if self.timer is not None: + self.timer.cancel() + self._redraw() + + def _load_configuration(self): + self.display_date = self.gconf_client.get_bool(self.gconf_key + "/display_date") + self.display_seconds = self.gconf_client.get_bool(self.gconf_key + "/display_seconds") + self.use_24hr_format = self.gconf_client.get_bool(self.gconf_key + "/use_24hr_format") + + def _redraw(self): + ''' + Invoked by the timer once a second to redraw the screen. If your page is currently activem + then the paint() functions will now get called. When done, we want to schedule the next + redraw + ''' + self.screen.redraw(self.page) + self._schedule_redraw() + + def _schedule_redraw(self): + if not self.active: + return + + ''' + Determine when to schedule the next redraw for. + ''' + now = datetime.datetime.now() + if self.display_seconds: + next_tick = now + datetime.timedelta(0, 1.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, int(next_tick.second)) + else: + next_tick = now + datetime.timedelta(0, 60.0) + next_tick = datetime.datetime(next_tick.year,next_tick.month,next_tick.day,next_tick.hour, next_tick.minute, 0) + delay = g15pythonlang.total_seconds( next_tick - now ) + + ''' + Try not to create threads or timers if possible. Use g15scheduler.schedule) instead + ''' + self.timer = g15scheduler.schedule("ClockRedraw", delay, self._redraw) + + def _reload_theme(self): + variant = None + if self.display_date: + variant = "with-date" + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), variant) + + ''' + Get the properties dictionary + ''' + def _get_properties(self): + properties = { } + + ''' + Get the details to display and place them as properties which are passed to + the theme + ''' + now = datetime.datetime.now() + if self.use_24hr_format: + properties["time"] = g15locale.format_time_24hour(now, self.gconf_client, self.display_seconds) + else: + properties["time"] = g15locale.format_time(now, self.gconf_client, self.display_seconds) + if self.display_date: + properties["date"] = g15locale.format_date(now, self.gconf_client) + + return properties diff --git a/src/plugins/clock/clock.ui b/src/plugins/clock/clock.ui new file mode 100644 index 0000000..07a2b43 --- /dev/null +++ b/src/plugins/clock/clock.ui @@ -0,0 +1,106 @@ + + + + + + 320 + False + 5 + Clock Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + Show Seconds + True + True + False + True + + + True + True + 0 + + + + + Show Date + True + True + False + True + + + True + True + 1 + + + + + Show Time in 24 hour format + True + True + False + 0.52999997138977051 + True + + + True + True + 2 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/clock/default/Makefile.am b/src/plugins/clock/default/Makefile.am new file mode 100644 index 0000000..1ca9a78 --- /dev/null +++ b/src/plugins/clock/default/Makefile.am @@ -0,0 +1,29 @@ +themedir = $(datadir)/gnome15/plugins/clock/default +theme_DATA = default.svg \ + default-with-date.svg \ + g19.svg \ + mx5500.svg \ + mx5500-with-date.svg \ + g19-with-date.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + done diff --git a/src/plugins/clock/default/default-with-date.svg b/src/plugins/clock/default/default-with-date.svg new file mode 100644 index 0000000..6ff4c4d --- /dev/null +++ b/src/plugins/clock/default/default-with-date.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + ${date} + + diff --git a/src/plugins/clock/default/default.svg b/src/plugins/clock/default/default.svg new file mode 100644 index 0000000..694767a --- /dev/null +++ b/src/plugins/clock/default/default.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/clock/default/g19-with-date.svg b/src/plugins/clock/default/g19-with-date.svg new file mode 100644 index 0000000..b4387fb --- /dev/null +++ b/src/plugins/clock/default/g19-with-date.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${date} + + ${time} + + diff --git a/src/plugins/clock/default/g19.svg b/src/plugins/clock/default/g19.svg new file mode 100644 index 0000000..3b70445 --- /dev/null +++ b/src/plugins/clock/default/g19.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${time} + + diff --git a/src/plugins/clock/default/mx5500-with-date.svg b/src/plugins/clock/default/mx5500-with-date.svg new file mode 100644 index 0000000..48a615f --- /dev/null +++ b/src/plugins/clock/default/mx5500-with-date.svg @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + ${date} + + + diff --git a/src/plugins/clock/default/mx5500.svg b/src/plugins/clock/default/mx5500.svg new file mode 100644 index 0000000..aa71e50 --- /dev/null +++ b/src/plugins/clock/default/mx5500.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/clock/i18n/clock.en_GB.po b/src/plugins/clock/i18n/clock.en_GB.po new file mode 100644 index 0000000..259b8d7 --- /dev/null +++ b/src/plugins/clock/i18n/clock.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "Show Seconds" diff --git a/src/plugins/clock/i18n/clock.glade.h b/src/plugins/clock/i18n/clock.glade.h new file mode 100644 index 0000000..3b37ec3 --- /dev/null +++ b/src/plugins/clock/i18n/clock.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Clock Preferences"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); diff --git a/src/plugins/clock/i18n/clock.pot b/src/plugins/clock/i18n/clock.pot new file mode 100644 index 0000000..7d6561c --- /dev/null +++ b/src/plugins/clock/i18n/clock.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "" diff --git a/src/plugins/debug/Makefile.am b/src/plugins/debug/Makefile.am new file mode 100644 index 0000000..b1b9e8d --- /dev/null +++ b/src/plugins/debug/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/debug +plugin_DATA = debug.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/debug/debug.py b/src/plugins/debug/debug.py new file mode 100644 index 0000000..f21867c --- /dev/null +++ b/src/plugins/debug/debug.py @@ -0,0 +1,473 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("clock", modfile = __file__).ugettext + +import gnome15.g15text as g15text +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15plugin as g15plugin +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import pango +import os +import sys +import traceback +import gc +import gnome15.objgraph as objgraph +import gnome15.g15logging as g15logging +import dbus.service + +# Logging +import logging +logger = logging.getLogger(__name__) + +id="debug" +name=_("Debug") +description=_("Displays some information useful for debugging Gnome15.\n \ +Also adds additional DBUS functions to inspect internals") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +single_instance=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +_proc_status = '/proc/%d/status' % os.getpid() + +_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0, + 'KB': 1024.0, 'MB': 1024.0*1024.0} + +DEBUG_NAME="/org/gnome15/Debug" +DEBUG_IF_NAME="org.gnome15.Debug" +EXCLUDED = [ + "instancemethod", + "weakref", + "instance", + "method_descriptor", + "member_descriptor", + "frame", + "function", + "intdict", + "builtin_function_or_method", + "builtin_function_or_method", + ] + +class intdict(dict): + + def __init__(self): + dict.__init__(self) + +class Snapshot(): + + def __init__(self): + self.stats = intdict() + self.objects = intdict() + +def referents_count(typename): + print "%d instances of type %s. Referents :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + for o in gc.get_referents(r): + name = _get_key(o) + if name != "type" and name != typename and not name in EXCLUDED and not name in done: + done[name] = True + count = objgraph.count(name) + if count > 1: + print " %s (%d)" % ( name, count ) + +def referents(typename, max_depth = 1): + print "%d instances of type %s. Referents :-" % ( objgraph.count(typename), typename) + for r in objgraph.by_type(typename): + _do_referents(r, 1, max_depth) + +def _do_referents(r, depth, max_depth = 1): + dep = "" + for _ in range(0, depth): + dep += " " + for o in gc.get_referents(r): + if not _get_key(o) in EXCLUDED: + if isinstance(o, dict): + print "%s%s" % (dep, _max_len(o, 120)) + if depth < max_depth: + _do_referents(o, depth + 1) + +def referrers(typename, max_depth = 1): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + for r in objgraph.by_type(typename): + _do_referrers(r, 1, max_depth, []) + +def _do_referrers(r, depth, max_depth, done): + dep = "" + for _ in range(0, depth): + dep += " " + l = gc.get_referrers(r) + for o in l: + if not o == done and not o == l and not _get_key(o) in EXCLUDED and not o in done: + print "%s%s" % (dep, _max_len(o, 120)) + done.append(o) + if depth < max_depth: + _do_referrers(o, depth + 1, max_depth, done) + +def referrers_count(typename): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + for o in gc.get_referrers(r): + name = _get_key(o) + if name != "type" and name != typename and not name in EXCLUDED and not name in done: + done[name] = True + count = objgraph.count(name) + if count > 1: + print " %s (%d)" % ( name, count ) + +def take_snapshot(snap_objects = True): + snapshot = Snapshot() + for o in gc.get_objects(): + k = _get_key(o) + if not k in EXCLUDED: + snapshot.stats.setdefault(k, 0) + snapshot.stats[k] += 1 + if snap_objects: + snapshot.objects.setdefault(k, []) + snapshot.objects[k].append(o) + return snapshot + +def compare_snapshots(snapshot1, snapshot2, show_removed = True): + new_types = [] + changed_types = [] + removed_types = [] + + # Find everything that has been removed or changed + for k, v in snapshot1.stats.iteritems(): + if not k in snapshot2.stats: + removed_types.append(k) + else: + if v != snapshot2.stats[k]: + changed_types.append(k) + + # Find everything that has been added + for k, v in snapshot2.stats.iteritems(): + if not k in snapshot1.stats: + new_types.append(k) + + # Print some stuff + print "New types" + _do_types(snapshot1, snapshot2, new_types) + + if show_removed: + print "Removed types" + for k in removed_types: + print " %-30s" % k + + # Find the actual objects that have been added for those that have changed + print "Changed types" + _do_types(snapshot1, snapshot2, changed_types, show_removed) + +def _get_key(o): + if isinstance(o, object): + try: + return o.__class__.__name__ + except: + return type(o).__name__ + else: + return type(o).__name__ + +def _do_types(snapshot1, snapshot2, types, show_removed = True): + for k in types: + print "%4s%-30s %10d (was %d)" % ("",k, snapshot2.stats[k], snapshot1.stats[k] if k in snapshot1.stats else 0) + old_objects = snapshot1.objects[k] if k in snapshot1.objects else [] + new_objects = snapshot2.objects[k] if k in snapshot2.objects else [] + + # Find any objects removed + removed = 0 + if show_removed: + for x in old_objects: + in_new = False + try: + in_new = x in new_objects + except: + pass + if not in_new: + removed += 1 + try : + _do_obj(x, "Removed") + except Exception as e: + print "%12sError! - %s" % ( "", _max_len(str(e), 80) ) + + # Find any objects added + added = 0 + for x in new_objects: + in_old = False + try: + in_old = x in old_objects + except: + pass + if not in_old: + added += 1 + try : + _do_obj(x, "Added") + except: + print "%12sError! - %s" % ( "", _max_len(str(e), 80) ) + + if added > 0 or removed > 0: + print "%4sAdded %d, Removed %d" % ("", added, removed ) + + + +def _do_obj(o, s): + if isinstance(o, list) and len(o) > 0: + # Ignore the list if it contains excluded items + if _get_key(o[0]) in EXCLUDED: + return + elif isinstance(o, dict): + for k, v in dict(o).iteritems(): + if _get_key(k) in EXCLUDED or _get_key(v) in EXCLUDED: + return + break + elif isinstance(o, tuple) and len(o) > 0: + # Ignore the list if it contains excluded items + for v in o: + if _get_key(v) in EXCLUDED: + return + + o_str = _max_len(o, 60) + print "%12s%8s : %-30s %-60s" % ("",s, _get_key(o), o_str) + +def _max_len(o, l): + o_str = str(o) + if len(o_str) > l: + o_str = o_str[:l] + return o_str + +class G15DBUSDebugService(dbus.service.Object): + + def __init__(self, dbus_service): + dbus.service.Object.__init__(self, dbus_service._bus_name, DEBUG_NAME) + self._service = dbus_service._service + self._snapshot1 = None + + @dbus.service.method(DEBUG_IF_NAME) + def Snapshot(self): + logger.info("Collecting garbage") + gc.collect() + logger.info("Collected garbage") + logger.info("Taking snapshot") + _snapshot2 = take_snapshot(False) + logger.info("Taken snapshot") + if self._snapshot1 is not None: + compare_snapshots(self._snapshot1, _snapshot2, show_removed = True) + else: + logger.info("FIRST snapshot taken, take another") + self._snapshot1 = _snapshot2 + + @dbus.service.method(DEBUG_IF_NAME) + def GC(self): + logger.info("Collecting garbage") + gc.collect() + logger.info("Collected garbage") + + @dbus.service.method(DEBUG_IF_NAME) + def ToggleDebugSVG(self): + g15theme.DEBUG_SVG = not g15theme.DEBUG_SVG + + @dbus.service.method(DEBUG_IF_NAME) + def MostCommonTypes(self): + print "Most used objects" + print "-----------------" + print + objgraph.show_most_common_types(limit=200) + print "Job Queues" + print "----------" + print + g15scheduler.scheduler.print_all_jobs() + print "Threads" + print "-------" + for threadId, stack in sys._current_frames().items(): + print "ThreadID: %s" % threadId + for filename, lineno, name, line in traceback.extract_stack(stack): + print ' File: "%s", line %d, in %s' % (filename, lineno, name) + + @dbus.service.method(DEBUG_IF_NAME) + def ShowGraph(self): + objgraph.show_refs(self._service) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def PluginObject(self, m): + for scr in self._service.screens: + print "Screen %s" % scr.device.uid + for p in scr.plugins.plugin_map: + pmod = scr.plugins.plugin_map[p] + if m == '' or m == pmod.id: + print " %s" % pmod.id + objgraph.show_backrefs(p, filename='%s-%s' %(scr.device.uid, pmod.id)) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Objects(self, typename): + print "%d instances of type %s. Referrers :-" % ( objgraph.count(typename), typename) + done = {} + for r in objgraph.by_type(typename): + if isinstance(r, list): + print "%s" % str(r[:min(20, len(r))]) + else: + print "%s" % str(r) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def SetDebugLevel(self, log_level): + logger = logging.getLogger() + logger.setLevel(g15logging.get_level(log_level)) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Referrers(self, typename): + referrers(typename, 1) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def ReferrersCount(self, typename): + referrers_count(typename) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def Referents(self, typename): + referents(typename) + + @dbus.service.method(DEBUG_IF_NAME, in_signature='s') + def ReferentsCount(self, typename): + referents_count(typename) + +def _VmB(VmKey): + '''Private. + ''' + global _proc_status, _scale + # get pseudo file /proc//status + try: + t = open(_proc_status) + v = t.read() + t.close() + except: + return 0.0 # non-Linux? + # get VmKey line e.g. 'VmRSS: 9999 kB\n ...' + i = v.index(VmKey) + v = v[i:].split(None, 3) # whitespace + if len(v) < 3: + return 0.0 # invalid format? + # convert Vm value to bytes + return float(v[1]) * _scale[v[2]] + + +def memory(since=0.0): + '''Return memory usage in bytes. + ''' + return _VmB('VmSize:') - since + + +def resident(since=0.0): + '''Return resident memory usage in bytes. + ''' + return _VmB('VmRSS:') - since + + +def stacksize(since=0.0): + '''Return stack size in bytes. + ''' + return _VmB('VmStk:') - since + +def create(gconf_key, gconf_client, screen): + return G15Debug(gconf_key, gconf_client, screen) + +class G15Debug(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, ["dialog-error"], id, name, refresh_interval = 1.0) + + def activate(self): + self._debug_service = G15DBUSDebugService(self.screen.service.dbus_service) + self.text = g15text.new_text(self.screen) + self.memory = 0 + self.resident = 0 + self.stack = 0 + self.only_refresh_when_visible = False + g15plugin.G15RefreshingPlugin.activate(self) + self.do_refresh() + + def deactivate(self): + self._silently_remove_from_connector(self._debug_service) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def refresh(self): + self.memory = memory() + self.resident = resident() + self.stack = stacksize() + + def get_theme_properties(self): + properties = g15plugin.G15RefreshingPlugin.get_theme_properties(self) + properties["memory_b"] = "%f" % self.memory + properties["memory_k"] = "%f" % ( self.memory / 1024 ) + properties["memory_mb"] = "%.2f" % ( self.memory / 1024 / 1024 ) + properties["memory_gb"] = "%.2f" % ( self.memory / 1024 / 1024 / 1024 ) + properties["resident_b"] = "%f" % self.resident + properties["resident_k"] = "%f" % ( self.resident / 1024 ) + properties["resident_mb"] = "%.2f" % ( self.resident / 1024 / 1024 ) + properties["resident_gb"] = "%.2f" % ( self.memory / 1024 / 1024 / 1024 ) + properties["stack_b"] = "%f" % self.stack + properties["stack_k"] = "%f" % ( self.stack / 1024 ) + properties["stack_mb"] = "%.2f" % ( self.stack / 1024 / 1024 ) + properties["stack_gb"] = "%.2f" % ( self.stack / 1024 / 1024 / 1024 ) + return properties + + def _silently_remove_from_connector(self, obj): + try: + obj.remove_from_connection() + except Exception: + pass + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page and not self.screen.is_visible(self.page): + # Don't display the date or seconds on mono displays, not enough room as it is + mem_mb = self.memory / 1024 / 1024 + res_mb = self.resident / 1024 / 1024 + if self.screen.driver.get_bpp() == 1: + text = "%.2f %.2f" % ( mem_mb, res_mb ) + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + x = 1 + gap = 1 + else: + factor = 1 if horizontal else 2 + font_name = "Sans" + text = "%.2f MiB\n%.2f MiB" % ( mem_mb, res_mb ) + font_size = allocated_size / 3 + x = 4 + gap = 8 + + self.text.set_canvas(canvas) + self.text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, \ + font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self.text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self.text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + diff --git a/src/plugins/debug/default/Makefile.am b/src/plugins/debug/default/Makefile.am new file mode 100644 index 0000000..8f18692 --- /dev/null +++ b/src/plugins/debug/default/Makefile.am @@ -0,0 +1,25 @@ +themedir = $(datadir)/gnome15/plugins/debug/default +theme_DATA = default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/debug/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/debug/default/i18n; \ + done diff --git a/src/plugins/debug/default/default.svg b/src/plugins/debug/default/default.svg new file mode 100644 index 0000000..2685f16 --- /dev/null +++ b/src/plugins/debug/default/default.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + Memory + Resident + Stack + ${memory_mb} MiB + ${resident_mb} MiB + ${stack_mb} MiB + + diff --git a/src/plugins/debug/default/g19.svg b/src/plugins/debug/default/g19.svg new file mode 100644 index 0000000..6843f8c --- /dev/null +++ b/src/plugins/debug/default/g19.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + Memory: + ${memory_mb} MiB + Resident: + ${resident_mb} MiB + Stack: + ${stack_mb} MiB + + diff --git a/src/plugins/debug/i18n/clock.en_GB.po b/src/plugins/debug/i18n/clock.en_GB.po new file mode 100644 index 0000000..259b8d7 --- /dev/null +++ b/src/plugins/debug/i18n/clock.en_GB.po @@ -0,0 +1,30 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "Clock Preferences" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "Show Date" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "Show Seconds" diff --git a/src/plugins/debug/i18n/clock.glade.h b/src/plugins/debug/i18n/clock.glade.h new file mode 100644 index 0000000..3b37ec3 --- /dev/null +++ b/src/plugins/debug/i18n/clock.glade.h @@ -0,0 +1,3 @@ +char *s = N_("Clock Preferences"); +char *s = N_("Show Date"); +char *s = N_("Show Seconds"); diff --git a/src/plugins/debug/i18n/clock.pot b/src/plugins/debug/i18n/clock.pot new file mode 100644 index 0000000..7d6561c --- /dev/null +++ b/src/plugins/debug/i18n/clock.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/clock.glade.h:1 +msgid "Clock Preferences" +msgstr "" + +#: i18n/clock.glade.h:2 +msgid "Show Date" +msgstr "" + +#: i18n/clock.glade.h:3 +msgid "Show Seconds" +msgstr "" diff --git a/src/plugins/display/Makefile.am b/src/plugins/display/Makefile.am new file mode 100644 index 0000000..a02c839 --- /dev/null +++ b/src/plugins/display/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/display +plugin_DATA = display.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/display/display.py b/src/plugins/display/display.py new file mode 100644 index 0000000..3f56389 --- /dev/null +++ b/src/plugins/display/display.py @@ -0,0 +1,186 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("profiles", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15icontools as g15icontools +import logging +import os +import re +logger = logging.getLogger(__name__) + +ICONS = [ "display", "gnome-display-properties", "system-config-display", "video-display", "xfce4-display", "display-capplet" ] + +# Custom actions +SELECT_PROFILE = "select-profile" + +# Register the action with all supported models +g15devices.g15_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.z10_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="display" +name=_("Display Resolution") +description=_("Allows selection of the resolution for your display.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Select resolution") + } + +def create(gconf_key, gconf_client, screen): + return G15XRandR(gconf_client, gconf_key, screen) + +""" +Represents a resolution as a single item in a menu +""" +class ResolutionMenuItem(g15theme.MenuItem): + def __init__(self, index, size, refresh_rate, plugin, id, display): + g15theme.MenuItem.__init__(self, id, group = False) + self.current = False + self.size = size + self.index = index + self.refresh_rate = refresh_rate + self._plugin = plugin + self.display = display + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = "%s x %s @ %s" % ( self.size[0], self.size[1], self.refresh_rate) + item_properties["item_radio"] = True + item_properties["item_radio_selected"] = self.current + item_properties["item_alt"] = "" + return item_properties + + def activate(self): + os.system("xrandr --auto --output %s --mode %sx%s -r %s" % (self.display, self.size[0], self.size[1], self.refresh_rate )) + self._plugin._reload_menu() + + +""" +XRANDR plugin class +""" +class G15XRandR(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ICONS, id, _("Display")) + + def activate(self): + self._current_active = None + self._last_items = -1 + g15plugin.G15MenuPlugin.activate(self) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + + def load_menu_items(self): + items = [] + display = "Default" + i = 0 + status, output = self._get_status_output("xrandr") + if status == 0: + old_active = self._current_active + new_active = [] + for line in output.split('\n'): + arr = re.findall(r'\S+', line) + if line.startswith(" "): + size = self._parse_size(arr[0]) + for a in range(1, len(arr)): + word = arr[a] + refresh_string = ''.join( c for c in word if c not in '*+' ) + if len(refresh_string) > 0: + refresh_rate = float(refresh_string) + i += 1 + item = ResolutionMenuItem(i, size, refresh_rate, self, "profile-%d-%s" % ( i, refresh_rate ), display ) + item.current = "*" in word + items.append(item) + if item.current: + new_active.append(item) + elif "connected" in line: + i += 1 + display = arr[0] + item = g15theme.MenuItem("display-%s" % i, True, arr[0], activatable=False, icon = g15icontools.get_icon_path(ICONS)) + items.append(item) + + + if len(items) != self._last_items or old_active is None or self._differs(new_active, old_active): + self.menu.set_children(items) + self._last_items = len(items) + + if new_active is not None: + self.menu.set_selected_item(new_active[0]) + self._current_active = new_active + + self._schedule_check() + else: + raise Exception("Failed to query XRandR. Is the xrandr command installed, and do you have the XRandR extension enabled in your X configuration?") + + ''' + Private + ''' + def _differs(self, old_active, new_active): + if ( old_active is None and new_active is not None ) or \ + ( old_active is not None and new_active is None ): + return True + if old_active is not None: + it = iter(old_active) + try: + for i in new_active: + if i.id != next(it).id: + return True + except StopIteration: + return True + + def _schedule_check(self): + if self.active == True: + g15scheduler.schedule("CheckResolution", 10.0, self.load_menu_items) + + def _parse_size(self, line): + arr = line.split("x") + return int(arr[0].strip()), int(arr[1].strip()) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _get_item_for_current_resolution(self): + return g15pythonlang.find(lambda m: m.current, self.menu.get_children()) + + def _get_status_output(self, cmd): + # TODO something like this is used in sense.py as well, make it a utility + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text \ No newline at end of file diff --git a/src/plugins/fx/Makefile.am b/src/plugins/fx/Makefile.am new file mode 100644 index 0000000..6dd75c4 --- /dev/null +++ b/src/plugins/fx/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/fx +plugin_DATA = fx.ui \ + fx.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/fx/fx.py b/src/plugins/fx/fx.py new file mode 100644 index 0000000..bf10c07 --- /dev/null +++ b/src/plugins/fx/fx.py @@ -0,0 +1,207 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("fx", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os +import time +import cairo +import random + + +# Plugin details - All of these must be provided +id="fx" +name=_("Special Effect") +description=_("This plugin introduces special effects when switching between screens. \ +Currently 3 main types of effect are provided, a sliding effect (in both directions) \ +a fading effect and a zoom effect . On a monochrome LCD such as the G15's, the fade appears as more \ +of a 'disolve' effect.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Fx(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "fx.ui")) + dialog = widget_tree.get_object("FxDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/transition_effect", "TransitionCombo", "random", widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/anim_speed", "AnimationSpeedAdjustment", 5.0, widget_tree) + dialog.run() + dialog.hide() + +effects = [ "vertical-scroll", "horizontal-scroll", "fade", "zoom" ] + +class G15Fx(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.chained_transition =self.screen.set_transition(self.transition) + self.notify_handler = self.gconf_client.notify_add(self.gconf_key, self.config_changed) + + def deactivate(self): + self.gconf_client.notify_remove(self.notify_handler) + self.screen.set_transition(self.chained_transition) + + def destroy(self): + pass + + ''' Callbacks + ''' + + def config_changed(self, client, connection_id, entry, args): + self.screen.redraw() + + + def transition(self, old_surface, new_surface, old_page, new_page, direction="up"): + # Determine effect to use + effect = self.gconf_client.get_string(self.gconf_key + "/transition_effect") + if effect == "": + effect = "random" + if effect == "random": + effect = effects[int(random.random() * len(effects))] + + # Animation speed + speed_entry = self.gconf_client.get(self.gconf_key + "/anim_speed") + speed = 5.0 if speed_entry == None else speed_entry.get_float() + + # Don't transition for high priority screens + if new_page == None or old_page == None or new_page.priority == g15screen.PRI_HIGH: + return + + width = self.screen.width + height = self.screen.height + + # Create a working surface + img_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, self.screen.width, self.screen.height) + img_context = cairo.Context(img_surface) + if effect == "vertical-scroll": + # Vertical scroll + step = max( int(speed), 1 ) + if direction == "down": + for i in range(0, self.screen.height, step): + img_context.save() + img_context.translate(0, -i) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.translate(0, self.screen.height) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(0, self.screen.height, step): + img_context.save() + img_context.translate(0, -(self.screen.height - i)) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.translate(0, self.screen.height) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + + elif effect == "horizontal-scroll": + # Horizontal scroll + step = max( ( width / height ) * speed, 1 ) + if direction == "down": + for i in range(0, self.screen.width, int(step)): + img_context.save() + img_context.translate(-i, 0) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.translate(self.screen.width, 0) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(0, self.screen.width, int(step)): + img_context.save() + img_context.translate(-(self.screen.width - i), 0) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.translate(self.screen.width, 0) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + elif effect == "fade": + step = max( int(speed), 1 ) + for i in range(0, 256, step): + img_context.set_source_surface(new_surface) + img_context.paint_with_alpha(float(i) / 256.0) + img_context.set_source_surface(old_surface) + img_context.paint_with_alpha(1.0 - ( float(i) / 256.0 ) ) + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + elif effect == "zoom": + step = max( int(speed), 1 ) + if direction == "down": + for i in range(1, self.screen.width, step): + img_context.save() + img_context.set_source_surface(old_surface) + img_context.paint() + scale = i / float(self.screen.width) + scaled_width = self.screen.width * scale + scaled_height = self.screen.height * scale + img_context.translate( ( self.screen.width - scaled_width) / 2, ( self.screen.height - scaled_height) / 2) + img_context.scale(scale, scale) + img_context.set_source_surface(new_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + else: + for i in range(self.screen.width, 0, step * -1): + img_context.save() + img_context.set_source_surface(new_surface) + img_context.paint() + scale = i / float(self.screen.width) + scaled_width = self.screen.width * scale + scaled_height = self.screen.height * scale + img_context.translate( ( self.screen.width - scaled_width) / 2, ( self.screen.height - scaled_height) / 2) + img_context.scale(scale, scale) + img_context.set_source_surface(old_surface) + img_context.paint() + img_context.restore() + self.screen.driver.paint(img_surface) + self.anim_delay(speed) + + if self.chained_transition != None: + self.chained_transition(old_surface, new_surface, old_page, new_page, direction) + + def anim_delay(self, speed): + if speed < 1.0: + time.sleep( ( 1.0 - speed ) / 50.0 ) \ No newline at end of file diff --git a/src/plugins/fx/fx.ui b/src/plugins/fx/fx.ui new file mode 100644 index 0000000..ed809bd --- /dev/null +++ b/src/plugins/fx/fx.ui @@ -0,0 +1,192 @@ + + + + + + 50 + 1 + 10 + 5 + + + + + + + + + + + horizontal-scroll + Horizontal Slide + + + vertical-scroll + Vertical Slide + + + fade + Fade + + + zoom + Zoom + + + random + Random + + + none + None + + + + + 320 + False + 5 + Special Effects Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + TransitionModel + + + + 1 + + + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Animation Speed + + + False + False + 0 + + + + + True + True + AnimationSpeedAdjustment + + + True + True + 1 + + + + + True + True + 4 + 1 + + + + + + + + + True + False + <b>Transitions</b> + True + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/fx/i18n/fx.en_GB.po b/src/plugins/fx/i18n/fx.en_GB.po new file mode 100644 index 0000000..1152e79 --- /dev/null +++ b/src/plugins/fx/i18n/fx.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/fx.glade.h:1 +msgid "Transitions" +msgstr "Transitions" + +#: i18n/fx.glade.h:2 +msgid "Animation Speed" +msgstr "Animation Speed" + +#: i18n/fx.glade.h:3 +msgid "Fade" +msgstr "Fade" + +#: i18n/fx.glade.h:4 +msgid "Horizontal Slide" +msgstr "Horizontal Slide" + +#: i18n/fx.glade.h:5 +msgid "None" +msgstr "None" + +#: i18n/fx.glade.h:6 +msgid "Random" +msgstr "Random" + +#: i18n/fx.glade.h:7 +msgid "Special Effects Preferences" +msgstr "Special Effects Preferences" + +#: i18n/fx.glade.h:8 +msgid "Vertical Slide" +msgstr "Vertical Slide" + +#: i18n/fx.glade.h:9 +msgid "Zoom" +msgstr "Zoom" diff --git a/src/plugins/fx/i18n/fx.glade.h b/src/plugins/fx/i18n/fx.glade.h new file mode 100644 index 0000000..ef9412c --- /dev/null +++ b/src/plugins/fx/i18n/fx.glade.h @@ -0,0 +1,9 @@ +char *s = N_("Transitions"); +char *s = N_("Animation Speed"); +char *s = N_("Fade"); +char *s = N_("Horizontal Slide"); +char *s = N_("None"); +char *s = N_("Random"); +char *s = N_("Special Effects Preferences"); +char *s = N_("Vertical Slide"); +char *s = N_("Zoom"); diff --git a/src/plugins/fx/i18n/fx.pot b/src/plugins/fx/i18n/fx.pot new file mode 100644 index 0000000..8b2e12c --- /dev/null +++ b/src/plugins/fx/i18n/fx.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/fx.glade.h:1 +msgid "Transitions" +msgstr "" + +#: i18n/fx.glade.h:2 +msgid "Animation Speed" +msgstr "" + +#: i18n/fx.glade.h:3 +msgid "Fade" +msgstr "" + +#: i18n/fx.glade.h:4 +msgid "Horizontal Slide" +msgstr "" + +#: i18n/fx.glade.h:5 +msgid "None" +msgstr "" + +#: i18n/fx.glade.h:6 +msgid "Random" +msgstr "" + +#: i18n/fx.glade.h:7 +msgid "Special Effects Preferences" +msgstr "" + +#: i18n/fx.glade.h:8 +msgid "Vertical Slide" +msgstr "" + +#: i18n/fx.glade.h:9 +msgid "Zoom" +msgstr "" diff --git a/src/plugins/g15daemon-server/Makefile.am b/src/plugins/g15daemon-server/Makefile.am new file mode 100644 index 0000000..a97dc96 --- /dev/null +++ b/src/plugins/g15daemon-server/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/g15daemon-server +plugin_DATA = g15daemon-server.ui \ + g15daemon-server.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/g15daemon-server/g15daemon-server.py b/src/plugins/g15daemon-server/g15daemon-server.py new file mode 100644 index 0000000..af9dedc --- /dev/null +++ b/src/plugins/g15daemon-server/g15daemon-server.py @@ -0,0 +1,493 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from threading import Thread +from PIL import Image +from PIL import ImageMath +from PIL import ImageOps +import array +import asyncore +import cairo +import gnome15.g15driver as g15driver +import gnome15.g15locale as g15locale +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gobject +import gtk +import logging +import os +import socket +import struct +import sys +_ = g15locale.get_translation("g15daemon-server", modfile = __file__).ugettext + +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="g15daemon-server" +name=_("G15Daemon Compatibility") +description=_("Starts a network server compatible with the g15daemon network protocol. \ +This allows you to use g15daemon compatible scripts and applications on all \ +models supported by Gnome15, including the G19. Note, if you are using \ +a real g15daemon server, you will configure this plugin to use a different \ +port.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +needs_network=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# Client commands +CLIENT_CMD_GET_KEYSTATE=ord('k') +CLIENT_CMD_SWITCH_PRIORITIES=ord('p') +CLIENT_CMD_NEVER_SELECT=ord('n') +CLIENT_CMD_IS_FOREGROUND=ord('v') +CLIENT_CMD_IS_USER_SELECTED=ord('u') +CLIENT_CMD_KB_BACKLIGHT_COLOR=0x79 +CLIENT_CMD_BACKLIGHT=0x80 +CLIENT_CMD_KB_BACKLIGHT=0x8 +CLIENT_CMD_CONTRAST=0x40 +CLIENT_CMD_MKEY_LIGHTS=0x20 + + +KEY_MAP = { + g15driver.G_KEY_G1 : 1<<0, + g15driver.G_KEY_G2 : 1<<1, + g15driver.G_KEY_G3 : 1<<2, + g15driver.G_KEY_G4 : 1<<3, + g15driver.G_KEY_G5 : 1<<4, + g15driver.G_KEY_G6 : 1<<5, + g15driver.G_KEY_G7 : 1<<6, + g15driver.G_KEY_G8 : 1<<7, + g15driver.G_KEY_G9 : 1<<8, + g15driver.G_KEY_G10 : 1<<9, + g15driver.G_KEY_G11 : 1<<10, + g15driver.G_KEY_G12 : 1<<11, + + g15driver.G_KEY_M1 : 1<<18, + g15driver.G_KEY_M2 : 1<<19, + g15driver.G_KEY_M3 : 1<<20, + g15driver.G_KEY_MR : 1<<21, + + # L1-L5 + g15driver.G_KEY_L1 : 1<<22, + g15driver.G_KEY_L2 : 1<<23, + g15driver.G_KEY_L3 : 1<<24, + g15driver.G_KEY_L4 : 1<<25, + g15driver.G_KEY_L5 : 1<<26, + + # Map to L1-L5 + g15driver.G_KEY_UP : 1<<22, + g15driver.G_KEY_LEFT : 1<<23, + g15driver.G_KEY_OK : 1<<24, + g15driver.G_KEY_RIGHT : 1<<25, + g15driver.G_KEY_DOWN : 1<<26, + + g15driver.G_KEY_LIGHT : 1<<27 + } + +def create(gconf_key, gconf_client, screen): + return G15DaemonServer(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "g15daemon-server.ui")) + + dialog = widget_tree.get_object("G15DaemonServerDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/port", "PortAdjustment", 15550, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/keep_aspect_ratio", "KeepAspectRatio", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/take_over_macro_keys", "TakeOverMacroKeys", True, widget_tree, True) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/use_custom_foreground", "UseCustomForeground", False, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/custom_foreground", "CustomForeground", ( 255, 255, 255 ), widget_tree) + + dialog.run() + dialog.hide() + +class G15DaemonClient(asyncore.dispatcher): + def __init__(self, conn, plugin): + asyncore.dispatcher.__init__(self, sock=conn) + self.out_buffer = "" + self.img_buffer = None + self.buffer_type = None + self.buffer_len = 0 + self.surface = None + self.last_img_buffer = None + self.enable_keys = False + self.plugin = plugin + self.plugin.join(self) + self.handshake = False + self.keyboard_backlight_value = None + self.backlight_value = None + + self.page = g15theme.G15Page("G15Daemon%d" % self.plugin.screen_index, plugin.screen, painter = self._paint, on_shown = self._on_shown, on_hidden = self._on_hidden, originating_plugin = self) + self.page.set_title(_("G15Daemon Screen %d") % self.plugin.screen_index) + self.plugin.screen.add_page(self.page) + self.plugin.screen_index += 1 + self.plugin.screen.redraw(self.page) + self.backlight_acquire = None + self.keyboard_backlight_acquire = None + + self.out_buffer += "G15 daemon HELLO" + self.oob_buffer = "" + + def handle_close(self): + self.plugin.screen.del_page(self.page) + self.plugin.leave(self) + self.close() + + def handle_expt(self): + data = self.socket.recv(1, socket.MSG_OOB) + val = ord(data[0]) + + if val == CLIENT_CMD_SWITCH_PRIORITIES: + self.plugin.screen.raise_page(self.page) + elif val == CLIENT_CMD_NEVER_SELECT: + self.plugin.screen.set_priority(self, self.page, g15screen.PRI_LOW) + elif val == CLIENT_CMD_IS_FOREGROUND: + self.oob_buffer += "1" if self.plugin.screen.get_visible_page() == self.page else "0" + elif val == CLIENT_CMD_IS_USER_SELECTED: + self.oob_buffer += "1" if self.plugin.screen.get_visible_page() == self.page and self.page.priority == g15screen.PRI_NORMAL else "0" + elif val & CLIENT_CMD_MKEY_LIGHTS > 0: + self.screen.driver.set_value(val - CLIENT_CMD_MKEY_LIGHTS) + elif val & CLIENT_CMD_KEY_HANDLER > 0: + # TODO - the semantics are slightly different here. gnome15 is already grabbing the keyboard, always. + # So instead, we just only send keyboard events if the client requests this. + self.enable_keys = True + elif val & CLIENT_CMD_BACKLIGHT: + level = val - CLIENT_CMD_BACKLIGHT + if isinstance(self.plugin.default_backlight, int): + # Others + bl = level + else: + # G19 + if level == 0: + bl = 0 + elif level == 1: + bl = self.plugin.default_lcd_brightness / 2 + elif level == 2: + bl = self.plugin.default_lcd_brightness + if self.backlight_acquire: + self.backlight_value = bl + self.backlight_acquire.set_value(bl) + else: + logger.warning("g15daemon client requested backlight be changed, but there is no backlight to change") + elif val & CLIENT_CMD_KB_BACKLIGHT: + level = val - CLIENT_CMD_KB_BACKLIGHT + + if isinstance(self.plugin.default_backlight, int): + # Others + bl = level + else: + # G19 + if level == 0: + bl = (0, 0, 0) + elif level == 1: + bl = ( self.plugin.default_backlight[0] / 2, self.plugin.default_backlight[1] / 2, self.plugin.default_backlight[2] / 2 ) + elif level == 2: + bl = self.plugin.default_backlight + + if self.keyboard_backlight_acquire: + self.keyboard_backlight_value = bl + self.keyboard_backlight_acquire.set_value(bl) + else: + logger.warning("g15daemon client requested keyboard backlight be changed, but there is no backlight to change") + elif val & CLIENT_CMD_CONTRAST: + logger.warning("Ignoring contrast command") + + + def handle_read(self): + if not self.handshake: + buf_type = self.recv(4) + + self.buffer_type = buf_type[0] + self.handshake = True + self.img_buffer = "" + + if self.buffer_type == "G": + self.buffer_len = 6880 + elif self.buffer_type == "R": + self.buffer_len = 1048 +# TODO +# elif self.buffer_type == "W": +# self.buffer_len = 865 + else: + logger.warning("WARNING: Unsupported buffer type. Closing") + self.handle_close() + return + else: + recv = self.recv(self.buffer_len - len(self.img_buffer)) + self.img_buffer += recv + if len(self.img_buffer) == self.buffer_len: + if self.buffer_type == "G": + self.img_buffer = self.convert_gbuf(self.img_buffer) + self.draw_buffer(self.img_buffer) + self.last_img_buffer = self.img_buffer + self.img_buffer = "" + + elif len(self.img_buffer) > self.buffer_len: + logger.warning("Received bad frame (%d bytes), should be %d", + len(self.img_buffer), + self.buffer_len) + + def draw_buffer(self, img_buffer): + + pil_img = Image.fromstring("1", (160,43), img_buffer) + mask_img = pil_img.copy() + mask_img = mask_img.convert("L") + pil_img = pil_img.convert("P") + if self.plugin.palette is not None: + pil_img.putpalette(self.plugin.palette) + pil_img = pil_img.convert("RGBA") + pil_img.putalpha(mask_img) + + # TODO find a quicker way of converting + pixbuf = g15cairo.image_to_pixbuf(pil_img, "png") + self.surface = g15cairo.pixbuf_to_surface(pixbuf) + self.plugin.screen.redraw(self.page) + + def dump_buf(self, buf): + i = 0 + for y in range(43): + l = "" + for x in range(160): + if buf[ ( y * 160 ) + x ] == chr(0): + l += " " + else: + l += "*" + logger.info(l) + + def convert_gbuf(self, g_buffer): + r_buffer = array.array('c', chr(0)*1048) + new_buf = "" + for x in range(160): + for y in range(43): + pixel_offset = y * 160 + x + byte_offset = pixel_offset / 8 + bit_offset = 7 - (pixel_offset % 8) + val = ord(g_buffer[x + ( y * 160)]) + if val: + r_buffer[byte_offset] = chr(ord(r_buffer[byte_offset]) | 1 << bit_offset) + else: + r_buffer[byte_offset] = chr(ord(r_buffer[byte_offset]) & ~(1 << bit_offset)) + return r_buffer.tostring() + + def convert_rbuf(self, buffer): + new_buf = "" + for x in range(160): + for y in range(43): + pixel_offset = y * 143 + x + byte_offset = pixel_offset / 8 + bit_offset = 7 - (pixel_offset % 8) + ch = ord(buffer[byte_offset]) + new_buf += chr( ( ch >> bit_offset ) & 0xfe ) + + return new_buf + + def writable(self): + return len(self.out_buffer) > 0 + + def handle_write(self): + if len(self.out_buffer) > 0: + sent = self.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] + + if len(self.oob_buffer) > 0: + sent = self.socket.send(self.oob_buffer, socket.MSG_OOB) + self.oob_buffer = self.oob_buffer[sent:] + + def handle_key(self, keys, state): + val = 0 + for key in keys: + if key in KEY_MAP: + val += KEY_MAP[key] + self.out_buffer += struct.pack(" G15 key") + + """ + Private + """ + + def _on_hidden(self): + if self.keyboard_backlight_acquire: + self.plugin.screen.driver.release_control(self.keyboard_backlight_acquire) + if self.backlight_acquire: + self.plugin.screen.driver.release_control(self.backlight_acquire) + + def _on_shown(self): + self.backlight_control = self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if self.backlight_control: + if self.backlight_value is None: + self.backlight_value = self.backlight_control.value + self.backlight_acquire = self.plugin.screen.driver.acquire_control(self.backlight_control, val = self.backlight_value) + self.keyboard_backlight_control = self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_SHADEABLE) + if self.keyboard_backlight_control: + if self.keyboard_backlight_value is None: + self.keyboard_backlight_value = self.keyboard_backlight_control.value + self.keyboard_backlight_acquire = self.plugin.screen.driver.acquire_control(self.keyboard_backlight_control, val = self.keyboard_backlight_value) + + def _paint(self, canvas): + if self.surface != None: + size = self.plugin.screen.driver.get_size() + + if self.plugin.gconf_client.get_bool(self.plugin.gconf_key + "/keep_aspect_ratio"): + canvas.translate(0.0, 77.0) + canvas.scale(2.0, 2.0) + else: + canvas.scale(float(size[0]) / 160, float(size[1]) / 43) + #canvas.scale(2.0, 3.0) + canvas.set_source_surface(self.surface) + canvas.paint() + +class G15Async(Thread): + def __init__(self): + Thread.__init__(self) + self.name = "G15Async" + self.setDaemon(True) + + def run(self): + try : + asyncore.loop(timeout=0.05) + except Exception as e: + logger.warning("Failed to connect to G15Daemon client", exc_info = e) + +class G15DaemonServer(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.daemon = None + + def activate(self): + self.screen_index = 0 + self.clients = [] + self.screen.driver.control_update_listeners.append(self) + self.load_configuration() + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed); + self.daemon = G15Daemon(self._get_port(), self) + self.async = G15Async() + self.async.start() + + def deactivate(self): + self._stop_all_clients() + self.daemon.close() + if self in self.screen.driver.control_update_listeners: + self.screen.driver.control_update_listeners.remove(self) + self.gconf_client.notify_remove(self.notify_handle); + self.daemon = None + + def destroy(self): + pass + + def control_updated(self, control): + if control.id == "foreground": + self.load_configuration() + self.screen.redraw() + + def _get_port(self): + port_entry = self.gconf_client.get(self.gconf_key + "/port") + return 15550 if port_entry == None else port_entry.get_int() + + def _config_changed(self, client, connection_id, entry, args): + self.load_configuration() + self.screen.redraw() + port = self._get_port() + if self.daemon == None or self.daemon.port != port: + if self.daemon != None: + logger.warning("Port changed to %d (will restart daemon - clients may have to be reconnected manually", port) + self._stop_all_clients() + self.daemon.close() + self.daemon = G15Daemon(port, self) + self.async = G15Async() + self.async.start() + else: + for c in self.clients: + if c.last_img_buffer is not None: + c.draw_buffer(c.last_img_buffer) + + def _stop_all_clients(self): + for c in self.clients: + c.handle_close() + + def load_configuration(self): + self.take_over_macro_keys = g15gconf.get_bool_or_default(self.gconf_client, "%s/take_over_macro_keys" % self.gconf_key, True) + + if g15gconf.get_bool_or_default(self.gconf_client, "%s/use_custom_foreground" % self.gconf_key, False): + col = g15gconf.get_rgb_or_default(self.gconf_client, "%s/custom_foreground" % self.gconf_key, (255,255,255)) + self.palette = [0 for n in range(768)] + self.palette[765] = col[0] + self.palette[766] = col[1] + self.palette[767] = col[2] + else: + foreground_control = self.screen.driver.get_control("foreground") + if foreground_control is None: + self.palette = None + else: + self.palette = [0 for n in range(768)] + self.palette[765] = foreground_control.value[0] + self.palette[766] = foreground_control.value[1] + self.palette[767] = foreground_control.value[2] + + backlight_control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + self.default_backlight = backlight_control.value if backlight_control is not None else None + + lcd_brightness_control = self.screen.driver.get_control_for_hint(g15driver.HINT_SHADEABLE) + self.default_lcd_brightness = lcd_brightness_control.value if lcd_brightness_control is not None else None + + def leave(self, client): + if client in self.clients: + self.clients.remove(client) + + def join(self, client): + self.clients.append(client) + + def handle_key(self, keys, state, post): + if ( not post and self.take_over_macro_keys ) or ( post and not self.take_over_macro_keys ): + visible = self.screen.get_visible_page() + for client in self.clients: + if client.enable_keys and client.page == visible: + client.handle_key(keys, state) + return True + +class G15Daemon(asyncore.dispatcher): + def __init__(self, port, plugin): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + logger.info('Binding to port %d', port) + self.bind(("127.0.0.1", port)) + logger.info('Bound to port %d', port) + self.listen(5) + self.plugin = plugin + self.port = port + + def handle_accept(self): + sock, addr = self.accept() + logger.debug('Got client') + client = G15DaemonClient(sock, self.plugin) + + def handle_sig_term(self, arg0, arg1): + self.close() \ No newline at end of file diff --git a/src/plugins/g15daemon-server/g15daemon-server.ui b/src/plugins/g15daemon-server/g15daemon-server.ui new file mode 100644 index 0000000..5473549 --- /dev/null +++ b/src/plugins/g15daemon-server/g15daemon-server.ui @@ -0,0 +1,176 @@ + + + + + + 320 + False + 5 + G15Daemon Compatibility Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + 4 + + + True + False + Port + + + False + False + 0 + + + + + True + True + + False + False + True + True + PortAdjustment + + + True + True + 1 + + + + + True + True + 0 + + + + + Keep Aspect Ratio + True + True + False + True + + + True + True + 1 + + + + + Take over all macro keys when active + True + True + False + True + + + True + True + 2 + + + + + True + False + + + Use custom foreground colour + True + True + False + True + + + True + True + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 3 + + + + + False + False + 1 + + + + + + button9 + + + + 65535 + 1 + 10 + + diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po b/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po new file mode 100644 index 0000000..7f97ca7 --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.en_GB.po @@ -0,0 +1,30 @@ +# English translations for g package. +# Copyright (C) 2012 THE g'S COPYRIGHT HOLDER +# This file is distributed under the same license as the g package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: g 15daemon-server\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g15daemon-server.glade.h:1 +msgid "G15Daemon Compatibility Preferences" +msgstr "G15Daemon Compatibility Preferences" + +#: i18n/g15daemon-server.glade.h:2 +msgid "Keep Aspect Ratio" +msgstr "Keep Aspect Ratio" + +#: i18n/g15daemon-server.glade.h:3 +msgid "Port" +msgstr "Port" diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h b/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h new file mode 100644 index 0000000..ccd64fb --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.glade.h @@ -0,0 +1,3 @@ +char *s = N_("G15Daemon Compatibility Preferences"); +char *s = N_("Keep Aspect Ratio"); +char *s = N_("Port"); diff --git a/src/plugins/g15daemon-server/i18n/g15daemon-server.pot b/src/plugins/g15daemon-server/i18n/g15daemon-server.pot new file mode 100644 index 0000000..5d2366f --- /dev/null +++ b/src/plugins/g15daemon-server/i18n/g15daemon-server.pot @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g15daemon-server.glade.h:1 +msgid "G15Daemon Compatibility Preferences" +msgstr "" + +#: i18n/g15daemon-server.glade.h:2 +msgid "Keep Aspect Ratio" +msgstr "" + +#: i18n/g15daemon-server.glade.h:3 +msgid "Port" +msgstr "" diff --git a/src/plugins/game-nexuiz/Makefile.am b/src/plugins/game-nexuiz/Makefile.am new file mode 100644 index 0000000..fd449aa --- /dev/null +++ b/src/plugins/game-nexuiz/Makefile.am @@ -0,0 +1,28 @@ +SUBDIRS = default resources + +plugindir = $(datadir)/gnome15/plugins/game-nexuiz +plugin_DATA = game-nexuiz.py \ + game-nexuiz.g13.macros \ + game-nexuiz.g19.macros + +EXTRA_DIST = \ + $(plugin_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/i18n; \ + done diff --git a/src/plugins/game-nexuiz/default/Makefile.am b/src/plugins/game-nexuiz/default/Makefile.am new file mode 100644 index 0000000..6632b53 --- /dev/null +++ b/src/plugins/game-nexuiz/default/Makefile.am @@ -0,0 +1,25 @@ +themedir = $(datadir)/gnome15/plugins/game-nexuiz/default +theme_DATA = default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/clock/default/i18n; \ + done diff --git a/src/plugins/game-nexuiz/default/default.svg b/src/plugins/game-nexuiz/default/default.svg new file mode 100644 index 0000000..24e158f --- /dev/null +++ b/src/plugins/game-nexuiz/default/default.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${time} + + diff --git a/src/plugins/game-nexuiz/default/g19.svg b/src/plugins/game-nexuiz/default/g19.svg new file mode 100644 index 0000000..abc16d1 --- /dev/null +++ b/src/plugins/game-nexuiz/default/g19.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + 0 + + diff --git a/src/plugins/game-nexuiz/game-nexuiz.g13.macros b/src/plugins/game-nexuiz/game-nexuiz.g13.macros new file mode 100644 index 0000000..71c8b02 --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.g13.macros @@ -0,0 +1,122 @@ +[DEFAULT] +name = Nexuiz +icon = resources/icon.png +window_name = +base_profile = 0 +activate_on_focus = False +send_delays = False +fixed_delays = False +press_delay = 50 +release_delay = 50 +background = +author = Brett Smith +version = 1.0 +models = g13 + +[LAUNCH] +activate_on_launch = True +pattern = 'nexuiz'|'/.*/nexuiz' +monitor = 'stdout' + +[m1] +keys_g4_name = W +keys_g4_mappedkey = KEY_W +keys_g4_maptype = keyboard +keys_g4_type = mapped-to-key +keys_g10_name = A +keys_g10_mappedkey = KEY_A +keys_g10_maptype = keyboard +keys_g10_type = mapped-to-key +keys_g11_name = S +keys_g11_mappedkey = KEY_S +keys_g11_maptype = keyboard +keys_g11_type = mapped-to-key +keys_g12_name = D +keys_g12_mappedkey = KEY_D +keys_g12_maptype = keyboard +keys_g12_type = mapped-to-key +keys_g22_name = Jump +keys_g22_mappedkey = KEY_SPACE +keys_g22_maptype = keyboard +keys_g22_type = mapped-to-key +keys_right_name = Right +keys_right_mappedkey = KEY_RIGHT +keys_right_maptype = keyboard +keys_right_type = mapped-to-key +keys_up_name = Up +keys_up_mappedkey = KEY_UP +keys_up_maptype = keyboard +keys_up_type = mapped-to-key +keys_down_name = Down +keys_down_mappedkey = KEY_DOWN +keys_down_maptype = keyboard +keys_down_type = mapped-to-key +keys_g15_name = Crouch +keys_g15_mappedkey = KEY_LEFTCTRL +keys_g15_maptype = keyboard +keys_g15_type = mapped-to-key +keys_jd_name = Tab +keys_jd_mappedkey = KEY_SLASH +keys_jd_maptype = keyboard +keys_jd_type = mapped-to-key +keys_jl_name = Space +keys_jl_mappedkey = KEY_SPACE +keys_jl_maptype = keyboard +keys_jl_type = mapped-to-key +keys_g20_name = Land +keys_g20_mappedkey = KEY_LEFTSHIFT +keys_g20_maptype = keyboard +keys_g20_type = mapped-to-key +keys_g7_name = Escape +keys_g7_type = mapped-to-key +keys_left_name = Left +keys_left_mappedkey = KEY_LEFT +keys_left_maptype = keyboard +keys_left_type = mapped-to-key +keys_jc_name = Enter +keys_jc_mappedkey = KEY_ENTER +keys_jc_maptype = keyboard +keys_jc_type = mapped-to-key +keys_g7_maptype = keyboard +keys_g7_mappedkey = KEY_1 +keys_g5_name = Use +keys_g5_type = mapped-to-key +keys_g5_maptype = keyboard +keys_g5_mappedkey = KEY_E +keys_g5_repeatdelay = -1.0 +keys_g5_repeatmode = held +keys_g4_repeatdelay = -1.0 +keys_g4_repeatmode = held +keys_g7_repeatdelay = -1.0 +keys_g7_repeatmode = held +keys_g10_repeatdelay = -1.0 +keys_g10_repeatmode = held +keys_g11_repeatdelay = -1.0 +keys_g11_repeatmode = held +keys_g12_repeatdelay = -1.0 +keys_g12_repeatmode = held +keys_g15_repeatdelay = -1.0 +keys_g15_repeatmode = held +keys_g20_repeatdelay = -1.0 +keys_g20_repeatmode = held +keys_g22_repeatdelay = -1.0 +keys_g22_repeatmode = held +keys_right_repeatdelay = -1.0 +keys_right_repeatmode = held +keys_up_repeatdelay = -1.0 +keys_up_repeatmode = held +keys_down_repeatdelay = -1.0 +keys_down_repeatmode = held +keys_jd_repeatdelay = -1.0 +keys_jd_repeatmode = held +keys_jl_repeatdelay = -1.0 +keys_jl_repeatmode = held +keys_left_repeatdelay = -1.0 +keys_left_repeatmode = held +keys_jc_repeatdelay = -1.0 +keys_jc_repeatmode = held + +[m2] + +[m3] + diff --git a/src/plugins/game-nexuiz/game-nexuiz.g19.macros b/src/plugins/game-nexuiz/game-nexuiz.g19.macros new file mode 100644 index 0000000..9662295 --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.g19.macros @@ -0,0 +1,122 @@ +[DEFAULT] +name = Nexuiz +icon = resources/icon.png +window_name = +base_profile = 0 +activate_on_focus = False +send_delays = False +fixed_delays = False +press_delay = 50 +release_delay = 50 +background = resources/g19-background.jpg +author = Brett Smith +version = 1.0 +models = g19 + +[LAUNCH] +activate_on_launch = True +pattern = 'nexuiz'|'/.*/nexuiz' +monitor = 'stdout' + +[m1] +keys_g4_name = W +keys_g4_mappedkey = KEY_W +keys_g4_maptype = keyboard +keys_g4_type = mapped-to-key +keys_g10_name = A +keys_g10_mappedkey = KEY_A +keys_g10_maptype = keyboard +keys_g10_type = mapped-to-key +keys_g11_name = S +keys_g11_mappedkey = KEY_S +keys_g11_maptype = keyboard +keys_g11_type = mapped-to-key +keys_g12_name = D +keys_g12_mappedkey = KEY_D +keys_g12_maptype = keyboard +keys_g12_type = mapped-to-key +keys_g22_name = Jump +keys_g22_mappedkey = KEY_SPACE +keys_g22_maptype = keyboard +keys_g22_type = mapped-to-key +keys_right_name = Right +keys_right_mappedkey = KEY_RIGHT +keys_right_maptype = keyboard +keys_right_type = mapped-to-key +keys_up_name = Up +keys_up_mappedkey = KEY_UP +keys_up_maptype = keyboard +keys_up_type = mapped-to-key +keys_down_name = Down +keys_down_mappedkey = KEY_DOWN +keys_down_maptype = keyboard +keys_down_type = mapped-to-key +keys_g15_name = Crouch +keys_g15_mappedkey = KEY_LEFTCTRL +keys_g15_maptype = keyboard +keys_g15_type = mapped-to-key +keys_jd_name = Tab +keys_jd_mappedkey = KEY_SLASH +keys_jd_maptype = keyboard +keys_jd_type = mapped-to-key +keys_jl_name = Space +keys_jl_mappedkey = KEY_SPACE +keys_jl_maptype = keyboard +keys_jl_type = mapped-to-key +keys_g20_name = Land +keys_g20_mappedkey = KEY_LEFTSHIFT +keys_g20_maptype = keyboard +keys_g20_type = mapped-to-key +keys_g7_name = Escape +keys_g7_type = mapped-to-key +keys_left_name = Left +keys_left_mappedkey = KEY_LEFT +keys_left_maptype = keyboard +keys_left_type = mapped-to-key +keys_jc_name = Enter +keys_jc_mappedkey = KEY_ENTER +keys_jc_maptype = keyboard +keys_jc_type = mapped-to-key +keys_g7_maptype = keyboard +keys_g7_mappedkey = KEY_1 +keys_g5_name = Use +keys_g5_type = mapped-to-key +keys_g5_maptype = keyboard +keys_g5_mappedkey = KEY_E +keys_g5_repeatdelay = -1.0 +keys_g5_repeatmode = held +keys_g4_repeatdelay = -1.0 +keys_g4_repeatmode = held +keys_g7_repeatdelay = -1.0 +keys_g7_repeatmode = held +keys_g10_repeatdelay = -1.0 +keys_g10_repeatmode = held +keys_g11_repeatdelay = -1.0 +keys_g11_repeatmode = held +keys_g12_repeatdelay = -1.0 +keys_g12_repeatmode = held +keys_g15_repeatdelay = -1.0 +keys_g15_repeatmode = held +keys_g20_repeatdelay = -1.0 +keys_g20_repeatmode = held +keys_g22_repeatdelay = -1.0 +keys_g22_repeatmode = held +keys_right_repeatdelay = -1.0 +keys_right_repeatmode = held +keys_up_repeatdelay = -1.0 +keys_up_repeatmode = held +keys_down_repeatdelay = -1.0 +keys_down_repeatmode = held +keys_jd_repeatdelay = -1.0 +keys_jd_repeatmode = held +keys_jl_repeatdelay = -1.0 +keys_jl_repeatmode = held +keys_left_repeatdelay = -1.0 +keys_left_repeatmode = held +keys_jc_repeatdelay = -1.0 +keys_jc_repeatmode = held + +[m2] + +[m3] + diff --git a/src/plugins/game-nexuiz/game-nexuiz.py b/src/plugins/game-nexuiz/game-nexuiz.py new file mode 100644 index 0000000..a16112a --- /dev/null +++ b/src/plugins/game-nexuiz/game-nexuiz.py @@ -0,0 +1,79 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15profile as g15profile +import os +import locale + +# Plugin details - All of these must be provided +id="game-nexuiz" +name=_("Nexuiz") +description=_("Gaming plugin for Nexuiz") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +""" +Register this as a location for profiles +""" +g15profile.add_profile_dir(os.path.dirname(__file__)) + +def create(gconf_key, gconf_client, screen): + return GameNexuiz(gconf_key, gconf_client, screen) + +class GameNexuiz(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._page = None + + def activate(self): + self._reload_theme() + self._page = g15theme.G15Page("Nexuiz", self._screen, + theme_properties_callback = self._get_properties, + theme = self._theme, + originating_plugin = self) + self._page.title = "Nexuiz" + self._screen.add_page(self._page) + self._redraw() + + # Add the right profile for the model + macro_file = os.path.join(os.path.dirname(__file__), "game-nexuiz.%s.macros") + if os.path.exists(macro_file): + profile = g15profile.G15Profile(self._screen.device, file_path = macro_file) + g15profiles.add_profile(profile) + + def deactivate(self): + self._screen.del_page(self._page) + + def destroy(self): + pass + + def _redraw(self): + self._screen.redraw(self._page) + + def _reload_theme(self): + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), None) + + def _get_properties(self): + properties = { } + return properties diff --git a/src/plugins/game-nexuiz/resources/Makefile.am b/src/plugins/game-nexuiz/resources/Makefile.am new file mode 100644 index 0000000..28739dc --- /dev/null +++ b/src/plugins/game-nexuiz/resources/Makefile.am @@ -0,0 +1,8 @@ +resourcesdir = $(datadir)/gnome15/plugins/game-nexuiz/resources +resources_DATA = \ + g19-background.jpg \ + icon.png + +EXTRA_DIST = \ + $(resources_DATA) + diff --git a/src/plugins/game-nexuiz/resources/g19-background.jpg b/src/plugins/game-nexuiz/resources/g19-background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..26a2eb7bd232846e5de19813da56e49d7c3bfa3e GIT binary patch literal 32075 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/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/cal/default/i18n; \ + done diff --git a/src/plugins/google-analytics/default/default.svg b/src/plugins/google-analytics/default/default.svg new file mode 100644 index 0000000..f661645 --- /dev/null +++ b/src/plugins/google-analytics/default/default.svg @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + ${alt_title} + + Visit: + ${visits} + Views: + ${views} + Uniq : + ${unique} + Pg/Vst: + ${pagesVisit} + Av.Dur: + ${avgDuration} + Bounce: + ${bounce}% + + diff --git a/src/plugins/google-analytics/default/g19.svg b/src/plugins/google-analytics/default/g19.svg new file mode 100644 index 0000000..048ee4d --- /dev/null +++ b/src/plugins/google-analytics/default/g19.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${visits} + _(Sites) + + Visits: + Unique: + ${unique} + Views: + ${views} + Pg/Visit: + ${pagesVisit} + Avg. Dur.: + ${avgDuration} + Bounce%: + ${bounce} + + + + ${title} + ${alt_title} + + + + + + + + + ${message} + + diff --git a/src/plugins/google-analytics/google-analytics.py b/src/plugins/google-analytics/google-analytics.py new file mode 100644 index 0000000..f6dd14f --- /dev/null +++ b/src/plugins/google-analytics/google-analytics.py @@ -0,0 +1,376 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("cal", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15screen as g15screen +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import datetime +import time +import os, os.path +import gobject +import calendar +import gtk +import gdata.analytics.client +import cairoplot +import cairo + +# Logging +import logging +logger = logging.getLogger(__name__) + + +id="google-analytics" +name=_("Google Analytics") +description=_("Displays some summary information about sites being monitored\n\ +by Google Analytics. You will require a Google Account, and the ID of the sites\n\ +you wish to monitor.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +needs_network=True +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous site"), + g15driver.NEXT_SELECTION : _("Next site"), + } +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +SOURCE_APP_NAME = '%s-%s' % ( g15globals.name, g15globals.version ) +CONFIG_PATH = os.path.join(g15globals.user_config_dir, + "plugin-data", + "google-analytics", + "accounts.xml") +CONFIG_ITEM_NAME = "accounts" +ACC_MGR_HOSTNAME = "www.google.com" + +""" +Functions +""" + +def create(gconf_key, gconf_client, screen): + return G15GoogleAnalytics(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15GoogleAnalyticsPreferences(parent, gconf_client, gconf_key) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 10 + return val + +class Site(): + + def __init__(self): + self.name = "Unknown" + +class SiteMenuItem(g15theme.MenuItem): + + def __init__(self, entry, account): + g15theme.MenuItem.__init__(self, entry.GetProperty('ga:webPropertyId').value) + self._entry = entry + self._account = account + self.aggregates = {} + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._entry.GetProperty('ga:accountName').value + item_properties["item_alt"] = self._account.name + + return item_properties + + def activate(self): + self.event.activate() + + +class GoogleAnalyticsOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "google-analytics.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + username = self.widget_tree.get_object("Username") + username.connect("changed", self._username_changed) + username.set_text(self.account.get_property("username", "")) + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + + +class G15VisitsGraph(g15theme.Component): + + def __init__(self, component_id, plugin): + g15theme.Component.__init__(self, component_id) + self.plugin = plugin + + def get_colors(self): + series_colors = None + fill_colors = None + if self.plugin._screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + highlight_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )) + series_colors = (highlight_color[0],highlight_color[1],highlight_color[2], 1.0) + fill_colors = (highlight_color[0],highlight_color[1],highlight_color[2], 0.50) + return series_colors, fill_colors + + def create_plot(self, graph_surface): + series_color, fill_color = self.get_colors() + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + + selected = self.plugin._menu.selected + pie_data = {} + if selected: + new_visits = float(selected.aggregates["ga:percentNewVisits"]) + returning = 100.0 - new_visits + pie_data[_("New %0.2f%%" % new_visits)] = new_visits + pie_data[_("Returning %0.2f%%" % returning)] = returning + + plot = cairoplot.PiePlot(graph_surface, pie_data, + self.view_bounds[2], + self.view_bounds[3], + background = None, + colors = [ series_color, alt_series_color ]) + plot.font_size = 18 + return plot + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.view_bounds: + graph_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + int(self.view_bounds[2]), + int(self.view_bounds[3])) + plot = self.create_plot(graph_surface) + plot.line_width = 2.0 + plot.line_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.label_color = self.plugin._screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.shadow = True + plot.bounding_box = False + plot.render() + plot.commit() + + canvas.save() + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.set_source_surface(graph_surface, 0.0, 0.0) + canvas.paint() + canvas.restore() + + +class G15GoogleAnalyticsPreferences(g15accounts.G15AccountPreferences): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME) + + def get_account_types(self): + return [ "google-analytics" ] + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + return GoogleAnalyticsOptions(account, self) + +class G15GoogleAnalytics(): + + def __init__(self, gconf_key, gconf_client, screen): + + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._timer = None + self._icon_path = g15icontools.get_icon_path([ "redhat-office", "package_office", "gnome-applications", "xfce-office", "baobab" ]) + self._thumb_icon = g15cairo.load_surface_from_file(self._icon_path) + self._timer = None + + def activate(self): + self._active = True + self._page = None + self._theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), auto_dirty = False) + self._loaded = 0 + + self.pie_data = [] + + # Backend + self._account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + self._account_manager.add_change_listener(self._accounts_changed) + + # Menu + self._menu = g15theme.Menu("menu") + self._menu.focusable = True + self._menu.on_selected = self._on_menu_selected + + # Page + self._page = g15theme.G15Page(name, self._screen, theme_properties_callback = self._get_properties, + thumbnail_painter = self._paint_thumbnail, + originating_plugin = self) + self._page.set_title(_("Google Analytics")) + self._page.set_theme(self._theme) + self._screen.key_handler.action_listeners.append(self) + self._page.add_child(G15VisitsGraph("visitsGraph", self)) + self._page.add_child(self._menu) + self._page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self._screen.add_page(self._page) + self._schedule_refresh(0) + + def deactivate(self): + self._account_manager.remove_change_listener(self._accounts_changed) + self._screen.key_handler.action_listeners.remove(self) + self._cancel_refresh() + self._page.delete() + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page and self._page.is_visible(): + pass + + """ + Private + """ + def _on_menu_selected(self): + self._gconf_client.set_string("%s/selected_site" % self._gconf_key, self._menu.selected.id) + + def _cancel_refresh(self): + if self._timer != None: + self._timer.cancel() + + def _do_refresh(self): + if self._page: + self._load_site_data() + self._page.redraw() + self._schedule_refresh(get_update_time(self._gconf_client, self._gconf_key) * 60.0) + selected = g15gconf.get_string_or_default(self._gconf_client, "%s/selected_site" % self._gconf_key, None) + if selected: + for m in self._menu.get_children(): + if m.id == selected: + self._menu.set_selected_item(m) + + def _schedule_refresh(self, time): + self._timer = g15scheduler.schedule("AnalyticsRedraw", time, self._do_refresh) + + def _accounts_changed(self, account_manager): + self._cancel_refresh() + self._do_refresh() + + def _get_properties(self): + properties = {} + properties["icon"] = self._icon_path + properties["title"] = self._page.title + properties["alt_title"] = "" + properties["message"] = _("No sites configured") if len(self._menu.get_children()) == 0 else "" + sel = self._menu.selected + if sel: + properties["visits"] = sel.aggregates["ga:visits"] + properties["unique"] = sel.aggregates["ga:newVisits"] + properties["views"] = sel.aggregates["ga:pageviews"] + properties["pagesVisit"] = "%0.2f" % float(sel.aggregates["ga:pageviewsPerVisit"]) + properties["avgDuration"] = str(datetime.timedelta(seconds=int(float(sel.aggregates["ga:avgTimeOnSite"])))) + properties["bounce"] = "%0.2f" % float(sel.aggregates["ga:visitBounceRate"]) + properties["uniquePercent"] = "%0.2f" % float(sel.aggregates["ga:percentNewVisits"]) + else: + properties["visits"] = "" + properties["unique"] = "" + properties["views"] = "" + properties["pagesVisit"] = "" + properties["avgDuration"] = "" + properties["bounce"] = "" + properties["uniquePercent"] = "" + + return properties + + def _load_site_data(self): + items = [] + for acc in self._account_manager.accounts: + self._load_account_site_data(items, acc) + self._menu.set_children(items) + self._page.mark_dirty() + + def _load_account_site_data(self, items, account): + self._client = gdata.analytics.client.AnalyticsClient(source=SOURCE_APP_NAME) + ex = None + for i in range(0, 3): + for j in range(0, 2): + password = self._account_manager.retrieve_password(account, ACC_MGR_HOSTNAME, None, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + return self._retrieve_site_data(items, account, password) + except gdata.client.BadAuthentication as e: + logger.debug("Error authenticating", exc_info = e) + ex = e + + if ex is not None: + raise ex + + def _retrieve_site_data(self, items, account, password): + username = account.get_property("username", "") + logger.info("Logging in as %s / %s for %s on %s", + username, + password, + account, + self._client.source) + self._client.ClientLogin(username, password, self._client.source) + account_query = gdata.analytics.client.AccountFeedQuery() + self._account_manager.store_password(account, password, ACC_MGR_HOSTNAME, None) + self.feed = self._client.GetAccountFeed(account_query) + + for entry in self.feed.entry: + item = SiteMenuItem(entry, account) + items.append(item) + end_date = datetime.date.today() + start_date = datetime.date(2005,01,01) + data_query = gdata.analytics.client.DataFeedQuery({ + 'ids': entry.table_id.text, + 'start-date': start_date.isoformat(), + 'end-date': end_date.isoformat(), + 'max-results': 0, + 'dimensions': 'ga:date', + 'metrics': 'ga:visits,ga:newVisits,ga:pageviews,ga:pageviewsPerVisit,ga:avgTimeOnSite,ga:visitBounceRate,ga:percentNewVisits'}) + + feed = self._client.GetDataFeed(data_query) + aggregates = feed.aggregates + for m in aggregates.metric: + item.aggregates[m.name] = m.value + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._page != None and self._thumb_icon != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + diff --git a/src/plugins/google-analytics/google-analytics.ui b/src/plugins/google-analytics/google-analytics.ui new file mode 100644 index 0000000..e898ad5 --- /dev/null +++ b/src/plugins/google-analytics/google-analytics.ui @@ -0,0 +1,60 @@ + + + + + + False + + + True + False + + + True + False + 2 + 8 + 8 + + + True + False + 0 + Username + + + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/im/Makefile.am b/src/plugins/im/Makefile.am new file mode 100644 index 0000000..860558b --- /dev/null +++ b/src/plugins/im/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/im +plugin_DATA = im.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/im/i18n/im.en_GB.po b/src/plugins/im/i18n/im.en_GB.po new file mode 100644 index 0000000..472d9b9 --- /dev/null +++ b/src/plugins/im/i18n/im.en_GB.po @@ -0,0 +1,92 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: im.py:61 +msgid "Instant Messenger" +msgstr "Instant Messenger" + +#: im.py:62 +msgid "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." +msgstr "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." + +#: im.py:66 +msgid "Copyright (C)2011 Brett Smith" +msgstr "Copyright (C)2011 Brett Smith" + +#: im.py:71 +msgid "Previous contact" +msgstr "Previous contact" + +#: im.py:72 +msgid "Next contact" +msgstr "Next contact" + +#: im.py:73 +msgid "Toggle mode" +msgstr "Toggle mode" + +#: im.py:74 +msgid "Next page" +msgstr "Next page" + +#: im.py:75 +msgid "Previous page" +msgstr "Previous page" + +#: im.py:84 +msgid "Offline" +msgstr "Offline" + +#: im.py:85 +msgid "Available" +msgstr "Available" + +#: im.py:86 +msgid "Chatty" +msgstr "Chatty" + +#: im.py:87 +msgid "Idle" +msgstr "Idle" + +#: im.py:88 +msgid "Busy" +msgstr "Busy" + +#: im.py:89 +msgid "Away" +msgstr "Away" + +#: im.py:97 +msgid "All Contacts" +msgstr "All Contacts" + +#: im.py:98 +msgid "Online Contacts" +msgstr "Online Contacts" + +#: im.py:99 +msgid "Available Contacts" +msgstr "Available Contacts" diff --git a/src/plugins/im/i18n/im.pot b/src/plugins/im/i18n/im.pot new file mode 100644 index 0000000..0e07810 --- /dev/null +++ b/src/plugins/im/i18n/im.pot @@ -0,0 +1,89 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: im.py:61 +msgid "Instant Messenger" +msgstr "" + +#: im.py:62 +msgid "" +"Integrates with a number of instant messengers, showing buddy " +"lists and messages on your LCD. Currently supports all clients " +"that use the Telepathy framework." +msgstr "" + +#: im.py:66 +msgid "Copyright (C)2011 Brett Smith" +msgstr "" + +#: im.py:71 +msgid "Previous contact" +msgstr "" + +#: im.py:72 +msgid "Next contact" +msgstr "" + +#: im.py:73 +msgid "Toggle mode" +msgstr "" + +#: im.py:74 +msgid "Next page" +msgstr "" + +#: im.py:75 +msgid "Previous page" +msgstr "" + +#: im.py:84 +msgid "Offline" +msgstr "" + +#: im.py:85 +msgid "Available" +msgstr "" + +#: im.py:86 +msgid "Chatty" +msgstr "" + +#: im.py:87 +msgid "Idle" +msgstr "" + +#: im.py:88 +msgid "Busy" +msgstr "" + +#: im.py:89 +msgid "Away" +msgstr "" + +#: im.py:97 +msgid "All Contacts" +msgstr "" + +#: im.py:98 +msgid "Online Contacts" +msgstr "" + +#: im.py:99 +msgid "Available Contacts" +msgstr "" diff --git a/src/plugins/im/im.py b/src/plugins/im/im.py new file mode 100644 index 0000000..8585815 --- /dev/null +++ b/src/plugins/im/im.py @@ -0,0 +1,544 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Notes +# ===== +# +# The program "contact-selector" was a big help in getting this working. The ContactList +# class is very loosely based on this, with many modifications. These are licensed under +# LGPL. See http://telepathy.freedesktop.org/wiki/Contact%20selector + + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("im", modfile = __file__).ugettext + +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import dbus +import telepathy +from telepathy.interfaces import ( + CHANNEL, + CHANNEL_INTERFACE_GROUP, + CHANNEL_TYPE_CONTACT_LIST, + CONNECTION, + CONNECTION_INTERFACE_ALIASING, + CONNECTION_INTERFACE_CONTACTS, + CONNECTION_INTERFACE_REQUESTS, + CONNECTION_INTERFACE_SIMPLE_PRESENCE) + +from telepathy.constants import ( + CONNECTION_PRESENCE_TYPE_AVAILABLE, + CONNECTION_PRESENCE_TYPE_AWAY, + CONNECTION_PRESENCE_TYPE_BUSY, + CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY, + HANDLE_TYPE_LIST) + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="im" +name=_("Instant Messenger") +description=_("Integrates with a number of instant messengers, showing \n\ +buddy lists and messages on your LCD. Currently supports all \n\ +clients that use the Telepathy framework.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous contact"), + g15driver.NEXT_SELECTION : _("Next contact"), + g15driver.VIEW : _("Toggle mode"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +# Other constants +POSSIBLE_ICON_NAMES = [ "im-user", "empathy", "pidgin", "emesene", "system-config-users", "im-message-new" ] +CONNECTION_PRESENCE_TYPE_OFFLINE = 1 + +IMAGE_DIR = 'images' +STATUS_MAP = { + ( CONNECTION_PRESENCE_TYPE_OFFLINE, None ): [ [ "offline", "user-offline-panel" ] , _("Offline")], + ( CONNECTION_PRESENCE_TYPE_AVAILABLE, None ): [ "user-available", _("Available") ], + ( CONNECTION_PRESENCE_TYPE_AVAILABLE, "chat" ): [ "im-message-new", _("Chatty") ], + ( CONNECTION_PRESENCE_TYPE_AWAY, None ): [ "user-idle", _("Idle") ], + ( CONNECTION_PRESENCE_TYPE_BUSY, None ): [ "user-busy", _("Busy") ], + ( CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY, None ): [ "user-away", _("Away") ] + } + +MODE_ALL = "all" +MODE_ONLINE = "online" +MODE_AVAILABLE = "available" +MODE_LIST= [ MODE_ONLINE, MODE_AVAILABLE, MODE_ALL ] +MODES = { + MODE_ALL : [ "All", _("All Contacts") ], + MODE_ONLINE : [ "Online", _("Online Contacts") ], + MODE_AVAILABLE : [ "Available", _("Available Contacts") ] + } + +def create(gconf_key, gconf_client, screen): + """ + Create the plugin instance + + gconf_key -- GConf key that may be used for plugin preferences + gconf_client -- GConf client instance + """ + return G15Im(gconf_client, gconf_key, screen) + +""" +Holds list of contacts for a single connection +""" +class ContactList: + + def __init__(self, list_store, conn, screen): + self.menu = list_store + self._conn = conn + self.screen = screen + self._contact_list = {} + self._conn.call_when_ready(self._connection_ready_cb) + + def deactivate(self): + pass + + def _connection_ready_cb(self, conn): + if CONNECTION_INTERFACE_SIMPLE_PRESENCE not in conn: + logger.warning("SIMPLE_PRESENCE interface not available on %s", conn.service_name) + return + if CONNECTION_INTERFACE_REQUESTS not in conn: + logger.warning("REQUESTS interface not available on %s", conn.service_name) + return + + conn[CONNECTION_INTERFACE_SIMPLE_PRESENCE].connect_to_signal( + "PresencesChanged", self._contact_presence_changed_cb) + self._ensure_channel() + + def _ensure_channel(self): + groups = ["subscribe", "publish"] + for group in groups: + requests = { + CHANNEL + ".ChannelType": CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + ".TargetHandleType": HANDLE_TYPE_LIST, + CHANNEL + ".TargetID": group} + self._conn[CONNECTION_INTERFACE_REQUESTS].EnsureChannel( + requests, + reply_handler = self._ensure_channel_cb, + error_handler = self._error_cb) + + def _ensure_channel_cb(self, is_yours, channel, properties): + channel = telepathy.client.Channel( + service_name = self._conn.service_name, + object_path = channel) + DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties' + channel[DBUS_PROPERTIES].Get( + CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler = self._request_contact_info, + error_handler = self._error_cb) + + def _request_contact_info(self, handles): + logger.debug("Requesting contact info for %s", str(handles)) + interfaces = [CONNECTION, + CONNECTION_INTERFACE_ALIASING, + CONNECTION_INTERFACE_SIMPLE_PRESENCE] + self._conn[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, + interfaces, + False, + reply_handler = self._get_contact_attributes_cb, + error_handler = self._error_cb) + + def _get_contact_attributes_cb(self, attributes): + logger.debug("Received contact attributes for %s", str(attributes)) + for handle, member in attributes.iteritems(): + contact_info = self._parse_member_attributes(member) + contact, alias, presence = contact_info + if handle not in self._contact_list: + self._add_contact(handle, contact, presence, str(alias)) + + def _parse_member_attributes(self, member): + contact_id, alias, presence = None, None, None + for key, value in member.iteritems(): + if key == CONNECTION + '/contact-id': + contact_id = value + elif key == CONNECTION_INTERFACE_ALIASING + '/alias': + alias = value + elif key == CONNECTION_INTERFACE_SIMPLE_PRESENCE + '/presence': + presence = value + + return (contact_id, alias, presence) + + def _add_contact(self, handle, contact, presence, alias): + logger.debug("Add contact %s (%s)", str(contact), str(handle)) + self._contact_list[handle] = contact + self.menu.add_contact(self._conn, handle, contact, presence, alias) + + def _contact_presence_changed_cb(self, presences): + logger.debug("Contact presence changed %s", str(presences)) + for handle, presence in presences.iteritems(): + if handle in self._contact_list: + self._update_contact_presence(handle, presence) + else: + self._request_contact_info([handle]) + + def _update_contact_presence(self, handle, presence): + logger.debug("Updating contact presence for %s", str(handle)) + self.menu.update_contact_presence(self._conn, handle, presence) + + def _error_cb(self, *args): + logger.error("Error happens: %s", args) + +""" +Represents a contact as a single item in a menu +""" +class ContactMenuItem(g15theme.MenuItem): + def __init__(self, conn, handle, contact, presence, alias): + g15theme.MenuItem.__init__(self, "contact-%s-%s" % ( str(conn), str(handle) ) ) + self.conn = conn + self.handle = handle + self.contact= contact + self.presence = presence + self.alias = alias + + def get_theme_properties(self): + """ + Render a single menu item + + Keyword arguments: + item -- item object + selected -- selected item object + canvas -- canvas to draw on + properties -- properties to pass to theme + attribtes -- attributes to pass to theme + + """ + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.alias + item_properties["item_alt"] = self._get_status_text(self.presence) + item_properties["item_type"] = "" + item_properties["item_icon"] = g15icontools.get_icon_path(self._get_status_icon_name(self.presence)) + return item_properties + + def set_presence(self, presence): + logger.debug("Setting presence of %s to %s", str(self.contact), str(presence)) + self.presence = presence + + ''' + Private + ''' + + def _get_status_text(self, presence): + key = ( presence[0], presence[1] ) + if key in STATUS_MAP: + return STATUS_MAP[key][1] + key = ( presence[0], None ) + if key in STATUS_MAP: + return STATUS_MAP[key][1] + logger.warning("Unknown presence %d = %s", presence[0], presence[1]) + return "Unknown" + + def _get_status_icon_name(self, presence): + key = ( presence[0], presence[1] ) + if key in STATUS_MAP: + return STATUS_MAP[key][0] + key = ( presence[0], None ) + if key in STATUS_MAP: + return STATUS_MAP[key][0] + logger.warning("Unknown presence %d = %s", presence[0], presence[1]) + return "dialog-warning" + +""" +Compare a single contact based on it's alias and presence +""" +def compare_contacts(a, b): + if ( a is None and b is not None ): + val = 1 + elif ( b is None and a is not None ): + val = -1 + elif ( b is None and a is None ): + val = 0 + else: + val = cmp(a.presence[0], b.presence[0]) + if val == 0: + val = cmp(a.alias, b.alias) + + return val + +""" +Theme menu for displaying all contacts across all monitored +connections. +""" +class ContactMenu(g15theme.Menu): + + def __init__(self, mode): + """ + Create the menu instance + + Keyword arguments: + screen -- screen instance + page -- page object + mode -- display mode + """ + g15theme.Menu.__init__(self, "menu") + self.mode = mode + self.on_update = None + if not self.mode: + self.mode = MODE_ONLINE + self._contacts = [] + self._contact_lists = {} + self._connections = [] + for connection in telepathy.client.Connection.get_connections(): + self._connect(connection) + + def deactivate(self): + for c in self._connections: + if c in self._contact_lists: + self._contact_lists[c].deactivate() + if c._status_changed_connection: + c._status_changed_connection.remove() + c._status_changed_connection = None + self._connections = [] + self._contact_lists = {} + self._contacts = [] + + def new_connection(self, bus_name, bus): + """ + Add a new connection to those monitored for contacts. + + Keyword arguments: + bus_name -- connection bus name + bus -- dbus instance + """ + connection = telepathy.client.Connection(bus_name, "/%s" % bus_name.replace(".", "/"), bus) + self._connect(connection) + + + def remove_connection(self, bus_name): + """ + Remove a connection given its name. All contacts attached to this connection + will be removed, and the menu reloaded + + Keyword arguments: + bus_name -- bus name + """ + for connection in list(self._connections): + if connection.service_name == bus_name: + del self._contact_lists[connection] + self._connections.remove(connection) + for item in list(self._contacts): + if item.conn == connection: + self._contacts.remove(item) + self.reload() + if self.on_update: + self.on_update() + return + + def is_connected(self, bus_name): + """ + Determine if the given connection name exists in the list of + connections currently being maintained + + Keyword arguments: + bus_name -- bus name + """ + for connection in self._connections: + if connection.service_name == bus_name: + return True + return False + + def reload(self): + """ + Build up the filter menu item list from the stored contacts. Only + contacts that are appropriate for the current mode will be added + """ + logger.debug("Reloading contacts") + c = [] + for item in self._contacts: + if self._is_presence_included(item.presence): + c.append(item) + self.sort_items(c) + self.select_first() + self.mark_dirty() + + def sort_items(self, children): + """ + Sort items based on their alias and presence + """ + self.set_children(sorted(children, cmp=compare_contacts)) + + def add_contact(self, conn, handle, contact, presence, alias): + """ + Add a new contact to the menu + + Keyword arguments: + conn -- connection + handle -- contact handle + contact -- contact id + alias - alias or real name + """ + item = ContactMenuItem(conn, handle, contact, presence, alias) + self._contacts.append(item) + self.reload() + if self.on_update: + self.on_update() + + def update_contact_presence(self, conn, handle, presence): + """ + Update a contact's presence in the list and reload + + Keyword arguments: + conn -- connection + handle -- contact handle + prescence -- presence object + """ + for row in self._contacts: + if row.handle == handle and row.conn == conn: + logger.debug("Updating presence of %s to %s", str(row.contact), str(presence)) + row.set_presence(presence) + self.selected = row + self.reload() + if self.on_update: + self.on_update() + return + logger.warning("Got presence update for unknown contact %s", str(presence)) + + + ''' + Private + ''' + + + def _connect(self, connection): + """ + Connect to the given path. Events will then be received to add new contacts + + Keyword arguments: + connection -- connection object + """ + self._contact_lists[connection] = ContactList(self, connection, self.screen) + self._connections.append(connection) + + def _is_presence_included(self, presence): + """ + Determine if presence is appropriate for the current mode + + Keyword arguments: + presence -- presence + """ + return ( self.mode == MODE_ONLINE and presence[0] != 1 ) or \ + ( self.mode == MODE_AVAILABLE and presence[0] == CONNECTION_PRESENCE_TYPE_AVAILABLE ) or \ + self.mode == MODE_ALL + +""" +Instant Messenger plugin class +""" + +class G15Im(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + """ + Constructor + + Keyword arguments: + gconf_client -- GConf client instance + gconf_key -- gconf_key for storing plugin preferences + screen -- screen manager + """ + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + + self.hidden = False + self._session_bus = dbus.SessionBus() + self._signal_handle = None + + def activate(self): + """ + Activate the plugin + """ + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + self._signal_handle = self._session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def create_menu(self): + mode = self.gconf_client.get_string(self.gconf_key + "/mode") + return ContactMenu(mode) + + def deactivate(self): + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + if self._signal_handle: + self._session_bus.remove_signal_receiver(self._signal_handle) + + def action_performed(self, binding): + """ + Handle actions. Most actions will be handle by the abstract menu plugin class, + but we want to switch the mode when the "View" action is selected. + + Keyword arguments: + binding -- binding + """ + if binding.action == g15driver.VIEW and self.page != None and self.page.is_visible(): + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + self.menu.mode = MODE_LIST[mode_index] + logger.info("Mode is now %s", self.menu.mode) + self.gconf_client.set_string(self.gconf_key + "/mode", self.menu.mode) + self.menu.reload() + self.screen.redraw(self.page) + return True + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + props["title"] = MODES[self.menu.mode][1] + + # Get what mode to switch to + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + props["list"] = MODES[MODE_LIST[mode_index]][0] + return props + + """ + DBUS callbacks functions + """ + + def _name_owner_changed(self, name, old_owner, new_owner): + """ + If the change is a telepathy connection, determine if it is + a connection that is to be removed, or a new connection to + be added + """ + if name.startswith("org.freedesktop.Telepathy.Connection"): + logger.info("Telepathy Name owner changed for %s from %s to %s", + name, + old_owner, + new_owner) + connected = self.menu.is_connected(name) + if new_owner == "" and connected: + logger.info("Removing %s", name) + g15scheduler.schedule("RemoveConnection", 5.0, self.menu.remove_connection, name) + elif old_owner == "" and not connected: + logger.info("Adding %s", name) + g15scheduler.schedule("NewConnection", 5.0, self.menu.new_connection, name, self._session_bus) + diff --git a/src/plugins/impulse15/Makefile.am b/src/plugins/impulse15/Makefile.am new file mode 100644 index 0000000..57e08a4 --- /dev/null +++ b/src/plugins/impulse15/Makefile.am @@ -0,0 +1,29 @@ +SUBDIRS = themes + +plugindir = $(datadir)/gnome15/plugins/impulse15 +plugin_DATA = impulse15.ui \ + impulse15.py + +EXTRA_DIST = \ + $(plugin_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/impulse15/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/impulse15/i18n; \ + done + + \ No newline at end of file diff --git a/src/plugins/impulse15/i18n/impulse15.en_GB.po b/src/plugins/impulse15/i18n/impulse15.en_GB.po new file mode 100644 index 0000000..48cac91 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.en_GB.po @@ -0,0 +1,106 @@ +# English translations for impulse package. +# Copyright (C) 2011 THE impulse'S COPYRIGHT HOLDER +# This file is distributed under the same license as the impulse package. +# Brett Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: impulse 15\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:36+0100\n" +"PO-Revision-Date: 2011-10-09 19:39+0100\n" +"Last-Translator: Brett Smith \n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/impulse15.glade.h:1 +msgid "Animate M-Key lights" +msgstr "Animate M-Key lights" + +#: i18n/impulse15.glade.h:2 +msgid "Audio Source" +msgstr "Audio Source" + +#: i18n/impulse15.glade.h:3 +msgid "Background" +msgstr "Background" + +#: i18n/impulse15.glade.h:4 +msgid "Bar Height" +msgstr "Bar Height" + +#: i18n/impulse15.glade.h:5 +msgid "Bar Width" +msgstr "Bar Width" + +#: i18n/impulse15.glade.h:6 +msgid "Bars" +msgstr "Bars" + +#: i18n/impulse15.glade.h:7 +msgid "Circle LCD" +msgstr "Circle LCD" + +#: i18n/impulse15.glade.h:8 +msgid "Circle Line" +msgstr "Circle Line" + +#: i18n/impulse15.glade.h:9 +msgid "Color 1" +msgstr "Color 1" + +#: i18n/impulse15.glade.h:10 +msgid "Color 2" +msgstr "Color 2" + +#: i18n/impulse15.glade.h:11 +msgid "Default" +msgstr "Default" + +#: i18n/impulse15.glade.h:12 +msgid "Disco mode" +msgstr "Disco mode" + +#: i18n/impulse15.glade.h:13 +msgid "Display" +msgstr "Display" + +#: i18n/impulse15.glade.h:14 +msgid "Foreground" +msgstr "Foreground" + +#: i18n/impulse15.glade.h:15 +msgid "Frame rate" +msgstr "Frame rate" + +#: i18n/impulse15.glade.h:16 +msgid "Impulse Preferences" +msgstr "Impulse Preferences" + +#: i18n/impulse15.glade.h:17 +msgid "Mode" +msgstr "Mode" + +#: i18n/impulse15.glade.h:18 +msgid "Original" +msgstr "Original" + +#: i18n/impulse15.glade.h:19 +msgid "Rows" +msgstr "Rows" + +#: i18n/impulse15.glade.h:20 +msgid "Screen" +msgstr "Screen" + +#: i18n/impulse15.glade.h:21 +msgid "Spacing" +msgstr "Spacing" + +#: i18n/impulse15.glade.h:22 +msgid "screen" +msgstr "screen" diff --git a/src/plugins/impulse15/i18n/impulse15.glade.h b/src/plugins/impulse15/i18n/impulse15.glade.h new file mode 100644 index 0000000..4e4ab68 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.glade.h @@ -0,0 +1,22 @@ +char *s = N_("Animate M-Key lights"); +char *s = N_("Audio Source"); +char *s = N_("Background"); +char *s = N_("Bar Height"); +char *s = N_("Bar Width"); +char *s = N_("Bars"); +char *s = N_("Circle LCD"); +char *s = N_("Circle Line"); +char *s = N_("Color 1"); +char *s = N_("Color 2"); +char *s = N_("Default"); +char *s = N_("Disco mode"); +char *s = N_("Display"); +char *s = N_("Foreground"); +char *s = N_("Frame rate"); +char *s = N_("Impulse Preferences"); +char *s = N_("Mode"); +char *s = N_("Original"); +char *s = N_("Rows"); +char *s = N_("Screen"); +char *s = N_("Spacing"); +char *s = N_("screen"); diff --git a/src/plugins/impulse15/i18n/impulse15.pot b/src/plugins/impulse15/i18n/impulse15.pot new file mode 100644 index 0000000..75cb130 --- /dev/null +++ b/src/plugins/impulse15/i18n/impulse15.pot @@ -0,0 +1,106 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:36+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/impulse15.glade.h:1 +msgid "Animate M-Key lights" +msgstr "" + +#: i18n/impulse15.glade.h:2 +msgid "Audio Source" +msgstr "" + +#: i18n/impulse15.glade.h:3 +msgid "Background" +msgstr "" + +#: i18n/impulse15.glade.h:4 +msgid "Bar Height" +msgstr "" + +#: i18n/impulse15.glade.h:5 +msgid "Bar Width" +msgstr "" + +#: i18n/impulse15.glade.h:6 +msgid "Bars" +msgstr "" + +#: i18n/impulse15.glade.h:7 +msgid "Circle LCD" +msgstr "" + +#: i18n/impulse15.glade.h:8 +msgid "Circle Line" +msgstr "" + +#: i18n/impulse15.glade.h:9 +msgid "Color 1" +msgstr "" + +#: i18n/impulse15.glade.h:10 +msgid "Color 2" +msgstr "" + +#: i18n/impulse15.glade.h:11 +msgid "Default" +msgstr "" + +#: i18n/impulse15.glade.h:12 +msgid "Disco mode" +msgstr "" + +#: i18n/impulse15.glade.h:13 +msgid "Display" +msgstr "" + +#: i18n/impulse15.glade.h:14 +msgid "Foreground" +msgstr "" + +#: i18n/impulse15.glade.h:15 +msgid "Frame rate" +msgstr "" + +#: i18n/impulse15.glade.h:16 +msgid "Impulse Preferences" +msgstr "" + +#: i18n/impulse15.glade.h:17 +msgid "Mode" +msgstr "" + +#: i18n/impulse15.glade.h:18 +msgid "Original" +msgstr "" + +#: i18n/impulse15.glade.h:19 +msgid "Rows" +msgstr "" + +#: i18n/impulse15.glade.h:20 +msgid "Screen" +msgstr "" + +#: i18n/impulse15.glade.h:21 +msgid "Spacing" +msgstr "" + +#: i18n/impulse15.glade.h:22 +msgid "screen" +msgstr "" diff --git a/src/plugins/impulse15/impulse15.py b/src/plugins/impulse15/impulse15.py new file mode 100644 index 0000000..ead265b --- /dev/null +++ b/src/plugins/impulse15/impulse15.py @@ -0,0 +1,398 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gobject +import gtk +import os +import sys +import datetime + +# Logging +import logging +logger = logging.getLogger(__name__) + +id="impulse15" +name="Impulse15" +description="Spectrum analyser. Based on the Impulse screenlet and desktop widget" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith, Ian Halpern" +site="https://launchpad.net/impulse.bzr" +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +has_preferences=True + +def get_source_index(source_name): + status, output = g15os.get_command_output("pacmd list-sources") + if status == 0 and len(output) > 0: + i = 0 + for line in output.split("\n"): + line = line.strip() + if line.startswith("index: "): + i = int(line[7:]) + elif line.startswith("name: <%s" % source_name): + return i + logger.warning("Audio source %s not found, default to first source", source_name) + return 0 + +def create(gconf_key, gconf_client, screen): + return G15Impulse(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "impulse15.ui")) + + dialog = widget_tree.get_object("ImpulseDialog") + dialog.set_transient_for(parent) + + # Set up the audio source model + audio_source_model = widget_tree.get_object("AudioSourceModel") + status, output = g15os.get_command_output("pacmd list-sources") + source_name = "0" + if status == 0 and len(output) > 0: + i = 0 + for line in output.split("\n"): + line = line.strip() + if line.startswith("index: "): + i = int(line[7:]) + source_name = str(i) + elif line.startswith("name: "): + source_name = line[7:-1] + elif line.startswith("device.description = "): + audio_source_model.append((source_name, line[22:-1])) + else: + for i in range(0, 9): + audio_source_model.append((str(i), "Source %d" % i)) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/disco", "Disco", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/animate_mkeys", "AnimateMKeys", False, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/mode", "ModeCombo", "spectrum", widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/paint", "PaintCombo", "screen", widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bars", "BarsSpinner", 16, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/audio_source_name", "AudioSource", source_name, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bar_width", "BarWidthSpinner", 16, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/spacing", "SpacingSpinner", 0, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/rows", "RowsSpinner", 16, widget_tree) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/bar_height", "BarHeightSpinner", 2, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/col1", "Color1", ( 255, 0, 0 ), widget_tree, default_alpha = 255) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/col2", "Color2", ( 0, 0, 255 ), widget_tree, default_alpha = 255) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/frame_rate", "FrameRateAdjustment", 10.0, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/gain", "GainAdjustment", 1.0, widget_tree) + + if driver.get_bpp() == 0: + widget_tree.get_object("LCDTable").set_visible(False) + + + dialog.run() + dialog.hide() + +class G15ImpulsePainter(g15screen.Painter): + + def __init__(self, plugin): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -5000) + self.theme_module = None + self.backlight_acquisition = None + self.mkey_acquisition = None + self.mode = "default" + self.plugin = plugin + self.last_sound = datetime.datetime.now() + + def do_lights(self, audio_sample_array = None): + if not audio_sample_array: + audio_sample_array = self._get_sample() + + if self.backlight_acquisition is not None: + self.backlight_acquisition.set_value(self._col_avg(audio_sample_array)) + tot_avg = self._tot_avg(audio_sample_array) + if self.mkey_acquisition is not None: + self._set_mkey_lights(tot_avg) + return tot_avg + + def is_idle(self): + return datetime.datetime.now() > ( self.last_sound + datetime.timedelta(0, 5.0) ) + + def paint(self, canvas): + if not self.theme_module: + return + audio_sample_array = self._get_sample() + tot_avg = self.do_lights(audio_sample_array) + if tot_avg > 0: + self.last_sound = datetime.datetime.now() + + canvas.save() + self.theme_module.on_draw( audio_sample_array, canvas, self.plugin ) + canvas.restore() + + """ + Private + """ + + def _get_sample(self): + fft = False + if hasattr( self.theme_module, "fft" ) and self.theme_module.fft: + fft = True + + audio_sample_array = impulse.getSnapshot( fft ) + if self.plugin.gain != 1: + arr = [] + for a in audio_sample_array: + arr.append(a * self.plugin.gain) + audio_sample_array = arr + + return audio_sample_array + + def _col_avg(self, list): + cols = [] + each = len(list) / 3 + z = 0 + for j in range(0, 3): + t = 0 + for x in range(0, each): + t += min(255, list[z] * 340) + z += 1 + cols.append(int(t / each)) + return ( cols[0], cols[1], cols[2] ) + + def _tot_avg(self, list): + sz = len(list) + z = 0 + t = 0 + for x in range(0, sz): + t += min(255, list[z] * 340) + z += 1 + return t / sz + + def _set_mkey_lights(self, val): + if val > 200: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_MR | g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif val > 100: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif val > 50: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2) + elif val > 25: + self.mkey_acquisition.set_value(g15driver.MKEY_LIGHT_1) + else: + self.mkey_acquisition.set_value(0) + + def _release_mkey_acquisition(self): + if self.mkey_acquisition: + self.plugin.screen.driver.release_control(self.mkey_acquisition) + self.mkey_acquisition = None + + def _release_backlight_acquisition(self): + if self.backlight_acquisition is not None: + self.plugin.screen.driver.release_control(self.backlight_acquisition) + self.backlight_acquisition = None + +class G15Impulse(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.hidden = False + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.active = False + self.last_paint = None + self.audio_source_index = 0 + self.config_change_timer = None + + import impulse + sys.modules[ __name__ ].impulse = impulse + sys.path.append(os.path.join(os.path.dirname(__file__), "themes")) + + def set_audio_source( self, *args, **kwargs ): + impulse.setSourceIndex( self.audio_source_index ) + + def activate(self): + self.painter = G15ImpulsePainter(self) + self.width = self.screen.driver.get_size()[0] + self.height = self.screen.driver.get_size()[1] + self.active = True + self.page = None + self.visible = False + self.timer = None + self._load_config() + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed) + self.redraw() + + def deactivate(self): + self.painter._release_backlight_acquisition() + self.painter._release_mkey_acquisition() + self.active = False + self.refresh_interval = 1.0 / 25.0 + self.gconf_client.notify_remove(self.notify_handle); + self.hide_page() + self._clear_painter() + + def hide_page(self): + self.stop_redraw() + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def on_shown(self): + self.visible = True + self._schedule_redraw() + + def on_hidden(self): + self.visible = False + self.stop_redraw() + + def stop_redraw(self): + if self.timer != None: + self.timer.cancel() + self.timer = None + g15scheduler.clear_jobs("impulseQueue") + + def destroy(self): + pass + + def paint(self, canvas): + if not self.theme_module: + return + + fft = False + if hasattr( self.theme_module, "fft" ) and self.theme_module.fft: + fft = True + + audio_sample_array = impulse.getSnapshot( fft ) + + if self.backlight_acquisition is not None: + self.backlight_acquisition.set_value(self._col_avg(audio_sample_array)) + + if self.mkey_acquisition is not None: + self._set_mkey_lights(self._tot_avg(audio_sample_array)) + + canvas.save() + self.theme_module.on_draw( audio_sample_array, canvas, self ) + canvas.restore() + + def redraw(self): + if self.screen.driver.get_bpp() == 0: + self.painter.do_lights() + else: + if self.paint_mode == "screen" and self.visible: + self.screen.redraw(self.page, queue = False) + elif self.paint_mode != "screen": + self.screen.redraw(redraw_content = False, queue = False) + self._schedule_redraw() + + """ + Private + """ + + def _schedule_redraw(self): + if self.active: + next_tick = self.refresh_interval + if self.painter.is_idle(): + next_tick = 1.0 + self.timer = g15scheduler.queue("impulseQueue", "ImpulseRedraw", next_tick, self.redraw) + + def _config_changed(self, client, connection_id, entry, args): + if self.config_change_timer is not None: + self.config_change_timer.cancel() + self.config_change_timer = g15scheduler.schedule("ConfigReload", 1, self._do_config_changed) + + def _do_config_changed(self): + self.stop_redraw() + self._load_config() + self.redraw() + self.config_change_timer = None + + def _on_load_theme (self): + if not self.painter.theme_module or self.mode != self.painter.theme_module.__name__: + self.painter.theme_module = __import__( self.mode ) + self.painter.theme_module.load_theme(self) + + def _activate_painter(self): + if not self.painter in self.screen.painters: + self.screen.painters.append(self.painter) + + def _clear_painter(self): + if self.painter in self.screen.painters: + self.screen.painters.remove(self.painter) + + def _load_config(self): + logger.info("Reloading configuration") + self.audio_source_index = get_source_index(self.gconf_client.get_string(self.gconf_key + "/audio_source_name")) + gobject.idle_add(self.set_audio_source) + self.mode = self.gconf_client.get_string(self.gconf_key + "/mode") + self.disco = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/disco", False) + self.refresh_interval = 1.0 / g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/frame_rate", 25.0) + self.gain = g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/gain", 1.0) + logger.info("Refresh interval is %f", self.refresh_interval) + self.animate_mkeys = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/animate_mkeys", False) + if self.mode == None or self.mode == "" or self.mode == "spectrum" or self.mode == "scope": + self.mode = "default" + self.paint_mode = self.gconf_client.get_string(self.gconf_key + "/paint") + if self.paint_mode == None or self.mode == "": + self.paint_mode = "screen" + self._on_load_theme() + + self.bars = self.gconf_client.get_int(self.gconf_key + "/bars") + if self.bars == 0: + self.bars = 16 + self.bar_width = self.gconf_client.get_int(self.gconf_key + "/bar_width") + if self.bar_width == 0: + self.bar_width = 16 + self.bar_height = self.gconf_client.get_int(self.gconf_key + "/bar_height") + if self.bar_height == 0: + self.bar_height = 2 + self.rows = self.gconf_client.get_int(self.gconf_key + "/rows") + if self.rows == 0: + self.rows = 16 + self.spacing = self.gconf_client.get_int(self.gconf_key + "/spacing") + self.col1 = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/col1", ( 255, 0, 0, 255 )) + self.col2 = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/col2", ( 0, 0, 255, 255 )) + + self.peak_heights = [ 0 for i in range( self.bars ) ] + + paint = self.gconf_client.get_string(self.gconf_key + "/paint") + if paint != self.last_paint and self.screen.driver.get_bpp() != 0: + self.last_paint = paint + self._clear_painter() + if paint == "screen": + if self.page == None: + self.page = g15theme.G15Page(id, self.screen, title = name, painter = self.painter.paint, on_shown = self.on_shown, on_hidden = self.on_hidden, originating_plugin = self) + self.screen.add_page(self.page) + else: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + elif paint == "foreground": + self.painter.place = g15screen.FOREGROUND_PAINTER + self._activate_painter() + self.hide_page() + elif paint == "background": + self.painter.place = g15screen.BACKGROUND_PAINTER + self._activate_painter() + self.hide_page() + + # Acquire the backlight control if appropriate + control = self.screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control: + if self.disco and self.painter.backlight_acquisition is None: + self.painter.backlight_acquisition = self.screen.driver.acquire_control(control) + elif not self.disco and self.painter.backlight_acquisition is not None: + self.painter._release_backlight_acquisition() + + # Acquire the M-Key lights control if appropriate + if self.animate_mkeys and self.painter.mkey_acquisition is None: + self.painter.mkey_acquisition = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + elif not self.animate_mkeys and self.painter.mkey_acquisition is not None: + self.painter._release_mkey_acquisition() diff --git a/src/plugins/impulse15/impulse15.ui b/src/plugins/impulse15/impulse15.ui new file mode 100644 index 0000000..f071e41 --- /dev/null +++ b/src/plugins/impulse15/impulse15.ui @@ -0,0 +1,632 @@ + + + + + + 64 + 1 + 10 + + + + + + + + + + + 240 + 1 + 10 + + + 320 + 1 + 10 + + + 1 + 255 + 32 + 1 + 1 + + + 50 + 1 + 10 + + + 10 + 1 + 10 + + + + + + + + + + + default + Default + + + original + Original + + + circlelcd + Circle LCD + + + circleline + Circle Line + + + + + + + + + + + + + background + Background + + + foreground + Foreground + + + screen + Screen + + + + + 8 + 240 + 1 + 1 + + + False + 5 + Impulse Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 8 + + + True + False + + + True + True + 0 + + + + + True + False + AudioSourceModel + + + + 1 + + + + + True + True + 1 + + + + + True + False + 6 + 4 + 8 + 5 + + + + + + + + + True + False + 0 + Mode + + + 2 + GTK_FILL + 2 + + + + + True + False + ModeModel + + + + 0 + + + + + 2 + 4 + 2 + + + + + True + False + 0 + Display + + + 2 + 1 + 2 + GTK_FILL + 2 + + + + + True + False + PaintModel + + + + 0 + + + + + 2 + 4 + 1 + 2 + 2 + + + + + 114 + True + False + 0 + Color 1 + + + 2 + 3 + + + + + 114 + True + False + 0 + Color 2 + + + 2 + 3 + 2 + 3 + + + + + 64 + True + True + True + True + #000000000000 + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + 64 + True + True + True + True + #000000000000 + + + 3 + 4 + 2 + 3 + GTK_FILL + + + + + 114 + True + False + 0 + Bars + + + 3 + 4 + + + + + 114 + True + False + 0 + Rows + + + 2 + 3 + 3 + 4 + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarsAdjustment + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + 64 + True + True + + 2 + True + False + False + True + True + RowsAdjustment + + + 3 + 4 + 3 + 4 + GTK_FILL + + + + + 114 + True + False + 0 + Bar Width + + + 4 + 5 + + + + + 114 + True + False + 0 + Bar Height + + + 2 + 3 + 4 + 5 + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarWidthAdjustment + + + 1 + 2 + 4 + 5 + GTK_FILL + + + + + 64 + True + True + + 2 + True + False + False + True + True + BarHeightAdjustment + + + 3 + 4 + 4 + 5 + GTK_FILL + + + + + 64 + True + True + + True + False + False + True + True + SpacingAdjustment + + + 1 + 2 + 5 + 6 + GTK_FILL + + + + + 114 + True + False + 0 + Spacing + + + 5 + 6 + GTK_FILL + + + + + True + True + 8 + 2 + + + + + False + False + 0 + + + + + True + False + + + Disco mode + True + True + False + True + + + True + True + 0 + + + + + Animate M-Key lights + True + True + False + True + + + True + True + 1 + + + + + False + False + 8 + 1 + + + + + True + False + 2 + 2 + 4 + 4 + + + True + False + 0 + Frame rate + + + GTK_FILL + + + + + True + True + FrameRateAdjustment + on + 2 + 2 + + + 1 + 2 + + + + + True + False + 0 + Gain + + + 1 + 2 + GTK_FILL + + + + + True + True + GainAdjustment + on + 2 + 2 + + + 1 + 2 + 1 + 2 + + + + + True + True + 2 + + + + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 5 + + + + + + button9 + + + + 100 + 1 + 10 + + diff --git a/src/plugins/impulse15/themes/Makefile.am b/src/plugins/impulse15/themes/Makefile.am new file mode 100644 index 0000000..c9fefd9 --- /dev/null +++ b/src/plugins/impulse15/themes/Makefile.am @@ -0,0 +1,2 @@ +SUBDIRS = circlelcd circleline default original + diff --git a/src/plugins/impulse15/themes/circlelcd/Makefile.am b/src/plugins/impulse15/themes/circlelcd/Makefile.am new file mode 100644 index 0000000..cf160bf --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/circlelcd +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/circlelcd/__init__.py b/src/plugins/impulse15/themes/circlelcd/__init__.py new file mode 100644 index 0000000..ae55247 --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/__init__.py @@ -0,0 +1,53 @@ +import math + +fft = True + +def load_theme( screenlet ): + + ''' + screenlet.resize( 300, 300 ) + + screenlet.add_option( ColorOption( + 'Impulse', 'cc', + cc, 'Color', + 'Example options group using color' + ) ) + ''' + + +def on_after_set_attribute ( self, name, value, screenlet ): + setattr( self, name, value ) + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + + n_bars = screenlet.bars + + cr.set_line_width( screenlet.bar_width ) + + for i in range( 0, l, l / n_bars ): + bar_amp_norm = audio_sample_array[ i ] + + + bar_height = ( bar_amp_norm * ( screenlet.width / 2 ) + screenlet.bar_width ) * ( screenlet.bar_height / 10.0 ) + + cc = screenlet.col2 + cr.set_source_rgba( cc[ 0 ], cc[ 1 ], cc[ 2 ], cc[ 3 ] ) + for j in range( 0, int( bar_height / 5 ), max(max(1, screenlet.spacing) / 5, 1) ): + cr.arc( + width / 2, + height / 2, + 20 + j * screenlet.bar_width, + ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) ), + ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) + 1 ) - .05 + ) + + cr.stroke( ) + + if j == 0: + cc = screenlet.col1 + cr.set_source_rgba( cc[ 0 ], cc[ 1 ], cc[ 2 ], cc[ 3 ] ) diff --git a/src/plugins/impulse15/themes/circlelcd/theme.conf b/src/plugins/impulse15/themes/circlelcd/theme.conf new file mode 100644 index 0000000..29279c8 --- /dev/null +++ b/src/plugins/impulse15/themes/circlelcd/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=dev +author=Ian Halpern +version=1.0 +info=The original theme diff --git a/src/plugins/impulse15/themes/circleline/Makefile.am b/src/plugins/impulse15/themes/circleline/Makefile.am new file mode 100644 index 0000000..0ed98d7 --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/circleline +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/circleline/__init__.py b/src/plugins/impulse15/themes/circleline/__init__.py new file mode 100644 index 0000000..2004abf --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/__init__.py @@ -0,0 +1,66 @@ +import math + +fft=True + +def load_theme( screenlet ): + ''' + screenlet.resize( 300, 300 ) + + screenlet.add_option( ColorOption( + 'Impulse', 'co', + co, 'Color', + 'Example options group using color' + ) ) + ''' + +def on_after_set_attribute ( self, name, value, screenlet ): + setattr( self, name, value ) + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + co = screenlet.col1 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + + n_bars = screenlet.bars + + cr.set_line_width( screenlet.bar_width ) + + h = screenlet.bar_height + + fx = 0 + fy = 0 + + for i in range( 0, l, l / n_bars ): + + bar_amp_norm = audio_sample_array[ i ] + + bar_height = bar_amp_norm * 100 + + + a = ( math.pi*2 / n_bars ) * ( i / ( l / n_bars ) ) + + x = ( math.sin( a ) * ( h + bar_height ) + width / 2 ) + y = ( math.cos( a ) * ( h + bar_height ) + height / 2 ) + + if not i: + fx = x + fy = y + cr.move_to( x, y ) + + cr.curve_to( + x, y, + x, y, + x, y + ) + + cr.curve_to( + fx, fy, + fx, fy, + fx, fy + ) + + cr.stroke( ) diff --git a/src/plugins/impulse15/themes/circleline/theme.conf b/src/plugins/impulse15/themes/circleline/theme.conf new file mode 100644 index 0000000..5d123d7 --- /dev/null +++ b/src/plugins/impulse15/themes/circleline/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=Circle Line +author=Ian Halpern +version=1.0 +info= diff --git a/src/plugins/impulse15/themes/default/Makefile.am b/src/plugins/impulse15/themes/default/Makefile.am new file mode 100644 index 0000000..40ebf63 --- /dev/null +++ b/src/plugins/impulse15/themes/default/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/default +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/default/__init__.py b/src/plugins/impulse15/themes/default/__init__.py new file mode 100644 index 0000000..353a788 --- /dev/null +++ b/src/plugins/impulse15/themes/default/__init__.py @@ -0,0 +1,67 @@ +peak_heights = [ 0 for i in range( 256 ) ] +peak_acceleration = [ 0.0 for i in range( 256 ) ] +fft = True + +def load_theme ( screenlet): + pass + +def on_draw ( audio_sample_array, cr, screenlet ): + n_cols = screenlet.bars + col_width = screenlet.bar_width + col_spacing = screenlet.spacing + bar_color = screenlet.col1 + row_height = screenlet.bar_height + n_rows = screenlet.rows + row_spacing = screenlet.spacing + peak_color = screenlet.col2 + freq = len( audio_sample_array ) / n_cols + actual_cols = ( len( audio_sample_array ) / freq ) + 1 + + total_width = ( actual_cols * ( col_width + col_spacing ) ) - col_spacing + +# print "Total width = %d, Cols = %d, width = %d, spacing = %d, freq = %f, len = %d, actual_cols = %d" % ( total_width, n_cols, col_width, col_spacing, freq, len(audio_sample_array), actual_cols ) + + cr.save() + cr.translate( ( screenlet.width - total_width ) / 2, 0) + + for i in range( 0, len( audio_sample_array ), freq ): + + col = i / freq + rows = int( audio_sample_array[ i ] * ( n_rows - 2 ) ) + + cr.set_source_rgba( bar_color[ 0 ], bar_color[ 1 ], bar_color[ 2 ], bar_color[ 3 ] ) + + if rows > peak_heights[ i ]: + peak_heights[ i ] = rows + peak_acceleration[ i ] = 0.0 + else: + peak_acceleration[ i ] += .1 + peak_heights[ i ] -= peak_acceleration[ i ] + + if peak_heights[ i ] < 0: + peak_heights[ i ] = 0 + + for row in range( 0, rows ): + + cr.rectangle( + col * ( col_width + col_spacing ), + screenlet.height - row * ( row_height + row_spacing ), + col_width, -row_height + ) + + cr.fill( ) + + cr.set_source_rgba( peak_color[ 0 ], peak_color[ 1 ], peak_color[ 2 ], peak_color[ 3 ] ) + + cr.rectangle( + col * ( col_width + col_spacing ), + screenlet.height - peak_heights[ i ] * ( row_height + row_spacing ), + col_width, -row_height + ) + + cr.fill( ) + + cr.fill( ) + cr.stroke( ) + cr.restore() + diff --git a/src/plugins/impulse15/themes/default/theme.conf b/src/plugins/impulse15/themes/default/theme.conf new file mode 100644 index 0000000..a12e64c --- /dev/null +++ b/src/plugins/impulse15/themes/default/theme.conf @@ -0,0 +1,9 @@ +# An example of theme-configuration file + +[Theme] +name=default +author=Ian Halpern +version=1.0 +info=The default theme + +fft=True diff --git a/src/plugins/impulse15/themes/original/Makefile.am b/src/plugins/impulse15/themes/original/Makefile.am new file mode 100644 index 0000000..899900e --- /dev/null +++ b/src/plugins/impulse15/themes/original/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/impulse15/themes/original +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/impulse15/themes/original/__init__.py b/src/plugins/impulse15/themes/original/__init__.py new file mode 100644 index 0000000..c0877e0 --- /dev/null +++ b/src/plugins/impulse15/themes/original/__init__.py @@ -0,0 +1,44 @@ +fft = True + +def load_theme( screenlet ): + pass + +def on_draw( audio_sample_array, cr, screenlet ): + + l = len( audio_sample_array ) + + width, height = ( screenlet.width, screenlet.height ) + + # start drawing spectrum + + + n_bars = screenlet.bars + bar_width = screenlet.bar_width + bar_spacing = screenlet.spacing + + + freq = len( audio_sample_array ) / n_bars + actual_cols = ( len( audio_sample_array ) / freq ) + 1 + total_width = ( actual_cols * ( bar_width + bar_spacing ) ) - bar_spacing + cr.translate( ( screenlet.width - total_width ) / 2, 0) + + for i in range( 0, l, l / n_bars ): + + bar_amp_norm = audio_sample_array[ i ] + + bar_height = ( bar_amp_norm * height + 2 ) * ( screenlet.bar_height / 10.0 ) + + cr.rectangle( + ( bar_width + bar_spacing ) * ( i / ( l / n_bars ) ), + height / 2 - bar_height / 2, + bar_width, + bar_height + ) + + co = screenlet.col1 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + cr.fill_preserve() + co = screenlet.col2 + cr.set_source_rgba( co[ 0 ], co[ 1 ], co[ 2 ], co[ 3 ] ) + cr.stroke() + diff --git a/src/plugins/impulse15/themes/original/theme.conf b/src/plugins/impulse15/themes/original/theme.conf new file mode 100644 index 0000000..b80dd18 --- /dev/null +++ b/src/plugins/impulse15/themes/original/theme.conf @@ -0,0 +1,7 @@ +# An example of theme-configuration file + +[Theme] +name=original +author=Ian Halpern +version=1.0 +info=The original theme diff --git a/src/plugins/indicator-messages/Makefile.am b/src/plugins/indicator-messages/Makefile.am new file mode 100644 index 0000000..8b58f19 --- /dev/null +++ b/src/plugins/indicator-messages/Makefile.am @@ -0,0 +1,8 @@ +plugindir = $(datadir)/gnome15/plugins/indicator-messages +plugin_DATA = indicator-messages.py \ + indicator-messages.ui \ + mono-mail-new.gif \ + mono-mail-error.gif + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/Makefile.am b/src/plugins/indicator-messages/default/Makefile.am new file mode 100644 index 0000000..8effca2 --- /dev/null +++ b/src/plugins/indicator-messages/default/Makefile.am @@ -0,0 +1,12 @@ +themedir = $(datadir)/gnome15/plugins/indicator-messages/default +theme_DATA = indicator_messages_default_common.py \ + default.svg \ + default-entry.svg \ + indicator_messages_default_default.py \ + g19.svg \ + g19-entry.svg \ + g19-separator.svg \ + indicator_messages_default_g19.py + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/indicator-messages/default/default-entry.svg b/src/plugins/indicator-messages/default/default-entry.svg new file mode 100644 index 0000000..1ae3abb --- /dev/null +++ b/src/plugins/indicator-messages/default/default-entry.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + + + ${item_name} + + + diff --git a/src/plugins/indicator-messages/default/default.svg b/src/plugins/indicator-messages/default/default.svg new file mode 100644 index 0000000..94dec73 --- /dev/null +++ b/src/plugins/indicator-messages/default/default.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + diff --git a/src/plugins/indicator-messages/default/g19-entry.svg b/src/plugins/indicator-messages/default/g19-entry.svg new file mode 100644 index 0000000..ba3d128 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19-entry.svg @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + ${item_alt} + + + + + ${item_name} + ${item_alt} + + diff --git a/src/plugins/indicator-messages/default/g19-separator.svg b/src/plugins/indicator-messages/default/g19-separator.svg new file mode 100644 index 0000000..950d0a4 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19-separator.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/indicator-messages/default/g19.svg b/src/plugins/indicator-messages/default/g19.svg new file mode 100644 index 0000000..ecf80b3 --- /dev/null +++ b/src/plugins/indicator-messages/default/g19.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_common.py b/src/plugins/indicator-messages/default/indicator_messages_default_common.py new file mode 100644 index 0000000..7f30bda --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_common.py @@ -0,0 +1,56 @@ +import gnome15.g15_theme as g15theme +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import os +import time + +class Theme(): + + def __init__(self, screen, theme): + self.theme = theme + self.entry_theme = g15theme.G15Theme(os.path.dirname(__file__), screen, "entry") + self.separator_theme = g15theme.G15Theme(os.path.dirname(__file__), screen, "separator") + self.screen = screen + + def paint_foreground(self, canvas, properties, attributes, args, x_offset, y_offset): + items = attributes["items"] + selected = attributes["selected"] + + canvas.save() + y = y_offset + + # How many complete items fit on the screen? Make sure the selected item is visible + # TODO again, this needs turning into a re-usable component - see menu and rss + item_height = self.entry_theme.bounds[3] + max_items = int( ( self.screen.height - y ) / item_height) + if selected != None: + sel_index = items.index(selected) + diff = sel_index + 1 - max_items + if diff > 0: + y -= diff * item_height + canvas.rectangle(x_offset, y_offset, self.screen.width - ( x_offset * 2 ), self.screen.height - y_offset) + canvas.clip() + + canvas.translate(x_offset, y) + for item in items: + item_properties = {} + if selected == item: + item_properties["item_selected"] = True + item_properties["item_name"] = item.get_label() + item_properties["item_alt"] = item.get_right_side_text() + item_properties["item_type"] = item.get_type() + icon_name = item.get_icon_name() + if icon_name != None: + item_properties["item_icon"] = g15cairo.load_surface_from_file(g15icontools.get_icon_path(self.screen.applet.conf_client, icon_name)) + else: + item_properties["item_icon"] = item.get_icon() + + if item.get_type() == "separator": + self.separator_theme.draw(canvas, item_properties) + else: + self.entry_theme.draw(canvas, item_properties) + canvas.translate(0, item_height) + y += item_height + if y + item_height > self.theme.screen.height: + break + canvas.restore() \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_default.py b/src/plugins/indicator-messages/default/indicator_messages_default_default.py new file mode 100644 index 0000000..1a8a6fa --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_default.py @@ -0,0 +1,10 @@ +import indicator_messages_default_common + +class Theme(indicator_messages_default_common.Theme): + + + def __init__(self, screen, theme): + indicator_messages_default_common.Theme.__init__(self, screen, theme) + + def paint_foreground(self, canvas, properties, attributes, args): + indicator_messages_default_common.Theme.paint_foreground(self, canvas, properties, attributes, args, 1, 12) \ No newline at end of file diff --git a/src/plugins/indicator-messages/default/indicator_messages_default_g19.py b/src/plugins/indicator-messages/default/indicator_messages_default_g19.py new file mode 100644 index 0000000..8ff2179 --- /dev/null +++ b/src/plugins/indicator-messages/default/indicator_messages_default_g19.py @@ -0,0 +1,9 @@ +import indicator_messages_default_common + +class Theme(indicator_messages_default_common.Theme): + + def __init__(self, screen, theme): + indicator_messages_default_common.Theme.__init__(self, screen, theme) + + def paint_foreground(self, canvas, properties, attributes, args): + indicator_messages_default_common.Theme.paint_foreground(self, canvas, properties, attributes, args, 0, 42) \ No newline at end of file diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po b/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po new file mode 100644 index 0000000..888ba8c --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/indicator-messages.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "Indicator Messages Preferences" + +#: i18n/indicator-messages.glade.h:2 +msgid "Raise page when menu changes" +msgstr "Raise page when menu changes" + +#: i18n/indicator-messages.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/indicator-messages.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/indicator-messages.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/indicator-messages.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/indicator-messages.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.glade.h b/src/plugins/indicator-messages/i18n/indicator-messages.glade.h new file mode 100644 index 0000000..61db099 --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Indicator Messages Preferences"); +char *s = N_("Raise page when menu changes"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/indicator-messages/i18n/indicator-messages.pot b/src/plugins/indicator-messages/i18n/indicator-messages.pot new file mode 100644 index 0000000..243e48b --- /dev/null +++ b/src/plugins/indicator-messages/i18n/indicator-messages.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/indicator-messages.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "" + +#: i18n/indicator-messages.glade.h:2 +msgid "Raise page when menu changes" +msgstr "" + +#: i18n/indicator-messages.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/indicator-messages.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/indicator-messages.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/indicator-messages.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/indicator-messages.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/indicator-messages/indicator-messages.py b/src/plugins/indicator-messages/indicator-messages.py new file mode 100644 index 0000000..8d4be2f --- /dev/null +++ b/src/plugins/indicator-messages/indicator-messages.py @@ -0,0 +1,258 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("indicator-messages", modfile = __file__).ugettext + +import gnome15.g15globals as g15globals +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gobject +import time +import dbus +import os +import gtk +from PIL import Image +import gnome15.dbusmenu as dbusmenu + +from lxml import etree + +import logging +logger = logging.getLogger(__name__) + +# Only works in Unity +if not "XDG_CURRENT_DESKTOP" in os.environ or os.environ["XDG_CURRENT_DESKTOP"] != "Unity": + raise Exception("Only works in Ubuntu Unity desktop") + +# Plugin details - All of these must be provided +id="indicator-messages" +name=_("Indicator Messages") +description=_("Indicator that shows waiting messages.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Activate item") + } + +def create(gconf_key, gconf_client, screen): + return G15IndicatorMessages(gconf_client, gconf_key, screen) + +''' +Indicator Messages DBUSMenu property names +''' + +APP_RUNNING = "app-running" +INDICATOR_LABEL = "indicator-label" +INDICATOR_ICON = "indicator-icon" +RIGHT_SIDE_TEXT = "right-side-text" + +''' +Indicator Messages DBUSMenu types +''' +TYPE_APPLICATION_ITEM = "application-item" +TYPE_INDICATOR_ITEM = "indicator-item" + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "indicator-messages.ui")) + dialog = widget_tree.get_object("IndicatorMessagesDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +class IndicatorMessagesMenuEntry(dbusmenu.DBUSMenuEntry): + def __init__(self, id, properties, menu): + dbusmenu.DBUSMenuEntry.__init__(self, id, properties, menu) + + def set_properties(self, properties): + dbusmenu.DBUSMenuEntry.set_properties(self, properties) + if self.type == TYPE_INDICATOR_ITEM and INDICATOR_LABEL in self.properties: + self.label = self.properties[INDICATOR_LABEL] + if self.type == TYPE_INDICATOR_ITEM: + self.icon = self.properties[INDICATOR_ICON] if INDICATOR_ICON in self.properties else None + + def get_alt_label(self): + return self.properties[RIGHT_SIDE_TEXT] if RIGHT_SIDE_TEXT in self.properties else "" + + def is_app_running(self): + return APP_RUNNING in self.properties and self.properties[APP_RUNNING] + +class IndicatorMessagesMenu(dbusmenu.DBUSMenu): + def __init__(self, session_bus, on_change = None): + try: + dbusmenu.DBUSMenu.__init__(self, session_bus, "com.canonical.indicator.messages", "/com/canonical/indicator/messages/menu", "com.canonical.dbusmenu", on_change, True) + except dbus.DBusException as dbe: + logger.debug("Could not create DBUS menu, trying alternative", exc_info = dbe) + dbusmenu.DBUSMenu.__init__(self, session_bus, "org.ayatana.indicator.messages", "/org/ayatana/indicator/messages/menu", "org.ayatana.dbusmenu", on_change, False) + + def create_entry(self, id, properties): + return IndicatorMessagesMenuEntry(id, properties, self) + + +class G15IndicatorMessages(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["indicator-messages"], id, name) + self._hide_timer = None + self._session_bus = None + self._gconf_client = gconf_client + self._session_bus = dbus.SessionBus() + + def activate(self): + self._status_icon = None + self._raise_timer = None + self._attention = False + self._light_control = None + + g15plugin.G15MenuPlugin.activate(self) + + # Start listening for events + if self._messages_menu.natty: + self._session_bus.add_signal_receiver(self._icon_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.add_signal_receiver(self._attention_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "AttentionChanged") + else: + self._session_bus.add_signal_receiver(self._icon_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.add_signal_receiver(self._attention_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "AttentionChanged") + + def create_menu(self): + self._messages_menu = IndicatorMessagesMenu(self._session_bus) + self._messages_menu.on_change = self._menu_changed + self._check_status() + return g15theme.DBusMenu(self._messages_menu) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self._stop_blink() + if self._messages_menu.natty: + self._session_bus.remove_signal_receiver(self._icon_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.remove_signal_receiver(self._attention_changed, dbus_interface = "com.canonical.indicator.messages.service", signal_name = "AttentionChanged") + else: + self._session_bus.remove_signal_receiver(self._icon_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "IconChanged") + self._session_bus.remove_signal_receiver(self._attention_changed, dbus_interface = "org.ayatana.indicator.messages.service", signal_name = "AttentionChanged") + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.panel_painter = self._paint_panel + return page + + def get_theme_properties(self): + return { + "title" : _("Messages"), + "alt_title" : "", + "icon" : g15icontools.get_icon_path("indicator-messages-new" if self._attention else "indicator-messages"), + "attention": self._attention + } + + ''' + Messages Service callbacks + ''' + + def _icon_changed(self, new_icon): + pass + + def _attention_changed(self, attention): + self._attention = attention + if self._attention == 1: + self._start_blink() + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-new.gif")) + else: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path("indicator-messages-new")) + self._popup() + else: + self._stop_blink() + if self.screen.driver.get_bpp() == 16: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path("indicator-messages")) + self.screen.redraw() + + def _menu_changed(self, menu = None, property = None, value = None): +# self._messages_menu.menu_changed(menu, property, value) + self._popup() + + def _check_status(self): + """ + indicator-messages replaces indicator-me from Oneiric, so we get the current status icon if available + to show that on the panel too + """ + self._status_icon = None + for c in self._messages_menu.menu_map: + menu_entry = self._messages_menu.menu_map[c] + if menu_entry.toggle_type == dbusmenu.TOGGLE_TYPE_RADIO and menu_entry.toggle_state == 1: + icon_name = menu_entry.get_icon_name() + if icon_name is not None and \ + icon_name in [ "user-available", "user-away", + "user-busy", "user-offline", + "user-invisible", "user-indeterminate" ]: + self._status_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(icon_name)) + + ''' + Private + ''' + + def _start_blink(self): + if not self._light_control: + self._light_control = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + self._light_control.blink(off_val = self._get_mkey_value) + + def _get_mkey_value(self): + return g15driver.get_mask_for_memory_bank(self.screen.get_memory_bank()) + + def _stop_blink(self): + if self._light_control: + self.screen.driver.release_control(self._light_control) + self._light_control = None + + def _popup(self): + self._check_status() + if g15gconf.get_bool_or_default(self.gconf_client,"%s/raise" % self.gconf_key, True): + if not self.page.is_visible(): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + else: + self._reset_raise() + + def _reset_raise(self): + ''' + Reset the timer if the page is already visible because of a timer + ''' + if self.screen.is_on_timer(self.page): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None: + t = 0 + if self.thumb_icon != None and self._attention == 1: + t += g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + if self._status_icon != None: + t += g15cairo.paint_thumbnail_image(allocated_size, self._status_icon, canvas) + return t diff --git a/src/plugins/indicator-messages/indicator-messages.ui b/src/plugins/indicator-messages/indicator-messages.ui new file mode 100644 index 0000000..bc636e2 --- /dev/null +++ b/src/plugins/indicator-messages/indicator-messages.ui @@ -0,0 +1,101 @@ + + + + + + + False + 5 + Indicator Messages Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when menu changes + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/indicator-messages/mono-mail-error.gif b/src/plugins/indicator-messages/mono-mail-error.gif new file mode 100644 index 0000000000000000000000000000000000000000..a616bfa7e5e026cd25a7b233e3c0c65094c0198a GIT binary patch literal 72 zcmZ?wbhEHbWM*JvXkcUj0xl0I&BFL;wH) literal 0 HcmV?d00001 diff --git a/src/plugins/keyhelp/Makefile.am b/src/plugins/keyhelp/Makefile.am new file mode 100644 index 0000000..28c5211 --- /dev/null +++ b/src/plugins/keyhelp/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/keyhelp +plugin_DATA = keyhelp.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/keyhelp/default/Makefile.am b/src/plugins/keyhelp/default/Makefile.am new file mode 100644 index 0000000..0f3abec --- /dev/null +++ b/src/plugins/keyhelp/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/macros/default +theme_DATA = g19-menu-screen.svg \ + g19-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/keyhelp/default/default-menu-entry.svg b/src/plugins/keyhelp/default/default-menu-entry.svg new file mode 100644 index 0000000..b160dae --- /dev/null +++ b/src/plugins/keyhelp/default/default-menu-entry.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_key} + + + + ${item_name} + ${item_key} + + diff --git a/src/plugins/keyhelp/default/default-menu-screen.svg b/src/plugins/keyhelp/default/default-menu-screen.svg new file mode 100644 index 0000000..c1f3add --- /dev/null +++ b/src/plugins/keyhelp/default/default-menu-screen.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + + ${mkey} + No Macros Configured on ${mkey} + + diff --git a/src/plugins/keyhelp/default/g19.svg b/src/plugins/keyhelp/default/g19.svg new file mode 100644 index 0000000..b9e5456 --- /dev/null +++ b/src/plugins/keyhelp/default/g19.svg @@ -0,0 +1,487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + ${view} + ${clear} + + + Ok + + + + + + + + + + + + + + + + + + + + + + + Menu + ${select} + ${up} + ${down} + ${right} + ${left} + + diff --git a/src/plugins/keyhelp/keyhelp.py b/src/plugins/keyhelp/keyhelp.py new file mode 100644 index 0000000..00729ef --- /dev/null +++ b/src/plugins/keyhelp/keyhelp.py @@ -0,0 +1,125 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("keyhelp", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15actions as g15actions +import gnome15.g15devices as g15devices +import gnome15.g15profile as g15profile +import logging +import os +logger = logging.getLogger(__name__) + +# Actions +SHOW_KEY_HELP = 'key-help' + +# Register the action with all supported models +g15devices.g15_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_HELD) +g15devices.z10_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SHOW_KEY_HELP] = g15actions.ActionBinding(SHOW_KEY_HELP, [ g15driver.G_KEY_M1 ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="keyhelp" +name=_("Key Help Screen") +description=_("Displays key bindings on the current screen showing what\n\ +actions are available for a particular plugin.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + SHOW_KEY_HELP : _("Show Key Help"), + } + +def create(gconf_key, gconf_client, screen): + return G15KeyHelp(gconf_client, gconf_key, screen) + + +class G15KeyHelp(g15plugin.G15Plugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + + def activate(self): + self._keyhelp = None + g15plugin.G15Plugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self._hide_keyhelp() + g15plugin.G15Plugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if binding.action == SHOW_KEY_HELP: + if self._keyhelp is None: + self._show_keyhelp() + else: + self._hide_keyhelp() + return True + + def _hide_keyhelp(self): + if self._keyhelp is not None: + self._keyhelp.remove_from_parent() + self._keyhelp = None + + def _get_theme_properties(self): + if self.screen.driver.get_model_name() == g15driver.MODEL_G19: + return { "up": self._get_key_help(g15driver.G_KEY_UP, g15driver.KEY_STATE_UP), + "down": self._get_key_help(g15driver.G_KEY_DOWN, g15driver.KEY_STATE_UP), + "left": self._get_key_help(g15driver.G_KEY_LEFT, g15driver.KEY_STATE_UP), + "right": self._get_key_help(g15driver.G_KEY_RIGHT, g15driver.KEY_STATE_UP), + "select": self._get_key_help(g15driver.G_KEY_OK, g15driver.KEY_STATE_UP), + "view": self._get_key_help(g15driver.G_KEY_SETTINGS, g15driver.KEY_STATE_UP), + "clear": self._get_key_help(g15driver.G_KEY_BACK, g15driver.KEY_STATE_UP) + } + + # TODO + return {} + + def _get_key_help(self, key, state): + page = self.screen.get_visible_page() + originating_plugin = page.originating_plugin + if originating_plugin: + import gnome15.g15pluginmanager as g15pluginmanager + actions = g15pluginmanager.get_actions(g15pluginmanager.get_module_for_id(originating_plugin.__module__), self.screen.device) + active_profile = g15profile.get_active_profile(self.screen.driver.device) if self.screen.driver is not None else None + for action_id in actions: + # First try the active profile to see if the action has been re-mapped + action_binding = active_profile.get_binding_for_action(state, action_id) + if action_binding is None: + # No other keys bound to action, try the device defaults + device_info = g15devices.get_device_info(self.screen.driver.get_model_name()) + if action_id in device_info.action_keys: + action_binding = device_info.action_keys[action_id] + + if action_binding is not None and key in action_binding.keys: + return actions[action_id] + + return "?" + + def _show_keyhelp(self): + self._keyhelp = g15theme.Component("glasspane") + self._keyhelp.get_theme_properties = self._get_theme_properties + page = self.screen.get_visible_page() + page.add_child(self._keyhelp) + self._keyhelp.set_theme(g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"))) + page.redraw() \ No newline at end of file diff --git a/src/plugins/lcdbiff/Makefile.am b/src/plugins/lcdbiff/Makefile.am new file mode 100644 index 0000000..81c653f --- /dev/null +++ b/src/plugins/lcdbiff/Makefile.am @@ -0,0 +1,13 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/lcdbiff +plugin_DATA = imap.ui \ + lcdbiff.py \ + mono-mail-new.gif \ + mono-mail-error.gif \ + mono-mail-refresh.gif \ + password.ui \ + pop3.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/lcdbiff/default/Makefile.am b/src/plugins/lcdbiff/default/Makefile.am new file mode 100644 index 0000000..1051af0 --- /dev/null +++ b/src/plugins/lcdbiff/default/Makefile.am @@ -0,0 +1,26 @@ +themedir = $(datadir)/gnome15/plugins/lcdbiff/default +theme_DATA = \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/lcdbiff/default/default-menu-entry.svg b/src/plugins/lcdbiff/default/default-menu-entry.svg new file mode 100644 index 0000000..7d244b7 --- /dev/null +++ b/src/plugins/lcdbiff/default/default-menu-entry.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + ${item_name} + ${item_alt} + + + diff --git a/src/plugins/lcdbiff/i18n/imap.en_GB.po b/src/plugins/lcdbiff/i18n/imap.en_GB.po new file mode 100644 index 0000000..ee7bead --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.en_GB.po @@ -0,0 +1,48 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/imap.glade.h:1 +msgid "Advanced" +msgstr "Advanced" + +#: i18n/imap.glade.h:2 +msgid "Folder" +msgstr "Folder" + +#: i18n/imap.glade.h:3 +msgid "Server:" +msgstr "Server:" + +#: i18n/imap.glade.h:4 +msgid "Use SSL" +msgstr "Use SSL" + +#: i18n/imap.glade.h:5 +msgid "Username" +msgstr "Username" + +#: i18n/imap.glade.h:6 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" +msgstr "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" diff --git a/src/plugins/lcdbiff/i18n/imap.glade.h b/src/plugins/lcdbiff/i18n/imap.glade.h new file mode 100644 index 0000000..8b8c7a9 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.glade.h @@ -0,0 +1,8 @@ +char *s = N_("Advanced"); +char *s = N_("Folder"); +char *s = N_("Server:"); +char *s = N_("Use SSL"); +char *s = N_("Username"); +char *s = N_("You may enter a hostname or IP address to use the\n" + "default port, or suffix the hostname with a colon and\n" + "the port number, e.g. mail.mycompany.com:143"); diff --git a/src/plugins/lcdbiff/i18n/imap.pot b/src/plugins/lcdbiff/i18n/imap.pot new file mode 100644 index 0000000..08636cf --- /dev/null +++ b/src/plugins/lcdbiff/i18n/imap.pot @@ -0,0 +1,45 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/imap.glade.h:1 +msgid "Advanced" +msgstr "" + +#: i18n/imap.glade.h:2 +msgid "Folder" +msgstr "" + +#: i18n/imap.glade.h:3 +msgid "Server:" +msgstr "" + +#: i18n/imap.glade.h:4 +msgid "Use SSL" +msgstr "" + +#: i18n/imap.glade.h:5 +msgid "Username" +msgstr "" + +#: i18n/imap.glade.h:6 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:143" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po b/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po new file mode 100644 index 0000000..f997f1b --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/lcdbiff.glade.h:1 +msgid "Account Options" +msgstr "Account Options" + +#: i18n/lcdbiff.glade.h:2 +msgid "Accounts" +msgstr "Accounts" + +#: i18n/lcdbiff.glade.h:3 +msgid "Options" +msgstr "Options" + +#: i18n/lcdbiff.glade.h:4 +msgid "Check every" +msgstr "Check every" + +#: i18n/lcdbiff.glade.h:5 +msgid "IMAP" +msgstr "IMAP" + +#: i18n/lcdbiff.glade.h:6 +msgid "Mail Notification Preferences" +msgstr "Mail Notification Preferences" + +#: i18n/lcdbiff.glade.h:7 +msgid "POP3" +msgstr "POP3" + +#: i18n/lcdbiff.glade.h:8 +msgid "Type:" +msgstr "Type:" + +#: i18n/lcdbiff.glade.h:9 +msgid "minutes" +msgstr "minutes" + +#: i18n/lcdbiff.glade.h:10 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/lcdbiff.glade.h:11 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.glade.h b/src/plugins/lcdbiff/i18n/lcdbiff.glade.h new file mode 100644 index 0000000..cf5ceba --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Account Options"); +char *s = N_("Accounts"); +char *s = N_("Options"); +char *s = N_("Check every"); +char *s = N_("IMAP"); +char *s = N_("Mail Notification Preferences"); +char *s = N_("POP3"); +char *s = N_("Type:"); +char *s = N_("minutes"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/lcdbiff/i18n/lcdbiff.pot b/src/plugins/lcdbiff/i18n/lcdbiff.pot new file mode 100644 index 0000000..11a8dab --- /dev/null +++ b/src/plugins/lcdbiff/i18n/lcdbiff.pot @@ -0,0 +1,62 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/lcdbiff.glade.h:1 +msgid "Account Options" +msgstr "" + +#: i18n/lcdbiff.glade.h:2 +msgid "Accounts" +msgstr "" + +#: i18n/lcdbiff.glade.h:3 +msgid "Options" +msgstr "" + +#: i18n/lcdbiff.glade.h:4 +msgid "Check every" +msgstr "" + +#: i18n/lcdbiff.glade.h:5 +msgid "IMAP" +msgstr "" + +#: i18n/lcdbiff.glade.h:6 +msgid "Mail Notification Preferences" +msgstr "" + +#: i18n/lcdbiff.glade.h:7 +msgid "POP3" +msgstr "" + +#: i18n/lcdbiff.glade.h:8 +msgid "Type:" +msgstr "" + +#: i18n/lcdbiff.glade.h:9 +msgid "minutes" +msgstr "" + +#: i18n/lcdbiff.glade.h:10 +msgid "toolbutton1" +msgstr "" + +#: i18n/lcdbiff.glade.h:11 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/password.en_GB.po b/src/plugins/lcdbiff/i18n/password.en_GB.po new file mode 100644 index 0000000..f6e7adc --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.en_GB.po @@ -0,0 +1,38 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/password.glade.h:1 +msgid "Cancel" +msgstr "Cancel" + +#: i18n/password.glade.h:2 +msgid "Email Notification Password" +msgstr "Email Notification Password" + +#: i18n/password.glade.h:3 +msgid "Ok" +msgstr "Ok" + +#: i18n/password.glade.h:4 +msgid "Password" +msgstr "Password" + +#: i18n/password.glade.h:5 +msgid "Password Text" +msgstr "Password Text" diff --git a/src/plugins/lcdbiff/i18n/password.glade.h b/src/plugins/lcdbiff/i18n/password.glade.h new file mode 100644 index 0000000..ccd7ad3 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.glade.h @@ -0,0 +1,5 @@ +char *s = N_("Cancel"); +char *s = N_("Email Notification Password"); +char *s = N_("Ok"); +char *s = N_("Password"); +char *s = N_("Password Text"); diff --git a/src/plugins/lcdbiff/i18n/password.pot b/src/plugins/lcdbiff/i18n/password.pot new file mode 100644 index 0000000..0a372f5 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/password.pot @@ -0,0 +1,38 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/password.glade.h:1 +msgid "Cancel" +msgstr "" + +#: i18n/password.glade.h:2 +msgid "Email Notification Password" +msgstr "" + +#: i18n/password.glade.h:3 +msgid "Ok" +msgstr "" + +#: i18n/password.glade.h:4 +msgid "Password" +msgstr "" + +#: i18n/password.glade.h:5 +msgid "Password Text" +msgstr "" diff --git a/src/plugins/lcdbiff/i18n/pop3.en_GB.po b/src/plugins/lcdbiff/i18n/pop3.en_GB.po new file mode 100644 index 0000000..454f8b3 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.en_GB.po @@ -0,0 +1,44 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/pop3.glade.h:1 +msgid "Advanced" +msgstr "Advanced" + +#: i18n/pop3.glade.h:2 +msgid "Server:" +msgstr "Server:" + +#: i18n/pop3.glade.h:3 +msgid "Use SSL" +msgstr "Use SSL" + +#: i18n/pop3.glade.h:4 +msgid "Username" +msgstr "Username" + +#: i18n/pop3.glade.h:5 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" +msgstr "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" diff --git a/src/plugins/lcdbiff/i18n/pop3.glade.h b/src/plugins/lcdbiff/i18n/pop3.glade.h new file mode 100644 index 0000000..6627d15 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Advanced"); +char *s = N_("Server:"); +char *s = N_("Use SSL"); +char *s = N_("Username"); +char *s = N_("You may enter a hostname or IP address to use the\n" + "default port, or suffix the hostname with a colon and\n" + "the port number, e.g. mail.mycompany.com:110"); diff --git a/src/plugins/lcdbiff/i18n/pop3.pot b/src/plugins/lcdbiff/i18n/pop3.pot new file mode 100644 index 0000000..aa3fd39 --- /dev/null +++ b/src/plugins/lcdbiff/i18n/pop3.pot @@ -0,0 +1,41 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/pop3.glade.h:1 +msgid "Advanced" +msgstr "" + +#: i18n/pop3.glade.h:2 +msgid "Server:" +msgstr "" + +#: i18n/pop3.glade.h:3 +msgid "Use SSL" +msgstr "" + +#: i18n/pop3.glade.h:4 +msgid "Username" +msgstr "" + +#: i18n/pop3.glade.h:5 +msgid "" +"You may enter a hostname or IP address to use the\n" +"default port, or suffix the hostname with a colon and\n" +"the port number, e.g. mail.mycompany.com:110" +msgstr "" diff --git a/src/plugins/lcdbiff/imap.ui b/src/plugins/lcdbiff/imap.ui new file mode 100644 index 0000000..2b89798 --- /dev/null +++ b/src/plugins/lcdbiff/imap.ui @@ -0,0 +1,175 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Server: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:143 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + True + True + 4 + + + True + False + 4 + + + True + False + + + True + False + Folder + + + False + False + 0 + + + + + True + True + + False + False + True + True + + + True + True + 4 + 1 + + + + + True + True + 0 + + + + + Use SSL + True + True + False + True + + + True + True + 1 + + + + + + + True + False + Advanced + + + + + False + False + 4 + 1 + + + + + + diff --git a/src/plugins/lcdbiff/lcdbiff.py b/src/plugins/lcdbiff/lcdbiff.py new file mode 100644 index 0000000..ff33a7d --- /dev/null +++ b/src/plugins/lcdbiff/lcdbiff.py @@ -0,0 +1,517 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("lcdbiff", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import os, os.path +import pwd +import gtk +import re +from poplib import POP3_SSL +from poplib import POP3 +from imaplib import IMAP4 +from imaplib import IMAP4_SSL + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "lcdbiff" +name = _("POP3 / IMAP Email Notification") +description = _("Periodically checks your email accounts for any waiting messages. Currently supports POP3 and IMAP \ +protocols. For models without a screen, the M-Key lights will be flashed when there is an email \ +waiting. For models with a screen, a page showing all unread mail counts will be displayed, and an \ +icon added to the panel indicating overall status.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network = True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Compose new mail"), + g15driver.VIEW : _("Check mail status") + } + +# Constants +CURRENT_USERNAME=pwd.getpwuid(os.getuid())[0] +PROTO_POP3 = "pop3" +PROTO_IMAP = "imap" +TYPES = [ PROTO_POP3, PROTO_IMAP ] +CONFIG_PATH = os.path.join(g15globals.user_config_dir, "plugin-data" , "lcdbiff", "mailboxes.xml") +CONFIG_ITEM_NAME = "mailbox" + +def create(gconf_key, gconf_client, screen): + return G15Biff(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15BiffPreferences(parent, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 10 + return val + +''' +Abstract mail checker. Subclasses are responsible for connecting +to mail stores and retrieving the number of unread messages. +''' +class Checker(): + + def __init__(self, account_manager): + self.account_manager = account_manager + + def get_username(self, account): + username = account.get_property("username", "") + return username if username != "" else CURRENT_USERNAME + + def get_hostname(self, account): + hostname = account.get_property("server", "") + pre, _, _ = hostname.partition(":") + return pre if pre != "" else "localhost" + + def get_port_or_default(self, account, default_port): + hostname = account.get_property("server", "") + _, sep, post = hostname.partition(":") + if sep == "": + return default_port + return int(post) + + def save_password(self, account, password, default_port): + hostname = self.get_hostname(account) + port = self.get_port_or_default(account, default_port) + self.account_manager.store_password(account, password, hostname, port) + + def get_password(self, account, default_port, force_dialog = False): + hostname = self.get_hostname(account) + port = self.get_port_or_default(account, default_port) + return self.account_manager.retrieve_password(account, hostname, port, force_dialog) + +''' +POP3 checker. Does the actual work of checking for emails using +the POP3 protocol. +''' +class POP3Checker(Checker): + + + def __init__(self, account_manager): + Checker.__init__(self, account_manager) + + def check(self, account): + ssl = account.get_property("ssl", "false") + default_port = 995 if ssl else 110 + port = self.get_port_or_default(account, default_port) + if ssl: + pop = POP3_SSL(self.get_hostname(account), port) + else: + pop = POP3(self.get_hostname(account), port, 7.0) + try : + username = self.get_username(account) + for i in range(0, 3): + password = self.get_password(account, default_port, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + try : + pop.user(username) + pop.pass_(password) + self.save_password(account, password, default_port) + return pop.stat() + except Exception as e: + logger.debug("Error while checking", exc_info = e) + try : + pop.apop(username, password) + self.save_password(account, password, default_port) + return pop.stat() + except Exception as e2: + logger.debug("Error while checking", exc_info = e2) + finally : + pop.quit() + return (0, 0) + +''' +IMAP checker. Does the actual work of checking for emails using +the IMAP protocol. +''' +class IMAPChecker(Checker): + + def __init__(self, account_manager): + Checker.__init__(self, account_manager) + + def check(self, account): + ssl = account.get_property("ssl", "false") + folder = account.get_property("folder", "INBOX") + default_port = 993 if ssl else 143 + port = self.get_port_or_default(account, default_port) + count = ( 0, 0 ) + username = self.get_username(account) + for i in range(0, 3): + + for j in range(0, 2): + if ssl: + imap = IMAP4_SSL(self.get_hostname(account), port) + else: + imap = IMAP4(self.get_hostname(account), port) + + try : + password = self.get_password(account, default_port, i > 0) + if password == None or password == "": + raise Exception(_("Authentication cancelled")) + + try : + if j == 0: + imap.login(username, password) + else: + imap.login_cram_md5(username, password) + self.save_password(account, password, default_port) + status = imap.status(folder, "(UNSEEN)") + unread = int(re.search("UNSEEN (\d+)", status[1][0]).group(1)) + count = ( unread, count ) + return count + except Exception as e: + logger.debug("Error while checking", exc_info = e) + + finally: + imap.logout() + + return count + + +''' +Superclass of the UI mail protocol specific configuration. Currently +all types support server, username and SSL options, although +this may change in future +''' +class G15BiffOptions(g15accounts.G15AccountOptions): + def __init__(self, account, account_ui): + g15accounts.G15AccountOptions.__init__(self, account, account_ui) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "%s.ui" % account.type)) + self.component = self.widget_tree.get_object("OptionPanel") + + # Both currently have server, username and SSL widgets + server = self.widget_tree.get_object("Server") + username = self.widget_tree.get_object("Username") + ssl = self.widget_tree.get_object("SSL") + + # Events + server.connect("changed", self._server_changed) + username.connect("changed", self._username_changed) + ssl.connect("toggled", self._ssl_changed) + + # Set initial values + server.set_text(self.account.properties["server"] if "server" in self.account.properties else "") + username.set_text(self.account.properties["username"] if "username" in self.account.properties else "") + ssl.set_active(self.account.properties["ssl"] == "true" if "ssl" in self.account.properties else False) + + def _server_changed(self, widget): + self.account.properties["server"] = widget.get_text() + self.account_ui.save_accounts() + + def _ssl_changed(self, widget): + self.account.properties["ssl"] = "true" if widget.get_active() else "false" + self.account_ui.save_accounts() + + def _username_changed(self, widget): + self.account.properties["username"] = widget.get_text() + self.account_ui.save_accounts() + +''' +POP3 configuration UI +''' +class G15BiffPOP3Options(G15BiffOptions): + + + def __init__(self, account, account_ui): + G15BiffOptions.__init__(self, account, account_ui) + +''' +IMAP configuration UI. Adds the additional Folder widget +''' +class G15BiffIMAPOptions(G15BiffOptions): + + def __init__(self, account, account_ui): + G15BiffOptions.__init__(self, account, account_ui) + folder = self.widget_tree.get_object("Folder") + folder.connect("changed", self._folder_changed) + folder.set_text(self.account.properties["folder"] if "folder" in self.account.properties else "INBOX") + + def _folder_changed(self, widget): + self.account.properties["folder"] = widget.get_text() + self.account_ui.save_accounts() + +''' +Configuration UI +''' +class G15BiffPreferences(g15accounts.G15AccountPreferences): + + + def __init__(self, parent, gconf_client, gconf_key): + g15accounts.G15AccountPreferences.__init__(self, parent, gconf_client, \ + gconf_key, \ + CONFIG_PATH, \ + CONFIG_ITEM_NAME, \ + 10) + + def get_account_types(self): + return [ PROTO_POP3, PROTO_IMAP ] + + def get_account_type_name(self, account_type): + return _(account_type) + + def create_options_for_type(self, account, account_type): + if account_type == PROTO_POP3: + return G15BiffPOP3Options(account, self) + else: + return G15BiffIMAPOptions(account, self) + + +''' +Account menu item +''' + +class MailItem(g15theme.MenuItem): + def __init__(self, component_id, gconf_client, account, plugin): + g15theme.MenuItem.__init__(self, component_id) + self.account = account + self.count = 0 + self.gconf_client = gconf_client + self.status = "Unknown" + self.error = None + self.plugin = plugin + self.refreshing = False + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.account.name + if self.error != None: + item_properties["item_alt"] = _("Error") + else: + if self.count > 0: + item_properties["item_alt"] = "%d" % ( self.count ) + else: + item_properties["item_alt"] = _("None") + item_properties["item_type"] = "" + + if self.refreshing: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-refresh.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path(["view-refresh", "stock_refresh", "gtk-refresh", "view-refresh-symbolic"]) + elif self.error is not None: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-error.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path("new-messages-red") + else: + if self.count > 0: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = os.path.join(os.path.dirname(__file__), "mono-mail-new.gif") + else: + item_properties["item_icon"] = g15icontools.get_icon_path("indicator-messages-new") + else: + if self.plugin.screen.driver.get_bpp() == 1: + item_properties["item_icon"] = "" + else: + item_properties["item_icon"] = g15icontools.get_icon_path("indicator-messages") + + return item_properties + + def activate(self): + email_client = self.gconf_client.get_string("/desktop/gnome/url-handlers/mailto/command") + logger.info("Running email client %s", email_client) + if email_client != None: + call_str = "%s &" % email_client.replace("%s", "").replace("%U", "mailto:") + os.system(call_str) + +''' +Gnome15 LCDBiff plugin +''' + +class G15Biff(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["mail-inbox", "mail-folder-inbox" ], id, "Email") + self.refresh_timer = None + + def activate(self): + self.total_count = 0 + self.items = [] + self.attention = False + self.thumb_icon = None + self.index = 0 + self.light_control = None + self.account_manager = g15accounts.G15AccountManager(CONFIG_PATH, CONFIG_ITEM_NAME) + self.account_manager.add_change_listener(self) + self.checkers = { PROTO_POP3 : POP3Checker(self.account_manager), PROTO_IMAP: IMAPChecker(self.account_manager) } + if self.screen.driver.get_bpp() > 0: + g15plugin.G15MenuPlugin.activate(self) + self.update_time_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/update_time", self._update_time_changed) + self.schedule_refresh(10.0) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + self._stop_blink() + if self.refresh_timer: + self.refresh_timer.cancel() + self.refresh_timer.task_queue.stop() + self.gconf_client.notify_remove(self.update_time_changed_handle) + + def action_performed(self, binding): + if binding.action == g15driver.VIEW: + if self.refresh_timer: + self.refresh_timer.cancel() + self.refresh() + + def load_menu_items(self): + items = [] + self.account_manager.load() + i = 0 + for account in self.account_manager.accounts: + items.append(MailItem("mailitem-%d" % i, self.gconf_client, account, self)) + i += 1 + if self.screen.driver.get_bpp() != 0: + self.menu.selected = items[0] if len(items) > 0 else None + self.menu.remove_all_children() + self.menu.set_children(items) + self.items = items + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.panel_painter = self._paint_panel + page.thumbnail_painter = self._paint_thumbnail + return page + + def schedule_refresh(self, time = - 1): + if time == -1: + time = get_update_time(self.gconf_client, self.gconf_key) * 60.0 + self.refresh_timer = g15scheduler.queue("lcdbiff-%s" % self.screen.device.uid, "MailRefreshTimer", time, self.refresh) + + def refresh(self): + t_count = 0 + t_errors = 0 + for item in self.items: + try : + item.refreshing = True + self.page.redraw() + status = self._check_account(item.account) + item.count = status[0] + t_count += item.count + item.error = None + item.refreshing = False + except Exception as e: + item.refreshing = False + t_errors += 1 + item.error = e + item.count = 0 + logger.debug("Error while refreshing item %s", str(item), exc_info = e) + + self.total_count = t_count + self.total_errors = t_errors + + if self.total_errors > 0: + self._stop_blink() + self.attention = True + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-error.gif")) + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["new-messages-red","messagebox_critical"])) + else: + if self.total_count > 0: + self._start_blink() + self.attention = True + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = g15cairo.load_surface_from_file(os.path.join(os.path.dirname(__file__), "mono-mail-new.gif")) + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["indicator-messages-new", "mail-message-new"])) + else: + self._stop_blink() + self.attention = False + if self.screen.driver.get_bpp() == 1: + self.thumb_icon = None + elif self.screen.driver.get_bpp() > 0: + self.thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["indicator-messages", "mail-message"])) + + if self.screen.driver.get_bpp() > 0: + self.screen.redraw(self.page) + + self.schedule_refresh() + + ''' + Private + ''' + def _accounts_changed(self, account_manager): + self._reload_menu() + self.schedule_refresh() + + def _check_account(self, account): + return self.checkers[account.type].check(account) + + def _start_blink(self): + if not self.light_control: + self.light_control = self.screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + self.light_control.blink(off_val = self._get_mkey_value) + + def _get_mkey_value(self): + return g15driver.get_mask_for_memory_bank(self.screen.get_memory_bank()) + + def _stop_blink(self): + if self.light_control: + self.screen.driver.release_control(self.light_control) + self.light_control = None + + def _reload_menu(self): + self.load_menu_items() + if self.screen.driver.get_bpp() == 1: + self.screen.redraw(self.page) + + def _update_time_changed(self, client, connection_id, entry, args): + self.refresh_timer.cancel() + self.schedule_refresh() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None: + if self.thumb_icon != None: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + return size + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None: + if self.thumb_icon != None and self.attention: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_icon, canvas) + return size + diff --git a/src/plugins/lcdbiff/mono-mail-error.gif b/src/plugins/lcdbiff/mono-mail-error.gif new file mode 100644 index 0000000000000000000000000000000000000000..a616bfa7e5e026cd25a7b233e3c0c65094c0198a GIT binary patch literal 72 zcmZ?wbhEHbWM*JvXkcUj0xl0I&BFL;wH) literal 0 HcmV?d00001 diff --git a/src/plugins/lcdbiff/mono-mail-refresh.gif b/src/plugins/lcdbiff/mono-mail-refresh.gif new file mode 100644 index 0000000000000000000000000000000000000000..af7a2c1fc3945a378afa309db3076c975dcc5f7c GIT binary patch literal 84 zcmZ?wbhEHb%3QEFmIYKlU6W=V!ZNJgrHyQgmegW^vXMlJ>> c1|5)ckVXb3o*u?!l6TtA1@-HNGcZ^K0OQdbHvj+t literal 0 HcmV?d00001 diff --git a/src/plugins/lcdbiff/password.ui b/src/plugins/lcdbiff/password.ui new file mode 100644 index 0000000..5e84ae8 --- /dev/null +++ b/src/plugins/lcdbiff/password.ui @@ -0,0 +1,159 @@ + + + + + + False + 5 + Email Notification Password + True + center + normal + True + + + True + False + 2 + + + True + False + end + + + Ok + True + True + True + True + True + True + True + + + False + False + 0 + + + + + Cancel + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + gtk-dialog-question + 6 + + + True + True + 0 + + + + + True + False + 8 + + + True + False + Password Text + True + + + False + False + 0 + + + + + True + False + + + True + False + Password + + + True + False + 0 + + + + + True + True + False + + True + False + False + True + True + + + True + False + 1 + + + + + True + True + 1 + + + + + False + False + 1 + + + + + True + True + 1 + + + + + + togglebutton1 + button1 + + + diff --git a/src/plugins/lcdbiff/pop3.ui b/src/plugins/lcdbiff/pop3.ui new file mode 100644 index 0000000..ddf128b --- /dev/null +++ b/src/plugins/lcdbiff/pop3.ui @@ -0,0 +1,122 @@ + + + + + + False + + + True + False + + + True + False + 2 + 2 + 8 + 8 + + + True + False + 0 + Server: + + + GTK_FILL + + + + + + True + False + 0 + Username + + + 1 + 2 + GTK_FILL + + + + + + True + True + You may enter a hostname or IP address to use the +default port, or suffix the hostname with a colon and +the port number, e.g. mail.mycompany.com:110 + + False + False + True + True + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 8 + 0 + + + + + True + True + + + Use SSL + True + True + False + True + + + + + True + False + Advanced + + + + + False + False + 4 + 1 + + + + + + diff --git a/src/plugins/lcdshot/Makefile.am b/src/plugins/lcdshot/Makefile.am new file mode 100644 index 0000000..b648a12 --- /dev/null +++ b/src/plugins/lcdshot/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/lcdshot +plugin_DATA = lcdshot.py \ + lcdshot.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/lcdshot/i18n/lcdshot.en_GB.po b/src/plugins/lcdshot/i18n/lcdshot.en_GB.po new file mode 100644 index 0000000..ac2c6ab --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.en_GB.po @@ -0,0 +1,50 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/lcdshot.glade.h:1 +msgid "Choose A Destination Folder For The LCD Shots" +msgstr "Choose A Destination Folder For The LCD Shots" + +#: i18n/lcdshot.glade.h:2 +msgid "Folder" +msgstr "Folder" + +#: i18n/lcdshot.glade.h:3 +msgid "LCD Screenshot Preferences" +msgstr "LCD Screenshot Preferences" + +#: i18n/lcdshot.glade.h:4 +msgid "center" +msgstr "center" + +#: i18n/lcdshot.glade.h:5 +msgid "scale" +msgstr "scale" + +#: i18n/lcdshot.glade.h:6 +msgid "stretch" +msgstr "stretch" + +#: i18n/lcdshot.glade.h:7 +msgid "tile" +msgstr "tile" + +#: i18n/lcdshot.glade.h:8 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/lcdshot/i18n/lcdshot.glade.h b/src/plugins/lcdshot/i18n/lcdshot.glade.h new file mode 100644 index 0000000..3c5b3f2 --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.glade.h @@ -0,0 +1,8 @@ +char *s = N_("Choose A Destination Folder For The LCD Shots"); +char *s = N_("Folder"); +char *s = N_("LCD Screenshot Preferences"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/lcdshot/i18n/lcdshot.pot b/src/plugins/lcdshot/i18n/lcdshot.pot new file mode 100644 index 0000000..18d30a7 --- /dev/null +++ b/src/plugins/lcdshot/i18n/lcdshot.pot @@ -0,0 +1,50 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/lcdshot.glade.h:1 +msgid "Choose A Destination Folder For The LCD Shots" +msgstr "" + +#: i18n/lcdshot.glade.h:2 +msgid "Folder" +msgstr "" + +#: i18n/lcdshot.glade.h:3 +msgid "LCD Screenshot Preferences" +msgstr "" + +#: i18n/lcdshot.glade.h:4 +msgid "center" +msgstr "" + +#: i18n/lcdshot.glade.h:5 +msgid "scale" +msgstr "" + +#: i18n/lcdshot.glade.h:6 +msgid "stretch" +msgstr "" + +#: i18n/lcdshot.glade.h:7 +msgid "tile" +msgstr "" + +#: i18n/lcdshot.glade.h:8 +msgid "zoom" +msgstr "" diff --git a/src/plugins/lcdshot/lcdshot.py b/src/plugins/lcdshot/lcdshot.py new file mode 100644 index 0000000..e6de1a0 --- /dev/null +++ b/src/plugins/lcdshot/lcdshot.py @@ -0,0 +1,240 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("lcdshot", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15devices as g15devices +import gnome15.g15globals as g15globals +import gnome15.g15actions as g15actions +import os.path +import gtk +import gobject +import gnome15.util.g15convert as g15convert +import gnome15.g15notify as g15notify +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.util.g15cairo as g15cairo +import subprocess +import shutil +from threading import Thread + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Custom actions +SCREENSHOT = "screenshot" + +# Register the action with all supported models +g15devices.g15_action_keys[SCREENSHOT] = g15actions.ActionBinding(SCREENSHOT, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SCREENSHOT] = g15actions.ActionBinding(SCREENSHOT, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="lcdshot" +name=_("LCD Screenshot") +description=_("Takes either a still screenshot or a video of the LCD\n\ +and places it in the configured directory.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35, g15driver.MODEL_Z10 ] +actions={ + SCREENSHOT : "Take LCD screenshot" + } + + +''' +This simple plugin takes a screenshot of the LCD +''' + +def create(gconf_key, gconf_client, screen): + return G15LCDShot(screen, gconf_client, gconf_key) + +def show_preferences(parent, driver, gconf_client, gconf_key): + LCDShotPreferences(parent, driver, gconf_client, gconf_key) + +class LCDShotPreferences(): + def __init__(self, parent, driver, gconf_client, gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "lcdshot.ui")) + dialog = widget_tree.get_object("LCDShotDialog") + dialog.set_transient_for(parent) + chooser = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + chooser.set_default_response(gtk.RESPONSE_OK) + chooser_button = widget_tree.get_object("FileChooserButton") + chooser_button.dialog = chooser + chooser_button.connect("file-set", self._file_set) + chooser_button.connect("file-activated", self._file_activated) + chooser_button.connect("current-folder-changed", self._file_activated) + bg_img = g15gconf.get_string_or_default(self.gconf_client, "%s/folder" % self.gconf_key, os.path.expanduser("~/Desktop")) + chooser_button.set_current_folder(bg_img) + + # Reset the value of the mode setting to 'still' if mencoder is not installed + mencoder_is_installed = g15os.is_program_in_path('mencoder') + if not mencoder_is_installed: + gconf_client.set_string("%s/mode" % self.gconf_key, "still") + + # Initialize the mode combobox content + modes = widget_tree.get_object("ModeModel") + modes.clear() + modes.append(('still','Still', True)) + modes.append(('video','Video', mencoder_is_installed)) + + # Display a warning message to the user if mencoder is not installed + warning = widget_tree.get_object("NoVideoMessage") + warning.set_visible(not mencoder_is_installed) + + g15uigconf.configure_combo_from_gconf(self.gconf_client, "%s/mode" % self.gconf_key, "Mode", "still", widget_tree) + mode = widget_tree.get_object("Mode") + mode.connect("changed", self._mode_changed) + + g15uigconf.configure_spinner_from_gconf(self.gconf_client, "%s/fps" % gconf_key, "FPS", 10, widget_tree, False) + self._spinner = widget_tree.get_object("FPS") + self._mode_changed(mode) + + dialog.run() + dialog.hide() + + def _mode_changed(self, widget): + self._spinner.set_sensitive(widget.get_active() == 1) + + def _file_set(self, widget): + self.gconf_client.set_string(self.gconf_key + "/folder", widget.get_filename()) + + def _file_activated(self, widget): + self.gconf_client.set_string(self.gconf_key + "/folder", widget.get_filename()) + + +class G15LCDShot(): + + def __init__(self, screen, gconf_client, gconf_key): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._recording = False + + def activate(self): + self._screen.key_handler.action_listeners.append(self) + + def deactivate(self): + self._screen.key_handler.action_listeners.remove(self) + + def destroy(self): + pass + + def action_performed(self, binding): + # TODO better key + if binding.action == SCREENSHOT: + mode = g15gconf.get_string_or_default(self._gconf_client, "%s/mode" % self._gconf_key, "still") + if mode == "still": + return self._take_still() + else: + if self._recording: + self._stop_recording() + else: + self._start_recording() + + def _encode(self): + cmd = ["mencoder", "-really-quiet", "mf://%s.tmp/*.jpeg" % self._record_to, "-mf", \ + "w=%d:h=%d:fps=%d:type=jpg" % (self._screen.device.lcd_size[0],self._screen.device.lcd_size[1],self._record_fps), "-ovc", "lavc", \ + "-lavcopts", "vcodec=mpeg4", "-oac", "copy", "-o", self._record_to] + try: + ret = subprocess.call(cmd) + if ret == 0: + g15notify.notify(_("LCD Screenshot"), _("Video encoding complete. Result at %s" % self._record_to), "dialog-info", timeout = 0) + shutil.rmtree("%s.tmp" % self._record_to, True) + else: + logger.error("Video encoding failed with status %d", ret) + g15notify.notify(_("LCD Screenshot"), _("Video encoding failed."), "dialog-error", timeout = 0) + except Exception as e: + logger.error("Video encoding failed.", exc_info = e) + g15notify.notify(_("LCD Screenshot"), _("Video encoding failed. Do you have mencoder installed?"), "dialog-error", timeout = 0) + + def _stop_recording(self): + self._recording = False + g15notify.notify(_("LCD Screenshot"), _("Video recording stopped. Now encoding"), "dialog-info", timeout = 0) + t = Thread(target = self._encode); + t.setName("LCDScreenshotEncode") + t.start() + + def _start_recording(self): + self._record_fps = g15gconf.get_int_or_default(self._gconf_client, "%s/fps" % self._gconf_key, 10) + path = self._find_next_free_filename("avi", _("Gnome15_Video")) + g15notify.notify(_("LCD Screenshot"), _("Started recording video"), "dialog-info") + g15os.mkdir_p("%s.tmp" % path) + self._frame_no = 1 + self._recording = True + self._record_to = path + self._frame() + + def _frame(self): + if self._recording: + try: + self._screen.draw_lock.acquire() + try: + path = os.path.join("%s.tmp" % self._record_to, "%012d.jpeg" % self._frame_no) + pixbuf = g15cairo.surface_to_pixbuf(self._screen.old_surface) + finally: + self._screen.draw_lock.release() + + pixbuf.save(path, "jpeg", {"quality":"100"}) + self._frame_no += 1 + except Exception as e: + logger.error("Failed to save screenshot.", exc_info = e) + self._screen.error_on_keyboard_display(_("Failed to save screenshot to %s. %s") % (dir, str(e))) + self._recording = False + + self._recording_timer = gobject.timeout_add(1000 / self._record_fps, self._frame) + + def _find_next_free_filename(self, ext, title): + dir_path = g15gconf.get_string_or_default(self._gconf_client, "%s/folder" % \ + self._gconf_key, os.path.expanduser("~/Desktop")) + for i in range(1, 9999): + path = "%s/%s-%s-%d.%s" % ( dir_path, \ + g15globals.name, title, i, ext ) + if not os.path.exists(path): + return path + raise Exception("Too many screenshots/videos in destination directory") + + def _take_still(self): + if self._screen.old_surface: + self._screen.draw_lock.acquire() + try: + path = self._find_next_free_filename("png", self._screen.get_visible_page().title) + self._screen.old_surface.write_to_png(path) + logger.info("Written to screenshot to %s", path) + g15notify.notify(_("LCD Screenshot"), _("Screenshot saved to %s") % path, "dialog-info", timeout = 0) + return True + except Exception as e: + logger.error("Failed to save screenshot.", exc_info = e) + self._screen.error_on_keyboard_display(_("Failed to save screenshot to %s. %s") % (dir, str(e))) + finally: + self._screen.draw_lock.release() + + return True + \ No newline at end of file diff --git a/src/plugins/lcdshot/lcdshot.ui b/src/plugins/lcdshot/lcdshot.ui new file mode 100644 index 0000000..2cb84d7 --- /dev/null +++ b/src/plugins/lcdshot/lcdshot.ui @@ -0,0 +1,200 @@ + + + + + + 1 + 100 + 10 + 1 + 10 + + + + + + + + + + + + + + 460 + False + 5 + LCD Screenshot Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 3 + 2 + 4 + + + 100 + True + False + 0 + Folder + + + + + 100 + True + False + 0 + Mode + + + 1 + 2 + + + + + True + False + select-folder + Choose A Destination Folder For The LCD Shots + + + 1 + 2 + + + + + True + False + ModeModel + + + + 2 + 1 + + + + + 1 + 2 + 1 + 2 + + + + + True + False + + + True + False + Frames per second + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + FPSModel + + + True + True + 1 + + + + + 2 + 2 + 3 + + + + + False + True + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + True + False + 0 + 0 + 2 + mencoder is not installed in this computer. +You won't be able to record videos. + + + + + + + True + True + 2 + 2 + + + + + + button9 + + + diff --git a/src/plugins/lens/Makefile.am b/src/plugins/lens/Makefile.am new file mode 100644 index 0000000..7ebeea6 --- /dev/null +++ b/src/plugins/lens/Makefile.am @@ -0,0 +1,11 @@ +plugindir = $(datadir)/gnome15/plugins/lens +plugin_DATA = lens.py + +unitydir = $(datadir)/unity/lenses +unity_DATA = gnome15.svg + +placedir = $(datadir)/unity/lenses +place_DATA = gnome15.lens + +EXTRA_DIST = \ + $(plugin_DATA) $(place_DATA) $(unity_DATA) \ No newline at end of file diff --git a/src/plugins/lens/gnome15.lens b/src/plugins/lens/gnome15.lens new file mode 100644 index 0000000..117ece7 --- /dev/null +++ b/src/plugins/lens/gnome15.lens @@ -0,0 +1,15 @@ +# +# Core info on how to contact the place over DBus +# +[Lens] +DBusName=org.gnome15.Gnome15Lens +DBusPath=/org/gnome15/Gnome15Lens/lens +Name=Gnome15 Lens +Icon=/usr/share/unity/lenses/gnome15/gnome15.svg +Description=Lens that allows selection of the information on the LCD of your Logitech devices. +SearchHint=LCD Page name ... +Shortcut=g + +[Desktop Entry] +X-Ubuntu-Gettext-Domain=gnome15-lens + diff --git a/src/plugins/lens/gnome15.svg b/src/plugins/lens/gnome15.svg new file mode 100644 index 0000000..df59f82 --- /dev/null +++ b/src/plugins/lens/gnome15.svg @@ -0,0 +1,3076 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + G + G + G + G + diff --git a/src/plugins/lens/lens.py b/src/plugins/lens/lens.py new file mode 100644 index 0000000..a07f8ec --- /dev/null +++ b/src/plugins/lens/lens.py @@ -0,0 +1,280 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import cairo +import os +logger = logging.getLogger(__name__) +import sys +from gi.repository import GLib, GObject, Gio +from gi.repository import Dee +# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe +# it *before* we import the Unity module... ?! +_m = dir(Dee.SequenceModel) +from gi.repository import Unity +from gnome15 import g15devices +from gnome15 import util.g15os as g15os +from gnome15 import util.g15icontools as g15icontools +from gnome15 import g15screen +from gnome15 import g15globals +from cStringIO import StringIO +import base64 + +# +# The primary bus name we grab *must* match what we specify in our .place file +# +BUS_NAME = "org.gnome15.Gnome15Lens" + +# These category ids must match the order in which we add them to the lens +CATEGORY_PAGES = 0 +CATEGORY_TOOLS = 1 + +# Plugin details - All of these must be provided +id="lens" +name="Unity Lens" +description="Integrates Gnome15 with Unity" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +global_plugin=True + +# Cached +cache_dir = os.path.join(g15globals.user_cache_dir, "lens") +if not os.path.exists(cache_dir): + g15os.mkdir_p(cache_dir) + +def create(gconf_key, gconf_client, service): + return G15Lens(service, gconf_client, gconf_key) + +class G15Lens(): + + def __init__(self, service, gconf_client, gconf_key): + self._service = service + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self.listeners = [] + + def activate(self): + + session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None) + session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None, + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', None) + result = session_bus.call_sync('RequestName', + GLib.Variant ("(su)", (BUS_NAME, 0x4)), + 0, -1, None) + + # Unpack variant response with signature "(u)". 1 means we got it. + result = result.unpack()[0] + + if result != 1 : + print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME + raise Exception("Failed to own name %s. Bailing out." % BUS_NAME) + + self._lens = Unity.Lens.new("/org/gnome15/Gnome15Lens", "Gnome15Lens") + self._scope = Unity.PlaceEntryInfo.new ("/org/gnome15/Gnome15Lens/scope/main") + # + self._lens.props.search_hint = "LCD Page name ..." + self._lens.props.visible = True + self._lens.props.search_in_global = True + + # Populate categories + cats = [] + cats.append (Unity.Category.new ("Pages", + Gio.ThemedIcon.new("display"), + Unity.CategoryRenderer.VERTICAL_TILE)) + cats.append (Unity.Category.new ("Tools", + Gio.ThemedIcon.new("configuration-section"), + Unity.CategoryRenderer.VERTICAL_TILE)) + self._lens.props.categories = cats + + # Listen for changes and requests + self._scope.connect ("notify::active-search", self._on_search_changed) + self._scope.connect ("notify::active-global-search", self._on_global_search_changed) + + self._lens.add_local_scope (self._scope); + self._lens.export (); + + def do_activate(self, *args): + print "activate:", args + return Unity.ActivationStatus.ACTIVATED_HIDE_DASH + + def _activation(self, uri): + print uri + return True + + def screen_removed(self, screen): + for l in self.listeners: + if l.screen == screen: + self.listeners.remove(l) + return + + def service_stopped(self): + pass + + def screen_added(self, screen): + self._add_screen(screen) + + def service_stopping(self): + pass + + def service_starting_up(self): + pass + + def service_started_up(self): + pass + + def get_search_string (self): + search = self._scope.props.active_search + return search.get_search_string() if search else None + + def get_global_search_string (self): + search = self._scope.props.active_global_search + return search.get_search_string() if search else None + + def search_finished (self): + search = self._scope.props.active_search + if search: + search.emit ("finished") + + def global_search_finished (self): + search = self._scope.props.active_global_search + if search: + search.emit("finished") + + """ + Private + """ + def _on_activation(self, uri, callback, callback_target): + print "URI %s, %s, %s" % ( uri, str(callback), str(callback_target)) + + def _add_screen(self, screen): + listener = MenuScreenChangeListener(self, screen) + self.listeners.append(listener) + screen.add_screen_change_listener(listener) + + def _on_sections_synchronized (self, sections_model, *args): + # Column0: display name + # Column1: GIcon in string format + sections_model.clear () + for device in g15devices.find_all_devices(): + if device.model_id == 'virtual': + icon_file = g15icontools.get_icon_path(["preferences-system-window", "preferences-system-windows", "gnome-window-manager", "window_fullscreen"]) + else: + icon_file = g15icontools.get_app_icon(self._gconf_client, device.model_id) + icon = Gio.FileIcon(Gio.File(icon_file)) + sections_model.append (device.model_fullname, + icon.to_string()) + + def _on_groups_synchronized (self, groups_model, *args): + groups_model.clear () + groups_model.append ("UnityDefaultRenderer", + "Screens", + Gio.ThemedIcon("display").to_string()) + groups_model.append ("UnityDefaultRenderer", + "Tools", + Gio.ThemedIcon("preferences-system").to_string()) + + def _on_global_groups_synchronized (self, global_groups_model, *args): + # Just the same as the normal groups + self._on_groups_synchronized (global_groups_model) + + def _on_search_changed (self, *args): + search = self.get_search_string() + results = self._entry.props.results_model + + print "Search changed to: '%s'" % search + + self._update_results_model (search, results) + self.search_finished() + + def _on_global_search_changed (self, entry, param_spec): + search = self.get_global_search_string() + results = self._entry.props.global_renderer_info.props.results_model + + print "Global search changed to: '%s'" % search + + self._update_results_model (search, results) + self.global_search_finished() + + def _update_results_model (self, search, model): + model.clear () + search = search.lower() + print "Search> %s" % search + for listener in self.listeners: + print " L[%s]" % str(listener) + for page in listener.screen.pages: + if len(search) == 0 or search in page.title.lower(): + icon_hint = listener._get_page_filename(page) + uri = "gnome15://%s" % base64.encodestring(page.id) + print " URI %s" % uri + model.append (uri, # uri + icon_hint, # string formatted GIcon + CATEGORY_PAGES, # numeric group id + "text/html", # mimetype + page.title, # display name + page.title, # comment, + uri) # FIXME WHATSTHIS? + + print str(model) + + def deactivate(self): + pass + + def destroy(self): + pass + +class MenuScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin, screen): + self.plugin = plugin + self.screen = screen + for page in screen.pages: + self._add_page(page) + + def new_page(self, page): + print "Adding page %s for screen %s" % (page.id, self.screen.device.uid) + self._add_page(page) + + def title_changed(self, page, title): + self._update_page(page) + + def del_page(self, page): + filename = self._get_page_filename(page) + logger.info("Removing page thumbnail image", filename) + os.remove(filename) + + """ + Private + """ + def _get_page_filename(self, page): + return "%s/%s.png" % ( cache_dir, base64.encodestring(page.id) ) + + def _add_page(self, page): + self._update_page(page) + + def _update_page(self, page): + if page.thumbnail_painter != None: + img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.width, self.screen.height) + thumb_canvas = cairo.Context(img) + try : + if page.thumbnail_painter(thumb_canvas, self.screen.height, True): + filename = self._get_page_filename(page) + logger.info("Writing thumbnail to %s", filename) + img.write_to_png(filename) + except Exception as e: + logger.warning("Problem with painting thumbnail.", exc_info = e) diff --git a/src/plugins/macro-recorder/Makefile.am b/src/plugins/macro-recorder/Makefile.am new file mode 100644 index 0000000..9a67b1d --- /dev/null +++ b/src/plugins/macro-recorder/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/macro-recorder +plugin_DATA = macro-recorder.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/macro-recorder/default/Makefile.am b/src/plugins/macro-recorder/default/Makefile.am new file mode 100644 index 0000000..957a82b --- /dev/null +++ b/src/plugins/macro-recorder/default/Makefile.am @@ -0,0 +1,6 @@ +themedir = $(datadir)/gnome15/plugins/macro-recorder/default +theme_DATA = g19.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/macro-recorder/default/default.svg b/src/plugins/macro-recorder/default/default.svg new file mode 100644 index 0000000..e09029e --- /dev/null +++ b/src/plugins/macro-recorder/default/default.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${profile} + ${memory} + + ${message} + ${profile} + ${memory} + + + diff --git a/src/plugins/macro-recorder/default/g19.svg b/src/plugins/macro-recorder/default/g19.svg new file mode 100644 index 0000000..60f0209 --- /dev/null +++ b/src/plugins/macro-recorder/default/g19.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${profile} + ${memory} + + ${message} + + diff --git a/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po b/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po new file mode 100644 index 0000000..9e74234 --- /dev/null +++ b/src/plugins/macro-recorder/i18n/macro-recorder.en_GB.po @@ -0,0 +1,60 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: macro-recorder.py:57 +msgid "Macro Recorder" +msgstr "Macro Recorder" + +#: macro-recorder.py:58 +msgid "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." +msgstr "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." + +#: macro-recorder.py:63 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: macro-recorder.py:69 +msgid "Start recording macro" +msgstr "Start recording macro" + +#: macro-recorder.py:307 +#, python-format +msgid "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." +msgstr "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." + +#: macro-recorder.py:311 +msgid "No Profile" +msgstr "No Profile" + +#: macro-recorder.py:313 +msgid "You have no profiles configured. Configure one now using the Macro tool" +msgstr "" +"You have no profiles configured. Configure one now using the Macro tool" diff --git a/src/plugins/macro-recorder/i18n/macro-recorder.pot b/src/plugins/macro-recorder/i18n/macro-recorder.pot new file mode 100644 index 0000000..7d22c3c --- /dev/null +++ b/src/plugins/macro-recorder/i18n/macro-recorder.pot @@ -0,0 +1,53 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: macro-recorder.py:57 +msgid "Macro Recorder" +msgstr "" + +#: macro-recorder.py:58 +msgid "" +"Allows recording of macros. All feedback is provided via the LCD (when " +"available), as well as blinking of memory bank lights when recording. You " +"may also delete macros by assigning an empty macro to a key. The macro will " +"be recorded on the currently selected profile and memory bank." +msgstr "" + +#: macro-recorder.py:63 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: macro-recorder.py:69 +msgid "Start recording macro" +msgstr "" + +#: macro-recorder.py:307 +#, python-format +msgid "" +"Recording on M%s. Type in your macro then press the G-Key to assign it to, " +"or MR to cancel." +msgstr "" + +#: macro-recorder.py:311 +msgid "No Profile" +msgstr "" + +#: macro-recorder.py:313 +msgid "You have no profiles configured. Configure one now using the Macro tool" +msgstr "" diff --git a/src/plugins/macro-recorder/macro-recorder.py b/src/plugins/macro-recorder/macro-recorder.py new file mode 100644 index 0000000..690379d --- /dev/null +++ b/src/plugins/macro-recorder/macro-recorder.py @@ -0,0 +1,334 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("macro-recorder", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.g15devices as g15devices +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15profile as g15profile +import gnome15.g15actions as g15actions +import datetime +from threading import Timer +import gtk +import os +import sys +import time +import logging +logger = logging.getLogger(__name__) + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +from threading import Thread + +# Custom actions +RECORD = "record" + +# Register the action with all supported models +g15devices.g15_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) +g15devices.g110_action_keys[RECORD] = g15actions.ActionBinding(RECORD, [ g15driver.G_KEY_MR ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="macro-recorder" +name=_("Macro Recorder") +description=_("Allows recording of macros. All feedback is provided via the LCD (when available), \ +as well as blinking of memory bank lights when recording. \ +You may also delete macros by assigning an empty macro to a key. \ +The macro will be recorded on the currently selected profile and memory bank.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_Z10, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + RECORD : _("Start recording macro") + } + + +local_dpy = display.Display() +record_dpy = display.Display() + +def create(gconf_key, gconf_client, screen): + return G15MacroRecorder(gconf_key, gconf_client, screen) + + +class RecordThread(Thread): + def __init__(self, _record_callback): + Thread.__init__(self) + self.setDaemon(True) + self.name = "RecordThread" + self._record_callback = _record_callback + self.ctx = record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.MotionNotify), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + def disable_record_context(self): + if self.ctx != None: + local_dpy.record_disable_context(self.ctx) + local_dpy.flush() + + def run(self): + record_dpy.record_enable_context(self.ctx, self._record_callback) + record_dpy.record_free_context(self.ctx) + +class MacroRecorderScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self._plugin = plugin + + def memory_bank_changed(self, new_bank_number): + self._plugin._redraw() + +class G15MacroRecorder(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._record_key = None + self._record_thread = None + self._last_keys = None + self._page = None + self._key_down = None + self._message = None + self._lights_control = None + + def activate(self): + self._theme = g15theme.G15Theme(self) + self._screen.key_handler.action_listeners.append(self) + self._listener = MacroRecorderScreenChangeListener(self) + self._screen.add_screen_change_listener(self._listener) + + def deactivate(self): + self._cancel_macro() + self._screen.key_handler.action_listeners.remove(self) + self._screen.remove_screen_change_listener(self._listener) + + def destroy(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + + def action_performed(self, binding): + if binding.action == RECORD: + if self._record_thread is None: + self._start_recording() + return True + else: + self._cancel_macro(None) + return True + + def handle_key(self, keys, state, post): + # Memory keys + + if self._record_thread != None: + # Let the M1-M3 and MR key be handled as actions + if g15driver.G_KEY_MR in keys or g15driver.G_KEY_M1 in keys or g15driver.G_KEY_M2 in keys or g15driver.G_KEY_M3 in keys: + return False + + # Stop recording on release of a macro key + if not post and ( state == g15driver.KEY_STATE_UP or state == g15driver.KEY_STATE_HELD): + """ + All other keys end recording. We use the UP keystate, so it doesn't trigger the + macro itself when it is released at the end of recording + """ + self._last_keys = keys + self._record_keys = keys + self._done_recording(state) + + # When recording, we want all key events until recording is done + return True + + ''' + Private + ''' + + def _lookup_keysym(self, keysym): + logger.debug("Looking up %s", keysym) + for name in dir(XK): + logger.debug(" %s", name) + if name[:3] == "XK_" and getattr(XK, name) == keysym: + return name[3:] + return "[%d]" % keysym + + def _record_callback(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, record_dpy.display, None, None) + if event.type in [X.KeyPress, X.KeyRelease]: + pr = event.type == X.KeyPress and "Press" or "Release" + logger.debug("Event detail = %s", event.detail) + keysym = local_dpy.keycode_to_keysym(event.detail, 0) + if not keysym: + logger.debug("Recorded %s", event.detail) + self._record_key_callback(event, event.detail) + else: + logger.debug("Keysym = %s", str(keysym)) + s = self._lookup_keysym(keysym) + logger.debug("Recorded %s", s) + self._record_key_callback(event, s) + + self._redraw() + + def _record_key_callback(self, event, keyname): + if self._key_down == None: + self._key_down = time.time() + else: + now = time.time() + delay = time.time() - self._key_down + self._script_model.append(["Delay", str(int(delay * 1000))]) + self._key_down = now + pr = event.type == X.KeyPress and "Press" or "Release" + keydown = self._key_state[keyname] if keyname in self._key_state else None + if keydown is None: + if event.type == X.KeyPress: + self._key_state[keyname] = True + self._script_model.append([pr, keyname]) + else: + # Got a release without getting a press - ignore + pass + else: + if event.type == X.KeyRelease: + self._script_model.append([pr, keyname]) + del self._key_state[keyname] + + def _done_recording(self, state): + if self._record_keys != None: + record_keys = self._record_keys + self._halt_recorder() + + active_profile = g15profile.get_active_profile(self._screen.device) + key_name = ", ".join(g15driver.get_key_names(record_keys)) + if len(self._script_model) == 0: + self.icon = "edit-delete" + self._message = key_name + " deleted" + active_profile.delete_macro(state, self._screen.get_memory_bank(), record_keys) + self._screen.redraw(self._page) + else: + macro_script = "" + for row in self._script_model: + if len(macro_script) != 0: + macro_script += "\n" + macro_script += row[0] + " " + row[1] + self.icon = "tag-new" + self._message = key_name + " created" + memory = self._screen.get_memory_bank() + macro = active_profile.get_macro(state, memory, record_keys) + if macro: + macro.type = g15profile.MACRO_SCRIPT + macro.macro = macro_script + macro.save() + else: + active_profile.create_macro(memory, record_keys, key_name, g15profile.MACRO_SCRIPT, macro_script, state) + self._redraw() + self._hide_recorder(3.0) + else: + self._hide_recorder() + + def _hide_recorder(self, after = 0.0): + if self._lights_control: + self._screen.release_defeat_profile_change() + self._screen.driver.release_control(self._lights_control) + self._lights_control = None + if self._page: + if after == 0.0: + self._screen.del_page(self._page) + else: + self._screen.delete_after(after, self._page) + self._page = None + + def _halt_recorder(self): + if self._record_thread != None: + self._record_thread.disable_record_context() + self._key_down = None + self._record_key = None + self._record_thread = None + + def _cancel_macro(self,event = None,data=None): + self._halt_recorder() + self._hide_recorder() + + def _redraw(self): + if self._page != None: + self._screen.redraw(self._page) + + def _start_recording(self): + self._script_model = [] + self._key_state = {} + self._key_down = None + if self._screen.driver.get_bpp() > 0: + if self._page == None: + self._page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_EXCLUSIVE,\ + title = name, theme_properties_callback = self._get_theme_properties, \ + theme = self._theme, + originating_plugin = self) + self._screen.add_page(self._page) + self.icon = "media-record" + self._message = None + self._redraw() + self._record_thread = RecordThread(self._record_callback) + self._record_thread.start() + self._lights_control = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + self._lights_control.set_value(self._screen.get_memory_bank() | g15driver.MKEY_LIGHT_MR) + self._lights_control.blink(0, 0.5) + self._screen.request_defeat_profile_change() + + def _get_theme_properties(self): + + active_profile = g15profile.get_active_profile(self._screen.device) + + properties = {} + properties["icon"] = g15icontools.get_icon_path(self.icon, self._screen.height) + properties["memory"] = "M%d" % self._screen.get_memory_bank() + + if active_profile != None: + properties["profile"] = active_profile.name + properties["profile_icon"] = active_profile.get_profile_icon_path(self._screen.height) + + if self._message == None: + properties["message"] = _("Recording on M%s. Type in your macro then press the G-Key to assign it to, or MR to cancel." % self._screen.get_memory_bank()) + else: + properties["message"] = self._message + else: + properties["profile"] = _("No Profile") + properties["profile_icon"] = "" + properties["message"] = _("You have no profiles configured. Configure one now using the Macro tool") + + return properties \ No newline at end of file diff --git a/src/plugins/macros/Makefile.am b/src/plugins/macros/Makefile.am new file mode 100644 index 0000000..52dd508 --- /dev/null +++ b/src/plugins/macros/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/macros +plugin_DATA = macros.py macros.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/macros/default/Makefile.am b/src/plugins/macros/default/Makefile.am new file mode 100644 index 0000000..0f3abec --- /dev/null +++ b/src/plugins/macros/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/macros/default +theme_DATA = g19-menu-screen.svg \ + g19-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/macros/default/default-menu-entry.svg b/src/plugins/macros/default/default-menu-entry.svg new file mode 100644 index 0000000..b160dae --- /dev/null +++ b/src/plugins/macros/default/default-menu-entry.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_key} + + + + ${item_name} + ${item_key} + + diff --git a/src/plugins/macros/default/default-menu-screen.svg b/src/plugins/macros/default/default-menu-screen.svg new file mode 100644 index 0000000..c1f3add --- /dev/null +++ b/src/plugins/macros/default/default-menu-screen.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + + + + + + ${mkey} + No Macros Configured on ${mkey} + + diff --git a/src/plugins/macros/default/g19-menu-entry.svg b/src/plugins/macros/default/g19-menu-entry.svg new file mode 100644 index 0000000..65a8f19 --- /dev/null +++ b/src/plugins/macros/default/g19-menu-entry.svg @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${item_name} + + + + + diff --git a/src/plugins/macros/default/g19-menu-screen.svg b/src/plugins/macros/default/g19-menu-screen.svg new file mode 100644 index 0000000..5a05828 --- /dev/null +++ b/src/plugins/macros/default/g19-menu-screen.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${mkey} + No Macros Configured For ${mkey} + + + + + + + diff --git a/src/plugins/macros/i18n/macros.en_GB.po b/src/plugins/macros/i18n/macros.en_GB.po new file mode 100644 index 0000000..01c562a --- /dev/null +++ b/src/plugins/macros/i18n/macros.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/macros.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "Indicator Messages Preferences" + +#: i18n/macros.glade.h:2 +msgid "Raise page when profile or memory bank changes" +msgstr "Raise page when profile or memory bank changes" + +#: i18n/macros.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/macros.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/macros.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/macros.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/macros.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/macros/i18n/macros.glade.h b/src/plugins/macros/i18n/macros.glade.h new file mode 100644 index 0000000..02e93ac --- /dev/null +++ b/src/plugins/macros/i18n/macros.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Indicator Messages Preferences"); +char *s = N_("Raise page when profile or memory bank changes"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/macros/i18n/macros.pot b/src/plugins/macros/i18n/macros.pot new file mode 100644 index 0000000..7b0f6a5 --- /dev/null +++ b/src/plugins/macros/i18n/macros.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/macros.glade.h:1 +msgid "Indicator Messages Preferences" +msgstr "" + +#: i18n/macros.glade.h:2 +msgid "Raise page when profile or memory bank changes" +msgstr "" + +#: i18n/macros.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/macros.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/macros.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/macros.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/macros.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/macros/macros.py b/src/plugins/macros/macros.py new file mode 100644 index 0000000..86c9f3e --- /dev/null +++ b/src/plugins/macros/macros.py @@ -0,0 +1,218 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("macros", modfile = __file__).ugettext + +import gnome15.g15profile as g15profile +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15plugin as g15plugin +import gtk +import os +import logging +import time +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="macros" +name=_("Macro Information") +description=_("Displays the currently active macro profile and a summary of available keys.\ +Also, the screen will be cycled to when a macro is activated and the key will be \ +highlighted.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_Z10, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous macro"), + g15driver.NEXT_SELECTION : _("Next macro"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +def create(gconf_key, gconf_client, screen): + return G15Macros(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "macros.ui")) + dialog = widget_tree.get_object("MacrosDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +""" +Represents a mount as a single item in a menu +""" +class MacroMenuItem(g15theme.MenuItem): + def __init__(self, macro, component_id): + g15theme.MenuItem.__init__(self, component_id) + self.macro = macro + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.macro.name + item_properties["item_type"] = "" + item_properties["item_key"] = ",".join(g15driver.get_key_names(self.macro.keys)) + for r in range(0, len(self.macro.keys)): + item_properties["icon%d" % (r + 1)] = os.path.join(g15globals.image_dir, "key-%s.png" % self.macro.keys[r]) + return item_properties + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def activate(self): + pass + +""" +Macros plugin class +""" +class G15Macros(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["preferences-desktop-keyboard-shortcuts", "input-keyboard"], id, name) + + def activate(self): + self._get_configuration() + g15plugin.G15MenuPlugin.activate(self) + self._notify_handles = [] + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._profiles_changed)) + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/locked" % self.screen.device.uid, self._profiles_changed)) + g15profile.profile_listeners.append(self._profiles_changed) + self.listener = MacrosScreenChangeAdapter(self) + self.screen.add_screen_change_listener(self.listener) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + for h in self._notify_handles: + self.gconf_client.notify_remove(h) + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.remove_screen_change_listener(self.listener) + + def get_theme_path(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + properties = g15plugin.G15MenuPlugin.get_theme_properties(self) + properties["title"] = self._active_profile.name + properties["mkey"] = "M%d" % self._mkey + properties["icon"] = self._get_active_profile_icon_path() + return properties + + def _get_active_profile_icon_path(self): + if self._active_profile == None: + return None + return self._active_profile.get_profile_icon_path(self.screen.height) + + """ + Screen change listener callbacks + + """ + def memory_bank_changed(self): + g15screen.run_on_redraw(self._reload_and_popup) + + """ + Private functions + """ + def _profiles_changed(self, arg0 = None, arg1 = None, arg2 = None, arg3 = None): + self._reload_and_popup() + + def _reload(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _reload_and_popup(self): + self._reload() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise" % self.gconf_key, True): + self._popup() + + def load_menu_items(self): + """ + Reload all items for the current profile and bank + """ + self._get_configuration() + self.menu.remove_all_children() + self.page.set_title(_("Macros - %s") % self._active_profile.name) + + macro_keys = [] + macros = [] + self._load_profile(self._active_profile, macros, macro_keys) + macros.sort(self._comparator) + for macro in macros: + self.menu.add_child(MacroMenuItem(macro, "macro-%s" % macro.key_list_key)) + + def _load_profile(self, profile, macros, macro_keys): + for bank in profile.macros.values(): + for m in bank[self._mkey - 1]: + if not m.keys in macro_keys: + macros.append(m) + macro_keys.append(m.keys) + if profile.base_profile != None and profile.base_profile != "": + self._load_profile(g15profile.get_profile(profile.device, profile.base_profile), macros, macro_keys) + + def _comparator(self, o1, o2): + return o1.compare(o2) + + def _get_configuration(self): + self._mkey = self.screen.get_memory_bank() + self._active_profile = g15profile.get_active_profile(self.screen.device) + + def _popup(self): + """ + Popup the page + """ + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _remove_macro(self, macro): + """ + Remove a macro from the menu + """ + logger.info("Removing macro %s", str(macro.name)) + self.menu.remove_child(self._get_item_for_macro(macro)) + self.screen.redraw(self.page) + + def _get_item_for_macro(self, macro): + """ + Get the menu item for the given macro + """ + for item in self.menu.get_children(): + if isinstance(item, MacroMenuItem) and item.macro == macro: + return item + + def _add_macro(self, macro): + """ + Add a new macro to the menu + """ + item = MacroMenuItem(macro, self, "macro-%s" % macro.key_list_key) + self.menu.add_child(item) + +class MacrosScreenChangeAdapter(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self.plugin = plugin + + def memory_bank_changed(self, new_bank_number): + self.plugin._get_configuration() + self.plugin._reload() + if g15gconf.get_bool_or_default(self.plugin.gconf_client, "%s/raise" % self.plugin.gconf_key, True): + self.plugin._popup() \ No newline at end of file diff --git a/src/plugins/macros/macros.ui b/src/plugins/macros/macros.ui new file mode 100644 index 0000000..e151c2e --- /dev/null +++ b/src/plugins/macros/macros.ui @@ -0,0 +1,101 @@ + + + + + + + False + 5 + Indicator Messages Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when profile or memory bank changes + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/mediaplayer/Makefile.am b/src/plugins/mediaplayer/Makefile.am new file mode 100644 index 0000000..6b8b8bd --- /dev/null +++ b/src/plugins/mediaplayer/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/mediaplayer +plugin_DATA = mediaplayer.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mediaplayer/default/Makefile.am b/src/plugins/mediaplayer/default/Makefile.am new file mode 100644 index 0000000..62e13e6 --- /dev/null +++ b/src/plugins/mediaplayer/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/mediaplayer/default +theme_DATA = g19.svg \ + g19-mediakeys.svg \ + default.svg \ + default-mediakeys.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mediaplayer/default/WIPg19.svg b/src/plugins/mediaplayer/default/WIPg19.svg new file mode 100644 index 0000000..a14b831 --- /dev/null +++ b/src/plugins/mediaplayer/default/WIPg19.svg @@ -0,0 +1,939 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + G1 + G2 + G3 + G4 + G5 + Open + Play + Stop + Aspect + Mute + ${aspect} + + + OK + + + + + + diff --git a/src/plugins/mediaplayer/default/default-mediakeys.svg b/src/plugins/mediaplayer/default/default-mediakeys.svg new file mode 100644 index 0000000..0f37757 --- /dev/null +++ b/src/plugins/mediaplayer/default/default-mediakeys.svg @@ -0,0 +1,987 @@ + + + + + + + ${track_name} + + + + + + + + + image/svg+xml + + + + + + + + + + + ${track_progress} / ${track_duration} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Aspect + + + Aspect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/mediaplayer/default/default.svg b/src/plugins/mediaplayer/default/default.svg new file mode 100644 index 0000000..3a7cee7 --- /dev/null +++ b/src/plugins/mediaplayer/default/default.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Stop + + + ${track_progress} / ${track_duration} + + <Hold + Aspect + + + + Rew + Fwd + ${play_pause} + + + + Aspect + + + + Rew + + + + + Fwd + + + + + + + ${play_pause} + + + + + + Stop + + ${track_name} + + diff --git a/src/plugins/mediaplayer/default/g19-mediakeys.svg b/src/plugins/mediaplayer/default/g19-mediakeys.svg new file mode 100644 index 0000000..70ac49e --- /dev/null +++ b/src/plugins/mediaplayer/default/g19-mediakeys.svg @@ -0,0 +1,1224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${play_pause} + Forward + Rewind + Aspect + Stop + ${aspect} + + + + + ${track_progress} / ${track_duration} + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/mediaplayer/default/g19.svg b/src/plugins/mediaplayer/default/g19.svg new file mode 100644 index 0000000..8f422e6 --- /dev/null +++ b/src/plugins/mediaplayer/default/g19.svg @@ -0,0 +1,1178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${play_pause} + Forward + Rewind + Aspect + Stop + ${aspect} + Ok + + + + + + + + + + ${track_progress} / ${track_duration} + + diff --git a/src/plugins/mediaplayer/gtkplayer.py b/src/plugins/mediaplayer/gtkplayer.py new file mode 100644 index 0000000..4444cfc --- /dev/null +++ b/src/plugins/mediaplayer/gtkplayer.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gst +import gtk + +class Main: + def __init__(self): + + # Create GUI objects + self.window = gtk.Window() + self.vbox = gtk.VBox() + self.da = gtk.DrawingArea() + self.bb = gtk.HButtonBox() + self.da.set_size_request(300, 150) + self.playButton = gtk.Button(stock="gtk - media - play") + self.playButton.connect("clicked", self.OnPlay) + self.stopButton = gtk.Button(stock="gtk - media - stop") + self.stopButton.connect("clicked", self.OnStop) + self.quitButton = gtk.Button(stock="gtk - quit") + self.quitButton.connect("clicked", self.OnQuit) + self.vbox.pack_start(self.da) + self.bb.add(self.playButton) + self.bb.add(self.stopButton) + self.bb.add(self.quitButton) + self.vbox.pack_start(self.bb) + self.window.add(self.vbox) + + # Create GStreamer pipeline + self.pipeline = gst.Pipeline("mypipeline") + # Set up our video test source + self.videotestsrc = gst.element_factory_make("videotestsrc", "video") + # Add it to the pipeline + self.pipeline.add(self.videotestsrc) + # Now we need somewhere to send the video + self.sink = gst.element_factory_make("xvimagesink", "sink") + # Add it to the pipeline + self.pipeline.add(self.sink) + # Link the video source to the sink - xv + self.videotestsrc.link(self.sink) + self.window.show_all() + + def OnPlay(self, widget): + print "play" + # Tell the video sink to display the output in our DrawingArea + self.sink.set_xwindow_id(self.da.window.xid) + self.pipeline.set_state(gst.STATE_PLAYING) + + def OnStop(self, widget): + print "stop" + self.pipeline.set_state(gst.STATE_READY) + + def OnQuit(self, widget): + gtk.main_quit() + +start = Main() +gtk.main() diff --git a/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po b/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po new file mode 100644 index 0000000..c4db6a8 --- /dev/null +++ b/src/plugins/mediaplayer/i18n/videoplayer.en_GB.po @@ -0,0 +1,64 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:46+0100\n" +"PO-Revision-Date: 2011-10-09 19:46+0100\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: videoplayer.py:39 +msgid "Video Player" +msgstr "Video Player" + +#: videoplayer.py:40 +msgid "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " +msgstr "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " + +#: videoplayer.py:45 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: videoplayer.py:50 +msgid "Stop" +msgstr "Stop" + +#: videoplayer.py:51 +msgid "Play" +msgstr "Play" + +#: videoplayer.py:52 +msgid "Open file" +msgstr "Open file" + +#: videoplayer.py:53 +msgid "Toggle Mute" +msgstr "Toggle Mute" + +#: videoplayer.py:54 +msgid "Change aspect" +msgstr "Change aspect" + +#: videoplayer.py:211 +msgid "All files" +msgstr "All files" + +#: videoplayer.py:216 +msgid "Movies" +msgstr "Movies" diff --git a/src/plugins/mediaplayer/i18n/videoplayer.pot b/src/plugins/mediaplayer/i18n/videoplayer.pot new file mode 100644 index 0000000..f617d20 --- /dev/null +++ b/src/plugins/mediaplayer/i18n/videoplayer.pot @@ -0,0 +1,61 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-09 19:46+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: videoplayer.py:39 +msgid "Video Player" +msgstr "" + +#: videoplayer.py:40 +msgid "" +"Plays videos! Very much experimental, this plugin uses mplayer to generate " +"JPEG images which are then loaded and displayed on the LCD. This means it is " +"very CPU AND disk intensive and should only be used as a toy. " +msgstr "" + +#: videoplayer.py:45 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: videoplayer.py:50 +msgid "Stop" +msgstr "" + +#: videoplayer.py:51 +msgid "Play" +msgstr "" + +#: videoplayer.py:52 +msgid "Open file" +msgstr "" + +#: videoplayer.py:53 +msgid "Toggle Mute" +msgstr "" + +#: videoplayer.py:54 +msgid "Change aspect" +msgstr "" + +#: videoplayer.py:211 +msgid "All files" +msgstr "" + +#: videoplayer.py:216 +msgid "Movies" +msgstr "" diff --git a/src/plugins/mediaplayer/mediaplayer.py b/src/plugins/mediaplayer/mediaplayer.py new file mode 100644 index 0000000..a63541e --- /dev/null +++ b/src/plugins/mediaplayer/mediaplayer.py @@ -0,0 +1,1077 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("videoplayer", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15screen as g15screen +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import gnome15.lcdsink as lcdsink +import gnome15.g15globals as g15globals +import gtk +import os +import gst +import cairo +import array +import gobject +import gio +import mimetypes +import dbus + +from threading import Lock + +import logging +logger = logging.getLogger(__name__) + +# Detect whether we will be able to grab multimedia keys +session_bus = dbus.SessionBus() +can_grab_media_keys = False +try: + dbus.Interface(session_bus.get_object('org.g.SettingsDaemon', + '/org/gnome/SettingsDaemon'), 'org.gnome.SettingsDaemon') + can_grab_media_keys = True +except dbus.DBusException as e: + logger.debug("Error when trying to check if media keys could be grabbed. Trying alternative.", + exc_info = e) + try: + dbus.Interface(session_bus.get_object('org.gnome.SettingsDaemon', + '/org/gnome/SettingsDaemon/MediaKeys'), + 'org.gnome.SettingsDaemon.MediaKeys') + can_grab_media_keys = True + except dbus.DBusException as e: + logger.debug("Error when trying to check if media keys could be grabbed.", exc_info = e) + pass + +# Register the custom actions + +NEXT_TRACK = "mediaplayer-next-track" +PREV_TRACK = "mediaplayer-previous-track" +PLAY_TRACK = "mediaplayer-play-track" +STOP_TRACK = "mediaplayer-stop-track" + +# Register the action with all supported models +g15devices.g15_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.z10_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) + + +# Plugin details - All of these must be provided +id = "mediaplayer" +name = _("Media Player") +description = _("GStreamer based media player and webcam viewer.\n\ +Supports audio and video from either DVDs, files, webcams or\n\ +pulse sources. The visualisation is displayed on the LCD for audio\n\ +sources.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://localhost" +has_preferences = False +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35, g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G11, g15driver.MODEL_MX5500 ] + +if can_grab_media_keys: + actions={ + PREV_TRACK : _("Skip Backward"), + NEXT_TRACK : _("Skip Forward"), + PLAY_TRACK : _("Play/Pause"), + STOP_TRACK : _("Stop"), + g15driver.VIEW : _("Change aspect") + } +else: + actions={ + g15driver.PREVIOUS_SELECTION : _("Skip Backward"), + g15driver.NEXT_SELECTION : _("Skip Forward"), + g15driver.SELECT : _("Play/Pause"), + g15driver.CLEAR : _("Stop"), + g15driver.VIEW : _("Change aspect") + } + actions_g19={ + g15driver.PREVIOUS_PAGE : _("Skip Backward"), + g15driver.NEXT_PAGE : _("Skip Forward"), + g15driver.SELECT : _("Play/Pause"), + g15driver.CLEAR : _("Stop"), + g15driver.VIEW : _("Change aspect") + } + + +icon_path = g15icontools.get_icon_path(["media-video", "emblem-video", "emblem-videos", "video", "video-player", "applications-multimedia" ]) + +def create(gconf_key, gconf_client, screen): + return G15MediaPlayer(gconf_client, gconf_key, screen) + +def get_visualisation(plugin): + """ + Get the currently configured visualisation. + + Keyword arguments: + plugin -- plugin instance + """ + return g15gconf.get_string_or_default( + plugin.gconf_client, + "%s/visualisation" % plugin.gconf_key, "goom") + +class PulseSourceMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single pulse source. + """ + def __init__(self, device_name, device_description, plugin): + g15theme.MenuItem.__init__(self, "pulse-%s" % device_name, False, device_description) + self._plugin = plugin + self._device_name = device_name + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.name + return item_properties + + def activate(self): + self._plugin._open_source(G15PulseSource(self._device_name, + get_visualisation(self._plugin))) + return True + +class MountMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single mount (DVD etc) + """ + def __init__(self, id, mount, plugin): + g15theme.MenuItem.__init__(self, id, False, mount.get_name()) + self._mount = mount + self._plugin = plugin + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._mount.get_name() + icon = self._mount.get_icon() + icon_names = [ icon.get_file().get_path() ] if isinstance(icon, gio.FileIcon) else icon.get_names() + icon_names += "gnome-dev-harddisk" + item_properties["item_icon"] = g15icontools.get_icon_path(icon_names) + return item_properties + + def activate(self): + self._plugin._open_source(G15RemovableSource(self._mount)) + return True + +class G15VideoDeviceMenuItem(g15theme.MenuItem): + """ + Menu item to activate a single V4L2 source (i.e. Webcam etc) + """ + + def __init__(self, plugin, video_device_name): + g15theme.MenuItem.__init__(self, video_device_name, False, video_device_name) + self.plugin = plugin + + def activate(self): + self.plugin._open_source(G15WebCamSource(self.id)) + +class G15VisualisationMenuItem(g15theme.MenuItem): + """ + Menu item to make a single visualisation the current default one + """ + + def __init__(self, name, plugin): + g15theme.MenuItem.__init__(self, "visualisation-%s" % name, False, name) + self._plugin = plugin + self.radio = True + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = get_visualisation(self._plugin) == self.name + return p + + def activate(self): + self._plugin.gconf_client.set_string("%s/visualisation" % self._plugin.gconf_key, self.name) + self._plugin.page.mark_dirty() + self._plugin.page.redraw() + +class G15VideoPainter(g15screen.Painter): + """ + Painter used to paint video or visualisation on the background of other + pages. + """ + + def __init__(self, video_page): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -2500) + self._video_page = video_page + + def paint(self, canvas): + if not self._video_page.is_visible(): + canvas.save() + self._video_page._paint_video_image(canvas) + canvas.restore() + +class G15MediaPlayerPage(g15theme.G15Page): + """ + The page used to display video or visualisation + """ + + def __init__(self, screen, source, plugin): + g15theme.G15Page.__init__(self, "videopage-%s" % source.name, screen, \ + priority = g15screen.PRI_NORMAL, \ + title = source.name, \ + theme = g15theme.G15Theme(self, variant = 'mediakeys' if plugin._grabbed_keys else None), thumbnail_painter = self._paint_thumbnail, originating_plugin = plugin) + self._sidebar_offset = 0 + self._source = source + self._muted = False + self._lock = Lock() + self._plugin = plugin + self._surface = None + self._hide_timer = None + self._screen = screen + self._full_screen = self._screen.driver.get_size() + self._aspect = self._full_screen + self._active = True + self._frame_index = 1 + self._last_seconds = -1 + self._thumb_icon = g15cairo.load_surface_from_file(icon_path) + self._setup_gstreamer() + self.screen.key_handler.action_listeners.append(self) + def on_delete(): + self._pipeline.set_state(gst.STATE_NULL) + self.screen.key_handler.action_listeners.remove(self) + self.screen.painters.remove(self.background_painter) + self._plugin.show_menu() + self._plugin._release_multimedia_keys() + self.on_deleted = on_delete + self.background_painter = G15VideoPainter(self) + self.screen.painters.append(self.background_painter) + self._plugin.hide_menu() + + def _setup_gstreamer(self): + # Create the video source + logger.info("Creating audio/visual source") + self._video_src = self._source.create_source() + + # Create our custom sink that is connected to the LCD + logger.info("Creating videosink that is connected to the LCD") + self._video_sink = lcdsink.CairoSurfaceThumbnailSink() + logger.info("Connecting to video sink") + self._video_sink.connect('thumbnail', self._redraw_cb) + + # Now create the actual pipeline + self._pipeline = gst.Pipeline("mypipeline") + logger.info("Building pipeline") + self._source.build_pipeline(self._video_src, self._video_sink, self._pipeline) + logger.info("Built pipeline") + self._connect_signals() + + def action_performed(self, binding): + # The custom actions which can be activated outside of visible page + if binding.action == PLAY_TRACK: + gobject.idle_add(self._play) + return True + elif binding.action == NEXT_TRACK: + gobject.idle_add(self._fwd) + return True + elif binding.action == PREV_TRACK: + gobject.idle_add(self._rew) + return True + elif binding.action == STOP_TRACK: + gobject.idle_add(self._stop) + return True + + if self.is_visible(): + if can_grab_media_keys: + # Default when media keys are available + if binding.action == g15driver.VIEW: + gobject.idle_add(self._change_aspect) + return True + else: + # Default when media keys are not available + if binding.action == g15driver.SELECT: + gobject.idle_add(self._play) + elif ( binding.action == g15driver.PREVIOUS_PAGE and self._screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.PREVIOUS_SELECTION and self._screen.device.model_id != g15driver.MODEL_G19 ): + gobject.idle_add(self._rew) + elif ( binding.action == g15driver.NEXT_PAGE and self._screen.device.model_id == g15driver.MODEL_G19 ) or \ + ( binding.action == g15driver.NEXT_SELECTION and self._screen.device.model_id != g15driver.MODEL_G19 ): + gobject.idle_add(self._fwd) + elif binding.action == g15driver.VIEW: + gobject.idle_add(self._change_aspect) + elif binding.action == g15driver.CLEAR: + gobject.idle_add(self._stop) + else: + return False + return True + + + def get_theme_properties(self): + properties = {} + properties["aspect"] = "%d:%d" % self._aspect + try: + progress_pc, progress, duration = self._get_track_progress() + except Exception as e: + logger.debug("Could not read track progress. Setting values to 0", exc_info = e) + progress_pc, progress, duration = 0,(0,0,0),(0,0,0) + + if self._last_seconds != progress[2]: + self.mark_dirty() + self._last_seconds = progress[2] + + if self._plugin._mm_key is not None: + properties["key_%s" % self._plugin._mm_key] = True + + properties["track_progress_pc"] = str(progress_pc) + properties["track_progress"] = "%02d:%02d.%02d" % progress + properties["track_duration"] = "%02d:%02d.%02d" % duration + properties["track_name"] = "%s" % self._source.name + + properties["play_pause"] = _("Pause") if self._is_playing() else _("Play") + return properties + + def paint_theme(self, canvas, properties, attributes): + g15theme.G15Page.paint_theme(self, canvas, properties, attributes) + + def paint(self, canvas): + self._paint_video_image(canvas) + canvas.save() + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + self._sidebar_offset -= 5 + canvas.translate(self._sidebar_offset, 0) + g15theme.G15Page.paint(self, canvas) + canvas.restore() + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + g15scheduler.schedule("RepaintVideoOverly", 0.1, self.redraw) + + """ + GStreamer callbacks + """ + + def _on_sync_message(self, bus, message): + if message.structure is None: + return + message_name = message.structure.get_name() + logger.debug("Sync. %s", message) + + def _on_message(self, bus, message): + """ + Handle changes in the playing state. + """ + t = message.type + logger.debug("Message. %s", message) + if t == gst.MESSAGE_EOS: + self._pipeline.set_state(gst.STATE_NULL) + self._show_sidebar() + elif t == gst.MESSAGE_ERROR: + err, debug = message.parse_error() + self._pipeline.set_state(gst.STATE_NULL) + self._show_sidebar() + + def _redraw_cb(self, unused_thsink, timestamp): + if not self._plugin.active: + return + buf = self._video_sink.data + width = self._video_sink.width + height = self._video_sink.height + b = array.array("b") + b.fromstring(buf) + self._surface = cairo.ImageSurface.create_for_data(b, + # We don't use FORMAT_ARGB32 because Cairo uses premultiplied + # alpha, and gstreamer does not. Discarding the alpha channel + # is not ideal, but the alternative would be to compute the + # conversion in python (slow!). + cairo.FORMAT_RGB24, + width, + height, + width * 4) + + if self.is_visible(): + self.redraw() + else: + self.get_screen().redraw(redraw_content = False, queue = False) + + + ''' + Private + ''' + def _get_track_progress(self): + raw_pos = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + raw_dur = self._pipeline.query_duration(gst.FORMAT_TIME, None)[0] + pos = self._convert_time(int(raw_pos)) + if raw_dur < 0: + return 100, pos, (0,0,0) + dur = self._convert_time(int(raw_dur)) + pc = float(raw_pos) / float(raw_dur) + return int(pc * 100), pos, dur + + def _connect_signals(self): + # Watch signals coming from the bus + logger.info("Connecting signals") + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.enable_sync_message_emission() + bus.connect("message", self._on_message) + bus.connect("sync-message::element", self._on_sync_message) + self._source.connect_signals() + logger.info("Connected signals") + + def _convert_time(self, time): + time = time / 1000000000 + mins = time % 3600 + time = time - mins + secs = mins % 60 + mins = mins - secs + hours = int(time / 3600) + mins = int(mins / 60) + secs = int(secs) + return hours,mins,secs + + def _paint_video_image(self, canvas): + size = self._screen.driver.get_size() + if self._surface != None: + target_size = ( float(size[0]), float(size[0]) * (float(self._aspect[1]) ) / float(self._aspect[0]) ) + sx = float(target_size[0]) / float(self._surface.get_width()) + sy = float(target_size[1]) / float(self._surface.get_height()) + canvas.save() + canvas.translate((size[0] - target_size[0]) / 2.0,(size[1] - target_size[1]) / 2.0) + canvas.scale(sx, sy) + canvas.set_source_surface(self._surface) + canvas.paint() + canvas.restore() + + def _hide_sidebar(self, after = 0.0): + if after == 0.0: + self._sidebar_offset = -1 + self._hide_timer = None + self.redraw() + else: + self._sidebar_offset = 0 + self._cancel_hide() + self._hide_timer = g15scheduler.schedule("HideSidebar", after, self._hide_sidebar) + + def _cancel_hide(self): + if self._hide_timer != None: + self._hide_timer.cancel() + self._hide_timer = None + + def _change_aspect(self): + if self._aspect == (16, 9): + self._aspect = (4, 3) + elif self._aspect == (4, 3): + # Just take up the most room + self._aspect = (24, 9) + elif self._aspect == (24, 9): + self._aspect = self._full_screen + else: + self._aspect = (16, 9) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _rew(self): + pos_int = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + seek_ns = pos_int - (10 * 1000000000) + self._pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _fwd(self): + pos_int = self._pipeline.query_position(gst.FORMAT_TIME, None)[0] + seek_ns = pos_int + (10 * 1000000000) + self._pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) + if self._sidebar_offset != 0: + self._show_sidebar() + self._hide_sidebar(3.0) + + def _is_paused(self): + return gst.STATE_PAUSED == self._pipeline.get_state()[1] + + def _is_playing(self): + return gst.STATE_PLAYING == self._pipeline.get_state()[1] + + def _play(self): + self._lock.acquire() + try: + if self._is_playing(): + self._pipeline.set_state(gst.STATE_PAUSED) + self._cancel_hide() + self._show_sidebar() + else: + self._pipeline.set_state(gst.STATE_PLAYING) + self._hide_sidebar(3.0) + finally: + self._lock.release() + + def _show_sidebar(self): + self._sidebar_offset = 0 + self.redraw() + + def _stop(self): + self._lock.acquire() + try: + self._pipeline.set_state(gst.STATE_READY) + self.delete() + self.screen.raise_page(self._plugin.page) + finally: + self._lock.release() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._surface != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._surface, canvas) + + +class G15MediaSource(): + """ + Superclass of all media sources + """ + + def __init__(self, name): + self.name = name + + def create_source(self): + raise Exception("Not implemented") + + def build_pipeline(self, video_src, video_sink, pipeline): + raise Exception("Not implemented") + + def connect_signals(self): + pass + +class G15VideoFileSource(G15MediaSource): + + """ + Media source for playing an audio visual movie. + """ + + def __init__(self, name, path): + G15MediaSource.__init__(self, name) + self._path = path + + def create_source(self): + src = gst.element_factory_make("filesrc", "video-source") + src.set_property("location", self._path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + + # Create the pipeline elements + self._decodebin = gst.element_factory_make("decodebin2") + self._autoconvert = gst.element_factory_make("autoconvert") + + # As a precaution add videio capability filter + # in the video processing pipeline. + videocap = gst.Caps("video/x-raw-yuv") + + self._filter = gst.element_factory_make("capsfilter") + self._filter.set_property("caps", videocap) + + # Converts the video from one colorspace to another + self._color_space = gst.element_factory_make("ffmpegcolorspace") + + self._audioconvert = gst.element_factory_make("audioconvert") + self._audiosink = gst.element_factory_make("autoaudiosink") + + # Queues + self._queue1 = gst.element_factory_make("queue") + self._queue2 = gst.element_factory_make("queue") + + pipeline.add(video_src, + self._decodebin, + self._autoconvert, + self._audioconvert, + self._queue1, + self._queue2, + self._filter, + self._color_space, + self._audiosink, + video_sink) + + # Link everything we can link now + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._queue1, self._autoconvert, + self._filter, self._color_space, + video_sink) + gst.element_link_many(self._queue2, self._audioconvert, + self._audiosink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + compatible_pad = None + caps = pad.get_caps() + name = caps[0].get_name() + if name[:5] == 'video': + compatible_pad = self._queue1.get_compatible_pad(pad, caps) + elif name[:5] == 'audio': + compatible_pad = self._queue2.get_compatible_pad(pad, caps) + + if compatible_pad: + pad.link(compatible_pad) + + +class G15AudioFileSource(G15MediaSource): + + """ + Media source for playing an audio file. Video is provided by the + currently configured visualisation + """ + + def __init__(self, name, path, visualisation): + G15MediaSource.__init__(self, name) + self._path = path + self._visualisation = visualisation + + def create_source(self): + src = gst.element_factory_make("filesrc", "video-source") + src.set_property("location", self._path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + self._decodebin = gst.element_factory_make("decodebin2") + self._visualiser = gst.element_factory_make(self._visualisation) + self._color_space = gst.element_factory_make("ffmpegcolorspace") + self._audioconvert = gst.element_factory_make("audioconvert") + self._audiosink = gst.element_factory_make("autoaudiosink") + self._tee = gst.element_factory_make('tee', "tee") + self._queue1 = gst.element_factory_make("queue") + self._queue2 = gst.element_factory_make("queue") + pipeline.add(video_src, + self._decodebin, + self._audioconvert, + self._tee, + self._queue1, + self._audiosink, + self._queue2, + self._visualiser, + self._color_space, + video_sink) + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._audioconvert, self._tee) + self._tee.link(self._queue1) + self._queue1.link(self._audiosink) + self._tee.link(self._queue2) + gst.element_link_many(self._queue2, self._visualiser,self._color_space, video_sink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + self._decodebin.link(self._audioconvert) + + +class G15PulseSource(G15MediaSource): + + """ + Media source for a pulse audio monitor. Audio is not directed, it is + just monitored to produce visualisation video + """ + + def __init__(self, name, visualisation): + G15MediaSource.__init__(self, name) + self._visualisation = visualisation + + def create_source(self): + src = gst.element_factory_make("pulsesrc", "video-source") + src.set_property("device", self.name) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + self._visualiser = gst.element_factory_make(self._visualisation) + self._color_space = gst.element_factory_make("ffmpegcolorspace") + self._audioconvert = gst.element_factory_make("audioconvert") + pipeline.add(video_src, + self._audioconvert, + self._visualiser, + self._color_space, + video_sink) + gst.element_link_many(video_src, self._audioconvert, self._visualiser,self._color_space, video_sink) + +class G15RemovableSource(G15VideoFileSource): + + """ + An audio / video source that reads from removable media such as DVD + """ + + def __init__(self, mount): + G15MediaSource.__init__(self, mount.get_name(), mount.get_root().get_path()) + self._mount = mount + + def create_source(self): + src = gst.element_factory_make("dvdreadsrc", "video-source") + return src + +class G15WebCamSource(G15MediaSource): + + """ + Video only source that reads from a V4L2 device such as a webcam + """ + + def __init__(self, name): + G15MediaSource.__init__(self, name) + + def create_source(self): + src = gst.element_factory_make("v4l2src", "video-source") + device_path = "/dev/%s" % self.name + logger.info("Opening Video device %s", device_path) + src.set_property("device", device_path) + return src + + def build_pipeline(self, video_src, video_sink, pipeline): + # Create the pipeline elements + self._decodebin = gst.element_factory_make("decodebin2") + self._autoconvert = gst.element_factory_make("autoconvert") + + videocap = gst.Caps("video/x-raw-yuv") + self._filter = gst.element_factory_make("capsfilter") + self._filter.set_property("caps", videocap) + + # Converts the video from one colorspace to another + self._color_space = gst.element_factory_make("ffmpegcolorspace") + + self._queue1 = gst.element_factory_make("queue") + + pipeline.add(video_src, + self._decodebin, + self._autoconvert, + self._queue1, + self._filter, + self._color_space, + video_sink) + + # Link everything we can link now + gst.element_link_many(video_src, self._decodebin) + gst.element_link_many(self._queue1, self._autoconvert, + self._filter, self._color_space, + video_sink) + + def connect_signals(self): + if not self._decodebin is None: + self._decodebin.connect("pad_added", self._decodebin_pad_added) + + def _decodebin_pad_added(self, decodebin, pad): + compatible_pad = None + caps = pad.get_caps() + name = caps[0].get_name() + if name[:5] == 'video': + compatible_pad = self._queue1.get_compatible_pad(pad, caps) + + if compatible_pad: + pad.link(compatible_pad) + + +class G15MediaPlayer(g15plugin.G15MenuPlugin): + """ + The main Media Player plugin class which is presented as a menu of + video sources and options + """ + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, icon_path, id, name) + self.player_pages = [] + self._grabbed_keys = None + self._settings = None + self._app_name = None + self._mm_key = None + self._mm_key_timer = None + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + + def load_menu_items(self): + items = [] + self.volume_monitor_signals = [] + + # Webcams etc + video_devices = [] + for i in os.listdir("/dev"): + if i.startswith("video"): + video_devices.append(i) + + if len(video_devices) > 0: + items.append(g15theme.MenuItem("video-devices", True, _("Video Devices"), icon = g15icontools.get_icon_path(["camera-web", "camera-video"]), activatable = False)) + for i in video_devices: + items.append(G15VideoDeviceMenuItem(self, i)) + + # Video File + def activate_video_file(): + gobject.idle_add(self._open_video_file) + items.append(g15theme.MenuItem("video-file", True, _("Open Audio/Video File"), activate = activate_video_file, icon = g15icontools.get_icon_path("folder"))) + + # DVD / Mounts + self.volume_monitor = gio.VolumeMonitor() + self.volume_monitor_signals.append(self.volume_monitor.connect("mount_added", self._on_mount_added)) + self.volume_monitor_signals.append(self.volume_monitor.connect("mount_removed", self._on_mount_removed)) + removable_media_items = [] + for i, mount in enumerate(self.volume_monitor.get_mounts()): + drive = mount.get_drive() + if not mount.is_shadowed() and drive is not None and drive.is_media_removable(): + removable_media_items.append(MountMenuItem('mount-%d' % i, mount, self)) + if len(removable_media_items): + items.append(g15theme.MenuItem("removable-devices", True, _("Removable Devices"), icon = g15icontools.get_icon_path(["driver-removable-media", "gnome-dev-removable"]), activatable = False)) + items += removable_media_items + + # Pulse + status, output = g15os.get_command_output("pacmd list-sources") + if status == 0 and len(output) > 0: + i = 0 + pulse_items = [] + for line in output.split("\n"): + line = line.strip() + if line.startswith("name: "): + name = line[7:-1] + elif line.startswith("device.description = "): + pulse_items.append(PulseSourceMenuItem(name, line[22:-1], self)) + if len(pulse_items) > 0: + items.append(g15theme.MenuItem("pulse-sources", True, _("PulseAudio Source"), icon = g15icontools.get_icon_path(["audio-card", "audio-speakers", "audio-volume-high", "audio-x-generic"]), activatable = False)) + items += pulse_items + + + # Visualisations - TODO - there must be a better way to list them + items.append(g15theme.MenuItem("visualisation-mode", True, _("Visualisation Mode"), icon = g15icontools.get_icon_path(["preferences-color", "gtk-select-color", "preferences-desktop-screensaver", "kscreensaver", "xscreensaver"]), activatable = False)) + for c in [ "goom", \ + "libvisual_bumpscope", \ + "libvisual_corona", \ + "libvisual_infinite", \ + "libvisual_jakdaw", \ + "libvisual_jess", \ + "libvisual_lv_analyzer", \ + "libvisual_lv_scope", \ + "libvisual_lv_oinksie", \ + "synaesthesia", \ + "spacescope", \ + "spectrascope", \ + "synaescope", \ + "wavescope", \ + "monoscope"]: + try: + gst.element_factory_make(c) + items.append(G15VisualisationMenuItem(c, self)) + except Exception as e: + logger.debug("Error creating visualizations", exc_info = e) + pass + + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + for p in self.player_pages: + p.delete() + for c in self.volume_monitor_signals: + self.volume_monitor.disconnect(c) + + ''' + Private + ''' + def _on_mount_added(self, monitor, mount, *args): + self.load_menu_items() + + def _on_mount_removed(self, monitor, mount, *args): + self.load_menu_items() + + def _open_video_file(self): + path = self._open_file() + if path: + mime_type, _ = mimetypes.guess_type(path) + if mime_type.startswith("audio"): + self._open_source(G15AudioFileSource(os.path.basename(path), path, get_visualisation(self))) + else: + self._open_source(G15VideoFileSource(os.path.basename(path), path)) + + def _grab_multimedia_keys(self): + try: + if self._grabbed_keys is not None: + raise Exception("Already grabbed") + self._app_name = "%s-%s" % ( g15globals.name, name) + def _on_key(app, key): + if app == self._app_name: + self._mm_key = None + if key == "Play": + self._mm_key = g15driver.G_KEY_PLAY + self._player_page._play() + elif key == "Stop": + self._mm_key = g15driver.G_KEY_STOP + self._player_page._stop() + elif key == "Next": + self._mm_key = g15driver.G_KEY_NEXT + self._player_page._fwd() + elif key == "Previous": + self._mm_key = g15driver.G_KEY_PREV + self._player_page._rew() + else: + logger.warning("Unsupported media key %s", key) + if self._mm_key_timer is not None: + self._mm_key_timer.cancel() + self._mm_key_timer = None + self._mm_key_timer = g15scheduler.schedule("CancelMMKey", 1.0, self._clear_mm_key) + + try: + self._settings = dbus.Interface(session_bus.get_object('org.g.SettingsDaemon', + '/org/gnome/SettingsDaemon'), 'org.gnome.SettingsDaemon') + self._settings.GrabMediaPlayerKeys(self._app_name, 0) + self._grabbed_keys = self._settings.connect_to_signal('MediaPlayerKeyPressed', _on_key) + except dbus.DBusException as e: + logger.debug("Error grabing multimedia keys. Trying alternative", exc_info = e) + self._settings = dbus.Interface(session_bus.get_object('org.gnome.SettingsDaemon', + '/org/gnome/SettingsDaemon/MediaKeys'), + 'org.gnome.SettingsDaemon.MediaKeys') + self._settings.GrabMediaPlayerKeys(self._app_name, 0) + self._grabbed_keys = self._settings.connect_to_signal('MediaPlayerKeyPressed', _on_key) + + logger.info("Grabbed multimedia keys") + except dbus.DBusException as error: + logger.warning("Could not grab multi-media keys.", exc_info = error) + + def _clear_mm_key(self): + self._mm_key = None + self.screen.redraw() + + def _release_multimedia_keys(self): + if self._grabbed_keys: + self._settings.ReleaseMediaPlayerKeys(self._app_name) + session_bus.remove_signal_receiver(self._grabbed_keys) + self._grabbed_keys = None + + def _open_source(self, source): + gobject.idle_add(self._do_open_source, source) + + def _do_open_source(self, source): + if can_grab_media_keys: + self._grab_multimedia_keys() + self._player_page = G15MediaPlayerPage(self.screen, source, self) + self.player_pages.append(self._player_page) + self.screen.add_page(self._player_page) + self.screen.redraw(self._player_page) + gobject.idle_add(self._player_page._play) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + + def _open_file(self): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name(_("All files")) + filter.add_pattern("*") + dialog.add_filter(filter) + + # Video + filter = gtk.FileFilter() + filter.set_name(_("Video Files")) + + filter.add_mime_type("application/ogg") + + filter.add_mime_type("video/ogg") + filter.add_mime_type("video/mpeg") + filter.add_mime_type("video/quicktime") + filter.add_mime_type("video/x-la-asf") + filter.add_mime_type("video/x-ms-asf") + filter.add_mime_type("video/x-msvideo") + filter.add_mime_type("video/x-sgi-movie") + + filter.add_pattern("*.ogx") + filter.add_pattern("*.ogv") + filter.add_pattern("*.mp2") + filter.add_pattern("*.mpa") + filter.add_pattern("*.mpe") + filter.add_pattern("*.mpeg") + filter.add_pattern("*.mpg") + filter.add_pattern("*.mpv2") + filter.add_pattern("*.mov") + filter.add_pattern("*.qt") + filter.add_pattern("*.lsf") + filter.add_pattern("*.lsx") + filter.add_pattern("*.asf") + filter.add_pattern("*.asr") + filter.add_pattern("*.asx") + filter.add_pattern("*.avi") + filter.add_pattern("*.movie") + dialog.add_filter(filter) + + # Audio + filter = gtk.FileFilter() + filter.set_name(_("Audio Files")) + + filter.add_mime_type("audio/ogg") + filter.add_mime_type("audio/vorbis") + filter.add_mime_type("audio/flac") + filter.add_mime_type("audio/x-ogg") + filter.add_mime_type("audio/x-vorbis") + filter.add_mime_type("audio/x-flac") + filter.add_mime_type("audio/basic") + filter.add_mime_type("audio/mid") + filter.add_mime_type("audio/mpeg") + filter.add_mime_type("audio/aiff") + filter.add_mime_type("audio/x-aiff") + filter.add_mime_type("audio/x-mpegurl") + filter.add_mime_type("audio/x-pn-realaudio") + filter.add_mime_type("audio/x-realaudio") + filter.add_mime_type("audio/wav") + filter.add_mime_type("audio/x-wav") + filter.add_mime_type("audio/x-au") + filter.add_mime_type("audio/x-midi") + filter.add_mime_type("audio/x-mpeg") + filter.add_mime_type("audio/x-mpeg3") + filter.add_mime_type("audio/x-mpeg-3") + filter.add_mime_type("audio/midi") + filter.add_mime_type("audio/x-mid") + + filter.add_pattern("*.flac") + filter.add_pattern("*.oga") + filter.add_pattern("*.ogg") + filter.add_pattern("*.au") + filter.add_pattern("*.snd") + filter.add_pattern("*.mid") + filter.add_pattern("*.rmi") + filter.add_pattern("*.mp3") + filter.add_pattern("*.aif") + filter.add_pattern("*.aifc") + filter.add_pattern("*.aiff") + filter.add_pattern("*.m3u") + filter.add_pattern("*.ra") + filter.add_pattern("*.ram") + filter.add_pattern("*.wav") + dialog.add_filter(filter) + + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + try: + if response == gtk.RESPONSE_OK: + return dialog.get_filename() + finally: + dialog.destroy() diff --git a/src/plugins/mediaplayer/oldvideoplayer.py b/src/plugins/mediaplayer/oldvideoplayer.py new file mode 100644 index 0000000..0890094 --- /dev/null +++ b/src/plugins/mediaplayer/oldvideoplayer.py @@ -0,0 +1,334 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("videoplayer", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +from threading import Timer +import gtk +import os +import select +import gobject +import tempfile +import subprocess +from threading import Lock +from threading import Thread + +# Plugin details - All of these must be provided +id = "videoplayer" +name = _("Video Player") +description = _("Plays videos! Very much experimental, this plugin uses \ +mplayer to generate JPEG images which are then loaded \ +and displayed on the LCD. This means it is very CPU AND \ +disk intensive and should only be used as a toy. ") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://localhost" +has_preferences = False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Stop"), + g15driver.NEXT_SELECTION : _("Play"), + g15driver.SELECT : _("Open file"), + g15driver.CLEAR : _("Toggle Mute"), + g15driver.VIEW : _("Change aspect") + } + +''' +This simple plugin displays system statistics +''' + +def create(gconf_key, gconf_client, screen): + return G15VideoPlayer(gconf_key, gconf_client, screen) + + +class PlayThread(Thread): + + def __init__(self, page): + Thread.__init__(self) + self.name = "PlayThread" + self.setDaemon(True) + self._page = page + + self.temp_dir = tempfile.mkdtemp("g15", "tmp") + self._process = subprocess.Popen(['mplayer', '-slave', '-noconsolecontrols','-really-quiet', + '-vo', 'jpeg', self._page._movie_path], cwd=self.temp_dir, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + self._page.redraw() + + def _playing(self): + return self._process.poll() == None + + def _stop(self): + try: + self._process.terminate() + except OSError: + # Got killed + pass + self._page.redraw() + + def _mute(self, mute): + if mute: + print self._command("mute", "1") + else: + print self._command("mute", "0") + + def _readlines(self): + ret = [] + while any(select.select([self._process.stdout.fileno()], [], [], 0.6)): + ret.append( self._process.stdout.readline() ) + return ret + + def _command(self, name, *args): + cmd = '%s%s%s\n'%(name, + ' ' if args else '', + ' '.join(repr(a) for a in args) + ) + self._process.stdin.write(cmd) + if name == 'quit': + return + return self.readlines() + + def set_aspect(self, aspect): + pass +# self.command("switch_ratio",str(float(aspect[0]) / float(aspect[1]))) + + def run(self): + self._process.wait() + +class G15VideoPage(g15theme.G15Page): + + def __init__(self, screen): + g15theme.G15Page.__init__(self, id, screen, title = name, theme = g15theme.G15Theme(self), thumbnail_painter = self._paint_thumbnail) + self._sidebar_offset = 0 + self._muted = False + self._lock = Lock() + self._surface = None + self._hide_timer = None + self._screen = screen + self._full_screen = self._screen.driver.get_size() + self._aspect = self._full_screen + self._playing = None + self._active = True + self._frame_index = 1 + self._frame_wait = 0.04 + self._thumb_icon = g15cairo.load_surface_from_file(g15icontools.get_icon_path(["media-video", "emblem-video", "emblem-videos", "video", "video-player" ])) + + def get_theme_properties(self): + properties = g15theme.G15Page.get_theme_properties(self) + properties["aspect"] = "%d:%d" % self._aspect + return properties + + def paint_theme(self, canvas, properties, attributes): + canvas.save() + + if self._sidebar_offset < 0 and self._sidebar_offset > -(self.theme.bounds[2]): + self._sidebar_offset -= 5 + + canvas.translate(self._sidebar_offset, 0) + g15theme.G15Page.paint_theme(self, canvas, properties, attributes) + canvas.restore() + + def paint(self, canvas): + g15theme.G15Page.paint(self, canvas) + wait = self._frame_wait + size = self._screen.driver.get_size() + + if self._playing != None: + + # Process may have been killed + if not self._playing._playing(): + self._stop() + + dir = sorted(os.listdir(self._playing.temp_dir), reverse=True) + if len(dir) > 1: + dir = dir[1:] + file = os.path.join(self._playing.temp_dir, dir[0]) + self._surface = g15cairo.load_surface_from_file(file) + for path in dir: + file = os.path.join(self._playing.temp_dir, path) + os.remove(file) + else: + wait = 0.1 + + if self._surface != None: + target_size = ( float(size[0]), float(size[0]) * (float(self._aspect[1]) ) / float(self._aspect[0]) ) + sx = float(target_size[0]) / float(self._surface.get_width()) + sy = float(target_size[1]) / float(self._surface.get_height()) + canvas.save() + canvas.translate((size[0] - target_size[0]) / 2.0,(size[1] - target_size[1]) / 2.0) + canvas.scale(sx, sy) + canvas.set_source_surface(self._surface) + canvas.paint() + canvas.restore() + + if self._playing != None: + timer = Timer(wait, self.redraw) + timer.name = "VideoRedrawTimer" + timer.setDaemon(True) + timer.start() + + ''' Functions specific to plugin + ''' + def _hide_sidebar(self, after = 0.0): + if after == 0.0: + self._sidebar_offset = -1 + self._hide_timer = None + else: + self._sidebar_offset = 0 + if self._hide_timer != None: + self._hide_timer.cancel() + self._hide_timer = g15scheduler.schedule("HideSidebar", after, self._hide_sidebar) + + def _open(self): + dialog = gtk.FileChooserDialog("Open..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + filter = gtk.FileFilter() + filter.set_name(_("All files")) + filter.add_pattern("*") + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name(_("Movies")) + filter.add_mime_type("video/mpeg") + filter.add_mime_type("video/quicktime") + filter.add_mime_type("video/x-la-asf") + filter.add_mime_type("video/x-ms-asf") + filter.add_mime_type("video/x-msvideo") + filter.add_mime_type("video/x-sgi-movie") + filter.add_pattern("*.mp2") + filter.add_pattern("*.mpa") + filter.add_pattern("*.mpe") + filter.add_pattern("*.mpeg") + filter.add_pattern("*.mpg") + filter.add_pattern("*.mpv2") + filter.add_pattern("*.mov") + filter.add_pattern("*.qt") + filter.add_pattern("*.lsf") + filter.add_pattern("*.lsx") + filter.add_pattern("*.asf") + filter.add_pattern("*.asr") + filter.add_pattern("*.asx") + filter.add_pattern("*.avi") + filter.add_pattern("*.movie") + dialog.add_filter(filter) + + response = dialog.run() + while gtk.events_pending(): + gtk.main_iteration(False) + if response == gtk.RESPONSE_OK: + print dialog.get_filename(), 'selected' + self._movie_path = dialog.get_filename() + if self._playing: + self._stop() + self._play() + dialog.destroy() + return False + + def _change_aspect(self): + if self._aspect == (16, 9): + self._aspect = (4, 3) + elif self._aspect == (4, 3): + # Just take up the most room + self._aspect = (24, 9) + elif self._aspect == (24, 9): + self._aspect = self._full_screen + else: + self._aspect = (16, 9) + self._screen.redraw(self._page) + + def _play(self): + self._lock.acquire() + try: + self._hide_sidebar(3.0) + self._playing = PlayThread(self) + self._playing.set_aspect(self._aspect) + self._playing.mute(self.muted) + self._playing.start() + finally: + self._lock.release() + + def _stop(self): + self._lock.acquire() + try: + if self._hide_timer != None: + self._hide_timer.cancel() + self._sidebar_offset = 0 + self._playing._stop() + self._playing = None + finally: + self._lock.release() + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._thumb_icon != None and self._screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + +class G15VideoPlayer(): + + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def activate(self): + self._page = G15VideoPage(self._screen) + self._screen.add_page(self._page) + self._screen.redraw(self._page) + self._screen.key_handler.action_listeners.append(self) + + def deactivate(self): + if self._page._playing != None: + self._page._stop() + self._screen.del_page(self._page) + self._screen.key_handler.action_listeners.remove(self) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page is not None and self._page.is_visible(): + if binding.action == g15driver.SELECT: + gobject.idle_add(self._page._open) + elif binding.action == g15driver.NEXT_SELECTION: + if self._page._playing == None: + self._page._play() + elif binding.action == g15driver.PREVIOUS_SELECTION: + if self._page._playing != None: + self._page._stop() + elif binding.action == g15driver.VIEW: + if self._page._playing != None: + self._page._hide_sidebar(3.0) + self._change_aspect() + elif binding.action == g15driver.CLEAR: + self._page.muted = not self._page.muted + if self._page._playing != None: + self._page._hide_sidebar(3.0) + self._playing.mute(self._page.muted) + else: + return False + return True diff --git a/src/plugins/menu/Makefile.am b/src/plugins/menu/Makefile.am new file mode 100644 index 0000000..6617517 --- /dev/null +++ b/src/plugins/menu/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/menu +plugin_DATA = menu.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/menu/i18n/menu.en_GB.po b/src/plugins/menu/i18n/menu.en_GB.po new file mode 100644 index 0000000..0b02f9b --- /dev/null +++ b/src/plugins/menu/i18n/menu.en_GB.po @@ -0,0 +1,56 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: menu.py:39 +msgid "Menu" +msgstr "Menu" + +#: menu.py:40 +msgid "" +"Allows selections of any currently active screen through a menu on the LCD." +msgstr "" +"Allows selections of any currently active screen through a menu on the LCD." + +#: menu.py:42 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: menu.py:48 +msgid "Previous item" +msgstr "Previous item" + +#: menu.py:49 +msgid "Next item" +msgstr "Next item" + +#: menu.py:50 +msgid "Next page" +msgstr "Next page" + +#: menu.py:51 +msgid "Previous page" +msgstr "Previous page" + +#: menu.py:52 +msgid "Show selected item" +msgstr "Show selected item" + +#: menu.py:53 +msgid "Show menu" +msgstr "Show menu" diff --git a/src/plugins/menu/i18n/menu.pot b/src/plugins/menu/i18n/menu.pot new file mode 100644 index 0000000..db009a0 --- /dev/null +++ b/src/plugins/menu/i18n/menu.pot @@ -0,0 +1,55 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: menu.py:39 +msgid "Menu" +msgstr "" + +#: menu.py:40 +msgid "" +"Allows selections of any currently active screen through a menu on the LCD." +msgstr "" + +#: menu.py:42 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: menu.py:48 +msgid "Previous item" +msgstr "" + +#: menu.py:49 +msgid "Next item" +msgstr "" + +#: menu.py:50 +msgid "Next page" +msgstr "" + +#: menu.py:51 +msgid "Previous page" +msgstr "" + +#: menu.py:52 +msgid "Show selected item" +msgstr "" + +#: menu.py:53 +msgid "Show menu" +msgstr "" diff --git a/src/plugins/menu/menu.py b/src/plugins/menu/menu.py new file mode 100644 index 0000000..7ff0f1b --- /dev/null +++ b/src/plugins/menu/menu.py @@ -0,0 +1,185 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("menu", modfile = __file__).ugettext + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15plugin as g15plugin +from gnome15.util.g15pythonlang import find +import sys +import cairo +import base64 +from cStringIO import StringIO +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="menu" +name=_("Menu") +description=_("Allows selections of any currently active screen through a menu on the LCD.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Show selected item"), + g15driver.MENU : _("Show menu") + } + + +def create(gconf_key, gconf_client, screen): + return G15Menu(gconf_client, gconf_key, screen) + +class MenuItem(g15theme.MenuItem): + + + def __init__(self, item_page, plugin, id): + g15theme.MenuItem.__init__(self, id) + self._item_page = item_page + self.thumbnail = None + self.plugin = plugin + + def get_page(self): + return self._item_page + + def activate(self): + self.plugin.hide_menu() + self.plugin.screen.raise_page(self._item_page) + self.plugin.screen.resched_cycle() + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self._item_page.title + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + item_properties["item_icon"] = self.thumbnail + return item_properties + +class G15Menu(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, [ "gnome-main-menu", "gnome-window-manager", "gnome-gmenu", "kmenuedit" ], id, name) + self._show_on_activate = False + + def activate(self): + self.delete_timer = None + g15plugin.G15MenuPlugin.activate(self) + self.reload_theme() + self.listener = MenuScreenChangeListener(self) + self.screen.add_screen_change_listener(self.listener) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + self.screen.remove_screen_change_listener(self.listener) + + def action_performed(self, binding): + if self.page is not None: + self._reset_delete_timer() + if binding.action == g15driver.MENU: + if self.page is not None: + self.hide_menu() + return True + else: + self.show_menu() + self.page.set_priority(g15screen.PRI_HIGH) + return True + + def hide_menu(self): + g15plugin.G15MenuPlugin.hide_menu(self) + + def show_menu(self): + visible_page = self.screen.get_visible_page() + g15plugin.G15MenuPlugin.show_menu(self) + self._reset_delete_timer() + if visible_page: + item = find(lambda m: m._item_page == visible_page, self.menu.get_children()) + if item: + self.menu.set_selected_item(item) + + def load_menu_items(self): + items = [] + for page in self.screen.pages: + if page != self.page and page.priority > g15screen.PRI_INVISIBLE: + items.append(MenuItem(page, self, "menuitem-%s" % page.id )) + items = sorted(items, key=lambda item: item._item_page.title) + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + for item in items: + self._load_item_icon(item) + + ''' + Private + ''' + def _load_item_icon(self, item): + if item._item_page.thumbnail_painter != None: + img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.height, self.screen.height) + thumb_canvas = cairo.Context(img) + try : + if item._item_page.thumbnail_painter(thumb_canvas, self.screen.height, True): + img_data = StringIO() + img.write_to_png(img_data) + item.thumbnail = base64.b64encode(img_data.getvalue()) + + except Exception as e: + logger.warning("Problem with painting thumbnail in %s", + item._item_page.id, + exc_info = e) + + def _reset_delete_timer(self): + if self.delete_timer: + self.delete_timer.cancel() + self.delete_timer = self.screen.delete_after(10.0, self.page) + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + +class MenuScreenChangeListener(g15screen.ScreenChangeAdapter): + def __init__(self, plugin): + self.plugin = plugin + + def new_page(self, page): + if self.plugin.page != None and page != self.plugin.page and page.priority > g15screen.PRI_INVISIBLE: + items = self.plugin.menu.get_children() + item = MenuItem(page, self.plugin, "menuitem-%s" % page.id ) + self.plugin._load_item_icon(item) + items.append(item) + items = sorted(items, key=lambda item: item._item_page.title) + self.plugin.menu.set_children(items) + self.plugin.page.redraw() + + def title_changed(self, page, title): + if self.plugin.page != None and page != self.plugin.page: + self.plugin.page.redraw() + + def deleted_page(self, page): + if self.plugin.page != None and page != self.plugin.page: + self.plugin.menu.remove_child(self.plugin.menu.get_child_by_id("menuitem-%s" % page.id)) + self.plugin.page.redraw() \ No newline at end of file diff --git a/src/plugins/mounts/Makefile.am b/src/plugins/mounts/Makefile.am new file mode 100644 index 0000000..16d01ce --- /dev/null +++ b/src/plugins/mounts/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/mounts +plugin_DATA = mounts.ui mounts.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mounts/default/Makefile.am b/src/plugins/mounts/default/Makefile.am new file mode 100644 index 0000000..9f33860 --- /dev/null +++ b/src/plugins/mounts/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/mounts/default +theme_DATA = \ + default-menu-entry.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mounts/default/default-menu-entry.svg b/src/plugins/mounts/default/default-menu-entry.svg new file mode 100644 index 0000000..61326d2 --- /dev/null +++ b/src/plugins/mounts/default/default-menu-entry.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + ${item_name} + ${item_alt} + + + + diff --git a/src/plugins/mounts/default/g19-menu-entry.svg b/src/plugins/mounts/default/g19-menu-entry.svg new file mode 100644 index 0000000..2af6329 --- /dev/null +++ b/src/plugins/mounts/default/g19-menu-entry.svg @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + + + + ${item_alt} + + + + + + ${item_name} + + + + ${item_alt} + + + + diff --git a/src/plugins/mounts/i18n/mounts.en_GB.po b/src/plugins/mounts/i18n/mounts.en_GB.po new file mode 100644 index 0000000..8545ee5 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/mounts.glade.h:1 +msgid "Mounts Preferences" +msgstr "Mounts Preferences" + +#: i18n/mounts.glade.h:2 +msgid "Raise page when mounts change" +msgstr "Raise page when mounts change" + +#: i18n/mounts.glade.h:3 +msgid "center" +msgstr "center" + +#: i18n/mounts.glade.h:4 +msgid "scale" +msgstr "scale" + +#: i18n/mounts.glade.h:5 +msgid "stretch" +msgstr "stretch" + +#: i18n/mounts.glade.h:6 +msgid "tile" +msgstr "tile" + +#: i18n/mounts.glade.h:7 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/mounts/i18n/mounts.glade.h b/src/plugins/mounts/i18n/mounts.glade.h new file mode 100644 index 0000000..a211f39 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Mounts Preferences"); +char *s = N_("Raise page when mounts change"); +char *s = N_("center"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/mounts/i18n/mounts.pot b/src/plugins/mounts/i18n/mounts.pot new file mode 100644 index 0000000..1cd7115 --- /dev/null +++ b/src/plugins/mounts/i18n/mounts.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/mounts.glade.h:1 +msgid "Mounts Preferences" +msgstr "" + +#: i18n/mounts.glade.h:2 +msgid "Raise page when mounts change" +msgstr "" + +#: i18n/mounts.glade.h:3 +msgid "center" +msgstr "" + +#: i18n/mounts.glade.h:4 +msgid "scale" +msgstr "" + +#: i18n/mounts.glade.h:5 +msgid "stretch" +msgstr "" + +#: i18n/mounts.glade.h:6 +msgid "tile" +msgstr "" + +#: i18n/mounts.glade.h:7 +msgid "zoom" +msgstr "" diff --git a/src/plugins/mounts/mounts.py b/src/plugins/mounts/mounts.py new file mode 100644 index 0000000..e289a08 --- /dev/null +++ b/src/plugins/mounts/mounts.py @@ -0,0 +1,345 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("mounts", modfile = __file__).ugettext + +import gnome15.g15plugin as g15plugin +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gio +import gtk +import os.path +import gobject + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="mounts" +name=_("Mounts") +description=_("Shows mount points, allows mounting, unmounting \ +and ejecting of removable media. Also displays \ +free, used or total disk space on mounted media.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous mount"), + g15driver.NEXT_SELECTION : _("Next mount"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Mount, unmount or eject"), + g15driver.VIEW : _("Toggle between free,\navailable and used"), + } + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "mounts.ui")) + dialog = widget_tree.get_object("MountsDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise" % gconf_key, "RaisePageCheckbox", True, widget_tree) + dialog.run() + dialog.hide() + +def create(gconf_key, gconf_client, screen): + return G15Places(gconf_client, gconf_key, screen) + +POSSIBLE_ICON_NAMES = [ "folder" ] + +MODES = { "free" : _("Free"), "used" : _("Used"), "size" : _("Size") } +MODE_LIST = list(MODES.keys()) + +""" +Represents a mount as a single item in a menu +""" +class MountMenuItem(g15theme.MenuItem): + def __init__(self, id, mount, plugin): + g15theme.MenuItem.__init__(self, id) + self.mount = mount + self._plugin = plugin + self._refresh() + + def _refresh(self): + self.disk_size = 0 + self.disk_free = 0 + self.disk_used = 0 + self.disk_used_pc = 0 + root = self.mount.get_root() + try: + self.fs_attr = root.query_filesystem_info("*") + self.disk_size = float(max(1, self.fs_attr.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_SIZE))) + self.disk_free = float(self.fs_attr.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)) + self.disk_used = float(self.disk_size - self.disk_free) + self.disk_used_pc = int ( ( self.disk_used / self.disk_size ) * 100.0 ) + except Exception as e: + logger.debug("Error refreshing", exc_info = e) + pass + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.mount.get_name() + item_properties["item_type"] = "" + icon_names = [] + icon = self.mount.get_icon() + if isinstance(icon, gio.FileIcon): + icon_names.append(icon.get_file().get_path()) + else: + icon_names += icon.get_names() + + icon_names += "gnome-dev-harddisk" + item_properties["item_icon"] = g15icontools.get_icon_path(icon_names) + item_properties["disk_usage"] = self.disk_used_pc + item_properties["sel_disk_usage"] = self.disk_used_pc + item_properties["disk_used_mb"] = "%4.2f" % (self.disk_used / 1024.0 / 1024.0 ) + item_properties["disk_free_mb"] = "%4.2f" % (self.disk_free / 1024.0 / 1024.0 ) + item_properties["disk_size_mb"] = "%4.2f" % (self.disk_size / 1024.0 / 1024.0 ) + item_properties["disk_used_gb"] = "%4.1f" % (self.disk_used / 1024.0 / 1024.0 / 1024.0 ) + item_properties["disk_free_gb"] = "%4.1f" % (self.disk_free / 1024.0 / 1024.0 / 1024.0 ) + item_properties["disk_size_gb"] = "%4.1f" % (self.disk_size / 1024.0 / 1024.0 / 1024.0 ) + suffix = "G" if self.disk_size >= ( 1 * 1024.0 * 1024.0 * 1024.0 ) else "M" + item_properties["disk_used"] = "%s %s" % ( item_properties["disk_used_gb"], suffix ) + item_properties["disk_free"] = "%s %s" % ( item_properties["disk_free_gb"], suffix ) + item_properties["disk_size"] = "%s %s" % ( item_properties["disk_size_gb"], suffix ) + + if self._plugin._mode == "free": + item_properties["item_alt"] = item_properties["disk_free"] + elif self._plugin._mode == "used": + item_properties["item_alt"] = item_properties["disk_used"] + elif self._plugin._mode == "size": + item_properties["item_alt"] = item_properties["disk_size"] + + return item_properties + + def activate(self): + if self.mount.can_eject(): + self.mount.eject(self._ejected, flags = gio.MOUNT_UNMOUNT_NONE) + else: + if self.mount.can_unmount(): + self.mount.unmount(self._unmounted, flags = gio.MOUNT_UNMOUNT_NONE) + return True + + def _ejected(self, arg1, arg2): + logger.info("Ejected %s %s %s", self.mount.get_name(), str(arg1), str(arg2)) + + def _unmounted(self, arg1, arg2): + logger.info("Unmounted %s %s %s", self.mount.get_name(), str(arg1), str(arg2)) + +""" +Represents a volumne as a single item in a menu +""" +class VolumeMenuItem(g15theme.MenuItem): + def __init__(self, id, volume): + g15theme.MenuItem.__init__(self, id) + self.volume = volume + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.volume.get_name() + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + + item_properties["item_icon"] = g15icontools.get_icon_path([ self.volume.get_icon().get_names()[0], "gnome-dev-harddisk" ]) + return item_properties + + def activate(self): + if self.volume.can_mount(): + self.volume.mount(None, self._mounted, flags = gio.MOUNT_UNMOUNT_NONE) + return True + + def _mounted(self, arg1, arg2): + logger.info("Mounted %s %s %s", self.volume.get_name(), str(arg1), str(arg2)) + + +""" +Places plugin class +""" +class G15Places(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + self._signal_handles = [] + self._handle = None + self._modes = [ "free", "used", "size" ] + self._mode = "free" + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + # Get the initial list of volumes and mounts + gobject.idle_add(self._do_activate) + + def _do_activate(self): + self.volume_monitor = gio.VolumeMonitor() + for mount in self.volume_monitor.get_mounts(): + if not mount.is_shadowed(): + self._add_mount(mount) + if len(self.menu.get_children()) > 0: + self.menu.add_separator() + for volume in self.volume_monitor.get_volumes(): + if volume.get_mount() == None: + self._add_volume(volume) + + # Watch for changes + self.volume_monitor.connect("mount_added", self._on_mount_added) + self.volume_monitor.connect("mount_removed", self._on_mount_removed) + + # Refresh disk etc space every minute + self._handle = g15scheduler.schedule("DiskRefresh", 60.0, self._refresh) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + for handle in self._signal_handles: + self.session_bus.remove_signal_receiver(handle) + if self._handle: + self._handle.cancel() + self._handle = None + + def get_theme_properties(self): + properties = g15plugin.G15MenuPlugin.get_theme_properties(self) + properties["alt_title"] = MODES[self._mode] + idx = MODE_LIST.index(self._mode) + 1 + properties["list"] = MODES[MODE_LIST[0] if idx == len(MODE_LIST) else MODE_LIST[idx]] + if isinstance(self.menu.selected, VolumeMenuItem): + properties["sel"] = _("Mount") + elif isinstance(self.menu.selected, MountMenuItem): + properties["sel"] = _("Eject") if self.menu.selected.mount.can_eject() else _("Unmo.") + else: + properties["sel"] = "" + return properties + + def action_performed(self, binding): + if binding.action == g15driver.VIEW: + idx = MODE_LIST.index(self._mode) + 1 + self._mode = MODE_LIST[0] if idx == len(MODE_LIST) else MODE_LIST[idx] + self.screen.redraw(self.page) + + + """ + Private functions + """ + + def _refresh(self): + """ + Refresh the free space etc for all items + """ + for item in self.menu.get_children(): + if isinstance(item, MountMenuItem): + item._refresh() + self.screen.redraw(self.page) + + def _on_mount_added(self, monitor, mount, *args): + + # Remove the volume for this remove + for item in self.menu.get_children(): + if isinstance(item, VolumeMenuItem) and self._get_key(item.volume) == self._get_key(mount): + self._remove_volume(item.volume) + + + """ + Invoked when new mount is available + """ + self._remove_mount(mount) + self._add_mount(mount) + + self._popup() + + def _on_mount_removed(self, monitor, mount, *args): + """ + Invoked when a mount is removed + """ + self._remove_mount(mount) + + # Look for new volumes + for volume in self.volume_monitor.get_volumes(): + if not self._get_item_for_volume(volume) and volume.get_mount() == None: + self._add_volume(volume) + + self._popup() + + def _popup(self): + if not self.page.is_visible() and g15gconf.get_bool_or_default(self.gconf_client,"%s/raise" % self.gconf_key, True): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 4.0) + self.screen.redraw(self.page) + + def _get_key(self, item): + """ + Get a unique key for volume / mount + """ + return "%s-%s" % ( str(item.get_uuid()), str(item.get_name())) + + def _remove_volume(self, volume): + """ + Remove a volume from the menu + """ + logger.info("Removing volume %s", str(volume)) + self.menu.remove_child(self._get_item_for_volume(volume)) + self.screen.redraw(self.page) + + def _remove_mount(self, mount): + """ + Remove a mount from the menu + """ + logger.info("Removing mount %s", str(mount)) + mnt = self._get_item_for_mount(mount) + if mnt: + self.menu.remove_child(mnt) + self.screen.redraw(self.page) + + def _get_item_for_mount(self, mount): + """ + Get the menu item for the given mount + """ + for item in self.menu.get_children(): + if isinstance(item, MountMenuItem) and self._get_key(mount) == self._get_key(item.mount): + return item + + def _get_item_for_volume(self, volume): + """ + Get the menu item for the given volume + """ + for item in self.menu.get_children(): + if isinstance(item, VolumeMenuItem) and self._get_key(volume) == self._get_key(item.volume): + return item + + def _add_volume(self, volume): + """ + Add a new volume to the menu + """ + logger.info("Adding volume %s", str(volume)) + item = VolumeMenuItem("volumeitem-%s" % self._get_key(volume), volume) + self.menu.add_child(item) + self.screen.redraw(self.page) + + def _add_mount(self, mount): + """ + Add a new mount to the menu + """ + logger.info("Adding mount %s", str(mount)) + item = MountMenuItem("mountitem-%s" % self._get_key(mount), mount, self) + self.menu.add_child(item, 0) + self.screen.redraw(self.page) \ No newline at end of file diff --git a/src/plugins/mounts/mounts.ui b/src/plugins/mounts/mounts.ui new file mode 100644 index 0000000..e195fee --- /dev/null +++ b/src/plugins/mounts/mounts.ui @@ -0,0 +1,100 @@ + + + + + + + False + 5 + Mounts Preferences + False + True + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise page when mounts change + True + True + False + True + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + diff --git a/src/plugins/mpris/Makefile.am b/src/plugins/mpris/Makefile.am new file mode 100644 index 0000000..9533a95 --- /dev/null +++ b/src/plugins/mpris/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default bigcover +plugindir = $(datadir)/gnome15/plugins/mpris +plugin_DATA = mpris.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/mpris/bigcover/Makefile.am b/src/plugins/mpris/bigcover/Makefile.am new file mode 100644 index 0000000..732b679 --- /dev/null +++ b/src/plugins/mpris/bigcover/Makefile.am @@ -0,0 +1,6 @@ +themedir = $(datadir)/gnome15/plugins/mpris/bigcover +theme_DATA = bigcover.theme \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mpris/bigcover/bigcover.theme b/src/plugins/mpris/bigcover/bigcover.theme new file mode 100644 index 0000000..21572be --- /dev/null +++ b/src/plugins/mpris/bigcover/bigcover.theme @@ -0,0 +1,4 @@ +[theme] +name=Big Cover +description=Album artwork takes most of screen. +supported_models=g19 \ No newline at end of file diff --git a/src/plugins/mpris/bigcover/g19.svg b/src/plugins/mpris/bigcover/g19.svg new file mode 100644 index 0000000..3f7a7b9 --- /dev/null +++ b/src/plugins/mpris/bigcover/g19.svg @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + ${time_text} + + diff --git a/src/plugins/mpris/default/Makefile.am b/src/plugins/mpris/default/Makefile.am new file mode 100644 index 0000000..36729e1 --- /dev/null +++ b/src/plugins/mpris/default/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/mpris/default +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + pause.gif \ + play.gif + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/mpris/default/default.svg b/src/plugins/mpris/default/default.svg new file mode 100644 index 0000000..03a1245 --- /dev/null +++ b/src/plugins/mpris/default/default.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${time_text} + ${title} + ${album} + ${artist} + + diff --git a/src/plugins/mpris/default/g19.svg b/src/plugins/mpris/default/g19.svg new file mode 100644 index 0000000..a964ecb --- /dev/null +++ b/src/plugins/mpris/default/g19.svg @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + ${album} + ${artist} + ${title} + ${time_text} + ${genre}${bitrate}bps + + + + + + + + + + + + + + + diff --git a/src/plugins/mpris/default/mx5500.svg b/src/plugins/mpris/default/mx5500.svg new file mode 100644 index 0000000..33e67ea --- /dev/null +++ b/src/plugins/mpris/default/mx5500.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${time_text} + ${title} + ${album} + ${artist} + + diff --git a/src/plugins/mpris/default/pause.gif b/src/plugins/mpris/default/pause.gif new file mode 100644 index 0000000000000000000000000000000000000000..7301bd13ee3c6baf973fe2d93dfacdfccb97133e GIT binary patch literal 71 zcmZ?wbhEHbWM*JvXkcUj0xl, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: mpris.py:61 +msgid "Media Player" +msgstr "" + +#: mpris.py:62 +msgid "" +"Displays information about currently playing media. Requires a player that " +"supports the MPRIS (version 1 or 2) specification. This includes Rhythmbox, " +"Banshee (with a plugin), Audacious, VLC and others. Supports multiple media " +"players running at the same time, each gets their own screen." +msgstr "" + +#: mpris.py:68 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: mpris.py:282 +msgid "No track playing" +msgstr "" diff --git a/src/plugins/mpris/mpris.py b/src/plugins/mpris/mpris.py new file mode 100644 index 0000000..b31bc1d --- /dev/null +++ b/src/plugins/mpris/mpris.py @@ -0,0 +1,768 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("mpris", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +import dbus +import os +import time + +import xdg.Mime as mime +import xdg.BaseDirectory +import urllib + +# Logging +import logging +from dbus.exceptions import DBusException +logger = logging.getLogger(__name__) + +# Custom actions +NEXT_TRACK = "mpris-next-track" +PREV_TRACK = "mpris-previous-track" +PLAY_TRACK = "mpris-play-track" +STOP_TRACK = "mpris-stop-track" + +# Register the action with all supported models +g15devices.g15_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[NEXT_TRACK] = g15actions.ActionBinding(NEXT_TRACK, [ g15driver.G_KEY_NEXT ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PREV_TRACK] = g15actions.ActionBinding(PREV_TRACK, [ g15driver.G_KEY_PREV ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[STOP_TRACK] = g15actions.ActionBinding(STOP_TRACK, [ g15driver.G_KEY_STOP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[PLAY_TRACK] = g15actions.ActionBinding(PLAY_TRACK, [ g15driver.G_KEY_PLAY ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="mpris" +name=_("Now Playing") +description=_("Displays information about currently playing media. Requires \ +a player that supports the MPRIS (version 1 or 2) specification. This \ +includes Rhythmbox, Banshee (with a plugin), Audacious, VLC and others. \ +Supports multiple media players running at the same time, each gets \ +their own screen.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + NEXT_TRACK : "Skip to the next track", + PREV_TRACK : "Skip to the previous track", + STOP_TRACK : "Stop the current track", + PLAY_TRACK : "Play / Pause the current track", + } + +# Players that are not supported +mpris_blacklist = [ "org.mpris.xbmc", "org.mpris.audacious" ] + +def create(gconf_key, gconf_client, screen): + return G15MPRIS(gconf_client,gconf_key, screen) + +class AbstractMPRISPlayer(): + + def __init__(self, gconf_client, screen, players, interface_name, session_bus, title, theme): + logger.info("Starting player %s", interface_name) + self.stopped = False + self.elapsed = 0 + self.volume = 0 + self.hidden = True + self.title = title + self.session_bus = session_bus + self.page = None + self.duration = 0 + self.screen = screen + self.interface_name = interface_name + self.players = players + self.playing_uri = None + self.playback_started = 0 + self.start_elapsed = 0 + self.gconf_client = gconf_client + self.cover_image = None + self.thumb_image = None + self.cover_uri = None + self.song_properties = {} + self.status = "Stopped" + self.redraw_timer = None + self.theme = theme + + def action_performed(self, binding): + if binding.action == NEXT_TRACK: + self.next_track() + return True + elif binding.action == PREV_TRACK: + self.prev_track() + return True + elif binding.action == PLAY_TRACK: + self.play_pause_track() + return True + elif binding.action == STOP_TRACK: + self.stop_track() + return True + + def next_track(self): + logger.warning("Next track not implemented") + + def prev_track(self): + logger.warning("Previous track not implemented") + + def play_pause_track(self): + logger.warning("Play pause track not implemented") + + def stop_track(self): + logger.warning("Stop track not implemented") + + def check_status(self): + new_status = self.get_new_status() + self.volume = self.get_volume() + if new_status != self.status: + self.set_status(new_status) + else: + if self.status == "Playing": + self.recalc_progress() + self.screen.redraw(self.page) + + def reset_elapsed(self): + logger.debug("Reset track elapsed time") + self.start_elapsed = self.get_progress() + self.playback_started = time.time() + + def set_status(self, new_status): + if new_status != self.status: + logger.info("Playback status changed to %s", new_status) + self.status = new_status + if self.status == "Playing": + g15scheduler.schedule("playbackStarted", 1.0, self._playback_started) + elif self.status == "Paused": + self.cancel_redraw() + if self.page != None: + logger.info("Paused.") + self.load_song_details() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + else: + self.show_page() + elif self.status == "Stopped": + self.cancel_redraw() + self.hide_page() + + def _playback_started(self): + self.reset_elapsed() + logger.info("Now playing, showing page") + self.show_page() + self.schedule_redraw() + + def stop(self): + logger.info("Stopping player %s", self.interface_name) + self.stopped = True + self.on_stop() + if self.redraw_timer != None: + self.redraw_timer.cancel() + if self.page != None: + self.hide_page() + del self.players[self.interface_name] + + def show_page(self): + self.load_song_details() + self.page = self.screen.get_page(page_id="MPRIS%s" % self.title) + if self.page == None: + self.page = g15theme.G15Page("MPRIS%s" % self.title, self.screen, on_shown=self.on_shown, \ + on_hidden=self.on_hidden, theme_properties_callback = self._get_properties, \ + panel_painter = self.paint_panel, thumbnail_painter = self.paint_thumbnail, \ + theme = self.theme, title = self.title, + originating_plugin = self) + self.screen.add_page(self.page) + self.screen.redraw(self.page) + else: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def hide_page(self): + self.screen.del_page(self.page) + self.page = None + + def redraw(self): + if self.status == "Playing": + self.elapsed = time.time() - self.playback_started + self.start_elapsed + self.recalc_progress() + self.screen.redraw(self.page) + self.schedule_redraw() + + def schedule_redraw(self): + self.cancel_redraw() + self.redraw_timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "MPRIS2Redraw", 1.0, self.redraw) + + def on_shown(self): + self.hidden = False + if self.status == "Playing": + self.schedule_redraw() + + def on_hidden(self): + self.hidden = True + self.cancel_redraw() + + def cancel_redraw(self): + if self.redraw_timer != None: + self.redraw_timer.cancel() + self.redraw_timer = None + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_image != None: + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_image, canvas) + return size + + def paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None and self.thumb_image != None and self.status == "Playing": + size = g15cairo.paint_thumbnail_image(allocated_size, self.thumb_image, canvas) + return size + + def process_properties(self): + + logger.debug("Processing properties") + + self.recalc_progress() + # Find the best icon for the media + + + if "art_uri" in self.song_properties and self.song_properties["art_uri"] != "": + new_cover_uri = self.song_properties["art_uri"] + else: + cover_art = os.path.join(xdg.BaseDirectory.xdg_cache_home, + "rhythmbox", + "covers", + "%s - %s.jpg" % (self.song_properties["artist"], + self.song_properties["album"])) + new_cover_uri = None + if cover_art != None and os.path.exists(cover_art): + new_cover_uri = cover_art + + if new_cover_uri == None: + new_cover_uri = self.get_default_cover() + + if new_cover_uri != self.cover_uri: + self.cover_uri = new_cover_uri + logger.info("Getting cover art from %s", self.cover_uri) + self.cover_image = None + self.thumb_image = None + if self.cover_uri != None: + cover_image = g15cairo.load_surface_from_file(self.cover_uri, self.screen.driver.get_size()[0]) + if cover_image: + self.cover_image = cover_image + + # If the cover URI was from HTTP, then we cached it. Use that as the URI + if self.cover_uri.startswith("http:") or self.cover_uri.startswith("http:"): + self.cover_uri = g15cairo.get_image_cache_file(self.cover_uri, self.screen.driver.get_size()[0]) + else: + cover_image = self.get_default_cover() + logger.warning("Failed to loaded preferred cover art, " \ + "falling back to default of %s", cover_image) + if cover_image: + self.cover_uri = cover_image + self.cover_image = g15cairo.load_surface_from_file(self.cover_uri, self.screen.driver.get_size()[0]) + + # Track status + if self.status == "Stopped": + self.song_properties["stopped"] = True + self.song_properties["icon"] = g15icontools.get_icon_path(["media-stop", "media-playback-stop", "gtk-media-stop", "player_stop" ], self.screen.height) + self.song_properties["title"] = _("No track playing") + self.song_properties["time_text"] = "" + else: + if self.status == "Playing": + if self.screen.driver.get_bpp() == 1: + self.thumb_image = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), "play.gif")) + else: + self.thumb_image = self.cover_image + self.song_properties["playing"] = True + else: + if self.screen.driver.get_bpp() == 1: + self.thumb_image = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), "pause.gif")) + else: + self.thumb_image = self.cover_image + self.song_properties["paused"] = True + self.song_properties["icon"] = self.cover_uri + + def get_default_cover(self): + mime_type = mime.get_type(self.playing_uri) + new_cover_uri = None + if mime_type != None: + mime_icon = g15icontools.get_icon_path(str(mime_type).replace("/","-"), size=self.screen.height) + if mime_icon != None: + new_cover_uri = mime_icon + if new_cover_uri != None: + try : + new_cover_uri = "file://" + urllib.pathname2url(new_cover_uri) + except Exception as e: + logger.debug("Error getting default cover, using None", exc_info = e) + new_cover_uri = None + + if new_cover_uri == None: + new_cover_uri = g15icontools.get_icon_path(["audio-player", "applications-multimedia" ], size=self.screen.height) + + return new_cover_uri + + def recalc_progress(self): + logger.debug("Recalculating progress") + if not self.duration or self.duration < 1: + self.song_properties["track_progress_pc"] = "0" + self.song_properties["time_text"] = self.get_formatted_time(self.elapsed) + else: + pc = 100 / float(self.duration) + val = int(pc * self.elapsed) + self.song_properties["track_progress_pc"] = str(val) + self.song_properties["time_text"] = self.get_formatted_time(self.elapsed) + " of " + self.get_formatted_time(self.duration) + + # Volume Icon + vol_icon = "audio-volume-muted" + if self.volume > 0.0 and self.volume < 34.0: + vol_icon = "audio-volume-low" + elif self.volume >= 34.0 and self.volume < 67.0: + vol_icon = "audio-volume-medium" + elif self.volume >= 67.0: + vol_icon = "audio-volume-high" + self.song_properties["vol_icon"] = g15icontools.get_icon_path(vol_icon, self.screen.height) + + # For the bars on the G15 (the icon is too small, bars are better) + for i in range(0, int( self.volume / 10 ) + 1, 1): + self.song_properties["bar" + str(i)] = True + + + def get_formatted_time(self, seconds): + return "%0d.%02d" % ( int (seconds / 60), int( seconds % 60 ) ) + + def get_new_status(self): + raise Exception("Not implemented.") + + def load_song_details(self): + raise Exception("Not implemented.") + + def get_progress(self): + raise Exception("Not implemented.") + + def get_volume(self): + raise Exception("Not implemented.") + + def on_stop(self): + raise Exception("Not implemented.") + + def _get_properties(self): + return dict(self.song_properties) + +class MPRIS1Player(AbstractMPRISPlayer): + + def __init__(self, gconf_client, screen, players, bus_name, session_bus, theme): + self.timer = None + root_obj = session_bus.get_object(bus_name, '/') + root = dbus.Interface(root_obj, 'org.freedesktop.MediaPlayer') + AbstractMPRISPlayer.__init__(self, gconf_client, screen, players, bus_name, session_bus, root.Identity(), theme) + + # There is no seek / position changed event in MPRIS1, so we poll + player_obj = session_bus.get_object(bus_name, '/Player') + self.player = dbus.Interface(player_obj, 'org.freedesktop.MediaPlayer') + + # Set the initial status + self._get_elapsed() + self.check_status() + + # Start polling for status, position and track changes + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + session_bus.add_signal_receiver(self.track_changed_handler, dbus_interface = "org.freedesktop.MediaPlayer", signal_name = "TrackChange") + + def next_track(self): + self.player.Next() + + def prev_track(self): + self.player.Prev() + + def play_pause_track(self): + status = self.player.GetStatus() + if status[0] == 0: + self.player.Pause() + else: + self.player.Play() + + def stop_track(self): + self.player.Stop() + + def update_track(self): + self._get_elapsed() + self.playback_started = time.time() + self.check_status() + if self.status == "Playing": + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + else: + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 5.0, self.update_track) + + def on_stop(self): + if self.timer != None: + self.timer.cancel() + self.session_bus.remove_signal_receiver(self.track_changed_handler, dbus_interface = "org.freedesktop.MediaPlayer", signal_name = "TrackChange") + + def track_changed_handler(self, detail): + g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "LoadTrackDetails", 1.0, self.load_and_draw) + + def load_and_draw(self): + self.load_song_details() + self.screen.redraw() + + def get_volume(self): + return 50 + + def get_new_status(self): + logger.debug("Getting status") + status = self.player.GetStatus() + if status[0] == 0: + return "Playing" + elif status[0] == 1: + return "Paused" + else: + return "Stopped" + + def load_song_details(self): + meta_data = self.player.GetMetadata() + + # Format properties that need formatting + bitrate = g15pythonlang.value_or_default(meta_data,"audio-bitrate", 0) + if str(bitrate) == "0": + bitrate = "" + self.playing_uri = g15pythonlang.value_or_blank(meta_data,"location") + self.duration = g15pythonlang.value_or_default(meta_data,"time", 0) + if self.duration == 0: + self.duration = g15pythonlang.value_or_default(meta_data,"mtime", 0) / 1000 + + # General properties + self.song_properties = { + "status": self.status, + "uri": self.playing_uri, + "art_uri": g15pythonlang.value_or_blank(meta_data,"arturl"), + "title": g15pythonlang.value_or_blank(meta_data,"title"), + "genre": g15pythonlang.value_or_blank(meta_data,"genre"), + "track_no": g15pythonlang.value_or_blank(meta_data,"tracknumber"), + "artist": g15pythonlang.value_or_blank(meta_data,"artist"), + "album": g15pythonlang.value_or_blank(meta_data,"album"), + "bitrate": bitrate, + "rating": g15pythonlang.value_or_default(meta_data,"rating", 0.0), + "album_artist": g15pythonlang.value_or_blank(meta_data,"mb album artist"), + } + + self.process_properties() + + def get_progress(self): + return float(self.player.PositionGet()) / 1000.0 + + def _get_elapsed(self): + self.start_elapsed = float(self.player.PositionGet()) / float(1000) + + +class MPRIS2Player(AbstractMPRISPlayer): + + def __init__(self, gconf_client, screen, players, bus_name, session_bus, theme): + self.last_properties = None + self.tracks = [] + + # Connect to DBUS + player_obj = session_bus.get_object(bus_name, '/org/mpris/MediaPlayer2') + self.player = dbus.Interface(player_obj, 'org.mpris.MediaPlayer2.Player') + self.player_properties = dbus.Interface(player_obj, 'org.freedesktop.DBus.Properties') + try: + identity = self.player_properties.Get("org.mpris.MediaPlayer2", "Identity") + except DBusException as e: + logger.debug("Error getting identify of player. Using default indentify.", exc_info = e) + # Set a default identity if we cannot get players identity + Identity = "MPRIS2" + + # Connect to DBUS + self.track_list = None + self.track_list_properties = None + try: + self.track_list = dbus.Interface(player_obj, 'org.mpris.MediaPlayer2.TrackList') + self.track_list_properties = dbus.Interface(self.track_list, 'org.freedesktop.DBus.Properties') + self.load_track_list() + except ( dbus.DBusException, KeyError ) as e: + logger.debug("Cound not load track list", exc_info = e) + pass + + if self.track_list is None: + logger.info("No TrackList interface") + + # Configure the initial state + AbstractMPRISPlayer.__init__(self, gconf_client, screen, players, bus_name, session_bus, identity, theme) + + session_bus.add_signal_receiver(self.properties_changed_handler, dbus_interface = "org.freedesktop.DBus.Properties", signal_name = "PropertiesChanged") + session_bus.add_signal_receiver(self.seeked, dbus_interface = "org.mpris.MediaPlayer2.Player", signal_name = "Seeked") + + # Set the initial status + self.check_status() + + # Workarounds + self.timer = None + + # xnoise doesn't send seeked signals, so we need to refresh + if "xnoise" in bus_name: + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0, self.update_track) + + def next_track(self): + self.player.Next() + + def prev_track(self): + self.player.Previous() + + def play_pause_track(self): + self.player.PlayPause() + + def stop_track(self): + self.player.Stop() + + def load_track_list(self): + logger.info("Loading tracklist") + track_list_props = self.track_list_properties.GetAll("org.mpris.MediaPlayer2.TrackList") + self.tracks = [] + for track in track_list_props["Tracks"]: + logger.info(" Track %s", track) + + def on_stop(self): + if self.timer: + self.timer.cancel() + self.session_bus.remove_signal_receiver(self.properties_changed_handler, dbus_interface = "org.freedesktop.DBus.Properties", signal_name = "PropertiesChanged") + self.session_bus.remove_signal_receiver(self.seeked, dbus_interface = "org.mpris.MediaPlayer2.Player", signal_name = "Seeked") + + def get_new_status(self): + logger.debug("Getting status") + status = self.player_properties.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + logger.debug("Finished geting status") + return status + + def seeked(self, seek_time): + + """ + This looks like some kind of timing problem with Banshee. It + sometimes sends 0 as the seektime, and rreading it immediately + also sometimes returns 0. Introducing a short delay fixes + the problem + """ + if seek_time == 0: + logger.warning("Received no progress in seeked event, working around problem") + time.sleep(0.5) + seek_time = self.get_progress() * 1000 * 1000 + + self.start_elapsed = seek_time / 1000 / 1000 + +# self.start_elapsed = self.get_progress() + logger.info("Seek changed to %f (%d)", self.start_elapsed, seek_time) + self.playback_started = time.time() + self.recalc_progress() + self.screen.redraw() + + def properties_changed_handler(self, something, properties, list): + logger.info("Properties changed, '%s' scheduling a reload", str(properties)) + + if "PlaybackStatus" in properties: + self.set_status(properties["PlaybackStatus"]) + + # Check if the track has changed + meta = {} + if "Metadata" in properties: + meta = properties["Metadata"] + if ( "xesam:url" in meta and meta["xesam:url"] != self.playing_uri ) or \ + ( "mpris:trackid" in meta and meta["mpris:trackid"] != self.playing_track_id ) or \ + ( "xesam:title" in meta and meta["xesam:title"] != self.playing_track_id ) or \ + ( "xesam:artist" in meta and meta["xesam:artist"] != self.playing_track_id ) or \ + ( "xesam:album" in meta and meta["xesam:album"] != self.playing_track_id ): + + """ + This doesn't seem right, but it stops the hanging problem when notify-lcd is enabled and + tracks change + """ + g15scheduler.schedule("loadMeta", 1.0, self.reset_elapsed) + + if "Volume" in properties: + self.volume = int(properties["Volume"] * 100) + + if self.last_properties == None: + self.last_properties = dict(properties) + else: + for key in properties: + self.last_properties[key] = properties[key] + + if "Metadata" in self.last_properties: + """ + This doesn't seem right, but it stops the hanging problem when notify-lcd is enabled and + tracks change + """ + g15scheduler.schedule("loadMeta", 1.0, self.load_meta) + + def _load_and_redraw(self): + self.load_meta() + self.schedule_redraw() + + def load_song_details(self): + if not self.stopped: + logger.info("Getting all song properties") + properties = self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Metadata") + logger.info("Got all song properties") + self.last_properties = {"Metadata":properties} + self.load_meta() + + def load_meta(self): + logger.debug("Loading MPRIS2 meta data") + meta_data = self.last_properties["Metadata"] + + # Format properties that need formatting + bitrate = g15pythonlang.value_or_default(meta_data,"xesam:audioBitrate", 0) + if bitrate == 0: + bitrate = "" + else: + bitrate = str(bitrate / 1024) + + self.playing_uri = g15pythonlang.value_or_blank(meta_data,"xesam:url") + self.playing_track_id = g15pythonlang.value_or_blank(meta_data,"mpris:trackid") + self.playing_title = g15pythonlang.value_or_blank(meta_data,"xesam:title") + self.playing_artist = g15pythonlang.value_or_blank(meta_data,"xesam:artist") + self.playing_album = g15pythonlang.value_or_blank(meta_data,"xesam:album") + + # General properties + self.song_properties = { + "status": self.status, + "tracklist": len(self.tracks) > 0, + "uri": self.playing_uri, + "track_id": self.playing_track_id, + "title": g15pythonlang.value_or_blank(meta_data,"xesam:title"), + "art_uri": g15pythonlang.value_or_blank(meta_data,"mpris:artUrl"), + "genre": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:genre"))), + "track_no": g15pythonlang.value_or_blank(meta_data,"xesam:trackNumber"), + "artist": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:artist"))), + "album": g15pythonlang.value_or_blank(meta_data,"xesam:album"), + "bitrate": bitrate, + "rating": g15pythonlang.value_or_default(meta_data,"xesam:userRating", 0.0), + "album_artist": ",".join(list(g15pythonlang.value_or_empty(meta_data,"xesam:albumArtist"))), + } + + self.duration = g15pythonlang.value_or_default(meta_data, "mpris:length", 0) / 1000 / 1000 + self.process_properties() + + def get_volume(self): + try: + return int(self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Volume") * 100) + except DBusException as d: + logger.debug("Could not read volume from player. Setting to 100", exc_info = d) + # Nuvola doesn't support the Volume property + return 100 + + def get_progress(self): + if self.status == "Playing": + try : + # This call seems to be where it usually hangs, although not always????? + return self.player_properties.Get("org.mpris.MediaPlayer2.Player", "Position") / 1000 / 1000 + except Exception as e: + logger.debug("Could not read player position.", exc_info = e) + pass + return 0 + + def update_track(self): + logger.debug("Updating elapsed time") + self.reset_elapsed() + self.timer = g15scheduler.queue("mprisDataQueue-%s" % self.screen.device.uid, "UpdateTrackData", 1.0 if self.status == "Playing" else 5.0, self.update_track) + +class G15MPRIS(g15plugin.G15Plugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15Plugin.__init__(self, gconf_client, gconf_key, screen) + self.session_bus = None + + def activate(self): + self.players = {} + g15plugin.G15Plugin.activate(self) + if self.session_bus == None: + self.session_bus = dbus.SessionBus() + self.session_bus.call_on_disconnection(self._dbus_disconnected) + + + self.screen.key_handler.action_listeners.append(self) + self._discover() + + # Watch for players appearing and disappearing + self.session_bus.add_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def deactivate(self): + g15plugin.G15Plugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + for key in self.players.keys(): + self.players[key].stop() + g15scheduler.stop_queue("mprisDataQueue-%s" % self.screen.device.uid) + self.session_bus.remove_signal_receiver(self._name_owner_changed, + dbus_interface='org.freedesktop.DBus', + signal_name='NameOwnerChanged') + + def destroy(self): + pass + + def action_performed(self, binding): + vis_page = self.screen.get_visible_page() + + # First send to the visible player + for p in self.players.values(): + if vis_page == p.page: + return p.action_performed(binding) + + # Now send to just the first player + if len(self.players) > 0: + return self.players.values()[0].action_performed(binding) + + def _name_owner_changed(self, name, old_owner, new_owner): + logger.debug("Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if name.startswith("org.mpris.MediaPlayer2"): + logger.info("MPRIS2 Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if new_owner == "" and name in self.players: + self.players[name].stop() + elif old_owner == "" and not name in self.players: + self.players[name] = MPRIS2Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + elif name.startswith("org.mpris."): + logger.info("MPRIS1 Name owner changed for %s from %s to %s", name, old_owner, new_owner) + if new_owner == "" and name in self.players: + self.players[name].stop() + elif old_owner == "" and not name in self.players: + if not name in mpris_blacklist: + self.players[name] = MPRIS1Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + else: + logger.info("%s is a blacklisted player, ignoring", name) + + def _discover(self): + # Find new players + active_list = self.session_bus.list_names() + for name in active_list: + if not name in mpris_blacklist: + # MPRIS 2 + if not name in self.players and name.startswith("org.mpris.MediaPlayer2"): + self.players[name] = MPRIS2Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + # MPRIS 1 + elif not name in self.players and name.startswith("org.mpris."): + self.players[name] = MPRIS1Player(self.gconf_client, self.screen, self.players, name, self.session_bus, self.create_theme()) + + def _dbus_disconnected(self, connection): + logger.debug("DBUS Disconnected") + self.session_bus = None diff --git a/src/plugins/mpris/mpris.ui b/src/plugins/mpris/mpris.ui new file mode 100644 index 0000000..81d5473 --- /dev/null +++ b/src/plugins/mpris/mpris.ui @@ -0,0 +1,79 @@ + + + + + + False + 5 + Now Playing Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + Start visualisation when media is playing + True + True + False + True + + + True + True + 0 + + + + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/nm/Makefile.am b/src/plugins/nm/Makefile.am new file mode 100644 index 0000000..8964a21 --- /dev/null +++ b/src/plugins/nm/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/nm +plugin_DATA = nm.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/nm/default/Makefile.am b/src/plugins/nm/default/Makefile.am new file mode 100644 index 0000000..a6fb5ae --- /dev/null +++ b/src/plugins/nm/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/nm/default +theme_DATA = \ + default.svg \ + g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/nm/default/default.svg b/src/plugins/nm/default/default.svg new file mode 100644 index 0000000..94dec73 --- /dev/null +++ b/src/plugins/nm/default/default.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + + diff --git a/src/plugins/nm/default/g19.svg b/src/plugins/nm/default/g19.svg new file mode 100644 index 0000000..ecf80b3 --- /dev/null +++ b/src/plugins/nm/default/g19.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + diff --git a/src/plugins/nm/nm.py b/src/plugins/nm/nm.py new file mode 100644 index 0000000..bcd7be5 --- /dev/null +++ b/src/plugins/nm/nm.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15theme as g15theme +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import os + +# Plugin details - All of these must be provided +id="nm" +name="Network Manager" +description="Displays current status of your network connections." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110 ] + +def create(gconf_key, gconf_client, screen): + return G15NM(gconf_client, gconf_key, screen) + +class MenuItem(): + + def __init__(self, page): + self.page = page + self.thumbnail = None + +class G15NM(): + + def __init__(self, gconf_client, gconf_key, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self._reload_theme() + self.page = self.screen.new_page(self.paint, id=id, priority = g15screen.PRI_EXCLUSIVE) + self.screen.redraw(self.page) + + def deactivate(self): + if self.page != None: + self.screen.del_page(self.page) + self.page = None + + def destroy(self): + pass + + def paint(self, canvas): + self.theme.draw(canvas, {}) + + def _reload_theme(self): + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), self.screen) \ No newline at end of file diff --git a/src/plugins/notify-lcd/Makefile.am b/src/plugins/notify-lcd/Makefile.am new file mode 100644 index 0000000..a2b5aa7 --- /dev/null +++ b/src/plugins/notify-lcd/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/notify-lcd +plugin_DATA = notify-lcd.py \ + notify-lcd.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/notify-lcd/default/Makefile.am b/src/plugins/notify-lcd/default/Makefile.am new file mode 100644 index 0000000..637f1cc --- /dev/null +++ b/src/plugins/notify-lcd/default/Makefile.am @@ -0,0 +1,10 @@ +themedir = $(datadir)/gnome15/plugins/notify-lcd/default +theme_DATA = g19.svg \ + g19-nobody.svg \ + default.svg \ + default-nobody.svg \ + mx5500.svg \ + mx5500-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/notify-lcd/default/default-nobody.svg b/src/plugins/notify-lcd/default/default-nobody.svg new file mode 100644 index 0000000..958ce0e --- /dev/null +++ b/src/plugins/notify-lcd/default/default-nobody.svg @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + L3 ${action1} + L4 Next + ${title} + + diff --git a/src/plugins/notify-lcd/default/default.svg b/src/plugins/notify-lcd/default/default.svg new file mode 100644 index 0000000..41a2a36 --- /dev/null +++ b/src/plugins/notify-lcd/default/default.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${message} + ${title} + + + L3 ${action1} + L4 Next + + diff --git a/src/plugins/notify-lcd/default/g19-nobody.svg b/src/plugins/notify-lcd/default/g19-nobody.svg new file mode 100644 index 0000000..c45cb71 --- /dev/null +++ b/src/plugins/notify-lcd/default/g19-nobody.svg @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd/default/g19.svg b/src/plugins/notify-lcd/default/g19.svg new file mode 100644 index 0000000..fded005 --- /dev/null +++ b/src/plugins/notify-lcd/default/g19.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + ${message} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd/default/mx5500-nobody.svg b/src/plugins/notify-lcd/default/mx5500-nobody.svg new file mode 100644 index 0000000..43ccd23 --- /dev/null +++ b/src/plugins/notify-lcd/default/mx5500-nobody.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + > Next + < ${action1} + + + diff --git a/src/plugins/notify-lcd/default/mx5500.svg b/src/plugins/notify-lcd/default/mx5500.svg new file mode 100644 index 0000000..9722e3a --- /dev/null +++ b/src/plugins/notify-lcd/default/mx5500.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${message} + ${title} + + > Next + < ${action1} + + + diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po b/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po new file mode 100644 index 0000000..6054c09 --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.en_GB.po @@ -0,0 +1,74 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/notify-lcd.glade.h:1 +msgid "Notification" +msgstr "Notification" + +#: i18n/notify-lcd.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/notify-lcd.glade.h:3 +msgid "Allow actions" +msgstr "Allow actions" + +#: i18n/notify-lcd.glade.h:4 +msgid "Allow application to cancel notification" +msgstr "Allow application to cancel notification" + +#: i18n/notify-lcd.glade.h:5 +msgid "Blink keyboard backlight" +msgstr "Blink keyboard backlight" + +#: i18n/notify-lcd.glade.h:6 +msgid "Blink memory bank lights" +msgstr "Blink memory bank lights" + +#: i18n/notify-lcd.glade.h:7 +msgid "Change keyboard backlight colour" +msgstr "Change keyboard backlight colour" + +#: i18n/notify-lcd.glade.h:8 +msgid "Color" +msgstr "Color" + +#: i18n/notify-lcd.glade.h:9 +msgid "Delay" +msgstr "Delay" + +#: i18n/notify-lcd.glade.h:10 +msgid "Enable sounds" +msgstr "Enable sounds" + +#: i18n/notify-lcd.glade.h:11 +msgid "Message on desktop (as normal)" +msgstr "Message on desktop (as normal)" + +#: i18n/notify-lcd.glade.h:12 +msgid "Message on keyboard's screen" +msgstr "Message on keyboard's screen" + +#: i18n/notify-lcd.glade.h:13 +msgid "Notify Preferences" +msgstr "Notify Preferences" + +#: i18n/notify-lcd.glade.h:14 +msgid "Respect requested timeout" +msgstr "Respect requested timeout" diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.glade.h b/src/plugins/notify-lcd/i18n/notify-lcd.glade.h new file mode 100644 index 0000000..8507b6b --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.glade.h @@ -0,0 +1,14 @@ +char *s = N_("Notification"); +char *s = N_("Options"); +char *s = N_("Allow actions"); +char *s = N_("Allow application to cancel notification"); +char *s = N_("Blink keyboard backlight"); +char *s = N_("Blink memory bank lights"); +char *s = N_("Change keyboard backlight colour"); +char *s = N_("Color"); +char *s = N_("Delay"); +char *s = N_("Enable sounds"); +char *s = N_("Message on desktop (as normal)"); +char *s = N_("Message on keyboard's screen"); +char *s = N_("Notify Preferences"); +char *s = N_("Respect requested timeout"); diff --git a/src/plugins/notify-lcd/i18n/notify-lcd.pot b/src/plugins/notify-lcd/i18n/notify-lcd.pot new file mode 100644 index 0000000..f318d4b --- /dev/null +++ b/src/plugins/notify-lcd/i18n/notify-lcd.pot @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/notify-lcd.glade.h:1 +msgid "Notification" +msgstr "" + +#: i18n/notify-lcd.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/notify-lcd.glade.h:3 +msgid "Allow actions" +msgstr "" + +#: i18n/notify-lcd.glade.h:4 +msgid "Allow application to cancel notification" +msgstr "" + +#: i18n/notify-lcd.glade.h:5 +msgid "Blink keyboard backlight" +msgstr "" + +#: i18n/notify-lcd.glade.h:6 +msgid "Blink memory bank lights" +msgstr "" + +#: i18n/notify-lcd.glade.h:7 +msgid "Change keyboard backlight colour" +msgstr "" + +#: i18n/notify-lcd.glade.h:8 +msgid "Color" +msgstr "" + +#: i18n/notify-lcd.glade.h:9 +msgid "Delay" +msgstr "" + +#: i18n/notify-lcd.glade.h:10 +msgid "Enable sounds" +msgstr "" + +#: i18n/notify-lcd.glade.h:11 +msgid "Message on desktop (as normal)" +msgstr "" + +#: i18n/notify-lcd.glade.h:12 +msgid "Message on keyboard's screen" +msgstr "" + +#: i18n/notify-lcd.glade.h:13 +msgid "Notify Preferences" +msgstr "" + +#: i18n/notify-lcd.glade.h:14 +msgid "Respect requested timeout" +msgstr "" diff --git a/src/plugins/notify-lcd/notify-lcd.py b/src/plugins/notify-lcd/notify-lcd.py new file mode 100644 index 0000000..515085e --- /dev/null +++ b/src/plugins/notify-lcd/notify-lcd.py @@ -0,0 +1,645 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("notify-lcd", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.util.g15markup as g15markup +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15desktop as g15desktop +import gconf +import time +import dbus +import dbus.service +import dbus.exceptions +import os +import gtk +import gtk.gdk +from PIL import Image +import subprocess +import tempfile +import lxml.html +import Queue +import gobject + +from threading import Timer +from threading import Thread +from threading import RLock +from dbus.exceptions import NameExistsException + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="notify-lcd" +name=_("Notify") +description=_("Displays desktop notification messages on the keyboard's screen (when available), and provides \ +various other methods of notification, such as blinking the keyboard backlight, \ +blinking the M-Key lights, or changing the backlight colour. On some desktops, \ +Gnome15 can completely take over the notification service and display messages \ +on the keyboard only.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +single_instance=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.CLEAR : _("Clear all queued messages"), + g15driver.NEXT_SELECTION : _("Next message"), + g15driver.SELECT : _("Perform action (if appropriate)") + } + +IF_NAME="org.freedesktop.Notifications" +BUS_NAME="/org/freedesktop/Notifications" + +# Match string to use for passive mode +PASSIVE_MATCH_STRING="type='method_call',interface='org.freedesktop.Notifications',member='Notify'" +EAVESDROP_MATCH_STRING="eavesdrop='true',%s" % PASSIVE_MATCH_STRING + +# List of processes to try and kill so the notification DBUS server can be replaced +OTHER_NOTIFY_DAEMON_PROCESS_NAMES = [ 'notify-osd', 'notification-daemon', 'knotify4' ] + +# NotificationClosed reasons +NOTIFICATION_EXPIRED = 1 +NOTIFICATION_DISMISSED = 2 +NOTIFICATION_CLOSED = 3 +NOTIFICATION_UNDEFINED = 4 + +def create(gconf_key, gconf_client, screen): + return G15NotifyLCD(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "notify-lcd.ui")) + dialog = widget_tree.get_object("NotifyLCDDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/respect_timeout", "RespectTimeout", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_actions", "AllowActions", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_cancel", "AllowCancel", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/on_keyboard_screen", "OnKeyboardScreen", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/on_desktop", "OnDesktop", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_keyboard_backlight", "BlinkKeyboardBacklight", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_memory_bank", "BlinkMemoryBank", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/change_keyboard_backlight_color", "ChangeKeyboardBacklightColor", False, widget_tree, True) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/blink_delay", "DelayAdjustment", 500, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/enable_sounds", "EnableSounds", True, widget_tree, True) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/keyboard_backlight_color", "KeyboardBacklightColor", ( 128, 128, 128 ), widget_tree, None) + + set_available(None, widget_tree) + widget_tree.get_object("ChangeKeyboardBacklightColor").connect("toggled", set_available, widget_tree) + widget_tree.get_object("BlinkKeyboardBacklight").connect("toggled", set_available, widget_tree) + + dialog.run() + dialog.hide() + +def set_available(widget, widget_tree): + widget_tree.get_object("KeyboardBacklightColor").set_sensitive(widget_tree.get_object("ChangeKeyboardBacklightColor").get_active()) + widget_tree.get_object("BlinkDelay").set_sensitive(widget_tree.get_object("BlinkKeyboardBacklight").get_active()) + + +''' +Queued notification message +''' +class G15Message(): + + def __init__(self, id, icon, summary, body, timeout, actions, hints): + self.id = id + self.set_details(icon, summary, body, timeout, actions, hints) + self.original_body = body + self.original_summary = summary + + def set_details(self, icon, summary, body, timeout, actions, hints): + self.icon = icon + self.summary = "None" if summary == None else summary + if body != None and len(body) > 0: + try: + self.body = lxml.html.fromstring(body).text_content() + except Exception as e: + logger.debug("Could not parse body as html", exc_info = e) + self.body = body + else: + self.body = body + self.timeout = timeout +# if timeout <= 0.0: +# timeout = 10.0 + self.timeout = 10.0 + self.actions = [] + i = 0 + if actions != None: + for j in range(0, len(actions), 2): + self.actions.append((actions[j], actions[j + 1])) + self.hints = hints + self.embedded_image = None + + if "image_path" in self.hints: + self.icon = self.hints["image_path"] + + if "image_data" in self.hints: + image_struct = self.hints["image_data"] + img_width = image_struct[0] + img_height = image_struct[1] + img_stride = image_struct[2] + has_alpha = image_struct[3] + bits_per_sample = image_struct[4] + channels = image_struct[5] + buf = "" + for b in image_struct[6]: + buf += chr(b) + + try : + pixbuf = gtk.gdk.pixbuf_new_from_data(buf, gtk.gdk.COLORSPACE_RGB, has_alpha, bits_per_sample, img_width, img_height, img_stride) + fh, self.embedded_image = tempfile.mkstemp(suffix=".png",prefix="notify-lcd") + file = os.fdopen(fh) + file.close() + pixbuf.save(self.embedded_image, "png") + self.icon = None + except Exception as e: + # Sometimes the image data seems to be bad + logger.warning("Failed to decode notification image", exc_info = e) + + if self.embedded_image == None and ( self.icon == None or self.icon == "" ): + self.icon = g15icontools.get_icon_path("dialog-information", 1024) + + def close(self): + if self.embedded_image != None: + os.remove(self.embedded_image) + +''' +DBus service implementing the freedesktop notification specification +''' +class G15NotifyService(dbus.service.Object): + + def __init__(self, gconf_client, gconf_key, screen, bus_name, plugin): + dbus.service.Object.__init__(self, bus_name, BUS_NAME) + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._screen = screen + self._plugin = plugin + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return (g15globals.name, "TT", g15globals.version, "1.1") + + @dbus.service.method(IF_NAME, in_signature='', out_signature='as') + def GetCapabilities(self): + logger.debug("Getting capabilities") + caps = [ "body", "body-images", "icon-static" ] + if self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + caps.append("actions") + if self._plugin._get_enable_sounds(): + caps.append("sounds") + + logger.debug("Got capabilities %s", str(caps)) + return caps + + @dbus.service.method(IF_NAME, in_signature='susssasa{sv}i', out_signature='u') + def Notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + return self._plugin.notify(app_name, id, icon, summary, body, actions, hints, timeout) + + @dbus.service.method(IF_NAME, in_signature='u', out_signature='') + def CloseNotification(self, id): + logger.info("Close notification %d", id) + self._plugin.close_notification(id) + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='us') + + def ActionInvoked(self, id, action_key): + logger.debug("Sending ActionInvoked for %d, %s", id, action_key) + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='uu') + def NotificationClosed(self, id, reason): + logger.debug("Sending NotificationClosed for %d, %s", id, reason) + +''' +Gnome15 notification plugin +''' +class G15NotifyLCD(): + + def __init__(self, gconf_client,gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._session_bus = dbus.SessionBus() + self._gconf_client = gconf_client + self._lock = RLock() + self.id = 1 + + def _load_configuration(self): + self.respect_timeout = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/respect_timeout", False) + self.allow_actions = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/allow_actions", False) + self.allow_cancel = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/allow_cancel", True) + self.on_keyboard_screen = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/on_keyboard_screen", True) + self.on_desktop = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/on_desktop", True) + self.blink_keyboard_backlight = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/blink_keyboard_backlight", True) + self.blink_memory_bank = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/blink_memory_bank", True) + self.change_keyboard_backlight_color = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/change_keyboard_backlight_color", False) + self.enable_sounds = g15gconf.get_bool_or_default(self._gconf_client, self._gconf_key + "/enable_sounds", True) + self.blink_delay = g15gconf.get_int_or_default(self._gconf_client, self._gconf_key + "/blink_delay", 500) + self.keyboard_backlight_color = g15gconf.get_rgb_or_default(self._gconf_client, self._gconf_key + "/keyboard_backlight_color", ( 128, 128, 128 )) + + def _get_busname(self, retry=False): + try: + return dbus.service.BusName(IF_NAME, + bus=self._bus, + replace_existing=True, + allow_replacement=True, + do_not_queue=True) + except NameExistsException as E: # name is taken. Look up who owns it + if not retry: return None + try: + proxy = self._bus.get_object('org.freedesktop.DBus', + '/org/freedesktop/DBus') + creds = proxy.GetConnectionCredentials(IF_NAME) + if 'ProcessID' in creds: + pid = creds['ProcessID'] + with open("/proc/%s/cmdline" % (pid,)) as F: + pn = F.read().split('\x00')[0] + if pn in OTHER_NOTIFY_DAEMON_PROCESS_NAMES: + process = subprocess.Popen(['killall', '--quiet', pn]) + if process.wait(): + logger.debug("Process still exists, Waiting one more second") + time.sleep(1.0) + return self._get_busname(retry=False) + else: + logger.debug("BusName is owned by unfamiliar process %s", pn) + except dbus.DBusException as E: + logger.debug("Failed to determine process owning %s", + IF_NAME, exc_info=E) + except IOError as E: + logger.debug("Error trying to retrieve notify daemon process name", + exc_info=E) + except OSError as E: + logger.debug("Error while trying to kill notify daemon", exc_info=E) + return None + + def activate(self): + self._last_variant = None + self._displayed_notification = 0 + self._active = True + self._timer = None + self._redraw_timer = None + self._blink_thread = None + self._control_values = [] + self._message_queue = [] + self._message_map = {} + self._current_message = None + self._service = None + self._load_configuration() + self._notify_handle = None + self._page = None + + # DBUS session instance must be private or monitoring will not work properly + self._bus = dbus.SessionBus(private=True) + + if not self.on_desktop: + # Already running + self._bus_name = self._get_busname() + try: + if self._bus_name: + self._service = G15NotifyService(self._gconf_client, + self._gconf_key, + self._screen, + self._bus_name, + self) + else: + logger.warning("Couldn't obtain BusName. Falling back to snooping.") + except KeyError as e: + logger.error("DBUS notify service failed to start. May already be started.", + exc_info = e) +# + if not self._service: + # Just monitor raw DBUS events + self._match_string = EAVESDROP_MATCH_STRING + try: + self._bus.add_match_string(self._match_string) + logger.info("Using eavesdrop for monitoring DBUS") + except Exception as e: + self._match_string = PASSIVE_MATCH_STRING + self._bus.add_match_string(self._match_string) + logger.info("Not using eavesdrop for monitoring DBUS", exc_info = e) + self._bus.add_message_filter(self.msg_cb) + + + self._screen.key_handler.action_listeners.append(self) + self._notify_handle = self._gconf_client.notify_add(self._gconf_key, self._configuration_changed) + + def msg_cb(self, bus, msg): + # Only interested in method calls + if isinstance(msg, dbus.lowlevel.MethodCallMessage): + if msg.get_member() == "Notify": + self.notify(*msg.get_args_list()) + + def deactivate(self): + # TODO How do we properly 'unexport' a service? This seems to kind of work, in + # that notify-osd can take over again, but trying to re-activate the plugin + # doesn't reclaim the bus name (I think because it is cached) + self.clear() + if self._notify_handle: + self._gconf_client.notify_remove(self._notify_handle) + self._screen.key_handler.action_listeners.remove(self) + if self._service: + if not self._screen.service.shutting_down: + logger.warning("Deactivated notify service. Currently the service cannot be reactivated once deactivated. You must completely restart Gnome15") + self._service.active = False + self._service.remove_from_connection() + self._bus_name.__del__() + del self._bus_name + else: + # Stop monitoring DBUS + self._bus.remove_match_string(self._match_string) + self._bus.remove_message_filter(self.msg_cb) + + def destroy(self): + pass + + def action_performed(self, binding): + if self._page != None and self._page.is_visible(): + if binding.action == g15driver.CLEAR: + self.clear() + elif binding.action == g15driver.NEXT_PAGE: + self.next() + elif binding.action == g15driver.SELECT: + self.action() + + def notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + logger.debug("Notify app=%s id=%s '%s' {%s}", app_name, id, summary, hints) + try : + if self._active: + timeout = float(timeout) / 1000.0 + if not self.respect_timeout: + timeout = 10.0 + if not self._service or not self.allow_actions: + actions = None + + # Check if this notification should be ignored, currently we ignore + # volume change notifications + # TODO should implement volume style notifications properly and deprecate alsa monitor + if "x-canonical-private-synchronous" in hints \ + and ( hints["x-canonical-private-synchronous"] == "volume" or \ + hints["x-canonical-private-synchronous"] == "indicator-sound" ): + return + + # Strip markup + if body: + body = g15markup.strip_tags(body) + if summary: + summary = g15markup.strip_tags(summary) + + if id != 0 and not id in self._message_map: + if len(self._message_queue) > 0: + new_id = self._message_queue[0].id + logger.warning("Got request to replace message %d, " \ + "but we do not know about it. " \ + "Just replacing visible message %d", id, new_id) + id = new_id + else: + id = 0 + + # If a message with this ID is already queued, replace it's details + if id == 0: + # Queue a new message + logger.debug("Queuing new message") + id = self.id + message = G15Message(self.id, icon, summary, body, timeout, actions, hints) + self._message_queue.append(message) + self._message_map[self.id] = message + self.id += 1 + + if len(self._message_queue) == 1: + self._notify() + else: + logger.debug("More than one message in queue, just redrawing") + if self._page != None: + self._screen.redraw(self._page) + else: + if id in self._message_map: + logger.debug("Message %s is already in queue, replacing its details", + str(id)) + message = self._message_map[id] + message.set_details(icon, summary, body, timeout, actions, hints) + + # If this message is the visible one, then reset the timer + if message == self._message_queue[0]: + logger.debug("It is the visible message") + self._start_timer(message) + else: + if self._page != None: + self._screen.redraw(self._page) + + logger.info("Notify message has ID of %s", str(id)) + return id + except Exception as blah: + logger.warning("Could not create notification", exc_info = blah) + + def close_notification(self, id): + logger.info("Closing notification %d. Message queue has %d items, allow cancel is %s", + id, + len(self._message_queue), + str(self.allow_cancel)) + self._lock.acquire() + try : + if self.allow_cancel and len(self._message_queue) > 0: + message = self._message_queue[0] + if message.id == id: + self._cancel_timer() + self._move_to_next(NOTIFICATION_CLOSED) + else: + del self._message_map[id] + for m in self._message_queue: + if m.id == id: + self._message_queue.remove(m) + if self._service: + gobject.idle_add(self._service.NotificationClosed, id, NOTIFICATION_CLOSED) + break + finally : + self._lock.release() + + def clear(self): + self._lock.acquire() + try : + for message in self._message_queue: + message.close() + self._message_queue = [] + self._message_map = {} + self._cancel_timer() + if self._page != None: + self._screen.del_page(self._page) + finally: + self._lock.release() + + def next(self): + logger.debug("User is selected next") + self._cancel_timer() + self._move_to_next() + + def action(self): + self._cancel_timer() + if len(self._message_queue) > 0: + message = self._message_queue[0] + if len(message.actions) > 0: + action = message.actions[0] + if self._service: + logger.debug("Action invoked") + self._service.ActionInvoked(message.id, action[0]) + self._move_to_next() + + ''' + Private + ''' + def _configuration_changed(self, client, connection_id, entry, args): + self._load_configuration() + + def _get_theme_properties(self): + width_available = self._screen.width + properties = {} + properties["title"] = self._current_message.summary + properties["message"] = self._current_message.body + if self._current_message.icon != None and len(self._current_message.icon) > 0: + icon_path = g15icontools.get_icon_path(self._current_message.icon) + + # Workaround on Natty missing new email notification icon (from Evolution)? + if icon_path == None and self._current_message.icon == "notification-message-email": + icon_path = g15icontools.get_icon_path([ "applications-email-pane", "mail_new", "mail-inbox", "mail-folder-inbox", "evolution-mail" ]) + + properties["icon"] = icon_path + elif self._current_message.embedded_image != None: + properties["icon"] = self._current_message.embedded_image + if not "icon" in properties or properties["icon"] == None: + properties["icon"] = g15icontools.get_icon_path(["dialog-info", "stock_dialog-info", "messagebox_info" ]) + + properties["next"] = len(self._message_queue) > 1 + action = 1 + for a in self._current_message.actions: + properties["action%d" % action] = a[1] + action += 1 + if len(self._current_message.actions) > 0: + properties["action"] = True + + time_displayed = time.time() - self._displayed_notification + remaining = self._current_message.timeout - time_displayed + remaining_pc = ( remaining / self._current_message.timeout ) * 100.0 + properties["remaining"] = int(remaining_pc) + return properties + + def _page_deleted(self): + self._page = None + + def _notify(self): + if len(self._message_queue) != 0: + logger.debug("Displaying first message in queue of %d", len(self._message_queue)) + message = self._message_queue[0] + + + # Which theme variant should we use + self._last_variant = "" + if message.body == None or message.body == "": + self._last_variant = "nobody" + + self._current_message = message + + # Get the page + + if self._page == None: + logger.debug("Creating new notification message page") + self._control_values = [] + for c in self._screen.driver.get_controls(): + if c.hint & g15driver.HINT_DIMMABLE != 0: + self._control_values.append(c.value) + + if self._screen.driver.get_bpp() != 0: + logger.debug("Creating notification message page") + self._page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_HIGH, title = name, \ + theme_properties_callback = self._get_theme_properties, \ + theme = g15theme.G15Theme(self, self._last_variant), + originating_plugin = self) + self._page.on_deleted = self._page_deleted + self._screen.add_page(self._page) + else: + logger.debug("Raising notification message page") + self._page.set_theme(g15theme.G15Theme(self, self._last_variant)) + self._screen.raise_page(self._page) + + self._start_timer(message) + self._do_redraw() + + # Play sound + if self.enable_sounds and "sound-file" in message.hints and ( not "suppress-sound" in message.hints or not message.hints["suppress-sound"]): + logger.debug("Will play sound",message.hints["sound-file"]) + os.system("aplay '%s' &" % message.hints["sound-file"]) + + control = self._screen.driver.get_control_for_hint(g15driver.HINT_DIMMABLE) + if control and self.blink_keyboard_backlight: + acquired_control = self._screen.driver.acquire_control(control, release_after = 3.0, val = self.keyboard_backlight_color if self.change_keyboard_backlight_color else control.value) + acquired_control.blink(delay = self.blink_delay / 1000.0) + elif control and self.change_keyboard_backlight_color: + acquired_control = self._screen.driver.acquire_control(control, release_after = 3.0, val = self.keyboard_backlight_color) + + if self.blink_memory_bank: + acquired_control = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS, release_after = 3.0, val = g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3 | g15driver.MKEY_LIGHT_MR) + acquired_control.blink(delay = self.blink_delay / 1000.0) + + def _do_redraw(self): + if self._page != None: + self._screen.redraw(self._page) + self._redraw_timer = g15scheduler.schedule("Notification", self._screen.service.animation_delay, self._do_redraw) + + def _cancel_redraw(self): + if self._redraw_timer != None: + self._redraw_timer.cancel() + + def _cancel_timer(self): + if self._timer != None: + self._timer.cancel() + + def _move_to_next(self, reason = NOTIFICATION_DISMISSED): + logger.debug("Dismissing current message. Reason code %d", reason) + self._lock.acquire() + try : + if len(self._message_queue) > 0: + message = self._message_queue[0] + message.close() + del self._message_queue[0] + del self._message_map[message.id] + if self._service: + self._service.NotificationClosed(message.id, reason) + if len(self._message_queue) != 0: + self._notify() + else: + self._screen.del_page(self._page) + self._page = None + finally: + self._lock.release() + + def _hide_notification(self): + logger.debug("Hiding notification") + self._move_to_next(NOTIFICATION_EXPIRED) + + def _start_timer(self, message): + logger.debug("Starting hide timeout") + self._cancel_timer() + self._displayed_notification = time.time() + self._timer = g15scheduler.schedule("Notification", message.timeout, self._hide_notification) + diff --git a/src/plugins/notify-lcd/notify-lcd.ui b/src/plugins/notify-lcd/notify-lcd.ui new file mode 100644 index 0000000..63ea59e --- /dev/null +++ b/src/plugins/notify-lcd/notify-lcd.ui @@ -0,0 +1,352 @@ + + + + + + 5000 + 10 + 100 + 100 + + + 320 + False + 5 + Notify Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Respect requested timeout + True + True + False + True + + + True + True + 0 + + + + + Allow actions + True + True + False + True + + + True + True + 1 + + + + + Allow application to cancel notification + True + True + False + True + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + Message on desktop (as normal) + True + True + False + True + + + True + True + 0 + + + + + Message on keyboard's screen + True + True + False + True + + + True + True + 1 + + + + + Blink memory bank lights + True + True + False + True + + + True + True + 2 + + + + + Blink keyboard backlight + True + True + False + True + + + True + True + 3 + + + + + True + False + + + True + False + Delay + + + False + False + 32 + 0 + + + + + True + True + DelayAdjustment + 0 + right + + + True + True + 1 + + + + + True + True + 4 + + + + + Change keyboard backlight colour + True + True + False + True + + + True + True + 5 + + + + + True + False + + + True + False + Color + + + False + False + 32 + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 6 + + + + + Enable sounds + True + True + False + True + + + True + True + 7 + + + + + + + + + True + False + <b>Notification</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + + diff --git a/src/plugins/notify-lcd2/Makefile.am b/src/plugins/notify-lcd2/Makefile.am new file mode 100644 index 0000000..1ef8931 --- /dev/null +++ b/src/plugins/notify-lcd2/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/notify-lcd2 +plugin_DATA = notify-lcd2.py \ + notify-lcd.ui + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/notify-lcd2/default/Makefile.am b/src/plugins/notify-lcd2/default/Makefile.am new file mode 100644 index 0000000..853f1f2 --- /dev/null +++ b/src/plugins/notify-lcd2/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/notify-lcd2/default +theme_DATA = g19.svg \ + g19-nobody.svg \ + default.svg \ + default-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/notify-lcd2/default/default-nobody.svg b/src/plugins/notify-lcd2/default/default-nobody.svg new file mode 100644 index 0000000..d953213 --- /dev/null +++ b/src/plugins/notify-lcd2/default/default-nobody.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + L2 Clear + L3 ${action1} + L4 Next + ${title} + + diff --git a/src/plugins/notify-lcd2/default/default.svg b/src/plugins/notify-lcd2/default/default.svg new file mode 100644 index 0000000..3ed2af0 --- /dev/null +++ b/src/plugins/notify-lcd2/default/default.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${body} + ${title} + + + L2 Clear + L3 ${action1} + L4 Next + + diff --git a/src/plugins/notify-lcd2/default/g19-nobody.svg b/src/plugins/notify-lcd2/default/g19-nobody.svg new file mode 100644 index 0000000..34644b7 --- /dev/null +++ b/src/plugins/notify-lcd2/default/g19-nobody.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd2/default/g19.svg b/src/plugins/notify-lcd2/default/g19.svg new file mode 100644 index 0000000..758287c --- /dev/null +++ b/src/plugins/notify-lcd2/default/g19.svg @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + + + ${message} + + + + Next + + + + + Clear + + + + ${action1} + Ok + + + + + diff --git a/src/plugins/notify-lcd2/notify-lcd.ui b/src/plugins/notify-lcd2/notify-lcd.ui new file mode 100644 index 0000000..aad14ef --- /dev/null +++ b/src/plugins/notify-lcd2/notify-lcd.ui @@ -0,0 +1,331 @@ + + + + + + 5000 + 10 + 100 + 100 + + + 320 + False + 5 + Notify Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 8 + + + True + False + 0 + none + + + True + False + 8 + 12 + + + True + False + 4 + + + Respect requested timeout + True + True + False + True + + + True + True + 0 + + + + + Allow actions + True + True + False + True + + + True + True + 1 + + + + + Allow application to cancel notification + True + True + False + True + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + On screen notification only + True + True + False + True + True + + + True + True + 4 + 0 + + + + + Blink Keyboard + True + True + False + True + True + LCDOnly + + + True + True + 4 + 1 + + + + + True + False + + + True + False + Delay + + + False + False + 32 + 0 + + + + + True + True + DelayAdjustment + 0 + right + + + True + True + 1 + + + + + True + True + 2 + + + + + Change keyboard color + True + True + False + True + True + LCDOnly + + + True + True + 3 + + + + + True + False + + + True + False + Color + + + False + False + 32 + 0 + + + + + True + True + True + #000000000000 + + + True + True + 1 + + + + + True + True + 4 + + + + + Enable sounds + True + True + False + True + + + True + True + 5 + + + + + + + + + True + False + <b>Notification</b> + True + + + + + True + True + 1 + + + + + True + True + 1 + + + + + + button9 + + + + + + + + + + + + + diff --git a/src/plugins/notify-lcd2/notify-lcd2.py b/src/plugins/notify-lcd2/notify-lcd2.py new file mode 100644 index 0000000..99e35ed --- /dev/null +++ b/src/plugins/notify-lcd2/notify-lcd2.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import time +import dbus +import dbus.service +import dbus.exceptions +import gtk +import gtk.gdk +from PIL import Image +import subprocess +import tempfile +import lxml.html +import Queue +import gconf + +from threading import Timer +from threading import Thread +from threading import RLock +from dbus.exceptions import NameExistsException + +# run it in a gtk window +if __name__ == "__main__": + + import gobject + from dbus.mainloop.glib import DBusGMainLoop + from dbus.mainloop.glib import threads_init + + gobject.threads_init() + dbus.mainloop.glib.threads_init() + DBusGMainLoop(set_as_default=True) + loop = gobject.MainLoop() + + # Allow running from local path + path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..", "..") + if os.path.exists(path): + print "Adding",path,"to python path" + sys.path.insert(0, path) + +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15globals as g15globals +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver + +# Logging +import gnome15.g15logging as g15logging +if __name__ == "__main__": + logger = g15logging.get_root_logger() +else: + logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="notify-lcd2" +name="Notify 2" +description="Take over as the Notification daemon and display messages on the LCD" +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110 ] +fork=True + +IF_NAME="org.freedesktop.Notifications" +BUS_NAME="/org/freedesktop/Notifications" + +# List of processes to try and kill so the notification DBUS server can be replaced +OTHER_NOTIFY_DAEMON_PROCESS_NAMES = [ 'notify-osd', 'notification-daemon', 'knotify4' ] + +# NotificationClosed reasons +NOTIFICATION_EXPIRED = 1 +NOTIFICATION_DISMISSED = 2 +NOTIFICATION_CLOSED = 3 +NOTIFICATION_UNDEFINED = 4 + +def create(gconf_key, gconf_client, screen): + dbus = dbus.SessionBus() + return G15NotifyLCD(gconf_client, gconf_key, screen, screen.driver ) + +def show_preferences(parent, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "notify-lcd.ui")) + dialog = widget_tree.get_object("NotifyLCDDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/respect_timeout", "RespectTimeout", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_actions", "AllowActions", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/allow_cancel", "AllowCancel", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/enable_sounds", "EnableSounds", True, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/lcd_only", "LCDOnly", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/blink_keyboard", "BlinkKeyboard", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/change_keyboard_color", "ChangeKeyboardColor", False, widget_tree, True) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/blink_delay", "DelayAdjustment", 500, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/color", "Color", ( 128, 128, 128 ), widget_tree, None) + + set_available(None, widget_tree) + widget_tree.get_object("ChangeKeyboardColor").connect("toggled", set_available, widget_tree) + widget_tree.get_object("BlinkKeyboard").connect("toggled", set_available, widget_tree) + + dialog.run() + dialog.hide() + +def set_available(widget, widget_tree): + widget_tree.get_object("Color").set_sensitive(widget_tree.get_object("ChangeKeyboardColor").get_active()) + widget_tree.get_object("Delay").set_sensitive(widget_tree.get_object("BlinkKeyboard").get_active()) + + +''' +Queued notification message +''' +class G15Message(): + + def __init__(self, id, icon, summary, body, timeout, actions, hints): + self.id = id + self.set_details(icon, summary, body, timeout, actions, hints) + + def set_details(self, icon, summary, body, timeout, actions, hints): + self.icon = icon + self.summary = "None" if summary == None else summary + if body != None and len(body) > 0: + self.body = lxml.html.fromstring(body).text_content() + else: + self.body = body + self.timeout = timeout +# if timeout <= 0.0: +# timeout = 10.0 + self.timeout = 10.0 + self.actions = [] + i = 0 + if actions != None: + for j in range(0, len(actions), 2): + self.actions.append((actions[j], actions[j + 1])) + self.hints = hints + self.embedded_image = None + + if self.icon == None or self.icon == "": + if "image_data" in self.hints: + image_struct = self.hints["image_data"] + img_width = image_struct[0] + img_height = image_struct[1] + img_stride = image_struct[2] + has_alpha = image_struct[3] + bits_per_sample = image_struct[4] + channels = image_struct[5] + buf = "" + for b in image_struct[6]: + buf += chr(b) + pixbuf = gtk.gdk.pixbuf_new_from_data(buf, gtk.gdk.COLORSPACE_RGB, has_alpha, bits_per_sample, img_width, img_height, img_stride) + fh, self.embedded_image = tempfile.mkstemp(suffix=".png",prefix="notify-lcd") + file = os.fdopen(fh) + file.close() + pixbuf.save(self.embedded_image, "png") + else: + self.icon = g15icontools.get_icon_path("dialog-info", 1024) + + def close(self): + if self.embedded_image != None: + os.remove(self.embedded_image) + +''' +DBus service implementing the freedesktop notification specification +''' +class G15NotifyService(dbus.service.Object): + + def __init__(self, gconf_client, gconf_key, service, driver, bus): + self.id = 1 + self._gconf_client = gconf_client + self._driver = driver + self._bus = bus + self._gconf_key = gconf_key + self._displayed_notification = 0 + self._active = True + self._service = service + self._timer = None + self._redraw_timer = None + self._blink_thread = None + self._control_values = [] + self._message_queue = [] + self._message_map = {} + self._current_message = None + self._page = None + + @dbus.service.method(IF_NAME, in_signature='', out_signature='ssss') + def GetServerInformation(self): + return (g15globals.name, "TT", g15globals.version, "1.1") + + @dbus.service.method(IF_NAME, in_signature='', out_signature='as') + def GetCapabilities(self): + caps = [ "body", "body-images", "icon-static" ] + if self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + caps.append("actions") + enable_sounds = self._gconf_client.get(self._gconf_key + "/enable_sounds") + if self._get_enable_sounds(): + caps.append("sounds") + + @dbus.service.method(IF_NAME, in_signature='susssasa{sv}i', out_signature='u') + def Notify(self, app_name, id, icon, summary, body, actions, hints, timeout): + try : + if self._active: + timeout = float(timeout) / 1000.0 + if not self._gconf_client.get_bool(self._gconf_key + "/respect_timeout"): + timeout = 10.0 + if not self._gconf_client.get_bool(self._gconf_key + "/allow_actions"): + actions = None + + # If a message with this ID is already queued, replace it's details + if id in self._message_map: + message = self._message_map[id] + message.set_details(icon, summary, body, timeout, actions, hints) + + # If this message is the visible one, then reset the timer + if message == self._message_queue[0]: + self._start_timer(message) + else: + self._do_draw() + else: + # Otherwise queue a new message + message = G15Message(self.id, icon, summary, body, timeout, actions, hints) + self._message_queue.append(message) + self._message_map[self.id] = message + self.id += 1 + + if len(self._message_queue) == 1: + try : + self._notify() + except Exception as blah: + logger.warning("Could not notify", exc_info = blah) + else: + self._do_draw() + + return message.id + except Exception as blah: + logger.warning("Could not notify", exc_info = blah) + + @dbus.service.method(IF_NAME, in_signature='u', out_signature='') + def CloseNotification(self, id): + if self._gconf_client.get_bool(self._gconf_key + "/allow_cancel") and len(self._message_queue) > 0: + message = self._message_queue[0] + if message.id == id: + self._cancel_timer() + self._move_to_next(NOTIFICATION_CLOSED) + else: + del self._message_map[id] + for m in self._message_queue: + if m.id == id: + self._message_queue.remove(m) + self.NotificationClosed(id, NOTIFICATION_CLOSED) + break + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='us') + def ActionInvoked(self, id, action_key): + pass + + @dbus.service.signal(dbus_interface=IF_NAME, + signature='uu') + def NotificationClosed(self, id, reason): + pass + + def clear(self): + self._driver.CancelBlink() + for message in self._message_queue: + message.close() + self._message_queue = [] + self._message_map = {} + self._cancel_timer() + page = self._screen.get_page("NotifyLCD") + if page != None: + self._screen.del_page(page) + + def next(self): + self._cancel_timer() + self._move_to_next() + + def action(self): + self._cancel_timer() + message = self._message_queue[0] + action = message.actions[0] + self.ActionInvoked(message.id, action[0]) + self._move_to_next() + + ''' + Private + ''' + def _get_enable_sounds(self): + enable_sounds = self._gconf_client.get(self._gconf_key + "/enable_sounds") + return enable_sounds == None or enable_sounds.get_bool() + + def _reload_theme(self): + self._page.LoadTheme(os.path.realpath(os.path.join(os.path.dirname(__file__), "default")), self._last_variant) + + def _get_properties(self): + properties = {} + properties["title"] = self._current_message.summary + properties["message"] = self._current_message.body + if self._current_message.icon != None and len(self._current_message.icon) > 0: + properties["icon"] = g15icontools.get_icon_path(self._current_message.icon) + elif self._current_message.embedded_image != None: + properties["icon"] = self._current_message.embedded_image + + if str(len(self._message_queue) > 1): + properties["next"] = "True" + + action = 1 + for a in self._current_message.actions: + properties["action%d" % action] = a[1] + action += 1 + if len(self._current_message.actions) > 0: + properties["action"] = "True" + + properties["remaining"] = str(self._get_remaining()) + + return properties + + def _get_remaining(self): + time_displayed = time.time() - self._displayed_notification + remaining = self._current_message.timeout - time_displayed + remaining_pc = ( remaining / self._current_message.timeout ) * 100.0 + return remaining_pc + + def _get_page(self, id): + sequence_number = self._service.GetPageSequenceNumber(id) + if sequence_number != 0: + return self._bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Page%d' % sequence_number) + + def _notify(self): + if len(self._message_queue) != 0: + message = self._message_queue[0] + + # Which theme variant should we use + self._last_variant = "" + if message.body == None or message.body == "": + self._last_variant = "nobody" + + self._current_message = message + + # Get the page + + if self._page == None: + page_sequence_number = self._service.CreatePage("NotifyLCD", "Notification Message", g15screen.PRI_HIGH) + self._page = self._bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Page%d' % page_sequence_number) + self._reload_theme() + else: + self._reload_theme() + self._page.Raise() + + self._page.SetThemeProperties(self._get_properties()) + self._start_timer(message) + self._do_redraw() + + # Play sound + if self._get_enable_sounds() and "sound-file" in message.hints and ( not "suppress-sound" in message.hints or not message.hints["suppress-sound"]): + print "WARNING: Will play sound",message.hints["sound-file"] + os.system("aplay '%s' &" % message.hints["sound-file"]) + + if self._gconf_client.get_bool(self._gconf_key + "/blink_keyboard"): + delay = gconf_client.get(gconf_key + "/blink_delay") + blink_delay = delay.get_int() if delay != None else 500 + self._driver.BlinkKeyboard(float(blink_delay) / 1000.0, 3.0, []) + if self._gconf_client.get_bool(self._gconf_key + "/change_keyboard_color"): + color = gconf_client.get_string(gconf_key + "/color") + self._driver.BlinkKeyboard(0.0, 3.0, (128, 128, 128) if color == None else g15convert.to_rgb(color)) + + def _do_redraw(self): + if self._page != None: + self._do_draw() + self._page.SetThemeProperty("remaining", str(self._get_remaining())) + self._redraw_timer = g15scheduler.schedule("Notification", 0.1, self._do_redraw) + + def _do_draw(self): + if self._page != None: + self._page.SetThemeProperties(self._get_properties()) + self._page.Redraw() + + def _cancel_redraw(self): + if self._redraw_timer != None: + self._redraw_timer.cancel() + + def _cancel_timer(self): + if self._timer != None: + self._timer.cancel() + + def _move_to_next(self, reason = NOTIFICATION_DISMISSED): + self._cancel_redraw() + message = self._message_queue[0] + message.close() + del self._message_queue[0] + del self._message_map[message.id] + self.NotificationClosed(message.id, reason) + if len(self._message_queue) != 0: + self._notify() + else: + logger.debug("Closing notification page") + self._page.Destroy() + self._page = None + + def _hide_notification(self): + self._move_to_next(NOTIFICATION_EXPIRED) + + def _start_timer(self, message): + self._cancel_timer() + self._displayed_notification = time.time() + self._timer = g15scheduler.schedule("Notification", message.timeout, self._hide_notification) + +''' +Gnome15 notification plugin +''' +class G15NotifyLCD(): + + def __init__(self, gconf_client, gconf_key, service, driver, bus): + self._service = service; + self._bus = bus + self._last_variant = None + self._driver = driver + + self._gconf_key = gconf_key + self._session_bus = dbus.SessionBus() + self._gconf_client = gconf_client + + def activate(self): + # Already running + for i in range(0, 6): + try : + for pn in OTHER_NOTIFY_DAEMON_PROCESS_NAMES: + logger.debug("Killing %s", pn) + process = subprocess.Popen(['killall', '--quiet', pn]) + process.wait() + self._bus_name = dbus.service.BusName(IF_NAME, bus=self._bus, replace_existing=True, allow_replacement=True, do_not_queue=True) + break + except NameExistsException as e: + logger.debug("Process still exists. Waiting one second and retrying to kill", + exc_info = e) + time.sleep(1.0) + if i == 2: + logger.debug("Process still exists after retry.", exc_info = e) + raise + + # Notification Service + self._notification_service = G15NotifyService(self._gconf_client, self._gconf_key, self._service, self._driver, self._bus) + try : + logger.info("Starting notification service %s", IF_NAME) + dbus.service.Object.__init__(self._notification_service, self._bus_name, BUS_NAME) + logger.info("Started notification service %s", IF_NAME) + except KeyError as ke: + logger.warning("DBUS notify service failed to start. May already be started.", + exc_info = ke) + + def deactivate(self): + # TODO How do we properly 'unexport' a service? This seems to kind of work, in + # that notify-osd can take over again, but trying to re-activate the plugin + # doesn't reclaim the bus name (I think because it is cached) + print "WARNING: Deactivated notify service. Note, currently the service cannot be reactivated once deactivated. You must completely restart Gnome15" + self._notification_service.active = False + self._notification_service.remove_from_connection() + self._bus_name.__del__() + del self._bus_name + + def destroy(self): + pass + + def handle_key(self, keys, state, post): + if not post and state == g15driver.KEY_STATE_UP: + page = self._screen.get_page("NotifyLCD") + if page != None: + if g15driver.G_KEY_BACK in keys or g15driver.G_KEY_L3 in keys: + if self._notification_service != None: + self._notification_service.clear() + return True + if g15driver.G_KEY_RIGHT in keys or g15driver.G_KEY_L4 in keys: + if self._notification_service != None: + self._notification_service.next() + return True + if g15driver.G_KEY_OK in keys or g15driver.G_KEY_L5 in keys: + if self._notification_service != None: + self._notification_service.action() + return True + +# run it in a gtk window +if __name__ == "__main__": + + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("setproctitle doesn't seem to be available", exc_info = e) + pass + + + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + bus = dbus.SessionBus() + try : + screen = bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service') + except dbus.DBusException as e: + logger.error("g15-desktop-service is not running.", exc_info = e) + sys.exit(0) + + plugin = G15NotifyLCD(gconf.client_get_default(), + "/apps/gnome15/plugins/notify-lcd2", screen, + bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Driver'), bus) + plugin.activate() + loop.run() \ No newline at end of file diff --git a/src/plugins/panel/Makefile.am b/src/plugins/panel/Makefile.am new file mode 100644 index 0000000..a0f3d78 --- /dev/null +++ b/src/plugins/panel/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/panel +plugin_DATA = panel.py \ + panel.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/panel/i18n/panel.en_GB.po b/src/plugins/panel/i18n/panel.en_GB.po new file mode 100644 index 0000000..652ee6d --- /dev/null +++ b/src/plugins/panel/i18n/panel.en_GB.po @@ -0,0 +1,54 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/panel.glade.h:1 +msgid "Bottom" +msgstr "Bottom" + +#: i18n/panel.glade.h:2 +msgid "Colour and opacity" +msgstr "Colour and opacity" + +#: i18n/panel.glade.h:3 +msgid "Left" +msgstr "Left" + +#: i18n/panel.glade.h:4 +msgid "Panel Preferences" +msgstr "Panel Preferences" + +#: i18n/panel.glade.h:5 +msgid "Position" +msgstr "Position" + +#: i18n/panel.glade.h:6 +msgid "Right" +msgstr "Right" + +#: i18n/panel.glade.h:7 +msgid "Size" +msgstr "Size" + +#: i18n/panel.glade.h:8 +msgid "Stretch screen content" +msgstr "Stretch screen content" + +#: i18n/panel.glade.h:9 +msgid "Top" +msgstr "Top" diff --git a/src/plugins/panel/i18n/panel.glade.h b/src/plugins/panel/i18n/panel.glade.h new file mode 100644 index 0000000..e8a5f85 --- /dev/null +++ b/src/plugins/panel/i18n/panel.glade.h @@ -0,0 +1,9 @@ +char *s = N_("Bottom"); +char *s = N_("Colour and opacity"); +char *s = N_("Left"); +char *s = N_("Panel Preferences"); +char *s = N_("Position"); +char *s = N_("Right"); +char *s = N_("Size"); +char *s = N_("Stretch screen content"); +char *s = N_("Top"); diff --git a/src/plugins/panel/i18n/panel.pot b/src/plugins/panel/i18n/panel.pot new file mode 100644 index 0000000..5c6b797 --- /dev/null +++ b/src/plugins/panel/i18n/panel.pot @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/panel.glade.h:1 +msgid "Bottom" +msgstr "" + +#: i18n/panel.glade.h:2 +msgid "Colour and opacity" +msgstr "" + +#: i18n/panel.glade.h:3 +msgid "Left" +msgstr "" + +#: i18n/panel.glade.h:4 +msgid "Panel Preferences" +msgstr "" + +#: i18n/panel.glade.h:5 +msgid "Position" +msgstr "" + +#: i18n/panel.glade.h:6 +msgid "Right" +msgstr "" + +#: i18n/panel.glade.h:7 +msgid "Size" +msgstr "" + +#: i18n/panel.glade.h:8 +msgid "Stretch screen content" +msgstr "" + +#: i18n/panel.glade.h:9 +msgid "Top" +msgstr "" diff --git a/src/plugins/panel/panel.py b/src/plugins/panel/panel.py new file mode 100644 index 0000000..8e1d10f --- /dev/null +++ b/src/plugins/panel/panel.py @@ -0,0 +1,220 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("panel", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import os +import gtk +import cairo + +# Plugin details - All of these must be provided +id="panel" +name=_("Panel") +description=_("Adds a small area at the bottom of the screen for other plugins to add permanent components to.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +def create(gconf_key, gconf_client, screen): + return G15Panel(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "panel.ui")) + dialog = widget_tree.get_object("PanelDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/size", "SizeAdjustment", 24, widget_tree) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/position", "PositionCombo", "bottom", widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/stretch", "Stretch", False, widget_tree) + g15uigconf.configure_colorchooser_from_gconf(gconf_client, gconf_key + "/color", "Color", ( 128, 128, 128 ), widget_tree, default_alpha = 128) + dialog.run() + dialog.hide() + +class G15PanelPainter(g15screen.Painter): + + def __init__(self, screen, gconf_client, gconf_key): + g15screen.Painter.__init__(self, g15screen.FOREGROUND_PAINTER, 1000) + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.screen = screen + + def paint(self, canvas): + panel_height = self._get_panel_size() + position = self._get_panel_position() + + # Panel is in one position on the 1 bit display + if self.screen.driver.get_bpp() == 1: + gap = 1 + inset = 1 + widget_size = panel_height + bg = None + position = "top" + align = "end" + else: + inset = 0 + align = "start" + gap = panel_height / 10.0 + bg = g15gconf.get_cairo_rgba_or_default(self.gconf_client, self.gconf_key + "/color", ( 128, 128, 128, 128 )) + widget_size = panel_height - ( gap * 2 ) + + # Paint the panel in memory first so it can be aligned easily + if position == "top" or position == "bottom": + panel_img = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.screen.width, panel_height) + else: + panel_img = cairo.ImageSurface(cairo.FORMAT_ARGB32, panel_height, self.screen.height) + panel_canvas = cairo.Context (panel_img) + self.screen.configure_canvas(panel_canvas) + + actual_size = 0 + if position == "top" or position == "bottom": + panel_canvas.translate(0, gap) + for page in self.screen.pages: + if page != self.screen.get_visible_page() and page.panel_painter != None: + if actual_size > 0: + panel_canvas.translate(inset + gap, 0) + actual_size += inset + gap + panel_canvas.save() + panel_canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 ))) + taken_up = page.panel_painter(panel_canvas, widget_size, True) + panel_canvas.restore() + if taken_up != None: + panel_canvas.translate(taken_up, 0) + actual_size += taken_up + else: + panel_canvas.translate(gap, 0) + for page in self.screen.pages: + if page != self.screen.get_visible_page() and page.panel_painter != None: + if actual_size > 0: + panel_canvas.translate(0, inset + gap) + actual_size += inset + gap + panel_canvas.save() + panel_canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 ))) + taken_up = page.panel_painter(panel_canvas, widget_size, False) + panel_canvas.restore() + if taken_up != None: + panel_canvas.translate(0, taken_up) + actual_size += taken_up + + # Position the panel + canvas.save() + + if position == "bottom": + canvas.translate(0 if align == "start" else self.screen.width - actual_size - gap, self.screen.height - panel_height) + elif position == "right": + canvas.translate(self.screen.width - panel_height, 0 if align == "start" else self.screen.height - actual_size - gap) + elif position == "top": + canvas.translate(0 if align == "start" else self.screen.width - actual_size - gap, 0) + elif position == "left": + canvas.translate(0, 0 if align == "start" else self.screen.height - actual_size - gap) + + # Paint background + if bg != None: + canvas.set_source_rgba(*bg) + if position == "top" or position == "bottom": + canvas.rectangle(0, 0, self.screen.width, panel_height) + else: + canvas.rectangle(0, 0, panel_height, self.screen.height) + canvas.fill() + + # Now actually paint the panel + canvas.set_source_surface(panel_img) + canvas.paint() + canvas.restore() + + """ + Private + """ + + def _get_panel_size(self): + # Panel is fixed size on the 1 bit display + if self.screen.driver.get_bpp() == 1: + return 8 + + panel_size = self.gconf_client.get_int(self.gconf_key + "/size") + if panel_size == 0: + panel_size = 24 + return panel_size + + def _get_panel_position(self): + panel_pos = self.gconf_client.get_string(self.gconf_key + "/position") + if panel_pos == None or panel_pos == "": + panel_pos = "bottom" + return panel_pos + + +class G15Panel(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def activate(self): + self.painter = G15PanelPainter(self.screen, self.gconf_client, self.gconf_key) + self.screen.painters.append(self.painter) + self.notify_handle = self.gconf_client.notify_add(self.gconf_key, self._config_changed); + self._set_available_screen_size() + self.screen.redraw() + + def deactivate(self): + self.screen.painters.remove(self.painter) + self.gconf_client.notify_remove(self.notify_handle); + self.screen.set_available_size((0, 0, self.screen.width, self.screen.height)) + self.screen.redraw() + + def destroy(self): + pass + + """ + Private + """ + + def _config_changed(self, client, connection_id, entry, args): + self._set_available_screen_size() + self.screen.redraw() + + def _set_available_screen_size(self): + # Scaling of any sort on the 1 bit display is a bit pointless + if self.screen.driver.get_bpp() == 1: + return + + x = 0 + y = 0 + pos = self.painter._get_panel_position() + panel_height = self.painter._get_panel_size() + stretch = self.gconf_client.get_bool(self.gconf_key + "/stretch") + + if pos == "bottom" or pos == "top": + scale = float( self.screen.height - panel_height ) / float(self.screen.height) + if not stretch: + x = ( float(self.screen.width) - float(self.screen.width * scale ) ) / 2.0 + if pos == "top": + y = panel_height + self.screen.set_available_size((x, y, self.screen.width, self.screen.height - panel_height)) + + if pos == "left" or pos == "right": + scale = float( self.screen.width - panel_height ) / float(self.screen.width) + if not stretch: + y = ( float(self.screen.height) - float(self.screen.height * scale ) ) / 2.0 + if pos == "left": + x = panel_height + self.screen.set_available_size((x, y, self.screen.width - panel_height, self.screen.height)) \ No newline at end of file diff --git a/src/plugins/panel/panel.ui b/src/plugins/panel/panel.ui new file mode 100644 index 0000000..73e79be --- /dev/null +++ b/src/plugins/panel/panel.ui @@ -0,0 +1,213 @@ + + + + + + 100 + 1 + 10 + 10 + + + + + + + + + + + top + Top + + + bottom + Bottom + + + left + Left + + + right + Right + + + + + False + 5 + Panel Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 3 + 2 + 8 + 4 + + + True + False + 0 + Colour and opacity + + + + + True + False + 0 + Size + + + 1 + 2 + + + + + True + True + + False + False + True + True + SizeAdjustment + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + False + 0 + Position + + + 2 + 3 + + + + + True + False + PositionModel + + + + 1 + + + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + True + True + True + #000000000000 + + + 1 + 2 + GTK_FILL + + + + + True + True + 0 + + + + + Stretch screen content + True + True + False + True + + + False + False + 12 + 1 + + + + + False + False + 1 + + + + + + button9 + + + + 200 + 1 + 10 + + diff --git a/src/plugins/pommodoro/Machovka_tomato.png b/src/plugins/pommodoro/Machovka_tomato.png new file mode 100644 index 0000000000000000000000000000000000000000..2526f57f3aff36026a47a1b0574915b5fee0db01 GIT binary patch literal 7776 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 literal 0 HcmV?d00001 diff --git a/src/plugins/pommodoro/Machovka_tomato_green.png b/src/plugins/pommodoro/Machovka_tomato_green.png new file mode 100644 index 0000000000000000000000000000000000000000..7a2b23e203626dce5ed241ae8faf30f3f842a00f GIT binary patch literal 6391 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 juLoXnM&~TK82JKNm@ literal 0 HcmV?d00001 diff --git a/src/plugins/pommodoro/Makefile.am b/src/plugins/pommodoro/Makefile.am new file mode 100644 index 0000000..bdd2741 --- /dev/null +++ b/src/plugins/pommodoro/Makefile.am @@ -0,0 +1,13 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/pommodoro +plugin_DATA = \ + Machovka_tomato.png \ + Machovka_tomato_green.png \ + pommodoro.py \ + pommodoro.ui \ + tomato_1bpp.png \ + tomato_empty_1bpp.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/pommodoro/default/Makefile.am b/src/plugins/pommodoro/default/Makefile.am new file mode 100644 index 0000000..1d7bc46 --- /dev/null +++ b/src/plugins/pommodoro/default/Makefile.am @@ -0,0 +1,28 @@ +themedir = $(datadir)/gnome15/plugins/pommodoro/default +theme_DATA = \ + default.svg \ + default-timerover.svg \ + g19.svg \ + g19-timerover.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/pommodoro/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/pommodoro/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/pommodoro/default/default-timerover.svg b/src/plugins/pommodoro/default/default-timerover.svg new file mode 100644 index 0000000..76da1be --- /dev/null +++ b/src/plugins/pommodoro/default/default-timerover.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${message} + ${timer} + L2 + L5 + Continue + Stop + + diff --git a/src/plugins/pommodoro/default/default.svg b/src/plugins/pommodoro/default/default.svg new file mode 100644 index 0000000..fd8fdb7 --- /dev/null +++ b/src/plugins/pommodoro/default/default.svg @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${action} + ${timer} + L2 + ${count} + L5 + Start/Stop + Done + Reset + + + + diff --git a/src/plugins/pommodoro/default/g19-timerover.svg b/src/plugins/pommodoro/default/g19-timerover.svg new file mode 100644 index 0000000..009f665 --- /dev/null +++ b/src/plugins/pommodoro/default/g19-timerover.svg @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Continue + + + OK + + Stop + ${timer} + ${message} + + + + + + + + + diff --git a/src/plugins/pommodoro/default/g19.svg b/src/plugins/pommodoro/default/g19.svg new file mode 100644 index 0000000..8e5cb77 --- /dev/null +++ b/src/plugins/pommodoro/default/g19.svg @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${timer} + Start / Stop + + + OK + + + + + + + + + Reset + ${count} + x + + + + + + + + + + ${action} + Done + + diff --git a/src/plugins/pommodoro/pommodoro.py b/src/plugins/pommodoro/pommodoro.py new file mode 100644 index 0000000..79a4398 --- /dev/null +++ b/src/plugins/pommodoro/pommodoro.py @@ -0,0 +1,525 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 Nuno Aruajo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Pommodoro timer plugin for Gnome15. +This plugin allows a user to apply the Pommodoro Technique to manage their time. +""" + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("pommodoro", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15uigconf as g15uigconf +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15scheduler as g15scheduler +import datetime +import gtk +import pango +import os +import locale + +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="pommodoro" +name=_("Pommodoro Timer") +description=_("A Pommodoro Timer.\n" \ + "The Pomodoro Technique is an " \ + "amazing way to get the most out of your work day - breaking up your time into " \ + "manageable sections lets you focus more on the task, and accomplish more!") +author="Nuno Araujo " +copyright=_("Copyright (C) 2013 Nuno Araujo") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [g15driver.MODEL_G110, + g15driver.MODEL_G11, + g15driver.MODEL_G930, + g15driver.MODEL_G35] +actions={ + g15driver.SELECT : _("Start / Stop Pommodoro"), + g15driver.CLEAR : _("Reset finished pommodoro counter"), + g15driver.VIEW : _("Cancel Pommodoro") + } +actions_g19={ + g15driver.SELECT : _("Start / Stop Pommodoro"), + g15driver.CLEAR : _("Reset finished pommodoro counter"), + g15driver.VIEW : _("Cancel Pommodoro") + } + + +DEFAULT_WORK_DURATION = 25 # [min] +DEFAULT_SHORTBREAK_DURATION = 5 # [min] +DEFAULT_LONGBREAK_DURATION = 15 # [min] + +def create(gconf_key, gconf_client, screen): + return G15PommodoroPlugin(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "pommodoro.ui")) + + dialog = widget_tree.get_object("PommodoroPreferencesDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/work_duration".format(gconf_key), + "WorkDuration", + DEFAULT_WORK_DURATION, + widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/shortbreak_duration".format(gconf_key), + "ShortBreakDuration", + DEFAULT_SHORTBREAK_DURATION, + widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, + "{0}/longbreak_duration".format(gconf_key), + "LongBreakDuration", + DEFAULT_LONGBREAK_DURATION, + widget_tree) + dialog.run() + dialog.hide() + + +class PommodoroTimer: + ''' + PommodoroTimer is a state machine with three main states: + STOPPED : No activity is taking place + RUNNING : An activity is taking place + WAITING : The time allocate to the activity is finished and the timer is waiting for a + command either to start the next activity or to stop. + + When in RUNNING state, three different activities are timed: + WORKING : The user is currently running a pommodoro + SHORT_PAUSING : The pommodoro is over and the user is taking a short break + LONG_PAUSING : The user is taking a long break each 4 finished pommodoros + The duration of these activities are configured by three parameters (work_duration, + shortbreak_duration and longbreak_duration). + + Switching to RUNNING and STOPPED states is made by calling respecively the start and stop + methods. + + Switching from the WAITING state to the RUNNING state is made by calling the go_on method. + + Switching from the RUNNING state to the WAITING state happens automatically when a activity + finishes. This is managed by a g15scheduler queue timer. + + If setup, the method assigned to on_state_change method is called each time the state of + PommodoroTimer changes. + + PommodoroTimer also counts the number of times the WORKING activity was finished in a counter. + ''' + + # States + STOPPED = 0 + RUNNING = 1 + WAITING = 2 + + #Activities + WORKING = 1 + SHORT_PAUSING = 2 + LONG_PAUSING = 3 + + NUMBER_OF_POMMODOROS_BEFORE_LONG_PAUSE = 4 + + def __init__(self): + self.work_duration = DEFAULT_WORK_DURATION + self.shortbreak_duration = DEFAULT_SHORTBREAK_DURATION + self.longbreak_duration = DEFAULT_LONGBREAK_DURATION + + self._count = 0 + + self._state = PommodoroTimer.STOPPED + self._activity = PommodoroTimer.WORKING + + self._state_change_timer = None + self._started_at = None + self._timer_value = self._minutes_to_timedelta(self.work_duration) + + self.on_state_change = None + self.on_count_change = None + + def start(self): + ''' + Start the timer + ''' + if self._state == PommodoroTimer.STOPPED: + self._state_next() + + def stop(self): + ''' + Stop the timer + ''' + if self._state in [PommodoroTimer.WAITING, PommodoroTimer.RUNNING]: + self._destroy_state_change_timer() + self._timer_value = self._minutes_to_timedelta(self.work_duration) + self._state = PommodoroTimer.STOPPED + self._activity = PommodoroTimer.WORKING + self._signal_state_change() + self._log_pommodoro_state() + + def go_on(self): + ''' + Continue the timer when in WAITING state + ''' + if self._state == PommodoroTimer.WAITING: + self._state_next() + + def init_count_at(self, value): + self._count = value + self._signal_count_change() + + def count_reset(self): + ''' + Resets the finished pommodoros counter + ''' + self._count = 0 + self._signal_count_change() + + def recalculate(self): + ''' + Recalculate the timer schedulers. + This method should be called when changes are made to any of the fields managing the + the activity durations (work_duration, shortbreak_duration or longbreak_duration) + ''' + + # Update the _timer_value to a new value depending on the activity. + if self._state in [PommodoroTimer.RUNNING, PommodoroTimer.STOPPED]: + # We don't set the new values if the timer is finished (WAITING). + if self._activity == PommodoroTimer.WORKING: + self._timer_value = self._minutes_to_timedelta(self.work_duration) + elif self._activity == PommodoroTimer.SHORT_PAUSING: + self._timer_value = self._minutes_to_timedelta(self.shortbreak_duration) + elif self._activity == PommodoroTimer.LONG_PAUSING: + self._timer_value = self._minutes_to_timedelta(self.longbreak_duration) + + # If the timer is running, reschedule the state change timer + if self._state == PommodoroTimer.RUNNING: + next_schedule = max(0, (self._timer_value - self._elapsed_time()).total_seconds()) + self._schedule_next_state(next_schedule) + logger.info("Scheduled next state change in %s", str(next_schedule)) + + @property + def state(self): + ''' + Returns the current state of the pommodoro timer + ''' + return self._state + + @property + def activity(self): + ''' + Returns the current activity + ''' + return self._activity + + @property + def timer_value(self): + ''' + Returns the current timer maximum value + ''' + return self._timer_value + + @property + def value(self): + ''' + Returns the current timer remaining time. + Note, this value can be less than 0 if the timer has elapsed. + ''' + if self._state == PommodoroTimer.STOPPED: + return self._timer_value + else: + return self._timer_value + self._started_at - datetime.datetime.now() + + @property + def started_at(self): + ''' + Returns the time at which the last activity started + ''' + return self._started_at + + @property + def count(self): + ''' + Returns the number of finished pommodoros + ''' + return self._count + + ''' + Private methods + ''' + def _elapsed_time(self): + return datetime.datetime.now() - self._started_at + + def _state_next(self): + ''' + Switch to next state within 'normal' workflow + ''' + logger.debug("Switching to next state") + if self._state == PommodoroTimer.STOPPED: + # Start pommodoro timer + self._schedule_next_state(self._timer_value.total_seconds()) + self._started_at = datetime.datetime.now() + self._state = PommodoroTimer.RUNNING + self._activity = PommodoroTimer.WORKING + + elif self._state == PommodoroTimer.RUNNING: + # Timer over, go to waiting state + self._destroy_state_change_timer() + if self._activity == PommodoroTimer.WORKING: + self._count_increase() + self._state = PommodoroTimer.WAITING + + elif self._state == PommodoroTimer.WAITING: + # User accepted to continue, cycle activity and go to RUNNING state + if self._activity == PommodoroTimer.WORKING: + # Start pause if we were working + if self._count % PommodoroTimer.NUMBER_OF_POMMODOROS_BEFORE_LONG_PAUSE == 0: + # If 4 pommodoros were completed, start long pause + self._timer_value = self._minutes_to_timedelta(self.longbreak_duration) + self._activity = PommodoroTimer.LONG_PAUSING + else: + # Start short pause + self._timer_value = self._minutes_to_timedelta(self.shortbreak_duration) + self._activity = PommodoroTimer.SHORT_PAUSING + else: + # Start working if we were in pause + self._timer_value = self._minutes_to_timedelta(self.work_duration) + self._activity = PommodoroTimer.WORKING + self._schedule_next_state(self._timer_value.total_seconds()) + self._started_at = datetime.datetime.now() + self._state = PommodoroTimer.RUNNING + self._signal_state_change() + self._log_pommodoro_state() + + def _signal_state_change(self): + if self.on_state_change is not None: + self.on_state_change() + + def _log_pommodoro_state(self): + logger.info("Switched to state {0} - {1}".format(self._state, self._activity)) + + def _schedule_next_state(self, when): + self._destroy_state_change_timer() + self._state_change_timer = g15scheduler.schedule("PommodoroTimerStateChange", + when, + self._state_next) + + def _destroy_state_change_timer(self): + if self._state_change_timer is not None: + self._state_change_timer.cancel() + self._state_change_timer = None + + def _minutes_to_timedelta(self, minutes): + return datetime.timedelta(0, 0, 0, 0, minutes) + + def _count_increase(self): + self._count += 1 + self._signal_count_change() + + def _signal_count_change(self): + if self.on_count_change is not None: + self.on_count_change() + + +class G15PommodoroPlugin(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, + gconf_client, + gconf_key, + screen, + self._get_icon_path("Machovka_tomato.png"), + id, + name) + self.waiting_image = \ + g15cairo.load_surface_from_file(self._get_icon_path("Machovka_tomato.png")) + self.running_image = \ + g15cairo.load_surface_from_file(self._get_icon_path("Machovka_tomato_green.png")) + self.waiting_image_1bpp = \ + g15cairo.load_surface_from_file(self._get_icon_path("tomato_empty_1bpp.png")) + self.running_image_1bpp = \ + g15cairo.load_surface_from_file(self._get_icon_path("tomato_1bpp.png")) + + self.pommodoro_timer = PommodoroTimer() + self.pommodoro_timer.on_state_change = self.timer_state_changed + self.pommodoro_timer.on_count_change = self.pommodoro_count_save + self._load_configuration() + + def activate(self): + self._load_configuration() + self.pommodoro_timer.stop() + g15plugin.G15RefreshingPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + self.watch([self._get_configuration_key("work_duration"), + self._get_configuration_key("shortbreak_duration"), + self._get_configuration_key("longbreak_duration")], self._config_changed) + + def deactivate(self): + self.pommodoro_timer.stop() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def action_performed(self, binding): + if not (self.page and self.page.is_visible()): + # Return if we are not displayed on screen + return + + if binding.action == g15driver.SELECT: + if self.pommodoro_timer.state == PommodoroTimer.STOPPED: + self.pommodoro_timer.start() + elif self.pommodoro_timer.state == PommodoroTimer.WAITING: + self.pommodoro_timer.go_on() + else: + self.pommodoro_timer.stop() + elif binding.action == g15driver.VIEW: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + self.pommodoro_timer.stop() + elif binding.action == g15driver.CLEAR: + self.pommodoro_timer.count_reset() + + def _paint_panel(self, canvas, allocated_size, horizontal): + # Nothing to paint if the timer is stopped + if self.pommodoro_timer.state == PommodoroTimer.STOPPED: + return + + # Nothing to paint if the page is visible + if not self.page or (self.page and self.page.is_visible()): + return + + if self.screen.driver.get_bpp() == 1: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + size = g15cairo.paint_thumbnail_image(allocated_size, + self.waiting_image_1bpp, + canvas) + elif self.pommodoro_timer.state == PommodoroTimer.RUNNING: + size = g15cairo.paint_thumbnail_image(allocated_size, + self.running_image_1bpp, + canvas) + else: + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + size = g15cairo.paint_thumbnail_image(allocated_size, self.waiting_image, canvas) + elif self.pommodoro_timer.state == PommodoroTimer.RUNNING: + size = g15cairo.paint_thumbnail_image(allocated_size, self.running_image, canvas) + + return size + + def get_theme_properties(self): + properties = { } + + properties["timer"] = self._format_timer_value_for_display() + + properties["pommodoro_timer"] = self._get_progress_in_percent() + + if self.pommodoro_timer.activity == PommodoroTimer.WORKING: + properties["action"] = "Work" + elif self.pommodoro_timer.activity == PommodoroTimer.SHORT_PAUSING: + properties["action"] = "Small break" + elif self.pommodoro_timer.activity == PommodoroTimer.LONG_PAUSING: + properties["action"] = "Long break" + + properties["count"] = str(self.pommodoro_timer.count) + + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + if self.pommodoro_timer.activity == PommodoroTimer.WORKING: + properties["message"] = "Time for a break" + else: + properties["message"] = "Break's over!" + else: + properties["message"] = "" + + return properties + + def timer_state_changed(self): + self._reload_theme() + # Raise the page for 10 seconds if a activity has just finished (state went to WAITING) + if self.pommodoro_timer.state == PommodoroTimer.WAITING \ + and self.page is not None \ + and self.page.theme is not None: + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 10.0) + + + def pommodoro_count_save(self): + pommodoro_count_conf_key = self._get_configuration_key("pommodoro_count") + self.gconf_client.set_int(pommodoro_count_conf_key, self.pommodoro_timer.count) + + def _format_timer_value_for_display(self): + total_seconds = int(self.pommodoro_timer.value.total_seconds()) + if total_seconds > 0: + return str(datetime.timedelta(0, total_seconds)) + else: + x = int((datetime.datetime.now() \ + - self.pommodoro_timer.started_at \ + - self.pommodoro_timer.timer_value).total_seconds()) + return "- {0}".format(str(datetime.timedelta(0, x))) + + def _get_progress_in_percent(self): + return 100 - int(self.pommodoro_timer.value.total_seconds() \ + / self.pommodoro_timer.timer_value.total_seconds() \ + * 100) + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _load_configuration(self): + work_duration_conf_key = self._get_configuration_key("work_duration") + self.pommodoro_timer.work_duration = g15gconf.get_int_or_default(self.gconf_client, + work_duration_conf_key, + DEFAULT_WORK_DURATION) + + shortbreak_conf_key = self._get_configuration_key("shortbreak_duration") + self.pommodoro_timer.shortbreak_duration = \ + g15gconf.get_int_or_default(self.gconf_client, + shortbreak_conf_key, + DEFAULT_SHORTBREAK_DURATION) + + longbreak_conf_key = self._get_configuration_key("longbreak_duration") + self.pommodoro_timer.longbreak_duration = \ + g15gconf.get_int_or_default(self.gconf_client, + longbreak_conf_key, + DEFAULT_LONGBREAK_DURATION) + + pommodoro_count_conf_key = self._get_configuration_key("pommodoro_count") + self.pommodoro_timer.init_count_at(g15gconf.get_int_or_default(self.gconf_client, + pommodoro_count_conf_key, + 0)) + self.pommodoro_timer.recalculate() + + def _get_configuration_key(self, key_name): + ''' + Returns the full gconf key name for the relative key_name passed as parameter + ''' + return "{0}/{1}".format(self.gconf_key, key_name) + + def _reload_theme(self): + variant = None + if self.pommodoro_timer.state == PommodoroTimer.WAITING: + variant = "timerover" + if self.page is not None and self.page.theme is not None: + self.page.theme.set_variant(variant) + + def _get_icon_path(self, name): + return os.path.join(os.path.dirname(__file__), name) diff --git a/src/plugins/pommodoro/pommodoro.ui b/src/plugins/pommodoro/pommodoro.ui new file mode 100644 index 0000000..345f785 --- /dev/null +++ b/src/plugins/pommodoro/pommodoro.ui @@ -0,0 +1,206 @@ + + + + + + 1 + 999999 + 15 + 1 + 10 + + + 1 + 99999 + 5 + 1 + 10 + + + 1 + 999999 + 25 + 1 + 10 + + + False + 5 + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + True + True + 0 + + + + + True + False + 3 + 3 + 4 + 2 + + + True + False + 1 + Work duration + + + + + True + False + 1 + Short break duration + + + 1 + 2 + + + + + True + False + 1 + Long break duration + + + 2 + 3 + + + + + True + False + [min] + + + 2 + 3 + + + + + True + False + [min] + + + 2 + 3 + 1 + 2 + + + + + True + False + [min] + + + 2 + 3 + 2 + 3 + + + + + True + True + + False + False + True + True + WorkDuration + True + + + 1 + 2 + + + + + True + True + + False + False + True + True + ShortBreakDuration + True + + + 1 + 2 + 1 + 2 + + + + + True + True + + False + False + True + True + LongBreakDuration + True + + + 1 + 2 + 2 + 3 + + + + + False + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/pommodoro/tomato_1bpp.png b/src/plugins/pommodoro/tomato_1bpp.png new file mode 100644 index 0000000000000000000000000000000000000000..8256c2ce853a854cbc5ca1c9e0663ef4c5cd8431 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_|Y3?y$c)pCP40X`wFK>Gjx|4VPqzYAn>7I;J! zGcYiPfiR W5=^SXjsn6!Sq4v6KbLh*2~7Z-HXu&` literal 0 HcmV?d00001 diff --git a/src/plugins/pommodoro/tomato_empty_1bpp.png b/src/plugins/pommodoro/tomato_empty_1bpp.png new file mode 100644 index 0000000000000000000000000000000000000000..da24f26566ade45ae40241c429f563f18d212f3e GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_`5Bp8k^-!KJ8aTa()7Bes~g@G`mhsUAYKtc8r zPhVH|yWHH2CY+2DjZ}d`3Z5>GAsp9rPj2L8Fc4@lxc|?2veJEpNFA|{!9F)$-eZ2V Wk?U8oU&BG5S_V&7KbLh*2~7a*bs}>B literal 0 HcmV?d00001 diff --git a/src/plugins/ppastats/Makefile.am b/src/plugins/ppastats/Makefile.am new file mode 100644 index 0000000..52e15ef --- /dev/null +++ b/src/plugins/ppastats/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/ppastats +plugin_DATA = ppastats.py \ + ppastats.ui + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/ppastats/default/Makefile.am b/src/plugins/ppastats/default/Makefile.am new file mode 100644 index 0000000..56e71cb --- /dev/null +++ b/src/plugins/ppastats/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/ppastats/default +theme_DATA = default.svg \ + default-menu-entry.svg \ + g19.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/ppastats/default/default-menu-entry.svg b/src/plugins/ppastats/default/default-menu-entry.svg new file mode 100644 index 0000000..39635fb --- /dev/null +++ b/src/plugins/ppastats/default/default-menu-entry.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + diff --git a/src/plugins/ppastats/default/default.svg b/src/plugins/ppastats/default/default.svg new file mode 100644 index 0000000..0f08afb --- /dev/null +++ b/src/plugins/ppastats/default/default.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + + + + + + + diff --git a/src/plugins/ppastats/default/g19-menu-entry.svg b/src/plugins/ppastats/default/g19-menu-entry.svg new file mode 100644 index 0000000..4e8ed9a --- /dev/null +++ b/src/plugins/ppastats/default/g19-menu-entry.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${ent_title} + ${ent_month_year} ${ent_time_24} + + diff --git a/src/plugins/ppastats/default/g19.svg b/src/plugins/ppastats/default/g19.svg new file mode 100644 index 0000000..25870dd --- /dev/null +++ b/src/plugins/ppastats/default/g19.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + ${updated} + + + + + + + + diff --git a/src/plugins/ppastats/ppastats.py b/src/plugins/ppastats/ppastats.py new file mode 100644 index 0000000..8a6cb1c --- /dev/null +++ b/src/plugins/ppastats/ppastats.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import subprocess +import time +import os +import feedparser +import gtk +import gconf +from launchpadlib.launchpad import Launchpad + + +# Plugin details - All of these must be provided +id="ppstats" +name="PPA Statistics" +description="Show download statistics and other information about your Ubuntu PPA." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110 ] + +def create(gconf_key, gconf_client, screen): + return G15PPAStats(gconf_client, gconf_key, screen) + +def show_preferences(parent, gconf_client, gconf_key): + G15PPAStatsPreferences(parent, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +def get_update_time(gconf_client, gconf_key): + val = gconf_client.get_int(gconf_key + "/update_time") + if val == 0: + val = 60 + return val + +class G15PPAStatsPreferences(): + + def __init__(self, parent, gconf_client,gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "ppastats.ui")) + + # Feeds + self.feed_model = widget_tree.get_object("PPAModel") + self.reload_model() + self.feed_list = widget_tree.get_object("PPAList") + self.url_renderer = widget_tree.get_object("URLRenderer") + + # Updates + self.update_adjustment = widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(get_update_time(gconf_client, gconf_key)) + self.update_adjustment.set_value(get_update_time(gconf_client, gconf_key)) + + # Connect to events + self.update_adjustment.connect("value-changed", self.update_time_changed) + self.url_renderer.connect("edited", self.url_edited) + widget_tree.get_object("NewPPA").connect("clicked", self.new_url) + widget_tree.get_object("RemovePPA").connect("clicked", self.remove_url) + + # Show dialog + dialog = widget_tree.get_object("PPADialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self.urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah); + + def update_time_changed(self, widget): + self.gconf_client.set_int(self.gconf_key + "/update_time", int(widget.get_value())) + + def url_edited(self, widget, row_index, value): + row = self.feed_model[row_index] + if value != "": + urls = self.gconf_client.get_list(self.gconf_key + "/urls", gconf.VALUE_STRING) + if row[0] in urls: + urls.remove(row[0]) + urls.append(value) + self.gconf_client.set_list(self.gconf_key + "/urls", gconf.VALUE_STRING, urls) + else: + self.feed_model.remove(self.feed_model.get_iter(row_index)) + + def urls_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.feed_model.clear() + for url in self.gconf_client.get_list(self.gconf_key + "/urls", gconf.VALUE_STRING): + self.feed_model.append([ url, True ]) + + def new_url(self, widget): + self.feed_model.append(["", True]) + self.feed_list.set_cursor_on_cell(str(len(self.feed_model) - 1), focus_column = self.feed_list.get_column(0), focus_cell = self.url_renderer, start_editing = True) + self.feed_list.grab_focus() + + def remove_url(self, widget): + (model, path) = self.feed_list.get_selection().get_selected() + url = model[path][0] + urls = self.gconf_client.get_list(self.gconf_key + "/projects", gconf.VALUE_STRING) + if url in urls: + urls.remove(url) + self.gconf_client.set_list(self.gconf_key + "/projects", gconf.VALUE_STRING, urls) + +class G15PPAPage(): + + def __init__(self, plugin, url): + self.launchpad = plugin.launchpad + self.gconf_client = plugin.gconf_client + self.gconf_key = plugin.gconf_key + self.screen = plugin.screen + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), self.screen) + self.url = url + self.index = -1 + self.selected_entry = None + self.reload() + self.page = self.screen.new_page(self.paint, id="PPA " + str(plugin.page_serial), thumbnail_painter = self.paint_thumbnail) + plugin.page_serial += 1 + self.screen.redraw(self.page) + self.project = self.launchpad.projects[self.url] + print self.project + + def reload(self): + self.icon = g15icontools.get_icon_path("application-rss+xml", self.screen.height ) + self.title = "PPA" + + def paint_thumbnail(self, canvas, allocated_size, horizontal): + return g15cairo.paint_thumbnail_image(allocated_size, g15cairo.load_surface_from_file(self.icon), canvas) + + def paint(self, canvas): + properties = {} + attributes = {} + properties["title"] = self.title + attributes["icon"] = self.icon + self.theme.draw(canvas, properties, attributes) + +class G15PPAStats(): + + def __init__(self, gconf_client,gconf_key, screen): + self.screen = screen; + self.gconf_key = gconf_key + self.gconf_client = gconf_client + self.page_serial = 1 + + def activate(self): + + self.cache_dir = os.path.expanduser("~/.gnome2/gnome15/ppastats") + self.launchpad = Launchpad.login_anonymously("just testing", "production", self.cache_dir) + bug_one = self.launchpad.bugs[1] + print "Bug one",bug_one.title + + self.pages = {} + self.update_time_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/update_time", self._update_time_changed) + self.ppas_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/ppas", self._ppas_changed) + self._load_ppas() + self._schedule_refresh() + + def deactivate(self): + self.gconf_client.notify_remove(self.update_time_changed_handle); + self.gconf_client.notify_remove(self.ppas_changed_handle); + for page in self.pages: + self.screen.del_page(self.pages[page].page) + self.pages = {} + + def destroy(self): + pass + + ''' + Private + ''' + + def _schedule_refresh(self): + schedule_seconds = get_update_time(self.gconf_client, self.gconf_key) * 60.0 + self.refresh_timer = g15scheduler.schedule("PPARefreshTimer", schedule_seconds, self._refresh) + + def _refresh(self): + for page_id in self.pages: + page = self.pages[page_id] + page.reload() + self.screen.redraw(page.page) + self._schedule_refresh() + + def _update_time_changed(self, client, connection_id, entry, args): + self.refresh_timer.cancel() + self._schedule_refresh() + + def _ppas_changed(self, client, connection_id, entry, args): + self._load_ppas() + + def _load_ppas(self): + ppa_list = self.gconf_client.get_list(self.gconf_key + "/ppas", gconf.VALUE_STRING) + + # Add new pages + for url in ppa_list: + if not url in self.pages: + self.pages[url] = G15PPAPage(self, url) + + # Remove pages that no longer exist + to_remove = [] + for page_url in self.pages: + page = self.pages[page_url] + if not page.url in feed_list: + self.screen.del_page(page.page) + to_remove.append(page_url) + for page in to_remove: + del self.pages[page] + diff --git a/src/plugins/ppastats/ppastats.ui b/src/plugins/ppastats/ppastats.ui new file mode 100644 index 0000000..ae11337 --- /dev/null +++ b/src/plugins/ppastats/ppastats.ui @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + 1 + 9999 + 1 + 1 + 1 + + + 320 + False + 5 + PPA Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + True + True + PPAModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>PPAS</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + Update every + + + True + True + 0 + + + + + True + True + + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/processes/Makefile.am b/src/plugins/processes/Makefile.am new file mode 100644 index 0000000..ca380e9 --- /dev/null +++ b/src/plugins/processes/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/processes +plugin_DATA = processes.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/processes/i18n/processes.en_GB.po b/src/plugins/processes/i18n/processes.en_GB.po new file mode 100644 index 0000000..66948f9 --- /dev/null +++ b/src/plugins/processes/i18n/processes.en_GB.po @@ -0,0 +1,99 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: processes.py:42 +msgid "Process List" +msgstr "Process List" + +#: processes.py:43 +msgid "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." +msgstr "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." + +#: processes.py:46 +msgid "Copyright (C)2010 Brett Smith" +msgstr "Copyright (C)2010 Brett Smith" + +#: processes.py:52 +msgid "Previous process" +msgstr "Previous process" + +#: processes.py:53 +msgid "Next process" +msgstr "Next process" + +#: processes.py:54 +msgid "Next page" +msgstr "Next page" + +#: processes.py:55 +msgid "Previous page" +msgstr "Previous page" + +#: processes.py:56 +msgid "Kill process" +msgstr "Kill process" + +#: processes.py:57 +msgid "" +"Toggle between applications,\n" +"all and user" +msgstr "" +"Toggle between applications,\n" +"all and user" + +#: processes.py:91 +msgid "Kill Process" +msgstr "Kill Process" + +#: processes.py:91 +#, python-format +msgid "" +"Are you sure you want to kill\n" +"%s" +msgstr "" +"Are you sure you want to kill\n" +"%s" + +#: processes.py:167 +msgid "Applications" +msgstr "Applications" + +#: processes.py:168 +msgid "All" +msgstr "All" + +#: processes.py:170 +msgid "All Processes" +msgstr "All Processes" + +#: processes.py:171 +msgid "Usr" +msgstr "Usr" + +#: processes.py:173 +msgid "User Processes" +msgstr "User Processes" + +#: processes.py:174 +msgid "App" +msgstr "App" diff --git a/src/plugins/processes/i18n/processes.pot b/src/plugins/processes/i18n/processes.pot new file mode 100644 index 0000000..178ca18 --- /dev/null +++ b/src/plugins/processes/i18n/processes.pot @@ -0,0 +1,93 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: processes.py:42 +msgid "Process List" +msgstr "" + +#: processes.py:43 +msgid "" +"Lists all running processes and allows them to be killed. through a menu on " +"the LCD." +msgstr "" + +#: processes.py:46 +msgid "Copyright (C)2010 Brett Smith" +msgstr "" + +#: processes.py:52 +msgid "Previous process" +msgstr "" + +#: processes.py:53 +msgid "Next process" +msgstr "" + +#: processes.py:54 +msgid "Next page" +msgstr "" + +#: processes.py:55 +msgid "Previous page" +msgstr "" + +#: processes.py:56 +msgid "Kill process" +msgstr "" + +#: processes.py:57 +msgid "" +"Toggle between applications,\n" +"all and user" +msgstr "" + +#: processes.py:91 +msgid "Kill Process" +msgstr "" + +#: processes.py:91 +#, python-format +msgid "" +"Are you sure you want to kill\n" +"%s" +msgstr "" + +#: processes.py:167 +msgid "Applications" +msgstr "" + +#: processes.py:168 +msgid "All" +msgstr "" + +#: processes.py:170 +msgid "All Processes" +msgstr "" + +#: processes.py:171 +msgid "Usr" +msgstr "" + +#: processes.py:173 +msgid "User Processes" +msgstr "" + +#: processes.py:174 +msgid "App" +msgstr "" diff --git a/src/plugins/processes/processes.py b/src/plugins/processes/processes.py new file mode 100644 index 0000000..38b1a24 --- /dev/null +++ b/src/plugins/processes/processes.py @@ -0,0 +1,392 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("processes", modfile = __file__).ugettext + +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import os +import dbus +import time +import gobject +import logging +logger = logging.getLogger(__name__) + +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop module. Will use g15top instead", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop + +from Xlib import X +import Xlib.protocol.event + +# Plugin details - All of these must be provided +id="processes" +name=_("Process List") +description=_("Lists all running processes and allows them to be \ +killed. through a menu on the LCD.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +reserved_keys = [ g15driver.G_KEY_SETTINGS ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous process"), + g15driver.NEXT_SELECTION : _("Next process"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Kill process"), + g15driver.VIEW : _("Toggle between applications,\nall and user") + } + +def create(gconf_key, gconf_client, screen): + return G15Processes(gconf_client, gconf_key, screen) + +class ProcessMenuItem(g15theme.MenuItem): + """ + MenuItem for individual processes + """ + + def __init__(self, item_id, plugin, process_id, process_name): + g15theme.MenuItem.__init__(self, item_id) + self.icon = None + self.process_id = process_id + self.process_name = process_name + self.plugin = plugin + + def get_default_theme_dir(self): + return os.path.join(os.path.dirname(__file__), "default") + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.process_name if self.process_name is not None and len(self.process_name) > 0 else "Unamed" + if isinstance(self.process_id, int): + item_properties["item_alt"] = self.process_id + else: + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + item_properties["item_icon"] = self.icon + return item_properties + + def activate(self): + kill_name = str(self.process_id) if isinstance(self.process_id, int) else self.process_name + self.plugin.confirm_screen = g15theme.ConfirmationScreen(self.get_screen(), _("Kill Process"), _("Are you sure you want to kill\n%s") % kill_name, + g15icontools.get_icon_path("utilities-system-monitor"), self.plugin._kill_process, self.process_id, + cancel_callback = self.plugin._cancel_kill) + + +class G15Processes(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, ["utilities-system-monitor"], id, name) + self.item_id = 0 + self.confirm_screen = None + + # Can't work out how to kill an application/window given its XID, so only wnck is used for killing + self.session_bus = dbus.SessionBus() + self.bamf_matcher = None + try : + bamf_object = self.session_bus.get_object('org.ayatana.bamf', '/org/ayatana/bamf/matcher') + self.bamf_matcher = dbus.Interface(bamf_object, 'org.ayatana.bamf.matcher') + except Exception as e: + logger.warning("BAMF not available, falling back to WNCK", exc_info = e) + + def activate(self): + self._modes = [ "applications", "all", "user" ] + self._mode = "applications" + self._timer = None + self._matches = [] + g15plugin.G15MenuPlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + if self.bamf_matcher is not None: + self._matches.append(self.bamf_matcher.connect_to_signal("ViewOpened", self._view_opened)) + self._matches.append(self.bamf_matcher.connect_to_signal("ViewClosed", self._view_closed)) + + def deactivate(self): + self._cancel_timer() + g15plugin.G15MenuPlugin.deactivate(self) + for m in self._matches: + m.remove() + self.screen.key_handler.action_listeners.remove(self) + if self.confirm_screen is not None: + self.confirm_screen.delete() + self.confirm_screen = None + + def load_menu_items(self): + pass + + def _get_next_id(self): + self.item_id += 1 + return self.item_id + + def action_performed(self, binding): + if self.page != None and self.page.is_visible(): + if binding.action == g15driver.VIEW: + if self._mode == "applications": + self._mode = "all" + elif self._mode == "all": + self._mode = "user" + elif self._mode == "user": + self._mode = "applications" + self._cancel_timer() + self._refresh() + return True + + def create_menu(self): + menu = g15plugin.G15MenuPlugin.create_menu(self) + menu.on_move = self._reschedule + return menu + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + page.on_shown = self._page_shown + page.on_hidden = self._page_hidden + return page + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + + props["mode"] = self._mode + if self._mode == "applications": + props["title"] = _("Applications") + props["list"] = _("All") + elif self._mode == "all": + props["title"] = _("All Processes") + props["list"] = _("Usr") + elif self._mode == "user": + props["title"] = _("User Processes") + props["list"] = _("App") + return props + + ''' + Private + ''' + def _view_opened(self, window_path, path_type): + if path_type == "application": + self._get_item_for_bamf_application(window_path) + + def _view_closed(self, window_path, path_type): + if path_type == "application": + self._remove_item_for_bamf_application(window_path) + + def _send_event(self, win, ctype, data, mask=None): + """ Send a ClientMessage event to the root """ + data = (data+[0]*(5-len(data)))[:5] + ev = Xlib.protocol.event.ClientMessage(window=win, client_type=ctype, data=(32,(data))) + + if not mask: + mask = (X.SubstructureRedirectMask|X.SubstructureNotifyMask) + + display = self.screen.service.macro_handler.get_x_display() + screen = display.screen() + root = screen.root + + root.send_event(ev, event_mask=mask) + display.flush() + + def _cancel_kill(self, process_id): + self.confirmation_screen = None + + def _do_kill(self, process_id): + os.system("kill %d" % process_id) + time.sleep(0.5) + if process_id in gtop.proclist(): + time.sleep(5.0) + if process_id in gtop.proclist(): + os.system("kill -9 %d" % process_id) + + def _kill_process(self, process_id): + if isinstance(process_id, int): + self._do_kill(process_id) + else: + gobject.idle_add(self._kill_window, process_id) + self.confirmation_screen = None + self._reload_menu() + + def _kill_window(self, window_path): + import wnck + import gtk + window_names = self._get_window_names(window_path) + screen = wnck.screen_get_default() + while gtk.events_pending(): + gtk.main_iteration() + windows = screen.get_windows() + for window_name in window_names: + for w in windows: + if w.get_name() == window_name: + self._do_kill(w.get_pid()) + return + + def _get_window_names(self, path, window_names = []): + app = self.session_bus.get_object("org.ayatana.bamf", path) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + window_names.append(view.Name()) + children = view.Children() + for c in children: + self._get_window_names(c, window_names) + return window_names + + def _get_process_name(self, args, cmd): + result = cmd + for i in range(min(2, len(args))): + basename = os.path.basename(args[i]) + if basename.find(cmd) != -1: + result = basename + break + return result + + def _reload_menu(self): + g15scheduler.schedule("ReloadProcesses", 0, self._do_reload_menu) + + def _get_menu_item(self, pid): + item = self.menu.get_child_by_id("process-%s" % pid) + if item == None: + item = ProcessMenuItem("process-%s" % pid, self, pid, None) + self.menu.add_child(item) + return item + + def _get_bamf_application_object(self, window): + app = self.session_bus.get_object("org.ayatana.bamf", window) + view = dbus.Interface(app, 'org.ayatana.bamf.view') + return view + + def _remove_item_for_bamf_application(self, window): + item = self.menu.get_child_by_id("process-%s" % window) + if item is not None: + self.menu.remove_child(item) + + def _get_item_for_bamf_application(self, window): + view = self._get_bamf_application_object(window) + item = self._get_menu_item(window) + try: + item.process_name = view.Name() + except dbus.DBusException as e: + logger.debug("Could not get process_name. Using default", exc_info = e) + item.process_name = "Unknown" + try: + icon_name = view.Icon() + if icon_name and len(icon_name) > 0: + icon_path = g15icontools.get_icon_path(icon_name, warning = False) + if icon_path: + item.icon = g15cairo.load_surface_from_file(icon_path, 32) + except dbus.DBusException as e: + logger.debug("Could not get icon", exc_info = e) + pass + + + return item + + def _do_reload_menu(self): + if not self.active: + return + + this_items = {} + if self._mode == "applications": + if self.bamf_matcher != None: + for window in self.bamf_matcher.RunningApplications(): + try: + item = self._get_item_for_bamf_application(window) + this_items[item.id] = item + except Exception as e: + logger.debug("Could not get info from BAMF", exc_info = e) + pass + else: + import wnck + screen = wnck.screen_get_default() + for window in screen.get_windows(): + pid = window.get_pid() + if pid > 0: + item = self._get_menu_item(pid) + item.process_name = window.get_name() + this_items[item.id] = item + pixbuf = window.get_icon() + if pixbuf: + item.icon = g15cairo.pixbuf_to_surface(pixbuf) + + else: + for process_id in gtop.proclist(): + process_id = "%d" % process_id + try : + pid = int(process_id) + proc_state = gtop.proc_state(pid) + proc_args = gtop.proc_args(pid) + if self._mode == "all" or ( self._mode != "all" and proc_state.uid == os.getuid()): + item = self._get_menu_item(pid) + item.icon = None + item.process_name = self._get_process_name(proc_args, proc_state.cmd) + this_items[item.id] = item + except Exception as e: + logger.debug("Process may have disappeared", exc_info = e) + # In case the process disappears + pass + + # Remove any missing items + for item in self.menu.get_children(): + if not item.id in this_items: + self.menu.remove_child(item) + + # Make sure selected still exists + if self.menu.selected != None and self.menu.get_child_by_id(self.menu.selected.id) is None: + if len(self.menu.get_child_count()) > 0: + self.menu.selected = self.menu.get_children()[0] + else: + self.menu.selected = None + + self.page.mark_dirty() + self.screen.redraw(self.page) + + def _on_move(self): + self._reschedule() + + def _on_selected(self): + self.screen.redraw(self.page) + + def _page_shown(self): + logger.debug("Process list activated") + self._reload_menu() + self._schedule_refresh() + + def _page_hidden(self): + self._cancel_timer() + + def _refresh(self): + self._reload_menu() + self._schedule_refresh() + + def _cancel_timer(self): + if self._timer != None: + logger.debug("Stopping refreshing process list") + self._timer.cancel() + + def _reschedule(self): + self._cancel_timer() + self._schedule_refresh() + + def _schedule_refresh(self): + """ + When viewing applications, we don't refresh, just rely on BAMF + events when BAMF is available + """ + if not self._mode == "applications" or self.bamf_matcher is None: + self._timer = g15scheduler.schedule("ProcessesRefresh", 5.0, self._refresh) diff --git a/src/plugins/profiles/Makefile.am b/src/plugins/profiles/Makefile.am new file mode 100644 index 0000000..2c854e0 --- /dev/null +++ b/src/plugins/profiles/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/profiles +plugin_DATA = profiles.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/profiles/bw-locked-inverted.gif b/src/plugins/profiles/bw-locked-inverted.gif new file mode 100644 index 0000000000000000000000000000000000000000..391c266c546bc4e2897be59e9ecf92dcd9f6a09e GIT binary patch literal 54 zcmZ?wbhEHb#ebsCMX8A;sVNHOnI#ztAsML(?w-B@42nNl7`Yf28FWAj ZL0TA?czfy@ByPW$(vz>*=F7-n4FIDW5w8FM literal 0 HcmV?d00001 diff --git a/src/plugins/profiles/profiles.py b/src/plugins/profiles/profiles.py new file mode 100644 index 0000000..b2c54c7 --- /dev/null +++ b/src/plugins/profiles/profiles.py @@ -0,0 +1,213 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("profiles", modfile = __file__).ugettext + +import gnome15.g15profile as g15profile +import gnome15.g15driver as g15driver +import gnome15.g15theme as g15theme +import gnome15.g15plugin as g15plugin +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions +from gnome15.util.g15pythonlang import find +import os +import logging +logger = logging.getLogger(__name__) + +# Custom actions +SELECT_PROFILE = "select-profile" + +# Register the action with all supported models +g15devices.g15_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_L1 ], g15driver.KEY_STATE_HELD) +g15devices.g19_action_keys[SELECT_PROFILE] = g15actions.ActionBinding(SELECT_PROFILE, [ g15driver.G_KEY_BACK ], g15driver.KEY_STATE_HELD) + +# Plugin details - All of these must be provided +id="profiles" +name=_("Profile Selector") +description=_("Allows selection of the currently active profile. You may also \n\ +lock a profile to the device it is running on, preventing\n\ +changes triggered by active window changes and other\n\ +automatic profile selection methods.\n\n\ +You may also use this plugin to set the window title that\n\ +activates the current profile by making the required\n\ +window foreground, and the pressing the key bound to\n\ +'Select current window as activator' (see below).") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +default_enabled=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_Z10, g15driver.MODEL_G11, g15driver.MODEL_MX5500, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + SELECT_PROFILE : _("Show profile selector"), + g15driver.PREVIOUS_SELECTION : _("Previous item"), + g15driver.NEXT_SELECTION : _("Next item"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.VIEW : _("Lock profile"), + g15driver.SELECT : _("Activate profile"), + g15driver.CLEAR : _("Set current window as activator") + } + +def create(gconf_key, gconf_client, screen): + return G15Profiles(gconf_client, gconf_key, screen) + +""" +Represents a profile as a single item in a menu +""" +class ProfileMenuItem(g15theme.MenuItem): + def __init__(self, profile, plugin, id): + g15theme.MenuItem.__init__(self, id) + self.profile = profile + self._plugin = plugin + self._surface = None + + def get_theme_properties(self): + locked = self.profile.is_active() and g15profile.is_locked(self._plugin.screen.device) + + if self.get_screen().device.bpp > 1: + locked_icon = g15icontools.get_icon_path(["locked","gdu-encrypted-lock", + "status_lock", "stock_lock" ]) + else: + if self.parent.selected == self: + locked_icon = os.path.join(os.path.dirname(__file__), 'bw-locked-inverted.gif') + else: + locked_icon = os.path.join(os.path.dirname(__file__), 'bw-locked.gif') + + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["item_name"] = self.profile.name + item_properties["item_radio"] = True + item_properties["item_radio_selected"] = self.profile.is_active() + item_properties["item_icon"] = self._surface + item_properties["item_alt_icon"] = locked_icon if locked else "" + item_properties["item_alt"] = "" + return item_properties + + def on_configure(self): + g15theme.MenuItem.on_configure(self) + self._surface = g15cairo.load_surface_from_file(self.profile.get_profile_icon_path(16), self.theme.bounds[3]) + + def activate(self): + locked = g15profile.is_locked(self._plugin.screen.device) + if locked: + g15profile.set_locked(self._plugin.screen.device, False) + self.profile.make_active() + if locked: + g15profile.set_locked(self._plugin.screen.device, True) + + # Raise the macros page if it is enabled and not raised + macros_page = self._plugin.screen.get_page("macros") + if macros_page is not None and not macros_page.is_visible(): + self._plugin.screen.raise_page(macros_page) + + +""" +Profiles plugin class +""" +class G15Profiles(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, [ "user-bookmarks", "bookmarks" ], id, _("Profiles")) + + def activate(self): + g15plugin.G15MenuPlugin.activate(self) + g15profile.profile_listeners.append(self._stored_profiles_changed) + self.delete_timer = None + self.screen.key_handler.action_listeners.append(self) + self._notify_handles = [] + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._profiles_changed)) + self._notify_handles.append(self.gconf_client.notify_add("/apps/gnome15/%s/locked" % self.screen.device.uid, self._profiles_changed)) + + def deactivate(self): + g15plugin.G15MenuPlugin.deactivate(self) + g15profile.profile_listeners.remove(self._stored_profiles_changed) + self.screen.key_handler.action_listeners.remove(self) + for h in self._notify_handles: + self.gconf_client.notify_remove(h) + + def action_performed(self, binding): + if self.page != None: + if binding.action == SELECT_PROFILE: + self.screen.raise_page(self.page) + elif self.page.is_visible(): + if binding.action == g15driver.VIEW: + active = g15profile.get_active_profile(self.screen.device) + if active.id == self.menu.selected.profile.id: + g15profile.set_locked(self.screen.device, not g15profile.is_locked(self.screen.device)) + else: + if g15profile.is_locked(self.screen.device): + g15profile.set_locked(self.screen.device, False) + self.menu.selected.profile.make_active() + g15profile.set_locked(self.screen.device, True) + return True + elif binding.action == g15driver.CLEAR: + profile = self.menu.selected.profile + if self.screen.service.active_application_name is not None: + self._configure_profile_with_window_name(profile, self.screen.service.active_application_name) + profile.save() + elif self.screen.service.active_window_title is not None: + self._configure_profile_with_window_name(profile, self.screen.service.active_window_title) + profile.save() + return True + + + def show_menu(self): + active_profile = g15profile.get_active_profile(self.screen.device) + g15plugin.G15MenuPlugin.show_menu(self) + if active_profile: + item = find(lambda m: m.profile == active_profile, self.menu.get_children()) + if item: + self.menu.set_selected_item(item) + + def load_menu_items(self): + items = [] + profile_list = g15profile.get_profiles(self.screen.device) + for profile in profile_list: + items.append(ProfileMenuItem(profile, self, "profile-%s" % profile.id )) + items = sorted(items, key=lambda item: item.profile.name) + self.menu.set_children(items) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def get_theme_properties(self): + p = g15plugin.G15MenuPlugin.get_theme_properties(self) + p["profile_locked"] = g15profile.is_locked(self.screen.device) + return p + + ''' + Private + ''' + def _configure_profile_with_window_name(self, profile, window_name): + profile.activate_on_focus = True + profile.activate_on_launch = False + profile.launch_pattern = None + profile.window_name = window_name + + def _profiles_changed(self, arg0 = None, arg1 = None, arg2 = None, arg3 = None): + self.screen.redraw(self.page) + + def _stored_profiles_changed(self, profile_id, device): + self._reload_menu() + + def _reload_menu(self): + self.load_menu_items() + self.screen.redraw(self.page) + \ No newline at end of file diff --git a/src/plugins/rss/Makefile.am b/src/plugins/rss/Makefile.am new file mode 100644 index 0000000..3590ed3 --- /dev/null +++ b/src/plugins/rss/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/rss +plugin_DATA = rss.py \ + rss.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/rss/default/Makefile.am b/src/plugins/rss/default/Makefile.am new file mode 100644 index 0000000..98c63ac --- /dev/null +++ b/src/plugins/rss/default/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/rss/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + mx5500-menu-entry.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/rss/default/default-menu-entry.svg b/src/plugins/rss/default/default-menu-entry.svg new file mode 100644 index 0000000..c1b25e7 --- /dev/null +++ b/src/plugins/rss/default/default-menu-entry.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${ent_month_year} ${ent_time} + ${ent_title} + + + + ${ent_month_year} ${ent_time} + ${ent_title} + + diff --git a/src/plugins/rss/default/default-menu-screen.svg b/src/plugins/rss/default/default-menu-screen.svg new file mode 100644 index 0000000..32fffea --- /dev/null +++ b/src/plugins/rss/default/default-menu-screen.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + + + + + + diff --git a/src/plugins/rss/default/g19-menu-entry.svg b/src/plugins/rss/default/g19-menu-entry.svg new file mode 100644 index 0000000..51edfac --- /dev/null +++ b/src/plugins/rss/default/g19-menu-entry.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${ent_title} + ${ent_month_year} ${ent_time} + + diff --git a/src/plugins/rss/default/g19-menu-screen.svg b/src/plugins/rss/default/g19-menu-screen.svg new file mode 100644 index 0000000..aab80cd --- /dev/null +++ b/src/plugins/rss/default/g19-menu-screen.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + ${updated} + + + + + + + There are no news items in this feed + + diff --git a/src/plugins/rss/default/mx5500-menu-entry.svg b/src/plugins/rss/default/mx5500-menu-entry.svg new file mode 100644 index 0000000..25d1c7d --- /dev/null +++ b/src/plugins/rss/default/mx5500-menu-entry.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + + + + ${ent_month_year} ${ent_time_24} + ${ent_title} + + + diff --git a/src/plugins/rss/i18n/rss.en_GB.po b/src/plugins/rss/i18n/rss.en_GB.po new file mode 100644 index 0000000..bdd1e08 --- /dev/null +++ b/src/plugins/rss/i18n/rss.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/rss.glade.h:1 +msgid "Feeds" +msgstr "Feeds" + +#: i18n/rss.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/rss.glade.h:3 +msgid "RSS Preferences" +msgstr "RSS Preferences" + +#: i18n/rss.glade.h:4 +msgid "Update every" +msgstr "Update every" + +#: i18n/rss.glade.h:5 +msgid "minutes" +msgstr "minutes" + +#: i18n/rss.glade.h:6 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/rss.glade.h:7 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/rss/i18n/rss.glade.h b/src/plugins/rss/i18n/rss.glade.h new file mode 100644 index 0000000..66ea145 --- /dev/null +++ b/src/plugins/rss/i18n/rss.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Feeds"); +char *s = N_("Options"); +char *s = N_("RSS Preferences"); +char *s = N_("Update every"); +char *s = N_("minutes"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/rss/i18n/rss.pot b/src/plugins/rss/i18n/rss.pot new file mode 100644 index 0000000..1350b73 --- /dev/null +++ b/src/plugins/rss/i18n/rss.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/rss.glade.h:1 +msgid "Feeds" +msgstr "" + +#: i18n/rss.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/rss.glade.h:3 +msgid "RSS Preferences" +msgstr "" + +#: i18n/rss.glade.h:4 +msgid "Update every" +msgstr "" + +#: i18n/rss.glade.h:5 +msgid "minutes" +msgstr "" + +#: i18n/rss.glade.h:6 +msgid "toolbutton1" +msgstr "" + +#: i18n/rss.glade.h:7 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/rss/rss.py b/src/plugins/rss/rss.py new file mode 100644 index 0000000..ea105d2 --- /dev/null +++ b/src/plugins/rss/rss.py @@ -0,0 +1,379 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("rss", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15desktop as g15desktop +import subprocess +import time +import os +import feedparser +import gtk +import gconf +import logging +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "rss" +name = _("RSS") +description = _("A simple RSS reader. Multiple feeds may be added, with a screen being \ +allocated to each one once it has loaded.\n\n\ +\ +Warning: This plugin has a small memory leak. If you experience problems, \ +try reducing the update interval time.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous news item"), + g15driver.NEXT_SELECTION : _("Next news items"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Open item in browser") + } + +def create(gconf_key, gconf_client, screen): + return G15RSS(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15RSSPreferences(parent, driver, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +class G15RSSPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "rss.ui")) + + # Feeds + self.feed_model = widget_tree.get_object("FeedModel") + self.reload_model() + self.feed_list = widget_tree.get_object("FeedList") + self.url_renderer = widget_tree.get_object("URLRenderer") + + # Optins + self.update_adjustment = widget_tree.get_object("UpdateAdjustment") + self.update_adjustment.set_value(g15gconf.get_int_or_default(self._gconf_client, "%s/update_time" % self._gconf_key, 60)) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_times" % gconf_key, "TwentyFourHourTimes", True, widget_tree) + + # Connect to events + self.update_adjustment.connect("value-changed", self.update_time_changed) + self.url_renderer.connect("edited", self.url_edited) + widget_tree.get_object("NewURL").connect("clicked", self.new_url) + widget_tree.get_object("RemoveURL").connect("clicked", self.remove_url) + + # Display + + # Show dialog + dialog = widget_tree.get_object("RSSDialog") + dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/urls", self.urls_changed); + dialog.run() + dialog.hide() + gconf_client.notify_remove(ah); + + def update_time_changed(self, widget): + self._gconf_client.set_int(self._gconf_key + "/update_time", int(widget.get_value())) + + def url_edited(self, widget, row_index, value): + row = self.feed_model[row_index] + if value != "": + urls = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + if row[0] in urls: + urls.remove(row[0]) + urls.append(value) + self._gconf_client.set_list(self._gconf_key + "/urls", gconf.VALUE_STRING, urls) + else: + self.feed_model.remove(self.feed_model.get_iter(row_index)) + + def urls_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.feed_model.clear() + for url in self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING): + self.feed_model.append([ url, True ]) + + def new_url(self, widget): + self.feed_model.append(["", True]) + self.feed_list.set_cursor_on_cell(str(len(self.feed_model) - 1), focus_column=self.feed_list.get_column(0), focus_cell=self.url_renderer, start_editing=True) + self.feed_list.grab_focus() + + def remove_url(self, widget): + (model, path) = self.feed_list.get_selection().get_selected() + url = model[path][0] + urls = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + if url in urls: + urls.remove(url) + self._gconf_client.set_list(self._gconf_key + "/urls", gconf.VALUE_STRING, urls) + +class G15FeedsMenuItem(g15theme.MenuItem): + def __init__(self, component_id, entry, gconf_client, gconf_key): + g15theme.MenuItem.__init__(self, component_id) + self.entry = entry + self.gconf_client = gconf_client + self.gconf_key = gconf_key + if "icon" in self.entry: + self.icon = self.entry["icon"] + elif "image" in self.entry: + img = self.entry["image"] + if "url" in img: + self.icon = img["url"] + elif "link" in img: + self.icon = img["link"] + else: + self.icon = None + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry")) + + def get_theme_properties(self): + + use_twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True) + + element_properties = g15theme.MenuItem.get_theme_properties(self) + element_properties["ent_title"] = self.entry.title + element_properties["ent_link"] = self.entry.link + if g15pythonlang.attr_exists(self.entry, "description"): + element_properties["ent_description"] = self.entry.description + + if hasattr(self.entry, 'date_parsed'): + dt = self.entry.date_parsed + elif hasattr(self.entry, 'published_parsed'): + logger.debug("Could not get date_parsed attribute. Trying published_parsed") + dt = self.entry.published_parsed + else: + logger.debug("Could not get publish_parsed attribute. Using current time.") + dt = time.localtime() + + element_properties["ent_locale_date_time"] = time.strftime("%x %X", dt) + element_properties["ent_locale_time"] = time.strftime("%X", dt) + element_properties["ent_locale_date"] = time.strftime("%x", dt) + element_properties["ent_time_24"] = time.strftime("%H:%M", dt) + if use_twenty_four_hour: + element_properties["ent_time"] = g15locale.format_time_24hour(time, self.gconf_client, False) + else: + element_properties["ent_time"] = g15locale.format_time(time, self.gconf_client, False) + element_properties["ent_full_time_24"] = time.strftime("%H:%M:%S", dt) + if use_twenty_four_hour: + element_properties["ent_full_time"] = g15locale.format_time_24hour(time, self.gconf_client, True) + else: + element_properties["ent_full_time"] = g15locale.format_time(time, self.gconf_client, True) + element_properties["ent_time_12"] = time.strftime("%I:%M %p", dt) + element_properties["ent_full_time_12"] = time.strftime("%I:%M:%S %p", dt) + element_properties["ent_short_date"] = time.strftime("%a %d %b", dt) + element_properties["ent_full_date"] = time.strftime("%A %d %B", dt) + element_properties["ent_month_year"] = time.strftime("%m/%y", dt) + + return element_properties + + def activate(self): + g15desktop.browse(self.entry.link) + return True + +class G15FeedPage(g15theme.G15Page): + + def __init__(self, plugin, url): + + self._gconf_client = plugin._gconf_client + self._gconf_key = plugin._gconf_key + self._screen = plugin._screen + self._icon_surface = None + self._icon_embedded = None + self._selected_icon_embedded = None + self.url = url + self.index = -1 + self._menu = g15theme.Menu("menu") + self._menu.on_selected = self._on_selected + g15theme.G15Page.__init__(self, "Feed " + str(plugin._page_serial), self._screen, + thumbnail_painter=self._paint_thumbnail, + theme=g15theme.G15Theme(self, "menu-screen"), + theme_properties_callback=self._get_theme_properties, + originating_plugin = plugin) + self.add_child(self._menu) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + plugin._page_serial += 1 + self._reload() + self._screen.add_page(self) + self._screen.redraw(self) + + """ + Private + """ + def _on_selected(self): + self._selected_icon_embedded = None + if self._menu.selected is not None and self._menu.selected.icon is not None: + try : + icon_surface = g15cairo.load_surface_from_file(self._menu.selected.icon) + self._selected_icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(self._menu.selected.icon), exc_info = e) + + def _reload(self): + self.feed = feedparser.parse(self.url) + icon = None + if "icon" in self.feed["feed"]: + icon = self.feed["feed"]["icon"] + elif "image" in self.feed["feed"]: + img = self.feed["feed"]["image"] + if "url" in img: + icon = img["url"] + elif "link" in img: + icon = img["link"] + + title = self.feed["feed"]["title"] if "title" in self.feed["feed"] else self.url + if icon is None and title.endswith("- Twitter Search"): + title = title[:-16] + icon = g15icontools.get_icon_path("gnome15") + if icon is None: + icon = g15icontools.get_icon_path(["application-rss+xml","gnome-mime-application-rss+xml"], self._screen.height) + + if icon == None: + self._icon_surface = None + self._icon_embedded = None + else: + try : + icon_surface = g15cairo.load_surface_from_file(icon) + self._icon_surface = icon_surface + self._icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(icon), exc_info = e) + self._icon_surface = None + self._icon_embedded = None + self.set_title(title) + self._subtitle = self.feed["feed"]["subtitle"] if "subtitle" in self.feed["feed"] else "" + self._menu.remove_all_children() + i = 0 + for entry in self.feed.entries: + self._menu.add_child(G15FeedsMenuItem("feeditem-%d" % i, entry, self._gconf_client, self._gconf_key)) + i += 1 + + def _get_theme_properties(self): + properties = {} + properties["title"] = self.title + if self._selected_icon_embedded is not None: + properties["icon"] = self._selected_icon_embedded + else: + properties["icon"] = self._icon_embedded + properties["subtitle"] = self._subtitle + properties["no_news"] = self._menu.get_child_count() == 0 + properties["alt_title"] = "" + try: + update_time = self.feed.updated + if isinstance(self.feed.updated, str): + update_time = self.feed.updated_parsed + + properties["updated"] = "%s %s" % (time.strftime("%H:%M", update_time), time.strftime("%a %d %b", update_time)) + except AttributeError as ae: + logger.debug("Could not get attribute", exc_info = ae) + pass + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._icon_surface: + return g15cairo.paint_thumbnail_image(allocated_size, self._icon_surface, canvas) + +class G15RSS(): + + def __init__(self, gconf_client, gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._gconf_client = gconf_client + self._page_serial = 1 + self._refresh_timer = None + + def activate(self): + self._pages = {} + self._schedule_refresh() + self._update_time_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/update_time", self._update_time_changed) + self._urls_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/urls", self._urls_changed) + g15scheduler.schedule("LoadFeeds", 0, self._load_feeds) + + def deactivate(self): + self._cancel_refresh() + self._gconf_client.notify_remove(self._update_time_changed_handle); + self._gconf_client.notify_remove(self._urls_changed_handle); + for page in self._pages: + self._screen.del_page(self._pages[page]) + self._pages = {} + + ''' + Private + ''' + + def _schedule_refresh(self): + schedule_seconds = g15gconf.get_int_or_default(self._gconf_client, "%s/update_time" % self._gconf_key, 60) * 60.0 + self._refresh_timer = g15scheduler.schedule("FeedRefreshTimer", schedule_seconds, self._refresh) + + def _refresh(self): + logger.info("Refreshing RSS feeds") + for page_id in list(self._pages): + page = self._pages[page_id] + page._reload() + page.redraw() + self._schedule_refresh() + + def destroy(self): + pass + + def _update_time_changed(self, client, connection_id, entry, args): + self._cancel_refresh() + self._schedule_refresh() + + def _cancel_refresh(self): + if self._refresh_timer: + self._refresh_timer.cancel() + + def _urls_changed(self, client, connection_id, entry, args): + self._load_feeds() + + def _load_feeds(self): + feed_list = self._gconf_client.get_list(self._gconf_key + "/urls", gconf.VALUE_STRING) + + # Add new pages + for url in feed_list: + if not url in self._pages: + self._pages[url] = G15FeedPage(self, url) + + # Remove pages that no longer exist + to_remove = [] + for page_url in self._pages: + page = self._pages[page_url] + if not page.url in feed_list: + self._screen.del_page(page) + to_remove.append(page_url) + for page in to_remove: + del self._pages[page] + diff --git a/src/plugins/rss/rss.ui b/src/plugins/rss/rss.ui new file mode 100644 index 0000000..24d48d5 --- /dev/null +++ b/src/plugins/rss/rss.ui @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + 320 + False + 5 + RSS Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + True + True + FeedModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Feeds</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + + + True + False + Update every + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + True + True + 0 + + + + + Show Time in 24 hour format + True + True + False + True + + + True + True + 1 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + + 1 + 9999 + 1 + 1 + 1 + + diff --git a/src/plugins/runapp/Makefile.am b/src/plugins/runapp/Makefile.am new file mode 100644 index 0000000..ad40f83 --- /dev/null +++ b/src/plugins/runapp/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/runapp +plugin_DATA = runapp.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/runapp/background-160x43.png b/src/plugins/runapp/background-160x43.png new file mode 100644 index 0000000000000000000000000000000000000000..e965e7984f2941ffd1bf55842c644d972e392a3a GIT binary patch literal 4197 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 mS1a(Pb8P*$9N^5%%>N(dCkch^?d**J0000 + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp_c} + + + + + + + ${sensor} + + + + 40 + 70 + 110 + 140 + 0 + + diff --git a/src/plugins/runapp/i18n/background.en_GB.po b/src/plugins/runapp/i18n/background.en_GB.po new file mode 100644 index 0000000..5db1e79 --- /dev/null +++ b/src/plugins/runapp/i18n/background.en_GB.po @@ -0,0 +1,66 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "Allow macro profiles to override background" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "Background Image" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "Center" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "Scale" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "Select A Background" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "Stretch" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "Style" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "Tile" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "Use _image file" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "Wallpaper Preferences" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "Zoom" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "_Same as desktop background" diff --git a/src/plugins/runapp/i18n/background.glade.h b/src/plugins/runapp/i18n/background.glade.h new file mode 100644 index 0000000..af34a46 --- /dev/null +++ b/src/plugins/runapp/i18n/background.glade.h @@ -0,0 +1,12 @@ +char *s = N_("Allow macro profiles to override background"); +char *s = N_("Background Image"); +char *s = N_("Center"); +char *s = N_("Scale"); +char *s = N_("Select A Background"); +char *s = N_("Stretch"); +char *s = N_("Style"); +char *s = N_("Tile"); +char *s = N_("Use _image file"); +char *s = N_("Wallpaper Preferences"); +char *s = N_("Zoom"); +char *s = N_("_Same as desktop background"); diff --git a/src/plugins/runapp/i18n/background.pot b/src/plugins/runapp/i18n/background.pot new file mode 100644 index 0000000..b945edf --- /dev/null +++ b/src/plugins/runapp/i18n/background.pot @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/background.glade.h:1 +msgid "Allow macro profiles to override background" +msgstr "" + +#: i18n/background.glade.h:2 +msgid "Background Image" +msgstr "" + +#: i18n/background.glade.h:3 +msgid "Center" +msgstr "" + +#: i18n/background.glade.h:4 +msgid "Scale" +msgstr "" + +#: i18n/background.glade.h:5 +msgid "Select A Background" +msgstr "" + +#: i18n/background.glade.h:6 +msgid "Stretch" +msgstr "" + +#: i18n/background.glade.h:7 +msgid "Style" +msgstr "" + +#: i18n/background.glade.h:8 +msgid "Tile" +msgstr "" + +#: i18n/background.glade.h:9 +msgid "Use _image file" +msgstr "" + +#: i18n/background.glade.h:10 +msgid "Wallpaper Preferences" +msgstr "" + +#: i18n/background.glade.h:11 +msgid "Zoom" +msgstr "" + +#: i18n/background.glade.h:12 +msgid "_Same as desktop background" +msgstr "" diff --git a/src/plugins/runapp/runapp.py b/src/plugins/runapp/runapp.py new file mode 100644 index 0000000..e6d3579 --- /dev/null +++ b/src/plugins/runapp/runapp.py @@ -0,0 +1,149 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("rundapp", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.g15profile as g15profile +import gnome15.g15desktop as g15desktop +import cairo +import gtk +import os +import logging +import gconf +from lxml import etree +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="rundapp" +name=_("aaaa") +description=_("Mi app en la pantalla") +author=_("Gustavo Adolfo Mesa Roldan ") +copyright="Copyright (C)2020 Brett Gustavo Adolfo Mesa Roldan" +site="http://hatthieves.es" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +time_2 = 0 +time_2 = time_2 + 1 + +def create(gconf_key, gconf_client, screen): + return G19app(gconf_key, gconf_client, screen) + +class G19appPainter(g15screen.Painter): + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.background_image = None + self.brightness = 0 + self._screen = screen + + def paint(self, canvas): + + print("1111111111111") + if self.background_image != None: + canvas.set_source_surface(self.background_image, 0.0, 0.0) + canvas.paint() + if self.brightness > 0: + canvas.set_source_rgba(1.0, 1.0, 1.0, ( self.brightness / 100.0 )) + else: + canvas.set_source_rgba(0.0, 0.0, 0.0, ( abs(self.brightness) / 100.0 )) + size = self._screen.device.lcd_size + canvas.rectangle(0,0,size[0],size[1]) + canvas.fill() + +class G19app(): + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + self.gconf_client.add_dir('/desktop/gnome/background', gconf.CLIENT_PRELOAD_NONE) + + def refresh(self): + print("2222222222222222") + self.painter = G19appPainter(self.screen) + + def activate(self): + self.painter = G19appPainter(self.screen) + self.notify_handlers = [] + self.screen.painters.append(self.painter) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/path", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/type", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/style", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add(self.gconf_key + "/brightness", self.config_changed)) + self.notify_handlers.append(self.gconf_client.notify_add("/apps/gnome15/%s/active_profile" % self.screen.device.uid, self._active_profile_changed)) + + g15profile.profile_listeners.append(self._profiles_changed) + self._do_config_changed() + + def deactivate(self): + g15profile.profile_listeners.remove(self._profiles_changed) + self.screen.painters.remove(self.painter) + self.screen.redraw() + + def config_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def destroy(self): + pass + + ''' + Private + ''' + def _active_profile_changed(self, client, connection_id, entry, args): + self._do_config_changed() + + def _profiles_changed(self, profile_id, device_uid): + self._do_config_changed() + + def _do_config_changed(self): + print("2222222222222222") + screen_size = self.screen.size + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, screen_size[0], screen_size[1]) + context = cairo.Context(surface) + context.scale(screen_size[0], screen_size[1]) + + context.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FontWeight.BOLD) + context.move_to(0.1, 0.1) + context.show_text("Paner Chupala") + + context.set_line_width(0.04) + context.move_to(0.1, 0.5) + context.curve_to(0.4, 0.9, 0.6, 0.1, 0.9, 0.5) + context.stroke() + context.set_source_rgba(1, 0.2, 0.2, 0.6) + context.set_line_width(0.02) + context.move_to(0.1, 0.5) + context.line_to(0.4, 0.9) + context.move_to(0.6, 0.1) + context.line_to(0.9, 0.5) + context.stroke() + context.save() + context.paint() + context.restore() + self.painter.background_image = surface + self.painter.brightness = self.gconf_client.get_int(self.gconf_key + "/brightness") + self.screen.redraw() + + + diff --git a/src/plugins/screensaver/Makefile.am b/src/plugins/screensaver/Makefile.am new file mode 100644 index 0000000..45d5bf4 --- /dev/null +++ b/src/plugins/screensaver/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/screensaver +plugin_DATA = screensaver.py \ + screensaver.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/screensaver/default/Makefile.am b/src/plugins/screensaver/default/Makefile.am new file mode 100644 index 0000000..ce388f6 --- /dev/null +++ b/src/plugins/screensaver/default/Makefile.am @@ -0,0 +1,10 @@ +themedir = $(datadir)/gnome15/plugins/screensaver/default +theme_DATA = default.svg \ + default-nobody.svg \ + g19.svg \ + g19-nobody.svg \ + mx5500.svg \ + mx5500-nobody.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/screensaver/default/default-nobody.svg b/src/plugins/screensaver/default/default-nobody.svg new file mode 100644 index 0000000..d81142c --- /dev/null +++ b/src/plugins/screensaver/default/default-nobody.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} + + diff --git a/src/plugins/screensaver/default/default.svg b/src/plugins/screensaver/default/default.svg new file mode 100644 index 0000000..4659567 --- /dev/null +++ b/src/plugins/screensaver/default/default.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${body} + ${title} + + + diff --git a/src/plugins/screensaver/default/g19-nobody.svg b/src/plugins/screensaver/default/g19-nobody.svg new file mode 100644 index 0000000..ff40ce9 --- /dev/null +++ b/src/plugins/screensaver/default/g19-nobody.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/screensaver/default/g19.svg b/src/plugins/screensaver/default/g19.svg new file mode 100644 index 0000000..706461f --- /dev/null +++ b/src/plugins/screensaver/default/g19.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${body} + + diff --git a/src/plugins/screensaver/default/mx5500-nobody.svg b/src/plugins/screensaver/default/mx5500-nobody.svg new file mode 100644 index 0000000..86fac04 --- /dev/null +++ b/src/plugins/screensaver/default/mx5500-nobody.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + + diff --git a/src/plugins/screensaver/default/mx5500.svg b/src/plugins/screensaver/default/mx5500.svg new file mode 100644 index 0000000..03f06c9 --- /dev/null +++ b/src/plugins/screensaver/default/mx5500.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${body} + ${title} + + + diff --git a/src/plugins/screensaver/i18n/screensaver.en_GB.po b/src/plugins/screensaver/i18n/screensaver.en_GB.po new file mode 100644 index 0000000..e1fd7c7 --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.en_GB.po @@ -0,0 +1,34 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/screensaver.glade.h:1 +msgid "Message" +msgstr "Message" + +#: i18n/screensaver.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/screensaver.glade.h:3 +msgid "Dim Controls" +msgstr "Dim Controls" + +#: i18n/screensaver.glade.h:4 +msgid "Screensaver Preferences" +msgstr "Screensaver Preferences" diff --git a/src/plugins/screensaver/i18n/screensaver.glade.h b/src/plugins/screensaver/i18n/screensaver.glade.h new file mode 100644 index 0000000..b665e5e --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.glade.h @@ -0,0 +1,4 @@ +char *s = N_("Message"); +char *s = N_("Options"); +char *s = N_("Dim Controls"); +char *s = N_("Screensaver Preferences"); diff --git a/src/plugins/screensaver/i18n/screensaver.pot b/src/plugins/screensaver/i18n/screensaver.pot new file mode 100644 index 0000000..b70e60d --- /dev/null +++ b/src/plugins/screensaver/i18n/screensaver.pot @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/screensaver.glade.h:1 +msgid "Message" +msgstr "" + +#: i18n/screensaver.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/screensaver.glade.h:3 +msgid "Dim Controls" +msgstr "" + +#: i18n/screensaver.glade.h:4 +msgid "Screensaver Preferences" +msgstr "" diff --git a/src/plugins/screensaver/screensaver.py b/src/plugins/screensaver/screensaver.py new file mode 100644 index 0000000..52dc422 --- /dev/null +++ b/src/plugins/screensaver/screensaver.py @@ -0,0 +1,219 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("screensaver", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15driver as g15driver +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +from threading import Timer +import gtk +import dbus +import logging +import os.path +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id="screensaver" +name=_("Screensaver") +description=_("Dim the keyboard and display a message (on models with an LCD screen) when the desktop screen saver activates.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] + + +''' +This plugin displays a high priority screen when the screensaver activates +''' + +def create(gconf_key, gconf_client, screen): + return G15ScreenSaver(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "screensaver.ui")) + + dialog = widget_tree.get_object("ScreenSaverDialog") + dialog.set_transient_for(parent) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/dim_keyboard" % gconf_key,"DimKeyboardCheckbox", True, widget_tree) + + if driver.get_bpp() == 0: + widget_tree.get_object("MessageFrame").hide() + + text_buffer = widget_tree.get_object("TextBuffer") + text = gconf_client.get_string(gconf_key + "/message_text") + if text == None: + text = "" + text_buffer.set_text(text) + text_h = text_buffer.connect("changed", changed, gconf_key + "/message_text", gconf_client) + + dialog.run() + dialog.hide() + text_buffer.disconnect(text_h) + +def changed(widget, key, gconf_client): + if key.endswith("/dim_keyboard"): + gconf_client.set_bool(key, widget.get_active()) + else: + bounds = widget.get_bounds() + gconf_client.set_string(key, widget.get_text(bounds[0],bounds[1])) + pass + +class G15ScreenSaver(): + + def __init__(self, gconf_key, gconf_client, screen): + self._screen = screen + self._session_bus = None + self._in_screensaver = False + self._page = None + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self.dimmed = False + + def activate(self): + self._controls = [] + self._control_values = [] + for control in self._screen.driver.get_controls(): + if control.hint & g15driver.HINT_DIMMABLE != 0 or control.hint & g15driver.HINT_SHADEABLE != 0: + self._controls.append(control) + self._dbus_name = "org.gnome.ScreenSaver" + self._dbus_interface = "org.gnome.ScreenSaver" + self._in_screen_saver = False + + if self._session_bus == None: + screen_saver = None + try: + self._session_bus = dbus.SessionBus() + except Exception as e: + self._session_bus = None + logger.error("Error. Retrying in 10 seconds", exc_info = e) + Timer(10, self.activate, ()).start() + return + + # Paths vary from desktop to desktop + screensavers = [ + ("org.gnome.ScreenSaver", "org.gnome.ScreenSaver", "/"), + ("org.gnome.ScreenSaver", "org.gnome.ScreenSaver", "/org/gnome/ScreenSaver"), + ("org.kde.screensaver", "org.freedesktop.ScreenSaver", "/ScreenSaver"), + ("org.mate.ScreenSaver", "org.mate.ScreenSaver", "/"), + ] + + for dbus_name, interface, path in screensavers: + try : + logger.debug("Searching for screensaver. " \ + "dbus_name: %s, dbus_interface: %s, dbus_object: %s", + dbus_name, + interface, + path) + screen_saver = dbus.Interface(self._session_bus.get_object(dbus_name, path), interface) + self._dbus_interface = interface + self._dbus_name = dbus_name + self._session_bus.add_signal_receiver(self._screensaver_changed_handler, dbus_interface = self._dbus_interface, signal_name = "ActiveChanged") + self._in_screensaver = screen_saver.GetActive() + break + except Exception as e: + logger.debug("Could not find screensaver", exc_info = e) + screen_saver = None + pass + + if screen_saver is None: + raise Exception("No supported DBUS screen saver interface found.") + + self._activated = True + self._check_page() + + def deactivate(self): + if self._in_screensaver: + if self._gconf_client.get_bool(self._gconf_key + "/dim_keyboard"): + self._light_keyboard() + self._remove_page() + self._activated = False + + def destroy(self): + if self._session_bus: + self._session_bus.remove_signal_receiver(self._screensaver_changed_handler, dbus_interface = self._dbus_interface, signal_name = "ActiveChanged") + + def handle_key(self, keys, state, post): + # Sinks all keyboard events when the page is active + return self._page is not None + + ''' Functions specific to plugin + ''' + + def _remove_page(self): + if self._page != None: + self._screen.del_page(self._page) + self._page = None + + def _check_page(self): + if self._in_screensaver: + if self._screen.driver.get_bpp() != 0 and self._page == None: + self._reload_theme() + self._page = g15theme.G15Page(id, self._screen, priority = g15screen.PRI_EXCLUSIVE, \ + title = name, theme = self._theme, + theme_properties_callback = self._get_theme_properties, + originating_plugin = self) + self._page.key_handlers.append(self) + self._screen.add_page(self._page) + self._screen.redraw(self._page) + if not self.dimmed and g15gconf.get_bool_or_default(self._gconf_client, "%s/dim_keyboard" % self._gconf_key, True): + self._dim_keyboard() + else: + if self._screen.driver.get_bpp() != 0: + self._remove_page() + if self.dimmed and g15gconf.get_bool_or_default(self._gconf_client,"%s/dim_keyboard" % self._gconf_key, True): + self._light_keyboard() + + def _screensaver_changed_handler(self, value): + if self._activated: + self._in_screensaver = bool(value) + self._check_page() + + def _dim_keyboard(self): + self._acquisitions = [] + for c in self._controls: + acquisition = self._screen.driver.acquire_control(c, val = c.value) + self._acquisitions.append(acquisition) + acquisition.fade(100.0 if c.hint & g15driver.HINT_DIMMABLE != 0 else 0.5, 3.0) + self.dimmed = True + + def _light_keyboard(self): + for c in self._acquisitions: + self._screen.driver.release_control(c) + self.dimmed = False + + def _reload_theme(self): + text = self._gconf_client.get_string(self._gconf_key + "/message_text") + variant = "" + if text == None or text == "": + variant = "nobody" + self._theme = g15theme.G15Theme(self, variant) + + def _get_theme_properties(self): + + properties = {} + properties["title"] = _("Workstation Locked") + properties["body"] = self._gconf_client.get_string(self._gconf_key + "/message_text") + properties["icon"] = g15icontools.get_icon_path("sleep", self._screen.height) + + return properties diff --git a/src/plugins/screensaver/screensaver.ui b/src/plugins/screensaver/screensaver.ui new file mode 100644 index 0000000..ec9097f --- /dev/null +++ b/src/plugins/screensaver/screensaver.ui @@ -0,0 +1,153 @@ + + + + + + 100 + 1 + 10 + + + + 320 + False + 5 + Screensaver Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + Dim Controls + True + True + False + True + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + True + automatic + automatic + in + + + 60 + True + True + 1 + word-char + TextBuffer + + + + + + + + + True + False + <b>Message</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/sense/Makefile.am b/src/plugins/sense/Makefile.am new file mode 100644 index 0000000..1cab369 --- /dev/null +++ b/src/plugins/sense/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/sense +plugin_DATA = sense.py \ + sense.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/sense/default/Makefile.am b/src/plugins/sense/default/Makefile.am new file mode 100644 index 0000000..c59c59f --- /dev/null +++ b/src/plugins/sense/default/Makefile.am @@ -0,0 +1,14 @@ +themedir = $(datadir)/gnome15/plugins/sense/default +theme_DATA = g19.svg \ + g19-menu-entry.svg \ + g19-fan.svg \ + g19-volt.svg \ + g19-none.svg \ + default.svg \ + default-none.svg \ + default-fan.svg \ + default-volt.svg \ + default-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/sense/default/default-fan.svg b/src/plugins/sense/default/default-fan.svg new file mode 100644 index 0000000..63dc829 --- /dev/null +++ b/src/plugins/sense/default/default-fan.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${rpm} RPM + + + + + + + ${sensor} + + + + 2000 + 3500 + 5000 + 7000 + 0 + + + diff --git a/src/plugins/sense/default/default-menu-entry.svg b/src/plugins/sense/default/default-menu-entry.svg new file mode 100644 index 0000000..7b6d7e9 --- /dev/null +++ b/src/plugins/sense/default/default-menu-entry.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + + + + + ${item_alt} + + + + ${item_name} + + + + + ${item_alt} + + diff --git a/src/plugins/sense/default/default-none.svg b/src/plugins/sense/default/default-none.svg new file mode 100644 index 0000000..acd2a08 --- /dev/null +++ b/src/plugins/sense/default/default-none.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + _(No sensors enabled) + + diff --git a/src/plugins/sense/default/default-volt.svg b/src/plugins/sense/default/default-volt.svg new file mode 100644 index 0000000..c00ab08 --- /dev/null +++ b/src/plugins/sense/default/default-volt.svg @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${voltage} V + + + + + + + ${sensor} + + + + 4 + 7 + 12 + 14 + 0 + + diff --git a/src/plugins/sense/default/default.svg b/src/plugins/sense/default/default.svg new file mode 100644 index 0000000..3e1ac38 --- /dev/null +++ b/src/plugins/sense/default/default.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp_c} + + + + + + + ${sensor} + + + + 40 + 70 + 110 + 140 + 0 + + diff --git a/src/plugins/sense/default/g19-fan.svg b/src/plugins/sense/default/g19-fan.svg new file mode 100644 index 0000000..e6113c7 --- /dev/null +++ b/src/plugins/sense/default/g19-fan.svg @@ -0,0 +1,1268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 1000 + 2000 + 3000 + + 4000 + 5000 + 6000 + 7000 + ${sensor} + ${rpm} RPM + + + + + + + + + + diff --git a/src/plugins/sense/default/g19-menu-entry.svg b/src/plugins/sense/default/g19-menu-entry.svg new file mode 100644 index 0000000..3f1a326 --- /dev/null +++ b/src/plugins/sense/default/g19-menu-entry.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + ${item_alt2} + Max + + + diff --git a/src/plugins/sense/default/g19-none.svg b/src/plugins/sense/default/g19-none.svg new file mode 100644 index 0000000..6e36f24 --- /dev/null +++ b/src/plugins/sense/default/g19-none.svg @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + _(No sensors enabled) + + diff --git a/src/plugins/sense/default/g19-volt.svg b/src/plugins/sense/default/g19-volt.svg new file mode 100644 index 0000000..97c4167 --- /dev/null +++ b/src/plugins/sense/default/g19-volt.svg @@ -0,0 +1,1269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 2 + 4 + 6 + + 8 + 10 + 12 + 14 + ${sensor} + ${voltage} V + + + + + + + + + + diff --git a/src/plugins/sense/default/g19.svg b/src/plugins/sense/default/g19.svg new file mode 100644 index 0000000..01395b6 --- /dev/null +++ b/src/plugins/sense/default/g19.svg @@ -0,0 +1,1358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + 0 + 20 + 30 + 40 + 50 + 60 + 70 + + 80 + 90 + 100 + 110 + 120 + 130 + 140 + ${sensor} + ${temp_c} + + + + + + + + + + diff --git a/src/plugins/sense/default/i18n/default-none.en_GB.po b/src/plugins/sense/default/i18n/default-none.en_GB.po new file mode 100644 index 0000000..e782e4e --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/default-none.h:1 +msgid "No sensors enabled" +msgstr "No sensors enabled" diff --git a/src/plugins/sense/default/i18n/default-none.h b/src/plugins/sense/default/i18n/default-none.h new file mode 100644 index 0000000..e6440f7 --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.h @@ -0,0 +1 @@ +char *s = N_("No sensors enabled"); diff --git a/src/plugins/sense/default/i18n/default-none.pot b/src/plugins/sense/default/i18n/default-none.pot new file mode 100644 index 0000000..f9a507d --- /dev/null +++ b/src/plugins/sense/default/i18n/default-none.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/default-none.h:1 +msgid "No sensors enabled" +msgstr "" diff --git a/src/plugins/sense/default/i18n/g19-none.en_GB.po b/src/plugins/sense/default/i18n/g19-none.en_GB.po new file mode 100644 index 0000000..982148c --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19-none.h:1 +msgid "No sensors enabled" +msgstr "No sensors enabled" diff --git a/src/plugins/sense/default/i18n/g19-none.h b/src/plugins/sense/default/i18n/g19-none.h new file mode 100644 index 0000000..e6440f7 --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.h @@ -0,0 +1 @@ +char *s = N_("No sensors enabled"); diff --git a/src/plugins/sense/default/i18n/g19-none.pot b/src/plugins/sense/default/i18n/g19-none.pot new file mode 100644 index 0000000..eb4b48b --- /dev/null +++ b/src/plugins/sense/default/i18n/g19-none.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19-none.h:1 +msgid "No sensors enabled" +msgstr "" diff --git a/src/plugins/sense/i18n/sense.en_GB.po b/src/plugins/sense/i18n/sense.en_GB.po new file mode 100644 index 0000000..a466e01 --- /dev/null +++ b/src/plugins/sense/i18n/sense.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/sense.glade.h:1 +msgid "Options" +msgstr "Options" + +#: i18n/sense.glade.h:2 +msgid "Enabled" +msgstr "Enabled" + +#: i18n/sense.glade.h:3 +msgid "Label" +msgstr "Label" + +#: i18n/sense.glade.h:4 +msgid "Refresh interval" +msgstr "Refresh interval" + +#: i18n/sense.glade.h:5 +msgid "Sensors Preferences" +msgstr "Sensors Preferences" + +#: i18n/sense.glade.h:6 +msgid "Type" +msgstr "Type" + +#: i18n/sense.glade.h:7 +msgid "seconds" +msgstr "seconds" diff --git a/src/plugins/sense/i18n/sense.glade.h b/src/plugins/sense/i18n/sense.glade.h new file mode 100644 index 0000000..9b300f5 --- /dev/null +++ b/src/plugins/sense/i18n/sense.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Options"); +char *s = N_("Enabled"); +char *s = N_("Label"); +char *s = N_("Refresh interval"); +char *s = N_("Sensors Preferences"); +char *s = N_("Type"); +char *s = N_("seconds"); diff --git a/src/plugins/sense/i18n/sense.pot b/src/plugins/sense/i18n/sense.pot new file mode 100644 index 0000000..45eee9d --- /dev/null +++ b/src/plugins/sense/i18n/sense.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/sense.glade.h:1 +msgid "Options" +msgstr "" + +#: i18n/sense.glade.h:2 +msgid "Enabled" +msgstr "" + +#: i18n/sense.glade.h:3 +msgid "Label" +msgstr "" + +#: i18n/sense.glade.h:4 +msgid "Refresh interval" +msgstr "" + +#: i18n/sense.glade.h:5 +msgid "Sensors Preferences" +msgstr "" + +#: i18n/sense.glade.h:6 +msgid "Type" +msgstr "" + +#: i18n/sense.glade.h:7 +msgid "seconds" +msgstr "" diff --git a/src/plugins/sense/sense.py b/src/plugins/sense/sense.py new file mode 100644 index 0000000..6194ae2 --- /dev/null +++ b/src/plugins/sense/sense.py @@ -0,0 +1,566 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("sensors", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15theme as g15theme +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15svg as g15svg +import os.path +import dbus +import sensors +import gtk +import gconf +import gobject + +import subprocess +from threading import Lock + +# Logging +import logging +logger = logging.getLogger(__name__) + +id = "sense" +name = _("Sensors") +description = _("Display information from various sensors. The plugin \ +supports Temperatures, Fans and Voltages from various sources. \ +\n\n\ +Sources include libsensors, nvidiactl and UDisks.\ +\n\n\ +NOTE: UDisk may cause a delay in starting up Gnome15. This bug is\ +being investigated.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.gnome15.org" +has_preferences = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous sensor"), + g15driver.NEXT_SELECTION : _("Next sensor"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page") + } + +UDISKS_DEVICE_NAME = "org.freedesktop.UDisks.Device" +UDISKS_BUS_NAME= "org.freedesktop.UDisks" +UDISKS2_BUS_NAME= "org.freedesktop.UDisks2" + +''' +Sensor types +''' +VOLTAGE = 0 +TEMPERATURE = 2 +FAN = 1 +UNKNOWN_1 = 3 +INTRUSION = 17 + +TYPE_NAMES = { VOLTAGE: "Voltage", FAN: "Fan", TEMPERATURE : "Temp" } +VARIANT_NAMES = { VOLTAGE : "volt", TEMPERATURE : None, FAN : "fan", UNKNOWN_1 : "volt", INTRUSION: "intrusion" } + +''' +This plugin displays sensor information +''' + +def create(gconf_key, gconf_client, screen): + return G15Sensors(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15SensorsPreferences(parent, driver, gconf_client, gconf_key) + +def get_sensor_sources(): + available_sensor_sources = [[LibsensorsSource()], \ + [NvidiaSource()], \ + [UDisks2Source(), UDisksSource()]] + sensor_sources = [] + for sensor_source_group in available_sensor_sources: + for candidade in sensor_source_group: + logger.info("Testing if '%s' is a valid sensor source", candidade.name) + try: + if candidade.is_valid(): + logger.info("Adding '%s' as a sensor source", candidade.name) + sensor_sources.append(candidade) + else: + candidade.stop() + break + except Exception as e: + logger.debug("Error when checking '%s'. Skipping", candidate.name, exc_info = e) + pass + + return sensor_sources + +class G15SensorsPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "sense.ui")) + + # Feeds + self.sensor_model = widget_tree.get_object("SensorModel") + self.reload_model() + self.sensor_list = widget_tree.get_object("SensorList") + self.enabled_renderer = widget_tree.get_object("EnabledRenderer") + self.label_renderer = widget_tree.get_object("LabelRenderer") + + # Lines + self.interval_adjustment = widget_tree.get_object("IntervalAdjustment") + self.interval_adjustment.set_value(g15gconf.get_float_or_default(self._gconf_client, "%s/interval" % self._gconf_key, 10)) + + # Connect to events + self.interval_adjustment.connect("value-changed", self.interval_changed) + self.label_renderer.connect("edited", self.label_edited) + self.enabled_renderer.connect("toggled", self.sensor_toggled) + + # Show dialog + self.dialog = widget_tree.get_object("SenseDialog") + self.dialog.set_transient_for(parent) + + self.dialog.run() + self.dialog.hide() + + def interval_changed(self, widget): + self._gconf_client.set_float(self._gconf_key + "/interval", int(widget.get_value())) + + def label_edited(self, widget, row_index, value): + row_index = int(row_index) + if value != "": + if self.sensor_model[row_index][2] != value: + self.sensor_model.set_value(self.sensor_model.get_iter(row_index), 2, value) + sensor_name = self.sensor_model[row_index][0] + self._gconf_client.set_string("%s/sensors/%s/label" % (self._gconf_key, gconf.escape_key(sensor_name, len(sensor_name))), value) + + def sensor_toggled(self, widget, row_index): + row_index = int(row_index) + now_active = not widget.get_active() + self.sensor_model.set_value(self.sensor_model.get_iter(row_index), 1, now_active) + sensor_name = self.sensor_model[row_index][0] + self._gconf_client.set_bool("%s/sensors/%s/enabled" % (self._gconf_key, gconf.escape_key(sensor_name, len(sensor_name))), now_active) + + def reload_model(self): + self.sensor_model.clear() + ss = get_sensor_sources() + for source in ss: + sa = source.get_sensors() + for sensor in sa: + sense_key = "%s/sensors/%s" % (self._gconf_key, gconf.escape_key(sensor.name, len(sensor.name))) + if sensor.sense_type in TYPE_NAMES: + self.sensor_model.append([ sensor.name, g15gconf.get_bool_or_default(self._gconf_client, "%s/enabled" % (sense_key), True), + g15gconf.get_string_or_default(self._gconf_client, "%s/label" % (sense_key), sensor.name), TYPE_NAMES[sensor.sense_type] ]) + source.stop() + + +class Sensor(): + + def __init__(self, sense_type, name, value, critical = None): + self.sense_type = sense_type + self.name = name + self.value = value + self.critical = critical + + def get_default_crit(self): + # Meaningless really, but more sensible than a single value + + if self.sense_type == FAN: + return 7000 + elif self.sense_type == VOLTAGE: + return 12 + else: + return 100 + + +class UDisksSource(): + + def __init__(self): + self.name = "UDisks" + self.udisks = None + self.system_bus = None + self.sensors = {} + self.lock = Lock() + + def get_sensors(self): + self.sensors = {} + for device in self.udisks.EnumerateDevices(): + udisk_object = self.system_bus.get_object(UDISKS_BUS_NAME, device) + udisk_properties = dbus.Interface(udisk_object, 'org.freedesktop.DBus.Properties') + + if udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartIsAvailable"): + sensor_name = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveModel") + if sensor_name in self.sensors: + # TODO get something else unique? + n = udisk_properties.Get(UDISKS_DEVICE_NAME, "DeviceFile") + if n: + sensor_name += " (%s)" % n + else: + n = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveSerial") + if n: + sensor_name += " (%s)" % n + sensor = Sensor(TEMPERATURE, sensor_name, 0.0) + device_file = str(udisk_properties.Get(UDISKS_DEVICE_NAME, "DeviceFile")) + if int(udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartTimeCollected")) > 0: + # Only get the temperature if SMART data is collected to avoid spinning up disk + smart_blob = udisk_properties.Get(UDISKS_DEVICE_NAME, "DriveAtaSmartBlob", byte_arrays=True) + smart_blob_str = str(smart_blob) + process = subprocess.Popen(['skdump', '--temperature', '--load=-'], shell = False, stdout = subprocess.PIPE, stdin = subprocess.PIPE) + result, stderrdata = process.communicate(smart_blob_str) + process.wait() + if len(result) > 0: + try: + kelvin = int(result) + kelvin /= 1000; + temp_c = kelvin - 273.15 + sensor.value = temp_c + except ValueError as ve: + logger.warning("Invalid temperature for device %s, %s.", + sensor_name, + result, + exc_info = ve) + sensor.value = 0 + else: + sensor.value = 0 + + self.sensors[sensor.name] = sensor + return self.sensors.values() + + def is_valid(self): + if self.udisks == None: + self.system_bus = dbus.SystemBus() + udisks_object = self.system_bus.get_object(UDISKS_BUS_NAME, '/org/freedesktop/UDisks') + # Easier way found to ensure that we can communicate with udisks + properties = dbus.Interface(udisks_object, 'org.freedesktop.DBus.Properties') + properties.Get(UDISKS_BUS_NAME, 'DaemonVersion') + self.udisks = dbus.Interface(udisks_object, UDISKS_BUS_NAME) + + return self.udisks is not None + + def stop(self): + pass + +class UDisks2Source(): + + def __init__(self): + self.name = "UDisks2" + + self.udisks = None + self.system_bus = None + self.udisks_data = None + self.sensors = {} + self.lock = Lock() + + def get_sensors(self): + def is_a_drive(device_path): + return device_path.find('/org/freedesktop/UDisks2/drives/') != -1 + def is_a_ata_drive_supporting_SMART(drive): + if 'org.freedesktop.UDisks2.Drive.Ata' in drive: + return drive['org.freedesktop.UDisks2.Drive.Ata']['SmartSupported'] + else: + return False + def find_valid_sensor_name(drive): + model = drive['org.freedesktop.UDisks2.Drive']['Model'] + serial = drive['org.freedesktop.UDisks2.Drive']['Serial'] + if not model in self.sensors: + return model + else: + return "(%s) (%s)" % (model, serial) + def kelvin_to_celsius(kelvin): + return kelvin - 273.15 + def drive_temperature(drive): + if drive['org.freedesktop.UDisks2.Drive.Ata']['SmartUpdated'] > 0: + return kelvin_to_celsius(drive['org.freedesktop.UDisks2.Drive.Ata']['SmartTemperature']) + else: + return 0 + + logger.debug("Refreshing disk drives temperatures") + self.sensors = {} + self.udisks_data = self.udisks.GetManagedObjects() + for device in self.udisks_data: + if not is_a_drive(device): + continue + if not is_a_ata_drive_supporting_SMART(self.udisks_data[device]): + logger.debug('SMART is disabled or unsupported by drive %s.', device) + continue + sensor_name = find_valid_sensor_name(self.udisks_data[device]) + logger.debug('Found sensor %s for drive %s.', sensor_name, device) + sensor = Sensor(TEMPERATURE, sensor_name, 0.0) + try: + sensor.value = drive_temperature(self.udisks_data[device]) + logger.debug('Temperature of drive %s is %f.', sensor_name,sensor.value) + except ValueError as ve: + logger.warning("Invalid temperature for device %s.", sensor_name, exc_info = ve) + sensor.value = 0 + self.sensors[sensor.name] = sensor + + return self.sensors.values() + + def is_valid(self): + if self.udisks == None: + self.system_bus = dbus.SystemBus() + udisks_object = self.system_bus.get_object(UDISKS2_BUS_NAME, '/org/freedesktop/UDisks2') + self.udisks = dbus.Interface(udisks_object, 'org.freedesktop.DBus.ObjectManager') + dbus_peer = dbus.Interface(udisks_object, 'org.freedesktop.DBus.Peer') + dbus_peer.Ping() + + return self.udisks is not None + + def stop(self): + pass + +class LibsensorsSource(): + def __init__(self): + self.name = "Libsensors" + self.started = False + + def get_sensors(self): + sensor_objects = [] + sensor_names = [] + for chip in sensors.iter_detected_chips(): + logger.debug("Found chip %s, adapter %s", chip, chip.adapter_name) + for feature in chip: + sensor_name = feature.label + + # Prevent name conflicts across chips + if not sensor_name in sensor_names: + sensor_names.append(sensor_name) + else: + o = sensor_name + idx = 1 + while sensor_name in sensor_names: + idx += 1 + sensor_name = "%s-%d" % (o, idx) + sensor_names.append(sensor_name) + + logger.debug("' %s: %.2f", sensor_name, feature.get_value()) + sensor = Sensor(feature.type, sensor_name, float(feature.get_value())) + sensor_objects.append(sensor) + + for subfeature in feature: + name = subfeature.name + if name.endswith("_crit"): + sensor.critical = float(subfeature.get_value()) + elif name.endswith("_input"): + sensor.value = float(subfeature.get_value()) + return sensor_objects + + def is_valid(self): + if not self.started: + sensors.init() + self.started = True + return self.started + + def stop(self): + if self.started: + sensors.cleanup() + +class NvidiaSource(): + def __init__(self): + self.name = "NVidia" + + def get_sensors(self): + status, value = self.getstatusoutput("nvidia-settings -q GPUCoreTemp -t") + return [Sensor(TEMPERATURE, "GPUCoreTemp", int(value.split('\n')[0]))] + + def getstatusoutput(self, cmd): + pipe = os.popen('{ ' + cmd + '; } 2>/dev/null', 'r') + text = pipe.read() + sts = pipe.close() + if sts is None: sts = 0 + if text[-1:] == '\n': text = text[:-1] + return sts, text + + def is_valid(self): + if not os.path.exists("/dev/nvidiactl"): + return False + if not os.access("/dev/nvidiactl", os.R_OK): + logger.warning("/dev/nvidiactl exists, but it is not readable by the current user, skipping sensor source.") + return False + return True + + def stop(self): + pass + +class SensorMenuItem(g15theme.MenuItem): + + def __init__(self, item_id, sensor, sensor_label): + g15theme.MenuItem.__init__(self, item_id) + self.sensor = sensor + self.sensor_label = sensor_label + + def get_theme_properties(self): + properties = g15theme.MenuItem.get_theme_properties(self) + properties["item_name"] = self.sensor_label + properties["item_alt"] = self._format_value(self.sensor.value) + properties["item_alt2"] = self._format_value(self.sensor.critical) if self.sensor.critical is not None else "" + max_val = self.sensor.critical if self.sensor.critical is not None else self.sensor.get_default_crit() + properties["temp_percent"] = ( self.sensor.value / max_val ) * 100.0 + return properties + + def _format_value(self, val): + return "%.2f" % val if val < 1000 else "%4d" % int(val) + +class G15Sensors(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, [ "system", "applications-system" ], id, name, 5.0) + self.schedule_on_gobject = True + self._sensors_changed_handle = None + self._menu = None + + def activate(self): + gobject.idle_add(self._do_activate) + + def _do_activate(self): + self._sensors_changed_handle = self.gconf_client.notify_add(self.gconf_key + "/sensors", self._sensors_changed) + self.sensor_sources = get_sensor_sources() + self.sensor_dict = {} + g15plugin.G15RefreshingPlugin.activate(self) + + def populate_page(self): + self._menu = g15theme.Menu("menu") + g15plugin.G15RefreshingPlugin.populate_page(self) + + enabled_sensors = [] + for c in self.sensor_sources: + for s in c.get_sensors(): + sense_key = "%s/sensors/%s" % (self.gconf_key, gconf.escape_key(s.name, len(s.name))) + if g15gconf.get_bool_or_default(self.gconf_client, "%s/enabled" % (sense_key), True): + enabled_sensors.append(s) + + + # If there are no sensors enabled, display the 'none' variant + # which shows a message + if len(enabled_sensors) == 0: + self.page.theme.set_variant("none") + else: + self.page.theme.set_variant(None) + def menu_selected(): + self.page.theme.set_variant(VARIANT_NAMES[self._menu.selected.sensor.sense_type]) + + self._menu.on_selected = menu_selected + self.page.add_child(self._menu) + self.page.theme.svg_processor = self._process_svg + self.page.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + i = 0 + for s in enabled_sensors: + if s.sense_type in TYPE_NAMES: + sense_key = "%s/sensors/%s" % (self.gconf_key, gconf.escape_key(s.name, len(s.name))) + sense_label = g15gconf.get_string_or_default(self.gconf_client, "%s/label" % (sense_key), s.name) + menu_item = SensorMenuItem("menuitem-%d" % i, s, sense_label) + self.sensor_dict[s.name] = menu_item + self._menu.add_child(menu_item) + + # If this is the first child, change the theme variant + if self._menu.get_child_count() == 1: + self.page.theme.set_variant(VARIANT_NAMES[menu_item.sensor.sense_type]) + + i += 1 + + + def deactivate(self): + for c in self.sensor_sources: + c.stop() + g15plugin.G15RefreshingPlugin.deactivate(self) + if self._sensors_changed_handle is not None: + self.gconf_client.notify_remove(self._sensors_changed_handle) + + def refresh(self): + self._get_stats() + + def get_next_tick(self): + return g15gconf.get_float_or_default(self.gconf_client, "%s/interval" % self.gconf_key, 5.0) + + ''' Private + ''' + + def _sensors_changed(self, client, connection_id, entry, args): + self.page.remove_all_children() + self.populate_page() + self.refresh() + + def _process_svg(self, document, properties, attributes): + root = document.getroot() + if self._menu.selected is not None: + needle = self.page.theme.get_element("needle", root = root) + needle_center = self.page.theme.get_element("needle_center", root = root) + val = float(self._menu.selected.sensor.value) + + """ + The title contains the bounds for the gauge, in the format + lower_val,upper_val,middle_val,lower_deg,upper_deg + """ + gauge_data = needle_center.get("title").split(",") + lower_val = float(gauge_data[0]) + upper_val = float(gauge_data[1]) + middle_val = float(gauge_data[2]) + lower_deg = float(gauge_data[3]) + upper_deg = float(gauge_data[4]) + + # Clamp the value + val = min(upper_val, max(lower_val, val)) + + # Ratio of gauge bounds to rotate by + ratio = val / ( upper_val - lower_val ) + + """ + Work out total number of degrees in the bounds + """ + total_deg = upper_deg + ( 360 - lower_deg ) + + + # Work out total number of degress to rotate + rot_degrees = total_deg * ratio + + # + degr = lower_deg + degr += rot_degrees + + """ + This is a bit weak. It doesn't take transformations into account, + so care is needed in the SVG. + """ + center_bounds = g15svg.get_bounds(needle_center) + needle.set("transform", "rotate(%f,%f,%f)" % (degr, center_bounds[0], center_bounds[1]) ) + + def _get_stats(self): + for c in self.sensor_sources: + for s in c.get_sensors(): + if s.name in self.sensor_dict: + self.sensor_dict[s.name].sensor = s + if s.critical is not None: + logger.debug("Sensor %s on %s is %f (critical %f)", + s.name, + c.name, + s.value, + s.critical) + else: + logger.debug("Sensor %s on %s is %f", s.name, c.name, s.value) + + def get_theme_properties(self): + properties = g15plugin.G15RefreshingPlugin.get_theme_properties(self) + if self._menu.selected is not None: + properties["sensor"] = self._menu.selected.sensor.name + if self._menu.selected.sensor.sense_type == FAN: + properties["rpm"] = "%4d" % float(self._menu.selected.sensor.value) + elif self._menu.selected.sensor.sense_type == VOLTAGE: + properties["voltage"] = "%.2f" % float(self._menu.selected.sensor.value) + else: + properties["temp_c"] = "%.2f C" % float(self._menu.selected.sensor.value) + return properties diff --git a/src/plugins/sense/sense.ui b/src/plugins/sense/sense.ui new file mode 100644 index 0000000..d97abe6 --- /dev/null +++ b/src/plugins/sense/sense.ui @@ -0,0 +1,217 @@ + + + + + + 9999 + 1 + 10 + + + + + + + + + + + + + + + 320 + 400 + False + 5 + Sensors Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + automatic + automatic + + + True + True + SensorModel + False + 0 + + + Enabled + + + + 1 + + + + + + + fixed + 48 + Type + + + + 3 + + + + + + + Label + + + True + + + 2 + + + + + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + 0 + Refresh interval + + + True + True + 0 + + + + + True + True + + False + False + True + True + IntervalAdjustment + + + False + True + 1 + + + + + True + False + seconds + + + False + True + 8 + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + False + True + 1 + + + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/stopwatch/Makefile.am b/src/plugins/stopwatch/Makefile.am new file mode 100644 index 0000000..8efafd5 --- /dev/null +++ b/src/plugins/stopwatch/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/stopwatch +plugin_DATA = \ + stopwatch.ui \ + stopwatch.py \ + timer.py \ + preferences.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/stopwatch/default/Makefile.am b/src/plugins/stopwatch/default/Makefile.am new file mode 100644 index 0000000..64066a9 --- /dev/null +++ b/src/plugins/stopwatch/default/Makefile.am @@ -0,0 +1,36 @@ +themedir = $(datadir)/gnome15/plugins/stopwatch/default +theme_DATA = \ + g19.svg \ + g19-one_timer.svg \ + g19-two_timers.svg \ + up.gif \ + playpause.gif \ + reset.gif \ + mx5500.svg \ + mx5500-one_timer.svg \ + mx5500-two_timers.svg \ + default.svg \ + default-one_timer.svg \ + default-two_timers.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/stopwatch/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/stopwatch/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/stopwatch/default/default-one_timer.svg b/src/plugins/stopwatch/default/default-one_timer.svg new file mode 100644 index 0000000..b1273d5 --- /dev/null +++ b/src/plugins/stopwatch/default/default-one_timer.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer_label} + ${timer} + L3 + L4 + + + + + + + diff --git a/src/plugins/stopwatch/default/default-two_timers.svg b/src/plugins/stopwatch/default/default-two_timers.svg new file mode 100644 index 0000000..cccbb0e --- /dev/null +++ b/src/plugins/stopwatch/default/default-two_timers.svg @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${timer1_label} + ${timer1} + L3 + L4 + ${timer2} + L2 + A/B + ${timer2_label} + + + + + + + diff --git a/src/plugins/stopwatch/default/default.svg b/src/plugins/stopwatch/default/default.svg new file mode 100644 index 0000000..67e324d --- /dev/null +++ b/src/plugins/stopwatch/default/default.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled) + + diff --git a/src/plugins/stopwatch/default/g19-one_timer.svg b/src/plugins/stopwatch/default/g19-one_timer.svg new file mode 100644 index 0000000..99e8d0c --- /dev/null +++ b/src/plugins/stopwatch/default/g19-one_timer.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${timer} + ${timer_label} + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/stopwatch/default/g19-two_timers.svg b/src/plugins/stopwatch/default/g19-two_timers.svg new file mode 100644 index 0000000..2824164 --- /dev/null +++ b/src/plugins/stopwatch/default/g19-two_timers.svg @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${timer1} + ${timer1_label} + ${timer2} + ${timer2_label} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/stopwatch/default/g19.svg b/src/plugins/stopwatch/default/g19.svg new file mode 100644 index 0000000..75538ae --- /dev/null +++ b/src/plugins/stopwatch/default/g19.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled.) + + diff --git a/src/plugins/stopwatch/default/i18n/default.en_GB.po b/src/plugins/stopwatch/default/i18n/default.en_GB.po new file mode 100644 index 0000000..3f6003f --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/default.h:1 +msgid "No timer enabled" +msgstr "No timer enabled" diff --git a/src/plugins/stopwatch/default/i18n/default.h b/src/plugins/stopwatch/default/i18n/default.h new file mode 100644 index 0000000..93cf7c2 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled"); diff --git a/src/plugins/stopwatch/default/i18n/default.pot b/src/plugins/stopwatch/default/i18n/default.pot new file mode 100644 index 0000000..18fbd05 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/default.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/default.h:1 +msgid "No timer enabled" +msgstr "" diff --git a/src/plugins/stopwatch/default/i18n/g19.en_GB.po b/src/plugins/stopwatch/default/i18n/g19.en_GB.po new file mode 100644 index 0000000..9751407 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19.h:1 +msgid "No timer enabled." +msgstr "No timer enabled." diff --git a/src/plugins/stopwatch/default/i18n/g19.h b/src/plugins/stopwatch/default/i18n/g19.h new file mode 100644 index 0000000..aaa6c03 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled."); diff --git a/src/plugins/stopwatch/default/i18n/g19.pot b/src/plugins/stopwatch/default/i18n/g19.pot new file mode 100644 index 0000000..4756340 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19.h:1 +msgid "No timer enabled." +msgstr "" diff --git a/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po b/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po new file mode 100644 index 0000000..9dfef42 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/mx5500.h:1 +msgid "No timer enabled" +msgstr "No timer enabled" diff --git a/src/plugins/stopwatch/default/i18n/mx5500.h b/src/plugins/stopwatch/default/i18n/mx5500.h new file mode 100644 index 0000000..93cf7c2 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.h @@ -0,0 +1 @@ +char *s = N_("No timer enabled"); diff --git a/src/plugins/stopwatch/default/i18n/mx5500.pot b/src/plugins/stopwatch/default/i18n/mx5500.pot new file mode 100644 index 0000000..1623df7 --- /dev/null +++ b/src/plugins/stopwatch/default/i18n/mx5500.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/mx5500.h:1 +msgid "No timer enabled" +msgstr "" diff --git a/src/plugins/stopwatch/default/mx5500-one_timer.svg b/src/plugins/stopwatch/default/mx5500-one_timer.svg new file mode 100644 index 0000000..2bef5c0 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500-one_timer.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer_label} + ${timer} + Go/Stop + + + Reset + + diff --git a/src/plugins/stopwatch/default/mx5500-two_timers.svg b/src/plugins/stopwatch/default/mx5500-two_timers.svg new file mode 100644 index 0000000..fde9953 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500-two_timers.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${timer1_label} + ${timer1} + Go/Stop + Reset + ${timer2_label} + ${timer2} + + + + + A/B + + diff --git a/src/plugins/stopwatch/default/mx5500.svg b/src/plugins/stopwatch/default/mx5500.svg new file mode 100644 index 0000000..4d0e190 --- /dev/null +++ b/src/plugins/stopwatch/default/mx5500.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + _(No timer enabled) + + diff --git a/src/plugins/stopwatch/default/playpause.gif b/src/plugins/stopwatch/default/playpause.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ae34448c4df93ce4031c0b813f395f6003ac65b GIT binary patch literal 82 zcmZ?wbhEHbOM1^`aD7K;D? literal 0 HcmV?d00001 diff --git a/src/plugins/stopwatch/default/reset.gif b/src/plugins/stopwatch/default/reset.gif new file mode 100644 index 0000000000000000000000000000000000000000..800acad002a967c59114ceaefded261a3a445b64 GIT binary patch literal 77 zcmZ?wbhEHbWM^P!XkcUjg0~DkivL8Ni&7IyQd1PlGfOfQLNZbn+&z5*7!-f9Fmf?4 bGU$L5g0wI&akDffwx6zhP~3TqiNP8G*(4Jl literal 0 HcmV?d00001 diff --git a/src/plugins/stopwatch/default/up.gif b/src/plugins/stopwatch/default/up.gif new file mode 100644 index 0000000000000000000000000000000000000000..362b68096b256a89f7ad9a9cc9d5f50e7d225e25 GIT binary patch literal 71 zcmZ?wbhEHbWMyDxXkcUjg0~DkivL8Ni&7IyQd1PlGfOfQLNZbn+&z5*7!-f9Fmf?4 VGU$L5g0wI&vHB#i?}%lv1^{In4`~1Z literal 0 HcmV?d00001 diff --git a/src/plugins/stopwatch/i18n/stopwatch.en_GB.po b/src/plugins/stopwatch/i18n/stopwatch.en_GB.po new file mode 100644 index 0000000..61da091 --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.en_GB.po @@ -0,0 +1,62 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/stopwatch.glade.h:1 +msgid "Mode" +msgstr "Mode" + +#: i18n/stopwatch.glade.h:2 +msgid "Time (hours:minutes:seconds)" +msgstr "Time (hours:minutes:seconds)" + +#: i18n/stopwatch.glade.h:3 +msgid "Countdown" +msgstr "Countdown" + +#: i18n/stopwatch.glade.h:4 +msgid "Enabled" +msgstr "Enabled" + +#: i18n/stopwatch.glade.h:5 +msgid "Keep stopwatch visible while actve" +msgstr "Keep stopwatch visible while actve" + +#: i18n/stopwatch.glade.h:6 +msgid "Label" +msgstr "Label" + +#: i18n/stopwatch.glade.h:7 +msgid "Loop" +msgstr "Loop" + +#: i18n/stopwatch.glade.h:8 +msgid "Stopwatch" +msgstr "Stopwatch" + +#: i18n/stopwatch.glade.h:9 +msgid "Stopwatch Preferences" +msgstr "Stopwatch Preferences" + +#: i18n/stopwatch.glade.h:10 +msgid "Timer 1" +msgstr "Timer 1" + +#: i18n/stopwatch.glade.h:11 +msgid "Timer 2" +msgstr "Timer 2" diff --git a/src/plugins/stopwatch/i18n/stopwatch.glade.h b/src/plugins/stopwatch/i18n/stopwatch.glade.h new file mode 100644 index 0000000..b85fcde --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.glade.h @@ -0,0 +1,11 @@ +char *s = N_("Mode"); +char *s = N_("Time (hours:minutes:seconds)"); +char *s = N_("Countdown"); +char *s = N_("Enabled"); +char *s = N_("Keep stopwatch visible while actve"); +char *s = N_("Label"); +char *s = N_("Loop"); +char *s = N_("Stopwatch"); +char *s = N_("Stopwatch Preferences"); +char *s = N_("Timer 1"); +char *s = N_("Timer 2"); diff --git a/src/plugins/stopwatch/i18n/stopwatch.pot b/src/plugins/stopwatch/i18n/stopwatch.pot new file mode 100644 index 0000000..59e4212 --- /dev/null +++ b/src/plugins/stopwatch/i18n/stopwatch.pot @@ -0,0 +1,62 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/stopwatch.glade.h:1 +msgid "Mode" +msgstr "" + +#: i18n/stopwatch.glade.h:2 +msgid "Time (hours:minutes:seconds)" +msgstr "" + +#: i18n/stopwatch.glade.h:3 +msgid "Countdown" +msgstr "" + +#: i18n/stopwatch.glade.h:4 +msgid "Enabled" +msgstr "" + +#: i18n/stopwatch.glade.h:5 +msgid "Keep stopwatch visible while actve" +msgstr "" + +#: i18n/stopwatch.glade.h:6 +msgid "Label" +msgstr "" + +#: i18n/stopwatch.glade.h:7 +msgid "Loop" +msgstr "" + +#: i18n/stopwatch.glade.h:8 +msgid "Stopwatch" +msgstr "" + +#: i18n/stopwatch.glade.h:9 +msgid "Stopwatch Preferences" +msgstr "" + +#: i18n/stopwatch.glade.h:10 +msgid "Timer 1" +msgstr "" + +#: i18n/stopwatch.glade.h:11 +msgid "Timer 2" +msgstr "" diff --git a/src/plugins/stopwatch/preferences.py b/src/plugins/stopwatch/preferences.py new file mode 100644 index 0000000..fcaa638 --- /dev/null +++ b/src/plugins/stopwatch/preferences.py @@ -0,0 +1,97 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os + +class G15StopwatchPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "stopwatch.ui")) + + self.dialog = widget_tree.get_object("StopwatchDialog") + self.dialog.set_transient_for(parent) + + # Timer 1 settings + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_enabled", "cb_timer1_enabled", False, widget_tree, True) + + timer1_label = widget_tree.get_object("e_timer1_label") + timer1_label.set_text(gconf_client.get_string(gconf_key + "/timer1_label") or "") + timer1_label.connect("changed", self._label_changed, gconf_key + "/timer1_label", gconf_client) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_mode_stopwatch", "rb_timer1_stopwatch_mode", True, widget_tree, True) + rb_timer1_stopwatch = widget_tree.get_object("rb_timer1_stopwatch_mode") + rb_timer1_stopwatch.connect("clicked", self._timer_timer_mode, widget_tree, "1", False) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_mode_countdown", "rb_timer1_countdown_mode", False, widget_tree, True) + rb_timer1_countdown = widget_tree.get_object("rb_timer1_countdown_mode") + rb_timer1_countdown.connect("clicked", self._timer_timer_mode, widget_tree, "1", True) + + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_hours", "sb_timer1_hours", 0, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_minutes", "sb_timer1_minutes", 5, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer1_seconds", "sb_timer1_seconds", 0, widget_tree, False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer1_loop", "cb_timer1_loop", False, widget_tree, True) + + # Timer 2 settings + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_enabled", "cb_timer2_enabled", False, widget_tree, True) + + timer2_label = widget_tree.get_object("e_timer2_label") + timer2_label.set_text(gconf_client.get_string(gconf_key + "/timer2_label") or "") + timer2_label.connect("changed", self._label_changed, gconf_key + "/timer2_label", gconf_client) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_mode_stopwatch", "rb_timer2_stopwatch_mode", True, widget_tree, True) + rb_timer2_stopwatch = widget_tree.get_object("rb_timer2_stopwatch_mode") + rb_timer2_stopwatch.connect("clicked", self._timer_timer_mode, widget_tree, "2", False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_mode_countdown", "rb_timer2_countdown_mode", False, widget_tree, True) + rb_timer2_countdown = widget_tree.get_object("rb_timer2_countdown_mode") + rb_timer2_countdown.connect("clicked", self._timer_timer_mode, widget_tree, "2", True) + + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_hours", "sb_timer2_hours", 0, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_minutes", "sb_timer2_minutes", 5, widget_tree, False) + g15uigconf.configure_spinner_from_gconf(gconf_client, gconf_key + "/timer2_seconds", "sb_timer2_seconds", 0, widget_tree, False) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/timer2_loop", "cb_timer2_loop", False, widget_tree, True) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/keep_page_visible", "cb_keep_page_visible", True, widget_tree, True) + + # Refresh UI state + self._timer_timer_mode(None, widget_tree, "1", rb_timer1_countdown.get_active()) + self._timer_timer_mode(None, widget_tree, "2", rb_timer2_countdown.get_active()) + + + def _label_changed(self, widget, gconf_key, gconf_client): + gconf_client.set_string(gconf_key, widget.get_text()) + + ''' + Set the UI sensivity according to the selected mode + ''' + def _timer_timer_mode(self, widget, widget_tree, timer_no, mode = False): + sb_timer_hours = widget_tree.get_object("sb_timer" + timer_no + "_hours") + sb_timer_hours.set_sensitive(mode) + sb_timer_minutes = widget_tree.get_object("sb_timer" + timer_no + "_minutes") + sb_timer_minutes.set_sensitive(mode) + sb_timer_seconds = widget_tree.get_object("sb_timer" + timer_no + "_seconds") + sb_timer_seconds.set_sensitive(mode) + cb_timer_loop = widget_tree.get_object("cb_timer" + timer_no + "_loop") + cb_timer_loop.set_sensitive(mode) + + def run(self): + self.dialog.run() + self.dialog.hide() + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/stopwatch/stopwatch.py b/src/plugins/stopwatch/stopwatch.py new file mode 100644 index 0000000..853d411 --- /dev/null +++ b/src/plugins/stopwatch/stopwatch.py @@ -0,0 +1,296 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("stopwatch", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15plugin as g15plugin +import gnome15.g15text as g15text +import datetime +import pango +import timer + +import preferences as g15preferences + +# Plugin details - All of these must be provided +id="stopwatch" +name=_("Stopwatch") +description=_("Stopwatch/Countdown timer plugin for gnome15.\ +Two timers are available. User can select the a mode (stopwatch/countdown) for each of them.") +author="Nuno Araujo " +copyright=_("Copyright (C)2011 Nuno Araujo") +site="http://www.russo79.com/gnome15" +has_preferences=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Toggle selected timer"), + g15driver.NEXT_SELECTION : _("Reset selected timer"), + g15driver.VIEW : _("Switch between timers") + } +actions_g19={ + g15driver.PREVIOUS_SELECTION : _("Toggle timer 1"), + g15driver.NEXT_SELECTION : _("Reset timer 1"), + g15driver.NEXT_PAGE : _("Toggle timer 2"), + g15driver.PREVIOUS_PAGE : _("Reset timer 2") + } + + +# +# A stopwatch / timer plugin for gnome15 +# + +def create(gconf_key, gconf_client, screen): + return G15Stopwatch(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + preferences = g15preferences.G15StopwatchPreferences(parent, driver, gconf_client, gconf_key) + preferences.run() + + +class G15Stopwatch(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, \ + screen, [ "cairo-clock", "clock", "gnome-panel-clock", "xfce4-clock", "rclock", "player-time" ], id, name) + self._active_timer = None + self._message = None + self._priority = g15screen.PRI_NORMAL + + def activate(self): + self._timer = None + self._text = g15text.new_text(self.screen) + self._notify_timer = None + self._timer1 = timer.G15Timer() + self._timer1.on_finish = self._on_finish + self._timer2 = timer.G15Timer() + self._timer2.on_finish = self._on_finish + self._load_configuration() + + g15plugin.G15RefreshingPlugin.activate(self) + + self.screen.key_handler.action_listeners.append(self) + self.watch(None, self._config_changed) + + def deactivate(self): + if self._timer1.is_running(): + self._timer1.toggle() + if self._timer2.is_running(): + self._timer2.toggle() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15RefreshingPlugin.deactivate(self) + + def destroy(self): + pass + + def create_page(self): + page = g15plugin.G15RefreshingPlugin.create_page(self) + if self.screen.driver.get_bpp() != 16: + """ + Don't show on the panel for G15, there just isn't enough room + Long term, this will be configurable per plugin + """ + page.panel_painter = None + return page + + def create_theme(self): + variant = None + if self._timer1.get_enabled() and self._timer2.get_enabled(): + variant = "two_timers" + elif self._timer1.get_enabled() or self._timer2.get_enabled(): + variant = "one_timer" + return g15theme.G15Theme(self, variant) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + # G19 we make use of more keys + if self.screen.driver.get_model_name() == g15driver.MODEL_G19: + if self._timer1.get_enabled(): + if binding.action == g15driver.PREVIOUS_SELECTION: + self._timer1.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_SELECTION: + self._timer1.reset() + + if self._timer2.get_enabled(): + if binding.action == g15driver.PREVIOUS_PAGE: + self._timer2.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_PAGE: + self._timer2.reset() + else: + # For everything else we allow switching between timers + if binding.action == g15driver.VIEW: + if self._active_timer == self._timer1: + self._active_timer = self._timer2 + else: + self._active_timer = self._timer1 + self._refresh() + + if self._active_timer: + if binding.action == g15driver.PREVIOUS_SELECTION: + self._active_timer.toggle() + self._check_page_priority() + self._refresh() + elif binding.action == g15driver.NEXT_SELECTION: + self._active_timer.reset() + self._check_page_priority() + self._refresh() + + def get_next_tick(self): + return g15pythonlang.total_seconds( datetime.timedelta( seconds = 1 )) + + def get_theme_properties(self): + properties = { } + if self._timer1.get_enabled() and self._timer2.get_enabled(): + properties["timer1_label"] = self._timer1.label + properties["timer1"] = self._format_time_delta(self._timer1.value()) + if self._active_timer == self._timer1: + properties["timer1_active"] = True + properties["timer2_active"] = False + else: + properties["timer1_active"] = False + properties["timer2_active"] = True + properties["timer2_label"] = self._timer2.label + properties["timer2"] = self._format_time_delta(self._timer2.value()) + elif self._timer1.get_enabled(): + properties["timer_label"] = self._timer1.label + properties["timer"] = self._format_time_delta(self._timer1.value()) + elif self._timer2.get_enabled(): + properties["timer_label"] = self._timer2.label + properties["timer"] = self._format_time_delta(self._timer2.value()) + + return properties + + def _paint_panel(self, canvas, allocated_size, horizontal): + if not self.page or self.screen.is_visible(self.page): + return + if not (self._timer1.get_enabled() or self._timer2.get_enabled()): + return + if not (self._timer1.is_running() or self._timer2.is_running()): + return + properties = self.get_theme_properties() + # Don't display the date or seconds on mono displays, not enough room as it is + if self.screen.driver.get_bpp() == 1: + if self._timer1.get_enabled() and self._timer2.get_enabled(): + text = "%s %s" % ( properties["timer1"], properties["timer2"] ) + else: + text = properties["timer"] + font_size = 8 + factor = 2 + font_name = g15globals.fixed_size_font_name + gap = 1 + else: + factor = 1 if horizontal else 1.2 + font_name = "Sans" + if self._timer1.get_enabled() and self._timer2.get_enabled(): + text = "%s\n%s" % (properties["timer1"], properties["timer2"]) + font_size = allocated_size / 3 + else: + text = properties["timer"] + font_size = allocated_size / 2 + gap = 8 + + self._text.set_canvas(canvas) + self._text.set_attributes(text, align = pango.ALIGN_CENTER, font_desc = font_name, font_absolute_size = font_size * pango.SCALE / factor) + x, y, width, height = self._text.measure() + if horizontal: + if self.screen.driver.get_bpp() == 1: + y = 0 + else: + y = (allocated_size / 2) - height / 2 + else: + x = (allocated_size / 2) - width / 2 + y = 0 + self._text.draw(x, y) + if horizontal: + return width + gap + else: + return height + 4 + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _config_changed(self, client, connection_id, entry, args): + self._load_configuration() + self.reload_theme() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _get_or_default(self, key, default_value): + v = self.gconf_client.get(key) + return v.get_int() if v != None else default_value + + def _load_timer(self, timer_object, number): + timer_object.set_enabled(self.gconf_client.get_bool(self.gconf_key + "/timer%d_enabled" % number) or False) + timer_object.label = self.gconf_client.get_string(self.gconf_key + "/timer%d_label" % number) or "" + if self.gconf_client.get_bool(self.gconf_key + "/timer%d_mode_countdown" % number): + timer_object.mode = timer.G15Timer.TIMER_MODE_COUNTDOWN + timer_object.initial_value = datetime.timedelta(hours = self._get_or_default(self.gconf_key + "/timer%d_hours" % number, 0), \ + minutes = self._get_or_default(self.gconf_key + "/timer%d_minutes" % number, 5), \ + seconds = self._get_or_default(self.gconf_key + "/timer%d_seconds" % number, 0)) + timer_object.loop = self.gconf_client.get_bool(self.gconf_key + "/timer%d_loop" % number ) + else: + timer_object.mode = timer.G15Timer.TIMER_MODE_STOPWATCH + timer_object.initial_value = datetime.timedelta(0, 0, 0) + + def _load_configuration(self): + self._load_timer(self._timer1, 1) + self._load_timer(self._timer2, 2) + + # Set active timer + if self._active_timer == None and self._timer1.get_enabled() and self._timer2.get_enabled(): + self._active_timer = self._timer1 + elif self._timer1.get_enabled() and self._timer2.get_enabled(): + #Keeps the current timer active + pass + elif self._timer1.get_enabled(): + self._active_timer = self._timer1 + elif self._timer2.get_enabled(): + self._active_timer = self._timer2 + + self._check_page_priority() + + def _check_page_priority(self): + self._priority = g15screen.PRI_EXCLUSIVE if self._is_any_timer_active() and g15gconf.get_bool_or_default(self.gconf_client, "%s/keep_page_visible" % self.gconf_key, True) \ + else g15screen.PRI_NORMAL + if self.page: + self.page.set_priority(self._priority) + + def _format_time_delta(self, td): + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + seconds = td.seconds % 60 + return '%s:%02d:%02d' % (hours, minutes, seconds) + + def _is_any_timer_active(self): + return ( self._timer1 is not None and self._timer1.is_running() ) or \ + ( self._timer2 is not None and self._timer2.is_running() ) + + def _on_finish(self): + self._check_page_priority() + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/stopwatch/stopwatch.ui b/src/plugins/stopwatch/stopwatch.ui new file mode 100644 index 0000000..b46797c --- /dev/null +++ b/src/plugins/stopwatch/stopwatch.ui @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + 999 + 1 + 10 + + + 59 + 1 + 10 + + + 60 + 1 + 10 + + + 999 + 1 + 10 + + + 59 + 1 + 10 + + + 59 + 1 + 10 + + + 320 + False + 5 + Stopwatch Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + True + + + True + False + 4 + + + Enabled + True + True + False + True + + + True + True + 4 + 0 + + + + + True + False + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + Stopwatch + True + True + False + True + True + + + True + True + 0 + + + + + Countdown + True + True + False + True + rb_timer1_stopwatch_mode + + + True + True + 1 + + + + + + + + + True + False + <b>Mode</b> + True + + + + + True + True + 2 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + True + True + + True + False + False + True + True + adj_timer1_hours + True + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + adj_timer1_minutes + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + adj_timer1_seconds + True + + + True + True + 2 + + + + + + + + + True + False + <b>Time (hours:minutes:seconds)</b> + True + + + + + True + True + 3 + + + + + Loop + True + True + False + True + + + True + True + 4 + 4 + + + + + + + True + False + Timer 1 + + + False + + + + + True + False + 4 + + + Enabled + True + True + False + True + + + True + True + 0 + + + + + True + False + + + True + False + Label + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + + + True + True + 1 + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + Stopwatch + True + True + False + True + True + + + True + True + 0 + + + + + Countdown + True + True + False + True + rb_timer2_stopwatch_mode + + + True + True + 1 + + + + + + + + + True + False + <b>Mode</b> + True + + + + + True + True + 2 + + + + + True + False + 0 + none + + + True + False + 4 + 4 + 12 + + + True + False + 4 + + + True + True + + True + False + False + True + True + adj_timer2_hours + True + + + True + True + 0 + + + + + True + True + + True + False + False + True + True + adj_timer2_minutes + True + + + True + True + 1 + + + + + True + True + + True + False + False + True + True + adj_timer2_seconds + True + + + True + True + 2 + + + + + + + + + True + False + <b>Time (hours:minutes:seconds)</b> + True + + + + + True + True + 3 + + + + + Loop + True + True + False + True + + + True + True + 4 + + + + + 1 + + + + + True + False + Timer 2 + + + 1 + False + + + + + True + True + 4 + 0 + + + + + Keep stopwatch visible while actve + True + True + False + True + + + True + True + 1 + + + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/stopwatch/timer.py b/src/plugins/stopwatch/timer.py new file mode 100644 index 0000000..d7c329d --- /dev/null +++ b/src/plugins/stopwatch/timer.py @@ -0,0 +1,91 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import datetime +import gnome15.g15notify as g15notify + +class G15Timer(): + TIMER_MODE_STOPWATCH = 0 + TIMER_MODE_COUNTDOWN = 1 + + def __init__(self): + self.__enabled = False + self.__running = False + self.label = "" + self.on_finish = None + self.mode = G15Timer.TIMER_MODE_STOPWATCH + self.initial_value = datetime.timedelta() + self.loop = False + self.reset() + + def set_enabled(self, value): + if value != self.__enabled: + self.__enabled = value + self.pause() + self.reset() + + def get_enabled(self): + return self.__enabled + + def value(self): + rv = self.__value() + if self.mode == G15Timer.TIMER_MODE_COUNTDOWN: + # Handle timeout + if rv >= self.initial_value: + # Stop timer if not in loop mode + if not self.loop: + self.pause() + self.reset() + rv = self.__value() + if self.on_finish: + self.on_finish() + self.notify() + rv = self.initial_value - rv + return rv + + def __value(self): + if not self.__running: + rv = self._last_value + else: + rv = datetime.datetime.now() - self._last_resume + self._last_value + return rv + + def toggle(self): + if self.__running: + self.pause() + else: + self.resume() + + def is_running(self): + return self.__running + + def pause(self): + self._last_value = self.__value() + self._last_resume = datetime.datetime.now() + self.__running = False + + def resume(self): + self._last_resume = datetime.datetime.now() + self.__running = True + + def reset(self): + self._last_value = datetime.timedelta() + self._last_resume = datetime.datetime.now() + + def notify(self): + g15notify.notify("Stopwatch", "Timer '" + self.label + "' is over.", timeout = 0) + +# vim:set ts=4 sw=4 et: diff --git a/src/plugins/sysmon/Makefile.am b/src/plugins/sysmon/Makefile.am new file mode 100644 index 0000000..73d794b --- /dev/null +++ b/src/plugins/sysmon/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = default dials graphs +plugindir = $(datadir)/gnome15/plugins/sysmon +plugin_DATA = sysmon.py \ + sysmon.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/default/Makefile.am b/src/plugins/sysmon/default/Makefile.am new file mode 100644 index 0000000..f1991e6 --- /dev/null +++ b/src/plugins/sysmon/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/default +theme_DATA = default.svg \ + g19.svg \ + mx5500.svg + +EXTRA_DIST = \ + $(theme_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/default/default.svg b/src/plugins/sysmon/default/default.svg new file mode 100644 index 0000000..eaa0a89 --- /dev/null +++ b/src/plugins/sysmon/default/default.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${cpu_no}: + ${net_no} + Mem: + ${cpu_pc}% + ${net_recv_mbps}/${net_send_mbps} + ${mem_used_gb}/${mem_total_gb} + + + ${info} + L3 ${next_cpu_no} + L4 ${next_net_no} + + diff --git a/src/plugins/sysmon/default/g19.svg b/src/plugins/sysmon/default/g19.svg new file mode 100644 index 0000000..14decd8 --- /dev/null +++ b/src/plugins/sysmon/default/g19.svg @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${cpu_pc}% + ${net_recv_mbps} Mbps + ${net_send_mbps} Mbps + ${mem_noncached_gb} GB of + ${mem_total_gb} GB + + + + ${next_cpu_no} + + + + + ${next_net_no} + + ${cpu_no} + ${net_no} + + diff --git a/src/plugins/sysmon/default/i18n/default.en_GB.po b/src/plugins/sysmon/default/i18n/default.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/default.pot b/src/plugins/sysmon/default/i18n/default.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/g19.en_GB.po b/src/plugins/sysmon/default/i18n/g19.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/g19.pot b/src/plugins/sysmon/default/i18n/g19.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/mx5500.en_GB.po b/src/plugins/sysmon/default/i18n/mx5500.en_GB.po new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/i18n/mx5500.pot b/src/plugins/sysmon/default/i18n/mx5500.pot new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/sysmon/default/mx5500.svg b/src/plugins/sysmon/default/mx5500.svg new file mode 100644 index 0000000..5c56f58 --- /dev/null +++ b/src/plugins/sysmon/default/mx5500.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + ${cpu_no}: + ${net_no} + Mem: + ${cpu_pc}% + ${net_recv_mbps}/${net_send_mbps} + ${mem_used_gb}/${mem_total_gb} + + + ${info} + + diff --git a/src/plugins/sysmon/dials/Makefile.am b/src/plugins/sysmon/dials/Makefile.am new file mode 100644 index 0000000..b8c698c --- /dev/null +++ b/src/plugins/sysmon/dials/Makefile.am @@ -0,0 +1,9 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/dials +theme_DATA = sysmon_dials_g19.py \ + g19.svg \ + g19-large-needle.svg \ + g19-small-needle.svg \ + g19-tiny-needle.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/sysmon/dials/g19-large-needle.svg b/src/plugins/sysmon/dials/g19-large-needle.svg new file mode 100644 index 0000000..5f46130 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-large-needle.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19-small-needle.svg b/src/plugins/sysmon/dials/g19-small-needle.svg new file mode 100644 index 0000000..5213be2 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-small-needle.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19-tiny-needle.svg b/src/plugins/sysmon/dials/g19-tiny-needle.svg new file mode 100644 index 0000000..0d76118 --- /dev/null +++ b/src/plugins/sysmon/dials/g19-tiny-needle.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/plugins/sysmon/dials/g19.svg b/src/plugins/sysmon/dials/g19.svg new file mode 100644 index 0000000..5fbf536 --- /dev/null +++ b/src/plugins/sysmon/dials/g19.svg @@ -0,0 +1,1024 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + CPU + + + + + + + + + Network + + + + + + + + + + Memory + ${mem_used_gb} GiB / ${mem_total_gb} GiB + ${cpu_pc}% + Receive: ${net_recv_mbps}mbps Send: ${net_send_mbps}mbps + ${mem_cached_gb} GiB cached + + diff --git a/src/plugins/sysmon/dials/sysmon_dials_g19.py b/src/plugins/sysmon/dials/sysmon_dials_g19.py new file mode 100644 index 0000000..aafbab0 --- /dev/null +++ b/src/plugins/sysmon/dials/sysmon_dials_g19.py @@ -0,0 +1,52 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15cairo as g15cairo +import os +import rsvg +import cairo + +needles = { + "cpu_pc" : (170, 90, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-large-needle.svg"))), + "net_send_pc" : (82, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-tiny-needle.svg"))), + "net_recv_pc" : (82, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-small-needle.svg"))), + "mem_used_pc" : (254, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-small-needle.svg"))), + "mem_cached_pc" : (254, 198, rsvg.Handle(os.path.join(os.path.dirname(__file__), "g19-tiny-needle.svg"))) + } + +def paint_foreground(theme, canvas, properties, attributes, args): + for key in needles.keys(): + needle = needles[key] + svg = needle[2] + surface = create_needle_surface(svg, ( ( 180.0 / 100.0 ) * float(properties[key]) ) ) + canvas.save() + svg_size = svg.get_dimension_data()[2:4] + canvas.translate (needle[0] - svg_size[0], needle[1] - svg_size[1]) + canvas.set_source_surface(surface) + canvas.paint() + canvas.restore() + +def create_needle_surface(svg, degrees): + svg_size = svg.get_dimension_data()[2:4] + surface = cairo.SVGSurface(None, svg_size[0] * 2,svg_size[1] *2) + context = cairo.Context(surface) + context.translate(svg_size[0], svg_size[1]) + g15cairo.rotate(context, -180) + g15cairo.rotate(context, degrees) + svg.render_cairo(context) + context.translate(-svg_size[0], -svg_size[1]) + return surface diff --git a/src/plugins/sysmon/graphs/Makefile.am b/src/plugins/sysmon/graphs/Makefile.am new file mode 100644 index 0000000..97d8d4e --- /dev/null +++ b/src/plugins/sysmon/graphs/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/sysmon/graphs +theme_DATA = g19.svg \ + default.svg \ + graphs.theme \ + sysmon_graphs_default.py + +EXTRA_DIST = \ + $(theme_DATA) \ No newline at end of file diff --git a/src/plugins/sysmon/graphs/default.svg b/src/plugins/sysmon/graphs/default.svg new file mode 100644 index 0000000..7b93da2 --- /dev/null +++ b/src/plugins/sysmon/graphs/default.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${info} + L3 ${next_cpu_no} + L4 ${next_net_no} + + + + ${cpu_no} + ${net_no} + Mem + ${cpu_pc}% + ${mem_used_gb}/${mem_total_gb} + ${net_recv_mbps}/${net_send_mbps} + + diff --git a/src/plugins/sysmon/graphs/g19.svg b/src/plugins/sysmon/graphs/g19.svg new file mode 100644 index 0000000..c05c929 --- /dev/null +++ b/src/plugins/sysmon/graphs/g19.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${cpu_no} + + ${cpu_pc}% + + + ${net_recv_mbps} Mbps + ${net_send_mbps} Mbps + ${net_no} + + + _(Mem) + ${mem_noncached_gb} GB of + ${mem_total_gb} GB + + + + + ${next_cpu_no} + + + + + ${next_net_no} + + + diff --git a/src/plugins/sysmon/graphs/graphs.theme b/src/plugins/sysmon/graphs/graphs.theme new file mode 100644 index 0000000..2574fb0 --- /dev/null +++ b/src/plugins/sysmon/graphs/graphs.theme @@ -0,0 +1,7 @@ +[theme] +name=Graphs +description=Displays system status as a number graphs. +#unsupported_models=g110,g11,mx5500,g930,g35 +# CairoPlot doesn't handle tiny graphs well at all. Disabled other models +# until this is fixed +supported_models=g19 diff --git a/src/plugins/sysmon/graphs/i18n/g19.en_GB.po b/src/plugins/sysmon/graphs/i18n/g19.en_GB.po new file mode 100644 index 0000000..511a6c4 --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.en_GB.po @@ -0,0 +1,22 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/g19.h:1 +msgid "Mem" +msgstr "Mem" diff --git a/src/plugins/sysmon/graphs/i18n/g19.h b/src/plugins/sysmon/graphs/i18n/g19.h new file mode 100644 index 0000000..21fdc9c --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.h @@ -0,0 +1 @@ +char *s = N_("Mem"); diff --git a/src/plugins/sysmon/graphs/i18n/g19.pot b/src/plugins/sysmon/graphs/i18n/g19.pot new file mode 100644 index 0000000..291191d --- /dev/null +++ b/src/plugins/sysmon/graphs/i18n/g19.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/g19.h:1 +msgid "Mem" +msgstr "" diff --git a/src/plugins/sysmon/graphs/sysmon_graphs_default.py b/src/plugins/sysmon/graphs/sysmon_graphs_default.py new file mode 100644 index 0000000..612e35a --- /dev/null +++ b/src/plugins/sysmon/graphs/sysmon_graphs_default.py @@ -0,0 +1,156 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import cairoplot +import cairo + +def create(theme): + page = theme.component + plugin = theme.plugin + page.add_child(G15CPUGraph("cpu", plugin)) + page.add_child(G15NetGraph("net", plugin)) + page.add_child(G15MemGraph("mem", plugin)) + +def destroy(theme): + page = theme.component +# page.remove_child(page.get_child_by_id("cpu")) +# page.remove_child(page.get_child_by_id("net")) +# page.remove_child(page.get_child_by_id("mem")) + +class G15Graph(g15theme.Component): + + def __init__(self, component_id, plugin): + g15theme.Component.__init__(self, component_id) + self.plugin = plugin + + def get_colors(self): + if self.plugin.screen.driver.get_bpp() == 1: + return (0.0,0.0,0.0,1.0), (0.0,0.0,0.0,1.0) + elif self.plugin.screen.driver.get_control_for_hint(g15driver.HINT_HIGHLIGHT): + highlight_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (255, 0, 0 )) + return (highlight_color[0],highlight_color[1],highlight_color[2], 1.0), \ + (highlight_color[0],highlight_color[1],highlight_color[2], 0.50) + + def create_plot(self, graph_surface): + raise Exception("Not implemented") + + def paint(self, canvas): + g15theme.Component.paint(self, canvas) + if self.view_bounds: + graph_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + int(self.view_bounds[2]), + int(self.view_bounds[3])) + plot = self.create_plot(graph_surface) + + if self.plugin.screen.driver.get_bpp() == 1: + plot.line_color = (1.0,1.0,1.0) + plot.line_width = 1.0 + plot.display_labels = False + else: + plot.line_width = 2.0 + plot.bounding_box = False + plot.line_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.label_color = self.plugin.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (255, 255, 255)) + plot.shadow = True + plot.render() + plot.commit() + + canvas.save() + canvas.translate(self.view_bounds[0], self.view_bounds[1]) + canvas.set_source_surface(graph_surface, 0.0, 0.0) + canvas.paint() + canvas.restore() + +class G15CPUGraph(G15Graph): + + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + series_colors, fill_colors = self.get_colors() + return cairoplot.AreaPlot(graph_surface, self.plugin.selected_cpu.history, + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = ["%-6d" % 0, "%-6d" % 50, "%-6d" % 100], + y_bounds = (0, 100), + series_colors = [ series_colors ], + fill_colors = [ fill_colors ]) + + +class G15NetGraph(G15Graph): + + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + y_labels = [] + max_y = max(max(self.plugin.selected_net.max_send, self.plugin.selected_net.max_recv), 102400) + for x in range(0, int(max_y), int(max_y / 4)): + y_labels.append("%-3.2f" % ( float(x) / 102400.0 ) ) + series_color, fill_color = self.get_colors() + if self.plugin.screen.driver.get_bpp() == 1: + alt_series_color = (1.0,1.0,1.0,1.0) + alt_fill_color = (1.0,1.0,1.0,1.0) + else: + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + return cairoplot.AreaPlot( graph_surface, [ self.plugin.selected_net.send_history, self.plugin.selected_net.recv_history ], + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = y_labels, + y_bounds = (0, max_y ), + series_colors = [ series_color, alt_series_color ], + fill_colors = [ fill_color, alt_fill_color ] ) + +class G15MemGraph(G15Graph): + """ + Memory graph + """ + def __init__(self, component_id, plugin): + G15Graph.__init__(self, component_id, plugin) + + def create_plot(self, graph_surface): + y_labels = [] + max_y = self.plugin.max_total_mem + for x in range(0, int(max_y), int(max_y / 4)): + y_labels.append("%-4d" % int( float(x) / 1024.0 / 1024.0 ) ) + series_color, fill_color = self.get_colors() + + if self.plugin.screen.driver.get_bpp() == 1: + alt_series_color = (1.0,1.0,1.0,1.0) + alt_fill_color = (1.0,1.0,1.0,1.0) + else: + alt_series_color = g15convert.get_alt_color(series_color) + alt_fill_color = g15convert.get_alt_color(fill_color) + return cairoplot.AreaPlot( graph_surface, [ self.plugin.used_history, self.plugin.cached_history ], + self.view_bounds[2], + self.view_bounds[3], + background = None, + grid = False, + x_labels = [], + y_labels = y_labels, + y_bounds = (0, max_y ), + series_colors = [ series_color, alt_series_color ], + fill_colors = [ fill_color, alt_fill_color ] ) \ No newline at end of file diff --git a/src/plugins/sysmon/i18n/sysmon.en_GB.po b/src/plugins/sysmon/i18n/sysmon.en_GB.po new file mode 100644 index 0000000..7569452 --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.en_GB.po @@ -0,0 +1,26 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/sysmon.glade.h:1 +msgid "Sensors Preferences" +msgstr "Sensors Preferences" + +#: i18n/sysmon.glade.h:2 +msgid "Show CPU usage on panel" +msgstr "Show CPU usage on panel" diff --git a/src/plugins/sysmon/i18n/sysmon.glade.h b/src/plugins/sysmon/i18n/sysmon.glade.h new file mode 100644 index 0000000..6714dbb --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.glade.h @@ -0,0 +1,2 @@ +char *s = N_("Sensors Preferences"); +char *s = N_("Show CPU usage on panel"); diff --git a/src/plugins/sysmon/i18n/sysmon.pot b/src/plugins/sysmon/i18n/sysmon.pot new file mode 100644 index 0000000..778f76a --- /dev/null +++ b/src/plugins/sysmon/i18n/sysmon.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/sysmon.glade.h:1 +msgid "Sensors Preferences" +msgstr "" + +#: i18n/sysmon.glade.h:2 +msgid "Show CPU usage on panel" +msgstr "" diff --git a/src/plugins/sysmon/sysmon.py b/src/plugins/sysmon/sysmon.py new file mode 100644 index 0000000..68b1a79 --- /dev/null +++ b/src/plugins/sysmon/sysmon.py @@ -0,0 +1,472 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("sysmon", modfile = __file__).ugettext + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import time +import logging +logger=logging.getLogger(__name__) +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop. Falling back to g15top", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop +import gtk +import os +import sys +import socket + +id = "sysmon" +name = _("System Monitor") +description = _("Display CPU, Memory, and Network statistics. Either a summary of each system's stats is displayed, or \ +you may cycle through the CPU and Network interfaces.") +author = "Brett Smith " +copyright = _("Copyright (C)2010 Brett Smith") +site = "http://www.gnome15.org" +default_enabled = True +has_preferences = True +actions={ + g15driver.PREVIOUS_SELECTION : _("Toggle Monitored CPU"), + g15driver.NEXT_SELECTION : _("Toggle Monitored Network\nInterface") } +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# Various constants +GRAPH_SIZE = 50 +CPU_ICONS = [ "utilities-system-monitor","gnome-cpu-frequency-applet", "computer" ] + +''' +This plugin displays system statistics +''' + +def create(gconf_key, gconf_client, screen): + return G15SysMon(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "sysmon.ui")) + dialog = widget_tree.get_object("SysmonDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/show_cpu_on_panel", "ShowCPUUsageOnPanel", True, widget_tree) + dialog.run() + dialog.hide() + +class Net(): + + def __init__(self, net_no, name): + self.net_no = net_no + self.name = name + self.recv_bps = 0.0 + self.send_bps = 0.0 + self.last_net_list = None + self.max_send = 0.0001 + self.max_recv = 0.0001 + self.send_history = [0] * GRAPH_SIZE + self.recv_history = [0] * GRAPH_SIZE + self.last_net_list = None + self.last_time = 0 + + def new_data(self, this_net_list): + now = time.time() + + ''' + Net + ''' + + self.recv_bps = 0.0 + self.send_bps = 0.0 + + if self.last_net_list != None: + time_taken = now - self.last_time + if self.net_no == 0: + this_total = self._get_net_total(this_net_list) + last_total = self._get_net_total(self.last_net_list) + else: + this_total = self._get_net(this_net_list[self.name]) + last_total = self._get_net(self.last_net_list[self.name]) + + # How many bps + self.recv_bps = (this_total[0] - last_total[0]) / time_taken + self.send_bps = (this_total[1] - last_total[1]) / time_taken + + # Adjust the maximums if necessary + if self.recv_bps > self.max_recv: + self.max_recv = self.recv_bps + if self.send_bps > self.max_send: + self.max_send = self.send_bps + + # History + self.send_history.append(self.recv_bps) + while len(self.send_history) > GRAPH_SIZE: + del self.send_history[0] + self.recv_history.append(self.send_bps) + while len(self.recv_history) > GRAPH_SIZE: + del self.recv_history[0] + + self.last_net_list = this_net_list + self.last_time = now + + def _get_net(self, card): + totals = (card[0], card[1]) + return totals + + def _get_net_total(self, net_list): + totals = (0, 0) + for l in net_list: + card = net_list[l] + totals = (totals[0] + card[0], totals[1]) + totals = (totals[0], totals[1] + card[1]) + return totals + +class CPU(): + + def __init__(self, number): + self.number = number + self.name = "cpu%d" % number if number >= 0 else "cpu" + self.history = [0] * GRAPH_SIZE + self.value = 0 + self.times = None + self.last_times = None + + def new_times(self, time_list): + + if self.last_times is not None: + working_list = list(time_list) + + ''' Work out the number of time units the CPU has spent on each task type since the last + time we checked + ''' + + for i in range(len(self.last_times)): + working_list[i] -= self.last_times[i] + + self.pc = self.get_pc(working_list) + else: + self.pc = 0 + + self.last_times = time_list + + # Update the history and trim it to the graph data size + self.history.append(self.pc) + while len(self.history) > GRAPH_SIZE: + del self.history[0] + + def get_pc(self, times): + sum_l = sum(times) + val = times[len(times)- 1] + if sum_l > 0: + return 100 - (val * 100.00 / sum_l) + return 0 + +class G15SysMon(g15plugin.G15RefreshingPlugin): + """ + Plugin implementation + """ + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, CPU_ICONS, id, name) + self.only_refresh_when_visible = False + + def activate(self): + self._net_icon = g15icontools.get_icon_path([ "network-transmit-receive", + "gnome-fs-network", + "network-server" ], + self.screen.height) + self._cpu_icon = g15icontools.get_icon_path( CPU_ICONS, + self.screen.height) + self._mem_icon = g15icontools.get_icon_path( [ "media-memory", + "media-flash" ], + self.screen.height) + self._thumb_icon = g15cairo.load_surface_from_file(self._cpu_icon) + + self.variant = 0 + self.graphs = {} + self.last_time_list = None + self.last_times_list = [] + self.last_time = 0 + + # CPU + self.selected_cpu = None + self.cpu_no = 0 + self.cpu_data = [] + selected_cpu_name = self.gconf_client.get_string(self.gconf_key + "/cpu") + cpus = gtop.cpu().cpus + for i in range(-1, len(cpus)): + cpu = CPU(i) + self.cpu_data.append(cpu) + if cpu.name == selected_cpu_name: + self.selected_cpu = cpu + if self.selected_cpu is None: + self.selected_cpu = self.cpu_data[0] + + # Net + self.selected_net = None + _, self.net_list = self._get_net_stats() + net_name = self.gconf_client.get_string(self.gconf_key + "/net") + self.net_data = [] + for idx, n in enumerate(self.net_list): + net = Net(idx, n) + self.net_data.append(net) + if net.name == net_name: + self.selected_net = net + + if self.selected_net is None and len(self.net_data) > 0: + self.selected_net = self.net_data[0] + + + # Memory + self.max_total_mem = 0 + self.total = 1.0 + self.cached = 0 + self.free = 0 + self.used = 0 + self.cached_history = [0] * GRAPH_SIZE + self.used_history = [0] * GRAPH_SIZE + + g15plugin.G15RefreshingPlugin.activate(self) + self._set_panel() + self.watch(["show_cpu_on_panel","theme"], self._config_changed) + self.screen.key_handler.action_listeners.append(self) + + # Start refreshing + self.do_refresh() + + def reload_theme(self): + g15plugin.G15RefreshingPlugin.reload_theme(self) + self._set_panel() + + def deactivate(self): + g15plugin.G15RefreshingPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_SELECTION: + idx = self.cpu_data.index(self.selected_cpu) + idx += 1 + if idx >= len(self.cpu_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/cpu", self.cpu_data[idx].name) + self.selected_cpu = self.cpu_data[idx] + self.do_refresh() + return True + elif binding.action == g15driver.NEXT_SELECTION: + if self.selected_net is not None: + idx = self.net_data.index(self.selected_net) + idx += 1 + if idx >= len(self.net_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/net", self.net_data[idx].name) + self.selected_net = self.net_data[idx] + self.do_refresh() + return True + + def refresh(self): + + # Memory + mem = self._get_mem_info() + now = time.time() + + ''' + CPU + ''' + for c in self.cpu_data: + c.new_times(self._get_time_list(c)) + + ''' + Net + ''' + + # Current net status + this_net_list, self.net_list = self._get_net_stats() + for n in self.net_data: + n.new_data(this_net_list) + + ''' + Memory + ''' + + self.total = float(mem.total) + self.max_total_mem = max(self.max_total_mem, self.total) + self.free = float(mem.free) + self.used = self.total - self.free + self.cached = float(mem.cached) + self.noncached = self.total - self.free - self.cached + self.used_history.append(self.used + self.cached) + + while len(self.used_history) > GRAPH_SIZE: + del self.used_history[0] + self.cached_history.append(self.cached) + while len(self.cached_history) > GRAPH_SIZE: + del self.cached_history[0] + + self.last_time = now + + ''' Private + ''' + def _config_changed(self, client, connection_id, entry, args): + self.reload_theme() + self._reschedule_refresh() + + def _set_panel(self, client = None, connection_id = None, entry = None, args = None): + self.page.panel_painter = self._paint_panel if g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/show_cpu_on_panel", True) else None + + def _refresh(self): + if self.page is not None: + if self.screen.is_visible(self.page): + self.refresh() + self.screen.redraw(self.page) + elif self.page.panel_painter is not None: + self.refresh() + self.screen.redraw(redraw_content = False) + self._schedule_refresh() + + def get_theme_properties(self): + + properties = {} + properties["cpu_pc"] = "%3d" % self.selected_cpu.pc + + properties["mem_total"] = "%f" % ( self.total / 1024 ) + properties["mem_free_k"] = "%f" % ( self.free / 1024 ) + properties["mem_used_k"] = "%f" % ( self.used / 1024 ) + properties["mem_cached_k"] = "%f" % ( self.cached / 1024 ) + properties["mem_noncached_k"] = "%f" % ( self.noncached / 1024 ) + + properties["mem_total_mb"] = "%.2f" % ( self.total / 1024 / 1024 ) + properties["mem_free_mb"] = "%.2f" % ( self.free / 1024 / 1024 ) + properties["mem_used_mb"] = "%.2f" % ( self.used / 1024 / 1024 ) + properties["mem_cached_mb" ] = "%3d" % ( self.cached / 1024 / 1024 ) + properties["mem_noncached_mb" ] = "%3d" % ( self.noncached / 1024 / 1024 ) + + properties["mem_total_gb"] = "%.1f" % ( self.total / 1024 / 1024 / 1024 ) + properties["mem_free_gb"] = "%.1f" % ( self.free / 1024 / 1024 / 1024 ) + properties["mem_used_gb"] = "%.1f" % ( self.used / 1024 / 1024 / 1024 ) + properties["mem_cached_gb" ] = "%.1f" % ( self.cached / 1024 / 1024 / 1024 ) + properties["mem_noncached_gb"] = "%.1f" % ( self.noncached / 1024 / 1024 / 1024 ) + + properties["mem_used_pc"] = int(self.used * 100.0 / self.total) + properties["mem_cached_pc"] = int(self.cached * 100.0 / self.total) + properties["mem_noncached_pc"] = int(self.noncached * 100.0 / self.total) + + if self.selected_net is not None: + properties["net_recv_pc"] = int(self.selected_net.recv_bps * 100.0 / self.selected_net.max_recv) + properties["net_send_pc"] = int(self.selected_net.send_bps * 100.0 / self.selected_net.max_send) + properties["net_recv_mbps"] = "%.2f" % (self.selected_net.recv_bps / 1024 / 1024) + properties["net_send_mbps"] = "%.2f" % (self.selected_net.send_bps / 1024 / 1024) + properties["net_no"] = self.selected_net.name.upper() + idx = self.net_data.index(self.selected_net) + properties["next_net_no"] = self.net_list[idx + 1].upper() if idx < ( len(self.net_list) - 1) else self.net_list[0].upper() + else: + for c in ["net_recv_pc","net_send_pc","net_recv_mbps","net_send_mbps"]: + properties[c] = "" + + # TODO we should ship some more appropriate default icons + properties["net_icon"] = self._net_icon + properties["cpu_icon"] = self._cpu_icon + properties["mem_icon"] = self._mem_icon + + try : + properties["info"] = socket.gethostname() + except Exception as e: + logger.debug("Could not get hostname. Falling back to 'System'", exc_info = e) + properties["info"] = "System" + + properties["cpu_no"] = self.selected_cpu.name.upper() + idx = self.cpu_data.index(self.selected_cpu) + properties["next_cpu_no"] = self.cpu_data[idx + 1].name.upper() if idx < ( len(self.cpu_data) - 1) else self.cpu_data[0].name.upper() + + + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self.page != None and self._thumb_icon != None and self.screen.driver.get_bpp() == 16: + return g15cairo.paint_thumbnail_image(allocated_size, self._thumb_icon, canvas) + + def _paint_panel(self, canvas, allocated_size, horizontal): + if self.page != None and self.screen.driver.get_bpp() == 16: + canvas.save() + + no_cpus = len(self.cpu_data) - 1 + if no_cpus < 2: + bar_width = 16 + elif no_cpus < 3: + bar_width = 8 + elif no_cpus < 5: + bar_width = 6 + elif no_cpus < 9: + bar_width = 4 + else: + bar_width = 2 + + total_width = ( bar_width + 1 ) * no_cpus + available_height = allocated_size - 4 + + r, g, b = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, (0,0,0)) + + canvas.set_line_width(1.0) + canvas.set_source_rgba(r, g, b, 0.3) + canvas.rectangle(0, 0, total_width + 4, allocated_size ) + canvas.stroke() + canvas.set_source_rgb(*self.screen.driver.get_color_as_ratios(g15driver.HINT_HIGHLIGHT, (0,0,0))) + canvas.translate(2, 0) + for i in self.cpu_data: + if i.number >= 0: + bar_height = float(available_height) * ( float(i.pc) / 100.0 ) + canvas.rectangle(0, available_height - bar_height + 2, bar_width, bar_height ) + canvas.fill() + canvas.translate(bar_width + 1, 0) + + canvas.restore() + + return 4 + total_width + + def _get_net_stats(self): + ifs = { } + nets = gtop.netlist() + for net in nets: + netload = gtop.netload(net) + ifs[net] = [ netload.bytes_in, netload.bytes_out ] + nets.insert(0, "Net") + return ifs, nets + + + def _get_time_list(self, cpu): + ''' + Returns a 4 element list containing the amount of time the CPU has + spent performing the different types of work + + 0 user + 1 nice + 2 system + 3 idle + + Values are in USER_HZ or Jiffies + ''' + if cpu.number == -1: + cpu_times = gtop.cpu() + else: + cpu_times = gtop.cpu().cpus[cpu.number] + return [cpu_times.user, cpu_times.nice, cpu_times.sys, cpu_times.idle] + + def _get_mem_info(self): + return gtop.mem() diff --git a/src/plugins/sysmon/sysmon.ui b/src/plugins/sysmon/sysmon.ui new file mode 100644 index 0000000..78e0cb8 --- /dev/null +++ b/src/plugins/sysmon/sysmon.ui @@ -0,0 +1,66 @@ + + + + + + 320 + False + 5 + Sensors Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + Show CPU usage on panel + True + True + False + True + + + True + True + 1 + + + + + + button1 + + + diff --git a/src/plugins/tails/LICENSE b/src/plugins/tails/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/src/plugins/tails/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/src/plugins/tails/Makefile.am b/src/plugins/tails/Makefile.am new file mode 100644 index 0000000..85ac349 --- /dev/null +++ b/src/plugins/tails/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = default tailer +plugindir = $(datadir)/gnome15/plugins/tails +plugin_DATA = tails.py \ + tails.ui \ + LICENSE \ + README + +EXTRA_DIST = \ + $(plugin_DATA) LICENSE README \ No newline at end of file diff --git a/src/plugins/tails/README b/src/plugins/tails/README new file mode 100644 index 0000000..c9c7bcb --- /dev/null +++ b/src/plugins/tails/README @@ -0,0 +1,53 @@ + +pytailer +A python implementation of GNU tail and head + +http://code.google.com/p/pytailer/ + +====== + +Python tail is a simple implementation of GNU tail and head. + +It provides 3 main functions that can be performed on any file-like object that +supports seek() and tell(). + +* tail - read lines from the end of a file +* head - read lines from the top of a file +* follow - read lines as a file grows + +It also comes with pytail, a command line version offering the same +functionality as GNU tail. This can be particularly useful on Windows systems +that have no tail equivalent. + + +:: + +import tailer +f = open('test.txt', 'w') +for i in range(11): +f.write('Line %d\n' % (i + 1)) +f.close() + +Tail +---- +:: + +# Get the last 3 lines of the file +tailer.tail(open('test.txt'), 3) +# ['Line 9', 'Line 10', 'Line 11'] + +Head +---- +:: + +# Get the first 3 lines of the file +tailer.head(open('test.txt'), 3) +# ['Line 1', 'Line 2', 'Line 3'] + +Follow +------ +:: + +# Follow the file as it grows +for line in tailer.follow(open('test.txt')): +print line \ No newline at end of file diff --git a/src/plugins/tails/default/Makefile.am b/src/plugins/tails/default/Makefile.am new file mode 100644 index 0000000..d4bdb66 --- /dev/null +++ b/src/plugins/tails/default/Makefile.am @@ -0,0 +1,8 @@ +themedir = $(datadir)/gnome15/plugins/tails/default +theme_DATA = default-menu-screen.svg \ + default-menu-entry.svg \ + g19-menu-screen.svg \ + g19-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/tails/default/default-menu-entry.svg b/src/plugins/tails/default/default-menu-entry.svg new file mode 100644 index 0000000..2b045bd --- /dev/null +++ b/src/plugins/tails/default/default-menu-entry.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${line} + + + + ${line} + + diff --git a/src/plugins/tails/default/default-menu-screen.svg b/src/plugins/tails/default/default-menu-screen.svg new file mode 100644 index 0000000..6deafb5 --- /dev/null +++ b/src/plugins/tails/default/default-menu-screen.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${title} - ${subtitle} + + + + + + ${message} + + diff --git a/src/plugins/tails/default/g19-menu-entry.svg b/src/plugins/tails/default/g19-menu-entry.svg new file mode 100644 index 0000000..65d8f39 --- /dev/null +++ b/src/plugins/tails/default/g19-menu-entry.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${line} + + diff --git a/src/plugins/tails/default/g19-menu-screen.svg b/src/plugins/tails/default/g19-menu-screen.svg new file mode 100644 index 0000000..265f25d --- /dev/null +++ b/src/plugins/tails/default/g19-menu-screen.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${title} + ${subtitle} + + ${message} + + + + + + + diff --git a/src/plugins/tails/i18n/tails.en_GB.po b/src/plugins/tails/i18n/tails.en_GB.po new file mode 100644 index 0000000..55c1b40 --- /dev/null +++ b/src/plugins/tails/i18n/tails.en_GB.po @@ -0,0 +1,46 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/tails.glade.h:1 +msgid "Files" +msgstr "Files" + +#: i18n/tails.glade.h:2 +msgid "Options" +msgstr "Options" + +#: i18n/tails.glade.h:3 +msgid "Show" +msgstr "Show" + +#: i18n/tails.glade.h:4 +msgid "Tails Preferences" +msgstr "Tails Preferences" + +#: i18n/tails.glade.h:5 +msgid "lines" +msgstr "lines" + +#: i18n/tails.glade.h:6 +msgid "toolbutton1" +msgstr "toolbutton1" + +#: i18n/tails.glade.h:7 +msgid "toolbutton2" +msgstr "toolbutton2" diff --git a/src/plugins/tails/i18n/tails.glade.h b/src/plugins/tails/i18n/tails.glade.h new file mode 100644 index 0000000..21ad402 --- /dev/null +++ b/src/plugins/tails/i18n/tails.glade.h @@ -0,0 +1,7 @@ +char *s = N_("Files"); +char *s = N_("Options"); +char *s = N_("Show"); +char *s = N_("Tails Preferences"); +char *s = N_("lines"); +char *s = N_("toolbutton1"); +char *s = N_("toolbutton2"); diff --git a/src/plugins/tails/i18n/tails.pot b/src/plugins/tails/i18n/tails.pot new file mode 100644 index 0000000..757c10e --- /dev/null +++ b/src/plugins/tails/i18n/tails.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/tails.glade.h:1 +msgid "Files" +msgstr "" + +#: i18n/tails.glade.h:2 +msgid "Options" +msgstr "" + +#: i18n/tails.glade.h:3 +msgid "Show" +msgstr "" + +#: i18n/tails.glade.h:4 +msgid "Tails Preferences" +msgstr "" + +#: i18n/tails.glade.h:5 +msgid "lines" +msgstr "" + +#: i18n/tails.glade.h:6 +msgid "toolbutton1" +msgstr "" + +#: i18n/tails.glade.h:7 +msgid "toolbutton2" +msgstr "" diff --git a/src/plugins/tails/tailer/Makefile.am b/src/plugins/tails/tailer/Makefile.am new file mode 100644 index 0000000..1919694 --- /dev/null +++ b/src/plugins/tails/tailer/Makefile.am @@ -0,0 +1,5 @@ +plugindir = $(datadir)/gnome15/plugins/tails/tailer +plugin_DATA = __init__.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/tails/tailer/__init__.py b/src/plugins/tails/tailer/__init__.py new file mode 100644 index 0000000..25df9f2 --- /dev/null +++ b/src/plugins/tails/tailer/__init__.py @@ -0,0 +1,300 @@ +# $Id: __init__.py 3 2008-01-29 18:39:09Z msthornton $ + +import re +import time + +class Tailer(object): + """\ + Implements tailing and heading functionality like GNU tail and head + commands. + """ + line_terminators = ('\r\n', '\n', '\r') + + def __init__(self, file, read_size=1024, end=False): + self.read_size = read_size + self.file = file + self.start_pos = self.file.tell() + if end: + self.seek_end() + + def splitlines(self, data): + return re.split('|'.join(self.line_terminators), data) + + def seek_end(self): + self.seek(0, 2) + + def seek(self, pos, whence=0): + self.file.seek(pos, whence) + + def read(self, read_size=None): + if read_size: + read_str = self.file.read(read_size) + else: + read_str = self.file.read() + + return len(read_str), read_str + + def seek_line_forward(self): + """\ + Searches forward from the current file position for a line terminator + and seeks to the charachter after it. + """ + pos = start_pos = self.file.tell() + + bytes_read, read_str = self.read(self.read_size) + + start = 0 + if bytes_read and read_str[0] in self.line_terminators: + # The first charachter is a line terminator, don't count this one + start += 1 + + while bytes_read > 0: + # Scan forwards, counting the newlines in this bufferfull + i = start + while i < bytes_read: + if read_str[i] in self.line_terminators: + self.seek(pos + i + 1) + return self.file.tell() + i += 1 + + pos += self.read_size + self.seek(pos) + + bytes_read, read_str = self.read(self.read_size) + + return None + + def seek_line(self): + """\ + Searches backwards from the current file position for a line terminator + and seeks to the charachter after it. + """ + pos = end_pos = self.file.tell() + + read_size = self.read_size + if pos > read_size: + pos -= read_size + else: + pos = 0 + read_size = end_pos + + self.seek(pos) + + bytes_read, read_str = self.read(read_size) + + if bytes_read and read_str[-1] in self.line_terminators: + # The last charachter is a line terminator, don't count this one + bytes_read -= 1 + + if read_str[-2:] == '\r\n' and '\r\n' in self.line_terminators: + # found crlf + bytes_read -= 1 + + while bytes_read > 0: + # Scan backward, counting the newlines in this bufferfull + i = bytes_read - 1 + while i >= 0: + if read_str[i] in self.line_terminators: + self.seek(pos + i + 1) + return self.file.tell() + i -= 1 + + if pos == 0 or pos - self.read_size < 0: + # Not enought lines in the buffer, send the whole file + self.seek(0) + return None + + pos -= self.read_size + self.seek(pos) + + bytes_read, read_str = self.read(self.read_size) + + return None + + def tail(self, lines=10): + """\ + Return the last lines of the file. + """ + self.seek_end() + end_pos = self.file.tell() + + for i in xrange(lines): + if not self.seek_line(): + break + + data = self.file.read(end_pos - self.file.tell() - 1) + if data: + return self.splitlines(data) + else: + return [] + + def head(self, lines=10): + """\ + Return the top lines of the file. + """ + self.seek(0) + + for i in xrange(lines): + if not self.seek_line_forward(): + break + + end_pos = self.file.tell() + + self.seek(0) + data = self.file.read(end_pos - 1) + + if data: + return self.splitlines(data) + else: + return [] + + def follow(self, delay=1.0): + """\ + Iterator generator that returns lines as data is added to the file. + + Based on: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/157035 + """ + trailing = True + + while 1: + where = self.file.tell() + line = self.file.readline() + if line: + if trailing and line in self.line_terminators: + # This is just the line terminator added to the end of the file + # before a new line, ignore. + trailing = False + continue + + if line[-1] in self.line_terminators: + line = line[:-1] + if line[-1:] == '\r\n' and '\r\n' in self.line_terminators: + # found crlf + line = line[:-1] + + trailing = False + yield line + else: + trailing = True + self.seek(where) + time.sleep(delay) + + def __iter__(self): + return self.follow() + + def close(self): + self.file.close() + +def tail(file, lines=10): + """\ + Return the last lines of the file. + + >>> import StringIO + >>> f = StringIO.StringIO() + >>> for i in range(11): + ... f.write('Line %d\\n' % (i + 1)) + >>> tail(f, 3) + ['Line 9', 'Line 10', 'Line 11'] + """ + return Tailer(file).tail(lines) + +def head(file, lines=10): + """\ + Return the top lines of the file. + + >>> import StringIO + >>> f = StringIO.StringIO() + >>> for i in range(11): + ... f.write('Line %d\\n' % (i + 1)) + >>> head(f, 3) + ['Line 1', 'Line 2', 'Line 3'] + """ + return Tailer(file).head(lines) + +def follow(file, delay=1.0): + """\ + Iterator generator that returns lines as data is added to the file. + + >>> import os + >>> f = file('test_follow.txt', 'w') + >>> fo = file('test_follow.txt', 'r') + >>> generator = follow(fo) + >>> f.write('Line 1\\n') + >>> f.flush() + >>> generator.next() + 'Line 1' + >>> f.write('Line 2\\n') + >>> f.flush() + >>> generator.next() + 'Line 2' + >>> f.close() + >>> fo.close() + >>> os.remove('test_follow.txt') + """ + return Tailer(file, end=True).follow(delay) + +def _test(): + import doctest + doctest.testmod() + +def _main(filepath, options): + tailer = Tailer(open(filepath, 'rb')) + + try: + try: + if options.lines > 0: + if options.head: + if options.follow: + print >>sys.stderr, 'Cannot follow from top of file.' + sys.exit(1) + lines = tailer.head(options.lines) + else: + lines = tailer.tail(options.lines) + + for line in lines: + print line + elif options.follow: + # Seek to the end so we can follow + tailer.seek_end() + + if options.follow: + for line in tailer.follow(delay=options.sleep): + print line + except KeyboardInterrupt: + # Escape silently + pass + finally: + tailer.close() + +def main(): + from optparse import OptionParser + import sys + + parser = OptionParser(usage='usage: %prog [options] filename') + parser.add_option('-f', '--follow', dest='follow', default=False, action='store_true', + help='output appended data as the file grows') + + parser.add_option('-n', '--lines', dest='lines', default=10, type='int', + help='output the last N lines, instead of the last 10') + + parser.add_option('-t', '--top', dest='head', default=False, action='store_true', + help='output lines from the top instead of the bottom. Does not work with follow') + + parser.add_option('-s', '--sleep-interval', dest='sleep', default=1.0, metavar='S', type='float', + help='with -f, sleep for approximately S seconds between iterations') + + parser.add_option('', '--test', dest='test', default=False, action='store_true', + help='Run some basic tests') + + (options, args) = parser.parse_args() + + if options.test: + _test() + elif not len(args) == 1: + parser.print_help() + sys.exit(1) + else: + _main(args[0], options) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/plugins/tails/tails.py b/src/plugins/tails/tails.py new file mode 100644 index 0000000..25d28c7 --- /dev/null +++ b/src/plugins/tails/tails.py @@ -0,0 +1,356 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("tails", modfile = __file__).ugettext + +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.util.g15markup as g15markup +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import subprocess +import time +import tailer +import os +import gtk +import gconf +import logging +import xdg.Mime as mime +from threading import Thread +logger = logging.getLogger(__name__) + +# Plugin details - All of these must be provided +id = "tails" +name = _("Tails") +description = _("Monitor multiple files, updating when they change. Just \ +like the tail command.\n\n\ +\ +Warning: When monitoring large files that grow quickly, this plugin may \ +cause massive memory usage.\n\n\ +Uses the pytailer library (http://code.google.com/p/pytailer/), licensed \ +under the LGPL. See %s and %s for more details." % ( os.path.join(__file__, "LICENSE" ), os.path.join(__file__, "README" ) ) ) +author = "Brett Smith " +copyright = _("Copyright (C)2011 Brett Smith, Michael Thornton") +site = "http://www.russo79.com/gnome15" +has_preferences = True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Previous line"), + g15driver.NEXT_SELECTION : _("Next line"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + g15driver.SELECT : _("Open file in browser") + } + +def create(gconf_key, gconf_client, screen): + return G15Tails(gconf_client, gconf_key, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15TailsPreferences(parent, driver, gconf_client, gconf_key) + +def changed(widget, key, gconf_client): + gconf_client.set_bool(key, widget.get_active()) + +class G15TailsPreferences(): + + def __init__(self, parent, driver, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "tails.ui")) + + # Feeds + self.file_model = widget_tree.get_object("FileModel") + self.reload_model() + self.file_list = widget_tree.get_object("FileList") + self.file_renderer = widget_tree.get_object("FileRenderer") + + # Lines + self.lines_adjustment = widget_tree.get_object("LinesAdjustment") + self.lines_adjustment.set_value(g15gconf.get_int_or_default(self._gconf_client, "%s/lines" % self._gconf_key, 10)) + + # Connect to events + self.lines_adjustment.connect("value-changed", self.lines_changed) + self.file_renderer.connect("edited", self.file_edited) + widget_tree.get_object("NewFile").connect("clicked", self.new_file) + widget_tree.get_object("RemoveFile").connect("clicked", self.remove_file) + + # Show dialog + self.dialog = widget_tree.get_object("TailsDialog") + self.dialog.set_transient_for(parent) + + ah = gconf_client.notify_add(gconf_key + "/files", self.files_changed); + self.dialog.run() + self.dialog.hide() + gconf_client.notify_remove(ah); + + def lines_changed(self, widget): + self._gconf_client.set_int(self._gconf_key + "/lines", int(widget.get_value())) + + def add_file(self, file_path): + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + if file_path in files: + files.remove(file_path) + files.append(file_path) + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + def file_edited(self, widget, row_index, value): + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + row_index = int(row_index) + if value != "": + if self.file_model[row_index][0] != value: + self.file_model.set_value(self.file_model.get_iter(row_index), 0, value) + files[row_index] = value + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + else: + self.file_model.remove(self.file_model.get_iter(row_index)) + del files[row_index] + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + def files_changed(self, client, connection_id, entry, args): + self.reload_model() + + def reload_model(self): + self.file_model.clear() + for url in self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING): + self.file_model.append([ url, True ]) + + def new_file(self, widget): + dialog = gtk.FileChooserDialog(_("Add file to monitor.."), + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_transient_for(self.dialog) + response = dialog.run() + if response == gtk.RESPONSE_OK: + self.file_model.append([dialog.get_filename(), True]) + self.add_file(dialog.get_filename()) + + dialog.destroy() + + def remove_file(self, widget): + (model, path) = self.file_list.get_selection().get_selected() + file = model[path][0] + files = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + if file in files: + files.remove(file) + self._gconf_client.set_list(self._gconf_key + "/files", gconf.VALUE_STRING, files) + + +class G15TailMenuItem(g15theme.MenuItem): + def __init__(self, id, line, file_path): + g15theme.MenuItem.__init__(self, id) + self.line = line + self.file = file_path + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry")) + + def get_theme_properties(self): + element_properties = g15theme.MenuItem.get_theme_properties(self) + element_properties["line"] = self.line + return element_properties + + def activate(self): + logger.info("xdg-open '%s'", self.file) + subprocess.Popen(['xdg-open', self.file]) + return True + +class G15TailThread(Thread): + def __init__(self, page): + Thread.__init__(self) + self.page = page + self.fd = None + self.line_seq = 0 + self.setDaemon(True) + self.setName("Monitor%s" % self.page.file_path) + self._stopped = False + + def stop_monitoring(self): + self._stopped = True + if self.fd is not None: + self.fd.close() + + def run(self): + for line in tailer.tail(open(self.page.file_path), self.page.plugin.lines): + g15screen.run_on_redraw(self._add_line, line) + self.fd = open(self.page.file_path) + try: + for line in tailer.follow(self.fd): + if self._stopped: + break + g15screen.run_on_redraw(self._add_line, line) + if self._stopped: + break + except ValueError as e: + logger.debug("Error while reading", exc_info = e) + if not self._stopped: + raise e + self.page.redraw() + + def _add_line(self, line): + line = line.strip() + if len(line) > 0 and not self._stopped: + line = g15markup.html_escape(line) + while self.page._menu.get_child_count() > self.page.plugin.lines: + self.page._menu.remove_child_at(0) + self.page._menu.add_child(G15TailMenuItem("Line-%d" % self.line_seq, line, self.page.file_path)) + self.page._menu.select_last_item() + self.line_seq += 1 + +class G15TailPage(g15theme.G15Page): + + def __init__(self, plugin, file_path): + + self._gconf_client = plugin._gconf_client + self._gconf_key = plugin._gconf_key + self._screen = plugin._screen + self._icon_surface = None + self._icon_embedded = None + self.plugin = plugin + self.file_path = file_path + self.thread = None + self.index = -1 + self._menu = g15theme.Menu("menu") + g15theme.G15Page.__init__(self, os.path.basename(file_path), self._screen, + thumbnail_painter=self._paint_thumbnail, + theme=g15theme.G15Theme(self, "menu-screen"), theme_properties_callback=self._get_theme_properties, + originating_plugin = plugin) + self.add_child(self._menu) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self._menu)) + self._reload() + self._screen.add_page(self) + self._screen.redraw(self) + self.on_deleted = self._stop + + """ + Private + """ + + def _reload(self): + icons = [] + mime_type = mime.get_type(self.file_path) + if mime_type != None: + icons.append(str(mime_type).replace("/","-")) + icons.append("text-plain") + icons.append("panel-searchtool") + icons.append("gnome-searchtool") + icon = g15icontools.get_icon_path(icons, size=self.plugin._screen.height) + + if icon is None: + self._icon_surface = None + self._icon_embedded = None + else: + try : + icon_surface = g15cairo.load_surface_from_file(icon) + self._icon_surface = icon_surface + self._icon_embedded = g15icontools.get_embedded_image_url(icon_surface) + except Exception as e: + logger.warning("Failed to get icon %s", str(icon), exc_info = e) + self._icon_surface = None + self._icon_embedded = None + + self._stop() + if os.path.exists(self.file_path): + self._subtitle = time.strftime('%Y-%m-%d %H:%M', time.localtime(os.path.getmtime(self.file_path))) + self._message = "" + self.thread = G15TailThread(self) + self.thread.start() + else: + self._subtitle = "" + self._message = "File does not exist" + + def _stop(self): + if self.thread is not None: + self.thread.stop_monitoring() + self.thread = None + + def _get_theme_properties(self): + properties = {} + properties["title"] = self.title + properties["icon"] = self._icon_embedded + properties["subtitle"] = self._subtitle + properties["message"] = self._message + properties["alt_title"] = "" + return properties + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + if self._icon_surface: + return g15cairo.paint_thumbnail_image(allocated_size, self._icon_surface, canvas) + +class G15Tails(): + + def __init__(self, gconf_client, gconf_key, screen): + self._screen = screen; + self._gconf_key = gconf_key + self._gconf_client = gconf_client + + def activate(self): + self._pages = {} + self._lines_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/lines", self._lines_changed) + self._files_changed_handle = self._gconf_client.notify_add(self._gconf_key + "/files", self._files_changed) + self._load_files() + + def deactivate(self): + self._gconf_client.notify_remove(self._lines_changed_handle); + self._gconf_client.notify_remove(self._files_changed_handle); + for page in self._pages: + self._screen.del_page(self._pages[page]) + self._pages = {} + + ''' + Private + ''' + + def destroy(self): + pass + + def _lines_changed(self, client, connection_id, entry, args): + self._load_files() + + def _files_changed(self, client, connection_id, entry, args): + self._load_files() + + def _load_files(self): + self.lines = g15gconf.get_int_or_default(self._gconf_client, "%s/lines" % self._gconf_key, 10) + file_list = self._gconf_client.get_list(self._gconf_key + "/files", gconf.VALUE_STRING) + + def init(): + # Add new pages + for file_path in file_list: + if not file_path in self._pages: + pg = G15TailPage(self, file_path) + self._pages[file_path] = pg + else: + self._pages[file_path]._reload() + + # Remove pages that no longer exist + to_remove = [] + for file_path in self._pages: + page = self._pages[file_path] + if not page.file_path in file_list: + self._screen.del_page(page) + to_remove.append(file_path) + for page in to_remove: + del self._pages[page] + g15screen.run_on_redraw(init) + diff --git a/src/plugins/tails/tails.ui b/src/plugins/tails/tails.ui new file mode 100644 index 0000000..6eebe35 --- /dev/null +++ b/src/plugins/tails/tails.ui @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + 1 + 100 + 1 + 1 + 1 + + + 320 + False + 5 + Tails Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + toolbutton1 + True + gtk-add + + + False + True + + + + + True + False + toolbutton2 + True + gtk-remove + + + False + True + + + + + False + False + 0 + + + + + True + True + automatic + automatic + in + + + 200 + True + True + FileModel + False + False + 0 + + + URL + + + + 1 + 0 + + + + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Files</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + Show + + + True + True + 0 + + + + + True + True + + False + False + True + True + LinesAdjustment + + + True + True + 1 + + + + + True + False + lines + + + True + True + 2 + + + + + + + + + True + False + <b>Options</b> + True + + + + + True + True + 1 + + + + + True + True + 0 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/things/Makefile.am b/src/plugins/things/Makefile.am new file mode 100644 index 0000000..771aa9e --- /dev/null +++ b/src/plugins/things/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = cg.stuff clouds.stuff + +plugindir = $(datadir)/gnome15/plugins/things +plugin_DATA = things.py \ + test1.py \ + cloudsthingum.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/things/cg.stuff/Makefile.am b/src/plugins/things/cg.stuff/Makefile.am new file mode 100644 index 0000000..02f231c --- /dev/null +++ b/src/plugins/things/cg.stuff/Makefile.am @@ -0,0 +1,5 @@ +cgstuffdir = $(datadir)/gnome15/plugins/things/cg.stuff +cgstuff_DATA = cairo.svg + +EXTRA_DIST = \ + $(cgstuff_DATA) diff --git a/src/plugins/things/cg.stuff/cairo.svg b/src/plugins/things/cg.stuff/cairo.svg new file mode 100644 index 0000000..770248e --- /dev/null +++ b/src/plugins/things/cg.stuff/cairo.svg @@ -0,0 +1,554 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/things/clouds.stuff/Makefile.am b/src/plugins/things/clouds.stuff/Makefile.am new file mode 100644 index 0000000..b5cad5e --- /dev/null +++ b/src/plugins/things/clouds.stuff/Makefile.am @@ -0,0 +1,6 @@ +cloudsstuffdir = $(datadir)/gnome15/plugins/things/clouds.stuff +cloudsstuff_DATA = README \ + clouds.svg + +EXTRA_DIST = \ + $(cloudsstuff_DATA) diff --git a/src/plugins/things/clouds.stuff/README b/src/plugins/things/clouds.stuff/README new file mode 100644 index 0000000..2bfa351 --- /dev/null +++ b/src/plugins/things/clouds.stuff/README @@ -0,0 +1,5 @@ +The following images are Copyright (C) 2009 Donn.C.Ingle and may be redistributed +and/or modified under the terms of the GNU General Public License as published +by the Free Software Foundation, either version 3 of the License, or (at your +option) any later version. +1. clouds.svg diff --git a/src/plugins/things/clouds.stuff/clouds.svg b/src/plugins/things/clouds.stuff/clouds.svg new file mode 100644 index 0000000..8481439 --- /dev/null +++ b/src/plugins/things/clouds.stuff/clouds.svg @@ -0,0 +1,1355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/things/cloudsthingum.py b/src/plugins/things/cloudsthingum.py new file mode 100644 index 0000000..01b223c --- /dev/null +++ b/src/plugins/things/cloudsthingum.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +## Things Copyright(C) 2009 Donn.C.Ingle +## +## Contact: donn.ingle@gmail.com - I hope this email lasts. +## +## This file is part of Things. +## +## Things is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Things 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Things. If not, see . + + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * + + + +## ---- General +SKYHEXBLUE="#aaccff"; SKYBLUE=hexfloat(SKYHEXBLUE) + +class BlueSky(DrawThing): + R = cairo.RadialGradient(0,200,0,0,200,400) + R.add_color_stop_rgb(0, 1,1,1 ) + R.add_color_stop_rgb(1, *SKYBLUE ) + def draw(self,ctx,fr): + ctx.set_source( BlueSky.R ) + ctx.paint() + +class Cloud1(DrawThing): + def draw(self,ctx,fr): + BOS['clouds:cloud1'].draw(ctx) +class Cloud2(DrawThing): + def draw(self,ctx,fr): + BOS['clouds:cloud2'].draw(ctx) + +class Puffer(Thing): + def __init__(self,smax,smin): + Thing.__init__(self) + self.keys("#----------------#----------------------#--------#",Props(),Props(sz=smax),Props(sz=smin),Props() ) + +class CloudA(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#" + "-"*190 + "#" + "-"*190 + "#",Props(x=250),Props(x=-250),Props(x=250)) + self.loops=True + + self.P = Puffer(1.5,0.6) + self.P.add( Cloud1() ) + self.add( self.P) + +class CloudB(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#" + "-"*90 + "#" + "-"*90 + "#",Props(x=-220),Props(x=220),Props(x=-220)) + self.loops=True + self.P = Puffer(1.2,0.8) + self.P.add( Cloud2() ) + self.add( self.P) + +class HugeCloud(Thing): + def __init__(self): + Thing.__init__(self) + self.keys('#',Props(sz=3,a=0.5, x=-90, y=-80)) + self.add( Cloud1() ) + +class SpinCity(Thing): + def __init__(self): + Thing.__init__(self) + ## Let's use the Python multiply string trick to get lots of tween frames: + self.keys ( "#" + "-"*250 + "#", Props(), Props(rot=-pi2)) + + def draw(self,ctx,fr): + BOS['clouds:city'].draw(ctx) + +## Here we use two LoopThings. +class Walking(LoopThing): + ## The legs - on the loops->walkloop layer in the Inkscape SVG file. + def __init__(self): + LoopThing.__init__(self) + self.keys("#--#---#---#---#---#----#---#---#---#---#===",Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props(),Props()) + + self.addLoop( BOS["clouds:walkloop"]) +class Torso(LoopThing): + ## The 'torso' -- loops->torsoloop layer in the SVG + def __init__(self): + LoopThing.__init__(self) + self.keys("#--#---#---#",Props(),Props(),Props(),Props()) + + self.addLoop( BOS["clouds:torsoloop"]) + +class Walker(Thing): + def __init__(self): + Thing.__init__(self) + self.keys('#',Props()) + self.add( Walking() ) + self.add( Torso(), globalProps=Props(x=-10, y=10) ) + +class Madness(Thing): + ### This is our 'main' Thing. It holds all the action. + def __init__(self): + Thing.__init__(self) + self.keys("#",Props()) + self.loops = False + + self.add( SpinCity(), globalProps=Props(y=250,sz=1.3), layer=10) + self.add( CloudA(), layer=5 ) + self.add( CloudB(), layer=20 ) + self.add( HugeCloud(), layer=1) + + self.add( Walker(), globalProps=Props(sz=1, y=100),layer=30 ) + +## BEGIN THE APP + +## Get a Bag of stuff +BOS = BagOfStuff() + +# Add stuff to it +BOS.add(os.path.join(os.path.dirname(__file__), "clouds.stuff/clouds.svg"),"clouds") diff --git a/src/plugins/things/test1.py b/src/plugins/things/test1.py new file mode 100644 index 0000000..ec1ffdf --- /dev/null +++ b/src/plugins/things/test1.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +## Things Copyright(C) 2009 Donn.C.Ingle +## +## Contact: donn.ingle@gmail.com - I hope this email lasts. +## +## This file is part of Things. +## +## Things is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Things 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Things. If not, see . + + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * + +## NOTE: +## Head down to the end: look for the first scene (class FadeStart) and work from there. +## Written with Version 0.1 of the API: 2 May 2009 + + +## ---- General +CAIROHEXBLUE="#162284"; CAIROBLUE=hexfloat(CAIROHEXBLUE) + +class Backdrop(Thing): + ## This Thing holds three frames with three draw methods; one for each. + ## We use the frame number to decide which to employ. + ## This thing serves as a background drawer for each scene. + def __init__(self): + Thing.__init__(self) + self.keys ( "#==", Props()) + self.stops( "^^^" ) + self.loops=False + + self.draws=[self.draw1,self.draw2,self.draw3] + + self.L = cairo.LinearGradient(0, -300, 0, 300) + self.L.add_color_stop_rgba(0, 1, 1, 1, 1) + self.L.add_color_stop_rgba(0.25, 0, 0.6, 1, 1) + self.L.add_color_stop_rgba(0.5, 0, 0.8, 1, 1) + self.L.add_color_stop_rgba(1, 1, 1, 1, 1) + + self.yell = hexfloat("#dfaa00") + + self.R = cairo.RadialGradient(0,-20,50,0,-20,300) + self.R.add_color_stop_rgb(0, 1,1,1) + self.R.add_color_stop_rgb(1, *self.yell) + def draw(self, ctx, fr): + self.draws[fr-1](ctx) + + def draw1(self, ctx): + ctx.set_source(self.L) + ctx.paint() + def draw2(self, ctx): + ctx.set_source(self.R) + ctx.paint() + def draw3(self, ctx): + ctx.set_source_rgb(*self.yell) + ctx.paint() + +class ScarabShape(DrawThing): + def draw(self, ctx, fr): + BOS["CG:cairo_scarab"].draw(ctx) +SCARABsh=ScarabShape() + +## ----------------------------------- SCENE 3 + +class Exit(Thing): + def __init__(self, app): + Thing.__init__(self) + self.keys ( "#======================================================================================.", Props()) + self.stops ( ".......................................................................................^") + self.funcs ( "^.....................................................................................^", (BACKDROP.goStop,3), app.quit ) + + class EndScarab(Thing): + def __init__(self): + Thing.__init__(self) + self.keys( "#----------------------------------#---------------------------#",Props(),Props(sz=6,rot=pi),Props(a=0,rot=pi2)) + self.loops=False + self.add(SCARABsh) + self.add( EndScarab() ) + + #def draw(self,ctx,fr): + # ctx.set_source_rgb(0,0,0) + # ctx.paint() + +## ----------------------------------- SCENE 2 + +class ClipWord(ClipThing): + def __init__(self): + ClipThing.__init__(self) + self.keys( "#", Props()) + self.loops = False + + class CairoWord(Thing): + def __init__(self): + Thing.__init__(self) + self.keys( ".#-----------------------------------------------------------------#", Props(y=45),Props()) + self.stops( "^..................................................................^") + self.funcs( "..................................................................^",self.dostuff ) + def dostuff(self): + self.parentThing.parentThing.SUN.goPlay(2) + + def draw(self, ctx, fr): + BOS["CG:cairo_word"].draw(ctx) + self.CAIROWORD = CairoWord() + self.add( self.CAIROWORD ) + + def draw(self,ctx,fr): + ctx.rectangle(-150,-35,260,65) + + +# Prep some shapes from the SVG file +class ArrowButtonN(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_normal"].draw(ctx) +class ArrowButtonO(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_over"].draw(ctx) +class ArrowButtonD(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_down"].draw(ctx) +class ArrowButtonU(DrawThing): + def draw(self,ctx,fr): + BOS["CG:button_right_up"].draw(ctx) + +class Next(Thing): + def __init__(self): + Thing.__init__(self) + self.keys("#--#-----#", Props(sz=2,a=0),Props(sz=0.5),Props()) + self.loops = False + # Just define the button within myself -- it shows the relationship better. + class NextButton(ButtonThing): + """ButtonThings cannot have keys().""" + def __init__(self): + ButtonThing.__init__(self,"testbutton") + self.addStates({"normal":ArrowButtonN(),"over":ArrowButtonO(),"down":ArrowButtonD(),"up":ArrowButtonU()} ) + def drawHitarea(self,ctx,frame): + ctx.rectangle(-14,-14,30,30) + def onButtonUp(self): + self.parentThing.parentThing.parentThing.goPlay('hide') + + self.add(NextButton()) + +class IntroText(Thing): + def __init__(self): + Thing.__init__(self) + self.keys ( "#----------------------------------------------------------------#",Props(a=0),Props(a=1)) + self.stops( ".................................................................^") + + ## Button appears a little later in the animation: + self.add( Next(), parentFrame=65, globalProps=Props(x=200,y=240),layer=20) + + fname = "Sans 10" + + txt= """Thanks to Cairo, Python and many others, there is now an easy way to produce vector animations in Python code. This library is called "Things" and it's what you are seeing right now. + +It needs work. It's slow and inefficient. But, oddly, it runs! If it could be converted into a C library "Things" would really start to cook! A GUI timeline & on-canvas designer would then be possible. + +"Things" works alongside Inkscape. You can pull items out by id and employ them in the API. You can also add images and font files as you need them. + +Please check it out and hack!""" + + self.text = '%s' % (fname, CAIROHEXBLUE, txt ) + + self.tbox = TextBlock() + self.tbox.setup(self.text, x=-250, y=40, align=pango.ALIGN_LEFT, width=500) + def draw(self,ctx,fr): + self.tbox.draw(ctx) + + + +class BlueBox(Thing): + def __init__(self): + Thing.__init__(self) + self.keys ( "#-------------------------#==================",Props(sx=0.1),Props()) + self.stops ( "..........................^.................^") + self.funcs ( ".........................^.^", self.tell,self.dostuff) + self.labels( "...........................^", "grow") + + self.h=1 + def dostuff(self): + ## Change a flag so draw can do stuff. + self.h=2 + + def tell(self): + ## Tell logo to pop-up. + self.parentThing.CAIROCLIP.CAIROWORD.play() + + def draw(self,ctx,fr): + ## Rather than tween this box down, I draw it bigger every time, to avoid distortion. + if self.h > 1: + fr = self.h + self.h += 1 + if self.h > 25: self.h=25 + + ctx.rectangle(-260,30,510,((self.h-1)*10)) + + # The blue outline that grows. + ctx.set_line_width(5) + ctx.set_source_rgb(*CAIROBLUE) # The * means make a list of CAIROBLUE -- so it's passed into the func as three params! + ctx.stroke_preserve() + ctx.set_source_rgba(1,1,1,0.7) + ctx.fill() + +class RisingSun(Thing): + def __init__(self): + Thing.__init__(self) + ## Starts on a blank + stop frame : i.e. it is not visible at first. + ## Somewhere there will be a command to tell this to play from frame 2. + self.keys ( ".#----#--#--------#",Props(a=0,sz=2),Props(sz=0.5),Props(sz=2),Props() ) + self.stops ( "^.................^") + self.funcs ( ".................^",self.dostuff) + + ## Here we add a pre-prepared Thing. It will draw itself. + self.add( SCARABsh ) + + def dostuff(self): + ## A func to tell some other thing to do something. + self.parentThing.BLUEBOX.goPlay("grow") + + +class IntroduceLogo(Thing): + """ + This is the main Thing in scene 2. It was elected thus when we added it to the scene2 var. + """ + def __init__(self, app): + Thing.__init__(self) + self.keys ( "#==============.", Props()) + self.stops ( ".........^.....^") + ## I want to spend some time simply + ## looping so that sub-animations get a chance to + ## finish. Hence this ^.^ cute hello-kitty stuff: + self.labels( ".^.^......^" ,"pause","go","hide") + self.funcs ( "^.^...........^", (BACKDROP.goStop,2), self.Delay, app.playNextScene ) + + ## Add the elements of my animation: + ## Some are given instance in self because I will refer to them from elsewhere. + self.BLUEBOX=BlueBox() + self.add( self.BLUEBOX, layer=12) + + self.CAIROCLIP = ClipWord() + self.add( self.CAIROCLIP, layer=11) + + self.SUN=RisingSun() + self.add( self.SUN, layer=0, globalProps=Props(x=5,y=-70)) + + ## This one starts on frame 6, just after the delay. + self.add( IntroText(), layer=15, parentFrame=6) # It's instanced on-the-fly. + + ## Used in Delay func. + self.countdown=60 + + def Delay(self): + ## So, we are on frame 3 and this func is called. + self.countdown -=1 + if self.countdown == 0: + self.goPlay("go") # all done, continue animation. + else: + self.goPlay("pause") # rewind and loop again. + + + +## ----------------------------------- SCENE 1 +class BuzzWord(Thing): + def __init__(self, pFrom, buzz): + Thing.__init__(self) + pFrom2 = Props(x=pFrom.y/2, y=pFrom.x/2,sz=5) + self.keys ( "#----------------------#----------------------------------------------------------#",pFrom,Props(),pFrom2) + ## Run a func in myself on the last frame. + self.funcs ( "..................................................................................^", self.atend) + + self.loops=False + self.buzz=buzz + + fname = "Serif 11" + self.text = '%s' % (fname, buzz ) + self.tbox = TextBlock() + self.tbox.setup(self.text, x=0, y=0, align=pango.ALIGN_CENTER) + def draw(self,ctx,fr): + self.tbox.draw(ctx) + def atend(self): + ## I am finished, so tell my parent. + self.parentThing.atend(self.buzz) # pass my buzz phrase. + +class BuzzWords(Thing): + def __init__(self): + Thing.__init__(self) + ## We provide a bunch of frames because we want to start manu instances + ## of a Thing -- and space them out every so-many frames. + self.keys ( "#=============================",Props()) + self.loops=False + + bzz=["Animation","Vectors","Cairo","Python","Tweening","Keyframes","Simple","API","Things","GPL"] + for bw in range(0,10): + x,y=circrandom(600) + BW = BuzzWord( Props(x=x,y=y,sz=20), bzz[bw] ) + self.add( BW, parentFrame=bw*3) # Here we start each one on different frames; this staggers the animation. + + def atend(self,buzz): + ## Am I done with the animations? + ## only the last one in the list is what we want. + if buzz=="GPL": + self.parentThing.play() # We tell the parentThing (FadeStart) to carry on playing now. + +class FadeStart(Thing): + """ + Start looking here to understand the whole animation. + """ + def __init__(self, app): + Thing.__init__(self) + ## We have a stop on frame 2. The BuzzWords() Thing is playing all the time, but this + ## timeline does not go past frame 2; until we tell it to... + self.keys ( "##----------#.", Props(),Props(),Props(a=0,sz=0.1)) + self.stops ( ".^...........^") + ## When this gets to the end, it will run a method of app: + self.funcs ( "............^", app.playNextScene ) # Off we go to scene2! + + self.add( BuzzWords() ) + + +## Get a Bag of stuff +BOS = BagOfStuff() + +# Add stuff to it +BOS.add(os.path.join(os.path.dirname(__file__), "cg.stuff/cairo.svg"),"CG") + +## Add Things to app +BACKDROP=Backdrop() + diff --git a/src/plugins/things/things.py b/src/plugins/things/things.py new file mode 100644 index 0000000..557791d --- /dev/null +++ b/src/plugins/things/things.py @@ -0,0 +1,164 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15driver as g15driver +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import logging +logger = logging.getLogger(__name__) + +from Things.ThingsApp import * +from Things.Thinglets import * +from Things.BoxOfTricks import * +from Things.OutputDevice import * + + +# Plugin details - All of these must be provided +id="things" +name="Things" +description="Integrates the Things. A python animation API. Doesn't do anything " + \ + "by itself, but provides a framework for other plugins to add " + \ + "animations and special effects" +author="Brett Smith " +copyright="Copyright (C)2011 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11 ] + +def create(gconf_key, gconf_client, screen): + return G15Things(gconf_key, gconf_client, screen) + +class G15ThingOutputDevice(OutputDevice): + + def __init__(self, canvas_width, canvas_height, screen): + OutputDevice.__init__(self) + self._screen = screen + self._windowSize = (canvas_width, canvas_height, canvas_width, canvas_height) + + def is_button_press(self, e): + return False + + def is_button_release(self, e): + return False + + def is_motion(self, e): + return False + + def comeToLife(self, owner): + self.owner = owner + g15scheduler.schedule("ThingPaint", self.owner.speed / 1000.0, self._mainLoop) + + ## This gives life to the whole show. + def _mainLoop(self): + """ + Private + ======= + + Called by timeout in comeToLife. Keeps looping on timeout. This is the heart of the app. + + """ + if self.pauseapp: return True + self.owner._tick() + self._screen.redraw() + if not self.stack.quitApp: + g15scheduler.schedule("ThingPaint", self.owner.speed / 1000.0, self._mainLoop) + +class G15ThingPainter(g15screen.Painter): + + def __init__(self, screen): + g15screen.Painter.__init__(self, g15screen.BACKGROUND_PAINTER, -9999) + self.output_device = G15ThingOutputDevice(screen.available_size[0], screen.available_size[1], screen) + self.screen = screen + self.app2() + + def app1(self): + ## BEGIN THE APP + + ## Get an app ref. + ## Fiddle with the speed param. Make it bigger if you want the animation slower. + + import test1 + + app = AllThings ( self.screen.available_size[0], self.screen.available_size[1], speed = 20, output = self.output_device) + app.add(test1.BACKDROP) + + ## Make some scene Things to hold many items each + scene1 = test1.SceneThing() + scene2 = test1.SceneThing() + scene3 = test1.SceneThing() + + ## Add Things to each scene + scene1.add( test1.FadeStart(app) ) + scene2.add( test1.IntroduceLogo(app) ) + scene3.add( test1.Exit(app) ) + + ## Add the scences to the app + app.add( scene1 ) + app.add( scene2 ) + app.add( scene3 ) + + ## Tell it which one to start with + app.startScene(1) + + + #app.showGrid() # optional for debugging + + ## Bring app to life! + app.comeToLife ( ) + + def app2(self): + + ## Get an app ref. + ## Fiddle with the speed param. Make it bigger if you want the animation slower. + import cloudsthingum + app = AllThings ( self.screen.available_size[0], self.screen.available_size[1], speed = 20, output = self.output_device) + app.add( cloudsthingum.BlueSky() ) + ## Add the main thing to the app + app.add( cloudsthingum.Madness() ) + + app.panZoom(True) + ## Bring app to life! + app.comeToLife ( ) + + def paint(self, canvas): + canvas.save() + self.output_device.stack._expose(canvas) + canvas.restore() + +class G15Things(): + + def __init__(self, gconf_key, gconf_client, screen): + self.screen = screen + self.gconf_client = gconf_client + self.gconf_key = gconf_key + self.target_surface = None + self.target_context = None + + def activate(self): + self.bg_img = None + self.this_image = None + self.current_style = None + self.notify_handlers = [] + self.painter = G15ThingPainter(self.screen) + self.screen.painters.append(self.painter) + + def deactivate(self): + self.screen.painters.remove(self.painter) + self.screen.redraw() + + def destroy(self): + pass \ No newline at end of file diff --git a/src/plugins/trafficstats/Makefile.am b/src/plugins/trafficstats/Makefile.am new file mode 100644 index 0000000..7226263 --- /dev/null +++ b/src/plugins/trafficstats/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/trafficstats +plugin_DATA = \ + trafficstats.ui \ + trafficstats.png \ + trafficstats.py + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/trafficstats/default/Makefile.am b/src/plugins/trafficstats/default/Makefile.am new file mode 100644 index 0000000..740d1c4 --- /dev/null +++ b/src/plugins/trafficstats/default/Makefile.am @@ -0,0 +1,27 @@ +themedir = $(datadir)/gnome15/plugins/trafficstats/default +theme_DATA = \ + g19.svg \ + mx5500.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/trafficstats/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/trafficstats/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/trafficstats/default/default.svg b/src/plugins/trafficstats/default/default.svg new file mode 100644 index 0000000..95fe685 --- /dev/null +++ b/src/plugins/trafficstats/default/default.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + + + ${sup} + ${sdn} + + + ${message} + + diff --git a/src/plugins/trafficstats/default/g19.svg b/src/plugins/trafficstats/default/g19.svg new file mode 100644 index 0000000..7b5fabc --- /dev/null +++ b/src/plugins/trafficstats/default/g19.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + ${sup} + ${sdn} + + + + ${message} + + diff --git a/src/plugins/trafficstats/default/mx5500.svg b/src/plugins/trafficstats/default/mx5500.svg new file mode 100644 index 0000000..6bce46a --- /dev/null +++ b/src/plugins/trafficstats/default/mx5500.svg @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${title} + ${des1} + ${dup1} + ${des2} + ${dup2} + ${des3} + ${dup3} + ${ddn1} + ${d} + ${ddn2} + ${d} + ${ddn3} + ${d} + ${sup} + ${sdn} + + + ${message} + + diff --git a/src/plugins/trafficstats/trafficstats.png b/src/plugins/trafficstats/trafficstats.png new file mode 100644 index 0000000000000000000000000000000000000000..a38bbfd9632058030086d5b1f9b3f87868040847 GIT binary patch literal 489 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYYs>cdx@v7EBjqG9#K|?0~J-%fkKict`Q~9`MJ5Nc_j?aMX8A;sVNHO znI#zt?w-B@;f;La3=E8co-U3d9>?EK-RsBXDB^aWOPAAn>z;-^T^BB#V9p7!&Z%H< zjPVikj_~c!HS6d%6ga(f*%2mTA)b0k?#2^`l1!R8JGaj|xBA-`+9Y&6}QeRdx2>1hd&M*>8N~WjOrMdSQTv{(-_%J;xtg zu2>PSbpG>)J$wwEM+`4!gaF0!7zGZg%=CG&H|4md{_&kL@1E!0%CJd{l$>=o-EHwi z|5@j{Oq^!Sj{kD#&7ZJ`yUIOoM$PO#-tVzA=txp~L>L>xWfRS*>zLOjDIX|(KQV|& zpwmTyhb{Wn<_-ZR0fBG&HNJW+ow@b%{JcEZemjt26#33~xxbkj|CAGJLngePzV*pJ z@#yvTm)`FW)QG8jz0CVSiPh6LUu3qrUcZ0Cw74P2YvuJg|EHhBz1(#^v6Wr?-@f^p dYjyNNc7u#_H?A;eD+6PI!PC{xWt~$(695yQ$4&qM literal 0 HcmV?d00001 diff --git a/src/plugins/trafficstats/trafficstats.py b/src/plugins/trafficstats/trafficstats.py new file mode 100644 index 0000000..49fbbf0 --- /dev/null +++ b/src/plugins/trafficstats/trafficstats.py @@ -0,0 +1,307 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2013 NoXPhasma +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("trafficstats", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.g15theme as g15theme +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15os as g15os +import gnome15.g15actions as g15actions +import gnome15.g15devices as g15devices +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import time +import datetime +import logging +logger=logging.getLogger(__name__) +try: + import gtop +except Exception as e: + logger.debug("Could not import gtop. Falling back to g15top", exc_info = e) + # API compatible work around for Ubuntu 12.10 + import gnome15.g15top as gtop +import os +import gtk +import locale + +# Plugin details - All of these must be provided +id="trafficstats" +name=_("Traffic Stats") +description=_("Displays network traffic stats. Either of actual session or from vnstat.") +author="NoXPhasma " +copyright=_("Copyright (C)2013 NoXPhasma") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +ICON=os.path.join(os.path.dirname(__file__), 'trafficstats.png') +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + g15driver.PREVIOUS_SELECTION : _("Switch daily/monthly stats (Only vnstat)"), + g15driver.NEXT_SELECTION : _("Switch network device") + } + +# +# This plugin displays the network traffic stats +# + +''' +This function must create your plugin instance. You are provided with +a GConf client and a Key prefix to use if your plugin has preferences +''' +def create(gconf_key, gconf_client, screen): + return G15TrafficStats(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "trafficstats.ui")) + dialog = widget_tree.get_object("TrafficStats") + + # Resets the value of the use_vnstat flag if vnstat is not installed + vnstat_installed = g15os.is_program_in_path('vnstat') + if not vnstat_installed: + gconf_client.set_bool("%s/use_vnstat" % gconf_key, False) + + # Displays a warning message to the user if vnstat is not installed + warning = widget_tree.get_object("NoVnstatMessage") + warning.set_visible(not vnstat_installed) + + # Disables the vnstat checkbox if vnstat is not installed + use_vnstat = widget_tree.get_object('UseVnstat') + use_vnstat.set_sensitive(vnstat_installed) + + g15uigconf.configure_checkbox_from_gconf(gconf_client, gconf_key + "/use_vnstat", "UseVnstat", vnstat_installed, widget_tree) + ndevice = widget_tree.get_object("NetDevice") + for netdev in gtop.netlist(): + ndevice.append([netdev]) + g15uigconf.configure_combo_from_gconf(gconf_client, gconf_key + "/networkdevice", "NetworkDevice", "lo", widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, gconf_key + "/refresh_interval", "RefreshingScale", 10.0, widget_tree) + dialog.set_transient_for(parent) + dialog.run() + dialog.hide() + +class G15TrafficStats(g15plugin.G15RefreshingPlugin): + + ''' + ****************************************************************** + * Lifecycle functions. You must provide activate and deactivate, * + * the constructor and destroy function are optional * + ****************************************************************** + ''' + + def __init__(self, gconf_key, gconf_client, screen): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, \ + screen, ICON, id, name, g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/refresh_interval", 10.0)) + self.hidden = False + + def activate(self): + ''' + The activate function is invoked when gnome15 starts up, or the plugin is re-enabled + after it has been disabled. When extending any of the provided base plugin classes, + you nearly always want to call the function in the supoer class as well + ''' + self._load_configuration() + + g15plugin.G15RefreshingPlugin.activate(self) + + ''' + Most plugins will delegate their drawing to a 'Theme'. A theme usually consists of an SVG file, one + for each model that is supported, and optionally a fragment of Python for anything that can't + be done with SVG and the built in theme facilities + ''' + self._reload_theme() + + self.watch(None, self._config_changed) + + self.page.title = "Traffic Stats" + + ''' + Once created, we should always ask for the screen to be drawn (even if another higher + priority screen is actually active. If the canvas is not displayed immediately, + the on_shown function will be invoked when it finally is. + ''' + self.screen.redraw(self.page) + + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15RefreshingPlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_SELECTION and self.use_vnstat is True: + if self.loadpage == 'vnstat_daily': + self.gconf_client.set_string(self.gconf_key + "/vnstat_view", "vnstat_monthly") + else: + self.gconf_client.set_string(self.gconf_key + "/vnstat_view", "vnstat_daily") + return True + elif binding.action == g15driver.NEXT_SELECTION: + if self.networkdevice is not None: + # get all network devices + self.net_data = gtop.netlist() + # set network device id +1, to get next device + idx = self.net_data.index(self.networkdevice) + 1 + # if next device id is not present, take first device + if idx >= len(self.net_data): + idx = 0 + self.gconf_client.set_string(self.gconf_key + "/networkdevice", self.net_data[idx]) + return True + + def destroy(self): + ''' + Invoked when the plugin is disabled or the applet is stopped + ''' + pass + + def _config_changed(self, client, connection_id, entry, args): + + ''' + Load the gconf configuration + ''' + self._load_configuration() + + ''' + This is called when the gconf configuration changes. See add_notify and remove_notify in + the plugin's activate and deactive functions. + ''' + self.do_refresh() + + ''' + Reload the theme as the layout required may have changed (i.e. with the 'show date' + option has been change) + ''' + self._reload_theme() + + ''' + In this case, we temporarily raise the priority of the page. This will force + the page to be painted (i.e. the paint function invoked). After the specified time, + the page will revert it's priority. Only one revert timer is active at any one time, + so it is safe to call this function in quick succession + ''' + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 3.0) + + def _load_configuration(self): + self.use_vnstat = g15gconf.get_bool_or_default(self.gconf_client, self.gconf_key + "/use_vnstat", os.path.isfile("/usr/bin/vnstat")) + self.networkdevice = g15gconf.get_string_or_default(self.gconf_client, self.gconf_key + "/networkdevice", 'lo') + self.loadpage = g15gconf.get_string_or_default(self.gconf_client, self.gconf_key + "/vnstat_view", "vnstat_daily") + self.refresh_interval = g15gconf.get_float_or_default(self.gconf_client, self.gconf_key + "/refresh_interval", 10.0) + + ''' + *********************************************************** + * Functions specific to plugin * + *********************************************************** + ''' + + def _reload_theme(self): + variant = None + self.theme = g15theme.G15Theme(os.path.join(os.path.dirname(__file__), "default"), variant) + + ''' + Get the properties dictionary + ''' + + def get_theme_properties(self): + properties = { } + + def convert_bytes(bytes): + bytes = float(bytes) + if bytes >= 1099511627776: + terabytes = bytes / 1099511627776 + size = '%.2fT' % terabytes + elif bytes >= 1073741824: + gigabytes = bytes / 1073741824 + size = '%.2fG' % gigabytes + elif bytes >= 1048576: + megabytes = bytes / 1048576 + size = '%.2fM' % megabytes + elif bytes >= 1024: + kilobytes = bytes / 1024 + size = '%.2fK' % kilobytes + else: + size = '%.2fb' % bytes + return size + + # Split vnstat data into array + def get_traffic_data(dataType, dataValue, vn): + line='' + for item in vn.split("\n"): + if "%s;%d;" % (dataType, dataValue) in item: + line = item.strip().split(';') + break + return line + + # convert MiB and KiB into KB + def cb(mib, kib): + return (int(mib) * 1000000) + (int(kib) * 1000) + + ''' + Get the details to display and place them as properties which are passed to + the theme + ''' + + if self.use_vnstat is False: + bootup = datetime.datetime.fromtimestamp(int(gtop.uptime().boot_time)).strftime('%d.%m.%y %H:%M') + sd = gtop.netload(self.networkdevice) + properties["sdn"] = "DL: " +convert_bytes(sd.bytes_in) + properties["sup"] = "UL: " +convert_bytes(sd.bytes_out) + properties["des1"] = "Traffic since: " +bootup + properties["title"] = self.networkdevice + " Traffic" + + else: + vnstat, vn = g15os.get_command_output('vnstat -i ' + self.networkdevice + ' --dumpdb') + if vnstat != 0: + properties["message"] = "vnstat is not installed!" + else: + chErr = str(vn.find("Error")); + if chErr != "-1": + properties["message"] = "No stats for device " + self.networkdevice + else: + properties["title"] = self.networkdevice +" Traffic (U/D)" + + def get_data(kind, period): + # get vnstat data as array, array content: 2 = unixtime, 4 = up MiB, 6 = up KiB, 3 = dn MiB, 5 = dn KiB + line = get_traffic_data(kind, period, vn) + if line[7] == '1': + up = convert_bytes(cb(line[4], line[6])) + dn = convert_bytes(cb(line[3], line[5])) + des = int(line[2]) + return [up, dn, des] + else: + return None + + if self.loadpage == 'vnstat_daily': + k = "d" + fmt = '%A' + elif self.loadpage == 'vnstat_monthly': + k = "m" + fmt = '%B' + + for p in range(0,3): + data = get_data(k,p) + if data is not None: + properties["d"] = "/" + properties["dup" + str(p + 1)] = data[0] + properties["ddn" + str(p + 1)] = data[1] + properties["des" + str(p + 1)] = datetime.datetime.fromtimestamp(data[2]).strftime(fmt) + + return properties diff --git a/src/plugins/trafficstats/trafficstats.ui b/src/plugins/trafficstats/trafficstats.ui new file mode 100644 index 0000000..79003fd --- /dev/null +++ b/src/plugins/trafficstats/trafficstats.ui @@ -0,0 +1,191 @@ + + + + + + + + + + + + False + 5 + Traffic Stats Config + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + False + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + Use vnstat? + True + True + False + False + True + + + False + True + 0 + + + + + True + False + The vnstat software is not available on this computer. + + + + + + False + True + 1 + + + + + True + False + True + + + 120 + True + False + 2 + Network Device + + + False + True + 0 + + + + + 190 + True + False + NetDevice + + + + 0 + + + + + False + True + 1 + + + + + False + True + 2 + + + + + True + False + True + + + 120 + True + False + Refresh Interval + + + False + True + 0 + + + + + 190 + True + True + adjustment1 + 1 + + + False + True + 1 + + + + + False + True + 3 + + + + + True + True + 1 + + + + + + + + + button1 + + + + 1 + 300 + 10 + 1 + 10 + + diff --git a/src/plugins/tweak/Makefile.am b/src/plugins/tweak/Makefile.am new file mode 100644 index 0000000..fb4f685 --- /dev/null +++ b/src/plugins/tweak/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/tweak +plugin_DATA = tweak.py \ + tweak.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/tweak/i18n/tweak.en_GB.po b/src/plugins/tweak/i18n/tweak.en_GB.po new file mode 100644 index 0000000..dbec32d --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.en_GB.po @@ -0,0 +1,110 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/tweak.glade.h:1 +msgid "Animation Delay" +msgstr "Animation Delay" + +#: i18n/tweak.glade.h:2 +msgid "Disable SVG Glow" +msgstr "Disable SVG Glow" + +#: i18n/tweak.glade.h:3 +msgid "Fade keyboard backlight on close" +msgstr "Fade keyboard backlight on close" + +#: i18n/tweak.glade.h:4 +msgid "Fade screen on close" +msgstr "Fade screen on close" + +#: i18n/tweak.glade.h:5 +msgid "Gnome15 Hidden Preferences" +msgstr "Gnome15 Hidden Preferences" + +#: i18n/tweak.glade.h:6 +msgid "Key Hold Duration" +msgstr "Key Hold Duration" + +#: i18n/tweak.glade.h:7 +msgid "Power off all lights on close" +msgstr "Power off all lights on close" + +#: i18n/tweak.glade.h:8 +msgid "Scroll Amount" +msgstr "Scroll Amount" + +#: i18n/tweak.glade.h:9 +msgid "Scroll Delay" +msgstr "Scroll Delay" + +#: i18n/tweak.glade.h:10 +msgid "Start each screen in it's own thread" +msgstr "Start each screen in it's own thread" + +#: i18n/tweak.glade.h:11 +msgid "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." +msgstr "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." + +#: i18n/tweak.glade.h:14 +msgid "Use XTEST for macros and macro recording" +msgstr "Use XTEST for macros and macro recording" + +#: i18n/tweak.glade.h:15 +msgid "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." +msgstr "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." + +#: i18n/tweak.glade.h:20 +msgid "center" +msgstr "center" + +#: i18n/tweak.glade.h:21 +msgid "ms" +msgstr "ms" + +#: i18n/tweak.glade.h:22 +msgid "scale" +msgstr "scale" + +#: i18n/tweak.glade.h:23 +msgid "stretch" +msgstr "stretch" + +#: i18n/tweak.glade.h:24 +msgid "tile" +msgstr "tile" + +#: i18n/tweak.glade.h:25 +msgid "zoom" +msgstr "zoom" diff --git a/src/plugins/tweak/i18n/tweak.glade.h b/src/plugins/tweak/i18n/tweak.glade.h new file mode 100644 index 0000000..5aaf196 --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.glade.h @@ -0,0 +1,25 @@ +char *s = N_("Animation Delay"); +char *s = N_("Disable SVG Glow"); +char *s = N_("Fade keyboard backlight on close"); +char *s = N_("Fade screen on close"); +char *s = N_("Gnome15 Hidden Preferences"); +char *s = N_("Key Hold Duration"); +char *s = N_("Power off all lights on close"); +char *s = N_("Scroll Amount"); +char *s = N_("Scroll Delay"); +char *s = N_("Start each screen in it's own thread"); +char *s = N_("The SVG glow effect currently\n" + "uses lots of CPU. Select this option\n" + "to remove the effect."); +char *s = N_("Use XTEST for macros and macro recording"); +char *s = N_("When enabled, the XTEST \n" + "extensions will be used for\n" + "recording macros and sending\n" + "keystrokes. When disable, raw\n" + "X11 events will be used."); +char *s = N_("center"); +char *s = N_("ms"); +char *s = N_("scale"); +char *s = N_("stretch"); +char *s = N_("tile"); +char *s = N_("zoom"); diff --git a/src/plugins/tweak/i18n/tweak.pot b/src/plugins/tweak/i18n/tweak.pot new file mode 100644 index 0000000..1b109fb --- /dev/null +++ b/src/plugins/tweak/i18n/tweak.pot @@ -0,0 +1,102 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/tweak.glade.h:1 +msgid "Animation Delay" +msgstr "" + +#: i18n/tweak.glade.h:2 +msgid "Disable SVG Glow" +msgstr "" + +#: i18n/tweak.glade.h:3 +msgid "Fade keyboard backlight on close" +msgstr "" + +#: i18n/tweak.glade.h:4 +msgid "Fade screen on close" +msgstr "" + +#: i18n/tweak.glade.h:5 +msgid "Gnome15 Hidden Preferences" +msgstr "" + +#: i18n/tweak.glade.h:6 +msgid "Key Hold Duration" +msgstr "" + +#: i18n/tweak.glade.h:7 +msgid "Power off all lights on close" +msgstr "" + +#: i18n/tweak.glade.h:8 +msgid "Scroll Amount" +msgstr "" + +#: i18n/tweak.glade.h:9 +msgid "Scroll Delay" +msgstr "" + +#: i18n/tweak.glade.h:10 +msgid "Start each screen in it's own thread" +msgstr "" + +#: i18n/tweak.glade.h:11 +msgid "" +"The SVG glow effect currently\n" +"uses lots of CPU. Select this option\n" +"to remove the effect." +msgstr "" + +#: i18n/tweak.glade.h:14 +msgid "Use XTEST for macros and macro recording" +msgstr "" + +#: i18n/tweak.glade.h:15 +msgid "" +"When enabled, the XTEST \n" +"extensions will be used for\n" +"recording macros and sending\n" +"keystrokes. When disable, raw\n" +"X11 events will be used." +msgstr "" + +#: i18n/tweak.glade.h:20 +msgid "center" +msgstr "" + +#: i18n/tweak.glade.h:21 +msgid "ms" +msgstr "" + +#: i18n/tweak.glade.h:22 +msgid "scale" +msgstr "" + +#: i18n/tweak.glade.h:23 +msgid "stretch" +msgstr "" + +#: i18n/tweak.glade.h:24 +msgid "tile" +msgstr "" + +#: i18n/tweak.glade.h:25 +msgid "zoom" +msgstr "" diff --git a/src/plugins/tweak/tweak.py b/src/plugins/tweak/tweak.py new file mode 100644 index 0000000..4b3650a --- /dev/null +++ b/src/plugins/tweak/tweak.py @@ -0,0 +1,62 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("tweak", modfile = __file__).ugettext + +import gnome15.util.g15uigconf as g15uigconf +import gtk +import os.path + +# Plugin details - All of these must be provided +id="tweak" +name=_("Tweak Gnome15") +description=_("Allows configuration of some hidden settings. These are mostly \ +performance tweaks. If Gnome15 is using too much CPU, \ +you will find adjusting some of these may reduce it. ") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +passive=True +global_plugin=True + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "tweak.ui")) + dialog = widget_tree.get_object("TweakDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/scroll_delay", "ScrollDelayAdjustment", 500, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/scroll_amount", "ScrollAmountAdjustment", 5, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/animation_delay", "AnimationDelayAdjustment", 100, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/animated_menus", "AnimatedMenus", True, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/key_hold_duration", "KeyHoldDurationAdjustment", 2000, widget_tree) + g15uigconf.configure_adjustment_from_gconf(gconf_client, "/apps/gnome15/usb_key_read_timeout", "UsbKeyReadTimeoutAdjustment", 100, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/use_xtest", "UseXTest", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/disable_svg_glow", "DisableSVGGlow", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/fade_screen_on_close", "FadeScreenOnClose", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/fade_keyboard_backlight_on_close", "FadeKeyboardBacklightOnClose", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/all_off_on_disconnect", "AllOffOnDisconnect", True, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/start_in_threads", "StartScreensInThreads", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "/apps/gnome15/monitor_desktop_session", "MonitorDesktopSession", True, widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/time_format", "TimeFormat", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/time_format_24hr", "TimeFormatTwentyFour", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/date_format", "DateFormat", "", widget_tree) + g15uigconf.configure_text_from_gconf(gconf_client, "/apps/gnome15/date_time_format", "DateTimeFormat", "", widget_tree) + + dialog.run() + dialog.hide() + diff --git a/src/plugins/tweak/tweak.ui b/src/plugins/tweak/tweak.ui new file mode 100644 index 0000000..d3b52d8 --- /dev/null +++ b/src/plugins/tweak/tweak.ui @@ -0,0 +1,596 @@ + + + + + + 100000 + 1 + 10 + + + + 10000 + 1 + 10 + + + 100 + 1 + 10 + + + 10000 + 1 + 10 + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + + 10000 + 1 + 10 + + + False + 5 + Gnome15 Hidden Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 8 + + + True + False + 4 + + + Animated Menus + True + True + False + True + + + True + True + 0 + + + + + Monitor desktop session + True + True + False + True + + + True + True + 1 + + + + + Use XTEST for macros and macro recording + True + True + False + True + + + True + True + 2 + + + + + Fade keyboard backlight on close + True + True + False + True + + + True + True + 3 + + + + + Fade screen on close + True + True + False + True + + + True + True + 4 + + + + + Disable SVG Glow + True + True + False + True + + + True + True + 5 + + + + + Start each screen in it's own thread + True + True + False + True + + + True + True + 6 + + + + + Power off all lights on close + True + True + False + True + + + True + True + 7 + + + + + True + False + 5 + 3 + 4 + 4 + + + + + + True + False + 0 + Scroll Delay + + + 1 + 2 + + + + + True + False + 0 + Scroll Amount + + + 2 + 3 + + + + + True + False + 0 + Animation Delay + + + 3 + 4 + + + + + True + False + 0 + Key Hold Duration + + + 4 + 5 + + + + + True + True + + True + False + False + True + True + ScrollDelayAdjustment + + + 1 + 2 + 1 + 2 + + + + + True + True + + True + False + False + True + True + ScrollAmountAdjustment + + + 1 + 2 + 2 + 3 + + + + + True + True + + True + False + False + True + True + AnimationDelayAdjustment + + + 1 + 2 + 3 + 4 + + + + + True + True + + True + False + False + True + True + KeyHoldDurationAdjustment + + + 1 + 2 + 4 + 5 + + + + + True + False + ms + + + 2 + 3 + 1 + 2 + + + + + True + False + ms + + + 2 + 3 + 3 + 4 + + + + + True + False + ms + + + 2 + 3 + 4 + 5 + + + + + True + False + 0 + USB Key Read Timeout + + + + + True + True + + True + False + False + True + True + UsbKeyReadTimeoutAdjustment + + + 1 + 2 + + + + + True + False + ms + + + 2 + 3 + + + + + True + True + 4 + 8 + + + + + False + False + 0 + + + + + True + False + + + True + False + 4 + 2 + 4 + 4 + + + True + False + 0 + Time Format + + + + + True + False + 0 + Time Format (24 hour) + + + 1 + 2 + + + + + True + False + 0 + Date Format + + + 2 + 3 + + + + + True + False + 0 + Datetime Format + + + 3 + 4 + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 2 + 3 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 3 + 4 + + + + + False + False + 0 + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + + + + + + + button9 + + + diff --git a/src/plugins/voip-mumble/Makefile.am b/src/plugins/voip-mumble/Makefile.am new file mode 100644 index 0000000..03dc05a --- /dev/null +++ b/src/plugins/voip-mumble/Makefile.am @@ -0,0 +1,6 @@ +plugindir = $(datadir)/gnome15/plugins/voip-mumble +plugin_DATA = voip-mumble.py \ + logo.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip-mumble/logo.png b/src/plugins/voip-mumble/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fc292b013a27200931a7438b19d9b868d87836fb GIT binary patch literal 15041 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 XTrRgasAKFh00000NkvXXu0mjfPrn2? literal 0 HcmV?d00001 diff --git a/src/plugins/voip-mumble/voip-mumble.py b/src/plugins/voip-mumble/voip-mumble.py new file mode 100644 index 0000000..641a9ed --- /dev/null +++ b/src/plugins/voip-mumble/voip-mumble.py @@ -0,0 +1,115 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2009-2012 Brett Smith +# Copyright (C) 2013 Gnome15 authors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip-mumble", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15convert as g15convert +import ts3 +from threading import Thread +from threading import Lock +from threading import RLock +from threading import Semaphore +import voip +import os +import base64 +import socket +import errno + +# Plugin details +id="voip-mumble" +name=_("Mumble") +description=_("Provides integration with Mumble. Note, this plugin also\n\ +requires the 'Voip' plugin as well which provides the user interface.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# This plugin only supplies classes to the 'voip' plugin and so is never activated +passive=True +global_plugin=True + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Calendar Back-end module functions +""" + +def create_backend(): + return MumbleBackend() + +""" +Mumble backend +""" + +class MumbleBackend(voip.VoipBackend): + + def __init__(self): + voip.VoipBackend.__init__(self) + + def get_name(self): + raise _("Mumble") + + def start(self, plugin): + raise Exception("Not implemented") + + def stop(self): + raise Exception("Not implemented") + + def get_current_channel(self): + """ + Get the current channel + """ + raise Exception("Not implemented") + + def get_talking(self): + """ + Get who is talking + """ + raise Exception("Not implemented") + + def get_me(self): + """ + Get the local user's buddy entry + """ + raise Exception("Not implemented") + + def get_channels(self): + raise Exception("Not implemented") + + def get_buddies(self, current_channel=True): + raise Exception("Not implemented") + + def get_icon(self): + raise Exception("Not implemented") + + def set_audio_input(self, mute): + raise Exception("Not implemented") + + def set_audio_output(self, mute): + raise Exception("Not implemented") + + def away(self): + raise Exception("Not implemented") + + def online(self): + raise Exception("Not implemented") diff --git a/src/plugins/voip-teamspeak3/Makefile.am b/src/plugins/voip-teamspeak3/Makefile.am new file mode 100644 index 0000000..107218c --- /dev/null +++ b/src/plugins/voip-teamspeak3/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = ts3 + +plugindir = $(datadir)/gnome15/plugins/voip-teamspeak3 +plugin_DATA = voip-teamspeak3.py \ + logo.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/logo.png b/src/plugins/voip-teamspeak3/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fc292b013a27200931a7438b19d9b868d87836fb GIT binary patch literal 15041 zcmV;yIzGjTP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FZT01FZU(%pXi00007bV*G`2iyf6 z3MLl$CdFj{03ZNKL_t(|+UPMvdN}*LV!S|1!-b|fPx63*dT!T z0SXF;@S&(+7f?Z@Dj=WqPG|uW0_i>5dz;yv+1cqcZ$0Py{+ON329ixcEZ_IzTvx6n zJ8wDj+~;Zc^PIE5N8_XM(fB71A;gFJydBNmANn)@U4uUC2TCbHMEm-T0q_Cv1MtO? zMj(^T>-oIFOv_=8%cTT_swmv!(ak`>XZbw3>GgZ<%7AJBu!%^H_5P-=F3g`k{r@ij z_9b*FWl8^Fs;afUr!tk!RRO#<;PcE;RAsJZyHhRGnQhs2g=JZ=9S+Vpqy&hFGllgl zObG!9NusFVp60(|t%n^sp%@7g)|4IY)CUhyK7r>M^-`+O6 zv#W2uqOe(}X-&=Ma#Iq?beUmVu5DRHHkY%ETrQQ*=X0iI*|zNn&RtbXsX7c2?(R+N`@>KbbkF7MS<3Y~J| z#|B0N^2k$f;QX^c@qbqXVu{otmT51}oA%{i0?>v{RIu>3S_=58^pcVizb`N@5D3oj2ZFVMKwx4Z z7@Ss8T2@OmuP&O;KR);z{_E?X``@JjQcA4dRR8!;B6ZH1bvw}5)+%6CZ;3-Bm{R8oV&gL zfOn{(tUR}`Ib44EcW}oYzkBzO6@ZP?y`@tFr4=Xpe8JC_R*pNicIs@*m^BBJCr@%p z0-iodu})Lf2flsr*>4nYyzTDCan(1^{oet=^RKR5Vp{gb&D)!3^NwZ-fK)|ACJ{k@ zcf+PkJaWlUtam#Abasb7m(J#6hc2A`4gfT@c9GZXJH&DMEvlj{tE{Tb2+keHwp|J7 z0x$>|0+5ivkd!hkqzp@mHopFY;Z~plWmu!?$ z(u&vDOz-RM{gub#{lbES4n}2V1^94s#fT&$l0*>1h%1a-#+buEHV9D(?0^&-1jGh{ zxn)Ytl#z&M%6SsKkM90j3}At>>T*@{d`$NRuL+b?eqv(nY|NN74>c3Vaj!?sLdwTH zs{W%Juld@*zgh#>`?>gu=iUMU*p7R6GMz&{ZxU19^#}TX!gZ{joB`v@c5Kvg@wC>8`IA@4NDbKm6wbaL(tB zFY3+WykWp`I21PaBeLl%np(Q#&iW449~la`uDB{bnA&{a@*gew!yUIr7B60St#F;& znj7knO2kQ3nN$>3$T1`!goNXA*p4IfhK+RAphP;aBvLs&oil=l<;>+=eoIRBrl)Ln zV@GfFm+hUQ1;zJwbcF!`t^`~oor?B%KAcYv9+Qf8|GJ}LQ)c^?jc9D`W~oemnr%CO zx%>yed+e$k|4?=NEmz=zD}VK$1polt^Wbw+Ez6ysOlKh=_DMU8ctys9!W5X6gMnyV zMu(D&3%S@4;>BfO`u=ejUwm=uxMLRo#4vIXH`Lc*D3&D7rDP1yGzGe*K+_l)V?;y% z5u^khSHLiA%4AJSWem#ZEonPqwv@OQOsi|_8hE;+tN#;@WGvJdCIHCfvzRgKASB|E z!AQ96S|dAn`cSxKOGo2Y-qg}Y{eubVy8K+jbaq~F89if-!=sFenN^Q3#9`G=*~k({d=CHId1h(zG2W zB+lZJRx*+=I)YAa>5h1$w9(n#fZzW99@NfRf@pu&TV^tHQYPMa=Rj9Ovb(d5dc#qe zmNkhHUi#`~w_Jbd3FqU|ub+lzANo0#efhe79{~6NZAIY*#*T7aj=W{j{#O662G~T5 z7gsK0j4azG)3POFY?AG|cb@*GE04VM_M5wPm0zEVM|KWIB1onU3JoM>Lwgw8>UyzZ zXD7Bd^rF3M5XqDQApph*L^Sd@AYnT$Yn`od9Ua~3hiZeDcI{JYQi>h)Cs@Y4VM z0;isL&A$hLRfT9TilHL_^xh{v4kYLK(T;BfinP0qY;{AX8 z^%jpR|CCNfliW4o)iroM8Z=FT;1Yw0EZTaa*w)yCZ4Eu>4hhD4{ z8bu~+5I~psyqoEs(|g-^HZ4|9GBxjG+>yDpgm$< zB0x&woI_DmFs6JaW4hlt>%2>}Ki>Z1yIk9RJQ=cJ&Q@TuMnN9(NQT zedHlbpK};`I~z9&H~ZOKGV*df+KZu~A!PCf0C}p%qu=xCFJ5yv0Q~)#-{HiwzWuKU zK)7N)cSUt3nq7|Eys; ze`)WDE;kKFJM^Hb=xW`LISZDcv#qW}SLFp(F7ZTqD1=lpiM(M$fK{Jw`74hE zfK|`@9w&bGvVScAj>A7DAXqMMfet){bBiWuG*&dcM5UC(NRdp@s~uNdaLg&^P59k! zelld3xf|2Tc+z#85ly3__Kc##RaJpUSHXou-#`+(nnGyriNZ7;=$g8RnG`~h;|l3= zE^U|3v@G}5u5j#HSGZmPXl>k#g@-Oh-HvsUQZKvAHj)pfhx(DrW?)$k7-PqJy!!7y z@%ir@0RXF>zZ1uudeJ{0fahL)2LNE0)?(9i6vMRklSJ>9G%*rIv-56cDQb`eplLdY zDW3({X=j}NC3fHMe)y(k)20QEZx-*w}LRf9f_ecv%N4`q^37)Bm07f=-S zC`D0jKYrPza{%D&m+!*~r+xjO3&7_-y%YdoTF$}gOir;ZXWtqy9IKDwI%H7;#sG|n zF{$Z3MohUXY0DV^kch`_F^pV0cO2T6x!Vmy0eDSSpePE`SrhfGVYGHdK_F1TI$);kkEK3R@p{VL{s;=F7 z{IZM60br=~iTDfY=6aZ7RIg^AKBLUmf-GUJW2>T3KN3=jn(sVC~sf!#}oO$fY zXL%mJ=VwXFG=6R5b0A`4IvW~(uapvu6$H+?Kxc0ZbuD3VA)#yPZV(_Lgd|Z=LB(;! z#T&P^{1O1V!V-&*EL6~TY<{y_IOY#+BeTZHrO0s|NyMP(`st)-w=P}wC06(7*Z=td zBr=(WQp!LsZ$LuIeL7$%_o%@LDI5aHv7AQHDHD;@bRW2+%fZxY07xdH4_H>dle_!Y zf_<4LMPXo!5Q(L*v#Adb=g>4|R5?{bN)Q)9GNv#ttq z0Cij6tmCeIi`Q)jeI0iS~@I5SAatZ)o*Tz>~<&Jf`na`N6 z>yQ*u>K+e>DL0?6?7|b@TJdLm`g0e4_y82Yh~v78vUx){4I2b@z%h`bc(MRo5aj-N z@q+naQup{Fp?p_B_X7Yo^AEYM9TQRKa23S*-*fgnUdsr`YV zj|;l{(Z4-b`uWd&2D>(|#N3Y+A{{Fgx!bjkCv7ti&N%@jHII+Dz=O-qzNqZg=N|ZQ z0VsY^DP>Sml!d0{s1A4EPY4RXU$+^7Gh+KgdgLFv#|K5x7HbR#A9?I2;jb8%ltMho zT@GRg9y!xULRRn#Od*5@66otspvbruKtPdGB3;*HNgz0tc`I%&KIejS55eKboQ{V2 z4W{dux7%jEPPi_Fkff@bR8)OxGGqOEx7>_>9DugY(8%iFu(f`+s;L!*VZn8UET;SW zT8UEb_5mtWb9}hcvOkkWROq5pR9z~X2dXl#*nw)nXh*Lw1|fjX z&=8UtV$(?l#Bpa~`??pmaM$^(Z5vrg z0VySPuU~?cGOHLUr-fR6;Eb{?WG)1 z-`zr$P*e?GUyyr!C2sL~sbnJ2*wW@kBGKZNa^J!$x?_;X8vuwc8(&pYdehbCL)Cm6 zxXX78n-uweRlPV*O=B<&3q9c>xbE(HM&Y@|aF$SBQ4#QXd^c2;7EomZm4hS3PxiI$)y120$d0PAs~b-Ix^_q0IDa=p0Qx@iA%2i*#jB?Tye?S9UZL= zH-x*Jdh(e#B;)~57!sPt2asCP)!VVT&P!d#`Yv1Q8&(fbR7Z6!oa3r<`*h z0L1d11M`ui|MTf(=i<1d79p9=>Y;(5i{i=jT}^F03pZ?Oz~DeaRt8FGVtEBBfQ{k|U{cJoRE!?aLY7J$#Ijc8sWNdrJ6 zRYj$2I_(RVlyt26`|I_Wf9D#!vf_Chb;|i@Z`mlP%sNb0RP7W+RaGgZWQxKp!>Fj8 zI%|8VySeR{k1s=CPwV~x*tDY&M=d@GQc7xU>;7CkmAO4Jm0wek!ArfPdLL!%mSNe4AZv)|;_?NzVsj8~V8IH>*xm=8x zkFhjqpSG6~Y=^_^QBhguA2C2hl8^vm3WH-ifmAYKz5d!uPrtI_c>qADV;2CRcKSgb zn(jSc)wD7x1u>>Ta91&NS$&|d<;ndCf5ZoT?9f6D`>pjmrzTRFYosAcCs&P^O^Z@= z$1u$YM2LM>{+B!9v&YZ$HjxH}L#q;C0(M|qeAf;}_ ze|bKIyZk@IJ*B_Ib>qK-FV6TJPOdo^3*kVdH(wcFype1pHhen79TWah2Zwh`Ah=Bbw=#xXquUbA)iSgmr1}dj06BwmisZGDyYt$ zR*i*oYEW73hvRVY5pGK2KyWB(i=q$=(}8I^pwThdZluWT^&=20nNkugI~oAqdZ$3t zWwU+5REvB@3Mmyufs~MnrbE#@Rfgq$5&(`nY3%+}cG_vj6vD+^u2MMSD5{N2=!UHj;K=XJ?i7Ceez}tU+Y5ykZ`Ip~enx-A8E85bKOyeH& zF`BGp@V(M2aEx|5x)H-h*@SJf0Uc;Z+Km8Qf+-4AU5Dy$pr-|Ke*^k!5-`*05k5}S zy;4e)Bom3H0I>9=(+kPn>gS^h(Qk$8gk0M$_~Q{9Ofo*uO-3f(=k@5{3imV_ufOyR z<#M@2s;bT)A~=ovG`f4qH1p&yUB0lpJ+lpJA`yLiJY5-U;?B7EYHDXeMpXvA&C^YVF3g%ra)0O zIJWyK0BGE?1xKHFHUOYJ=-tE^?&7Y60>N4^J>0b^lNyBU*f%biHog@AmK=FB^ZJ5U zsHzUjcEKeIt6oUVs2zt{Q>(#+IAEW!o2&yY+rhqUmjsc=>w}_cle|9PbO1Q|=;I1? zl5c#!=+HN+OaW6EBp_AQ12Cm3Z`cPN5CGieUdM61l6FkIZETi4eF(0uqK>-JgbuXR zb~Mmhbjo_<^ZiJrV~FI#$havuss*k~a1D^@1WZp9$&wVzYzk5U#1v9h9WHl|tf-u* z$K(B2zv?Lfz$-62WGh53LW-?ixBwMsYbgaZ%>&)5H>}^j72CG&qEIybHBX@Avxe=! zv}{?-o4F8hU5;5($6@NE%HeK{_ZJlji7|bL$LmGF=O3pqWhMZuU;lQYQuvo&7U&jL z-3*dY6-7dVn4*FyN+}4l0ATK{gT@9RF_dEQp|swYjiH-&kyqmwf{Aw7iB7o-onj06 zWe+Sfj(lbaVWS&SF@U@`54(&bUkO;11X~l}A_r5+A)iYFLVyZt0Lw5+a_J#|@x`AS zrf$zTT-iyCo(7j734#<*6cr`q6HqmN+L8&i2U9L@QaY16cQBqsDr-t1B#o{Q3n}4p zfw?m#puEfvKAfE&cvLhNn<}Ae8g!2*peStC2}_Ujzx%pCz}LhS#nBXn00~kURMq2W zOqm7%E$t0s1CY&{GM&j|Aen^YI+D9K`kh|1IrZpq>X32cFolaOPa!HJ7}SQ4FEL?M z0r{FB+^t6{QGHo{QSqM`H5)BM_WE9Vqoe z2mu<|$17o5SqQ<0#!s4>AN}%|p12DDP+8&Mp{U9Rg%u_wq=e@6Nw2SLBA41-XPoy< z%Jg<^Hgc(pBmE&{vso$@XGU}WmgAtRB8W*7%HX(SKa8y$X&=y31=#o2S`ZbeYsYb> zNn(2daQ)5q;Hqz(XZwBLL61ithK!)78c8Xq8ICq(|2{wpCvR9AlFDqH)6R0wG%7$sCtd%gaIJn*T*P~6x)(S;ZmQ^JNna~{r4qBXKraK zLy^}bra($jNkrBA1)ywFiIt3{9?nK{dGAmKxLXM|3h+c8hQlGWi6DJGgm*6Nz&zMx z2f@k6#{_5`G ztXJ9280%n+6#~g&UfAO+f#c%VPoH_gv~OQ=q2;*dt*K zi5D*C`?c{ZV+aO3W7a671UZfa$8}6DgtHGojnTy-D*PjI~FsPBQTR4iUo8q4#8Z^q8dz*74W4@ zB+_Z{ZUvq}z+YYgQWT_z2FS7Ud0p3E-rEFjy!M*_fQi-BO+K%;%cE-`R*-6;IHO8M z_f_Rh`G+gMe}i(_r5E7K%Nr%|hvbdMiQ^_;$|E_^8fWYWpk za@ouu1|tJ-oI(;eI`$VrqO#l%Vgyle>wB)&VZ-HeiPBOZJer2lMW)d{f{5VQ4lp*szQ$6;$FO;50Bo~@K&y?)+8JP~CZnM? zGV`en5&kr3O08&V*fKU(545X{<=KcT6QmFT5p=JgTuE1dV%Y_UTz}nn^XY8*zIZ&g zHa--4PrYAAS?H=(l)XD)DhgO)d}Rp~Weh(-1j96tPN!_gw)^h=)lW0~Flbt_>5Gc} zXi^FS_j0C4lm4|PHjH|ssQL|_FcPvTISb;_`%VKtuH`~X%qK7|eKuVG!s z>sZl#zXB!_HL*$rKfu$3M*yxcQRHuJB&>z02D&>E!K$k%4G7n;p@D;_78o zD8j#+V<-aRx&kEuFUm@MW43D$fP6NCOgfd}f_Kw|Df_y-v>@|BBc&7qkTMS-ydScY4UHJB6HDdxjo#6SK8mkvn_web1i?z%N=X6 zsl5k-nco5tKN|CiqvwgTy@64Ny` zuNG4J5fKncl2S@d^OB-^PHE}RT@CtE`C5Y;kRD?A5>30953AU2d8B2#Je@W=i5re2&>P9$@5^2)Mqqly$EDp zFpz!Dg!2rC_Fx{t$5I&A>qqVEMN%&-MPK_4iu5#F+_it^(Y-(IZm-J%z`R8t+iyzb z(Y3tCqdBUgyf;%F=89C^=i`!nBrmmHt%~Le7;=O)EdWeA%<(E zwq;80I(^cy`VJU?FMak*WCmL?d*RVo`}AY{Ro5a+Wq?8? zNx*jVluQjEHqc>Y6OmViV_%@D>KTEOl4si*w-^90YyRQrYTtQ4q<}}$R83VFF&YV6 zq%6?hOi|%FHc~@jM7kSUJlZ=Kq*VgIjA=6;8CN}S%dqbt5FwV#jWi06W=OdZm^iK! zHRDT1nW=q~7va7hg!+010ldAhb{I)i1<0yw-T*;DNDkY|J5q{pI-efX2dH~(%Bo+F z8FLro&$rwH0QqV24tt$D&ML3&xxsau!wtijOdtkG$rLr}^Llm?#DHnKPES_@FA!8! z_{`Z~!BY?Y?tobtT~!o&cuP&OPlquEJD)>zpaYpigd8&u!QFb@qyM^Y$NDA!IPTa* znf9LOCc|=$6GC_$?jn^oMtXjV)iT!=2>QL4QCkH?VQ?J&?p$uS>m`C?+p@V~7v(aU z&KWbOzXbrl{q1e|-uJG0SDql|Tb8Tk@&Ow-YIO-0{80@=I;T~ms|RG~dx zS1@np1e654upD=v4aP)(!W8rm^ioq}oduF_KJdW3odDh={6cAy9ho;xSf(wRqL7)- z!ZLDMF5GSam@$10I=fom9{|PC*Ri|5<;4|`?@lI1t7*@kC(|_5Z+u^Zzw?2oaoL5R zhmgEn0$!K%g2@Lk6zQULVqn#rnbXf+zi#zNMA6*bM%P??9{%{Z=cWgOT1L}_cSB%Rp{Cr_DM3sVbpF2$Eaqhy%Wx#Fw zH|y7}j$eJvwWw=oRNFhcYKiD;cm3^!$9%q$9k#hiKCzQjo-2%C@58)Tu*%jH>(~Yeg*I6oKFF*F!-va=Zg@WQKWCtJ3? zI5w0VE^$iaGYN3+bVkD6`|V5lj{snl2x?5*@i@a8AR(c9yzrNlDgbcovB&C;f5^H*ikQbV~-~8Oa4lERmzm-q^@?Bq0Pem0{t$NtimR5{BiD!I1852rw;+ z-d^rNsuyxaVY~8dz?e+S9b=RNn z2%qzX^KjQ)zu)8fOTYHnLQcW?HwFgdifK8d=^C<$1k8MDNY~Xj0HCRH+xypmy&a1B z&ps&Cv3U(KsxeIqLWlxg#u&VTQn;@Bje`$4YUU?CaZ)OiG4BjVVngv%4z44hsVWT9 zMty4->6|el$SD&muz2AVl$H2kn9hELU-(SdHFUMN%9Sra4=(sKD_6e!5CA;!O13g+jvwddpwb76QOMj(<>a3tgXLb!HwxUXZ|2Wn3*+Pb0&b6z;lbF}^A{KGq< z^U-JCD6YWSmg!7RWpcv}&;;(fGBp%Na;TpfhFJ>$rcgE!@Mm3R&^;=p(s}G^4q-5! zfx;Ln%lue4rv?jV)j(AhShoAFHu}8_-{bXQFcOh3J@+KV2BVv%PMdly0GxgHp2wJl z24eWkCyqjYZ&-6(etCEx=1C+oP*nv+Hib+gYN(p_Bmm5wbI5+b|Ncx+ad)HSoR4hv zQ55Av1K_-~3cC>nuqczu*Jg9Z@FGx8yqJ zo zqGz0O7M^>Z?n&CYZF5vr)o;D_ z>WkYx`N`Ap!VAyranF{W%~*c^GSoG7RJxphy(bh2Mq>%6szydOg=8!wR84v0omH>o zXU;to@5=;TbkW81ZZlM*co}2zsi&SI#@NXAq$7_!YOfDmzWl(q?!WkY;jBEHI|~fU zUObdYgL5v400D_WX%)(XZR#96uP>7Pn(cDTn>9fmcF+_|si}a+ zqr!E?NEz}#gzxqGkj|v!3(r15n>Vgaa4s%-^Y5>{F>mg{c>VPi@44@YL*@Yh;)%>f zLy6R3E$w}9IG2EwvHlKl+i0CMdD=Yy(9yEvfMlR9Uw-keM9ANF-@X66eEG%Wh;Xr# zvU>OnC;;NBs@{L@x##v>bkW7v1E>Iyx$nMvf3ke}MLH3k1IQx*v;g$befQm)IZyyj z|KzcdQsRjhRv(i{X6D3`=^_bPAkG|1#s|Z-(ly5eKsJ|k#(S&Uyt;;9zzaZh`#E-IjVBbHqZd z-`cP^naqB zl%JAPo-CzYDuf6YYbR35E2WgzU3S^!6#%>dt^?@XQqmUy1P)aAZ>=i?39qf%G%Ify zr}jtU$mcCtXaQjm+?9i&HZrn_m$TW-qX2OBX~*%(Ql+&dsJog{*a^?M*mHLajn!mT z=<)G-z3A@hkdHm`7uvREQ-^IkU!F7T;1`S6{^?J5?s0E>xF5$Hu@G@HqdAuI&7M%? z(B}3Ya7og2EE(x-CMn$K<}RH3C;(jhgI^!86;J_rHGsE9dE62y)Vk?<|I=XMS5D1OUKPX zAn^S+UV2_kpM5ZX_4Dr?upAgb$@}iRFF88o2jG`d&NNN)AOP%#Km1{F*P{Yp<#1kb z3V@|VWCM7Ih_nN3$6T|%5FM;uyJK2DZ(i6Fj!-I{LxG1b3Qj=N^=wsnpaTHv>zkC) zs)^?X%gS$%f?zwYloAIJzA=`2fQB7_fGoMsm%b;)mHbu z0+78$K@Nb!0ldsPUuWC)r{7f~O%Ty`Ddnj|bQ*w7Qc_0B1M3JBLg3>^AA;8IkYm;_y3VzdGWbKE}Qwm#7UEuuYB{>E&w?Bsp!%M4oS*^N3g8AsQBsRx+LYNn>jdlIfgORYt`vDnd#qswO?U zm#jQH48Vq*;c~`g3sb~M+u^(0(2Ix^&yFL;FgP%PwQsMY4eQ==)5*kEpFjBB*I!@p z4gf4$_Bp)t(sOw8%~$uBXJbnj7R;TFLl@0KOKWd1k;z`s8y@`9`YjDergKsXPW|2W zaPsN4>ha^gvt#R8hltSJu<3mR!1nR83}dXmre@-1uh*Nqg{y|AnAllP}^iY3N)Ag?(ddSky2i>;}0IJ->a~8rn z0v2)3f7{m=vWO^NS66>4fNm+J3P6)mt{N6V`u_Xx+qQi9MV?`8+UHCx%ZA6JWBbk) zZz`L=G!l)UwQX0cv~7n}RT*I;<%mEPNGV|Cb8`T|%M3^&AxMJ+?ICnx3?v~<%S5E7 zo9cFMrS01`y941+6u`^l##jIL*=L_@T)k=~mMuFIFTM0EHgDa$*LgO#_F&$esptz2 zdIn>O^CN@t->uoO6D=KmQV5QrP&*Apy7G!bm(E-;Yu&>S{swdAACA_htse{kB1(=@ z;XHsf0HzW!9e^f*e#ddV!=JwcpyR&#?j5}7qKjVzuy*v9MoVB3~VCKA-s)j>@SyU^C!Y$oHeu#{L+US9sd3oktL766=f+Gnw1#q)USrDyjk zjQXZd%$qY6J$;eDP$K=+NG$ogH5+zeS7WE-u8a6U7Y&BnvIKtJjA=8TTD9VNIeqq` z4=ntmJ;xX41I!2fZ4CbI;XmC#U%m7e`GYql{p_T|QIgkhmGra2cNeO^b>j={uAiPE z_Lg#jCwTNWh;yAbVbO}LKrqpHd& zcQw|pc4y39gwD2I{}2sB-~8_F`rWr)X#>C|SKY1>p$Za;Bse4?K@b4aci(!Yd&yO| zRRU1&x$UaxC0E@xfru19_5%n=i4r1;-+k+q*4|$9+q)k7S|k$vOlaT1K{6QI$W3x|{AMBDA+QmkOpoAB|;Ccpm&-Zo}-+9e7-@~te{o64G(AC$E>9sWlo8L1qKb6j2-__gy z)xW>90}U4|E{b-_g%)^QMIh7T*8&RWI?{X$PUNyWt<^!PGtKyM=}_7X%BC zX$gn|(OulVPc8s?$yK)z5eW%!0_jqM6Tu!U{|-lnx?frG+7)f}8z%?+-ZGEZvyecO zh%v8}QZ2M=%8bjM^@6(_0{)Wq-JSK#M1+CfR^&{Shr@%F&pi9wh7D`q+Rhm5DlIK- zn_N4&>#u)(uw``1$oc1g9gjTn;C_YQ+}1rJ{EqI>X^CX|`^{~=$FEwu106m6(#&UQ zFw~0VU~iA)?hSW8zv{1N9W@AzM3SPej_opi7+C2Cy3D;db+x?E9JRWe1* z>7LS#rn*hZ*zo<+XD&jpqzqXzmFVjWU;gTg&m}9$JiYI%+ma~0-mDirtcyc3sQ zb{QUdLbz2+JA4$kuDoU|XE3&D< ztrGJ3tA6~O*Uma>I%=maKyP>BhsFe}qg#uIogpurGX>8uJ7J)3_$mM?KvZB13SGaJ zl+cLak}PP)UJ45D{t<6)tup|04FB2dm_6?>v^8%>SH~^{%f|yirbw9-$Jf5P91lHw z|L)@cWn-UYn@XoqR$7V~(J{TZoL=8B%a$cbyWG-`EC-AE>rG7;&fp@B}=R&Isg zqhH_I(XivXOFoaOGY>{jM;$)Qp}w0dB#eEBqCl$<0dORwUGM{iCWgHM z@Igmg^Y;B-Gg|m#3OQerKJFXNHj4woy}f7IFvfQzPYPr)Akm0^+k~yipW@~ zl`^R)NQz%EM!)UsXov#9ocTwfwQ0+TUW!%sIA2WCC_xgx=e8^NT+NaM3TBA_a!}Y< z%7>$=|G3|-nKMiTd|s5570BE~DqA%aPhAm-#n12RjZELPqgici?L~ZONXGg*DIV!2 z?wDIORs8`|wYR%kcjW=#xD!sp+pAanQ|73GNd4wjx8v^Hu5^Z-fS^KhrhW6fw-qB* z_wHM-ED*YdBd-dFgBECD4xqRrPX4C|KQfp=V0b^BVcF%qq39)jq3H6+V0^}|#!hWV zLnr$BBQh}f*MJ z-V27}>DtEDUfNdQPThU|h)2UR5$U3Aau9%6;rID}0$@vDYoj~R*M_+ZmY}6^^FMvg zBOdab-@P4o-+JXfFMRjsQc6T(Ng5nV$%E%jdylj2j}D?{eDyzN(rul+m_1|9?YF(5 z2>SYCIQoc#(9zw0Oy00AvK{BFcsyOv(%FYyja{<6vzOx05K^&_%%-CxxVzr#_5QSK z+_=}aZCvdFz(EHeiH5q3|FT8%fmozRo_>RlIby*AA#h zp$CJ3+=TJtR+g3q)_OhK)?hHu?A1LPk4JI*x@J$BSiytA;6LP~CAX+6G^m({qYID* zDN7xfFXTcjbzOdkr*~lK_AZgHHID1MtN2tCe#;`>g~lF!miRq3K#t?OTxAfds&IlVoW4PfIG4mZSD2zOg)xDSrYIMsHzra3d?J%nkPniMpVe63mb$zQc6F8 zyo^yf=X|nbyA_6Qm1Q$|q%#>LlPL^DV+e)&G0;DN#83>mY%=Fsxdfz}gy4+;Hh8?= z)zhXQzIEN2M@O`8`kX`2)%x!j{6C}td*1#Ya~}k7IFzzEzF^tRvhs?>rDYWpN=ixt z!CobY5$(p9>M9eP%T}r78A7d{d;ebJM*UdVPlY?$6av-3V9+Z;elCSBIah}FIdewjdb~a%g~;Xd`K+p{ z@hOvQqCdXjCiAp2Ke^ZE&Ry^^v@~wQN8?{GmVNFE9|_^3@zMBbd^A29AB~U32Os|r XTrRgasAKFh00000NkvXXu0mjfPrn2? literal 0 HcmV?d00001 diff --git a/src/plugins/voip-teamspeak3/test.py b/src/plugins/voip-teamspeak3/test.py new file mode 100644 index 0000000..a990e9c --- /dev/null +++ b/src/plugins/voip-teamspeak3/test.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ts3 +import gnome15.g15logging as g15logging +import logging + +if __name__ == "__main__": + logger = g15logging.get_root_logger() + logger.setLevel(logging.INFO) + + t = ts3.TS3() + t.start() + + logger.info("schandlerid : %d", t.schandlerid) + + + logger.info("channel: %s", t.send_command(ts3.Command('channelconnectinfo')).args['path']) + \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/ts3/Makefile.am b/src/plugins/voip-teamspeak3/ts3/Makefile.am new file mode 100644 index 0000000..1553717 --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/Makefile.am @@ -0,0 +1,8 @@ +ts3dir = $(datadir)/gnome15/plugins/voip-teamspeak3/ts3 +ts3_DATA = \ + __init__.py \ + message.py + +EXTRA_DIST = \ + $(ts3_DATA) + diff --git a/src/plugins/voip-teamspeak3/ts3/__init__.py b/src/plugins/voip-teamspeak3/ts3/__init__.py new file mode 100644 index 0000000..5ecd020 --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/__init__.py @@ -0,0 +1,206 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from telnetlib import Telnet +from threading import Thread +from threading import RLock +from message import MessageFactory +from message import Command + +# Logging +import logging +logger = logging.getLogger(__name__) + +def _receive_message(client): + while True: + incoming_message = client.read_until('\n', 10).strip() + if incoming_message is not None and incoming_message.strip(): + logger.info("Received: %s", incoming_message) + message = MessageFactory.get_message(incoming_message) + if message: + return message + +class TS3CommandException(Exception): + + def __init__(self, code, message): + Exception.__init__(self, message) + self.code = code + +class TS3(): + + class ReceiveThread(Thread): + def __init__(self, client): + Thread.__init__(self) + self._client = client + self.setDaemon(True) + self.setName("TS3ReceiveThread") + self._reply_handler = None + self._error_handler = None + self._stop = False + + def stop(self): + self._stop = True + def run(self): + try: + while True: + try: + if self._stop: + raise EOFError() + msg = _receive_message(self._client) + except TS3CommandException as e: + logger.debug("Error while receving message", exc_info = e) + self._error_handler(e) + else: + self._reply_handler(msg) + except Exception as e: + logger.debug("Error in main loop", exc_info = e) + self._error_handler(e) + + def __init__(self, hostname="127.0.0.1", port=25639, timeout=10): + self.timeout = timeout + self.hostname = hostname + self.port = port + + self._event_client = None + self._event_thread = None + self._command_client = None + self._lock = RLock() + + self.schandlerid = None + + def change_server(self, schandlerid): + if self._event_client is not None: + self._write_command(self._event_client, Command( + 'clientnotifyunregister') + ) + + self.schandlerid = schandlerid + self._send_command(self._command_client, Command( + 'use', + schandlerid=self.schandlerid) + ) + if self._event_client is not None: + self._send_command(self._event_client, Command( + 'use', + schandlerid=self.schandlerid) + ) + self._send_command(self._event_client, Command( + 'clientnotifyregister', + schandlerid=self.schandlerid, + event=self._event_type + ) + ) + + def close(self): + if self._event_thread is not None: + self._event_thread.stop() + self._command_client.close() + self._command_client = None + if self._event_client is not None: + self._event_client.close() + self._event_client = None + + def start(self): + self._create_command_client() + + + def send_event_command(self, command): + try: + self._lock.acquire() + if self._event_client is not None: + self._write_command(self._event_client, command) + finally: + self._lock.release() + + def send_command(self, command): + try: + self._lock.acquire() + if self._command_client is None: + self.start() + return self._send_command(self._command_client, command) + finally: + self._lock.release() + + def subscribe(self, reply_handler, type='any', error_handler = None): + """ + Shortcut method to subscribe to all messages received from the client. + + Keyword arguments: + reply_handler -- function called with Message as argument + error_handler -- function called with TSCommandException as argument + type -- type of event to subscribe to + """ + try: + self._lock.acquire() + + if self._event_client is not None: + raise Exception("Already subscribed") + + self._event_type = type + self._create_event_client() + self._event_thread._reply_handler = reply_handler + self._event_thread._error_handler = error_handler + self._write_command(self._event_client, Command( + 'clientnotifyregister', + schandlerid=self.schandlerid, + event=type + ) + ) + + finally: + self._lock.release() + + + """ + Private + """ + def _send_command(self, client, command): + try: + self._lock.acquire() + self._write_command(client, command) + r_reply = None + while True: + reply = _receive_message(client) + if reply.command == 'error': + msg = reply.args['msg'] + if msg != 'ok': + raise TS3CommandException(int(reply.args['id']), msg) + else: + break + else: + if r_reply is None: + r_reply = reply + else: + raise TS3CommandException(9999, "Multiple replies") + + + return r_reply + finally: + self._lock.release() + + def _write_command(self, client, command): + logger.info("Sending command: %s", command.output) + client.write("%s\n" % command.output) + + def _create_command_client(self): + self._command_client = Telnet(host=self.hostname, port=self.port) + self.schandlerid = int(_receive_message(self._command_client).args['schandlerid']) + + def _create_event_client(self): + self._event_client = Telnet(host=self.hostname, port=self.port) + _receive_message(self._event_client) + self._event_thread = self.ReceiveThread(self._event_client) + self._event_thread.start() \ No newline at end of file diff --git a/src/plugins/voip-teamspeak3/ts3/message.py b/src/plugins/voip-teamspeak3/ts3/message.py new file mode 100644 index 0000000..842996b --- /dev/null +++ b/src/plugins/voip-teamspeak3/ts3/message.py @@ -0,0 +1,226 @@ +# Copyright (c) 2012 Adam Coddington +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +__all__ = ['Message', 'Command'] + +class MessageFactory(object): + @classmethod + def get_message(cls, incoming_string): + if incoming_string[0] != incoming_string[0].upper(): + first_item = incoming_string.split(' ')[0] + if "=" in first_item and "|" in incoming_string: + return MultipartMessage(incoming_string) + else: + return Message(incoming_string) + +class MessageBase(object): + MAPPINGS = { + '\\\\': '\\', + '\\/': '/', + '\\s': ' ', + '\\p': '|', + '\\a': '', + '\\b': '', + '\\f': '\n', + '\\n': '\n', + '\\r': '\n', + '\\t': '\t', + '\\v': '', + } + + def __eq__(self, other): + if self.__repr__() == other.__repr__(): + return True + return False + + def __unicode__(self): + return unicode(self.__str__()) + + def __repr__(self): + return "<%s>" % self.__str__() + + def _clean_incoming_value_multipart(self, value): + raw_values = value.split('|') + + items = [] + items.append( + raw_values[0] + ) + for raw_item in raw_values[1:]: + items.append( + raw_item.split('=')[1] + ) + return tuple(items) + + def _clean_incoming_value(self, value): + for fr, to in self.MAPPINGS.items(): + value = value.replace(fr, to) + return value + + def _clean_outgoing_value(self, value): + value = str(value) + for fr, to in self.MAPPINGS.items(): + if to: + value = value.replace(to, fr) + return value + + @property + def ultimate_origination(self): + if self.is_response(): + return self.origination.command + else: + return self.command + +class Message(MessageBase): + def __init__(self, command): + command = command.strip() + if not command: + raise ValueError("No command") + + self.raw_command = command + + self.command = self._get_command_from_string(self.raw_command) + self.args = self._get_arguments_from_string(self.raw_command) + + def is_reset_message(self): + if self.command == 'error': + return True + return False + + def is_response(self): + if self.command: + return False + return True + + def is_response_to(self, command): + if self.is_response() and self.origination == command: + return True + return False + + def set_origination(self, command): + self.origination = command + + def _get_command_from_string(self, cmd): + command = cmd.split(' ')[0] + if command.find('=') > -1: + command = None + return command + + def _get_arguments_from_string(self, cmd): + args = {} + raw_args = cmd.split(' ')[1 if self.command else 0:] + for raw_arg in raw_args: + arg = raw_arg.split('=', 1) + attribute = arg[0] + if len(arg) > 1: + value = arg[1] + else: + value = None + if value: + if "|" in value: + args[attribute] = self._clean_incoming_value_multipart(value) + else: + args[attribute] = self._clean_incoming_value(value) + else: + args[attribute] = None + return args + + def __getitem__(self, key): + return self.args[key] + + def keys(self): + return self.args.keys() + + @property + def output(self): + arglist = [] + for param, value in self.args.items(): + arglist.append("%s=%s" % ( + param, + self._clean_outgoing_value(value), + )) + if self.is_response(): + return "%s %s" % ( + self.origination.__repr__(), + " ".join(arglist), + ) + else: + return "%s %s" % ( + self.command, + " ".join(arglist), + ) + + def __str__(self): + if self.is_response(): + return "%s %s" % ( + self.origination.__repr__(), + self.args + ) + else: + return "%s %s" % ( + self.command, + self.args + ) + +class MultipartMessage(MessageBase): + def __init__(self, command_string): + self.command = command_string + self.origination = None + + self.responses = self.parse_command( + self.command + ) + + def parse_command(self, string): + responses = [] + for string_part in string.split('|'): + responses.append( + Message( + string_part + ) + ) + return responses + + def set_origination(self, command): + self.origination = command + for response in self.responses: + response.set_origination(command) + + def __getitem__(self, key): + return self.responses[key] + + def __str__(self): + string_list = [] + for response in self.responses: + string_list.append(repr(response)) + return "[%s]" % ( + ", ".join(string_list) + ) + + def is_response(self): + return True + + def is_reset_message(self): + return False + +class Command(Message): + def __init__(self, command, **kwargs): + self.command = command + self.args = kwargs diff --git a/src/plugins/voip-teamspeak3/voip-teamspeak3.py b/src/plugins/voip-teamspeak3/voip-teamspeak3.py new file mode 100644 index 0000000..8fba11b --- /dev/null +++ b/src/plugins/voip-teamspeak3/voip-teamspeak3.py @@ -0,0 +1,673 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# Copyright (C) 2013 Brett Smith +# Nuno Araujo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip-teamspeak3", modfile = __file__).ugettext + +import gnome15.g15driver as g15driver +import gnome15.util.g15icontools as g15icontools +import ts3 +from threading import Thread +from threading import Lock +from threading import RLock +from threading import Semaphore +import voip +import os +import base64 +import socket +import errno +import re + +# Plugin details +id="voip-teamspeak3" +name=_("Teamspeak3") +description=_("Provides integration with TeamSpeak3. Note, this plugin also\n\ +requires the 'Voip' plugin as well which provides the user interface.") +author="Brett Smith " +copyright=_("Copyright (C)2011 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +# This plugin only supplies classes to the 'voip' plugin and so is never activated +passive=True +global_plugin=True + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Calendar Back-end module functions +""" + +def create_backend(): + return Teamspeak3Backend() + +def find_avatar(server_unique_identifier, client_unique_identifier): + decoded = "" + try: + for c in base64.b64decode(client_unique_identifier): + decoded += chr(((ord(c) & 0xf0) >> 4) + 97) + decoded += chr((ord(c) & 0x0f) + 97) + except TypeError as e: + logger.debug("Error decoding client_unique_identifier. Using raw value", exc_info = e) + # Sometimes the client_unique_identifier is not base64 encoded + decoded = client_unique_identifier + + return os.path.expanduser("~/.ts3client/cache/%s/clients/avatar_%s" % (base64.b64encode(server_unique_identifier), decoded)) + +""" +Teamspeak3 backend +""" + +class Teamspeak3BuddyMenuItem(voip.BuddyMenuItem): + + def __init__(self, db_id, clid, nickname, channel, client_type, plugin): + voip.BuddyMenuItem.__init__(self, "client-%s" % clid, nickname, channel, plugin) + self.db_id = db_id + self.clid = clid + self.client_type = client_type + self.avatar = None + self.uid = None + + def set_uid(self, server_uid, uid): + self.uid = uid + self.avatar = find_avatar(server_uid, uid) + +class Teamspeak3ServerMenuItem(voip.ChannelMenuItem): + + def __init__(self, schandlerid, name, backend): + voip.ChannelMenuItem.__init__(self, "server-%s" % schandlerid, name, backend, icon=g15icontools.get_icon_path(['server', 'redhat-server', 'network-server', 'redhat-network-server', 'gnome-fs-server' ], include_missing=False)) + self.schandlerid = schandlerid + self.activatable = False + self.radio = False + self.path = "" + +class Teamspeak3ChannelMenuItem(voip.ChannelMenuItem): + + def __init__(self, schandlerid, cid, cpid, name, order, backend): + voip.ChannelMenuItem.__init__(self, "channel-%s-%d" % (cid, schandlerid), name, backend) + self.group = False + self.cid = cid + self.cpid = cpid + self.order = order + self._backend = backend + self.schandlerid = schandlerid + + @property + def path(self): + result = self.name + if self.cpid != 0: + parent_item = self.backend._channel_map[self.cpid] + result = parent_item.path + "/" + result + return result + + @property + def parent_count(self): + result = 0 + if self.cpid != 0: + parent_item = self.backend._channel_map[self.cpid] + result = 1 + parent_item.parent_count + return result + + @property + def direct_children(self): + return [ child for child in self._backend._channels if type(child) is Teamspeak3ChannelMenuItem and child.cpid == self.cid ] + + @property + def children(self): + children = [] + for direct_child in self.direct_children: + children.append(direct_child) + children.extend(direct_child.children) + return children + + @property + def child_count(self): + return len(self.children) + + def get_theme_properties(self): + p = voip.ChannelMenuItem.get_theme_properties(self) + p["item_name"] = self.parent_count * " " + p["item_name"] + return p + + def on_activate(self): + if self._backend._client.schandlerid != self.schandlerid: + self._backend._client.change_server(self.schandlerid) + self._backend.set_current_channel(self) + return True + +class Teamspeak3Backend(voip.VoipBackend): + + def __init__(self): + voip.VoipBackend.__init__(self) + self._buddies = None + self._buddy_map = {} + self._channels = None + self._channels_map = {} + self._me = None + self._clid = None + self._server_uid = None + self._client = None + self._current_channel = None + + def get_talking(self): + if self._buddies is not None: + for d in self._buddies: + if d.talking: + return d + + def set_current_channel(self, channel_item): + try: + reply = self._client.send_command(ts3.Command( + 'clientmove', + clid=self._clid, + cid=channel_item.cid + )) + + except ts3.TS3CommandException as e: + logger.debug("Error when changing channel", exc_info = e) + + def get_current_channel(self): + if self._current_channel is None: + if self._channels is None: + self.get_channels() + + reply = self._client.send_command( + ts3.Command( + 'channelconnectinfo' + )) + if 'path' in reply.args: + channel_path = reply.args['path'] + for c in self._channels: + if c.path == channel_path: + self._current_channel = c + + return self._current_channel + + def get_buddies(self): + if self._buddies == None: + self._buddy_map = {} + # Get the basic details + reply = self._client.send_command( + ts3.Command( + 'clientlist -away -voice -uid' + )) + self._parse_clientlist_reply(reply) + + return self._buddies + + def get_channels(self): + if self._channels == None: + self._channel_map = {} + self._channels = [] + + reply = self._client.send_command(ts3.Command( + 'serverconnectionhandlerlist')) + + for r in reply.responses if isinstance(reply, ts3.message.MultipartMessage) else [ reply ]: + s = int(r.args['schandlerid']) + reply = self._client.send_command(ts3.Command( + 'use', schandlerid = s)) + + # Get the server IP and port + try: + reply = self._client.send_command(ts3.Command( + 'serverconnectinfo')) + ip = reply.args['ip'] + port = int(reply.args['port']) + + # A menu item for the server + item = Teamspeak3ServerMenuItem(s, "%s:%d" % (ip, port), self) + self._channels.append(item) + + reply = self._client.send_command( + ts3.Command( + 'channellist' + )) + self._parse_channellist_reply(reply, s) + except ts3.TS3CommandException as e: + logger.debug("Error when getting channel list", exc_info = e) + + + # Switch back to the selected server connection + reply = self._client.send_command(ts3.Command( + 'use', schandlerid = self._client.schandlerid)) + + return self._channels + + def get_name(self): + return _("Teamspeak3") + + def start(self, plugin): + self._plugin = plugin + self._client = ts3.TS3() + + # Connect to ClientQuery plugin + try : + self._client.start() + except socket.error as v: + logger.debug("Error starting client. Could not open socket.", exc_info = v) + self._client = None + if v.error_code == errno.ECONNREFUSED: + return False + raise v + + # Get initial buddy lists, channel lists and other stuff + try: + self._get_clid() + self._get_server_uid() + self.get_channels() + self.get_current_channel() + self.get_buddies() + self._client.subscribe(self._handle_message, "any", self._handle_error) + + return True + except ts3.TS3CommandException as e: + logger.debug("TeamSpeak error getting initial data", exc_info = e) + self._client.close() + self._client = None + if e.code == 1794: + # Not connected to server + return False + else: + raise e + except Exception as e: + logger.debug("Error getting initial data", exc_info = e) + self._client.close() + self._client = None + raise e + + def is_connected(self): + return self._client is not None + + def stop(self): + if self._client is not None: + self._client.close() + self._client = None + + def get_me(self): + return self._me + + def get_icon(self): + return os.path.join(os.path.dirname(__file__), "logo.png") + + def kick(self, buddy): + reply = self._client.send_command(ts3.Command( + 'clientkick', clid = buddy.clid, reasonid = 5, reasonmsg = 'No reason given')) + logger.info("Kicked %s (%s)", buddy.nickname, buddy.clid) + + def ban(self, buddy): + if buddy.uid is None: + raise Exception("UID is not known") + reply = self._client.send_command(ts3.Command( + 'banadd', banreason = 'No reason given', uid = buddy.uid)) + logger.info("Banned %s (%s)", buddy.nickname, buddy.uid) + + def away(self): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_away=1 + )) + + def online(self): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_away=0 + )) + + def set_audio_input(self, mute): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_input_muted=1 if mute else 0 + )) + + def set_audio_output(self, mute): + reply = self._client.send_command(ts3.Command( + 'clientupdate', + client_output_muted=1 if mute else 0 + )) + + """ + Private + """ + def _handle_error(self, error): + print error + if isinstance(error, EOFError): + self._disconnected() + else: + logger.warning("Teamspeak3 error. %s", str(error)) + + def _handle_message(self, message): + print message.command + try: + if message.command == 'notifyclientupdated': + self._parse_notifyclientupdated(message) + self._do_redraw() + elif message.command == 'notifyclientpermlist': + self._parse_notifyclientpermlist_reply(message) + elif message.command == 'notifytextmessage': + self._parse_notifytextmessage_reply(message) + elif message.command == 'notifytalkstatuschange': + self._parse_notifytalkstatuschange_reply(message) + elif message.command == 'notifyclientchannelgroupchanged': + self._parse_notifyclientchannelgroupchanged_reply(message) + elif message.command == 'notifycliententerview': + self._parse_notifycliententerview_reply(message) + elif message.command == 'notifyclientleftview': + self._parse_notifyclientleftview_reply(message) + elif message.command == 'notifyconnectstatuschange': + self._parse_notifyconnectstatuschange_reply(message) + elif message.command == 'notifychannelcreated': + self._parse_notifychannelcreated_reply(message) + elif message.command == 'notifychanneledited': + self._parse_notifychanneledited_reply(message) + elif message.command == 'notifychanneldeleted': + self._parse_notifychanneldeleted_reply(message) + elif message.command == 'notifychannelmoved': + self._parse_notifychannelmoved_reply(message) + elif message.command == 'notifycurrentserverconnectionchanged': + self._parse_notifycurrentserverconnectionchanged_reply(message) + + + + + except Exception as e: + logger.error("Possible corrupt reply.", exc_info = e) + + def _disconnected(self): + print "disconnex" + self._plugin._disconnected() + + def _create_channel_item(self, message, schandlerid): + item = Teamspeak3ChannelMenuItem(schandlerid, int(message.args['cid']), + int(message.args['cpid']) if 'cpid' in message.args else int(message.args['pid']), + message.args['channel_name'], + int(message.args['channel_order']), self) + if 'channel_topic' in message.args: + item.topic = message.args['channel_topic'] + return item + + def _create_menu_item(self, message, channel = None): + return Teamspeak3BuddyMenuItem(int(message.args['client_database_id']), + int(message.args['clid']), + message.args['client_nickname'], + channel, + int(message.args['client_type']), + self._plugin) + + def _get_clid(self): + reply = self._client.send_command(ts3.Command( + 'whoami', virtualserver_unique_identifier=None + )) + self._clid = int(reply.args['clid']) + logger.info("Your CLID is %d", self._clid) + + def _get_server_uid(self): + reply = self._client.send_command(ts3.Command( + 'servervariable', virtualserver_unique_identifier=None + + )) + self._server_uid = reply.args['virtualserver_unique_identifier'] + + def _do_redraw(self): + self._plugin.redraw() + + def _update_item_from_message(self, item, message): + if 'client_input_muted' in message.args: + item.input_muted = message.args['client_input_muted'] == '1' + if 'client_output_muted' in message.args: + item.output_muted = message.args['client_output_muted'] == '1' + if 'client_away' in message.args: + item.away = message.args['client_away'] == '1' + if 'client_away_message' in message.args: + a = message.args['client_away_message'] + if a and len(a) > 0: + item.away = a + if 'client_unique_identifier' in message.args: + item.set_uid(self._server_uid, message.args['client_unique_identifier']) + + def _my_channel_changed(self): + self._current_channel = None + self.get_current_channel() + self._buddies = None + self._plugin.reload_buddies() + + """ + Reply handlers + """ + def _parse_notifycurrentserverconnectionchanged_reply(self, message): + self._client.change_server(int(message.args['schandlerid'])) + self._my_channel_changed() + + def _parse_notifychanneledited_reply(self, message): + item = self._channel_map[int(message.args['cid'])] + if 'channel_topic' in message.args: + item.topic = message.args['channel_topic'] + if 'channel_name' in message.args: + if self._current_channel is not None and item.name == self._current_channel: + self._current_channel = None + item.name = message.args['channel_name'] + if self._current_channel is None: + self.get_current_channel() + + # Update the position of the channel in the channel list if it's order has been changed + if 'channel_order' in message.args: + children = self._remove_channel(item) + item.order = int(message.args['channel_order']) + self._insert_channel(item) + for child in children: + self._insert_channel(child) + + self._plugin.channel_updated(item) + + def _parse_notifyclientpermlist_reply(self, message): + pass + + def _parse_notifychanneldeleted_reply(self, message): + item = self._channel_map[int(message.args['cid'][-1] if type(message.args['cid']) is tuple else message.args['cid'])] + children = self._remove_channel(item) + del self._channel_map[item.cid] + for child in children: + del self._channel_map[child.cid] + self._plugin.channel_removed(item) + + def _parse_notifychannelmoved_reply(self, message): + item = self._channel_map[int(message.args['cid'])] + + children = self._remove_channel(item) + item.order = int(message.args['order']) + item.cpid = int(message.args['cpid']) + self._insert_channel(item) + for child in children: + self._insert_channel(child) + + self._plugin.channel_moved(item) + + def _remove_channel(self, item): + position = self._channels.index(item) + children = item.children + + # Update the following item order if necessary + try: + next_item = self._channels[position + item.child_count + 1] + if next_item.cpid == item.cpid: + next_item.order = item.order + except IndexError as e: + logger.debug("Did not found channel to remove", exc_info = e) + pass + + self._channels.remove(item) + for channel in children: + self._channels.remove(channel) + + return children + + def _find_teamspeak3servermenuitem(self, id): + matching_items = [ x for x in self._channels if x.schandlerid == id and type(x) is Teamspeak3ServerMenuItem ] + if len(matching_items) > 0: + return matching_items[0] + else: + return None + + def _parse_notifychannelcreated_reply(self, message): + item = self._create_channel_item(message, self._client.schandlerid) + self._channel_map[item.cid] = item + self._insert_channel(item) + self._plugin.new_channel(item) + + def _insert_channel(self, item): + # Insert the item at the correct position in the menu + if item.cpid == 0 and item.order == 0: + # If first channel of server + position = self._channels.index(self._find_teamspeak3servermenuitem(item.schandlerid)) + 1 + elif item.order == 0: + # If first sub-channel of a channel + position = self._channels.index(self._channel_map[item.cpid]) + 1 + else: + # Other cases + future_previous_item = self._channel_map[item.order] + position = self._channels.index(future_previous_item) + future_previous_item.child_count + 1 + self._channels.insert(position, item) + + # Update the following item order if necessary + try: + next_item = self._channels[position + 1] + if next_item.cpid == item.cpid: + next_item.order = item.cid + except IndexError as e: + logger.debug("Did not found channel to update", exc_info = e) + pass + + def _parse_notifyconnectstatuschange_reply(self, message): + status = message.args['status'] + if status == "disconnected": + logger.info("Disconnected from server. Stopping client") + self.stop() + + def _parse_notifyclientleftview_reply(self, message): + clid = int(message.args['clid']) + if clid in self._buddy_map: + item = self._buddy_map[clid] + self._buddies.remove(item) + del self._buddy_map[clid] + self._plugin.buddy_left(item) + self._do_redraw() + else: + logger.warning("Client left that we knew nothing about yet (%d)", clid) + + def _parse_notifycliententerview_reply(self, message): + reply= self._client.send_command(ts3.Command( + 'clientvariable', + clid=message.args['clid'] + )) + item = self._create_menu_item(message, None) + item.channel = self._channel_map[int(message.args['ctid'])] + self._buddies.append(item) + self._buddy_map[item.clid] = item + self._update_item_from_message(item, message) + c = self._plugin.new_buddy(item) + + def _parse_notifyclientchannelgroupchanged_reply(self, message): + if int(message.args['clid']) == self._clid: + self._my_channel_changed() + else: + buddy_id = int(message.args['clid']) + buddy = self._buddy_map[buddy_id] + new_channel_id = int(message.args['cid']) + new_channel = self._channel_map[new_channel_id] + old_channel = buddy.channel + buddy.channel = new_channel + self._plugin.moved_channels(buddy, old_channel, new_channel) + + def _parse_clientlist_reply(self, message): + items = [] + item_map = {} + for r in message.responses if isinstance(message, ts3.message.MultipartMessage) else [ message ]: + ch = self._channel_map[int(r.args['cid'])] + item = self._create_menu_item(r, ch) + self._update_item_from_message(item, r) + items.append(item) + item_map[item.clid] = item + if item.clid == self._clid: + self._me = item + self._buddies = items + self._buddy_map = item_map + + def _sort_channellist(self, channels): + """ + Sort the channel list the same way that it's done in TeamSpeak3 + """ + result = [] + search_stack = [] + # Initialize the search stack with the criteria for the first item (always 0,0) + search_stack.append((0, 0)) + while len(channels) > len(result): + search_criteria = search_stack.pop() + try: + item = channels[search_criteria] + result.append(item) + search_stack.append((item.cpid, item.cid)) + search_stack.append((item.cid, 0)) + except KeyError as e: + logger.debug("Did not found channels matching search_criteria", exc_info = e) + continue + + return result + + def _parse_channellist_reply(self, message, schandlerid): + channels = {} + for r in message.responses if isinstance(message, ts3.message.MultipartMessage) else [ message ]: + item = self._create_channel_item(r, schandlerid) + channels[item.cpid, item.order] = item + self._channel_map[item.cid] = item + + self._channels.extend(self._sort_channellist(channels)) + + def _parse_notifyclientupdated(self, message): + item = self._buddy_map[int(message.args['clid'])] + self._update_item_from_message(item, message) + item.mark_dirty() + + def _parse_notifytextmessage_reply(self, message): + if 'invokername' in message.args and 'msg' in message.args: + self._plugin.message_received(message.args['invokername'], self._filter_formatting_tags(message.args['msg'])) + else: + logger.warning("Got text messsage I didn't understand. %s", str(message)) + + def _parse_notifytalkstatuschange_reply(self, message): + clid = int(message.args['clid']) + if clid in self._buddy_map: + item = self._buddy_map[clid] + item.talking = message.args['status'] == '1' + item.mark_dirty() + if not self._plugin.menu.is_focused(): + self._plugin.menu.selected = item + self._plugin.menu.centre_on_selected() + + self._plugin.talking_status_changed(self.get_talking()) + + def _filter_formatting_tags(self, message): + filtered = message + for regex in ('\[B\](?P.*?)\[/B\]', \ + '\[I\](?P.*?)\[/I\]', \ + '\[U\](?P.*?)\[/U\]', \ + '\[COLOR=#([0-9]|[a-f]).*?\](?P.*?)\[/COLOR\]'): + filtered = re.sub(regex, '\g', filtered, flags = re.IGNORECASE) + return filtered diff --git a/src/plugins/voip/Makefile.am b/src/plugins/voip/Makefile.am new file mode 100644 index 0000000..5aaaecb --- /dev/null +++ b/src/plugins/voip/Makefile.am @@ -0,0 +1,18 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/voip +plugin_DATA = voip.py \ + g19_microphone-sensitivity-high.png \ + g19_microphone-sensitivity-muted.png \ + default_audio-high.gif \ + default_audio-muted.gif \ + default_available.gif \ + default_away.gif \ + default_microphone-sensitivity-high.gif \ + default_microphone-sensitivity-muted.gif \ + default_record.gif \ + voip.ui + + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/Makefile.am b/src/plugins/voip/buddies-only/Makefile.am new file mode 100644 index 0000000..3775d9d --- /dev/null +++ b/src/plugins/voip/buddies-only/Makefile.am @@ -0,0 +1,31 @@ +themedir = $(datadir)/gnome15/plugins/voip/default +theme_DATA = \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-message-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg \ + default-message-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/buddies-only.theme b/src/plugins/voip/buddies-only/buddies-only.theme new file mode 100644 index 0000000..e17a8fb --- /dev/null +++ b/src/plugins/voip/buddies-only/buddies-only.theme @@ -0,0 +1,4 @@ +[theme] +name=Buddies Only +description=Displays only buddies +supported_models=g19,g13,g15,g15v2,g510 \ No newline at end of file diff --git a/src/plugins/voip/buddies-only/default-menu-entry.svg b/src/plugins/voip/buddies-only/default-menu-entry.svg new file mode 100644 index 0000000..a17f95c --- /dev/null +++ b/src/plugins/voip/buddies-only/default-menu-entry.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/buddies-only/default-menu-screen.svg b/src/plugins/voip/buddies-only/default-menu-screen.svg new file mode 100644 index 0000000..32686a1 --- /dev/null +++ b/src/plugins/voip/buddies-only/default-menu-screen.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${channel} - ${mode} + + + + + + + + + + + + + + + + + ${emptyMessage} + + diff --git a/src/plugins/voip/buddies-only/g19-menu-entry.svg b/src/plugins/voip/buddies-only/g19-menu-entry.svg new file mode 100644 index 0000000..51d52a1 --- /dev/null +++ b/src/plugins/voip/buddies-only/g19-menu-entry.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/buddies-only/g19-menu-screen.svg b/src/plugins/voip/buddies-only/g19-menu-screen.svg new file mode 100644 index 0000000..7b252d9 --- /dev/null +++ b/src/plugins/voip/buddies-only/g19-menu-screen.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + ${emptyMessage} + + + + + + ${name} + ${channel} - ${mode} + + diff --git a/src/plugins/voip/default/Makefile.am b/src/plugins/voip/default/Makefile.am new file mode 100644 index 0000000..3775d9d --- /dev/null +++ b/src/plugins/voip/default/Makefile.am @@ -0,0 +1,31 @@ +themedir = $(datadir)/gnome15/plugins/voip/default +theme_DATA = \ + g19-menu-screen.svg \ + g19-menu-entry.svg \ + g19-message-menu-entry.svg \ + default-menu-screen.svg \ + default-menu-entry.svg \ + default-message-menu-entry.svg + +EXTRA_DIST = \ + $(theme_DATA) + + +all-local: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p i18n/$$M_LOCALE/LC_MESSAGES ; \ + if [ `ls i18n/*.po 2>/dev/null|wc -l` -gt 0 ]; then \ + for M_PO in i18n/*.po; do \ + BN=`basename $$M_PO .po`; \ + LL=`basename $$BN .$$M_LOCALE`.mo; \ + echo "$$M_PO -> $$LL"; \ + msgfmt $$M_PO --output-file i18n/$$M_LOCALE/LC_MESSAGES/$$LL; \ + done; \ + fi; \ + done + +install-exec-hook: + for M_LOCALE in @ENABLED_LOCALES@; do \ + mkdir -p $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + cp -pR i18n/$$M_LOCALE $(DESTDIR)$(datadir)/gnome15/plugins/im/default/i18n; \ + done \ No newline at end of file diff --git a/src/plugins/voip/default/default-menu-entry.svg b/src/plugins/voip/default/default-menu-entry.svg new file mode 100644 index 0000000..a17f95c --- /dev/null +++ b/src/plugins/voip/default/default-menu-entry.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/default/default-menu-screen.svg b/src/plugins/voip/default/default-menu-screen.svg new file mode 100644 index 0000000..32686a1 --- /dev/null +++ b/src/plugins/voip/default/default-menu-screen.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${channel} - ${mode} + + + + + + + + + + + + + + + + + ${emptyMessage} + + diff --git a/src/plugins/voip/default/default-message-menu-entry.svg b/src/plugins/voip/default/default-message-menu-entry.svg new file mode 100644 index 0000000..8245cd6 --- /dev/null +++ b/src/plugins/voip/default/default-message-menu-entry.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ${line} + + + + ${sender} - ${line} + + diff --git a/src/plugins/voip/default/default.theme b/src/plugins/voip/default/default.theme new file mode 100644 index 0000000..135f732 --- /dev/null +++ b/src/plugins/voip/default/default.theme @@ -0,0 +1,4 @@ +[theme] +name=Default +description=Displays buddies, messages and avatar (when supported) +supported_models=g19,g13,g15,g15v2,g510 \ No newline at end of file diff --git a/src/plugins/voip/default/g19-menu-entry.svg b/src/plugins/voip/default/g19-menu-entry.svg new file mode 100644 index 0000000..ba5de93 --- /dev/null +++ b/src/plugins/voip/default/g19-menu-entry.svg @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${item_name} + ${item_alt} + + + + + + + + ${item_name} + ${item_alt} + + + + + + diff --git a/src/plugins/voip/default/g19-menu-screen.svg b/src/plugins/voip/default/g19-menu-screen.svg new file mode 100644 index 0000000..1ecd835 --- /dev/null +++ b/src/plugins/voip/default/g19-menu-screen.svg @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + ${emptyMessage} + + + + + + + + ${name} + ${channel} - ${mode} + + + + + + + ${talking} + + diff --git a/src/plugins/voip/default/g19-message-menu-entry.svg b/src/plugins/voip/default/g19-message-menu-entry.svg new file mode 100644 index 0000000..215ca32 --- /dev/null +++ b/src/plugins/voip/default/g19-message-menu-entry.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${sender} + ${line} + : + ${sender} + + diff --git a/src/plugins/voip/default_audio-high.gif b/src/plugins/voip/default_audio-high.gif new file mode 100644 index 0000000000000000000000000000000000000000..38852d382f3f52ac996ca0171c4c429408f522d1 GIT binary patch literal 78 zcmZ?wbhEHbWM^PwXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI c$e;sK2-3p9#N(5|nX$WE#%2A@5C#To06vow!2kdN literal 0 HcmV?d00001 diff --git a/src/plugins/voip/default_audio-muted.gif b/src/plugins/voip/default_audio-muted.gif new file mode 100644 index 0000000000000000000000000000000000000000..94e0935f97dc5d9c16ca97f37884c4bfdae291c4 GIT binary patch literal 77 zcmZ?wbhEHbWM^PwXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI b$e;sK2-3p9#O;&7d7`^qEXVUI6N5DXD$x^G literal 0 HcmV?d00001 diff --git a/src/plugins/voip/default_available.gif b/src/plugins/voip/default_available.gif new file mode 100644 index 0000000000000000000000000000000000000000..43f933aa8676676f9272e2f606720c8b515374f8 GIT binary patch literal 76 zcmZ?wbhEHbWMg1sXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI a$e;sK2-4!f#O1@#Halnfm8LZe4AuZ15EJA8 literal 0 HcmV?d00001 diff --git a/src/plugins/voip/default_away.gif b/src/plugins/voip/default_away.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a0e1e05d8fafbafd0605190568fda56730f32e5 GIT binary patch literal 76 zcmZ?wbhEHbWMg1sXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI a$e;sK2-4!f#O1>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI a$e;sK2-3p9#MRO)FzeXPfNE|L25SHuClgEn literal 0 HcmV?d00001 diff --git a/src/plugins/voip/default_microphone-sensitivity-muted.gif b/src/plugins/voip/default_microphone-sensitivity-muted.gif new file mode 100644 index 0000000000000000000000000000000000000000..045b6ef81324d741c877bbc0cfadbf9c08119c4f GIT binary patch literal 75 zcmZ?wbhEHbWMg1sXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI Z$e;sK2-3p9#MzV2u>InBDI-<}YXApV64U?y literal 0 HcmV?d00001 diff --git a/src/plugins/voip/default_record.gif b/src/plugins/voip/default_record.gif new file mode 100644 index 0000000000000000000000000000000000000000..e5a613d411c09b8efe551ab94a70a106f2933e40 GIT binary patch literal 71 zcmZ?wbhEHbWMN=oXkcUjg8%>jEB+I8E=o--Nlj5G&n(GM2+2rQaQE~LU{L(Y!pOzI V$e;sK2-3p9#LAPfe0m^*H2}9p5g-5n literal 0 HcmV?d00001 diff --git a/src/plugins/voip/g19_microphone-sensitivity-high.png b/src/plugins/voip/g19_microphone-sensitivity-high.png new file mode 100644 index 0000000000000000000000000000000000000000..65209c50b99dee1ceb83797135d0aa06b050dd1f GIT binary patch literal 3420 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 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Notes +# ===== +# +# The program "contact-selector" was a big help in getting this working. The ContactList +# class is very loosely based on this, with many modifications. These are licensed under +# LGPL. See http://telepathy.freedesktop.org/wiki/Contact%20selector + + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("voip", modfile=__file__).ugettext + +import gnome15.g15globals as g15globals +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15plugin as g15plugin +import gnome15.g15screen as g15screen +import os +import time +import gnome15.colorpicker as colorpicker +from math import pi +import base64 +import cairo +import gtk + +# Logging +import logging +logger = logging.getLogger(__name__) + +# Actions +MUTE_INPUT = "voip-mute-input" +MUTE_OUTPUT = "voip-mute-ouptut" + +# Plugin details - All of these must be provided +id = "voip" +name = _("VoIP") +description = _("Provides integration with VoIP clients such as TeamSpeak3, showing \n\ +buddy lists, microphone status and more.\n\n\ +Note, TeamSpeak3 is currently the only supported client, but the intention is\n\ +to add support for others such as Mumble") +author = "Brett Smith " +copyright = _("Copyright (C)2012 Brett Smith") +site = "http://www.russo79.com/gnome15" +has_preferences = True +needs_network=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions = { + g15driver.PREVIOUS_SELECTION : _("Previous contact"), + g15driver.NEXT_SELECTION : _("Next contact"), + g15driver.VIEW : _("Show settings"), + g15driver.SELECT : _("Buddy options"), + g15driver.CLEAR : _("Toggle buddies/messages"), + g15driver.NEXT_PAGE : _("Next page"), + g15driver.PREVIOUS_PAGE : _("Previous page"), + MUTE_INPUT : _("Mute Input (Microphone)"), + MUTE_OUTPUT : _("Mute Output (Headphones/Speakers)") + } + +# Other constants +POSSIBLE_ICON_NAMES = [ "im-user", "empathy", "pidgin", "emesene", "system-config-users", "im-message-new" ] +MUTED_ICONS = ["microphone-sensitivity-muted", "microphone-sensitivity-muted-symbolic", \ + "audio-input-microphone-muted", "audio-input-microphone-muted-symbolic", \ + os.path.join(os.path.dirname(__file__), "g19_microphone-sensitivity-muted.png")] +UNMUTED_ICONS = ["microphone-sensitivity-high", "microphone-sensitivity-high-symbolic", \ + "audio-input-microphone-high", "audio-input-microphone-high-symbolic", \ + os.path.join(os.path.dirname(__file__), "g19_microphone-sensitivity-high.png")] +RECORD_ICONS = [ "media-record", "player_record" ] +MONO_RECORD_ICON = os.path.join(os.path.dirname(__file__), "default_record.gif") +MONO_MIC_UNMUTED = os.path.join(os.path.dirname(__file__), "default_microphone-sensitivity-high.gif") +MONO_MIC_MUTED = os.path.join(os.path.dirname(__file__), "default_microphone-sensitivity-muted.gif") +MONO_SPKR_UNMUTED = os.path.join(os.path.dirname(__file__), "default_audio-high.gif") +MONO_SPKR_MUTED = os.path.join(os.path.dirname(__file__), "default_audio-muted.gif") +MONO_AWAY = os.path.join(os.path.dirname(__file__), "default_away.gif") +MONO_ONLINE = os.path.join(os.path.dirname(__file__), "default_available.gif") + +IMAGE_DIR = 'images' + +MODE_ALL = "all" +MODE_ONLINE = "online" +MODE_TALKING = "talking" +MODE_LIST = [ MODE_ONLINE, MODE_TALKING, MODE_ALL ] +MODES = { + MODE_ALL : [ "All", _("All") ], + MODE_ONLINE : [ "Online", _("Online") ], + MODE_TALKING : [ "Talking", _("Talking") ] + } + + +def show_preferences(parent, driver, gconf_client, gconf_key): + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "voip.ui")) + dialog = widget_tree.get_object("VoipDialog") + dialog.set_transient_for(parent) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise_on_talk_status_change" % gconf_key, "RaiseOnTalkStatusChange", False, widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/raise_on_chat_message" % gconf_key, "RaiseOnChatMessage", False, widget_tree) + dialog.run() + dialog.hide() + +def create(gconf_key, gconf_client, screen): + """ + Create the plugin instance + + gconf_key -- GConf key that may be used for plugin preferences + gconf_client -- GConf client instance + """ + return G15Voip(gconf_client, gconf_key, screen) + +def get_backend(backend_type): + """ + Get the backend plugin module, given the backend_type + + Keyword arguments: + backend_type -- backend type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("voip-%s" % backend_type) + +def get_available_backends(): + """ + Get the "backend type" names that are available by listing all of the voip + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("voip-"): + l.append(p.id[5:]) + return l + +def get_backlight_key(gconf_key, buddy): + """ + Get the gconf key used to store the buddy backlight color + + Keyword arguments: + gconf_key -- key root (from plugin) + buddy -- buddy menuitem object + """ + enc_name = base64.b16encode(buddy.nickname) + return "%s/backlight_colors/%s" % (gconf_key, enc_name) + + +def compare_buddies(a, b): + """ + Compare two buddies based on their alias and presence + + Keyword arguments: + a -- buddy 1 + b -- buddy 2 + """ + if ( a is None and b is not None ): + val = 1 + elif ( b is None and a is not None ): + val = -1 + elif ( b is None and a is None ): + val = 0 + else: + val = cmp(a.talking, b.talking) + if val == 0: + val = cmp(a.away, b.away) + + return val + + +class VoipBackend(): + + def __init__(self): + pass + + def get_name(self): + """ + Get the backend name + """ + raise Exception("Not implemented") + + def get_current_channel(self): + """ + Get the current channel + """ + raise Exception("Not implemented") + + def get_talking(self): + """ + Get who is talking + """ + raise Exception("Not implemented") + + def get_me(self): + """ + Get the local user's buddy entry + """ + raise Exception("Not implemented") + + def get_channels(self): + raise Exception("Not implemented") + + def get_buddies(self, current_channel=True): + raise Exception("Not implemented") + + def start(self, plugin): + raise Exception("Not implemented") + + def stop(self): + raise Exception("Not implemented") + + def get_icon(self): + raise Exception("Not implemented") + + def set_audio_input(self, mute): + raise Exception("Not implemented") + + def set_audio_output(self, mute): + raise Exception("Not implemented") + + def away(self): + raise Exception("Not implemented") + + def online(self): + raise Exception("Not implemented") + +class MessageMenuItem(g15theme.MenuItem): + def __init__(self, sender, text, highlight): + g15theme.MenuItem.__init__(self, "message-%s" % time.time(), text, highlight) + self._text = text + self._highlight = highlight + self._sender = sender + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + item_properties["highlight"] = self._highlight + item_properties["sender"] = self._sender + item_properties["line"] = self._text + return item_properties + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "message-menu-entry")) + +class ChannelMenuItem(g15theme.MenuItem): + def __init__(self, component_id, name, backend, icon = None): + g15theme.MenuItem.__init__(self, component_id, icon = icon) + self.name = name + self.topic = None + self.backend = backend + self.radio = True + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = self.radio + if self.radio: + p["item_radio_selected"] = self == self.backend.get_current_channel() + return p + + def activate(self): + ret = self.on_activate() + self.get_root().delete() + return ret + + def on_activate(self): + return True + +class BuddyMenuItem(g15theme.MenuItem): + def __init__(self, component_id, nickname, channel, plugin): + self.nickname = nickname + self._plugin = plugin + g15theme.MenuItem.__init__(self, component_id) + self.input_muted = None + self.output_muted = None + self.away = False + self.talking = False + self.channel = channel + + def activate(self): + self._plugin.activate_item(self) + return True + + def on_configure(self): + self.set_theme(g15theme.G15Theme(self.parent.get_theme().dir, "menu-entry" if self.group else "menu-child-entry")) + + def is_showing(self): + menu = self.parent + if not menu: + return False + is_showing_status = (menu.mode == MODE_TALKING and self.talking) or \ + (menu.mode == MODE_ONLINE and not self.away) or \ + menu.mode == MODE_ALL + is_showing_channel = self.channel == self._plugin.backend.get_current_channel() + return is_showing_status and is_showing_channel + + def get_theme_properties(self): + item_properties = g15theme.MenuItem.get_theme_properties(self) + + if self.away and isinstance(self.away, str): + item_properties["item_name"] = "%s - %s" % (self.nickname, self.away) + else: + item_properties["item_name"] = self.nickname + + item_properties["item_alt"] = "" + item_properties["item_type"] = "" + + if self.get_screen().device.bpp == 1: + item_properties["item_talking_icon"] = MONO_RECORD_ICON if self.talking else "" + item_properties["item_input_muted_icon"] = MONO_MIC_MUTED if self.input_muted else MONO_MIC_UNMUTED + item_properties["item_output_muted_icon"] = MONO_SPKR_MUTED if self.output_muted else MONO_SPKR_UNMUTED + item_properties["item_icon"] = MONO_ONLINE if not self.away else MONO_AWAY + else: + item_properties["item_input_muted_icon"] = g15icontools.get_icon_path(MUTED_ICONS if self.input_muted else UNMUTED_ICONS) + item_properties["item_output_muted_icon"] = g15icontools.get_icon_path("audio-volume-muted" if self.output_muted else "audio-volume-high") + item_properties["item_icon"] = g15icontools.get_icon_path("user-available" if not self.away else "user-away") + item_properties["item_talking_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if self.talking else "" + + return item_properties + + +class BuddyMenu(g15theme.Menu): + + def __init__(self): + g15theme.Menu.__init__(self, "menu") + self.mode = MODE_ONLINE + self.focusable = True + +class G15Voip(g15plugin.G15MenuPlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15MenuPlugin.__init__(self, gconf_client, gconf_key, screen, POSSIBLE_ICON_NAMES, id, name) + self.hidden = False + + def activate(self): + """ + We override default activate behavior as we only want the page to + appear once connected to the backend + """ + self.backend = None + self.message_menu = None + self.active = True + + self._talking = None + self._backlight_ctrl = self.screen.driver.get_control("backlight_colour") + self._backlight_acq = None + self._raise_timer = None + self._connected = False + self._connection_timer = None + + self.screen.key_handler.action_listeners.append(self) + self.reload_theme() + self._attempt_connection() + + def _attempt_connection(self): + try: + if self._raise_timer is not None: + self._raise_timer.cancel() + # For now, TS3 only backend + self.backend = get_backend("teamspeak3").create_backend() + self.set_icon(self.backend.get_icon()) + if self.backend.start(self): + self.show_menu() + self._connected = True + else: + self._connection_timer = g15scheduler.schedule("ReconnectVoip", 5, self._attempt_connection) + except Exception as e: + logger.debug("Error connecting. Will retry...", exc_info = e) + self._connection_timer = g15scheduler.schedule("ReconnectVoip", 5, self._attempt_connection) + + def create_menu(self): + return BuddyMenu() + + def create_page(self): + page = g15plugin.G15MenuPlugin.create_page(self) + m = g15theme.Menu("messagesMenu") + m.focusable = True + page.add_child(m) + self.message_menu = m + page.add_child(g15theme.MenuScrollbar("messagesScrollbar", self.message_menu)) + return page + + def deactivate(self): + if self._backlight_acq: + self.screen.driver.release_control(self._backlight_acq) + self._backlight_acq = None + if self._connection_timer is not None: + self._connection_timer.cancel() + if self.backend is not None: + self.backend.stop() + self.screen.key_handler.action_listeners.remove(self) + g15plugin.G15MenuPlugin.deactivate(self) + + def load_menu_items(self): + g15plugin.G15MenuPlugin.load_menu_items(self) + self._load_buddy_list() + + def action_performed(self, binding): + if not self._connected: + return False + + if self.page != None and self.page.is_visible(): + + if binding.action == g15driver.VIEW: + MeOperationMenu(self.gconf_client, self.gconf_key, self.screen, self.backend, self.menu, self) + return True + + if binding.action == g15driver.CLEAR and self.page != None and self.page.is_visible(): + self.page.next_focus() + return True + + if self.menu.is_focused(): + return self.menu.action_performed(binding) + + def get_theme_properties(self): + props = g15plugin.G15MenuPlugin.get_theme_properties(self) + props["mode"] = MODES[self.menu.mode][1] + props["name"] = self.backend.get_name() if self.backend is not None else "" + props["channel"] = self.backend.get_current_channel().name if self.backend is not None else "" + + if self.menu.get_showing_count() == 0: + if self.menu.mode == MODE_ALL: + props["emptyMessage"] = _("Nobody connected") + elif self.menu.mode == MODE_ONLINE: + props["emptyMessage"] = _("Nobody online") + elif self.menu.mode == MODE_TALKING: + props["emptyMessage"] = _("Nobody talking") + else: + props["emptyMessage"] = "" + + # Get what mode to switch to + mode_index = MODE_LIST.index(self.menu.mode) + 1 + if mode_index >= len(MODE_LIST): + mode_index = 0 + + props["list"] = MODES[MODE_LIST[mode_index]][0] + + talking_buddy = self.backend.get_talking() + me = self.backend.get_me() + + props["talking"] = talking_buddy.nickname if talking_buddy is not None else "" + props["talking_avatar"] = talking_buddy.avatar if talking_buddy is not None else (me.avatar if me is not None and me.avatar is not None else self.backend.get_icon()) + props["talking_avatar_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if talking_buddy is not None else None + + if self.screen.device.bpp == 1: + props["talking_icon"] = MONO_RECORD_ICON if me is not None and me.talking else "" + props["input_muted_icon"] = MONO_MIC_MUTED if me is not None and me.input_muted else MONO_MIC_UNMUTED + props["output_muted_icon"] = MONO_SPKR_MUTED if me is not None and me.output_muted else MONO_SPKR_UNMUTED + props["status_icon"] = MONO_ONLINE if me is not None and not me.away else MONO_AWAY + else: + props["status_icon"] = g15icontools.get_icon_path("user-available" if me is not None and not me.away else "user-away") + props["input_muted_icon"] = g15icontools.get_icon_path(MUTED_ICONS if me is not None and me.input_muted else UNMUTED_ICONS) + props["output_muted_icon"] = g15icontools.get_icon_path("audio-volume-muted" if me is not None and me.output_muted else "audio-volume-high") + props["talking_icon"] = g15icontools.get_icon_path(RECORD_ICONS) if me is not None and me.talking else "" + + return props + + + """ + Backends may call these functions when they get events internally + """ + def buddy_left(self, buddy_item): + if buddy_item.channel.name == self.backend.get_current_channel().name: + self.message_received(self.backend.get_name(), _("%s left channel" % buddy_item.nickname), True) + if self.menu is not None: + self.menu.remove_child(buddy_item) + + def redraw(self): + """ + Redraw the page + """ + if self.page is not None: + self.page.mark_dirty() + self.page.redraw() + + def message_received(self, sender, message, highlight = False): + """ + Add a message to the message list + + Keyword arguments: + sender -- sender + message -- message + """ + if self.message_menu is not None: + while self.message_menu.get_child_count() > 20: + self.message_menu.remove_child_at(0) + self.message_menu.add_child(MessageMenuItem(sender, message, highlight)) + self.message_menu.select_last_item() + self.page.mark_dirty() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise_on_chat_message" % self.gconf_key, False): + self._popup() + + def activate_item(self, item): + """ + Activate a buddy item, showing the menu + + Keyword arugments: + item -- buddy menu item object + """ + BuddyOperationMenu(self.gconf_client, self.gconf_key, self.screen, self.backend, item, self) + + def reload_buddies(self): + """ + Reload all buddies + """ + if self.page is not None: + self._load_buddy_list() + self.redraw() + + def new_buddy(self, buddy_item): + """ + A new buddy has entered view + + Keyword arguments: + buddy_item -- new buddy + """ + if buddy_item.channel.name == self.backend.get_current_channel().name: + self.message_received(self.backend.get_name(), _("%s entered channel" % buddy_item.nickname), True) + if self.menu is not None: + items = self.menu.get_children() + items.append(buddy_item) + self.menu.set_children(sorted(items, cmp=compare_buddies)) + + self.redraw() + + def new_channel(self, channel_item): + """ + A new channel has been created + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_removed(self, channel_item): + """ + A channel has been removed + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_updated(self, channel_item): + """ + A channel has been updated + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def channel_moved(self, channel_item): + """ + A channel has been moved + + Keyword arugments: + channel_item -- channel menu item object + """ + self.redraw() + + def moved_channels(self, buddy_item, old_channel, new_channel): + """ + A buddy has moved channels + + Keyword arugments: + buddy_item -- buddy menu item object + old_channel -- old channel menu item object + new_channel -- new channel menu item object + """ +# if buddy_item.channel.name == self.backend.get_current_channel().name: +# self.message_received(self.backend.get_name(), _("%s changed channels" % buddy_item.nickname), True) + self.redraw() + + def talking_status_changed(self, talking): + """ + Current talking buddy has changed. + + Keyword arguments: + talking -- new buddy talking + """ + if (self._talking is None and talking is not None) or \ + (talking is None and self._talking is not None) or \ + (talking is not None and talking != self._talking): + self._talking = talking + if self._backlight_acq is not None and self._talking is None: + self.screen.driver.release_control(self._backlight_acq) + self._backlight_acq = None + if self._talking is not None: + hex_color = g15gconf.get_string_or_default(self.gconf_client, get_backlight_key(self.gconf_key, self._talking), "") + if hex_color != "": + if self._backlight_acq is None: + self._backlight_acq = self.screen.driver.acquire_control(self._backlight_ctrl) + self._backlight_acq.set_value(g15convert.to_rgb(hex_color)) + + self.redraw() + if g15gconf.get_bool_or_default(self.gconf_client, "%s/raise_on_talk_status_change" % self.gconf_key, False): + self._popup() + + """ + Private + """ + + def _popup(self): + self._raise_timer = self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after=6.0) + + def _load_buddy_list(self): + items = self.backend.get_buddies() + self.menu.set_children(sorted(items, cmp=compare_buddies)) + if len(items) > 0: + self.menu.selected = items[0] + else: + self.menu.selected = None + + def _disconnected(self): + self._connected = False + self.hide_menu() + self._attempt_connection() + +class BuddyActionMenuItem(g15theme.MenuItem): + def __init__(self, component_id, name, buddy, backend, icon=None): + g15theme.MenuItem.__init__(self, component_id, True, name, icon=icon) + self.buddy = buddy + self.backend = backend + +class KickBuddyMenuItem(BuddyActionMenuItem): + def __init__(self, buddy, backend): + BuddyActionMenuItem.__init__(self, 'kick', _('Kick'), buddy, backend, icon=g15icontools.get_icon_path(['force-exit', 'gnome-panel-force-quit'], include_missing=False)) + + def _confirm(self, arg): + self.backend.kick(self.buddy) + self.get_root().delete() + + def activate(self): + g15theme.ConfirmationScreen(self.get_screen(), _("Kick Buddy"), _("Are you sure you want to kick\n%s from the server") % self.buddy.nickname, + self.backend.get_icon(), self._confirm, None) + +class BanBuddyMenuItem(BuddyActionMenuItem): + def __init__(self, buddy, backend): + BuddyActionMenuItem.__init__(self, 'ban', _('Ban'), buddy, backend, icon=g15icontools.get_icon_path(['audio-volume-muted-blocked', 'mail_spam', 'stock_spam'], include_missing=False)) + + def _confirm(self, arg): + self.backend.ban(self.buddy) + self.get_root().delete() + + def activate(self): + g15theme.ConfirmationScreen(self.get_screen(), _("Ban Buddy"), _("Are you sure you want to ban\n%s from the server") % self.buddy.nickname, + self.backend.get_icon(), self._confirm, None) + + +class SelectChannelMenuItem(g15theme.MenuItem): + def __init__(self, gconf_client, gconf_key, backend, plugin): + g15theme.MenuItem.__init__(self, 'channel', True, _('Select channel / server'), icon=g15icontools.get_icon_path(['addressbook', 'office-address-book', 'stcok_addressbook', 'x-office-address-book' ], include_missing=False)) + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._backend = backend + self._plugin = plugin + + def activate(self): + SelectChannelMenu(self._gconf_client, self._gconf_key, self.get_screen(), self._backend, self._plugin) + +class BuddyBacklightMenuItem(BuddyActionMenuItem): + def __init__(self, gconf_client, gconf_key, buddy, backend, ctrl, plugin): + BuddyActionMenuItem.__init__(self, 'color', _('Select backlight'), buddy, backend, icon=g15icontools.get_icon_path(['preferences-color', 'gtk-select-color', 'color-picker' ], include_missing=False)) + self._ctrl = ctrl + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._plugin = plugin + + def activate(self): + BuddyBacklightMenu(self._gconf_client, self._gconf_key, self.get_screen(), self.backend, self.buddy, self._ctrl, self._plugin) + +class ReturnMenuItem(g15theme.MenuItem): + def __init__(self): + g15theme.MenuItem.__init__(self, 'return', True, _('Back to previous menu'), icon=g15icontools.get_icon_path(['back', 'gtk-go-back-ltr'])) + + def activate(self): + self.get_root().delete() + +class AudioInputMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'audio-input', False, _("Input")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + me = self._backend.get_me() + if self.get_screen().device.bpp == 1: + p["item_icon"] = MONO_MIC_MUTED if me.input_muted else MONO_MIC_UNMUTED + else: + p["item_icon"] = g15icontools.get_icon_path(MUTED_ICONS if me.input_muted else UNMUTED_ICONS) + p["item_name"] = _("Un-mute audio input") if me.input_muted else _("Mute audio input") + return p + + def activate(self): + self._backend.set_audio_input(not self._backend.get_me().input_muted) + self.get_root().delete() + +class AudioOutputMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'audio-output', False, _("Output")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + me = self._backend.get_me() + if self.get_screen().device.bpp == 1: + p["item_icon"] = MONO_SPKR_MUTED if me.output_muted else MONO_SPKR_UNMUTED + else: + p["item_icon"] = g15icontools.get_icon_path("audio-volume-muted" if me.output_muted else "audio-volume-high") + p["item_name"] = _("Un-mute audio output") if me.output_muted else _("Mute audio output") + return p + + def activate(self): + self._backend.set_audio_output(not self._backend.get_me().output_muted) + self.get_root().delete() + +class AwayStatusMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'away', False, _("Away"), icon=g15icontools.get_icon_path("user-away")) + self._backend = backend + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = self._backend.get_me().away + return p + + def activate(self): + self._backend.away() + self.get_root().delete() + +class OnLineStatusMenuItem(g15theme.MenuItem): + def __init__(self, backend): + g15theme.MenuItem.__init__(self, 'online', False, _("Online"), icon=g15icontools.get_icon_path("user-available")) + self._backend = backend + + def activate(self): + self._backend.online() + self.get_root().delete() + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = not self._backend.get_me().away + return p + +class SelectModeMenuItem(g15theme.MenuItem): + def __init__(self, gconf_client, gconf_key, mode, mode_name, backend, buddy_menu): + g15theme.MenuItem.__init__(self, 'mode-%s' % mode, False, mode_name) + self._buddy_menu = buddy_menu + self._mode = mode + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def get_theme_properties(self): + p = g15theme.MenuItem.get_theme_properties(self) + p["item_radio"] = True + p["item_radio_selected"] = self._mode == g15gconf.get_string_or_default(self._gconf_client, "%s/mode" % self._gconf_key, MODE_ONLINE) + return p + + def activate(self): + self._buddy_menu.mode = self._mode + logger.info("Mode is now %s", self._mode) + self._gconf_client.set_string(self._gconf_key + "/mode", self._mode) + self._buddy_menu.get_screen().redraw(self._buddy_menu.get_root()) + self.get_root().delete() + +class ColorMenuItem(BuddyActionMenuItem): + def __init__(self, gconf_client, gconf_key, color, color_name, buddy, backend): + fmt_color = "%02x%02x%02xff" % color + BuddyActionMenuItem.__init__(self, 'color-%s' % fmt_color, color_name, buddy, backend) + self.icon = self.color_square(color, 16, 2) + self.color = color + self._gconf_client = gconf_client + self._gconf_key = gconf_key + + def activate(self): + self._gconf_client.set_string(get_backlight_key(self._gconf_key, self.buddy), g15convert.rgb_to_string(self.color)) + self.get_root().delete() + + def color_square(self, color, size, radius=0): + surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, size, size) + cr = cairo.Context(surface) + cr.set_source_rgba(color[0] / 255.0, + color[1] / 255.0, + color[2] / 255.0, 1.0) + cr.move_to(radius, 0) + cr.line_to(size - radius, 0) + cr.arc(size - radius, radius, radius, 3 * pi / 2, 2 * pi) + cr.line_to(size, size - radius) + cr.arc(size - radius, size - radius, radius, 0, pi / 2) + cr.line_to(radius, size) + cr.arc(radius, size - radius, radius, pi / 2, pi) + cr.line_to(0, radius) + cr.arc(radius, radius, radius, pi, 3 * pi / 2) + cr.close_path() + cr.fill() + return surface + + +class SelectChannelMenu(g15theme.G15Page): + + def __init__(self, gconf_client, gconf_key, screen, backend, plugin): + g15theme.G15Page.__init__(self, _("Server/Channel"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Server/Channel"), + "icon": backend.get_icon(), + "alt_title": '' + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + for c in backend.get_channels(): + self.menu.add_child(c) + if c == backend.get_current_channel(): + self.menu.set_selected_item(c) + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + +class BuddyBacklightMenu(g15theme.G15Page): + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy, ctrl, plugin): + g15theme.G15Page.__init__(self, _("Backlight"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Backlight"), + "icon": backend.get_icon() if buddy.avatar is None else buddy.avatar, + "alt_title": buddy.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + self.ctrl = ctrl + self.acq = None + + sel_color = g15convert.to_rgb(g15gconf.get_string_or_default( + gconf_client, get_backlight_key(gconf_key, buddy), + "255,255,255")) + for i, c in enumerate(colorpicker.COLORS_FULL): + c = (c[0], c[1], c[2]) + item = ColorMenuItem(gconf_client, gconf_key, c, + colorpicker.COLORS_NAMES[i], buddy, backend) + self.menu.add_child(item) + if c == sel_color: + self.menu.set_selected_item(item) + + self.menu.on_selected = self._handle_selected + self.on_deleted = self._release_control + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + self._handle_selected() + + def _handle_selected(self): + self._release_control() + if isinstance(self.menu.selected, ColorMenuItem): + self.acq = self.get_screen().driver.acquire_control(self.ctrl) + self.acq.set_value(self.menu.selected.color) + + def _release_control(self): + if self.acq is not None: + self.get_screen().driver.release_control(self.acq) + self.acq = None + +class MeOperationMenu(g15theme.G15Page): + """ + Me to select operations appropriate for the current local user. Includes + setting channel, status, buddy list mode and others + """ + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy_menu, plugin): + g15theme.G15Page.__init__(self, _("Settings"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + me = backend.get_me() + self.theme_properties = { + "title": _("Settings"), + "icon": backend.get_icon() if me.avatar is None else me.avatar, + "alt_title": me.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + + + self.menu.add_child(SelectChannelMenuItem(gconf_client, gconf_key, backend, plugin)) + + self.menu.add_child(g15theme.MenuItem('audio-status', True, _('Audio'), activatable=False)) + self.menu.add_child(AudioInputMenuItem(backend)) + self.menu.add_child(AudioOutputMenuItem(backend)) + + self.menu.add_child(g15theme.MenuItem('select-status', True, _('Select Status'), activatable=False)) + self.menu.add_child(OnLineStatusMenuItem(backend)) + self.menu.add_child(AwayStatusMenuItem(backend)) + + self.menu.add_child(g15theme.MenuItem('select-mode', True, _('Select Buddy List Mode'), activatable=False)) + for i in MODE_LIST: + self.menu.add_child(SelectModeMenuItem(gconf_client, gconf_key, i, MODES[i][1], backend, buddy_menu)) + + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + +class BuddyOperationMenu(g15theme.G15Page): + """ + Menu for operations appropriate for other buddies including kick, ban + and select backlight + """ + + def __init__(self, gconf_client, gconf_key, screen, backend, buddy, plugin): + g15theme.G15Page.__init__(self, _("Actions"), screen, priority=g15screen.PRI_HIGH, \ + theme=g15theme.G15Theme(os.path.join(g15globals.themes_dir, "default"), "menu-screen"), + originating_plugin = plugin) + self.theme_properties = { + "title": _("Actions"), + "icon": backend.get_icon() if buddy.avatar is None else buddy.avatar, + "alt_title": buddy.nickname + } + self.menu = g15theme.Menu("menu") + self.get_screen().add_page(self) + self.add_child(self.menu) + self.menu.add_child(KickBuddyMenuItem(buddy, backend)) + self.menu.add_child(BanBuddyMenuItem(buddy, backend)) + ctrl = screen.driver.get_control("backlight_colour") + if ctrl is not None: + self.menu.add_child(BuddyBacklightMenuItem(gconf_client, gconf_key, buddy, backend, ctrl, plugin)) + self.menu.add_child(ReturnMenuItem()) + self.add_child(g15theme.MenuScrollbar("viewScrollbar", self.menu)) + diff --git a/src/plugins/voip/voip.ui b/src/plugins/voip/voip.ui new file mode 100644 index 0000000..eb9315c --- /dev/null +++ b/src/plugins/voip/voip.ui @@ -0,0 +1,115 @@ + + + + + + + + + + + + + zoom + + + tile + + + center + + + scale + + + stretch + + + + + False + 5 + VoIP Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + 4 + + + Raise when talk status changes + True + True + False + True + + + True + True + 0 + + + + + Raise on chat message + True + True + False + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 1 + + + + + + button9 + + + diff --git a/src/plugins/volume/Makefile.am b/src/plugins/volume/Makefile.am new file mode 100644 index 0000000..468eb2a --- /dev/null +++ b/src/plugins/volume/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default + +plugindir = $(datadir)/gnome15/plugins/volume +plugin_DATA = volume.py \ + volume.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/volume/default/Makefile.am b/src/plugins/volume/default/Makefile.am new file mode 100644 index 0000000..20da2ea --- /dev/null +++ b/src/plugins/volume/default/Makefile.am @@ -0,0 +1,7 @@ +themedir = $(datadir)/gnome15/plugins/volume/default +theme_DATA = g19.svg \ + mx5500.svg \ + default.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/volume/default/default.svg b/src/plugins/volume/default/default.svg new file mode 100644 index 0000000..7c4998f --- /dev/null +++ b/src/plugins/volume/default/default.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Muted + + diff --git a/src/plugins/volume/default/g19.svg b/src/plugins/volume/default/g19.svg new file mode 100644 index 0000000..40dbffc --- /dev/null +++ b/src/plugins/volume/default/g19.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugins/volume/default/mx5500.svg b/src/plugins/volume/default/mx5500.svg new file mode 100644 index 0000000..243bd0d --- /dev/null +++ b/src/plugins/volume/default/mx5500.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Muted + + diff --git a/src/plugins/volume/i18n/volume.en_GB.po b/src/plugins/volume/i18n/volume.en_GB.po new file mode 100644 index 0000000..b707fba --- /dev/null +++ b/src/plugins/volume/i18n/volume.en_GB.po @@ -0,0 +1,26 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/volume.glade.h:1 +msgid "Device" +msgstr "Device" + +#: i18n/volume.glade.h:2 +msgid "Volume Preferences" +msgstr "Volume Preferences" diff --git a/src/plugins/volume/i18n/volume.glade.h b/src/plugins/volume/i18n/volume.glade.h new file mode 100644 index 0000000..b40ba0c --- /dev/null +++ b/src/plugins/volume/i18n/volume.glade.h @@ -0,0 +1,2 @@ +char *s = N_("Device"); +char *s = N_("Volume Preferences"); diff --git a/src/plugins/volume/i18n/volume.pot b/src/plugins/volume/i18n/volume.pot new file mode 100644 index 0000000..dd9423b --- /dev/null +++ b/src/plugins/volume/i18n/volume.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/volume.glade.h:1 +msgid "Device" +msgstr "" + +#: i18n/volume.glade.h:2 +msgid "Volume Preferences" +msgstr "" diff --git a/src/plugins/volume/volume.py b/src/plugins/volume/volume.py new file mode 100644 index 0000000..76afbc1 --- /dev/null +++ b/src/plugins/volume/volume.py @@ -0,0 +1,415 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("volume", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15icontools as g15icontools +import gnome15.g15theme as g15theme +import gnome15.g15driver as g15driver +import gnome15.g15devices as g15devices +import gnome15.g15actions as g15actions + +import alsaaudio +import select +import os +import gtk +import logging +logger = logging.getLogger(__name__) + +from threading import Thread + +# Custom actions +VOLUME_UP = "volume-up" +VOLUME_DOWN = "volume-down" +MUTE = "mute" + +# Register the action with all supported models +g15devices.g15_action_keys[VOLUME_UP] = g15actions.ActionBinding(VOLUME_UP, [ g15driver.G_KEY_VOL_UP ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[VOLUME_UP] = g15actions.ActionBinding(VOLUME_UP, [ g15driver.G_KEY_VOL_UP ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[VOLUME_DOWN] = g15actions.ActionBinding(VOLUME_DOWN, [ g15driver.G_KEY_VOL_DOWN ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[VOLUME_DOWN] = g15actions.ActionBinding(VOLUME_DOWN, [ g15driver.G_KEY_VOL_DOWN ], g15driver.KEY_STATE_UP) +g15devices.g15_action_keys[MUTE] = g15actions.ActionBinding(MUTE, [ g15driver.G_KEY_MUTE ], g15driver.KEY_STATE_UP) +g15devices.g19_action_keys[MUTE] = g15actions.ActionBinding(MUTE, [ g15driver.G_KEY_MUTE ], g15driver.KEY_STATE_UP) + +# Plugin details - All of these must be provided +id="volume" +name=_("Volume Monitor") +description=_("Uses the M-Key lights as a volume meter. If your model has \ +a screen, a page will also popup showing the current volume. \ +You may choose the mixer that is monitored in the preferences for this plugin.\n\n \ +This plugin also registers some actions that may be assigned to macro keys. \ +The actions volume-up, volume-down and mute all work directly on the mixer, \ +so may be used control the master volume when full screen games are running too.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +unsupported_models = [ g15driver.MODEL_G930, g15driver.MODEL_G35 ] +actions={ + VOLUME_UP : "Increase the volume", + VOLUME_DOWN : "Decrease the volume", + MUTE : "Mute", + } + + +''' +This plugin displays a high priority screen when the volume is changed for a +fixed number of seconds +''' + +def create(gconf_key, gconf_client, screen): + return G15Volume(screen, gconf_client, gconf_key) + +def show_preferences(parent, driver, gconf_client, gconf_key): + def refresh_devices(widget): + new_soundcard_name = soundcard_model[widget.get_active()][0] + new_soundcard_index = alsa_soundcards.index(new_soundcard_name) + ''' + We temporarily block the handler for the mixer_combo 'changed' signal, since we are going + to change the combobox contents. + ''' + mixer_combo.handler_block(changed_handler_id) + mixer_model.clear() + for mixer in alsaaudio.mixers(new_soundcard_index): + mixer_model.append([mixer]) + # Now we can unblock the handler + mixer_combo.handler_unblock(changed_handler_id) + # And since the list of mixers has changed, we select the first one by default + mixer_combo.set_active(0) + + widget_tree = gtk.Builder() + widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "volume.ui")) + dialog = widget_tree.get_object("VolumeDialog") + soundcard_combo = widget_tree.get_object('SoundcardCombo') + mixer_combo = widget_tree.get_object('MixerCombo') + soundcard_model = widget_tree.get_object("SoundcardModel") + mixer_model = widget_tree.get_object("MixerModel") + alsa_soundcards = alsaaudio.cards() + soundcard_name = g15gconf.get_string_or_default(gconf_client, + gconf_key + "/soundcard", + str(alsa_soundcards[0])) + soundcard_index = alsa_soundcards.index(soundcard_name) + soundcard_mixers = alsaaudio.mixers(soundcard_index) + + for card in alsa_soundcards: + soundcard_model.append([card]) + for mixer in soundcard_mixers: + mixer_model.append([mixer]) + + g15uigconf.configure_combo_from_gconf(gconf_client, \ + gconf_key + "/soundcard", \ + "SoundcardCombo", \ + str(alsa_soundcards[0]), \ + widget_tree) + + changed_handler_id = g15uigconf.configure_combo_from_gconf(gconf_client, \ + gconf_key + "/mixer", \ + "MixerCombo", \ + str(soundcard_mixers[0]), \ + widget_tree) + soundcard_combo.connect('changed', refresh_devices) + + dialog.set_transient_for(parent) + dialog.run() + dialog.hide() + +class G15Volume(): + + def __init__(self, screen, gconf_client, gconf_key): + self._screen = screen + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._volume = 0.0 + self._volthread = None + self._mute = False + self._light_controls = None + self._lights_timer = None + self._reload_config_timer = None + + def activate(self): + self._screen.key_handler.action_listeners.append(self) + self._activated = True + self._read_config() + self._start_monitoring() + self._notify_handler = self._gconf_client.notify_add(self._gconf_key, self._config_changed); + + def deactivate(self): + self._screen.key_handler.action_listeners.remove(self) + self._activated = False + self._stop_monitoring() + self._gconf_client.notify_remove(self._notify_handler) + + def destroy(self): + pass + + def action_performed(self, binding): + if binding.action in [ VOLUME_UP, VOLUME_DOWN, MUTE ]: + vol_mixer = self._open_mixer() + try : + if binding.action == MUTE: + # Handle mute + mute = False + mutes = None + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.debug("Could not get mute channel. Trying PCM mixer", exc_info = e) + if vol_mixer is not None: + vol_mixer.close() + # Some pulse weirdness maybe? + vol_mixer = self._open_mixer("PCM", self.current_card_index) + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.warning("No mute switch found", exc_info = e) + if mutes != None: + for ch_mute in mutes: + if ch_mute: + mute = True + vol_mixer.setmute(1 if not mute else 0) + else: + volumes = vol_mixer.getvolume() + total = 0 + for vol in volumes: + total += vol + volume = total / len(volumes) + + if binding.action == VOLUME_UP and volume < 100: + volume += 10 + vol_mixer.setvolume(min(volume, 100)) + elif binding.action == VOLUME_DOWN and volume > 0: + volume -= 10 + vol_mixer.setvolume(max(volume, 0)) + + finally : + if vol_mixer is not None: + vol_mixer.close() + + + + ''' Functions specific to plugin + ''' + def _start_monitoring(self): + self._volthread = VolumeThread(self) + self._volthread.start() + + def _config_changed(self, client, connection_id, entry, args): + ''' + If the user changes the soundcard on the preferences dialog this method + would be called two times. A first time for the soundcard change, and a + second time because the first mixer of the newly selected soundcard is + automatically selected. + The volume monitoring would then be restarted twice, which makes no sense. + Instead of restarting the monitoring as soon as this method is called, + we put it as a task on a queue for 1 second. If during that time, any + other change happens to the configuration, the previous restart request + is cancelled, and another one takes it's place. + This way, the monitoring is only restarted once when the user selects another + sound card. + ''' + if self._reload_config_timer is not None: + if not self._reload_config_timer.is_complete(): + self._reload_config_timer.cancel() + self._reload_config_timer = None + + self._reload_config_timer = g15scheduler.queue('VolumeMonitorQueue', + 'RestartVolumeMonitoring', + 1.0, + self._restart_monitoring) + + def _restart_monitoring(self): + self._stop_monitoring() + self._read_config() + self._start_monitoring() + + def _read_config(self): + self.soundcard_name = g15gconf.get_string_or_default(self._gconf_client, \ + self._gconf_key + "/soundcard", \ + str(alsaaudio.cards()[0])) + self.soundcard_index = alsaaudio.cards().index(self.soundcard_name) + + self.mixer_name = g15gconf.get_string_or_default(self._gconf_client, \ + self._gconf_key + "/mixer", \ + str(alsaaudio.mixers(self.soundcard_index)[0])) + if not self.mixer_name in alsaaudio.mixers(self.soundcard_index): + self.mixer_name = str(alsaaudio.mixers(self.soundcard_index)[0]) + self._gconf_client.set_string(self._gconf_key + "/mixer", self.mixer_name) + + + def _stop_monitoring(self): + if self._volthread != None: + self._volthread._stop_monitoring() + self._volthread.join(1.0) + + def _get_theme_properties(self): + properties = {} + icon = "audio-volume-muted" + if not self._mute: + if self._volume < 34: + icon = "audio-volume-low" + elif self._volume < 67: + icon = "audio-volume-medium" + else: + icon = "audio-volume-high" + else: + properties [ "muted"] = True + icon_path = g15icontools.get_icon_path(icon, self._screen.driver.get_size()[0]) + properties["state"] = icon + properties["icon"] = icon_path + properties["vol_pc"] = self._volume + for i in range(0, int( self._volume / 10 ) + 1, 1): + properties["bar" + str(i)] = True + return properties + + def _release_lights(self): + if self._light_controls is not None: + self._screen.driver.release_control(self._light_controls) + self._light_controls = None + + def _open_mixer(self, mixer_name = None): + mixer_name = self.mixer_name if mixer_name is None else mixer_name + if not mixer_name or mixer_name == "": + mixer_name = "Master" + + logger.info("Opening soundcard %s mixer %s", self.soundcard_name, mixer_name) + + vol_mixer = alsaaudio.Mixer(mixer_name, cardindex=self.soundcard_index) + return vol_mixer + + def _popup(self): + if not self._activated: + logger.warning("Cannot popup volume when it is deactivated. This suggests the volume thread has not died.") + return + + if not self._light_controls: + self._light_controls = self._screen.driver.acquire_control_with_hint(g15driver.HINT_MKEYS) + if self._lights_timer is not None: + self._lights_timer.cancel() + if self._light_controls is not None: + self._lights_timer = g15scheduler.schedule("ReleaseMKeyLights", 3.0, self._release_lights) + + page = self._screen.get_page(id) + if page == None: + if self._screen.driver.get_bpp() != 0: + page = g15theme.G15Page(id, self._screen, priority=g15screen.PRI_HIGH, title="Volume", theme = g15theme.G15Theme(self), \ + theme_properties_callback = self._get_theme_properties, + originating_plugin = self) + self._screen.delete_after(3.0, page) + self._screen.add_page(page) + else: + self._screen.raise_page(page) + self._screen.delete_after(3.0, page) + + + vol_mixer = self._open_mixer() + mute_mixer = None + + try : + + # Handle mute + mute = False + mutes = None + try : + mutes = vol_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.debug("Could note get mute channel. Trying PCM", exc_info = e) + # Some pulse weirdness maybe? + mute_mixer = alsaaudio.Mixer("PCM", cardindex=self.soundcard_index) + try : + mutes = mute_mixer.getmute() + except alsaaudio.ALSAAudioError as e: + logger.warning("No mute switch found", exc_info = e) + if mutes != None: + for ch_mute in mutes: + if ch_mute: + mute = True + + + # TODO better way than averaging + volumes = vol_mixer.getvolume() + finally : + vol_mixer.close() + if mute_mixer: + mute_mixer.close() + + total = 0 + for vol in volumes: + total += vol + volume = total / len(volumes) + + self._volume = volume + + if self._light_controls is not None: + if self._volume > 90: + self._light_controls.set_value(g15driver.MKEY_LIGHT_MR | g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif self._volume > 75: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2 | g15driver.MKEY_LIGHT_3) + elif self._volume > 50: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1 | g15driver.MKEY_LIGHT_2) + elif self._volume > 25: + self._light_controls.set_value(g15driver.MKEY_LIGHT_1) + else: + self._light_controls.set_value(0) + + self._mute = mute + + self._screen.redraw(page) + +class VolumeThread(Thread): + def __init__(self, volume): + Thread.__init__(self) + self.name = "VolumeThread" + self.setDaemon(True) + self._volume = volume + + logger.info("Opening soundcard %s mixer %s", volume.soundcard_name, volume.mixer_name) + + self._mixer = alsaaudio.Mixer(volume.mixer_name, cardindex=volume.soundcard_index) + self._poll_desc = self._mixer.polldescriptors() + self._poll = select.poll() + self._fd = self._poll_desc[0][0] + self._event_mask = self._poll_desc[0][1] + self._open = os.fdopen(self._fd) + self._poll.register(self._open, select.POLLIN) + self._stop = False + + def _stop_monitoring(self): + self._stop = True + self._open.close() + self._mixer.close() + + def run(self): + try : + while not self._stop: + if self._poll.poll(5): + if self._stop: + break + g15scheduler.schedule("popupVolume", 0, self._volume._popup) + if not self._open.read(): + break + finally: + try : + self._poll.unregister(self._open) + except Exception as e: + logger.debug("Error when unregistering", exc_info = e) + pass + self._open.close() diff --git a/src/plugins/volume/volume.ui b/src/plugins/volume/volume.ui new file mode 100644 index 0000000..58e9b76 --- /dev/null +++ b/src/plugins/volume/volume.ui @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + 320 + False + 5 + Volume Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + SoundcardModel + + + + 0 + + + + + True + True + 0 + + + + + + + + + True + False + <b>Soundcard</b> + True + + + + + True + False + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + MixerModel + + + + 0 + + + + + True + True + 0 + + + + + + + + + True + False + <b>Mixer</b> + True + + + + + True + True + 1 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/weather-noaa/Makefile.am b/src/plugins/weather-noaa/Makefile.am new file mode 100644 index 0000000..a573f5c --- /dev/null +++ b/src/plugins/weather-noaa/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/weather-noaa +plugin_DATA = weather-noaa.py \ + weather-noaa.ui \ + icon.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather-noaa/icon.png b/src/plugins/weather-noaa/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..16a665ac12ad3cb82831952eafe4238970736d1c GIT binary patch literal 1784 zcmVD+r9>D>_X;f1H000McNliru+zJ;D87z#_gS7wv28BsP zK~z}7t(SXD)#n+XenlmYGtR?wA_)ah(enF~;rB~m4= ze^^(8)L~Ks$j)e$c5^E@qclulP%A<08U@=1P|ILRZN1=y%D@Yr%dYUw5z*4p_C^?E;3=FJOT*{n!=(*#+ds0+}6Uv2DWWW0-(OW zp5)}@;X7AVohL1AKC4%kzH#{QxdPC5JZ2{@Zsv;9r{Ap0$*E-3s$j3=4xx zT=<750F%iCfZp|YbMrSL2?+)N@=Uv(+S)BV`|J+#@>UQL5kh+UeyXc~k4~p?>Qo1@ zu|^UTXAu$cB*n!ic;SWF)YV-a#+bjquf)gC`s&;{d_@GM)S$No3#J$M^_5azp9@$= zL9kkF^z__Ar_F)-FH7hDaGfXcafM_ zIIMD(EsG{6Ckb(sl=R$Fe+SXrNhl(30XrSfj%sgjzrx(PJKRaOWlIufa}$df|CILj zYwX$c6332x$)!uz2n`Ly&(E8S7q78oiILAf`!9MuT)unua71)xWSAf?=ZP)bo&)Q3_0d19Yfc_jzj1 zrxK{5i)6)xUTUzx&!2{~8*oo{nNIgR?Dok?k5US|{k>torB%Skor7}mvnN=o^;wze z@B+=(!M6aKuM8nt#97xeDdj6IUFVKm1j<_A)!Be=W-FryqSP@lz4Lgu*&Hm@fvEYK-dHo!ty zSRgGeJ29D}`Dcehy$yf*(h9b6*@34TMFZ^~66s;WH zBi$VUn}Ed*-}b`%10Wvu4e{n$PoA2l2MF?}xX((?ILVEl7X(d#EsJ1l9PSdkbq{to z!M-!jh)3#_hrOQgVpk8Ko-Y@47K}~Z&RlrJf~^Z7?-vl@<#2ju01mW3{uvmsGx6q0 z5KU0KWAS@qnY!B|JhhM9t)H2sXfrIk<+tXl`s@{q=5}e#=Toc%Kc<}R0+_* z?KiC@gME_IS~jU0kp|XGN^vLW96(L=CQ2zUjvP5MoWx%r7AjXOqD#bl;d?SsKe!iv zBmkp7DP6g$c6g`g5}ehFqz{D)n^pGgAl`dyOgNk5=wLQ<<*M3W9g?xrDCq;CVZ9=} zP+q!zpQC6K>^<<@wR-_EPqMV-HPyKGAHqj3D?BpbvWE4F*+s(jZR%LUG{UF)Q0Hbt zCVbW)FvFkPb+ZU;c}+EL|75iD2^Z4yW{GU?WxdtT>-`pdoU3F=o(ST2_R~Rt7v`V% zQF3;ZYJV(W$JxgPbE;!%+FvB{^moib$4sRoL?HN~l`G-C+z!z3@AEn8JU~X3P@Xpa a_WuWFQgiR-cEjla0000 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather-noaa", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import weather +import gtk +import os +import pywapi +import email.utils +import time +import datetime + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +backend_name="NOAA" +id="weather-noaa" +name=_("Weather (NOAA support)") +description=_("Adds the National Oceanic and Atmospheric Administration as a source of weather data.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="weather" +unsupported_models=weather.unsupported_models + +""" +Weather Back-end module functions +""" +def create_options(gconf_client, gconf_key): + return NOAAWeatherOptions(gconf_client, gconf_key) + +def create_backend(gconf_client, gconf_key): + return NOAAWeatherBackend(gconf_client, gconf_key) + +class NOAAWeatherOptions(weather.WeatherOptions): + def __init__(self, gconf_client, gconf_key): + weather.WeatherOptions.__init__(self) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather-noaa.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + g15uigconf.configure_text_from_gconf(gconf_client, "%s/station_id" % gconf_key, "StationID", "KPEO", self.widget_tree) + +class NOAAWeatherData(weather.WeatherData): + + def __init__(self, station_id): + weather.WeatherData.__init__(self, station_id) + +class NOAAWeatherBackend(weather.WeatherBackend): + + def __init__(self, gconf_client, gconf_key): + weather.WeatherBackend.__init__(self, gconf_client, gconf_key) + + def get_weather_data(self): + station_id = g15gconf.get_string_or_default(self.gconf_client, "%s/station_id" % self.gconf_key, "KPEO") + p = pywapi.get_weather_from_noaa(station_id) + + tm = email.utils.parsedate_tz(p["observation_time_rfc822"])[:9] + data = { + "location" : p["location"], + "datetime" : datetime.datetime.fromtimestamp(time.mktime(tm)), + "current_conditions" : { + "wind_speed" : g15pythonlang.to_int_or_none(weather.mph_to_kph(float(p["wind_mph"]))) if "wind_mph" in p else None, + "wind_direction" : g15pythonlang.to_int_or_none(p["wind_degrees"]) if "wind_degrees" in p else None, + "pressure" : p["pressure_mb"] if "pressure_mb" in p else None, + "humidity" : p["relative_humidity"] if "relative_humidity" in p else None, + "condition" : p["weather"] if "weather" in p else None, + "temp_c" : p["temp_c"] if "temp_c" in p else None, + "icon" : self._get_icon(p["icon_url_name"]) if "icon_url_name" in p else None, + "fallback_icon" : "http://w1.weather.gov/images/fcicons/%s" % ( "%s.jpg" % os.path.splitext(p["icon_url_name"])[0] ) if "icon_url_name" in p else None + } + } + + return data + + def _get_icon(self, icon): + night = False + icon_name = icon + if icon.startswith("n"): + icon_name = icon_name[1:] + night = True + elif icon.startswith("hi_n"): + icon_name = "hi_" + icon_name[3:] + night = True + + name, extension = os.path.splitext(icon_name) + theme_icon = None + if name in [ "bkn" ]: + # Mostly Cloudy | Mostly Cloudy with Haze | Mostly Cloudy and Breezy + theme_icon = "weather-overcast" + elif name in [ "skc" ]: + # Fair | Clear | Fair with Haze | Clear with Haze | Fair and Breezy | Clear and Breezy + theme_icon = "weather-clear" + elif name in [ "few" ]: + # A Few Clouds | A Few Clouds with Haze | A Few Clouds and Breezy + theme_icon = "weather-few-clouds" + elif name in [ "sct" ]: + # Partly Cloudy | Partly Cloudy with Haze | Partly Cloudy and Breezy + theme_icon = "weather-clouds" + elif name in [ "ovc" ]: + # Overcast | Overcast with Haze | Overcast and Breezy + theme_icon = "weather-overcast" + elif name in [ "fg" ]: + # Fog/Mist | Fog | Freezing Fog | Shallow Fog | Partial Fog | Patches of Fog | Fog in Vicinity | Freezing Fog in Vicinity | Shallow Fog in Vicinity | Partial Fog in Vicinity | Patches of Fog in Vicinity | Showers in Vicinity Fog | Light Freezing Fog | Heavy Freezing Fog + theme_icon = "weather-fog" + elif name in [ "shra", "hi_shwrs", "ra1", "ra" ]: + # Rain Showers | Light Rain Showers | Light Rain and Breezy | Heavy Rain Showers | Rain Showers in Vicinity | Light Showers Rain | Heavy Showers Rain | Showers Rain | Showers Rain in Vicinity | Rain Showers Fog/Mist | Light Rain Showers Fog/Mist | Heavy Rain Showers Fog/Mist | Rain Showers in Vicinity Fog/Mist | Light Showers Rain Fog/Mist | Heavy Showers Rain Fog/Mist | Showers Rain Fog/Mist | Showers Rain in Vicinity Fog/Mist + theme_icon = "weather-showers" + elif name in [ "tsra" ]: + # Rain Showers | Light Rain Showers | Light Rain and Breezy | Heavy Rain Showers | Rain Showers in Vicinity | Light Showers Rain | Heavy Showers Rain | Showers Rain | Showers Rain in Vicinity | Rain Showers Fog/Mist | Light Rain Showers Fog/Mist | Heavy Rain Showers Fog/Mist | Rain Showers in Vicinity Fog/Mist | Light Showers Rain Fog/Mist | Heavy Showers Rain Fog/Mist | Showers Rain Fog/Mist | Showers Rain in Vicinity Fog/Mist + theme_icon = "weather-storm" + elif name in [ "sn" ]: + # Snow | Light Snow | Heavy Snow | Snow Showers | Light Snow Showers | Heavy Snow Showers | Showers Snow | Light Showers Snow | Heavy Showers Snow | Snow Fog/Mist | Light Snow Fog/Mist | Heavy Snow Fog/Mist | Snow Showers Fog/Mist | Light Snow Showers Fog/Mist | Heavy Snow Showers Fog/Mist | Showers Snow Fog/Mist | Light Showers Snow Fog/Mist | Heavy Showers Snow Fog/Mist | Snow Fog | Light Snow Fog | Heavy Snow Fog | Snow Showers Fog | Light Snow Showers Fog | Heavy Snow Showers Fog | Showers Snow Fog | Light Showers Snow Fog | Heavy Showers Snow Fog | Showers in Vicinity Snow | Snow Showers in Vicinity | Snow Showers in Vicinity Fog/Mist | Snow Showers in Vicinity Fog | Low Drifting Snow | Blowing Snow | Snow Low Drifting Snow | Snow Blowing Snow | Light Snow Low Drifting Snow | Light Snow Blowing Snow | Light Snow Blowing Snow Fog/Mist | Heavy Snow Low Drifting Snow | Heavy Snow Blowing Snow | Thunderstorm Snow | Light Thunderstorm Snow | Heavy Thunderstorm Snow | Snow Grains | Light Snow Grains | Heavy Snow Grains | Heavy Blowing Snow | Blowing Snow in Vicinity + theme_icon = "weather-snow" + elif name in [ "svrtsra" ]: + # Funnel Cloud | Funnel Cloud in Vicinity | Tornado/Water Spout + theme_icon = "weather-severe-alert" + + if theme_icon is not None and night: + theme_icon = "%s-night" % theme_icon + + if theme_icon is None: + # Fallback to using actual image + theme_icon = "http://w1.weather.gov/images/fcicons/%s.jpg" % name + + return theme_icon + diff --git a/src/plugins/weather-noaa/weather-noaa.ui b/src/plugins/weather-noaa/weather-noaa.ui new file mode 100644 index 0000000..0f35fdc --- /dev/null +++ b/src/plugins/weather-noaa/weather-noaa.ui @@ -0,0 +1,96 @@ + + + + + + False + + + True + False + + + True + False + 3 + 2 + 8 + 8 + + + True + False + 0 + Station ID: + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + To use the NOAA feeds, you must know your station ID. +You can find your station ID by visiting the NOAA site using +the button below. The ID is usually at least 4 character code. + + + 2 + + + + + Visit NOAA for Stationd ID + True + True + True + True + none + http://w1.weather.gov/xml/current_obs/ + + + 2 + 2 + 3 + GTK_EXPAND + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/weather-yahoo/Makefile.am b/src/plugins/weather-yahoo/Makefile.am new file mode 100644 index 0000000..258b57e --- /dev/null +++ b/src/plugins/weather-yahoo/Makefile.am @@ -0,0 +1,7 @@ +plugindir = $(datadir)/gnome15/plugins/weather-yahoo +plugin_DATA = weather-yahoo.py \ + weather-yahoo.ui \ + icon.png + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather-yahoo/icon.png b/src/plugins/weather-yahoo/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..16a665ac12ad3cb82831952eafe4238970736d1c GIT binary patch literal 1784 zcmVD+r9>D>_X;f1H000McNliru+zJ;D87z#_gS7wv28BsP zK~z}7t(SXD)#n+XenlmYGtR?wA_)ah(enF~;rB~m4= ze^^(8)L~Ks$j)e$c5^E@qclulP%A<08U@=1P|ILRZN1=y%D@Yr%dYUw5z*4p_C^?E;3=FJOT*{n!=(*#+ds0+}6Uv2DWWW0-(OW zp5)}@;X7AVohL1AKC4%kzH#{QxdPC5JZ2{@Zsv;9r{Ap0$*E-3s$j3=4xx zT=<750F%iCfZp|YbMrSL2?+)N@=Uv(+S)BV`|J+#@>UQL5kh+UeyXc~k4~p?>Qo1@ zu|^UTXAu$cB*n!ic;SWF)YV-a#+bjquf)gC`s&;{d_@GM)S$No3#J$M^_5azp9@$= zL9kkF^z__Ar_F)-FH7hDaGfXcafM_ zIIMD(EsG{6Ckb(sl=R$Fe+SXrNhl(30XrSfj%sgjzrx(PJKRaOWlIufa}$df|CILj zYwX$c6332x$)!uz2n`Ly&(E8S7q78oiILAf`!9MuT)unua71)xWSAf?=ZP)bo&)Q3_0d19Yfc_jzj1 zrxK{5i)6)xUTUzx&!2{~8*oo{nNIgR?Dok?k5US|{k>torB%Skor7}mvnN=o^;wze z@B+=(!M6aKuM8nt#97xeDdj6IUFVKm1j<_A)!Be=W-FryqSP@lz4Lgu*&Hm@fvEYK-dHo!ty zSRgGeJ29D}`Dcehy$yf*(h9b6*@34TMFZ^~66s;WH zBi$VUn}Ed*-}b`%10Wvu4e{n$PoA2l2MF?}xX((?ILVEl7X(d#EsJ1l9PSdkbq{to z!M-!jh)3#_hrOQgVpk8Ko-Y@47K}~Z&RlrJf~^Z7?-vl@<#2ju01mW3{uvmsGx6q0 z5KU0KWAS@qnY!B|JhhM9t)H2sXfrIk<+tXl`s@{q=5}e#=Toc%Kc<}R0+_* z?KiC@gME_IS~jU0kp|XGN^vLW96(L=CQ2zUjvP5MoWx%r7AjXOqD#bl;d?SsKe!iv zBmkp7DP6g$c6g`g5}ehFqz{D)n^pGgAl`dyOgNk5=wLQ<<*M3W9g?xrDCq;CVZ9=} zP+q!zpQC6K>^<<@wR-_EPqMV-HPyKGAHqj3D?BpbvWE4F*+s(jZR%LUG{UF)Q0Hbt zCVbW)FvFkPb+ZU;c}+EL|75iD2^Z4yW{GU?WxdtT>-`pdoU3F=o(ST2_R~Rt7v`V% zQF3;ZYJV(W$JxgPbE;!%+FvB{^moib$4sRoL?HN~l`G-C+z!z3@AEn8JU~X3P@Xpa a_WuWFQgiR-cEjla0000 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Based on bits of pywapi :- +# +#Copyright (c) 2009 Eugene Kaznacheev + +#Permission is hereby granted, free of charge, to any person +#obtaining a copy of this software and associated documentation +#files (the "Software"), to deal in the Software without +#restriction, including without limitation the rights to use, +#copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the +#Software is furnished to do so, subject to the following +#conditions: + +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +#OTHER DEALINGS IN THE SOFTWARE. + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather-yahoo", modfile = __file__).ugettext + +import gnome15.g15accounts as g15accounts +import gnome15.g15globals as g15globals +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import weather +import gtk +import os +import datetime +import urllib2, re +import json +from xml.dom import minidom +from urllib import quote +import time + +#select * from xml where url="http://weather.yahooapis.com/forecastrss?w=26350898" + +YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?w=%s&u=%s&d=5' +YAHOO_WEATHER_URL_JSON = 'http://query.yahooapis.com/v1/public/yql?q=select%20item%20from%20weather.forecast%20where%20location%3D%2248907%22&format=json' +YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' + +# Logging +import logging +logger = logging.getLogger(__name__) + +""" +Plugin definition +""" +backend_name="Yahoo" +id="weather-yahoo" +name=_("Weather (Yahoo support)") +description=_("Adds Yahoo as a source of weather data.") +author="Brett Smith " +copyright=_("Copyright (C)2012 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=False +passive=True +needs_network=True +global_plugin=True +requires="weather" +unsupported_models=weather.unsupported_models + +""" +Weather Back-end module functions +""" +def create_options(gconf_client, gconf_key): + return YahooWeatherOptions(gconf_client, gconf_key) + +def create_backend(gconf_client, gconf_key): + return YahooWeatherBackend(gconf_client, gconf_key) + +""" +Utilities for parsing +""" + +def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): + """ + Parses the necessary tag and returns the dictionary with values + + Parameters: + dom - DOM + ns - namespace + tag - necessary tag + attrs - tuple of attributes + + Returns: a dictionary of elements + """ + element = dom.getElementsByTagNameNS(ns, tag)[0] + return xml_get_attrs(element,attrs) + + +def xml_get_attrs(xml_element, attrs): + """ + Returns the list of necessary attributes + + Parameters: + element: xml element + attrs: tuple of attributes + + Return: a dictionary of elements + """ + + result = {} + for attr in attrs: + result[attr] = xml_element.getAttribute(attr) + return result + + +class YahooWeatherOptions(weather.WeatherOptions): + def __init__(self, gconf_client, gconf_key): + weather.WeatherOptions.__init__(self) + + self.widget_tree = gtk.Builder() + self.widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather-yahoo.ui")) + self.component = self.widget_tree.get_object("OptionPanel") + + g15uigconf.configure_text_from_gconf(gconf_client, "%s/location_id" % gconf_key, "LocationID", "", self.widget_tree) + +class YahooWeatherData(weather.WeatherData): + + def __init__(self, station_id): + weather.WeatherData.__init__(self, station_id) + +class YahooWeatherBackend(weather.WeatherBackend): + + def __init__(self, gconf_client, gconf_key): + weather.WeatherBackend.__init__(self, gconf_client, gconf_key) + + def get_weather_data(self): + return self._do_get_weather_data_xml() + + def _do_get_weather_data_json(self): + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL_JSON % (location_id, unit) + handler = urllib2.urlopen(url) + jobj = json.load(handler) + handler.close() + + def _do_get_weather_data_xml(self): + location_id = g15gconf.get_string_or_default(self.gconf_client, "%s/location_id" % self.gconf_key, "2487956") + p = self._get_weather_from_yahoo(location_id) + if p is None: + return None + + # Get location + location_el = p["location"] + location = g15pythonlang.append_if_exists(location_el, "city", "") + location = g15pythonlang.append_if_exists(location_el, "region", location) + location = g15pythonlang.append_if_exists(location_el, "country", location) + + # Get current condition + condition_el = p["condition"] + wind_el = p["wind"] if "wind" in p else None + + # Observed date + try: + observed_datetime = datetime.datetime.strptime(condition_el["date"], "%a, %d %b %Y %H:%M %p %Z") + except ValueError as v: + logger.debug("Error parsing date, trying alternative method.", exc_info = v) + import email.utils + dxt = email.utils.parsedate_tz(condition_el["date"]) + class TZ(datetime.tzinfo): + def dst(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return dxt[9] + + def utcoffset(self, dt): return datetime.timedelta(seconds=dxt[9]) + observed_datetime = datetime.datetime(*dxt[:7], tzinfo=TZ()) + + # Forecasts (we only get 2 from yahoo) + forecasts_el = p["forecasts"] + forecasts = [] + today_low = None + today_high = None + for f in forecasts_el: + condition_code = g15pythonlang.to_int_or_none(f["code"]) + high = g15pythonlang.to_float_or_none(f["high"]) + low = g15pythonlang.to_float_or_none(f["low"]) + if today_low is None: + today_low = low + today_high = high + forecasts.append({ + "condition" : f["text"], + "high" : high, + "low" : low, + "day_of_week" : f["day"], + "icon" : self._translate_icon(condition_code), + "fallback_icon" : "http://l.yimg.com/a/i/us/we/52/%s.gif" % condition_code + }) + + # Sunset and sunrise + sunset = None + sunrise = None + if "astronomy" in p: + astronomy = p["astronomy"] + if "sunset" in astronomy: + sunset = g15locale.parse_US_time_or_none(astronomy["sunset"]) + if "sunrise" in astronomy: + sunrise = g15locale.parse_US_time_or_none(astronomy["sunrise"]) + + # Pressure, Visibility and Humidity + pressure = None + if "atmosphere" in p: + atmosphere = p["atmosphere"] + if "pressure" in atmosphere: + pressure = g15pythonlang.to_float_or_none(atmosphere["pressure"]) + if "visibility" in atmosphere: + visibility = g15pythonlang.to_float_or_none(atmosphere["visibility"]) + if "humidity" in atmosphere: + humidity = g15pythonlang.to_float_or_none(atmosphere["humidity"]) + + # Build data structure + condition_code = g15pythonlang.to_int_or_none(condition_el["code"]) + data = { + "location" : location, + "forecasts" : forecasts, + "datetime": observed_datetime, + "current_conditions" : { + "wind_chill": wind_el["chill"] if wind_el is not None and "chill" in wind_el else None, + "wind_direction": wind_el["direction"] if wind_el is not None and "direction" in wind_el else None, + "wind_speed": wind_el["speed"] if wind_el is not None and "speed" in wind_el else None, + "condition" : condition_el["text"], + "sunset" : sunset, + "sunrise" : sunrise, + "pressure" : pressure, + "visibility" : visibility, + "humidity" : humidity, + "low" : today_low, + "high" : today_high, + "temp_c" : g15pythonlang.to_float_or_none(condition_el["temp"]), + "icon" : self._translate_icon(condition_code), + "fallback_icon" : "http://l.yimg.com/a/i/us/we/52/%s.gif" % condition_code if condition_code is not None else None + } + } + + return data + + def _translate_icon(self, code): + + theme_icon = None + if code in [ 0, 1, 2, 3, 4 ]: + theme_icon = "weather-severe-alert" + elif code in [ 8, 9, 10, 11, 12, 35 ]: + theme_icon = "weather-showers" + elif code in [ 5, 6, 7, 13, 14, 15, 16, 41, 42, 43, 46 ]: + theme_icon = "weather-snow" + elif code in [ 20 ]: + theme_icon = "weather-fog" + elif code in [ 37, 38, 39, 45, 47 ]: + theme_icon = "weather-storm" + elif code in [ 40 ]: + theme_icon = "weather-showers-scattered" + elif code in [ 31 ]: + theme_icon = "weather-clear-night" + elif code in [ 30,44 ]: + theme_icon = "weather-few-clouds" + elif code in [ 29 ]: + theme_icon = "weather-few-clouds-night" + elif code in [ 28, 26 ]: + theme_icon = "weather-overcast" + elif code in [ 27 ]: + theme_icon = "weather-overcast-night" + elif code in [ 34, 21 ]: + theme_icon = "weather-clouds" + elif code in [ 33 ]: + theme_icon = "weather-clouds-night" + elif code in [ 32, 36 ]: + theme_icon = "weather-clear" + + + if theme_icon is None: + # Fallback to using image extracted from data + theme_icon = "http://l.yimg.com/a/i/us/we/52/%s.gif" % code + + """ + The following will always use yahoo images + + + + + + + + + + """ + + return theme_icon + + + def _get_weather_from_yahoo(self, location_id, units = 'metric'): + """ + Fetches weather report from Yahoo! + + Parameters + location_id: A five digit US zip code or location ID. To find your location ID, + browse or search for your city from the Weather home page(http://weather.yahoo.com/) + The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. + + units: type of units. 'metric' for metric and '' for non-metric + Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel + """ + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL % (location_id, unit) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + weather_data = {} + weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data + linkel = dom.getElementsByTagName('link') + + if len(linkel) < 1: + return None + + weather_data['link'] = linkel[0].firstChild.data + + ns_data_structure = { + 'location': ('city', 'region', 'country'), + 'units': ('temperature', 'distance', 'pressure', 'speed'), + 'wind': ('chill', 'direction', 'speed'), + 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), + 'astronomy': ('sunrise', 'sunset'), + 'condition': ('text', 'code', 'temp', 'date', 'day') + } + + for (tag, attrs) in ns_data_structure.iteritems(): + weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) + + weather_data['geo'] = {} + weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data + weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data + + weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data + weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data + + forecasts = [] + for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): + forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code', 'day'))) + weather_data['forecasts'] = forecasts + + dom.unlink() + + return weather_data + + diff --git a/src/plugins/weather-yahoo/weather-yahoo.ui b/src/plugins/weather-yahoo/weather-yahoo.ui new file mode 100644 index 0000000..d612ae4 --- /dev/null +++ b/src/plugins/weather-yahoo/weather-yahoo.ui @@ -0,0 +1,105 @@ + + + + + + False + + + True + False + + + True + False + 3 + 2 + 8 + 8 + + + True + False + 0 + Location ID: + + + 1 + 2 + GTK_FILL + + + + + + True + True + + False + False + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + A five digit US zip code or location ID. To find your location ID, +browse or search for your city from the Weather home page. +The weather ID is in the URL for the forecast page for that city. +You can also get the location ID by entering your zip code on +the home page. + +For example, if you search for San Francisco on the Weather +home page, the forecast page for that city is :- + +../united-states/california/san-francisco-2487956, +this means the location ID is 2487956. + + + 2 + GTK_FILL + + + + + Visit Yahoo Weather For Location ID + True + True + True + True + none + http://weather.yahoo.com/ + + + 2 + 2 + 3 + + + + + + False + False + 8 + 0 + + + + + + + + + diff --git a/src/plugins/weather/Makefile.am b/src/plugins/weather/Makefile.am new file mode 100644 index 0000000..28dbcda --- /dev/null +++ b/src/plugins/weather/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = default forecasts +plugindir = $(datadir)/gnome15/plugins/weather +plugin_DATA = weather.py \ + pywapi.py \ + weather.ui + +EXTRA_DIST = \ + $(plugin_DATA) \ No newline at end of file diff --git a/src/plugins/weather/default/Makefile.am b/src/plugins/weather/default/Makefile.am new file mode 100644 index 0000000..c26d99d --- /dev/null +++ b/src/plugins/weather/default/Makefile.am @@ -0,0 +1,16 @@ +themedir = $(datadir)/gnome15/plugins/weather/default +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + mono-clouds.gif \ + mono-dark-clouds.gif \ + mono-few-clouds.gif \ + mono-fog.gif \ + mono-more-clouds.gif \ + mono-rain.gif \ + mono-snow.gif \ + mono-sunny.gif \ + mono-thunder.gif + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/weather/default/default.svg b/src/plugins/weather/default/default.svg new file mode 100644 index 0000000..50d4587 --- /dev/null +++ b/src/plugins/weather/default/default.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + ${temp} + ${condition} + Hum:${humidity} + ${pressure} + Wnd:${wind} + Vis:${visibility} + S${sunset_time} + R${sunrise_time} + + + + ${message} + + diff --git a/src/plugins/weather/default/g19.svg b/src/plugins/weather/default/g19.svg new file mode 100644 index 0000000..6968ef2 --- /dev/null +++ b/src/plugins/weather/default/g19.svg @@ -0,0 +1,331 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + ${condition} + ${temp} + ${lo} Low + ${hi} High + Wind: + ${humidity} + ${pressure} + ${sunrise_time} + ${location} @ ${time} + ${wind} + Humidity: + Pressure: + Sunrise at + ${sunset_time} + Sunset at + ${visibility} + Visibility: + + + ${message} + + diff --git a/src/plugins/weather/default/mono-clouds.gif b/src/plugins/weather/default/mono-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..6dce86a0621a784c445b2549d8605e35debb95fd GIT binary patch literal 78 zcmZ?wbhEHb`lwlo&;YFfdpH0G^u=fB*mh literal 0 HcmV?d00001 diff --git a/src/plugins/weather/default/mono-dark-clouds.gif b/src/plugins/weather/default/mono-dark-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..f657814837936c265093fd58b49b6d4bfeb671ed GIT binary patch literal 78 zcmZ?wbhEHb`ZHR_0HD$k@c;k- literal 0 HcmV?d00001 diff --git a/src/plugins/weather/default/mono-few-clouds.gif b/src/plugins/weather/default/mono-few-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..d2e59970096506d6950985a5ddf672f206e23276 GIT binary patch literal 78 zcmZ?wbhEHb0H?VTU;qFB literal 0 HcmV?d00001 diff --git a/src/plugins/weather/default/mono-snow.gif b/src/plugins/weather/default/mono-snow.gif new file mode 100644 index 0000000000000000000000000000000000000000..feaaed0e9c765f63950a6c167ed2be4ab8d774b7 GIT binary patch literal 74 zcmZ?wbhEHbWMyDuXkcUj0xl(^b literal 0 HcmV?d00001 diff --git a/src/plugins/weather/default/mono-thunder.gif b/src/plugins/weather/default/mono-thunder.gif new file mode 100644 index 0000000000000000000000000000000000000000..a452327519b0f095caa888ac0568812d812db0cf GIT binary patch literal 74 zcmZ?wbhEHbWMN=qXkcUj0xl + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${day_letter2} + ${day_letter3} + ${day_letter4} + + + ${condition1} + ${condition2} + ${condition3} + ${condition4} + + + ${hi1} + ${hi2} + ${hi3} + ${hi4} + + ${temp} + ${condition} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/Makefile.am b/src/plugins/weather/forecasts/Makefile.am new file mode 100644 index 0000000..a1e30fa --- /dev/null +++ b/src/plugins/weather/forecasts/Makefile.am @@ -0,0 +1,17 @@ +themedir = $(datadir)/gnome15/plugins/weather/forecasts +theme_DATA = g19.svg \ + default.svg \ + mx5500.svg \ + mono-clouds.gif \ + mono-dark-clouds.gif \ + mono-few-clouds.gif \ + mono-fog.gif \ + mono-more-clouds.gif \ + mono-rain.gif \ + mono-snow.gif \ + mono-sunny.gif \ + mono-thunder.gif \ + forecasts.theme + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/weather/forecasts/default.svg b/src/plugins/weather/forecasts/default.svg new file mode 100644 index 0000000..144a0c3 --- /dev/null +++ b/src/plugins/weather/forecasts/default.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${condition1} + ${hi1} + ${day_letter2} + ${condition2} + ${hi2} + ${temp} + ${condition} + ${wind} + ${humidity} ${pressure} ${visibility} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/forecasts.theme b/src/plugins/weather/forecasts/forecasts.theme new file mode 100644 index 0000000..39869c5 --- /dev/null +++ b/src/plugins/weather/forecasts/forecasts.theme @@ -0,0 +1,4 @@ +[theme] +name=Forecasts +description=Displays a two day forecast when available +unsupported_models=g110,g11,mx5500,g930,g35 diff --git a/src/plugins/weather/forecasts/g19.svg b/src/plugins/weather/forecasts/g19.svg new file mode 100644 index 0000000..fd308ce --- /dev/null +++ b/src/plugins/weather/forecasts/g19.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + ${condition} + ${temp} + ${lo} Low + ${hi} High + ${sunrise_time} + ${location} @ ${time} + Sunrise at + ${sunset_time} + Sunset at + + ${day1} + ${lo1} - ${hi1} + ${condition1} + + ${day2} + ${lo2} - ${hi2} + ${condition2} + + ${wind} + ${humidity} + ${pressure} + ${visibility} + + + ${message} + + diff --git a/src/plugins/weather/forecasts/mono-clouds.gif b/src/plugins/weather/forecasts/mono-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..6dce86a0621a784c445b2549d8605e35debb95fd GIT binary patch literal 78 zcmZ?wbhEHb`lwlo&;YFfdpH0G^u=fB*mh literal 0 HcmV?d00001 diff --git a/src/plugins/weather/forecasts/mono-dark-clouds.gif b/src/plugins/weather/forecasts/mono-dark-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..f657814837936c265093fd58b49b6d4bfeb671ed GIT binary patch literal 78 zcmZ?wbhEHb`ZHR_0HD$k@c;k- literal 0 HcmV?d00001 diff --git a/src/plugins/weather/forecasts/mono-few-clouds.gif b/src/plugins/weather/forecasts/mono-few-clouds.gif new file mode 100644 index 0000000000000000000000000000000000000000..d2e59970096506d6950985a5ddf672f206e23276 GIT binary patch literal 78 zcmZ?wbhEHb0H?VTU;qFB literal 0 HcmV?d00001 diff --git a/src/plugins/weather/forecasts/mono-snow.gif b/src/plugins/weather/forecasts/mono-snow.gif new file mode 100644 index 0000000000000000000000000000000000000000..feaaed0e9c765f63950a6c167ed2be4ab8d774b7 GIT binary patch literal 74 zcmZ?wbhEHbWMyDuXkcUj0xl(^b literal 0 HcmV?d00001 diff --git a/src/plugins/weather/forecasts/mono-thunder.gif b/src/plugins/weather/forecasts/mono-thunder.gif new file mode 100644 index 0000000000000000000000000000000000000000..a452327519b0f095caa888ac0568812d812db0cf GIT binary patch literal 74 zcmZ?wbhEHbWMN=qXkcUj0xl + + + + + + + + + + + image/svg+xml + + + + + + + + + ${day_letter1} + ${day_letter2} + ${day_letter3} + ${day_letter4} + + + ${condition1} + ${condition2} + ${condition3} + ${condition4} + + + ${hi1} + ${hi2} + ${hi3} + ${hi4} + + ${temp} + ${condition} + + + ${message} + + diff --git a/src/plugins/weather/i18n/weather.en_GB.po b/src/plugins/weather/i18n/weather.en_GB.po new file mode 100644 index 0000000..0bd5a8f --- /dev/null +++ b/src/plugins/weather/i18n/weather.en_GB.po @@ -0,0 +1,74 @@ +# English translations for gnome15-plugins package. +# Copyright (C) 2012 THE gnome15-plugins'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome15-plugins package. +# Automatically generated, 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome15-plugins 0.8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: 2012-01-08 11:23+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: i18n/weather.glade.h:1 +msgid "Display" +msgstr "Display" + +#: i18n/weather.glade.h:2 +msgid "Location" +msgstr "Location" + +#: i18n/weather.glade.h:3 +msgid "Update" +msgstr "Update" + +#: i18n/weather.glade.h:4 +msgid "Automatically update every" +msgstr "Automatically update every" + +#: i18n/weather.glade.h:5 +msgid "Celsius" +msgstr "Celsius" + +#: i18n/weather.glade.h:6 +msgid "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." +msgstr "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." + +#: i18n/weather.glade.h:10 +msgid "Faranheit" +msgstr "Faranheit" + +#: i18n/weather.glade.h:11 +msgid "Kelvin" +msgstr "Kelvin" + +#: i18n/weather.glade.h:12 +msgid "Location:" +msgstr "Location:" + +#: i18n/weather.glade.h:13 +msgid "Tempature Unit:" +msgstr "Tempature Unit:" + +#: i18n/weather.glade.h:14 +msgid "Weather Preferences" +msgstr "Weather Preferences" + +#: i18n/weather.glade.h:15 +msgid "minutes" +msgstr "minutes" diff --git a/src/plugins/weather/i18n/weather.glade.h b/src/plugins/weather/i18n/weather.glade.h new file mode 100644 index 0000000..3830a73 --- /dev/null +++ b/src/plugins/weather/i18n/weather.glade.h @@ -0,0 +1,15 @@ +char *s = N_("Display"); +char *s = N_("Location"); +char *s = N_("Update"); +char *s = N_("Automatically update every"); +char *s = N_("Celsius"); +char *s = N_("Enter your location. This may be a town and\n" + "or country name. such as London, England ,\n" + "a zip or post code, or a longitude and latitude \n" + "such as 30670000,104019996."); +char *s = N_("Faranheit"); +char *s = N_("Kelvin"); +char *s = N_("Location:"); +char *s = N_("Tempature Unit:"); +char *s = N_("Weather Preferences"); +char *s = N_("minutes"); diff --git a/src/plugins/weather/i18n/weather.pot b/src/plugins/weather/i18n/weather.pot new file mode 100644 index 0000000..42fade3 --- /dev/null +++ b/src/plugins/weather/i18n/weather.pot @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-01-08 11:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: i18n/weather.glade.h:1 +msgid "Display" +msgstr "" + +#: i18n/weather.glade.h:2 +msgid "Location" +msgstr "" + +#: i18n/weather.glade.h:3 +msgid "Update" +msgstr "" + +#: i18n/weather.glade.h:4 +msgid "Automatically update every" +msgstr "" + +#: i18n/weather.glade.h:5 +msgid "Celsius" +msgstr "" + +#: i18n/weather.glade.h:6 +msgid "" +"Enter your location. This may be a town and\n" +"or country name. such as London, England ,\n" +"a zip or post code, or a longitude and latitude \n" +"such as 30670000,104019996." +msgstr "" + +#: i18n/weather.glade.h:10 +msgid "Faranheit" +msgstr "" + +#: i18n/weather.glade.h:11 +msgid "Kelvin" +msgstr "" + +#: i18n/weather.glade.h:12 +msgid "Location:" +msgstr "" + +#: i18n/weather.glade.h:13 +msgid "Tempature Unit:" +msgstr "" + +#: i18n/weather.glade.h:14 +msgid "Weather Preferences" +msgstr "" + +#: i18n/weather.glade.h:15 +msgid "minutes" +msgstr "" diff --git a/src/plugins/weather/pywapi.py b/src/plugins/weather/pywapi.py new file mode 100644 index 0000000..5c21100 --- /dev/null +++ b/src/plugins/weather/pywapi.py @@ -0,0 +1,334 @@ +#Copyright (c) 2009 Eugene Kaznacheev + +#Permission is hereby granted, free of charge, to any person +#obtaining a copy of this software and associated documentation +#files (the "Software"), to deal in the Software without +#restriction, including without limitation the rights to use, +#copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the +#Software is furnished to do so, subject to the following +#conditions: + +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +#OTHER DEALINGS IN THE SOFTWARE. + +""" +Fetches weather reports from Google Weather, Yahoo Wheather and NOAA +""" + +import urllib2, re +from xml.dom import minidom +from urllib import quote + +GOOGLE_WEATHER_URL = 'http://www.google.com/ig/api?weather=%s&hl=%s' +GOOGLE_COUNTRIES_URL = 'http://www.google.com/ig/countries?output=xml&hl=%s' +GOOGLE_CITIES_URL = 'http://www.google.com/ig/cities?output=xml&country=%s&hl=%s' + +YAHOO_WEATHER_URL = 'http://xml.weather.yahoo.com/forecastrss?w=%s&u=%s&d=5' +YAHOO_WEATHER_NS = 'http://xml.weather.yahoo.com/ns/rss/1.0' + +NOAA_WEATHER_URL = 'http://www.weather.gov/xml/current_obs/%s.xml' + +def get_weather_from_google(location_id, hl = ''): + """ + Fetches weather report from Google + + Parameters + location_id: a zip code (10001); city name, state (weather=woodland,PA); city name, country (weather=london, england); + latitude/longitude(weather=,,,30670000,104019996) or possibly other. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + """ + location_id, hl = map(quote, (location_id, hl)) + url = GOOGLE_WEATHER_URL % (location_id, hl) + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + weather_data = {} + weather_dom = dom.getElementsByTagName('weather')[0] + + data_structure = { + 'forecast_information': ('city', 'postal_code', 'latitude_e6', 'longitude_e6', 'forecast_date', 'current_date_time', 'unit_system'), + 'current_conditions': ('condition','temp_f', 'temp_c', 'humidity', 'wind_condition', 'icon') + } + for (tag, list_of_tags2) in data_structure.iteritems(): + tmp_conditions = {} + for tag2 in list_of_tags2: + try: + tmp_conditions[tag2] = weather_dom.getElementsByTagName(tag)[0].getElementsByTagName(tag2)[0].getAttribute('data') + except IndexError: + pass + weather_data[tag] = tmp_conditions + + forecast_conditions = ('day_of_week', 'low', 'high', 'icon', 'condition') + forecasts = [] + + for forecast in dom.getElementsByTagName('forecast_conditions'): + tmp_forecast = {} + for tag in forecast_conditions: + tmp_forecast[tag] = forecast.getElementsByTagName(tag)[0].getAttribute('data') + forecasts.append(tmp_forecast) + + weather_data['forecasts'] = forecasts + dom.unlink() + + return weather_data + +def get_countries_from_google(hl = ''): + """ + Get list of countries in specified language from Google + + Parameters + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + countries: a list of elements(all countries that exists in XML feed). Each element is a dictionary with 'name' and 'iso_code' keys. + For example: [{'iso_code': 'US', 'name': 'USA'}, {'iso_code': 'FR', 'name': 'France'}] + """ + url = GOOGLE_COUNTRIES_URL % hl + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + countries = [] + countries_dom = dom.getElementsByTagName('country') + + for country_dom in countries_dom: + country = {} + country['name'] = country_dom.getElementsByTagName('name')[0].getAttribute('data') + country['iso_code'] = country_dom.getElementsByTagName('iso_code')[0].getAttribute('data') + countries.append(country) + + dom.unlink() + return countries + +def get_cities_from_google(country_code, hl = ''): + """ + Get list of cities of necessary country in specified language from Google + + Parameters + country_code: code of the necessary country. For example 'de' or 'fr'. + hl: the language parameter (language code). Default value is empty string, in this case Google will use English. + Returns: + cities: a list of elements(all cities that exists in XML feed). Each element is a dictionary with 'name', 'latitude_e6' and 'longitude_e6' keys. For example: [{'longitude_e6': '1750000', 'name': 'Bourges', 'latitude_e6': '47979999'}] + """ + url = GOOGLE_CITIES_URL % (country_code.lower(), hl) + + handler = urllib2.urlopen(url) + content_type = handler.info().dict['content-type'] + charset = re.search('charset\=(.*)',content_type).group(1) + if not charset: + charset = 'utf-8' + if charset.lower() != 'utf-8': + xml_response = handler.read().decode(charset).encode('utf-8') + else: + xml_response = handler.read() + dom = minidom.parseString(xml_response) + handler.close() + + cities = [] + cities_dom = dom.getElementsByTagName('city') + + for city_dom in cities_dom: + city = {} + city['name'] = city_dom.getElementsByTagName('name')[0].getAttribute('data') + city['latitude_e6'] = city_dom.getElementsByTagName('latitude_e6')[0].getAttribute('data') + city['longitude_e6'] = city_dom.getElementsByTagName('longitude_e6')[0].getAttribute('data') + cities.append(city) + + dom.unlink() + + return cities + +def get_weather_from_yahoo(location_id, units = 'metric'): + """ + Fetches weather report from Yahoo! + + Parameters + location_id: A five digit US zip code or location ID. To find your location ID, + browse or search for your city from the Weather home page(http://weather.yahoo.com/) + The weather ID is in the URL for the forecast page for that city. You can also get the location ID by entering your zip code on the home page. For example, if you search for Los Angeles on the Weather home page, the forecast page for that city is http://weather.yahoo.com/forecast/USCA0638.html. The location ID is USCA0638. + + units: type of units. 'metric' for metric and '' for non-metric + Note that choosing metric units changes all the weather units to metric, for example, wind speed will be reported as kilometers per hour and barometric pressure as millibars. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. See http://developer.yahoo.com/weather/#channel + """ + location_id = quote(location_id) + if units == 'metric': + unit = 'c' + else: + unit = 'f' + url = YAHOO_WEATHER_URL % (location_id, unit) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + weather_data = {} + weather_data['title'] = dom.getElementsByTagName('title')[0].firstChild.data + weather_data['link'] = dom.getElementsByTagName('link')[0].firstChild.data + + ns_data_structure = { + 'location': ('city', 'region', 'country'), + 'units': ('temperature', 'distance', 'pressure', 'speed'), + 'wind': ('chill', 'direction', 'speed'), + 'atmosphere': ('humidity', 'visibility', 'pressure', 'rising'), + 'astronomy': ('sunrise', 'sunset'), + 'condition': ('text', 'code', 'temp', 'date') + } + + for (tag, attrs) in ns_data_structure.iteritems(): + weather_data[tag] = xml_get_ns_yahoo_tag(dom, YAHOO_WEATHER_NS, tag, attrs) + + weather_data['geo'] = {} + weather_data['geo']['lat'] = dom.getElementsByTagName('geo:lat')[0].firstChild.data + weather_data['geo']['long'] = dom.getElementsByTagName('geo:long')[0].firstChild.data + + weather_data['condition']['title'] = dom.getElementsByTagName('item')[0].getElementsByTagName('title')[0].firstChild.data + weather_data['html_description'] = dom.getElementsByTagName('item')[0].getElementsByTagName('description')[0].firstChild.data + + forecasts = [] + for forecast in dom.getElementsByTagNameNS(YAHOO_WEATHER_NS, 'forecast'): + forecasts.append(xml_get_attrs(forecast,('date', 'low', 'high', 'text', 'code'))) + weather_data['forecasts'] = forecasts + + dom.unlink() + + return weather_data + + + +def get_weather_from_noaa(station_id): + """ + Fetches weather report from NOAA: National Oceanic and Atmospheric Administration (United States) + + Parameter: + station_id: the ID of the weather station near the necessary location + To find your station ID, perform the following steps: + 1. Open this URL: http://www.weather.gov/xml/current_obs/seek.php?state=az&Find=Find + 2. Select the necessary state state. Click 'Find'. + 3. Find the necessary station in the 'Observation Location' column. + 4. The station ID is in the URL for the weather page for that station. + For example if the weather page is http://weather.noaa.gov/weather/current/KPEO.html -- the station ID is KPEO. + + Other way to get the station ID: use this library: http://code.google.com/p/python-weather/ and 'Weather.location2station' function. + + Returns: + weather_data: a dictionary of weather data that exists in XML feed. + + (useful icons: http://www.weather.gov/xml/current_obs/weather.php) + """ + station_id = quote(station_id) + url = NOAA_WEATHER_URL % (station_id) + handler = urllib2.urlopen(url) + dom = minidom.parse(handler) + handler.close() + + data_structure = ('suggested_pickup', + 'suggested_pickup_period', + 'location', + 'station_id', + 'latitude', + 'longitude', + 'observation_time', + 'observation_time_rfc822', + 'weather', + 'temperature_string', + 'temp_f', + 'temp_c', + 'relative_humidity', + 'wind_string', + 'wind_dir', + 'wind_degrees', + 'wind_mph', + 'wind_gust_mph', + 'pressure_string', + 'pressure_mb', + 'pressure_in', + 'dewpoint_string', + 'dewpoint_f', + 'dewpoint_c', + 'heat_index_string', + 'heat_index_f', + 'heat_index_c', + 'windchill_string', + 'windchill_f', + 'windchill_c', + 'icon_url_base', + 'icon_url_name', + 'two_day_history_url', + 'ob_url' + ) + weather_data = {} + current_observation = dom.getElementsByTagName('current_observation')[0] + for tag in data_structure: + try: + weather_data[tag] = current_observation.getElementsByTagName(tag)[0].firstChild.data + except IndexError: + pass + + dom.unlink() + return weather_data + + + +def xml_get_ns_yahoo_tag(dom, ns, tag, attrs): + """ + Parses the necessary tag and returns the dictionary with values + + Parameters: + dom - DOM + ns - namespace + tag - necessary tag + attrs - tuple of attributes + + Returns: a dictionary of elements + """ + element = dom.getElementsByTagNameNS(ns, tag)[0] + return xml_get_attrs(element,attrs) + + +def xml_get_attrs(xml_element, attrs): + """ + Returns the list of necessary attributes + + Parameters: + element: xml element + attrs: tuple of attributes + + Return: a dictionary of elements + """ + + result = {} + for attr in attrs: + result[attr] = xml_element.getAttribute(attr) + return result diff --git a/src/plugins/weather/weather.py b/src/plugins/weather/weather.py new file mode 100644 index 0000000..0da15df --- /dev/null +++ b/src/plugins/weather/weather.py @@ -0,0 +1,537 @@ +# coding=UTF-8 +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15locale as g15locale +_ = g15locale.get_translation("weather", modfile = __file__).ugettext + +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.util.g15scheduler as g15scheduler +import gnome15.util.g15uigconf as g15uigconf +import gnome15.util.g15pythonlang as g15pythonlang +import gnome15.util.g15gconf as g15gconf +import gnome15.util.g15cairo as g15cairo +import gnome15.util.g15icontools as g15icontools +import gnome15.g15driver as g15driver +import gnome15.g15globals as g15globals +import gnome15.g15text as g15text +import gnome15.g15plugin as g15plugin +import gtk +import os +import pango +import logging +import time +import sys +logger = logging.getLogger(__name__) + + +# Plugin details - All of these must be provided +id="weather" +name=_("Weather") +description=_("Displays the current weather at a location. It can currently use NOAA and Yahoo as sources \ +of weather information.") +author="Brett Smith " +copyright=_("Copyright (C)2010 Brett Smith") +site="http://www.russo79.com/gnome15" +has_preferences=True +default_enabled=True +needs_network=True +unsupported_models = [ g15driver.MODEL_G110, g15driver.MODEL_G11, g15driver.MODEL_G930, g15driver.MODEL_G35 ] + +DEFAULT_LOCATION="london,england" + +''' +This simple plugin displays the current weather at a location +''' + +CELSIUS=0 +FARANHEIT=1 +KELVIN=2 + +DEFAULT_UPDATE_INTERVAL = 60 # minutes + +def create(gconf_key, gconf_client, screen): + return G15Weather(gconf_key, gconf_client, screen) + +def show_preferences(parent, driver, gconf_client, gconf_key): + G15WeatherPreferences(parent, gconf_client, gconf_key) + +def get_location(gconf_client, gconf_key): + loc = gconf_client.get_string(gconf_key + "/location") + if loc == None: + return DEFAULT_LOCATION + return loc + +def get_backend(account_type): + """ + Get the backend plugin module, given the account_type + + Keyword arguments: + account_type -- account type + """ + import gnome15.g15pluginmanager as g15pluginmanager + return g15pluginmanager.get_module_for_id("weather-%s" % account_type) + +def get_available_backends(): + """ + Get the "account type" names that are available by listing all of the + backend plugins that are installed + """ + l = [] + import gnome15.g15pluginmanager as g15pluginmanager + for p in g15pluginmanager.imported_plugins: + if p.id.startswith("weather-"): + l.append(p.id[8:]) + return l + +def c_to_f(c): + return c * 9/5.0 + 32 + +def c_to_k(c): + return c + 273.15 + +def mph_to_kph(mph): + return mph * 1.609344 + +def kph_to_mph(mph): + return mph * 0.621371192 + +class G15WeatherPreferences(): + ''' + Configuration UI + ''' + + def __init__(self, parent, gconf_client, gconf_key): + self._gconf_client = gconf_client + self._gconf_key = gconf_key + self._visible_options = None + + self._widget_tree = gtk.Builder() + self._widget_tree.add_from_file(os.path.join(os.path.dirname(__file__), "weather.ui")) + + dialog = self._widget_tree.get_object("WeatherDialog") + dialog.set_transient_for(parent) + + self._source = self._widget_tree.get_object("Source") + self._source.connect("changed", self._load_options_for_source) + + self._sources_model = self._widget_tree.get_object("SourcesModel") + for b in get_available_backends(): + l = [b, get_backend(b).backend_name ] + self._sources_model.append(l) + g15uigconf.configure_combo_from_gconf(gconf_client, "%s/source" % gconf_key, "Source", self._sources_model[0][0] if len(self._sources_model) > 0 else None, self._widget_tree) + self._load_options_for_source() + + update = self._widget_tree.get_object("UpdateAdjustment") + update.set_value(g15gconf.get_int_or_default(gconf_client, gconf_key + "/update", DEFAULT_UPDATE_INTERVAL)) + update.connect("value-changed", self._value_changed, update, gconf_key + "/update") + + unit = self._widget_tree.get_object("UnitCombo") + unit.set_active(gconf_client.get_int(gconf_key + "/units")) + unit.connect("changed", self._unit_changed, unit, gconf_key + "/units") + + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/use_theme_icons" % gconf_key, "UseThemeIcons", True, self._widget_tree) + g15uigconf.configure_checkbox_from_gconf(gconf_client, "%s/twenty_four_hour_times" % gconf_key, "TwentyFourHourTimes", True, self._widget_tree) + + dialog.run() + dialog.hide() + + def _create_options_for_source(self, source): + backend = get_backend(source) + if backend is None: + logger.warning("No backend for weather source %s", source) + return None + return backend.create_options(self._gconf_client, "%s/%s" % ( self._gconf_key, source ) ) + + def _get_selected_source(self): + active = self._source.get_active() + return None if active == -1 else self._sources_model[active][0] + + def _load_options_for_source(self, widget = None): + source = self._get_selected_source() + if source: + options = self._create_options_for_source(source) + else: + options = None + if self._visible_options != None: + self._visible_options.component.destroy() + self._visible_options = options + place_holder = self._widget_tree.get_object("PlaceHolder") + for c in place_holder.get_children(): + place_holder.remove(c) + if self._visible_options is not None: + self._visible_options.component.reparent(place_holder) + else: + l = gtk.Label("No options found for this source\n") + l.xalign = 0.5 + l.show() + place_holder.add(l) + + def _changed(self, widget, location, gconf_key): + self._gconf_client.set_string(gconf_key, widget.get_text()) + + def _unit_changed(self, widget, location, gconf_key): + self._gconf_client.set_int(gconf_key, widget.get_active()) + + def _value_changed(self, widget, location, gconf_key): + self._gconf_client.set_int(gconf_key, int(widget.get_value())) + + +class WeatherOptions(): + + def __init__(self): + pass + +class WeatherData(): + + def __init__(self, location): + self.location = location + +class WeatherBackend(): + + def __init__(self, gconf_client, gconf_key): + self.gconf_client = gconf_client + self.gconf_key = gconf_key + + def get_weather_data(self): + raise Exception("Not implemented") + +class G15Weather(g15plugin.G15RefreshingPlugin): + + def __init__(self, gconf_key, gconf_client, screen): + g15plugin.G15RefreshingPlugin.__init__(self, gconf_client, gconf_key, screen, "weather-few-clouds", id, name) + self.only_refresh_when_visible = False + + def activate(self): + self._page_properties = {} + self._page_attributes = {} + self._weather = None + self._config_change_handle = None + self._load_config() + self._text = g15text.new_text(self.screen) + g15plugin.G15RefreshingPlugin.activate(self) + self.watch(None, self._loc_changed) + + def deactivate(self): + self._page_properties = {} + self._page_attributes = {} + if self._config_change_handle is not None: + self._config_change_handle.cancel() + g15plugin.G15RefreshingPlugin.deactivate(self) + + def destroy(self): + pass + + def refresh(self): + try : + backend_type = g15gconf.get_string_or_default(self.gconf_client, "%s/source" % self.gconf_key, None) + if backend_type: + backend = get_backend(backend_type).create_backend(self.gconf_client, "%s/%s" % (self.gconf_key, backend_type) ) + self._weather = backend.get_weather_data() + else: + self._weather = None + self._page_properties, self._page_attributes = self._build_properties() + except Exception as e: + logger.debug("Error while refreshing", exc_info = e) + self._weather = None + self._page_properties = {} + self._page_attributes = {} + self._page_properties['message'] = _("Error parsing weather data!") + + def get_theme_properties(self): + return self._page_properties + + def get_theme_attributes(self): + return self._page_properties + + """ + Private + """ + + def _load_config(self): + val = g15gconf.get_int_or_default(self.gconf_client, self.gconf_key + "/update", DEFAULT_UPDATE_INTERVAL) + self.refresh_interval = val * 60.0 + + def _loc_changed(self, client, connection_id, entry, args): + if not entry.get_key().endswith("/theme") and not entry.get_key().endswith("/enabled"): + if self._config_change_handle is not None: + self._config_change_handle.cancel() + self._config_change_handle = g15scheduler.schedule("ApplyConfig", 3.0, self._config_changed) + + def _config_changed(self): + self.reload_theme() + self._load_config() + self._refresh() + self.screen.set_priority(self.page, g15screen.PRI_HIGH, revert_after = 6.0) + + def _get_icons(self, current): + c_icon = current['icon'] if 'icon' in current else None + f_icon = current['fallback_icon'] if 'fallback_icon' in current else None + t_icon = self._translate_icon(c_icon, f_icon) + return c_icon, f_icon, t_icon + + def _build_properties(self): + properties = {} + attributes = {} + use_twenty_four_hour = g15gconf.get_bool_or_default(self.gconf_client, "%s/twenty_four_hour_times" % self.gconf_key, True) + if self._weather is None: + properties["message"] = _("No weather source configuration") + else: + current = self._weather['current_conditions'] + if len(current) == 0: + properties["message"] = _("No weather data for location:-\n%s") % self._weather['location'] + else: + properties["location"] = self._weather['location'] + dt = self._weather['datetime'] + if use_twenty_four_hour: + properties["time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["time"] = g15locale.format_time(dt, self.gconf_client, False) + properties["date"] = g15locale.format_date(dt, self.gconf_client) + properties["datetime"] = g15locale.format_date_time(dt, self.gconf_client, False) + properties["message"] = "" + c_icon, f_icon, t_icon = self._get_icons(current) + if t_icon != None: + attributes["icon"] = g15cairo.load_surface_from_file(t_icon) + properties["icon"] = g15icontools.get_embedded_image_url(attributes["icon"]) + else: + logger.warning("No translated weather icon for %s", c_icon) + mono_thumb = self._get_mono_thumb_icon(c_icon) + if mono_thumb != None: + attributes["mono_thumb_icon"] = g15cairo.load_surface_from_file(os.path.join(os.path.join(os.path.dirname(__file__), "default"), mono_thumb)) + properties["condition"] = current['condition'] + + temp_c = g15pythonlang.to_float_or_none(current['temp_c']) + if temp_c is not None: + temp_f = c_to_f(temp_c) + temp_k = c_to_k(temp_c) + low_c = g15pythonlang.to_float_or_none(current['low']) if 'low' in current else None + if low_c is not None : + low_f = c_to_f(low_c) + low_k = c_to_k(low_c) + high_c = g15pythonlang.to_float_or_none(current['high']) if 'high' in current else None + if high_c is not None : + high_f = c_to_f(high_c) + high_k = c_to_k(high_c) + + properties["temp_c"] = "%3.1f°C" % temp_c if temp_c is not None else "" + properties["hi_c"] = "%3.1f°C" % high_c if high_c is not None else "" + properties["lo_c"] = "%3.1f°C" % low_c if low_c is not None else "" + properties["temp_f"] = "%3.1f°F" % temp_f if temp_c is not None else "" + properties["lo_f"] = "%3.1f°F" % low_f if low_c is not None else "" + properties["high_f"] = "%3.1f°F" % high_f if high_c is not None else "" + properties["temp_k"] = "%3.1f°K" % temp_k if temp_c is not None else "" + properties["lo_k"] = "%3.1f°K" % low_k if low_c is not None else "" + properties["high_k"] = "%3.1f°K" % high_k if high_c is not None else "" + + units = self.gconf_client.get_int(self.gconf_key + "/units") + if units == CELSIUS: + unit = "C" + properties["temp"] = properties["temp_c"] + properties["temp_short"] = "%2.0f°" % temp_c if temp_c else "" + properties["hi"] = properties["hi_c"] + properties["hi_short"] = "%2.0f°" % high_c if high_c else "" + properties["lo"] = properties["lo_c"] + properties["lo_short"] = "%2.0f°" % low_c if low_c else "" + elif units == FARANHEIT: + unit = "F" + properties["lo"] = properties["lo_f"] + properties["lo_short"] = "%2.0f°" % low_f if low_c is not None else "" + properties["hi"] = properties["high_f"] + properties["hi_short"] = "%2.0f°" % high_f if high_c is not None else "" + properties["temp"] = properties["temp_f"] + properties["temp_short"] = "%2.0f°" % temp_f if temp_c is not None else "" + else: + unit = "K" + properties["lo"] = properties["lo_k"] + properties["lo_short"] = "%2.0f°" % low_k if low_c is not None else "" + properties["hi"] = properties["high_k"] + properties["hi_short"] = "%2.0f°" % high_k if high_c is not None else "" + properties["temp"] = properties["temp_k"] + properties["temp_short"] = "%2.0f°" % temp_k if temp_c is not None else "" + + + # Wind + wind = g15pythonlang.append_if_exists(current, "wind_chill", "", "%sC") + wind = g15pythonlang.append_if_exists(current, "wind_speed", wind, "%sKph") + wind = g15pythonlang.append_if_exists(current, "wind_direction", wind, "%sdeg") + properties["wind"] = wind + + # Visibility + visibility = g15pythonlang.append_if_exists(current, "visibility", "", "%sM") + properties["visibility"] = visibility + + # Pressure + pressure = g15pythonlang.append_if_exists(current, "pressure", "", "%smb") + properties["pressure"] = pressure + + # Humidity + humidity = g15pythonlang.append_if_exists(current, "humidity", "", "%s%%") + properties["humidity"] = humidity + + # Sunrise + dt = current['sunrise'] if 'sunrise' in current else None + if dt is None: + properties["sunrise_time"] = "" + elif use_twenty_four_hour: + properties["sunrise_time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["sunrise_time"] = g15locale.format_time(dt, self.gconf_client, False) + + # Sunset + dt = current['sunset'] if 'sunset' in current else None + if dt is None: + properties["sunset_time"] = "" + elif use_twenty_four_hour: + properties["sunset_time"] = g15locale.format_time_24hour(dt, self.gconf_client, False) + else: + properties["sunset_time"] = g15locale.format_time(dt, self.gconf_client, False) + + # Blank all the forecasts by default + for y in range(1, 10): + properties["condition" + str(y)] = "" + properties["hi" + str(y)] = "" + properties["lo" + str(y)] = "" + properties["day" + str(y)] = "" + properties["day_letter" + str(y)] = "" + properties["icon" + str(y)] = "" + + # Forecasts + y = 1 + if 'forecasts' in self._weather: + for forecast in self._weather['forecasts']: + properties["condition" + str(y)] = forecast['condition'] + + lo_c = g15pythonlang.to_float_or_none(forecast['low']) + if lo_c is not None: + lo_f = c_to_f(temp_c) + lo_k = c_to_k(temp_c) + hi_c = g15pythonlang.to_float_or_none(forecast['high']) + if hi_c is not None: + hi_f = c_to_f(hi_c) + hi_k = c_to_k(hi_c) + + if units == CELSIUS: + properties["hi" + str(y)] = "%3.0f°C" % hi_c + properties["lo" + str(y)] = "%3.0f°C" % lo_c + elif units == FARANHEIT: + properties["hi" + str(y)] = "%3.0f°F" % hi_f + properties["lo" + str(y)] = "%3.0f°F" % lo_f + else: + properties["hi" + str(y)] = "%3.0f°K" % hi_k + properties["lo" + str(y)] = "%3.0f°K" % lo_k + + properties["day" + str(y)] = forecast['day_of_week'] + properties["day_letter" + str(y)] = forecast['day_of_week'][:1] + + c_icon, f_icon, t_icon = self._get_icons(forecast) + properties["icon" + str(y)] = g15icontools.get_embedded_image_url(g15cairo.load_surface_from_file(t_icon)) + + y += 1 + + return properties, attributes + + def _get_mono_thumb_icon(self, icon): + if icon == None or icon == "": + return None + else : + base_icon= self._get_base_icon(icon) + + if base_icon in [ "chanceofrain", "scatteredshowers" ]: + return "weather-showers-scattered.gif" + elif base_icon in [ "sunny", "haze" ]: + return "mono-sunny.gif" + elif base_icon == "mostlysunny": + return "mono-few-clouds.gif" + elif base_icon == "partlycloudy": + return "mono-clouds.gif" + elif base_icon in [ "mostlycloudy", "cloudy" ]: + return "mono-more-clouds.gif" + elif base_icon == "rain": + return "mono-rain.gif" + elif base_icon in [ "mist", "fog" ]: + return "mono-fog.gif" + elif base_icon in [ "chanceofsnow", "snow", "sleet", "flurries" ]: + return "mono-snow.gif" + elif base_icon in [ "storm", "chanceofstorm" ]: + return "mono-dark-clouds.gif" + elif base_icon in [ "thunderstorm", "chanceoftstorm" ]: + return "mono-thunder.gif" + + def _translate_icon(self, icon, fallback_icon): + theme_icon = icon + if theme_icon == None or theme_icon == "": + return None + else: + if not g15gconf.get_bool_or_default(self.gconf_client, "%s/use_theme_icons" % self.gconf_key, True): + return fallback_icon + + if theme_icon != None: + icon_path = g15icontools.get_icon_path(theme_icon, warning = False, include_missing = False) + if icon_path == None and theme_icon.endswith("-night"): + icon_path = g15icontools.get_icon_path(theme_icon[:len(theme_icon) - 6], include_missing = False) + + if icon_path != None: + return icon_path + + return g15icontools.get_icon_path(icon) + + def _get_base_icon(self, icon): + # Strips off URL path, image extension, size and weather prefix if present + base_icon = os.path.splitext(os.path.basename(icon))[0].rsplit("-")[0] + if base_icon.startswith("weather_"): + base_icon = base_icon[8:] + base_icon = base_icon.replace('_','') + return base_icon + + def _paint_panel(self, canvas, allocated_size, horizontal): + return self._paint_thumbnail(canvas, allocated_size, horizontal) + + def _paint_thumbnail(self, canvas, allocated_size, horizontal): + total_taken = 0 + self._text.set_canvas(canvas) + if self.screen.driver.get_bpp() == 1: + if "mono_thumb_icon" in self._page_attributes: + size = g15cairo.paint_thumbnail_image(allocated_size, self._page_attributes["mono_thumb_icon"], canvas) + canvas.translate(size + 2, 0) + total_taken += size + 2 + if "temp_short" in self._page_properties: + self._text.set_attributes(self._page_properties["temp_short"], \ + font_desc = g15globals.fixed_size_font_name, \ + font_absolute_size = 6 * pango.SCALE / 2) + x, y, width, height = self._text.measure() + total_taken += width + self._text.draw(x, y) + else: + rgb = self.screen.driver.get_color_as_ratios(g15driver.HINT_FOREGROUND, ( 0, 0, 0 )) + canvas.set_source_rgb(rgb[0],rgb[1],rgb[2]) + if "icon" in self._page_attributes: + size = g15cairo.paint_thumbnail_image(allocated_size, self._page_attributes["icon"], canvas) + total_taken += size + if "temp" in self._page_properties: + if horizontal: + self._text.set_attributes(self._page_properties["temp"], font_desc = "Sans", font_absolute_size = allocated_size * pango.SCALE / 2) + x, y, width, height = self._text.measure() + self._text.draw(total_taken, (allocated_size / 2) - height / 2) + total_taken += width + 4 + else: + self._text.set_attributes(self._page_properties["temp"], font_desc = "Sans", font_absolute_size = allocated_size * pango.SCALE / 4) + x, y, width, height = self._text.measure() + self._text.draw((allocated_size / 2) - width / 2, total_taken) + total_taken += height + 4 + return total_taken + diff --git a/src/plugins/weather/weather.ui b/src/plugins/weather/weather.ui new file mode 100644 index 0000000..33706fa --- /dev/null +++ b/src/plugins/weather/weather.ui @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + + + + Celsius + + + Faranheit + + + Kelvin + + + + + 1 + 99999 + 1 + 1 + + + 450 + False + 5 + Weather Preferences + False + True + center-on-parent + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + False + True + end + 0 + + + + + True + False + 4 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + SourcesModel + + + + 1 + + + + + True + True + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + + + + + True + False + <b>Source</b> + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 8 + + + True + False + Automatically update every + + + True + True + 0 + + + + + True + True + 6 + + False + False + True + True + UpdateAdjustment + + + True + True + 1 + + + + + True + False + minutes + + + True + True + 2 + + + + + + + + + True + False + <b>Update</b> + True + + + + + True + True + 1 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 4 + + + True + False + 8 + + + True + False + 0 + Tempature Unit: + + + False + False + 0 + + + + + True + False + Units + + + + 0 + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + Use theme icons + True + True + False + True + + + True + True + 0 + + + + + Show time in 24 hour format + True + True + False + True + + + True + True + 1 + + + + + True + True + 1 + + + + + + + + + True + False + <b>Display</b> + True + + + + + True + True + 2 + + + + + False + False + 1 + + + + + + button9 + + + diff --git a/src/plugins/webkitbrowser/Makefile.am b/src/plugins/webkitbrowser/Makefile.am new file mode 100644 index 0000000..a94c1ec --- /dev/null +++ b/src/plugins/webkitbrowser/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = default +plugindir = $(datadir)/gnome15/plugins/webkitbrowser +plugin_DATA = webkitbrowser.py + +EXTRA_DIST = \ + $(plugin_DATA) diff --git a/src/plugins/webkitbrowser/default/Makefile.am b/src/plugins/webkitbrowser/default/Makefile.am new file mode 100644 index 0000000..abea5b9 --- /dev/null +++ b/src/plugins/webkitbrowser/default/Makefile.am @@ -0,0 +1,5 @@ +themedir = $(datadir)/gnome15/plugins/webkitbrowser/default +theme_DATA = g19.svg + +EXTRA_DIST = \ + $(theme_DATA) diff --git a/src/plugins/webkitbrowser/default/g19.svg b/src/plugins/webkitbrowser/default/g19.svg new file mode 100644 index 0000000..285635a --- /dev/null +++ b/src/plugins/webkitbrowser/default/g19.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ${url} + + + diff --git a/src/plugins/webkitbrowser/webkitbrowser.py b/src/plugins/webkitbrowser/webkitbrowser.py new file mode 100644 index 0000000..b2e29b9 --- /dev/null +++ b/src/plugins/webkitbrowser/webkitbrowser.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gnome15.g15driver as g15driver +import gnome15.g15gtk as g15gtk +import gnome15.g15plugin as g15plugin +import gtk +import gobject +import webkit + +# Plugin details - All of these must be provided +id="webkitbrowser" +name="Webkit Browser" +description="Webkit based browser." +author="Brett Smith " +copyright="Copyright (C)2010 Brett Smith" +site="http://www.gnome15.org/" +has_preferences=False +supported_models = [ g15driver.MODEL_G19 ] + +def create(gconf_key, gconf_client, screen): + return G15WebkitBrowser(gconf_client, gconf_key, screen) + +class G15WebkitBrowser(g15plugin.G15PagePlugin): + + def __init__(self, gconf_client, gconf_key, screen): + g15plugin.G15PagePlugin.__init__(self, gconf_client, gconf_key, screen, \ + [ "browser", "gnome-web-browser", "web-browser", "www-browser", \ + "redhat-web-browser", "internet-web-browser" ], id, name) + self.add_page_on_activate = False + + def populate_page(self): + g15plugin.G15PagePlugin.populate_page(self) + self.window = g15gtk.G15OffscreenWindow("offscreenWindow") + self.page.add_child(self.window) + gobject.idle_add(self._create_browser) + + def activate(self): + g15plugin.G15PagePlugin.activate(self) + self.screen.key_handler.action_listeners.append(self) + + def deactivate(self): + g15plugin.G15PagePlugin.deactivate(self) + self.screen.key_handler.action_listeners.remove(self) + + def action_performed(self, binding): + if self.page is not None and self.page.is_visible(): + if binding.action == g15driver.PREVIOUS_PAGE: + gobject.idle_add(self._scroll_up) + return True + elif binding.action == g15driver.NEXT_PAGE: + gobject.idle_add(self._scroll_down) + return True + + ''' + Private + ''' + + def get_theme_properties(self): + return dict(g15plugin.G15PagePlugin.get_theme_properties(self).items() + { + "url" : "www.somewhere.com" + }.items()) + + def _scroll_up(self): + adj = self.scroller.get_vadjustment() + adj.set_value(adj.get_value() - adj.get_page_increment()) + self.screen.redraw(self.page) + + def _scroll_down(self): + adj = self.scroller.get_vadjustment() + adj.set_value(adj.get_value() + adj.get_page_increment()) + self.screen.redraw(self.page) + + def _create_browser(self): + view = webkit.WebView() + self.scroller = gtk.ScrolledWindow() + self.scroller.add(view) + view.open("http://www.youtube.com") + self.window.set_content(self.scroller) + self.screen.add_page(self.page) + self.screen.redraw(self.page) \ No newline at end of file diff --git a/src/pylibg19/Makefile.am b/src/pylibg19/Makefile.am new file mode 100644 index 0000000..8443237 --- /dev/null +++ b/src/pylibg19/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = g19 \ No newline at end of file diff --git a/src/pylibg19/g19/Makefile.am b/src/pylibg19/g19/Makefile.am new file mode 100644 index 0000000..17319b8 --- /dev/null +++ b/src/pylibg19/g19/Makefile.am @@ -0,0 +1,11 @@ +g19dir = $(pkgpythondir)/../g19 +g19_PYTHON = \ + __init__.py \ + g19.py \ + globals.py \ + keys.py \ + receivers.py \ + runnable.py + +EXTRA_DIST = \ + $(g19_PYTHON) \ No newline at end of file diff --git a/src/pylibg19/g19/__init__.py b/src/pylibg19/g19/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pylibg19/g19/g19.py b/src/pylibg19/g19/g19.py new file mode 100644 index 0000000..17f0376 --- /dev/null +++ b/src/pylibg19/g19/g19.py @@ -0,0 +1,442 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from receivers import G19Receiver + +import sys +import threading +import time +import usb +from PIL import Image as Img +import logging +import array +logger = logging.getLogger(__name__) + +class G19(object): + '''Simple access to Logitech G19 features. + + All methods are thread-safe if not denoted otherwise. + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, write_timeout = 10000, reset_wait = 0): + '''Initializes and opens the USB device.''' + + logger.info("Setting up G19 with write timeout of %d", write_timeout) + self.enable_mm_keys = enable_mm_keys + self.__write_timeout = write_timeout + self.__usbDevice = G19UsbController(resetOnStart, enable_mm_keys, reset_wait) + self.__usbDeviceMutex = threading.Lock() + self.__keyReceiver = G19Receiver(self) + self.__threadDisplay = None + + self.__frame_content = [0x10, 0x0F, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x01, 0xEF, 0x00, 0x0F] + for i in range(16, 256): + self.__frame_content.append(i) + for i in range(256): + self.__frame_content.append(i) + + @staticmethod + def convert_image_to_frame(filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + @return Frame data to be used with send_frame(). + + ''' + img = Img.open(filename) + access = img.load() + if img.size != (320, 240): + img = img.resize((320, 240), Img.CUBIC) + access = img.load() + data = [] + for x in range(320): + for y in range(240): + ax = access[x, y] + if len(ax) == 3: + r, g, b = ax + else: + r, g, b, a = ax + val = G19.rgb_to_uint16(r, g, b) + data.append(val >> 8) + data.append(val & 0xff) + return data + + @staticmethod + def rgb_to_uint16(r, g, b): + '''Converts a RGB value to 16bit highcolor (5-6-5). + + @return 16bit highcolor value in little-endian. + + ''' + rBits = r * 2**5 / 255 + gBits = g * 2**6 / 255 + bBits = b * 2**5 / 255 + + rBits = rBits if rBits <= 0b00011111 else 0b00011111 + gBits = gBits if gBits <= 0b00111111 else 0b00111111 + bBits = bBits if bBits <= 0b00011111 else 0b00011111 + + valueH = (rBits << 3) | (gBits >> 3) + valueL = (gBits << 5) | bBits + return valueL << 8 | valueH + + def add_input_processor(self, input_processor): + self.__keyReceiver.add_input_processor(input_processor) + + def add_applet(self, applet): + '''Starts an applet.''' + self.add_input_processor(applet.get_input_processor()) + + def fill_display_with_color(self, r, g, b): + '''Fills display with given color.''' + # 16bit highcolor format: 5 red, 6 gree, 5 blue + # saved in little-endian, because USB is little-endian + value = self.rgb_to_uint16(r, g, b) + valueH = value & 0xff + valueL = value >> 8 + frame = [valueL, valueH] * (320 * 240) + self.send_frame(frame) + + def load_image(self, filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + ''' + self.send_frame(self.convert_image_to_frame(filename)) + + def read_g_and_m_keys(self, maxLen=20): + '''Reads interrupt data from G, M and light switch keys. + + @return maxLen Maximum number of bytes to read. + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf1.interruptRead( + 0x83, maxLen, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading g and m keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_display_menu_keys(self): + '''Reads interrupt data from display keys. + + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading display menu keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_multimedia_keys(self): + '''Reads interrupt data from multimedia keys. + + @return Read data or empty list. + + ''' + if not self.enable_mm_keys: + return False + + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading multimedia keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def reset(self): + '''Initiates a bus reset to USB device.''' + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.reset() + finally: + self.__usbDeviceMutex.release() + + def save_default_bg_color(self, r, g, b): + '''This stores given color permanently to keyboard. + + After a reset this will be color used by default. + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def send_frame(self, data): + '''Sends a frame to display. + + @param data 320x240x2 bytes, containing the frame in little-endian + 16bit highcolor (5-6-5) format. + Image must be row-wise, starting at upper left corner and ending at + lower right. This means (data[0], data[1]) is the first pixel and + (data[239 * 2], data[239 * 2 + 1]) the lower left one. + + ''' + if len(data) != (320 * 240 * 2): + raise ValueError("illegal frame size: " + str(len(data)) + + " should be 320x240x2=" + str(320 * 240 * 2)) + frame = list(self.__frame_content) + frame += data + + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf0.bulkWrite(2, frame, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_bg_color(self, r, g, b): + '''Sets backlight to given color.''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x307, 0x01, 10000) + finally: + self.__usbDeviceMutex.release() + + def set_enabled_m_keys(self, keys): + '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R. + + example: + from logitech.g19_keys import Data + lg19 = G19() + lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR) + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_brightness(self, val): + '''Sets display brightness. + + @param val in [0,100] (off..maximum). + + ''' + data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00] + rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def start_event_handling(self): + '''Start event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.stop_event_handling() + self.__threadDisplay = threading.Thread( + target=self.__keyReceiver.run) + self.__keyReceiver.start() + self.__threadDisplay.name = "EventThread" + self.__threadDisplay.setDaemon(True) + self.__threadDisplay.start() + + def stop_event_handling(self): + '''Stops event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.__keyReceiver.stop() + if self.__threadDisplay: + self.__threadDisplay.join() + self.__threadDisplay = None + + def close(self): + logger.info("Closing G19") + self.stop_event_handling() + self.__usbDevice.close() + + +class G19UsbController(object): + '''Controller for accessing the G19 USB device. + + The G19 consists of two composite USB devices: + * 046d:c228 + The keyboard consisting of two interfaces: + MI00: keyboard + EP 0x81(in) - INT the keyboard itself + MI01: (ifacMM) + EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch + + * 046d:c229 + LCD display with two interfaces: + MI00 (0x05): (iface0) via control data in: display keys + EP 0x81(in) - INT + EP 0x02(out) - BULK display itself + MI01 (0x06): (iface1) backlight + EP 0x83(in) - INT G-keys, M1..3/MR key, light key + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0): + self.enable_mm_keys = enable_mm_keys + logger.info("Looking for LCD device") + self.__lcd_device = self._find_device(0x046d, 0xc229) + if not self.__lcd_device: + raise usb.USBError("G19 LCD not found on USB bus") + + # Reset + self.handleIf0 = self.__lcd_device.open() + if resetOnStart: + logger.info("Resetting LCD device") + self.handleIf0.reset() + time.sleep(float(resetWait) / 1000.0) + logger.info("Re-opening LCD device") + self.handleIf0 = self.__lcd_device.open() + logger.info("Re-opened LCD device") + + self.handleIf1 = self.__lcd_device.open() + + config = self.__lcd_device.configurations[0] + display_interface = config.interfaces[0][0] + + # This is to cope with a difference in pyusb 1.0 compatibility layer + if len(config.interfaces) > 1: + macro_and_backlight_interface = config.interfaces[1][0] + else: + macro_and_backlight_interface = config.interfaces[0][1] + + try: + logger.debug("Detaching kernel driver for LCD device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf0.detachKernelDriver(display_interface.interfaceNumber) + logger.debug("Detached kernel driver for LCD device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e) + + try: + logger.debug("Detaching kernel driver for macro / backlight device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber) + logger.debug("Detached kernel driver for macro / backlight device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e) + + logger.debug("Setting configuration") + + #self.handleIf0.setConfiguration(1) + #self.handleIf1.setConfiguration(1) + + logger.debug("Claiming LCD interface") + self.handleIf0.claimInterface(display_interface.interfaceNumber) + logger.info("Claimed LCD interface") + logger.debug("Claiming macro interface") + self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber) + logger.info("Claimed macro interface") + + if self.enable_mm_keys: + logger.debug("Looking for multimedia keys device") + self.__kbd_device = self._find_device(0x046d, 0xc228) + if not self.__kbd_device: + raise usb.USBError("G19 keyboard not found on USB bus") + self.handleIfMM = self.__kbd_device.open() + + if resetOnStart: + logger.debug("Resetting multimedia keys device") + self.handleIfMM.reset() + logger.debug("Re-opening multimedia keys device") + self.handleIfMM = self.__kbd_device.open() + logger.debug("Re-opened multimedia keys device") + + + config = self.__kbd_device.configurations[0] + ifacMM = config.interfaces[1][0] + + try: + self.handleIfMM.setConfiguration(1) + except usb.USBError as e: + logger.debug("Error when trying to set configuration", exc_info = e) + pass + try: + logger.debug("Detaching kernel driver for multimedia keys device") + self.handleIfMM.detachKernelDriver(ifacMM) + logger.debug("Detached kernel driver for multimedia keys device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e) + + logger.debug("Claiming multimedia interface") + self.handleIfMM.claimInterface(1) + logger.info("Claimed multimedia keys interface") + + + def close(self): + if self.enable_mm_keys: + self.handleIfMM.releaseInterface() + self.handleIf1.releaseInterface() + self.handleIf0.releaseInterface() + + @staticmethod + def _find_device(idVendor, idProduct): + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == idVendor and \ + dev.idProduct == idProduct: + return dev + return None + + def reset(self): + '''Resets the device on the USB.''' + self.handleIf0.reset() + self.handleIf1.reset() + +def main(): + lg19 = G19() + lg19.start_event_handling() + time.sleep(20) + lg19.stop_event_handling() + +if __name__ == '__main__': + main() diff --git a/src/pylibg19/g19/globals.py b/src/pylibg19/g19/globals.py new file mode 100644 index 0000000..6b7e7e3 --- /dev/null +++ b/src/pylibg19/g19/globals.py @@ -0,0 +1,18 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +name = "pylibg19" +version = "0.0.1" diff --git a/src/pylibg19/g19/keys.py b/src/pylibg19/g19/keys.py new file mode 100644 index 0000000..c90e688 --- /dev/null +++ b/src/pylibg19/g19/keys.py @@ -0,0 +1,205 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +class Key(object): + '''Static container containing all keys.''' + + # G/M keys + # light switch + LIGHT, \ + M1, \ + M2, \ + M3, \ + MR, \ + G01, \ + G02, \ + G03, \ + G04, \ + G05, \ + G06, \ + G07, \ + G08, \ + G09, \ + G10, \ + G11, \ + G12 = range(17) + + # special keys at display + BACK, \ + DOWN, \ + LEFT, \ + MENU, \ + OK, \ + RIGHT, \ + SETTINGS, \ + UP = range(G12 + 1, G12 + 9) + + # multimedia keys + WINKEY_SWITCH, \ + NEXT, \ + PREV, \ + STOP, \ + PLAY, \ + MUTE, \ + SCROLL_UP, \ + SCROLL_DOWN = range(UP + 1, UP + 9) + + displayKeys = set([ + BACK, + DOWN, + LEFT, + MENU, + OK, + RIGHT, + SETTINGS, + UP + ]) + + mmKeys = set([ + WINKEY_SWITCH, + NEXT, + PREV, + STOP, + PLAY, + MUTE, + SCROLL_UP, + SCROLL_DOWN]) + + gmKeys = set([ + G01, + G02, + G03, + G04, + G05, + G06, + G07, + G08, + G09, + G10, + G11, + G12, + LIGHT, + M1, + M2, + M3, + MR]) + + +class Data(object): + '''Static container with all data values for all keys.''' + + ## + ## display keys + ## + + # special keys at display + # The current state of pressed keys is an OR-combination of the following + # codes. + # Incoming data always has 0x80 appended, e.g. pressing and releasing the menu + # key results in two INTERRUPT transmissions: [0x04, 0x80] and [0x00, 0x80] + # Pressing (and holding) UP and OK at the same time results in [0x88, 0x80]. + displayKeys = {} + displayKeys[0x01] = Key.SETTINGS + displayKeys[0x02] = Key.BACK + displayKeys[0x04] = Key.MENU + displayKeys[0x08] = Key.OK + displayKeys[0x10] = Key.RIGHT + displayKeys[0x20] = Key.LEFT + displayKeys[0x40] = Key.DOWN + displayKeys[0x80] = Key.UP + + + ## + ## G- and M-Keys + ## + + # these are the bit fields for setting the currently illuminated keys + # (see set_enabled_m_keys()) + LIGHT_KEY_M1 = 0x80 + LIGHT_KEY_M2 = 0x40 + LIGHT_KEY_M3 = 0x20 + LIGHT_KEY_MR = 0x10 + + # specific codes sent by M- and G-keys + # received as [0x02, keyL, keyH, 0x40] + # example: G3: [0x02, 0x04, 0x00, 0x40] + # G1 + G2 + G11: [0x02, 0x03, 0x04, 0x40] + KEY_G01 = 0x000001 + KEY_G02 = 0x000002 + KEY_G03 = 0x000004 + KEY_G04 = 0x000008 + KEY_G05 = 0x000010 + KEY_G06 = 0x000020 + KEY_G07 = 0x000040 + KEY_G08 = 0x000080 + KEY_G09 = 0x000100 + KEY_G10 = 0x000200 + KEY_G11 = 0x000400 + KEY_G12 = 0x000800 + KEY_M1 = 0x001000 + KEY_M2 = 0x002000 + KEY_M3 = 0x004000 + KEY_MR = 0x008000 + + # light switch + # this on is similar to G-keys: + # down: [0x02, 0x00, 0x00, 0x48] + # up: [0x02, 0x00, 0x00, 0x40] + KEY_LIGHT = 0x080000 + + gmKeys = {} + gmKeys[KEY_G01] = Key.G01 + gmKeys[KEY_G02] = Key.G02 + gmKeys[KEY_G03] = Key.G03 + gmKeys[KEY_G04] = Key.G04 + gmKeys[KEY_G05] = Key.G05 + gmKeys[KEY_G06] = Key.G06 + gmKeys[KEY_G07] = Key.G07 + gmKeys[KEY_G08] = Key.G08 + gmKeys[KEY_G09] = Key.G09 + gmKeys[KEY_G10] = Key.G10 + gmKeys[KEY_G11] = Key.G11 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_M1] = Key.M1 + gmKeys[KEY_M2] = Key.M2 + gmKeys[KEY_M3] = Key.M3 + gmKeys[KEY_MR] = Key.MR + gmKeys[KEY_LIGHT] = Key.LIGHT + + + ## + ## MM-keys + ## + + # multimedia keys + # received as [0x01, key] + # example: NEXT+SCROLL_UP: [0x01, 0x21] + # after scroll stopped: [0x01, 0x01] + # after release: [0x01, 0x00] + mmKeys = {} + mmKeys[0x01] = Key.NEXT + mmKeys[0x02] = Key.PREV + mmKeys[0x04] = Key.STOP + mmKeys[0x08] = Key.PLAY + mmKeys[0x10] = Key.MUTE + mmKeys[0x20] = Key.SCROLL_UP + mmKeys[0x40] = Key.SCROLL_DOWN + + # winkey switch to winkey off: [0x03, 0x01] + # winkey switch to winkey on: [0x03, 0x00] + KEY_WIN_SWITCH = 0x0103 + diff --git a/src/pylibg19/g19/receivers.py b/src/pylibg19/g19/receivers.py new file mode 100644 index 0000000..b4154ef --- /dev/null +++ b/src/pylibg19/g19/receivers.py @@ -0,0 +1,311 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from keys import (Data, Key) +from runnable import Runnable + +import threading +import time +import logging +logger = logging.getLogger(__name__) + +class InputProcessor(object): + '''Object to process key presses.''' + + def process_input(self, inputEvent): + '''Processes given event. + + Should return as fast as possible. Any time-consuming processing + should be done in another thread. + + @param inputEvent Event to process. + @return True if event was consumed, or False if ignored. + + ''' + return False + + +class InputEvent(object): + '''Event created by a key press or release.''' + + def __init__(self, oldState, newState, keysDown, keysUp): + '''Creates an InputEvent. + + @param oldState State before event happened. + @param newState State after event happened. + @param keysDown Keys newly pressed. + @param keysUp Kys released by this event. + + ''' + self.oldState = oldState + self.newState = newState + self.keysDown = keysDown + self.keysUp = keysUp + + +class State(object): + '''Current state of keyboard.''' + + def __init__(self): + self.__keysDown = set() + + def _data_to_keys_g_and_m(self, data): + '''Converts a G/M keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 4 or data[0] != 2: + raise ValueError("not a multimedia key packet: " + str(data)) + empty = 0x400000 + curVal = data[3] << 16 | data[2] << 8 | data[1] + keys = [] + while curVal != empty: + foundAKey = False + for val in Data.gmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.gmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect g/m key packet: " + + str(data)) + + return set(keys) + self.__keysDown = set() + + def _data_to_keys_d(self, data): + '''Converts a D data package to a set of keys defined as + pressed by it. + ''' + if len(data) != 2 or data[1] != 0x80: + raise ValueError("not a D key packet: " + str(data)) + curVal = data[0] + keys = [] + + '''Zero is release + ''' + if curVal != 0: + foundAKey = False + for val in Data.displayKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.displayKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect D key packet: " + + str(data)) + return set(keys) + + def _data_to_keys_mm(self, data): + '''Converts a multimedia keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 2 or data[0] not in [1, 3]: + raise ValueError("not a multimedia key packet: " + str(data)) + if data[0] == 1: + curVal = data[1] + keys = [] + while curVal: + foundAKey = False + for val in Data.mmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.mmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect multimedia key packet: " + + str(data)) + elif data == [3, 1]: + keys = [Key.WINKEY_SWITCH] + elif data == [3, 0]: + keys = [] + else: + raise ValueError("incorrect multimedia key packet: " + str(data)) + + return set(keys) + + def _update_keys_down(self, possibleKeys, keys): + '''Updates internal keysDown set. + + Updates the current state of all keys in 'possibleKeys' with state + given in 'keys'. + + Example: + Currently set as pressed in self.__keysDown: [A, B] + possibleKeys: [B, C, D] + keys: [C] + + This would set self.__keysDown to [A, C] and return ([C], [B]) + + @param possibleKeys Keys whose state could be given as 'pressed' at the + same time by 'keys'. + @param keys Current state of all keys in possibleKeys. + @return A pair of sets listing newly pressed and newly released keys. + + ''' + keysDown = set() + keysUp = set() + for key in possibleKeys: + if key in keys: + if key not in self.__keysDown: + self.__keysDown.add(key) + keysDown.add(key) + else: + if key in self.__keysDown: + self.__keysDown.remove(key) + keysUp.add(key) + return (keysDown, keysUp) + + def clone(self): + '''Returns an exact copy of this state.''' + state = State() + state.__keysDown = set(self.__keysDown) + return state + + def packet_received_g_and_m(self, data): + '''Mutates the state by given data packet from G- and M- keys. + + @param data Data packet received. + @return InputEvent for data packet, or None if data packet was ignored. + + ''' + oldState = self.clone() + evt = None + logger.debug("G key of %d", len(data)) + if len(data) == 4: + keys = self._data_to_keys_g_and_m(data) + keysDown, keysUp = self._update_keys_down(Key.gmKeys, keys) + newState = self.clone() + evt = InputEvent(oldState, newState, keysDown, keysUp) + return evt + + def packet_received_d(self, data): + '''Mutates the state by given data packet from D- keys. + + @param data Data packet received. + @return InputEvent for data packet, or None if data packet was ignored. + + ''' + oldState = self.clone() + evt = None + logger.debug("D key of %d", len(data)) + if len(data) == 2: + keys = self._data_to_keys_d(data) + keysDown, keysUp = self._update_keys_down(Key.displayKeys, keys) + newState = self.clone() + evt = InputEvent(oldState, newState, keysDown, keysUp) + return evt + + def packet_received_mm(self, data): + '''Mutates the state by given data packet from multimedia keys. + + @param data Data packet received. + @return InputEvent for data packet. + + ''' + oldState = self.clone() + if len(data) != 2: + raise ValueError("incorrect multimedia key packet: " + str(data)) + logger.debug("MM or Win key of %d", len(data)) + keys = self._data_to_keys_mm(data) + winKeySet = set([Key.WINKEY_SWITCH]) + if data[0] == 1: + # update state of all mm keys + logger.debug("MM key %d", len(data)) + possibleKeys = Key.mmKeys.difference(winKeySet) + keysDown, keysUp = self._update_keys_down(possibleKeys, keys) + else: + # update winkey state + logger.debug("Win key") + keysDown, keysUp = self._update_keys_down(winKeySet, keys) + newState = self.clone() + return InputEvent(oldState, newState, keysDown, keysUp) + + +class G19Receiver(Runnable): + '''This receiver consumes all data sent by special keys.''' + + def __init__(self, g19): + Runnable.__init__(self) + self.__g19 = g19 + self.__ips = [] + self.__mutex = threading.Lock() + self.__state = State() + + def add_input_processor(self, processor): + '''Adds an input processor.''' + self.__mutex.acquire() + self.__ips.append(processor) + self.__mutex.release() + pass + + def execute(self): + gotData = False + processors = self.list_all_input_processors() + + if self.__g19.enable_mm_keys: + data = self.__g19.read_multimedia_keys() + if data: + logger.debug('MM keys data %s', len(data)) + evt = self.__state.packet_received_mm(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('MM keys ignored') + gotData = True + + data = self.__g19.read_g_and_m_keys() + if data: + logger.debug('G/M keys data %s', len(data)) + evt = self.__state.packet_received_g_and_m(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('G/M keys ignored') + gotData = True + + data = self.__g19.read_display_menu_keys() + if data: + logger.debug('Menu keys Data %s', len(data)) + evt = self.__state.packet_received_d(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + logger.info('Menu keys ignored') + gotData = True + + if not gotData: + time.sleep(0.05) + + def list_all_input_processors(self): + '''Returns a list of all input processors currently registered to this + receiver. + + @return All registered processors. This list is a copy of the internal + one. + + ''' + self.__mutex.acquire() + allProcessors = list(self.__ips) + self.__mutex.release() + return allProcessors diff --git a/src/pylibg19/g19/runnable.py b/src/pylibg19/g19/runnable.py new file mode 100644 index 0000000..a8f3932 --- /dev/null +++ b/src/pylibg19/g19/runnable.py @@ -0,0 +1,85 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import threading + +class Runnable(object): + '''Helper object to create thread content objects doing periodic tasks, or + tasks supporting premature termination. + + Override execute() in inherited class. This will be called until the + thread is stopped. A Runnable can be started multiple times opposed to + threading.Thread. + + To write a non-periodic task that should support premature termination, + simply override run() and call is_about_to_stop() at possible termination + points. + + ''' + + def __init__(self): + self.__keepRunning = True + self.__mutex = threading.Lock() + + def execute(self): + '''This method must be implemented and will be executed in an infinite + loop as long as stop() was not called. + + An implementation is free to check is_about_to_stop() at any time to + allow a clean termination of current processing before reaching the end + of execute(). + + ''' + pass + + def is_about_to_stop(self): + '''Returns whether this thread will terminate after completing the + current execution cycle. + + @return True if thread will terminate after current execution cycle. + + ''' + self.__mutex.acquire() + val = self.__keepRunning + self.__mutex.release() + return not val + + def run(self): + '''Implements the infinite loop. Do not override, but override + execute() instead. + + ''' + while not self.is_about_to_stop(): + self.execute() + + def start(self): + '''Starts the thread. If stop() was called, but start() was not, run() + will do nothing. + + ''' + self.__mutex.acquire() + self.__keepRunning = True + self.__mutex.release() + + def stop(self): + '''Flags this thread to be terminated after next completed execution + cycle. Calling this method will NOT stop the thread instantaniously, + but will complete the current operation and terminate in a clean way. + + ''' + self.__mutex.acquire() + self.__keepRunning = False + self.__mutex.release() diff --git a/src/scripts/Makefile.am b/src/scripts/Makefile.am new file mode 100644 index 0000000..aa6024b --- /dev/null +++ b/src/scripts/Makefile.am @@ -0,0 +1,16 @@ + +if ENABLE_SYSTEMTRAY + MAYBE_SYSTEMTRAY = g15-systemtray +endif + +if ENABLE_INDICATOR + MAYBE_INDICATOR = g15-indicator +endif + +if ENABLE_DRIVER_KERNEL + MAYBE_KERNEL = g15-system-service +endif + +bin_SCRIPTS = g15-launch libg15test g15-diag g15-config g15-desktop-service g15-support-dump $(MAYBE_SYSTEMTRAY) $(MAYBE_INDICATOR) $(MAYBE_KERNEL) + +EXTRA_DIST = g15-launch libg15test g15-diag g15-config g15-desktop-service g15-systemtray g15-indicator g15-system-service g15-support-dump diff --git a/src/scripts/evtest b/src/scripts/evtest new file mode 100755 index 0000000..9b1f419 --- /dev/null +++ b/src/scripts/evtest @@ -0,0 +1,77 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import optparse +import select +import fcntl +import pyinputevent.scancodes as S +from pyinputevent.uinput import UInputDevice +from pyinputevent.pyinputevent import InputEvent, SimpleDevice +from pyinputevent.keytrans import * + +EVIOCGRAB = 0x40044590 + +class ForwardDevice(SimpleDevice): + def __init__(self, *args, **kwargs): + SimpleDevice.__init__(self, *args, **kwargs) + self.ctrl = False + self.alt = False + self.shift = False + + def monitor(self): + poll = select.poll() + poll.register(self, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) + fno = self.fileno() + fcntl.ioctl(fno, EVIOCGRAB, 1) + while True: + for x, e in poll.poll(): + self.read() + + @property + def modcode(self): + code = 0 + if self.shift: + code += 1 + if self.ctrl: + code += 2 + if self.alt: + code += 4 + return code + + def receive(self, event): + print "Event: %s" % str(event) + if event.etype == S.EV_KEY: + key = str(event.ecode) + if event.evalue == 2: + print "Auto %s" % key + else: + if event.evalue == 1: + print "Down %s" % key + else: + print "Up %s" % key + elif event.etype == 0: + print "Etype 0" + else: + print "Unhandled event: %s" % str(event) + +if __name__ == "__main__": + parser = optparse.OptionParser() + (options, args) = parser.parse_args() + device = ForwardDevice(args[0]) + device.monitor() + \ No newline at end of file diff --git a/src/scripts/g15-config b/src/scripts/g15-config new file mode 100755 index 0000000..585995d --- /dev/null +++ b/src/scripts/g15-config @@ -0,0 +1,61 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os + +# with pygobject-3.14.0 this seems to fix the import error +import gconf + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +#Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This is a work around - Now Gio is used in the lens plugin, it must be +# initialised before GTK. +try: + from gi.repository import Gio +except Exception as a: + logger.debug("Error when importing Gio", exc_info = a) + pass + +import pygtk +pygtk.require('2.0') +import gtk +import gconf +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +dbus.mainloop.glib.threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15config as g15config +import gnome15.g15globals as g15globals +import gnome15.g15drivermanager as g15drivermanager + +a = g15config.G15Config() +a.run() diff --git a/src/scripts/g15-desktop-service b/src/scripts/g15-desktop-service new file mode 100755 index 0000000..037ec77 --- /dev/null +++ b/src/scripts/g15-desktop-service @@ -0,0 +1,112 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import glib +import time + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# +import gobject +import gnome15.util.g15pythonlang as g15pythonlang +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + + +# Server host class + +def check_service_status(session_dbus): + try : + session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').GetServerInformation() + return True + except Exception as e: + logger.debug("Did not found enabled service", exc_info = e) + return False + +def start_service(options): + + if g15pythonlang.module_exists("setproctitle"): + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + else: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'") + + # Start the loop + try : + import gnome15.g15service as g15service + service = g15service.G15Service(None, no_trap=options.no_trap) + service.exit_on_no_devices = options.exit_on_no_devices + g15service.logger.setLevel(logger.level) + service.start_loop() + except dbus.exceptions.NameExistsException as e: + logger.debug("DBus service already exist", exc_info = e) + print "Gnome15 desktop service is already running" + sys.exit(1) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-f", "--foreground", action="store_true", dest="foreground", + default=False, help="Run desktop service in foreground.") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + parser.add_option("-x", "--exit", action="store_true", dest="exit_on_no_devices", + default=False, help="Exit immediately if there are no devices.") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + session_bus = None + + if len(args) == 1 and ( args[0] == "stop" or args[0] == "restart" ): + session_bus = dbus.SessionBus() + if not check_service_status(session_bus): + if args[0] == "stop": + print "Gnome15 desktop service is not running" + else: + session_bus.get_object('org.gnome15.Gnome15', '/org/gnome15/Service').Stop() + while check_service_status(session_bus): + pass + session_bus.close() + + if len(args) == 0 or ( len(args) == 1 and ( args[0] == "start" or args[0] == "restart" ) ): + session_bus = dbus.SessionBus() + if check_service_status(session_bus): + print "Gnome15 desktop service already running" + else: + if options.foreground or ( not options.foreground and os.fork() == 0 ): + start_service(options) diff --git a/src/scripts/g15-diag b/src/scripts/g15-diag new file mode 100755 index 0000000..dbe08c2 --- /dev/null +++ b/src/scripts/g15-diag @@ -0,0 +1,194 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +DBUS System Service that is intended to replace 'lgsetled', the command line tool initially +used by the kernel driver support to set the brightness of keyboard lights (the device +files of which require root access, as they are in /sys). +""" + +import sys +import os +import glib + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# +import gobject +gobject.threads_init() + +import gconf +import gnome15.g15drivermanager as g15drivermanager +import gnome15.g15devices as g15devices +import gnome15.g15uinput as g15uinput +import gnome15.g15driver as g15driver +import termios, sys, os + +TERMIOS = termios +conf_client = gconf.client_get_default() + +def getkey(): + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + new[3] = new[3] & ~TERMIOS.ICANON & ~TERMIOS.ECHO + new[6][TERMIOS.VMIN] = 1 + new[6][TERMIOS.VTIME] = 0 + termios.tcsetattr(fd, TERMIOS.TCSANOW, new) + c = None + try: + c = os.read(fd, 1) + finally: + termios.tcsetattr(fd, TERMIOS.TCSAFLUSH, old) + return c + +def list_drivers(): + for driver in g15drivermanager.imported_drivers.values(): + print "Id: %s" % driver.id + print "\tName: %s" % driver.name + print "\tDescription: %s" % driver.description + +def list_devices(): + for device in g15devices.find_all_devices(): + print "UID: %s" % device.uid + print "\tModel: %s" % device.model_id + print "\tUSB ID: 0x%0.4x:0x%0.4x" % ( device.controls_usb_id[0], device.controls_usb_id[1] ) + print "\tLCD BPP: %d" % ( device.bpp ) + print "\tLCD Size: %s" % ( str(device.lcd_size) ) + driver, reconfigured = get_driver(device) + if driver is None: + print "\tConfigured Driver: None found" + else: + driver_mod = sys.modules[driver.__module__] + if reconfigured: + print "\tConfigured Driver: %s (%s) [Next best, configured driver not available]" % ( driver_mod.id, driver_mod.name ) + else: + print "\tConfigured Driver: %s (%s)" % ( driver_mod.id, driver_mod.name ) + print "\t\tName: %s" % driver.get_name() + print "\t\tModel: %s" % driver.get_model_name() + print "\t\tSupported Models: %s" % driver.get_model_names() + print "\t\tBPP: %s" % driver.get_bpp() + print "\t\tAntialias: %s" % driver.get_antialias() + +def get_driver(device): + reconfigured = False + try: + driver = g15drivermanager.get_driver(conf_client, device) + except: + driver = g15drivermanager.get_best_driver(conf_client, device) + reconfigured = True + return driver, reconfigured + +def _check_hint(hint, value, name, list): + if value & hint: + list.append(name) + +def controls(uid): + g15uinput.open_devices() + device = g15devices.get_device(uid) + driver, reconfigured = get_driver(device) + if driver is None: + raise Exception("No driver for device with UID of %s." % uid) + + for c in driver.get_controls(): + print "%s" % c.id + print "\tName: %s" % c.name + print "\tLower: %s" % str(c.lower) + print "\tHigher: %s" % str(c.upper) + print "\tValue: %s" % str(c.value) + print "\tDefault Value: %s" % str(c.default_value) + hint_names = [] + _check_hint(g15driver.HINT_DIMMABLE, c.hint, "Dimmable", hint_names) + _check_hint(g15driver.HINT_SHADEABLE, c.hint, "Shadeable", hint_names) + _check_hint(g15driver.HINT_FOREGROUND, c.hint, "Foreground", hint_names) + _check_hint(g15driver.HINT_BACKGROUND, c.hint, "Background", hint_names) + _check_hint(g15driver.HINT_HIGHLIGHT, c.hint, "Highlight", hint_names) + _check_hint(g15driver.HINT_SWITCH, c.hint, "Switch", hint_names) + _check_hint(g15driver.HINT_MKEYS, c.hint, "MKeys", hint_names) + _check_hint(g15driver.HINT_VIRTUAL, c.hint, "Virtual", hint_names) + _check_hint(g15driver.HINT_RED_BLUE_LED, c.hint, "Red/Blue", hint_names) + print "\tHints: %s" % ",".join(hint_names) + +def keytest(uid): + g15uinput.open_devices() + device = g15devices.get_device(uid) + if uid is None: + raise Exception("No device with UID of %s." % uid) + driver, reconfigured = get_driver(device) + if driver is None: + raise Exception("No driver for device with UID of %s." % uid) + driver.connect() + print "Connected, now monitoring macro keys" + driver.grab_keyboard(handle_key) + print "Press any standard key to stop monitoring macro keys" + getkey() + driver.disconnect() + +def handle_key(keys, state): + print "Keys: %s, State: %d" % (keys, state) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + (options, args) = parser.parse_args() + + if len(args) == 0: + print "No command" + sys.exit(1) + + if args[0] == "devices": + list_devices() + elif args[0] == "drivers": + list_drivers() + elif args[0] == "keytest": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + keytest(args[0]) + elif args[0] == "controls": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + controls(args[0]) + elif args[0] == "control": + del args[0] + if len(args) == 0: + print "No device UID specified. Use 'g15-diag devices' to show devices" + sys.exit(1) + device = arg[0] + del args[0] + if len(args) == 0: + print "No control ID specified. Use 'g15-diag controls ' to show controls" + sys.exit(1) + control(device, args[0]) diff --git a/src/scripts/g15-indicator b/src/scripts/g15-indicator new file mode 100755 index 0000000..53f9eee --- /dev/null +++ b/src/scripts/g15-indicator @@ -0,0 +1,110 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Provides a panel indicator that can be used to control and monitor the Gnome15 +desktop service (g15-desktop-service). It will display a list of currently active +screens on activation, and allow the configuration UI to be launched (g15-config) +''' + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import os +import appindicator +import gconf +from threading import RLock + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This block MUST be before the imports of the gnome15 modules +import dbus +import gobject +gobject.threads_init() +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15globals as g15globals +import gnome15.g15service as g15service +import gnome15.g15screen as g15screen +import gnome15.util.g15convert as g15convert +import gnome15.g15desktop as g15desktop + +class G15Indicator(g15desktop.G15GtkMenuPanelComponent): + + def __init__(self): + g15desktop.G15GtkMenuPanelComponent.__init__(self) + + def create_component(self): + + item = gtk.MenuItem("Preferences") + item.connect("activate", self.show_configuration) + self.menu.append(item) + + item = gtk.MenuItem("About Gnome15") + item.connect("activate", self.about_info) + self.menu.append(item) + + self.menu.append(gtk.MenuItem()) + + self.indicator = appindicator.Indicator("gnome15", + self.get_icon_path("logitech-g-keyboard-panel"), + appindicator.CATEGORY_HARDWARE) + self.indicator.set_status (appindicator.STATUS_ACTIVE) + self.indicator.set_menu(self.menu) + + def clear_attention(self): + self.remove_attention_menu_item() + if self.conf_client.get_bool("/apps/gnome15/indicate_only_on_error"): + self.indicator.set_status (appindicator.STATUS_PASSIVE) + else: + self.indicator.set_status (appindicator.STATUS_ACTIVE) + + def attention(self, message = None): + self.indicator.set_status (appindicator.STATUS_ATTENTION) + + def icons_changed(self): + self.indicator.set_icon(self.get_icon_path([ "logitech-g-keyboard-panel", "logitech-g-keyboard-applet" ])) + self.indicator.set_attention_icon(self.get_icon_path([ "logitech-g-keyboard-error-panel", "logitech-g-keyboard-error-applet" ])) + +# run it in a gtk window +if __name__ == "__main__": + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("Could not import setproctitle. Using python as processname", exc_info = e) + pass + + if g15desktop.get_desktop() == "gnome-shell": + sys.stderr.write("Indicator is not supported in GNOME Shell, use the GNOME Shell extension instead") + sys.exit(1) + + G15Indicator().start_service() + gtk.main() \ No newline at end of file diff --git a/src/scripts/g15-launch b/src/scripts/g15-launch new file mode 100755 index 0000000..00bf358 --- /dev/null +++ b/src/scripts/g15-launch @@ -0,0 +1,67 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import glib +import time + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +import dbus +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-p", "--profile", action="store_true", dest="profile", + default="", help="Name of profile to activate. Defaults to automatic selection.") + parser.add_option("-s", "--screens", action="store_true", dest="screens", + default="", help="Which device(s) to use. Defaults to best device. \ +Names are in the format [model]_[index]. So if you have two G13 keyboards \ +and a G19, to select the first G13 you would use g13_0.") + (options, args) = parser.parse_args() + if len(args) == 0: + print "You must provide the command to launch through Gnome15 as an argument." + sys.exit(2) + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + session_bus = dbus.SessionBus() + try : + service = session_bus.get_object('org.gnome15.Gnome15', \ + '/org/gnome15/Service') + except Exception as e: + logger.debug("D-Bus service not available.", exc_info = e) + print "Gnome15 desktop service is not running. Applications may not be \ +launched through it. You can start the service using g15-desktop-service." + sys.exit(1) + + service.Launch(options.profile, options.screens, args) + \ No newline at end of file diff --git a/src/scripts/g15-support-dump b/src/scripts/g15-support-dump new file mode 100755 index 0000000..9a88f54 --- /dev/null +++ b/src/scripts/g15-support-dump @@ -0,0 +1,130 @@ +#!/bin/bash + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# +# Simple script to gather as much information about the environmen +# Gnome15 is running in as possible. +# +# Sorry it's a bit untidy, it will improve :) + +# Check running as root +if [ $(id -u) != 0 ] +then echo "$0: you should run that as root using either 'sudo $0' or 'su -c $0'" >&2 + exit 1 +fi + +separator() { + echo "------------------------------------------------------------" +} + +# System details +echo -e "System Details\n" +uname -a +if [ -f /etc/lsb-release ] +then cat /etc/lsb-release +fi +echo +cat /proc/cpuinfo +echo +cat /proc/meminfo + +# Gnome15 packages +echo -e "Gnome15 Packages\n" +if which dpkg >/dev/null 2>&1 +then echo -e "Debian based packaging found\n" + dpkg -l 'gnome15*' 'pylibg19*' 'lg4l*' 'python-uinput*' 'python-inputevent*' 'libsuinput*' 'libg15*' 'g15*' 2>/dev/null +fi +if which rpm >/dev/null 2>&1 +then echo -e "RPM based packaging found\n" + rpm -qa 'gnome15*' 'pylibg19*' 'lg4l*' 'python-uinput*' 'python-inputevent*' 'libsuinput*' 'libg15*' 'g15*' 2>/dev/null +fi +separator + +# lsusb +echo -e "USB Device Summary (lsusb)\n" +lsusb +echo -e "\nUSB Device Details (lsusb -v)\n" +lsusb -v +separator + +# kernel modules +echo -n "Kernel modules :" +mods=$(lsmod|awk '{ print $1 }'|grep "hid_"|sort -u) +if [ -z "${mods}" ] +then echo "No kernel modules used" +else echo "${mods}" + echo -e "\nFrame buffers: " + ls -l /dev/fb* + echo -e "\nInput Devices: " + for i in /dev/input/by-id/* + do + linked_to=$(ls -l $i|awk '{ print $10 }') + linked_to_name=$(basename $linked_to) + linked_to_file=/dev/input/$linked_to_name + linked_to_details=$(ls -l $linked_to_file|awk '{ print $1, $3, $4 }') + echo $(basename $i)" -> ${linked_to_name} ( ${linked_to_details} )" + done +fi +separator +if [ -f /etc/default/lg4l-linux ] +then echo "/etc/default/lg4l-linux contents :-" + cat /etc/default/lg4l-linux + separator +fi + +# USB device permissions (for g15direct/g19direct) +echo -n "USB device permissions" +ls -lR /dev/bus/usb +separator + +if [ -d /sys/class/leds ] +then echo -n "LED files (/sys/class/leds)" + ls -l /sys/class/leds + separator +fi + + +if [ -d /sys/class/graphics ] +then echo "Frame buffer information" + for i in /sys/class/graphics/* + do + echo "$i ->" + pushd $i >/dev/null + if [ -f name ]; then + echo " Name : "$(cat name) + fi + if [ -f mode ]; then + echo " Mode : "$(cat mode) + fi + if [ -f modes ]; then + echo " Modes: "$(cat modes) + fi + if [ -f bits_per_pixel ]; then + echo " BPP : "$(cat bits_per_pixel) + fi + ls -l|awk '{ print "\t" $0 }' + popd >/dev/null + done + separator +fi + +if [ -d /sys/bus/usb/drivers ] +then echo "Drivers bound to USB devices" + find /sys/bus/usb/drivers + separator + fi diff --git a/src/scripts/g15-system-service b/src/scripts/g15-system-service new file mode 100755 index 0000000..802d699 --- /dev/null +++ b/src/scripts/g15-system-service @@ -0,0 +1,105 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +DBUS System Service that is intended to replace 'lgsetled', the command line tool initially +used by the kernel driver support to set the brightness of keyboard lights (the device +files of which require root access, as they are in /sys). +""" + + +import sys +import os +import glib + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# +import gobject +gobject.threads_init() + +# DBUS - Use to check current desktop service status or stop it +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +# Server host class + +def check_service_status(bus): + return bus.name_has_owner('org.gnome15.SystemService') + +def start_service(bus, no_trap=False,): + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except ImportError as ie: + # Not a big issue + logger.debug("No setproctitle, process will be named 'python'", exc_info = ie) + + # Start the loop + try : + import gnome15.g15system as g15system + service = g15system.G15SystemServiceController(bus, no_trap=no_trap) + service.start_loop() + except dbus.exceptions.NameExistsException as e: + logger.debug("Gnome15 service already running", exc_info = e) + print "Gnome15 desktop service is already running" + sys.exit(1) + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-l", "--log", dest="log_level", metavar="INFO,DEBUG,WARNING,ERROR,CRITICAL", + default="warning" , help="Log level") + parser.add_option("-f", "--foreground", action="store_true", dest="foreground", + default=False, help="Run desktop service in foreground.") + parser.add_option("-s", "--session", action="store_true", dest="use_session_bus", + default=False, help="Use the session bus instead of system bus.") + parser.add_option("-n", "--notrap", action="store_true", dest="no_trap", + default=False, help="Do not try to trap signals.") + (options, args) = parser.parse_args() + + if options.log_level != None: + logger.setLevel(g15logging.get_level(options.log_level)) + + if len(args) == 1 and ( args[0] == "stop" or args[0] == "restart" ): + bus = dbus.SessionBus() if options.use_session_bus else dbus.SystemBus() + if not check_service_status(bus): + if args[0] == "stop": + print "Gnome15 system service is not running" + else: + service_object = bus.get_object('org.gnome15.SystemService', '/org/gnome15/SystemService') + system_service = dbus.Interface(service_object, 'org.gnome15.SystemService') + system_service.Stop() + + if len(args) == 0 or ( len(args) == 1 and ( args[0] == "start" or args[0] == "restart" ) ): + bus = dbus.SessionBus() if options.use_session_bus else dbus.SystemBus() + if check_service_status(bus): + print "Gnome15 desktop service already running" + else: + if options.foreground or ( not options.foreground and os.fork() == 0 ): + start_service(bus, options.no_trap) diff --git a/src/scripts/g15-systemtray b/src/scripts/g15-systemtray new file mode 100755 index 0000000..0de2696 --- /dev/null +++ b/src/scripts/g15-systemtray @@ -0,0 +1,129 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +Provides a panel indicator that can be used to control and monitor the Gnome15 +desktop service (g15-desktop-service). It will display a list of currently active +screens on activation, and allow the configuration UI to be launched (g15-config) +''' + + +import sys +import pygtk +pygtk.require('2.0') +import gtk +import os +import gconf +from threading import RLock + +# Allow running from local path +path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..") +if os.path.exists(path): + sys.path.insert(0, path) + +# Logging +import gnome15.g15logging as g15logging +logger = g15logging.get_root_logger() + +# This block MUST be before the imports of the gnome15 modules +import dbus +import gobject +gobject.threads_init() +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from dbus.mainloop.glib import threads_init +threads_init() +DBusGMainLoop(set_as_default=True) + +import gnome15.g15globals as g15globals +import gnome15.g15service as g15service +import gnome15.g15screen as g15screen +import gnome15.util.g15icontools as g15icontools +import gnome15.g15desktop as g15desktop + +class G15SystemTray(g15desktop.G15GtkMenuPanelComponent): + + def __init__(self): + self.prefs_menu = gtk.Menu() + g15desktop.G15GtkMenuPanelComponent.__init__(self) + + def create_component(self): + self.status_icon = gtk.StatusIcon() + + self.status_icon.connect('popup-menu', self._on_popup_menu) + self.status_icon.connect('activate', self._on_activate) + self.status_icon.connect('scroll_event', self.scroll_event) + + def clear_attention(self): + self.remove_attention_menu_item() + self.status_icon.set_from_pixbuf(self.normal_icon) + self.status_icon.set_tooltip("") + self.status_icon.set_visible(not self.conf_client.get_bool("/apps/gnome15/indicate_only_on_error")) + + def attention(self, message=None): + self.status_icon.set_visible(True) + self.status_icon.set_from_pixbuf(self.attention_icon) + self.status_icon.set_tooltip(message if message != None else self.default_message) + + def _on_popup_menu(self, status, button, time): + self.prefs_menu.popup(None, None, None, button, time) + + def _on_activate(self, status): + if len(self.menu.get_children()) > 0: + self.menu.popup(None, None, None, 1, gtk.get_current_event_time()) + + def add_service_item(self, item): + self._append_item(item, self.prefs_menu) + + def add_start_desktop_service(self): + g15desktop.G15GtkMenuPanelComponent.add_start_desktop_service(self) + self.add_service_item(gtk.MenuItem()) + + def rebuild_desktop_component(self): + g15desktop.G15GtkMenuPanelComponent.rebuild_desktop_component(self) + if len(self.devices)> 1: + self.add_service_item(gtk.MenuItem()) + item = gtk.MenuItem("Properties") + item.connect("activate", self.show_configuration) + self.add_service_item(item) + item = gtk.MenuItem("About") + item.connect("activate", self.about_info) + self.add_service_item(item) + self.status_icon.menu = self.prefs_menu + + self.prefs_menu.show_all() + + def icons_changed(self): + self.normal_icon = gtk.gdk.pixbuf_new_from_file_at_size(g15icontools.get_icon_path([ "logitech-g-keyboard-applet", "logitech-g-keyboard-panel" ]), self.status_icon.get_size(), self.status_icon.get_size()) + self.attention_icon = gtk.gdk.pixbuf_new_from_file_at_size(g15icontools.get_icon_path([ "logitech-g-keyboard-error-panel", "logitech-g-keyboard-error-applet" ]), self.status_icon.get_size(), self.status_icon.get_size()) + +# run it in a gtk window +if __name__ == "__main__": + try : + import setproctitle + setproctitle.setproctitle(os.path.basename(os.path.abspath(sys.argv[0]))) + except Exception as e: + logger.debug("setproctitle not available. Process will be named python", + exc_info = e) + + if g15desktop.get_desktop() == "gnome-shell": + sys.stderr.write("System Tray is not recommended in GNOME Shell, use the GNOME Shell extension instead (if you have version 3.4 or above)") + + tray = G15SystemTray() + tray.start_service() + gtk.main() diff --git a/src/scripts/lg4l-image b/src/scripts/lg4l-image new file mode 100755 index 0000000..cec72c8 --- /dev/null +++ b/src/scripts/lg4l-image @@ -0,0 +1,204 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2011 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Simple tool to draw an image on the framebuffer +""" + + +import sys +import os +import glib +import cairo +import array +from PIL import Image +from PIL import ImageMath +from cStringIO import StringIO + +# Allow running from local path +path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "..")) +if os.path.exists(path): + sys.path.insert(0, path) + +import gnome15.util.g15convert as g15convert +import gnome15.util.g15cairo as g15cairo +import gnome15.drivers.fb as fb + +if __name__ == "__main__": + import optparse + parser = optparse.OptionParser() + parser.add_option("-s", "--scale", dest="scale", metavar="stretch,zoom,tile,center,scale", + default="zoom" , help="Scale type") + parser.add_option("-d", "--device", dest="device", + default="/dev/fb0" , help="Framebuffer device") + (options, args) = parser.parse_args() + bg_style = options.scale + + # Check arguments + if len(args) != 1: + sys.stderr.write("You must provide a single image filenanme") + sys.exit(1) + + # Locate and configure the framebuffer + fb_dev = fb.fb_device(options.device) + var_info = fb_dev.get_var_info() + fixed_info = fb_dev.get_fixed_info() + screen_size = ( var_info.xres, var_info.yres ) + width, height = screen_size + + # Create an empty string buffer for use with monochrome LCD + empty_buf = "" + for i in range(0, fixed_info.smem_len): + empty_buf += chr(0) + + # Load the image + bg_img = args[0] + if g15cairo.is_url(bg_img) or os.path.exists(bg_img): + img_surface = g15cairo.load_surface_from_file(bg_img) + if img_surface is not None: + sx = float(width) / img_surface.get_width() + sy = float(height) / img_surface.get_height() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + context = cairo.Context(surface) + context.save() + if bg_style == "zoom": + scale = max(sx, sy) + context.scale(scale, scale) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "stretch": + context.scale(sx, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "scale": + x = ( width - img_surface.get_width() * sy ) / 2 + context.translate(x, 0) + context.scale(sy, sy) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "center": + x = ( width - img_surface.get_width() ) / 2 + y = ( height - img_surface.get_height() ) / 2 + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + elif bg_style == "tile": + context.set_source_surface(img_surface) + context.paint() + y = 0 + x = img_surface.get_width() + while y < height + img_surface.get_height(): + if x >= height + img_surface.get_width(): + x = 0 + y += img_surface.get_height() + context.restore() + context.save() + context.translate(x, y) + context.set_source_surface(img_surface) + context.paint() + x += img_surface.get_width() + + context.restore() + else: + sys.stderr.write("Failed to load image file %s." % bg_img) + sys.exit(1) + else: + sys.stderr.write("Image path %s is not a URL or an existing file." % bg_img) + sys.exit(1) + + # Convert the image to the required format for this device + if var_info.bits_per_pixel == 16: + try: + back_surface = cairo.ImageSurface (4, width, height) + except Exception as e: + logger.debug("Could not create ImageSurface. Trying alternative method", exc_info = e) + # Earlier version of Cairo + back_surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height) + back_context = cairo.Context (back_surface) + back_context.set_source_surface(surface, 0, 0) + back_context.set_operator (cairo.OPERATOR_SOURCE); + back_context.paint() + + if back_surface.get_format() == cairo.FORMAT_ARGB32: + """ + If the creation of the type 4 image failed (i.e. earlier version of Cairo) + then we have to convert it ourselves. This is slow. + + TODO Replace with C routine + """ + file_str = StringIO() + data = back_surface.get_data() + for i in range(0, len(data), 4): + r = ord(data[i + 2]) + g = ord(data[i + 1]) + b = ord(data[i + 0]) + file_str.write(g15convert.rgb_to_uint16(r, g, b)) + buf = file_str.getvalue() + else: + buf = str(back_surface.get_data()) + else: + arrbuf = array.array('B', empty_buf) + + argb_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + argb_context = cairo.Context(argb_surface) + argb_context.set_source_surface(surface) + argb_context.paint() + + ''' + Now convert the ARGB to a PIL image so it can be converted to a 1 bit monochrome image, with all + colours dithered. It would be nice if Cairo could do this :( Any suggestions? + ''' + pil_img = Image.frombuffer("RGBA", (width, height), argb_surface.get_data(), "raw", "RGBA", 0, 1) + pil_img = ImageMath.eval("convert(pil_img,'1')",pil_img=pil_img) + pil_img = ImageMath.eval("convert(pil_img,'P')",pil_img=pil_img) + pil_img = pil_img.point(lambda i: i >= 250,'1') + + # Invert the screen if required + if options.invert: + pil_img = pil_img.point(lambda i: 1^i) + + # Data is 160x43, 1 byte per pixel. Will have value of 0 or 1. + data = list(pil_img.getdata()) + v = 0 + b = 1 + + # TODO Replace with C routine + for row in range(0, height): + for col in range(0, width): + if data[( row * width ) + col]: + v += b + b = b << 1 + if b == 256: + # Full byte + b = 1 + i = row * fixed_info.line_length + col / 8 + + if row > 7 and col < 96: + ''' + ????? This was discovered more by trial and error rather than any + understanding of what is going on + ''' + i -= 12 + ( 7 * fixed_info.line_length ) + + arrbuf[i] = v + v = 0 + buf = arrbuf.tostring() + + # Write to buffer + fb_dev.buffer[0:len(buf)] = buf + diff --git a/src/scripts/libg15test b/src/scripts/libg15test new file mode 100755 index 0000000..ca12b53 --- /dev/null +++ b/src/scripts/libg15test @@ -0,0 +1,86 @@ +#!/usr/bin/env python2 + +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2012 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Utility to test the functioning of libg15 +""" + +import gnome15.drivers.pylibg15 as libg15 +import gnome15.g15devices as g15devices +import sys + +def clamp(val, min_val, max_val): + return max(min_val, min(val, max_val)) + +print 'libg15 test\n' +print 'Choose a device:' +mono_devices = [] +for idx, device in enumerate(g15devices.find_all_devices()): + if device.bpp == 1: + mono_devices.append(device) + print ' %d %-10s - %-40s (%04x %04x)' % (idx, device.uid, device.model_fullname, device.usb_id[0],device.usb_id[1] ) + +device_idx = int(raw_input('\nDevice:')) +if device_idx < 0 or device_idx >= len(mono_devices): + sys.stderr.write('Invalid device number\n') + sys.exit(1) +device = mono_devices[device_idx] + +print "WARNING: Reset sometimes doesn't work, try without it first" +reset_usb = raw_input('Reset USB Y/(N):').lower()[:1] == 'y' + +libg15.set_debug(libg15.G15_LOG_INFO) +libg15.init(reset_usb, device.usb_id[0], device.usb_id[1]) + +print "Intialised\n" +print "WARNING: Some operations may not be appropriate for your hardware. " +print "Do try to use options your device does not have. This script is a" +print "bit dumb and won't try to stop you." + +while True: + print + print "1 - Set the keyboard backlight brightness (G15,G11)" + print "2 - Set the LCD brightness (G15,Z10?)" + print "3 - Set the LCD contrast (G15,Z10?)" + print "4 - Set the keyboard backlight color (G13,G510)" + print "5 - Set the M-Key LEDs" + print "6 - Test extra keys" + print "0 - Exit" + option = int(raw_input('\nDevice:')) + if option == 0: + break + elif option == 1: + libg15.set_keyboard_brightness(clamp(int(raw_input('Backlight brightness (0-2):')), 0, 2)) + elif option == 2: + libg15.set_lcd_brightness(clamp(int(raw_input('LCD brightness (0-2):')), 0, 2)) + elif option == 3: + libg15.set_contrast(clamp(int(raw_input('LCD contrast (0-2):')), 0, 2)) + elif option == 4: + libg15.set_keyboard_color((clamp(int(raw_input('Red (0-255):')), 0, 255), \ + clamp(int(raw_input('Green (0-255):')), 0, 255), \ + clamp(int(raw_input('Blue (0-255):')), 0, 255))) + elif option == 5: + libg15.set_leds(clamp(int(raw_input('LED mask (0-15):')), 0, 15)) + elif option == 6: + print "******************************************************************" + print "* Now testing keys, abort with Ctrl+\\" + print "******************************************************************" + def callback(code, extended_code): + print "%04x/%04x - %08d/%08d"% (code, extended_code, code, extended_code) + libg15.grab_keyboard(callback).join() + \ No newline at end of file

  • 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/devices/g19.png b/data/icons/hicolor/scalable/devices/g19.png new file mode 100644 index 0000000000000000000000000000000000000000..911e7165949a68022d1d45c6213ddf98de4c550b GIT binary patch literal 63693 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%0DgBSZ40h7cFrKfV)HXB~MuWUcxrBStlb2s$|a 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 W1)q2AozebxB~4X5l`18xu>S#Z$R*zZ literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/devices/g35.png b/data/icons/hicolor/scalable/devices/g35.png new file mode 100644 index 0000000000000000000000000000000000000000..cc073c2145990f67ff169c6876790cb02561cafe GIT binary patch literal 44488 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/scalable/devices/g510.png b/data/icons/hicolor/scalable/devices/g510.png new file mode 100644 index 0000000000000000000000000000000000000000..5361267a761cecfeb180d6a62593b75acae0d13d GIT binary patch literal 81868 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