summaryrefslogtreecommitdiff
path: root/timer-applet/src
diff options
context:
space:
mode:
authorStefano Karapetsas <[email protected]>2012-06-12 17:49:56 +0200
committerStefano Karapetsas <[email protected]>2012-06-12 17:49:56 +0200
commit4bbd16182dab69da9ca7ad13309962af40529469 (patch)
treec77202017ea3739f137a86f817bdb2ecef381f80 /timer-applet/src
parentc9b9454068f090a7132c6bea6ac07c91843ffb6e (diff)
downloadmate-applets-4bbd16182dab69da9ca7ad13309962af40529469.tar.bz2
mate-applets-4bbd16182dab69da9ca7ad13309962af40529469.tar.xz
add timer-applet
Diffstat (limited to 'timer-applet/src')
-rw-r--r--timer-applet/src/Makefile.am6
-rwxr-xr-xtimer-applet/src/timer-applet136
-rw-r--r--timer-applet/src/timerapplet/Makefile.am27
-rw-r--r--timer-applet/src/timerapplet/__init__.py1
-rw-r--r--timer-applet/src/timerapplet/config.py39
-rw-r--r--timer-applet/src/timerapplet/controllers/GlobalController.py91
-rw-r--r--timer-applet/src/timerapplet/controllers/Makefile.am8
-rw-r--r--timer-applet/src/timerapplet/controllers/TimerApplet.py635
-rw-r--r--timer-applet/src/timerapplet/controllers/TimerManagerService.py42
-rw-r--r--timer-applet/src/timerapplet/controllers/TimerService.py49
-rw-r--r--timer-applet/src/timerapplet/controllers/__init__.py20
-rw-r--r--timer-applet/src/timerapplet/core/AppletMateConfWrapper.py89
-rw-r--r--timer-applet/src/timerapplet/core/Makefile.am6
-rw-r--r--timer-applet/src/timerapplet/core/PresetsStore.py160
-rw-r--r--timer-applet/src/timerapplet/core/Timer.py175
-rw-r--r--timer-applet/src/timerapplet/core/__init__.py19
-rw-r--r--timer-applet/src/timerapplet/defs.py.in7
-rw-r--r--timer-applet/src/timerapplet/logger.py23
-rw-r--r--timer-applet/src/timerapplet/ui/AddEditPresetDialog.py75
-rw-r--r--timer-applet/src/timerapplet/ui/ContinueTimerDialog.py63
-rw-r--r--timer-applet/src/timerapplet/ui/DurationChooser.py166
-rw-r--r--timer-applet/src/timerapplet/ui/Makefile.am15
-rw-r--r--timer-applet/src/timerapplet/ui/ManagePresetsDialog.py86
-rw-r--r--timer-applet/src/timerapplet/ui/Notifier.py73
-rw-r--r--timer-applet/src/timerapplet/ui/PieMeter.py77
-rw-r--r--timer-applet/src/timerapplet/ui/PreferencesDialog.py192
-rw-r--r--timer-applet/src/timerapplet/ui/PulseButton.py69
-rw-r--r--timer-applet/src/timerapplet/ui/ScrollableButtonList.py65
-rw-r--r--timer-applet/src/timerapplet/ui/StartNextTimerDialog.py66
-rw-r--r--timer-applet/src/timerapplet/ui/StartTimerDialog.py267
-rw-r--r--timer-applet/src/timerapplet/ui/StatusButton.py101
-rw-r--r--timer-applet/src/timerapplet/ui/__init__.py28
-rw-r--r--timer-applet/src/timerapplet/utils.py79
33 files changed, 2955 insertions, 0 deletions
diff --git a/timer-applet/src/Makefile.am b/timer-applet/src/Makefile.am
new file mode 100644
index 00000000..d266f640
--- /dev/null
+++ b/timer-applet/src/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = timerapplet
+
+libexec_SCRIPTS = timer-applet
+
+EXTRA_DIST = \
+ timer-applet
diff --git a/timer-applet/src/timer-applet b/timer-applet/src/timer-applet
new file mode 100755
index 00000000..cd550fbb
--- /dev/null
+++ b/timer-applet/src/timer-applet
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 os import path
+import gettext
+import locale
+import sys
+import gtk
+import mateapplet
+from timerapplet import config
+from timerapplet.controllers import GlobalController, TimerApplet, TimerService, TimerManagerService
+from timerapplet.core import AppletMateConfWrapper, Timer
+
+from dbus.mainloop.glib import DBusGMainLoop
+DBusGMainLoop(set_as_default=True)
+
+DBUS_BUS_NAME = 'net.launchpad.timerapplet.TimerApplet'
+DBUS_OBJ_NAMESPACE = '/net/launchpad/timerapplet/TimerApplet'
+
+gettext.bindtextdomain(config.GETTEXT_PACKAGE, config.LOCALE_DIR)
+gettext.bind_textdomain_codeset(config.GETTEXT_PACKAGE, 'UTF-8')
+gettext.textdomain(config.GETTEXT_PACKAGE)
+locale.bindtextdomain(config.GETTEXT_PACKAGE, config.LOCALE_DIR)
+locale.bind_textdomain_codeset(config.GETTEXT_PACKAGE, 'UTF-8')
+locale.textdomain(config.GETTEXT_PACKAGE)
+
+global_controller = GlobalController()
+timer_manager_obj_path = path.join(DBUS_OBJ_NAMESPACE, 'TimerManager')
+print 'Timer Manager D-Bus object path: %s' % timer_manager_obj_path
+
+timer_manager = None
+try:
+ timer_manager = TimerManagerService(DBUS_BUS_NAME, timer_manager_obj_path)
+except Exception, err:
+ print 'ERROR: Could not start TimerManagerService. D-Bus support will not be available. Error message: %s' % err
+
+def check_dependencies():
+ # Check for optional dependencies
+ try:
+ import dbus # >= 0.80
+ except ImportError, err:
+ print 'Missing optional dependency: %s' % err
+
+ # Check for required dependencies
+ try:
+ import gobject # >= 2.12
+ import gtk # >= 2.10, includes pango
+ import gtk.glade # >= 2.10
+ import mateconf # >= 2.18
+ import mate # >= 2.18, includes matecomponent.ui
+ import mateapplet # >= 2.18, included in python-mate2-desktop
+ import pynotify # >= 0.1.1
+ except ImportError, err:
+ dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ buttons=gtk.BUTTONS_CLOSE,
+ message_format='%s\n\nPlease install required dependencies.' % err)
+ dialog.run()
+ dialog.destroy()
+ sys.exit(1)
+
+def get_timer_id(mateconf_wrapper):
+ path_components = mateconf_wrapper.get_base_path().split('/')
+
+ # Use the second component from the end, which should usually be 'applet_*',
+ # where '*' is some integer assigned by the system.
+ # It could also be 'timer-applet' if we're running in standalone mode.
+ # D-Bus doesn't like hyphens in object paths, so we have to replace them
+ # with underscores.
+ return path_components[-2].replace('-', '_')
+
+def applet_factory(applet, iid):
+ check_dependencies()
+
+ timer = Timer()
+ mateconf_wrapper = AppletMateConfWrapper(applet,
+ '/schemas/apps/timer-applet/prefs',
+ '/apps/timer-applet/prefs')
+ timer_id = get_timer_id(mateconf_wrapper)
+ print 'Timer ID: %s' % timer_id
+
+ if timer_manager is not None:
+ timer_manager.register_timer_id(timer_id)
+ applet.connect('destroy', lambda sender: timer_manager.unregister_timer_id(timer_id))
+
+ TimerApplet(global_controller.get_presets_store(),
+ global_controller.get_manage_presets_dialog(),
+ applet,
+ timer,
+ mateconf_wrapper)
+
+ timer_obj_path = path.join(DBUS_OBJ_NAMESPACE, 'Timers', timer_id)
+ print 'Timer D-Bus object path: %s' % timer_obj_path
+
+ try:
+ TimerService(DBUS_BUS_NAME, timer_obj_path, timer)
+ except Exception, err:
+ print 'ERROR: Could not start TimerService. D-Bus support will not be available. Error message: %s' % err
+
+ return True
+
+if __name__ == '__main__':
+ windowed_mode = (len(sys.argv) > 1 and sys.argv[1] == '-w')
+
+ if windowed_mode:
+ win = gtk.Window()
+ win.set_title('Timer Applet')
+ applet = mateapplet.Applet()
+ applet_factory(applet, None)
+ applet.reparent(win)
+
+ applet.connect('destroy', gtk.main_quit)
+ win.show()
+
+ gtk.main()
+ else:
+ mateapplet.matecomponent_factory(
+ 'OAFIID:TimerApplet_Factory',
+ mateapplet.Applet.__gtype__,
+ config.PACKAGE,
+ config.VERSION,
+ applet_factory)
diff --git a/timer-applet/src/timerapplet/Makefile.am b/timer-applet/src/timerapplet/Makefile.am
new file mode 100644
index 00000000..08f474c0
--- /dev/null
+++ b/timer-applet/src/timerapplet/Makefile.am
@@ -0,0 +1,27 @@
+SUBDIRS = controllers core ui
+
+defs.py: defs.py.in Makefile
+ sed -e "s|\@PACKAGE\@|$(PACKAGE)|" \
+ -e "s|\@VERSION\@|$(VERSION)|" \
+ -e "s|\@RESOURCESDIR\@|$(pkgdatadir)|" \
+ -e "s|\@IMAGESDIR\@|$(datadir)/pixmaps|" \
+ -e "s|\@LOCALEDIR\@|$(localedir)|" \
+ -e "s|\@GETTEXT_PACKAGE\@|$(GETTEXT_PACKAGE)|" $< > $@
+
+EXTRA_DIST = \
+ defs.py.in
+
+CLEANFILES = \
+ defs.py
+
+# Need this so that defs.py is actually created after cleaning.
+BUILT_SOURCES = defs.py
+
+moduledir = $(pythondir)/timerapplet
+module_PYTHON = \
+ __init__.py \
+ config.py \
+ defs.py \
+ utils.py \
+ logger.py
+
diff --git a/timer-applet/src/timerapplet/__init__.py b/timer-applet/src/timerapplet/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/timer-applet/src/timerapplet/__init__.py
@@ -0,0 +1 @@
+
diff --git a/timer-applet/src/timerapplet/config.py b/timer-applet/src/timerapplet/config.py
new file mode 100644
index 00000000..ca8a4f9d
--- /dev/null
+++ b/timer-applet/src/timerapplet/config.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import os.path as path
+
+try:
+ from defs import *
+except ImportError:
+ PACKAGE = 'timer-applet'
+ VERSION = '0'
+ GETTEXT_PACKAGE = ''
+ LOCALE_DIR = ''
+ RESOURCES_DIR = path.join(path.dirname(__file__), '../../data')
+ IMAGES_DIR = path.join(path.dirname(__file__), '../../images')
+
+print 'Using these definitions:'
+print 'GETTEXT_PACKAGE: %s' % GETTEXT_PACKAGE
+print 'LOCALE_DIR: %s' % LOCALE_DIR
+print 'RESOURCES_DIR: %s' % RESOURCES_DIR
+print 'IMAGES_DIR: %s' % IMAGES_DIR
+
+GLADE_PATH = path.join(RESOURCES_DIR, 'timer-applet.glade')
+POPUP_MENU_FILE_PATH = path.join(RESOURCES_DIR, 'TimerApplet.xml')
+ICON_PATH = path.join(IMAGES_DIR, 'timer-applet.png')
+PRESETS_PATH = path.expanduser('~/.config/mate/timer-applet/presets.xml')
+DEFAULT_SOUND_PATH = '/usr/share/sounds/gtk-events/clicked.wav'
diff --git a/timer-applet/src/timerapplet/controllers/GlobalController.py b/timer-applet/src/timerapplet/controllers/GlobalController.py
new file mode 100644
index 00000000..860573a6
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/GlobalController.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 gettext import gettext as _
+import gtk
+from timerapplet import core
+from timerapplet import ui
+from timerapplet import utils
+from timerapplet import config
+
+class GlobalController(object):
+ def __init__(self):
+ self._presets_store = core.PresetsStore(config.PRESETS_PATH)
+ self._manage_presets_dialog = ui.ManagePresetsDialog(config.GLADE_PATH,
+ self._presets_store.get_model(),
+ lambda row_iter: utils.get_preset_display_text(self._presets_store,
+ row_iter))
+
+ self._manage_presets_dialog.connect('clicked-add', self._on_mgr_clicked_add)
+ self._manage_presets_dialog.connect('clicked-edit', self._on_mgr_clicked_edit)
+ self._manage_presets_dialog.connect('clicked-remove', self._on_mgr_clicked_remove)
+
+ gtk.window_set_default_icon_from_file(config.ICON_PATH)
+
+ def get_presets_store(self):
+ return self._presets_store
+
+ def get_manage_presets_dialog(self):
+ return self._manage_presets_dialog
+
+ def _on_mgr_clicked_add(self, sender, data=None):
+ add_dialog = ui.AddEditPresetDialog(
+ config.GLADE_PATH,
+ _('Add Preset'),
+ lambda name: utils.is_valid_preset_name(name, self._presets_store))
+
+ result = add_dialog.get_preset()
+ if result is not None:
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = result
+ self._presets_store.add_preset(name, hours, minutes, seconds,
+ command, next_timer, auto_start)
+
+ def _on_mgr_clicked_edit(self, sender, row_path, data=None):
+ row_iter = self._presets_store.get_model().get_iter(row_path)
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ self._presets_store.get_preset(row_iter)
+
+ edit_dialog = ui.AddEditPresetDialog(config.GLADE_PATH,
+ _('Edit Preset'),
+ lambda name: utils.is_valid_preset_name(name,
+ self._presets_store,
+ (name,)),
+ name,
+ hours,
+ minutes,
+ seconds,
+ command,
+ next_timer,
+ auto_start
+ )
+
+ result = edit_dialog.get_preset()
+ if result is not None:
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = result
+ self._presets_store.modify_preset(row_iter, name, hours, minutes,
+ seconds, command, next_timer,
+ auto_start)
+
+ def _on_mgr_clicked_remove(self, sender, row_path, data=None):
+ row_iter = self._presets_store.get_model().get_iter(row_path)
+ self._presets_store.remove_preset(row_iter)
+
+ # TODO
+ def _on_mgr_next_timer_is_being_edited(self, sender, row_path, data=None):
+ """Show a dropdown widget to help completing the next timer."""
+ raise NotImplementedError("Not implemented, yet")
+
+
diff --git a/timer-applet/src/timerapplet/controllers/Makefile.am b/timer-applet/src/timerapplet/controllers/Makefile.am
new file mode 100644
index 00000000..bb5018d6
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/Makefile.am
@@ -0,0 +1,8 @@
+moduledir = $(pythondir)/timerapplet/controllers
+module_PYTHON = \
+ __init__.py \
+ GlobalController.py \
+ TimerApplet.py \
+ TimerManagerService.py \
+ TimerService.py
+
diff --git a/timer-applet/src/timerapplet/controllers/TimerApplet.py b/timer-applet/src/timerapplet/controllers/TimerApplet.py
new file mode 100644
index 00000000..3c53754a
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/TimerApplet.py
@@ -0,0 +1,635 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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 gettext import gettext as _
+from gettext import ngettext
+from datetime import datetime, timedelta
+import mateapplet
+import gst
+import gtk
+import gtk.glade as glade
+import gtk.gdk as gdk
+import subprocess
+import shlex
+import threading
+from timerapplet import config
+from timerapplet import core
+from timerapplet import ui
+from timerapplet import utils
+
+def on_widget_button_press_event(sender, event, data=None):
+ if event.button != 1:
+ sender.emit_stop_by_name('button-press-event')
+ return False
+
+def force_no_focus_padding(widget):
+ gtk.rc_parse_string('\n'
+ ' style "timer-applet-button-style"\n'
+ ' {\n'
+ ' GtkWidget::focus-line-width=0\n'
+ ' GtkWidget::focus-padding=0\n'
+ ' }\n'
+ '\n'
+ ' widget "*.timer-applet-button" style "timer-applet-button-style"\n'
+ '\n')
+ widget.set_name('timer-applet-button')
+
+class TimerApplet(object):
+ # MateConf key identifiers
+ ## You can find Timer Applet's schemas file in data/timer-applet.schemas.in
+ _SHOW_REMAINING_TIME_KEY = 'show_remaining_time'
+ _PLAY_SOUND_KEY = 'play_notification_sound'
+ _USE_CUSTOM_SOUND_KEY = 'use_custom_notification_sound'
+ _SHOW_POPUP_NOTIFICATION_KEY = 'show_popup_notification'
+ _SHOW_PULSING_ICON_KEY = 'show_pulsing_icon'
+ _CUSTOM_SOUND_PATH_KEY = 'custom_notification_sound_path'
+
+ _PRESETS_PLACEHOLDER_NAME = 'Placeholder'
+ _PRESETS_PLACEHOLDER_PATH = '/popups/popup/Presets/' + _PRESETS_PLACEHOLDER_NAME
+ _PRESETS_PATH = '/popups/popup/Presets'
+
+ def __init__(self, presets_store, manage_presets_dialog, applet, timer, mateconf_wrapper):
+ self._presets_store = presets_store
+ self._manage_presets_dialog = manage_presets_dialog
+ self._applet = applet
+ self._timer = timer
+
+ self._gst_playbin = gst.element_factory_make('playbin', 'player')
+ def bus_event(bus, message):
+ t = message.type
+ if t == gst.MESSAGE_EOS:
+ self._gst_playbin.set_state(gst.STATE_NULL)
+ elif t == gst.MESSAGE_ERROR:
+ self._gst_playbin.set_state(gst.STATE_NULL)
+ err, debug = message.parse_error()
+ print 'Error playing sound: %s' % err, debug
+ return True
+ self._gst_playbin.get_bus().add_watch(bus_event)
+
+ self._status_button = ui.StatusButton()
+ self._notifier = ui.Notifier('TimerApplet', gtk.STOCK_DIALOG_INFO, self._status_button)
+ self._start_next_timer_dialog = ui.StartNextTimerDialog(
+ config.GLADE_PATH,
+ "Start next timer",
+ "Would you like to start the next timer?")
+ self._start_timer_dialog = ui.StartTimerDialog(config.GLADE_PATH,
+ lambda name: utils.is_valid_preset_name(name,
+ self._presets_store),
+ self._presets_store.get_model(),
+ lambda row_iter: utils.get_preset_display_text(self._presets_store,
+ row_iter))
+ self._continue_dialog = ui.ContinueTimerDialog(config.GLADE_PATH,
+ _('Continue timer countdown?'),
+ _('The timer is currently paused. Would you like to continue countdown?'))
+ self._preferences_dialog = ui.PreferencesDialog(config.GLADE_PATH)
+ self._mateconf = mateconf_wrapper
+
+ self._about_dialog = glade.XML(config.GLADE_PATH, 'about_dialog').get_widget('about_dialog')
+ self._about_dialog.set_version(config.VERSION)
+
+ self._applet.set_applet_flags(mateapplet.EXPAND_MINOR)
+ self._applet.setup_menu_from_file(
+ None,
+ config.POPUP_MENU_FILE_PATH,
+ None,
+ [('PauseTimer', lambda component, verb: self._timer.stop()),
+ ('ContinueTimer', lambda component, verb: self._timer.start()),
+ ('StopTimer', lambda component, verb: self._timer.reset()),
+ ('RestartTimer', lambda component, verb: self._restart_timer()),
+ ('StartNextTimer', lambda component, verb: self._start_next_timer()),
+ ('ManagePresets', lambda component, verb: self._manage_presets_dialog.show()),
+ ('Preferences', lambda component,
+ verb: self._preferences_dialog.show()),
+ ('About', lambda component, verb: self._about_dialog.show())]
+ )
+ self._applet.add(self._status_button)
+
+ # Remove padding around button contents.
+ force_no_focus_padding(self._status_button)
+
+ # TODO:
+ # Fix bug in which button would not propogate middle-clicks
+ # and right-clicks to the applet.
+ self._status_button.connect('button-press-event', on_widget_button_press_event)
+
+ self._status_button.set_relief(gtk.RELIEF_NONE)
+ self._status_button.set_icon(config.ICON_PATH);
+
+ self._connect_signals()
+ self._update_status_button()
+ self._update_popup_menu()
+ self._update_preferences_dialog()
+ self._status_button.show()
+ self._applet.show()
+
+ def _connect_signals(self):
+ self._applet.connect('change-orient', lambda applet, orientation: self._update_status_button())
+ self._applet.connect('change-size', lambda applet, size: self._update_status_button())
+ self._applet.connect('change-background', self._on_applet_change_background)
+ self._applet.connect('destroy', self._on_applet_destroy)
+
+ self._presets_store.get_model().connect('row-deleted',
+ lambda model,
+ row_path: self._update_popup_menu())
+ self._presets_store.get_model().connect('row-changed',
+ lambda model,
+ row_path,
+ row_iter: self._update_popup_menu())
+
+ self._timer.connect('time-changed', self._on_timer_time_changed)
+ self._timer.connect('state-changed', self._on_timer_state_changed)
+ self._status_button.connect('clicked', self._on_status_button_clicked)
+ self._start_timer_dialog.connect('clicked-start',
+ self._on_start_dialog_clicked_start)
+ self._start_timer_dialog.connect('clicked-manage-presets',
+ self._on_start_dialog_clicked_manage_presets)
+ self._start_timer_dialog.connect('clicked-save',
+ self._on_start_dialog_clicked_save)
+ self._start_timer_dialog.connect('clicked-preset',
+ self._on_start_dialog_clicked_preset)
+ self._start_timer_dialog.connect('double-clicked-preset',
+ self._on_start_dialog_double_clicked_preset)
+
+ self._preferences_dialog.connect('show-remaining-time-changed', self._on_prefs_show_time_changed)
+ self._preferences_dialog.connect('play-sound-changed', self._on_prefs_play_sound_changed)
+ self._preferences_dialog.connect('use-custom-sound-changed', self._on_prefs_use_custom_sound_changed)
+ self._preferences_dialog.connect('show-popup-notification-changed', self._on_prefs_show_popup_notification_changed)
+ self._preferences_dialog.connect('show-pulsing-icon-changed', self._on_prefs_show_pulsing_icon_changed)
+ self._preferences_dialog.connect('custom-sound-path-changed', self._on_prefs_custom_sound_path_changed)
+
+ self._about_dialog.connect('delete-event', gtk.Widget.hide_on_delete)
+ self._about_dialog.connect('response', lambda dialog, response_id: self._about_dialog.hide())
+
+ self._mateconf.add_notification(TimerApplet._SHOW_REMAINING_TIME_KEY, self._on_mateconf_changed)
+ self._mateconf.add_notification(TimerApplet._PLAY_SOUND_KEY, self._on_mateconf_changed)
+ self._mateconf.add_notification(TimerApplet._USE_CUSTOM_SOUND_KEY, self._on_mateconf_changed)
+ self._mateconf.add_notification(TimerApplet._SHOW_PULSING_ICON_KEY, self._on_mateconf_changed)
+ self._mateconf.add_notification(TimerApplet._SHOW_POPUP_NOTIFICATION_KEY, self._on_mateconf_changed)
+ self._mateconf.add_notification(TimerApplet._CUSTOM_SOUND_PATH_KEY, self._on_mateconf_changed)
+
+ ## Private methods for updating UI ##
+
+ def _update_status_button(self):
+ current_state = self._timer.get_state()
+ if current_state == core.Timer.STATE_IDLE:
+ print 'Idle'
+ # This label text should not be visible because the label
+ # is hidden when the timer is idle.
+ self._status_button.set_label('--:--:--')
+ self._status_button.set_tooltip(_('Click to start a new timer countdown.'))
+ elif current_state == core.Timer.STATE_RUNNING:
+ print 'Running'
+ elif current_state == core.Timer.STATE_PAUSED:
+ print 'Paused'
+ self._status_button.set_tooltip(_('Paused. Click to continue timer countdown.'))
+ elif current_state == core.Timer.STATE_FINISHED:
+ print 'Finished'
+ self._status_button.set_label(_('Finished'))
+ name_str = self._timer.get_name()
+ time_str = utils.get_display_text_from_datetime(self._timer.get_end_time())
+ if len(name_str) > 0:
+ # "<timer name>" finished at <time>
+ self._status_button.set_tooltip(_('"%s" finished at %s.\nClick to stop timer.') % (name_str, time_str))
+ else:
+ # Timer finished at <time>
+ self._status_button.set_tooltip(_('Timer finished at %s.\nClick to stop timer.') % time_str)
+
+ self._status_button.set_sensitized(current_state == core.Timer.STATE_RUNNING or
+ current_state == core.Timer.STATE_FINISHED)
+ self._status_button.set_use_icon(current_state == core.Timer.STATE_IDLE)
+ self._status_button.set_show_remaining_time(current_state != core.Timer.STATE_IDLE and
+ self._mateconf.get_bool(TimerApplet._SHOW_REMAINING_TIME_KEY))
+
+ if current_state == core.Timer.STATE_PAUSED:
+ self._status_button.set_pie_fill_color(0.4, 0.4, 0.4)
+ else:
+ # Use theme color
+ color = self._applet.style.base[gtk.STATE_SELECTED]
+ red = color.red / 65535.0
+ green = color.green / 65535.0
+ blue = color.blue / 65535.0
+ self._status_button.set_pie_fill_color(red, green, blue)
+
+ orientation = self._applet.get_orient()
+ size = self._applet.get_size()
+ use_vertical = (orientation == mateapplet.ORIENT_LEFT or
+ orientation == mateapplet.ORIENT_RIGHT or
+ size >= mateapplet.SIZE_MEDIUM)
+ self._status_button.set_use_vertical_layout(use_vertical)
+
+ def _update_popup_menu(self):
+ popup = self._applet.get_popup_component()
+
+ timer_state = self._timer.get_state()
+ has_next_timer = self._timer.get_next_timer()
+ show_pause = (timer_state == core.Timer.STATE_RUNNING)
+ show_continue = (timer_state == core.Timer.STATE_PAUSED)
+ show_stop = (timer_state == core.Timer.STATE_RUNNING or
+ timer_state == core.Timer.STATE_PAUSED or
+ timer_state == core.Timer.STATE_FINISHED)
+ show_restart = (timer_state == core.Timer.STATE_RUNNING or
+ timer_state == core.Timer.STATE_PAUSED or
+ timer_state == core.Timer.STATE_FINISHED)
+ show_next_timer = ((timer_state == core.Timer.STATE_RUNNING or
+ timer_state == core.Timer.STATE_PAUSED or
+ timer_state == core.Timer.STATE_FINISHED) and
+ # Only show this popup menu item if it has a
+ # next_timer defined. Clever, huh? ;)
+ has_next_timer)
+
+ show_presets_menu = (len(self._presets_store.get_model()) > 0)
+ show_separator = (
+ show_presets_menu or
+ show_pause or
+ show_next_timer or
+ show_continue or
+ show_stop or
+ show_restart)
+
+ to_hidden_str = lambda show: ('0', '1')[not show]
+ popup.set_prop('/commands/PauseTimer', 'hidden', to_hidden_str(show_pause))
+ popup.set_prop('/commands/ContinueTimer', 'hidden', to_hidden_str(show_continue))
+ popup.set_prop('/commands/StopTimer', 'hidden', to_hidden_str(show_stop))
+ popup.set_prop('/commands/RestartTimer', 'hidden', to_hidden_str(show_restart))
+ popup.set_prop('/commands/StartNextTimer', 'hidden', to_hidden_str(show_next_timer))
+ popup.set_prop(TimerApplet._PRESETS_PATH, 'hidden', to_hidden_str(show_presets_menu))
+ popup.set_prop('/popups/popup/Separator1', 'hidden', to_hidden_str(show_separator))
+
+ # Rebuild the Presets submenu
+ if popup.path_exists(TimerApplet._PRESETS_PLACEHOLDER_PATH):
+ popup.rm(TimerApplet._PRESETS_PLACEHOLDER_PATH)
+ popup.set_translate(TimerApplet._PRESETS_PATH,
+ '<placeholder name="%s"/>' % TimerApplet._PRESETS_PLACEHOLDER_NAME)
+
+ preset_number = 1
+ row_iter = self._presets_store.get_model().get_iter_first()
+ while row_iter is not None:
+ verb = ('Preset_%d' % preset_number)
+ preset_number += 1
+ display_text = utils.get_preset_display_text(self._presets_store, row_iter)
+ node_xml = '<menuitem verb="%s" name="%s" label="%s"/>' % (verb, verb, display_text)
+ popup.set_translate(TimerApplet._PRESETS_PLACEHOLDER_PATH, node_xml)
+ popup.add_verb(verb,
+ self._on_presets_submenu_item_activated,
+ self._presets_store.get_model().get_path(row_iter))
+ row_iter = self._presets_store.get_model().iter_next(row_iter)
+
+ def _update_preferences_dialog(self):
+ self._preferences_dialog.props.show_remaining_time = \
+ self._mateconf.get_bool(TimerApplet._SHOW_REMAINING_TIME_KEY)
+ self._preferences_dialog.props.play_sound = \
+ self._mateconf.get_bool(TimerApplet._PLAY_SOUND_KEY)
+ self._preferences_dialog.props.use_custom_sound = \
+ self._mateconf.get_bool(TimerApplet._USE_CUSTOM_SOUND_KEY)
+ self._preferences_dialog.props.show_popup_notification = \
+ self._mateconf.get_bool(TimerApplet._SHOW_POPUP_NOTIFICATION_KEY)
+ self._preferences_dialog.props.show_pulsing_icon = \
+ self._mateconf.get_bool(TimerApplet._SHOW_PULSING_ICON_KEY)
+ self._preferences_dialog.props.custom_sound_path = \
+ self._mateconf.get_string(TimerApplet._CUSTOM_SOUND_PATH_KEY)
+
+ ## Applet callbacks ##
+
+ def _on_applet_change_background(self, applet, background_type, color, pixmap):
+ applet.set_style(None)
+ rc_style = gtk.RcStyle()
+ applet.modify_style(rc_style)
+
+ if background_type == mateapplet.NO_BACKGROUND:
+ pass
+ elif background_type == mateapplet.COLOR_BACKGROUND:
+ applet.modify_bg(gtk.STATE_NORMAL, color)
+ elif background_type == mateapplet.PIXMAP_BACKGROUND:
+ style = applet.style.copy()
+ style.bg_pixmap[gtk.STATE_NORMAL] = pixmap
+ applet.set_style(style)
+
+ def _on_applet_destroy(self, sender, data=None):
+ self._call_notify(show=False)
+ if self._timer.get_state() != core.Timer.STATE_IDLE:
+ self._timer.reset() # will stop timeout
+ self._mateconf.delete()
+
+ ## Popup menu callbacks ##
+
+ def _on_presets_submenu_item_activated(self, component, verb, row_path):
+ # Try hiding the Start Timer dialog, just in case it's open.
+ self._start_timer_dialog.hide()
+ row_iter = self._presets_store.get_model().get_iter(row_path)
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = self._presets_store.get_preset(row_iter)
+ self._start_timer_with_settings(name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+
+ ## MateConf callbacks ##
+
+ def _on_mateconf_changed(self, mateconf_value, data=None):
+ self._update_status_button()
+ self._update_preferences_dialog()
+
+ ## PreferencesDialog callbacks ##
+
+ def _on_prefs_show_time_changed(self, sender, show_time):
+ self._mateconf.set_bool(TimerApplet._SHOW_REMAINING_TIME_KEY,
+ show_time)
+
+ def _on_prefs_play_sound_changed(self, sender, play_sound):
+ self._mateconf.set_bool(TimerApplet._PLAY_SOUND_KEY,
+ play_sound)
+
+ def _on_prefs_use_custom_sound_changed(self, sender, use_custom_sound):
+ self._mateconf.set_bool(TimerApplet._USE_CUSTOM_SOUND_KEY,
+ use_custom_sound)
+
+ def _on_prefs_show_pulsing_icon_changed(self, sender, show_pulsing_icon):
+ self._mateconf.set_bool(TimerApplet._SHOW_PULSING_ICON_KEY,
+ show_pulsing_icon)
+
+ def _on_prefs_show_popup_notification_changed(self, sender,
+ show_popup_notification):
+ self._mateconf.set_bool(TimerApplet._SHOW_POPUP_NOTIFICATION_KEY,
+ show_popup_notification)
+
+ def _on_prefs_custom_sound_path_changed(self, sender, custom_sound_path):
+ self._mateconf.set_string(TimerApplet._CUSTOM_SOUND_PATH_KEY,
+ custom_sound_path)
+
+ ## Timer callbacks ##
+
+ def _on_timer_time_changed(self, timer):
+ hours, minutes, seconds = utils.seconds_to_hms(timer.get_remaining_time())
+ print 'Remaining time: %d, %d, %d' % (hours, minutes, seconds)
+ name = self._timer.get_name()
+ self._status_button.set_label(utils.construct_time_str(self._timer.get_remaining_time(),
+ show_all=False))
+
+ fraction_remaining = float(self._timer.get_remaining_time()) / self._timer.get_duration()
+ progress = min(1.0, max(0.0, 1.0 - fraction_remaining))
+ self._status_button.set_progress(progress)
+
+ if len(name) > 0:
+ # HH:MM:SS (<timer name>)
+ self._status_button.set_tooltip(_('%02d:%02d:%02d (%s)') % (hours, minutes, seconds, name))
+ else:
+ # HH:MM:SS
+ self._status_button.set_tooltip(_('%02d:%02d:%02d') % (hours, minutes, seconds))
+
+ def _on_timer_state_changed(self, timer, data=None):
+ # TODO:
+ # Refactor me!
+ print 'State changed'
+ new_state = timer.get_state()
+ print ' new state: %d' % new_state
+
+ # These actions should be done once upon a state change.
+ # That's why they're done here and not in self._update_status_button();
+ # self._update_status_button() could be called multiple times
+ # while in the same state.
+ if new_state == core.Timer.STATE_FINISHED:
+ name = self._timer.get_name()
+ command = self._timer.get_command()
+ end_time = self._timer.get_end_time()
+ time_text = utils.get_display_text_from_datetime(end_time)
+ summary = None
+ message = None
+ if len(name) > 0:
+ # "<timer name>" Finished
+ summary = (_('"%s" Finished') % name)
+
+ # "<timer name>" finished at <time>
+ message = (_('"%s" finished at %s') % (name, time_text))
+ else:
+ summary = _('Timer Finished')
+
+ # Timer finished at <time>
+ message = (_('Timer finished at %s') % time_text)
+
+
+ def reminder_message_func():
+ elapsed_time = datetime.now() - end_time
+ message = None
+ if elapsed_time < timedelta(seconds=60):
+ message = ngettext('Timer finished about <b>%d second</b> ago',
+ 'Timer finished about <b>%d seconds</b> ago',
+ elapsed_time.seconds) % elapsed_time.seconds
+ else:
+ minutes = elapsed_time.seconds / 60
+ message = ngettext('Timer finished about <b>%d minute</b> ago',
+ 'Timer finished about <b>%d minutes</b> ago',
+ minutes) % minutes
+ return message
+
+ # TODO:
+ # FIXME:
+ # Reason for using a Python thread:
+ # To do all the procedures after timer has ended. If I don't do
+ # this then after the timer ended and it had an auto-start and next
+ # timer defined, it would directly switch without any notification.
+ # Trying time.sleep() doesn't work as expected; it correctly starts
+ # the next timer, but it doesn't show the notification and the
+ # rest.
+ class MyThread(threading.Thread):
+ def __init__(self, timer_instance):
+ threading.Thread.__init__(self)
+ self.timer = timer_instance
+
+ def run(self):
+ print "Starting thread..."
+ print "Calling popup notification.",
+ self.timer._call_notify(summary, message, reminder_message_func)
+ print "Starting pulsing button.",
+ self.timer._start_pulsing_button()
+ print "Playing notification sound.",
+ self.timer._play_notification_sound()
+ print "Running custom command.",
+ self.timer._run_custom_command(command)
+
+ print "Ending Thread..."
+ thread = MyThread(self)
+ thread.start()
+ thread.join()
+
+ next_timer = self._timer.get_next_timer()
+ auto_start = self._timer.get_auto_start()
+ if auto_start and next_timer:
+ # Start next timer
+ self._stop_sound()
+ self._call_notify(show=False)
+ self._stop_pulsing_button()
+ self._start_next_timer()
+ elif not(auto_start) and next_timer:
+ self._status_button.props.sensitive = False
+ dialog_result = self._start_next_timer_dialog.get_response()
+ self._status_button.props.sensitive = True
+ if dialog_result:
+ # Start next timer
+ self._stop_sound()
+ self._call_notify(show=False)
+ self._stop_pulsing_button()
+ self._start_next_timer()
+ else:
+ self._stop_sound()
+ self._call_notify(show=False)
+ self._stop_pulsing_button()
+
+ print "Updating status button..."
+ self._update_status_button()
+ print "Updating popup menu..."
+ self._update_popup_menu()
+
+ ## StatusButton callbacks ##
+
+ def _on_status_button_clicked(self, button, data=None):
+ current_state = self._timer.get_state()
+ if current_state == core.Timer.STATE_IDLE:
+ self._start_timer_dialog.show()
+ elif current_state == core.Timer.STATE_FINISHED:
+ self._timer.reset()
+ elif current_state == core.Timer.STATE_PAUSED:
+ # Temporarily disable status button while the Continue dialog is open.
+ self._status_button.props.sensitive = False
+ dialog_result = self._continue_dialog.get_response()
+ self._status_button.props.sensitive = True
+ if dialog_result == ui.ContinueTimerDialog.CONTINUE_TIMER:
+ self._timer.start()
+ elif dialog_result == ui.ContinueTimerDialog.STOP_TIMER:
+ self._timer.reset()
+ elif dialog_result == ui.ContinueTimerDialog.KEEP_PAUSED:
+ pass
+ else:
+ assert False
+ elif current_state == core.Timer.STATE_RUNNING:
+ self._timer.stop()
+
+ ## StartTimerDialog callbacks ##
+
+ def _on_start_dialog_clicked_start(self, sender, data=None):
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ self._start_timer_dialog.get_control_data()
+ self._start_timer_with_settings(name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+
+ def _on_start_dialog_clicked_manage_presets(self, sender, data=None):
+ self._manage_presets_dialog.show()
+
+ def _on_start_dialog_clicked_save(self, sender, name,
+ hours, minutes, seconds, command,
+ next_timer, auto_start, data=None):
+ self._presets_store.add_preset(name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+
+ def _on_start_dialog_clicked_preset(self, sender, row_path, data=None):
+ row_iter = self._presets_store.get_model().get_iter(row_path)
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ self._presets_store.get_preset(row_iter)
+ self._start_timer_dialog.set_name_and_duration(name, hours, minutes,
+ seconds, command,
+ next_timer, auto_start)
+
+ def _on_start_dialog_double_clicked_preset(self, sender, row_path, data=None):
+ """Preset is double-clicked. Start the selected preset, and hide the
+ dialog."""
+ row_iter = self._presets_store.get_model().get_iter(row_path)
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ self._presets_store.get_preset(row_iter)
+ self._start_timer_with_settings(name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+ self._start_timer_dialog.hide()
+
+ ## Private methods ##
+
+ def _start_timer_with_settings(self, name, hours, minutes, seconds,
+ command, next_timer, auto_start):
+ print "Resetting timer"
+ if self._timer.get_state() != core.Timer.STATE_IDLE:
+ self._timer.reset()
+ self._timer.set_duration(utils.hms_to_seconds(hours, minutes, seconds))
+ self._timer.set_name(name)
+ self._timer.set_command(command)
+ self._timer.set_next_timer(next_timer)
+ self._timer.set_auto_start(auto_start)
+ self._timer.start()
+
+ def _restart_timer(self):
+ self._timer.reset()
+ self._timer.start()
+
+ def _start_next_timer(self):
+ """Start next timer, if defined."""
+ next_timer = self._timer.get_next_timer()
+ for row in self._presets_store.get_model():
+ #print dir(row)
+ if str(row[0]) == next_timer:
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ self._presets_store.get_preset(row.iter)
+ break
+ print "Starting timer with settings: ",
+ print (name, hours, minutes, seconds, command, next_timer, auto_start)
+ self._start_timer_with_settings(name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+
+
+ def _play_notification_sound(self):
+ if not self._mateconf.get_bool(TimerApplet._PLAY_SOUND_KEY):
+ return
+
+ sound_path = config.DEFAULT_SOUND_PATH
+ if self._mateconf.get_bool(TimerApplet._USE_CUSTOM_SOUND_KEY):
+ sound_path = self._mateconf.get_string(TimerApplet._CUSTOM_SOUND_PATH_KEY)
+
+ print 'Playing notification sound: "%s"' % str(sound_path)
+ self._play_sound(sound_path)
+ print 'Started playing notification sound.'
+
+ def _play_sound(self, file_path):
+ if not file_path:
+ print 'Invalid path to sound file'
+ return
+ self._gst_playbin.set_state(gst.STATE_NULL)
+ sound_uri = 'file://' + file_path
+ print 'Using GStreamer to play: ' + sound_uri
+ self._gst_playbin.set_property('uri', sound_uri)
+ self._gst_playbin.set_state(gst.STATE_PLAYING)
+
+ def _run_custom_command(self, command):
+ if command:
+ print "Running custom command: " + command
+ try:
+ subprocess.call(shlex.split(command))
+ except OSError:
+ print "... failed. Command not found."
+
+ def _stop_sound(self):
+ self._gst_playbin.set_state(gst.STATE_NULL)
+
+ def _start_pulsing_button(self):
+ if self._mateconf.get_bool(TimerApplet._SHOW_PULSING_ICON_KEY):
+ self._status_button.start_pulsing()
+
+ def _stop_pulsing_button(self):
+ self._status_button.stop_pulsing()
+
+ def _show_about_dialog(self):
+ self._about_dialog.run()
+ self._about_dialog.hide()
+
+ def _call_notify(self, summary=None, message=None,
+ reminder_message_func=None, show=True):
+ if self._mateconf.get_bool(TimerApplet._SHOW_POPUP_NOTIFICATION_KEY):
+ if show:
+ self._notifier.begin(summary, message, reminder_message_func)
+ else:
+ self._notifier.end()
diff --git a/timer-applet/src/timerapplet/controllers/TimerManagerService.py b/timer-applet/src/timerapplet/controllers/TimerManagerService.py
new file mode 100644
index 00000000..ca902884
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/TimerManagerService.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import dbus
+import dbus.service
+
+DBUS_INTERFACE_NAMESPACE = 'net.launchpad.timerapplet.TimerApplet.TimerManager'
+
+class TimerManagerService(dbus.service.Object):
+ def __init__(self, bus_name, object_path):
+ dbus.service.Object.__init__(self,
+ dbus.service.BusName(bus_name, bus=dbus.SessionBus()),
+ object_path)
+ self._timer_id_list = []
+
+ def create_and_register_timer_id(self):
+ timer_id = str(uuid.uuid4())
+ self.register_timer_id(timer_id)
+ return timer_id
+
+ def register_timer_id(self, timer_id):
+ self._timer_id_list.append(timer_id)
+
+ def unregister_timer_id(self, timer_id):
+ self._timer_id_list.remove(timer_id)
+
+ @dbus.service.method(dbus_interface=DBUS_INTERFACE_NAMESPACE, out_signature='as')
+ def GetTimerIDList(self):
+ return self._timer_id_list
diff --git a/timer-applet/src/timerapplet/controllers/TimerService.py b/timer-applet/src/timerapplet/controllers/TimerService.py
new file mode 100644
index 00000000..02976101
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/TimerService.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import dbus
+import dbus.service
+from timerapplet import core
+from timerapplet import utils
+
+DBUS_INTERFACE_NAMESPACE = 'net.launchpad.timerapplet.TimerApplet.Timer'
+
+class TimerService(dbus.service.Object):
+ def __init__(self, bus_name, object_path, timer):
+ dbus.service.Object.__init__(self,
+ dbus.service.BusName(bus_name, bus=dbus.SessionBus()),
+ object_path)
+ self._timer = timer
+
+ @dbus.service.method(dbus_interface=DBUS_INTERFACE_NAMESPACE, in_signature='siii')
+ def Start(self, name, hours, minutes, seconds):
+ if self._timer.get_state() != core.Timer.STATE_IDLE:
+ self._timer.reset()
+ self._timer.set_duration(utils.hms_to_seconds(hours, minutes, seconds))
+ self._timer.set_name(name)
+ self._timer.start()
+
+ @dbus.service.method(dbus_interface=DBUS_INTERFACE_NAMESPACE)
+ def Stop(self):
+ if self._timer.get_state() != core.Timer.STATE_IDLE:
+ self._timer.reset()
+
+ @dbus.service.method(dbus_interface=DBUS_INTERFACE_NAMESPACE)
+ def PauseContinue(self):
+ if self._timer.get_state() == core.Timer.STATE_RUNNING:
+ self._timer.stop()
+ elif self._timer.get_state() == core.Timer.STATE_PAUSED:
+ self._timer.start()
diff --git a/timer-applet/src/timerapplet/controllers/__init__.py b/timer-applet/src/timerapplet/controllers/__init__.py
new file mode 100644
index 00000000..b0b130ef
--- /dev/null
+++ b/timer-applet/src/timerapplet/controllers/__init__.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 GlobalController import GlobalController
+from TimerApplet import TimerApplet
+from TimerManagerService import TimerManagerService
+from TimerService import TimerService
diff --git a/timer-applet/src/timerapplet/core/AppletMateConfWrapper.py b/timer-applet/src/timerapplet/core/AppletMateConfWrapper.py
new file mode 100644
index 00000000..c5d09619
--- /dev/null
+++ b/timer-applet/src/timerapplet/core/AppletMateConfWrapper.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 os import path
+import mateconf
+
+class AppletMateConfWrapper(object):
+ def __init__(self, applet, schema_path, standalone_key):
+ object.__init__(self)
+ self._connection_ids = []
+
+ self._client = mateconf.client_get_default()
+
+ # Get preferences key path for the given applet instance.
+ self._base_path = applet.get_preferences_key()
+ if self._base_path is not None:
+ # Apply the schema to the applet instance preferences key.
+ applet.add_preferences(schema_path)
+ else:
+ # NOTE: Don't need to apply schema here because the Timer Applet schema file
+ # already specifies that the schema be automatically applied to the standalone key.
+
+ self._base_path = standalone_key
+
+ # Applet would usually do this for us, but since we're running in standalone mode,
+ # we have to do this ourselves in order to receive MateConf change notifications.
+ self._client.add_dir(self._base_path, mateconf.CLIENT_PRELOAD_RECURSIVE)
+
+ print 'Base prefs path = %s' % self._base_path
+
+ def get_base_path(self):
+ return self._base_path
+
+ def add_notification(self, relative_key, callback, data=None):
+ """Register for notifications of changes to the given preference key.
+
+ relative_key should be relative to the applet's base preferences key path.
+ callback should look like: callback(MateConfValue, data=None)
+ """
+ connection_id = self._client.notify_add(self._get_full_path(relative_key),
+ self._notification_callback, (callback, data))
+ self._connection_ids.append(connection_id)
+
+ def get_string(self, relative_key):
+ return self._client.get_string(self._get_full_path(relative_key))
+
+ def get_bool(self, relative_key):
+ return self._client.get_bool(self._get_full_path(relative_key))
+
+ def set_string(self, relative_key, val):
+ self._client.set_string(self._get_full_path(relative_key), val)
+
+ def set_bool(self, relative_key, val):
+ self._client.set_bool(self._get_full_path(relative_key), val)
+
+ def delete(self):
+ for connection_id in self._connection_ids:
+ self._client.notify_remove(connection_id)
+ self._connection_ids = []
+
+ def _notification_callback(self, client, cnxn_id, entry, data=None):
+ (callback, real_data) = data
+
+ # mateconf_value is of type MateConfValue (mateconf.Value)
+ mateconf_value = entry.get_value()
+
+ # Ignore when mateconf_value is None because that
+ # means that the settings are being removed
+ # because the applet has been removed from
+ # the panel.
+ if mateconf_value != None:
+ callback(mateconf_value, real_data)
+
+ def _get_full_path(self, relative_key):
+ return path.join(self._base_path, relative_key)
+
diff --git a/timer-applet/src/timerapplet/core/Makefile.am b/timer-applet/src/timerapplet/core/Makefile.am
new file mode 100644
index 00000000..6c974866
--- /dev/null
+++ b/timer-applet/src/timerapplet/core/Makefile.am
@@ -0,0 +1,6 @@
+moduledir = $(pythondir)/timerapplet/core
+module_PYTHON = \
+ __init__.py \
+ AppletMateConfWrapper.py \
+ PresetsStore.py \
+ Timer.py
diff --git a/timer-applet/src/timerapplet/core/PresetsStore.py b/timer-applet/src/timerapplet/core/PresetsStore.py
new file mode 100644
index 00000000..edd5655a
--- /dev/null
+++ b/timer-applet/src/timerapplet/core/PresetsStore.py
@@ -0,0 +1,160 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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
+
+try:
+ from xml.etree import ElementTree as et
+except:
+ from elementtree import ElementTree as et
+
+import os
+from os import path
+import gobject
+import gtk
+import timerapplet.utils as utils
+
+from timerapplet.utils import (serialize_bool,
+ deserialize_bool,
+ seconds_to_hms,
+ hms_to_seconds)
+from timerapplet.defs import VERSION
+
+class PersistentStore(gtk.ListStore):
+ def __init__(self, load_func, save_func, *args):
+ gtk.ListStore.__init__(self, *args)
+ load_func(self)
+
+ self.connect('row-deleted', lambda model, row_path: save_func(self))
+ self.connect('row-changed', lambda model, row_path, row_iter: save_func(self))
+
+class PresetsStore(gobject.GObject):
+ (_NAME_COL,
+ _HOURS_COL,
+ _MINUTES_COL,
+ _SECONDS_COL,
+ _COM_COL,
+ _NEXT_COL,
+ _AUTO_START_COL) = xrange(7)
+
+ def __init__(self, filename):
+ object.__init__(self)
+ self._model = PersistentStore(lambda model: PresetsStore._load_presets(model, filename),
+ lambda model: PresetsStore._save_presets(model, filename),
+ gobject.TYPE_STRING,
+ gobject.TYPE_INT,
+ gobject.TYPE_INT,
+ gobject.TYPE_INT,
+ gobject.TYPE_STRING,
+ gobject.TYPE_STRING,
+ gobject.TYPE_BOOLEAN,
+ )
+
+ def get_model(self):
+ """Return GtkTreeModel.
+
+ Should not rely on it being any particular subtype of GtkTreeModel.
+
+ """
+ return self._model
+
+ def get_preset(self, row_iter):
+ return self._model.get(row_iter,
+ PresetsStore._NAME_COL,
+ PresetsStore._HOURS_COL,
+ PresetsStore._MINUTES_COL,
+ PresetsStore._SECONDS_COL,
+ PresetsStore._COM_COL,
+ PresetsStore._NEXT_COL,
+ PresetsStore._AUTO_START_COL,
+ )
+
+ def add_preset(self, name, hours, minutes, seconds, command, next_timer,
+ auto_start):
+ self._model.append((name, hours, minutes, seconds, command, next_timer,
+ auto_start))
+
+ def modify_preset(self, row_iter, name, hours, minutes, seconds, command,
+ next_timer, auto_start):
+ self._model.set(row_iter,
+ PresetsStore._NAME_COL, name,
+ PresetsStore._HOURS_COL, hours,
+ PresetsStore._MINUTES_COL, minutes,
+ PresetsStore._SECONDS_COL, seconds,
+ PresetsStore._COM_COL, command,
+ PresetsStore._NEXT_COL, next_timer,
+ PresetsStore._AUTO_START_COL, auto_start
+ )
+
+ def remove_preset(self, row_iter):
+ self._model.remove(row_iter)
+
+ def preset_name_exists_case_insensitive(self, preset_name):
+ preset_name = preset_name.lower()
+ for preset in self._model:
+ if preset_name == preset[PresetsStore._NAME_COL].lower():
+ return True
+ return False
+
+ def _load_presets(model, file_path):
+ try:
+ tree = et.parse(file_path)
+ except:
+ return
+
+ root = tree.getroot()
+
+ for node in root:
+ name = node.get('name')
+ (hours, minutes, seconds) = seconds_to_hms(int(node.get('duration')))
+ command = node.get('command')
+ next_timer = node.get('next_timer')
+ auto_start = node.get('auto_start')
+ model.append((name, hours, minutes, seconds, command, next_timer,
+ deserialize_bool(auto_start)))
+ _load_presets = staticmethod(_load_presets)
+
+ def _save_presets(model, file_path):
+ root = et.Element('timerapplet')
+ root.set('version', VERSION)
+
+ def add_xml_node(model, path, row_iter):
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ model.get(row_iter,
+ PresetsStore._NAME_COL,
+ PresetsStore._HOURS_COL,
+ PresetsStore._MINUTES_COL,
+ PresetsStore._SECONDS_COL,
+ PresetsStore._COM_COL,
+ PresetsStore._NEXT_COL,
+ PresetsStore._AUTO_START_COL
+ )
+ node = et.SubElement(root, 'preset')
+ node.set('name', name)
+ node.set('duration', str(hms_to_seconds(hours, minutes, seconds)))
+ node.set('command', command or '')
+ node.set('next_timer', next_timer or '')
+ node.set('auto_start', serialize_bool(auto_start))
+
+ model.foreach(add_xml_node)
+ tree = et.ElementTree(root)
+
+ file_dir = path.dirname(file_path)
+ if not path.exists(file_dir):
+ print 'Creating config directory: %s' % file_dir
+ os.makedirs(file_dir, 0744)
+ tree.write(file_path)
+ _save_presets = staticmethod(_save_presets)
+
diff --git a/timer-applet/src/timerapplet/core/Timer.py b/timer-applet/src/timerapplet/core/Timer.py
new file mode 100644
index 00000000..f4ba57a2
--- /dev/null
+++ b/timer-applet/src/timerapplet/core/Timer.py
@@ -0,0 +1,175 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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
+
+import datetime
+import time
+import gobject
+
+class Timer(gobject.GObject):
+ (STATE_IDLE, STATE_RUNNING, STATE_PAUSED, STATE_FINISHED) = xrange(4)
+
+ __gsignals__ = {'time-changed':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'state-changed':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())}
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._state = Timer.STATE_IDLE
+ self._duration_seconds = 0
+ self._remaining_seconds = 0
+ self._end_time = 0
+ self._name = ''
+ self._command = ''
+ self._next_timer = ''
+ self._auto_start = False
+
+ def set_duration(self, seconds):
+ """Set the duration of the timer in seconds."""
+ assert self._state == Timer.STATE_IDLE
+ self._duration_seconds = seconds
+
+ def get_duration(self):
+ """Return the duration of the timer in seconds."""
+ return self._duration_seconds
+
+ def set_name(self, name):
+ """Set the name of the timer."""
+ assert self._state == Timer.STATE_IDLE
+ self._name = name
+
+ def get_name(self):
+ """Return the name of the timer."""
+ return self._name
+
+ def set_command(self, command):
+ """Set the command to run of the timer."""
+ assert self._state == Timer.STATE_IDLE
+ self._command = command
+
+ def get_command(self):
+ """Return the name of the command of the timer."""
+ return self._command
+
+ def set_next_timer(self, timer):
+ """Set the next timeer of the timer."""
+ assert self._state == Timer.STATE_IDLE
+ self._next_timer = timer
+
+ def get_next_timer(self):
+ """Get the next timer of the timer."""
+ return self._next_timer
+
+ def set_auto_start(self, auto_start):
+ """Set the auto-start value of the timer."""
+ assert self._state == Timer.STATE_IDLE
+ self._auto_start = auto_start
+
+ def get_auto_start(self):
+ """Get the auto-start value."""
+ return self._auto_start
+
+ def start(self):
+ """Start or resume the timer.
+
+ This method should only be called when the timer is IDLE or PAUSED.
+
+ """
+ assert self._state == Timer.STATE_IDLE or self._state == Timer.STATE_PAUSED
+ self._timer_transition_to_state(Timer.STATE_RUNNING)
+
+ def stop(self):
+ """Pause the timer.
+
+ This method should only be called when the timer is RUNNING.
+
+ """
+ assert self._state == Timer.STATE_RUNNING
+ self._timer_transition_to_state(Timer.STATE_PAUSED)
+
+ def reset(self):
+ """Reset the timer.
+
+ This method should only be called when the timer is not IDLE.
+
+ """
+ assert self._state != Timer.STATE_IDLE
+ self._timer_transition_to_state(Timer.STATE_IDLE)
+
+ def get_state(self):
+ """Return the current state."""
+ return self._state
+
+ def get_remaining_time(self):
+ """Return the remaining time in seconds."""
+ return min(self._duration_seconds, max(0, self._remaining_seconds))
+
+ def get_end_time(self):
+ """Return a datetime object representing the end time.
+
+ This method should only be called when the timer is RUNNING or FINISHED.
+
+ """
+ assert self._state == Timer.STATE_RUNNING or self._state == Timer.STATE_FINISHED
+ return datetime.datetime.fromtimestamp(self._end_time)
+
+ def _timer_set_state(self, state):
+ self._state = state
+ self.emit('state-changed')
+
+ def _timer_transition_to_state(self, dest_state):
+ cur_time = int(time.time())
+
+ if dest_state == Timer.STATE_IDLE:
+ self._end_time = 0
+ self._set_remaining_time(self._duration_seconds)
+ elif dest_state == Timer.STATE_RUNNING:
+ assert self._duration_seconds >= 0
+
+ if self._state == Timer.STATE_IDLE:
+ self._end_time = cur_time + self._duration_seconds
+ self._set_remaining_time(self._duration_seconds)
+ elif self._state == Timer.STATE_PAUSED:
+ self._end_time = cur_time + self._remaining_seconds
+
+ gobject.timeout_add(500, self._on_timeout)
+ elif dest_state == Timer.STATE_PAUSED:
+ self._set_remaining_time(self._end_time - cur_time)
+ self._end_time = 0
+ elif dest_state == Timer.STATE_FINISHED:
+ pass
+ else:
+ assert False
+
+ self._timer_set_state(dest_state)
+
+ def _on_timeout(self):
+ if self._state != Timer.STATE_RUNNING:
+ return False # remove timeout source
+
+ new_remaining = self._end_time - int(time.time())
+ if self._remaining_seconds != new_remaining:
+ self._set_remaining_time(new_remaining)
+
+ if self._remaining_seconds < 0:
+ self._timer_transition_to_state(Timer.STATE_FINISHED)
+ return False # remove timeout source
+ return True # keep timeout source
+
+ def _set_remaining_time(self, new_remaining):
+ self._remaining_seconds = new_remaining
+ self.emit('time-changed')
diff --git a/timer-applet/src/timerapplet/core/__init__.py b/timer-applet/src/timerapplet/core/__init__.py
new file mode 100644
index 00000000..861b496d
--- /dev/null
+++ b/timer-applet/src/timerapplet/core/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 Timer import Timer
+from PresetsStore import PresetsStore
+from AppletMateConfWrapper import AppletMateConfWrapper
diff --git a/timer-applet/src/timerapplet/defs.py.in b/timer-applet/src/timerapplet/defs.py.in
new file mode 100644
index 00000000..55087654
--- /dev/null
+++ b/timer-applet/src/timerapplet/defs.py.in
@@ -0,0 +1,7 @@
+PACKAGE = '@PACKAGE@'
+VERSION = '@VERSION@'
+GETTEXT_PACKAGE = '@GETTEXT_PACKAGE@'
+LOCALE_DIR = '@LOCALEDIR@'
+RESOURCES_DIR = '@RESOURCESDIR@'
+IMAGES_DIR = '@IMAGESDIR@'
+
diff --git a/timer-applet/src/timerapplet/logger.py b/timer-applet/src/timerapplet/logger.py
new file mode 100644
index 00000000..c40c0aac
--- /dev/null
+++ b/timer-applet/src/timerapplet/logger.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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
+
+import logging
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)-8s %(message)s',
+ datefmt='%a, %d %b %Y %H:%M:%S',
+ filename='/tmp/myapp.log',
+ filemode='w')
diff --git a/timer-applet/src/timerapplet/ui/AddEditPresetDialog.py b/timer-applet/src/timerapplet/ui/AddEditPresetDialog.py
new file mode 100644
index 00000000..d6773769
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/AddEditPresetDialog.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gtk
+import gtk.glade as glade
+from DurationChooser import DurationChooser
+
+class AddEditPresetDialog(object):
+ def __init__(self, glade_file_name, title, name_validator_func,
+ name='', hours=0, minutes=0, seconds=0, command='',
+ next_timer='', auto_start=False):
+ self._valid_name_func = name_validator_func
+
+ glade_widgets = glade.XML(glade_file_name, 'add_edit_preset_dialog')
+ self._dialog = glade_widgets.get_widget('add_edit_preset_dialog')
+ self._ok_button = glade_widgets.get_widget('ok_button')
+ self._cancel_button = glade_widgets.get_widget('cancel_button')
+ self._name_entry = glade_widgets.get_widget('name_entry')
+ duration_chooser_container = glade_widgets.get_widget('duration_chooser_container')
+ self._duration_chooser = DurationChooser(gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL))
+ self._command_entry = glade_widgets.get_widget('command_entry')
+ self._next_timer_entry = glade_widgets.get_widget('next_timer_entry')
+ self._auto_start_check = glade_widgets.get_widget('auto_start_check')
+
+ duration_chooser_container.pack_start(self._duration_chooser)
+
+ self._dialog.set_title(title)
+ self._dialog.set_default_response(gtk.RESPONSE_OK)
+ self._name_entry.set_text(name)
+ self._command_entry.set_text(command)
+ self._duration_chooser.set_duration(hours, minutes, seconds)
+ self._next_timer_entry.set_text(next_timer)
+ self._auto_start_check.set_active(auto_start)
+
+ self._name_entry.connect('changed', lambda entry: self._check_for_valid_save_preset_input())
+ self._duration_chooser.connect('duration-changed',
+ lambda chooser: self._check_for_valid_save_preset_input())
+ self._duration_chooser.show()
+
+ def _non_zero_duration(self):
+ (hours, minutes, seconds) = self._duration_chooser.get_duration()
+ return (hours > 0 or minutes > 0 or seconds > 0)
+
+ def _check_for_valid_save_preset_input(self):
+ self._ok_button.props.sensitive = (self._non_zero_duration() and
+ self._valid_name_func(self._name_entry.get_text()))
+
+ ## Callback for saving ##
+
+ def get_preset(self):
+ self._check_for_valid_save_preset_input()
+ result = self._dialog.run()
+ self._dialog.hide()
+ if result == gtk.RESPONSE_OK:
+ (hours, minutes, seconds) = self._duration_chooser.get_duration()
+ cmd = self._command_entry.get_text()
+ next_timer = self._next_timer_entry.get_text()
+ auto_start = self._auto_start_check.get_active()
+ return (self._name_entry.get_text(), hours, minutes, seconds, cmd,
+ next_timer, auto_start)
+ else:
+ return None
diff --git a/timer-applet/src/timerapplet/ui/ContinueTimerDialog.py b/timer-applet/src/timerapplet/ui/ContinueTimerDialog.py
new file mode 100644
index 00000000..a6b1bfb2
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/ContinueTimerDialog.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 gettext import gettext as _
+import gtk
+
+class ContinueTimerDialog(object):
+ (STOP_TIMER, KEEP_PAUSED, CONTINUE_TIMER) = xrange(3)
+
+ def __init__(self, glade_file_name, header_text, body_text):
+ self._dialog = gtk.Dialog(_('Continue Timer'),
+ None,
+ gtk.DIALOG_DESTROY_WITH_PARENT,
+ (_('_Stop Timer'), gtk.RESPONSE_NO,
+ _('_Keep Paused'), gtk.RESPONSE_CLOSE,
+ _('_Continue Timer'), gtk.RESPONSE_YES))
+ self._dialog.props.border_width = 6
+ self._dialog.props.has_separator = False
+ self._dialog.props.resizable = False
+ self._dialog.vbox.props.spacing = 12
+ self._dialog.set_default_response(gtk.RESPONSE_YES)
+
+ hbox = gtk.HBox(False, 0)
+ hbox.props.spacing = 12
+ hbox.props.border_width = 6
+
+ image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
+ image.props.yalign = 0.0
+
+ label = gtk.Label('<span weight="bold" size="larger">%s</span>\n\n%s' % (header_text, body_text))
+ label.props.use_markup = True
+ label.props.wrap = True
+ label.props.yalign = 0.0
+
+ hbox.pack_start(image, False, False, 0)
+ hbox.pack_start(label, False, False, 0)
+ self._dialog.vbox.pack_start(hbox, False, False, 0)
+
+ hbox.show_all()
+
+ def get_response(self):
+ dialog_result = self._dialog.run()
+ self._dialog.hide()
+ if dialog_result == gtk.RESPONSE_YES:
+ return ContinueTimerDialog.CONTINUE_TIMER
+ elif dialog_result == gtk.RESPONSE_NO:
+ return ContinueTimerDialog.STOP_TIMER
+ else:
+ return ContinueTimerDialog.KEEP_PAUSED
+
diff --git a/timer-applet/src/timerapplet/ui/DurationChooser.py b/timer-applet/src/timerapplet/ui/DurationChooser.py
new file mode 100644
index 00000000..77d0a7ed
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/DurationChooser.py
@@ -0,0 +1,166 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 gettext import gettext as _
+import math
+import gobject
+import gtk
+
+class DurationChooser(gtk.VBox):
+ __gsignals__ = {'duration-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())}
+ MAX_HOURS = 48
+ MAX_MINUTES = 999
+ MAX_SECONDS = 999
+
+ def __init__(self, size_group):
+ gtk.VBox.__init__(self, False, 6)
+
+ self._hours_spin = self._add_row(_('_Hours:'),
+ size_group,
+ DurationChooser.MAX_HOURS)
+ self._minutes_spin = self._add_row(_('_Minutes:'),
+ size_group,
+ DurationChooser.MAX_MINUTES)
+ self._seconds_spin = self._add_row(_('_Seconds:'),
+ size_group,
+ DurationChooser.MAX_SECONDS)
+
+ def get_duration(self):
+ """Return numerical representations of the values in the spinbuttons.
+
+ This method does not just call get_value_as_int() on each spinbutton because
+ get_value_as_int() does not return the most up-to-date value if the user is
+ in the middle of editing the value in a spinbutton. Instead, it actually
+ returns the value previously "confirmed" for the spinbutton (for example,
+ by moving focus to another widget).
+
+ Example: User starts with all spinboxes set to 0. He types 5 in 'Minutes' and
+ immediately uses the keyboard to activate the 'Save as Preset' button. Because
+ the user never moved focus out of the spinbutton, get_value_as_int() will report
+ the previous value of 0, and the saved preset will not have the expected duration
+ of 5 minutes.
+
+ If a particular spinbutton is empty or contains a non-float value, then this method
+ will report its value as zero.
+
+ Returns a tuple in this format: (hours, minutes, seconds)
+
+ """
+ hours_str = self._hours_spin.props.text
+ minutes_str = self._minutes_spin.props.text
+ seconds_str = self._seconds_spin.props.text
+ hours = 0
+ minutes = 0
+ seconds = 0
+
+ try:
+ hours = float(hours_str)
+ except TypeError:
+ pass
+ except ValueError:
+ pass
+
+ try:
+ minutes = float(minutes_str)
+ except TypeError:
+ pass
+ except ValueError:
+ pass
+
+ try:
+ seconds = float(seconds_str)
+ except TypeError:
+ pass
+ except ValueError:
+ pass
+
+ minutes_fraction = minutes - int(minutes)
+ hours_fraction = hours - int(hours)
+
+ # Distribute overflow from seconds to minutes to hours.
+ if seconds > 59:
+ minutes = math.floor(seconds / 60)
+ seconds = seconds % 60
+ if minutes > 59:
+ hours = math.floor(minutes / 60)
+ minutes = minutes % 60
+ if hours > DurationChooser.MAX_HOURS:
+ hours = DurationChooser.MAX_HOURS
+
+ # Distribute fractions.
+ if hours_fraction > 0:
+ minutes = int(hours_fraction * 60)
+ if minutes_fraction > 0:
+ seconds = int(minutes_fraction * 60)
+
+ return (int(hours), int(minutes), int(seconds))
+
+ def set_duration(self, hours, minutes, seconds):
+ self._hours_spin.set_value(hours)
+ self._minutes_spin.set_value(minutes)
+ self._seconds_spin.set_value(seconds)
+
+ def normalize_fields(self):
+ (hours, minutes, seconds) = self.get_duration()
+ self._hours_spin.set_value(hours)
+ self._minutes_spin.set_value(minutes)
+ self._seconds_spin.set_value(seconds)
+
+ def clear(self):
+ self._hours_spin.set_value(0)
+ self._minutes_spin.set_value(0)
+ self._seconds_spin.set_value(0)
+
+ def focus_hours(self):
+ self._hours_spin.grab_focus()
+
+ def _add_row(self, row_label_str, size_group, max_spin_val):
+ row_hbox = gtk.HBox(False, 6)
+
+ label = gtk.Label(row_label_str)
+ label.set_property('xalign', 0.0)
+ label.set_property('use-underline', True)
+ size_group.add_widget(label)
+
+ spin_adj = gtk.Adjustment(0, 0, max_spin_val, 1, 1, 0)
+ spin_button = gtk.SpinButton(spin_adj, 1.0, 0)
+ spin_button.props.activates_default = True
+ spin_button.props.numeric = False
+ spin_button.props.update_policy = gtk.UPDATE_IF_VALID
+
+ label.set_mnemonic_widget(spin_button)
+
+ row_hbox.pack_start(label, False, False, 0)
+ row_hbox.pack_start(spin_button, True, True, 0)
+ self.pack_start(row_hbox, False, False, 0)
+
+ spin_button.connect('changed', self._on_spin_button_val_changed)
+ spin_button.connect('focus-out-event', self._on_spin_button_focus_out)
+ spin_button.connect('activate', self._on_spin_button_activate)
+
+ label.show()
+ spin_button.show()
+ row_hbox.show()
+ return spin_button
+
+ def _on_spin_button_val_changed(self, spin_button):
+ self.emit('duration-changed')
+
+ def _on_spin_button_focus_out(self, spin_button, event):
+ self.normalize_fields()
+
+ def _on_spin_button_activate(self, entry):
+ self.normalize_fields()
diff --git a/timer-applet/src/timerapplet/ui/Makefile.am b/timer-applet/src/timerapplet/ui/Makefile.am
new file mode 100644
index 00000000..0021823e
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/Makefile.am
@@ -0,0 +1,15 @@
+moduledir = $(pythondir)/timerapplet/ui
+module_PYTHON = \
+ __init__.py \
+ AddEditPresetDialog.py \
+ ContinueTimerDialog.py \
+ DurationChooser.py \
+ ManagePresetsDialog.py \
+ Notifier.py \
+ PieMeter.py \
+ PreferencesDialog.py \
+ PulseButton.py \
+ ScrollableButtonList.py \
+ StartTimerDialog.py \
+ StartNextTimerDialog.py \
+ StatusButton.py
diff --git a/timer-applet/src/timerapplet/ui/ManagePresetsDialog.py b/timer-applet/src/timerapplet/ui/ManagePresetsDialog.py
new file mode 100644
index 00000000..0ee38625
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/ManagePresetsDialog.py
@@ -0,0 +1,86 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gobject
+import gtk
+import gtk.glade as glade
+
+class ManagePresetsDialog(gobject.GObject):
+ __gsignals__ = {'clicked-add':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'clicked-edit':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ 'clicked-remove':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))}
+
+ def __init__(self, glade_file_name, presets_store, preset_display_func):
+ gobject.GObject.__init__(self)
+
+ glade_widgets = glade.XML(glade_file_name, 'manage_presets_dialog')
+ self._dialog = glade_widgets.get_widget('manage_presets_dialog')
+ self._presets_view = glade_widgets.get_widget('presets_view')
+ self._delete_button = glade_widgets.get_widget('delete_button')
+ self._edit_button = glade_widgets.get_widget('edit_button')
+ self._add_button = glade_widgets.get_widget('add_button')
+
+ self._presets_view.set_model(presets_store)
+ renderer = gtk.CellRendererText()
+ col = gtk.TreeViewColumn('Preset', renderer)
+
+ def preset_cell_data_func(col, cell, model, row_iter, user_data=None):
+ cell.props.text = preset_display_func(row_iter)
+
+ col.set_cell_data_func(renderer, preset_cell_data_func)
+ self._presets_view.append_column(col)
+
+ self._dialog.connect('response', self._on_dialog_response)
+ self._dialog.connect('delete-event', self._dialog.hide_on_delete)
+ self._presets_view.get_selection().connect('changed', lambda selection: self._update_button_states())
+ self._delete_button.connect('clicked', self._on_delete_button_clicked)
+ self._edit_button.connect('clicked', self._on_edit_button_clicked)
+ self._add_button.connect('clicked', self._on_add_button_clicked)
+
+ self._update_button_states()
+ self._dialog.set_default_size(300, 220)
+
+ def show(self):
+ self._dialog.present()
+
+ def _get_selected_path(self):
+ selection = self._presets_view.get_selection()
+ (model, selection_iter) = selection.get_selected()
+ return model.get_path(selection_iter)
+
+ def _update_button_states(self):
+ selection = self._presets_view.get_selection()
+ num_selected = selection.count_selected_rows()
+ self._delete_button.props.sensitive = num_selected >= 1
+ self._edit_button.props.sensitive = num_selected == 1
+
+ def _on_delete_button_clicked(self, button):
+ row_path = self._get_selected_path()
+ self.emit('clicked-remove', row_path)
+
+ def _on_edit_button_clicked(self, button):
+ row_path = self._get_selected_path()
+ self.emit('clicked-edit', row_path)
+
+ def _on_add_button_clicked(self, button):
+ self.emit('clicked-add')
+
+ def _on_dialog_response(self, dialog, response_id):
+ dialog.hide()
+
diff --git a/timer-applet/src/timerapplet/ui/Notifier.py b/timer-applet/src/timerapplet/ui/Notifier.py
new file mode 100644
index 00000000..92e814b7
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/Notifier.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gobject
+import pynotify
+
+class Notifier(object):
+ _NOTIFICATION_REDISPLAY_INTERVAL_SECONDS = 60
+
+ def __init__(self, app_name, icon, attach):
+ self._icon = icon
+ self._attach = attach
+ self._notify = None
+ self._handler_id = None
+ self._timeout_id = None
+
+ if not pynotify.is_initted():
+ pynotify.init(app_name)
+
+ def begin(self, summary, body, get_reminder_message_func):
+ # NOTE: This callback wrapper is to workaround an API-breaking change present in
+ # the version of libmatenotify used by Fedora 10. The API break adds an additional
+ # 'reason' parameter to the callback signature. This was fixed before
+ # the latest official release of libmatenotify, but it looks like Fedora 10
+ # is using an unofficial libmatenotify build that still contains the API-breaking change.
+ def closed_callback_wrapper(notification, reason_UNUSED=None):
+ self._on_notification_closed(notification, get_reminder_message_func)
+
+ self.end()
+ self._notify = pynotify.Notification(summary, body, self._icon)
+ self._handler_id = self._notify.connect('closed', closed_callback_wrapper)
+ self._notify.show()
+
+ def end(self):
+ if self._notify is not None:
+ if self._timeout_id is not None:
+ gobject.source_remove(self._timeout_id)
+ self._timeout_id = None
+ self._notify.disconnect(self._handler_id)
+ self._handler_id = None
+ try:
+ self._notify.close()
+ except gobject.GError:
+ # Throws a GError exception if the notification bubble has already been closed.
+ # Ignore the exception.
+ pass
+ self._notify = None
+
+ def _on_notification_closed(self, notification, get_reminder_message_func):
+ self._timeout_id = gobject.timeout_add(Notifier._NOTIFICATION_REDISPLAY_INTERVAL_SECONDS * 1000,
+ self._on_notification_redisplay_timeout,
+ get_reminder_message_func)
+
+ def _on_notification_redisplay_timeout(self, get_reminder_message_func):
+ message = get_reminder_message_func()
+ self._notify.props.body = message
+ self._notify.show()
+
+ self._timeout_id = None
+ return False
diff --git a/timer-applet/src/timerapplet/ui/PieMeter.py b/timer-applet/src/timerapplet/ui/PieMeter.py
new file mode 100644
index 00000000..f97d02d7
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/PieMeter.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import math
+import gobject
+import gtk
+
+class PieMeter(gtk.Image):
+ _DEFAULT_SIZE = 24
+
+ def __init__(self):
+ gtk.Image.__init__(self)
+ self._progress = 0.0
+ self._fill_color = (0.0, 1.0, 0.0)
+
+ def set_progress(self, progress):
+ assert progress >= 0.0
+ assert progress <= 1.0
+ self._progress = progress
+ if self.window is not None:
+ self.window.invalidate_rect(self.allocation, True)
+
+ def set_fill_color(self, red, green, blue):
+ assert 0.0 <= red <= 1.0
+ assert 0.0 <= green <= 1.0
+ assert 0.0 <= blue <= 1.0
+ self._fill_color = (red, green, blue)
+
+ if self.window is not None:
+ self.window.invalidate_rect(self.allocation, True)
+
+ def do_size_request(self, requisition):
+ requisition.width = PieMeter._DEFAULT_SIZE
+ requisition.height = PieMeter._DEFAULT_SIZE
+
+ def do_expose_event(self, event):
+ context = event.window.cairo_create()
+
+ rect = self.allocation
+ x = rect.x + (rect.width / 2)
+ y = rect.y + (rect.height / 2)
+ radius = (min(rect.width, rect.height) / 2)
+
+ # Draw background circle
+ context.arc(x, y, radius, 0, 2 * math.pi)
+ context.set_source_rgba(0.8, 0.8, 0.8)
+ context.fill()
+
+ # Draw pie
+ context.arc(x, y, radius, (-0.5 * math.pi) + self._progress * 2 * math.pi, 1.5 * math.pi)
+ context.line_to(x, y)
+ context.close_path()
+ (red, green, blue) = self._fill_color
+ context.set_source_rgb(red, green, blue)
+ context.fill()
+
+ # Draw circle outline
+ context.arc(x, y, radius, 0, 2 * math.pi)
+ context.set_source_rgba(1, 1, 1)
+ context.set_line_width(1.0)
+ context.stroke()
+
+gobject.type_register(PieMeter)
+
diff --git a/timer-applet/src/timerapplet/ui/PreferencesDialog.py b/timer-applet/src/timerapplet/ui/PreferencesDialog.py
new file mode 100644
index 00000000..6341fe8d
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/PreferencesDialog.py
@@ -0,0 +1,192 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gobject
+import gtk
+import gtk.glade as glade
+
+class PreferencesDialog(gobject.GObject):
+ __gsignals__ = \
+ {
+ 'show-remaining-time-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_BOOLEAN,)
+ ),
+ 'play-sound-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_BOOLEAN,)
+ ),
+ 'use-custom-sound-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_BOOLEAN,)
+ ),
+ 'show-popup-notification-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_BOOLEAN,)
+ ),
+ 'show-pulsing-icon-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_BOOLEAN,)
+ ),
+ 'custom-sound-path-changed':
+ (
+ gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,)
+ )
+ }
+
+ __gproperties__ = \
+ {
+ 'show-remaining-time':
+ (bool,
+ 'Show remaining time',
+ 'Whether to show remaining time when the timer is running',
+ False,
+ gobject.PARAM_WRITABLE
+ ),
+ 'play-sound':
+ (bool,
+ 'Play notification sound',
+ 'Whether to play notification sound when the timer is finished',
+ False,
+ gobject.PARAM_WRITABLE
+ ),
+ 'use-custom-sound':
+ (bool,
+ 'Use custom sound',
+ 'Whether to use a custom notification sound',
+ False,
+ gobject.PARAM_WRITABLE
+ ),
+ 'show-popup-notification':
+ (bool,
+ 'Show Popup notification',
+ 'Whether to show a popup notifcation when timer finished',
+ False,
+ gobject.PARAM_WRITABLE
+ ),
+ 'show-pulsing-icon':
+ (bool,
+ 'Show pulsing icon',
+ 'Whether to show pulsing icon when timer finished',
+ False,
+ gobject.PARAM_WRITABLE
+ ),
+ 'custom-sound-path':
+ (str,
+ 'Custom sound path',
+ 'Path to a custom notification sound',
+ '',
+ gobject.PARAM_WRITABLE
+ )
+ }
+
+ def __init__(self, glade_file_name):
+ gobject.GObject.__init__(self)
+ glade_widgets = glade.XML(glade_file_name, 'preferences_dialog')
+ self._preferences_dialog = glade_widgets.get_widget('preferences_dialog')
+ self._show_time_check = glade_widgets.get_widget('show_time_check')
+ self._play_sound_check = glade_widgets.get_widget('play_sound_check')
+ self._use_default_sound_radio = glade_widgets.get_widget('use_default_sound_radio')
+ self._use_custom_sound_radio = glade_widgets.get_widget('use_custom_sound_radio')
+ self._sound_chooser_button = glade_widgets.get_widget('sound_chooser_button')
+ self._play_sound_box = glade_widgets.get_widget('play_sound_box')
+ #: Popup notification checkbutton
+ self._popup_notification_check = glade_widgets.get_widget('popup_notification_check')
+ #: Pulsing icon checkbutton
+ self._pulsing_icon_check = glade_widgets.get_widget('pulsing_trayicon_check')
+
+ #######################################################################
+ # Signals
+ #######################################################################
+ self._show_time_check.connect('toggled', self._on_show_time_check_toggled)
+ self._play_sound_check.connect('toggled', self._on_play_sound_check_toggled)
+ self._use_custom_sound_radio.connect('toggled', self._on_use_custom_sound_radio_toggled)
+ #: Popup notification checkbutton 'toggled' signal
+ self._popup_notification_check.connect('toggled', self._on_popup_notification_toggled)
+ #: Pulsing icon checkbutton 'toggled' signal
+ self._pulsing_icon_check.connect('toggled', self._on_pulsing_icon_toggled)
+
+ self._sound_chooser_button.connect('selection-changed', self._on_sound_chooser_button_selection_changed)
+ self._preferences_dialog.connect('delete-event', gtk.Widget.hide_on_delete)
+ self._preferences_dialog.connect('response', lambda dialog, response_id: self._preferences_dialog.hide())
+
+ def show(self):
+ self._preferences_dialog.present()
+
+ def _on_show_time_check_toggled(self, widget):
+ self.emit('show-remaining-time-changed', widget.props.active)
+
+ def _on_play_sound_check_toggled(self, widget):
+ self.emit('play-sound-changed', widget.props.active)
+
+ def _on_use_custom_sound_radio_toggled(self, widget):
+ self.emit('use-custom-sound-changed', widget.props.active)
+
+ def _on_popup_notification_toggled(self, widget):
+ """Emit a signal when `self._popup_notification_check` gets toggled in
+ the Preferences dialog window."""
+ self.emit('show-popup-notification-changed', widget.props.active)
+
+ def _on_pulsing_icon_toggled(self, widget):
+ """Emit a signal when `self._popup_notification_check` gets toggled in
+ the Preferences dialog window."""
+ self.emit('show-pulsing-icon-changed', widget.props.active)
+
+ def _on_sound_chooser_button_selection_changed(self, chooser_button):
+ filename = chooser_button.get_filename()
+
+ # Work around an issue where calling set_filename() will cause
+ # 3 selection-changed signals to be emitted: first two will have a None filename
+ # and the third will finally have the desired filename.
+ if filename is not None:
+ print 'Custom sound changed to path: %s' % filename
+ self.emit('custom-sound-path-changed', filename)
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'show-remaining-time':
+ self._show_time_check.props.active = value
+ elif pspec.name == 'play-sound':
+ self._play_sound_check.props.active = value
+ self._play_sound_box.props.sensitive = value
+ elif pspec.name == 'use-custom-sound':
+ if value == True:
+ self._use_custom_sound_radio.props.active = True
+ self._sound_chooser_button.props.sensitive = True
+ else:
+ # Note: Setting _use_custom_sound_radio.props.active to False
+ # does not automatically set _use_default_sound_radio.props.active to True
+ self._use_default_sound_radio.props.active = True
+ self._sound_chooser_button.props.sensitive = False
+ elif pspec.name == 'show-popup-notification':
+ self._popup_notification_check.props.active = value
+ elif pspec.name == 'show-pulsing-icon':
+ self._pulsing_icon_check.props.active = value
+ elif pspec.name == 'custom-sound-path':
+ # Prevent infinite loop of events.
+ if self._sound_chooser_button.get_filename() != value:
+ self._sound_chooser_button.set_filename(value)
diff --git a/timer-applet/src/timerapplet/ui/PulseButton.py b/timer-applet/src/timerapplet/ui/PulseButton.py
new file mode 100644
index 00000000..8a9fe48c
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/PulseButton.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import math
+import time
+import gobject
+import gtk
+
+class PulseButton(gtk.Button):
+ def __init__(self):
+ super(PulseButton, self).__init__()
+
+ self._anim_period_seconds = 0.7
+ self._start_time = 0.0
+ self._factor = 0.0
+
+ def start_pulsing(self):
+ self._start_time = time.time()
+ gobject.timeout_add(10, self._on_timeout)
+
+ def stop_pulsing(self):
+ self._start_time = 0
+
+ def _on_timeout(self, data=None):
+ if self._start_time <= 0.0:
+ return False
+ if self.window != None:
+ delta = time.time() - self._start_time
+
+ if delta > self._anim_period_seconds:
+ delta = self._anim_period_seconds
+ self._start_time = time.time()
+ fraction = delta/self._anim_period_seconds
+ self._factor = math.sin(fraction * math.pi)
+
+ self.window.invalidate_rect(self.allocation, True)
+ return True
+
+ def do_expose_event(self, event):
+ gtk.Button.do_expose_event(self, event)
+ if self._start_time > 0:
+ context = event.window.cairo_create()
+ context.rectangle(0, 0, self.allocation.width, self.allocation.height)
+
+ #color = self.style.bg[gtk.STATE_SELECTED]
+ #color = gtk.gdk.Color(65535, 65535, 65535)
+ color = gtk.gdk.Color(0, 0, 0)
+ red = color.red / 65535.0
+ green = color.green / 65535.0
+ blue = color.blue / 65535.0
+ context.set_source_rgba(red, green, blue, self._factor * 0.8)
+ context.fill()
+
+ return False
+
+gobject.type_register(PulseButton)
diff --git a/timer-applet/src/timerapplet/ui/ScrollableButtonList.py b/timer-applet/src/timerapplet/ui/ScrollableButtonList.py
new file mode 100644
index 00000000..2f8bb378
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/ScrollableButtonList.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gtk
+
+def get_scroll_value_to_reveal_widget(widget_rect, viewport_rect, scroll_val):
+ if widget_rect.y < scroll_val:
+ scroll_val = widget_rect.y
+ elif widget_rect.y + widget_rect.height + 1 > scroll_val + viewport_rect.height:
+ scroll_val = widget_rect.y + widget_rect.height + 1 - viewport_rect.height
+ return scroll_val
+
+class ScrollableButtonList(gtk.ScrolledWindow):
+ def __init__(self):
+ gtk.ScrolledWindow.__init__(self)
+
+ self._vadjust = gtk.Adjustment()
+ self._vbox = gtk.VBox()
+ self._viewport = gtk.Viewport()
+
+ self.set_vadjustment(self._vadjust)
+ self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ self._viewport.modify_bg(gtk.STATE_NORMAL, self.style.base[gtk.STATE_NORMAL])
+
+ self._viewport.add(self._vbox)
+ self.add(self._viewport)
+
+ self._vbox.show()
+ self._viewport.show()
+
+ def add_button(self, button):
+ button.connect('focus-in-event', self._on_focus)
+ button.connect('clicked', self._on_clicked)
+ self._vbox.pack_start(button, False, False)
+
+ def get_buttons(self):
+ return self._vbox.get_children()
+
+ def _on_focus(self, widget, event):
+ self._scroll_to_widget(widget)
+
+ def _on_clicked(self, widget):
+ self._scroll_to_widget(widget)
+
+ def _scroll_to_widget(self, widget):
+ cur_val = self._vadjust.get_value()
+ new_val = get_scroll_value_to_reveal_widget(widget.allocation, self._viewport.allocation, cur_val)
+
+ if new_val != cur_val:
+ self._vadjust.set_value(new_val)
+ self._vadjust.value_changed()
+
diff --git a/timer-applet/src/timerapplet/ui/StartNextTimerDialog.py b/timer-applet/src/timerapplet/ui/StartNextTimerDialog.py
new file mode 100644
index 00000000..ad9aca1b
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/StartNextTimerDialog.py
@@ -0,0 +1,66 @@
+# -*- mode: python; coding: utf-8; -*-
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+# 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
+
+"""
+The Start Next Timer dialog
+
+When a timer ended and the auto_start option was disabled this dialog shows up.
+"""
+
+from gettext import gettext as _
+import gtk
+
+class StartNextTimerDialog(object):
+ def __init__(self, glade_file_name, header_text, body_text):
+ # TODO: Include next_timer in body_text
+ self._dialog = gtk.Dialog(
+ _("Start Next Timer"),
+ None,
+ gtk.DIALOG_DESTROY_WITH_PARENT,
+ (_("_Don't start next timer"), gtk.RESPONSE_CLOSE,
+ _("_Start next timer"), gtk.RESPONSE_YES))
+ self._dialog.props.border_width = 6
+ self._dialog.props.has_separator = False
+ self._dialog.props.resizable = False
+ self._dialog.vbox.props.spacing = 12
+ self._dialog.set_default_response(gtk.RESPONSE_YES)
+
+ hbox = gtk.HBox(False, 0)
+ hbox.props.spacing = 12
+ hbox.props.border_width = 6
+
+ image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
+ image.props.yalign = 0.0
+
+ label = gtk.Label('<span weight="bold" size="larger">%s</span>\n\n%s' % (header_text, body_text))
+ label.props.use_markup = True
+ label.props.wrap = True
+ label.props.yalign = 0.0
+
+ hbox.pack_start(image, False, False, 0)
+ hbox.pack_start(label, False, False, 0)
+ self._dialog.vbox.pack_start(hbox, False, False, 0)
+
+ hbox.show_all()
+
+ def get_response(self):
+ dialog_result = self._dialog.run()
+ self._dialog.hide()
+ if dialog_result == gtk.RESPONSE_YES:
+ return True
+ else:
+ return False
+
diff --git a/timer-applet/src/timerapplet/ui/StartTimerDialog.py b/timer-applet/src/timerapplet/ui/StartTimerDialog.py
new file mode 100644
index 00000000..69728c73
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/StartTimerDialog.py
@@ -0,0 +1,267 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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
+
+import gobject
+import gtk
+import gtk.glade as glade
+import gtk.gdk as gdk
+import pango
+
+from gettext import gettext as _
+from shlex import split as shell_tokenize
+from subprocess import check_call, CalledProcessError
+
+from DurationChooser import DurationChooser
+from ScrollableButtonList import ScrollableButtonList
+
+class StartTimerDialog(gobject.GObject):
+ __gsignals__ = {'clicked-start':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'clicked-cancel':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'clicked-manage-presets':
+ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'clicked-save':
+ (gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,
+ gobject.TYPE_INT,
+ gobject.TYPE_INT,
+ gobject.TYPE_INT,
+ gobject.TYPE_STRING,
+ gobject.TYPE_STRING,
+ gobject.TYPE_BOOLEAN)),
+ 'clicked-preset':
+ (gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,)),
+ 'double-clicked-preset':
+ (gobject.SIGNAL_RUN_LAST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))}
+
+ def __init__(self, glade_file_name, name_validator_func, presets_store, preset_display_func):
+ gobject.GObject.__init__(self)
+
+ self._valid_name_func = name_validator_func;
+ self._presets_store = presets_store
+ self._preset_display_func = preset_display_func
+
+ self._presets_list = ScrollableButtonList()
+ labels_size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+ self._duration_chooser = DurationChooser(labels_size_group)
+
+ glade_widgets = glade.XML(glade_file_name, 'start_timer_dialog')
+ self._dialog = glade_widgets.get_widget('start_timer_dialog')
+ self._ok_button = glade_widgets.get_widget('ok_button')
+ name_label = glade_widgets.get_widget('name_label')
+ self._name_entry = glade_widgets.get_widget('name_entry')
+ self._save_button = glade_widgets.get_widget('save_button')
+ duration_chooser_container = glade_widgets.get_widget('duration_chooser_container')
+ presets_chooser_container = glade_widgets.get_widget('presets_chooser_container')
+ self._presets_section = glade_widgets.get_widget('presets_section')
+ #: The TextEntry control for running a custom command
+ self._command_entry = glade_widgets.get_widget('command_entry')
+ #: The "Invalid Command" label
+ self._invalid_cmd_label = glade_widgets.get_widget('invalid_command_label')
+ #: The next timer combo box
+ self._next_timer_combo = glade_widgets.get_widget('next_timer_combo_entry')
+ self._next_timer_combo.set_model(self._presets_store)
+ self._next_timer_combo.set_text_column(0) # The column to be shown
+ #: The auto-start check button.
+ self._auto_start_check = glade_widgets.get_widget('auto_start_check')
+
+ labels_size_group.add_widget(name_label)
+ self._dialog.set_default_response(gtk.RESPONSE_OK)
+ duration_chooser_container.pack_start(self._duration_chooser)
+ presets_chooser_container.pack_start(self._presets_list)
+
+ self._dialog.connect('response', self._on_dialog_response)
+ self._dialog.connect('delete-event', self._dialog.hide_on_delete)
+ self._dialog.add_events(gdk.BUTTON_PRESS_MASK)
+ self._duration_chooser.connect('duration-changed', self._on_duration_changed)
+ self._name_entry.connect('changed', self._on_name_entry_changed)
+ self._save_button.connect('clicked', self._on_save_button_clicked)
+ # Check that executable is valid while inserting text
+ self._command_entry.connect('changed', self._check_is_valid_command)
+ self._next_timer_combo.child.connect("changed",
+ self._on_next_timer_combo_entry_child_changed)
+ glade_widgets.get_widget('manage_presets_button').connect('clicked',
+ self._on_manage_presets_button_clicked)
+ self._presets_store.connect('row-deleted',
+ lambda model, row_path: self._update_presets_list())
+ self._presets_store.connect('row-changed',
+ lambda model, row_path, row_iter: self._update_presets_list())
+
+ self._update_presets_list()
+ self._duration_chooser.show()
+ self._presets_list.show()
+
+ def show(self):
+ if not self._dialog.props.visible:
+ self._duration_chooser.clear()
+ self._duration_chooser.focus_hours()
+ self._name_entry.set_text('')
+ self._check_for_valid_start_timer_input()
+ self._check_for_valid_save_preset_input()
+ self._dialog.present()
+
+ def hide(self):
+ self._dialog.hide()
+
+ def get_control_data(self):
+ """Return name and duration in a tuple.
+
+ The returned tuple is in this format:
+
+ (name, hours, minutes, seconds, next_timer, auto_start)
+
+ """
+ return (self._name_entry.get_text().strip(),) + \
+ self._duration_chooser.get_duration() + \
+ (self._command_entry.get_text().strip(),
+ self._next_timer_combo.child.get_text().strip(),
+ self._auto_start_check.get_active())
+
+ def set_name_and_duration(self, name, hours, minutes, seconds, *args):
+ self._name_entry.set_text(name)
+ self._command_entry.set_text(args[0])
+ self._next_timer_combo.child.set_text(args[1])
+ self._auto_start_check.set_active(args[2])
+ self._duration_chooser.set_duration(hours, minutes, seconds)
+
+ def _update_presets_list(self):
+ self._check_for_valid_save_preset_input()
+
+ if len(self._presets_store) == 0:
+ self._presets_section.hide()
+
+ # Make window shrink
+ self._dialog.resize(1, 1)
+ else:
+ self._presets_section.show()
+
+ for button in self._presets_list.get_buttons():
+ button.destroy()
+
+ row_iter = self._presets_store.get_iter_first()
+ while row_iter is not None:
+ name = self._preset_display_func(row_iter)
+ label = gtk.Label(name)
+ label.set_ellipsize(pango.ELLIPSIZE_END)
+ button = gtk.Button()
+ button.set_relief(gtk.RELIEF_NONE)
+ button.add(label)
+ self._presets_list.add_button(button)
+
+ button.connect('clicked',
+ self._on_preset_button_clicked,
+ self._presets_store.get_path(row_iter))
+ button.connect('button_press_event',
+ self._on_preset_button_double_clicked,
+ self._presets_store.get_path(row_iter))
+
+ label.show()
+ button.show()
+
+ row_iter = self._presets_store.iter_next(row_iter)
+
+ def _check_is_valid_command(self, widget, data=None):
+ """
+ Check that input in the command entry TextBox control is a valid
+ executable.
+ """
+ try:
+ data = widget.get_text()
+ executable = shell_tokenize(data)[0]
+ # Check if command in path, else raise CalledProcessError
+ # The idea of using `which` to check if a command is in PATH
+ # originated from the Python mailing list.
+ check_call(['which', executable])
+ self._invalid_cmd_label.set_label('')
+ except (ValueError, IndexError, CalledProcessError):
+ self._invalid_cmd_label.set_label(_("<b>Command not found.</b>"))
+ if data is '':
+ self._invalid_cmd_label.set_label('')
+
+ def _non_zero_duration(self):
+ (hours, minutes, seconds) = self._duration_chooser.get_duration()
+ return (hours > 0 or minutes > 0 or seconds > 0)
+
+ def _check_for_valid_save_preset_input(self):
+ self._save_button.props.sensitive = (self._non_zero_duration() and
+ self._valid_name_func(self._name_entry.get_text()))
+ # TODO: Add validator for next_timer_combo
+
+ def _check_for_valid_start_timer_input(self):
+ self._ok_button.props.sensitive = self._non_zero_duration()
+
+ def _on_preset_button_clicked(self, button, row_path):
+ self.emit('clicked-preset', row_path)
+
+ def _on_preset_button_double_clicked(self, button, event, row_path):
+ """Emit the `double-clicked-preset' signal."""
+ # Check that the double-click event shot off on the preset button
+ if event.type == gdk._2BUTTON_PRESS:
+ self.emit('double-clicked-preset', row_path)
+
+ def _on_manage_presets_button_clicked(self, button):
+ self.emit('clicked-manage-presets')
+
+ def _on_duration_changed(self, data=None):
+ self._check_for_valid_start_timer_input()
+ self._check_for_valid_save_preset_input()
+
+ def _on_name_entry_changed(self, entry):
+ self._check_for_valid_save_preset_input()
+
+ def _on_dialog_response(self, dialog, response_id):
+ if response_id == gtk.RESPONSE_OK:
+ self._duration_chooser.normalize_fields()
+ self.emit('clicked-start')
+ elif response_id == gtk.RESPONSE_CANCEL:
+ self.emit('clicked-cancel')
+ self._dialog.hide()
+
+ def _on_save_button_clicked(self, button):
+ self._duration_chooser.normalize_fields()
+ (hours, minutes, seconds) = self._duration_chooser.get_duration()
+ name = self._name_entry.get_text()
+ command = self._command_entry.get_text()
+ next_timer = self._next_timer_combo.child.get_text()
+ auto_start = self._auto_start_check.get_active()
+ self.emit('clicked-save', name, hours, minutes, seconds, command,
+ next_timer, auto_start)
+
+ def _on_next_timer_combo_entry_child_changed(self, widget, data=None):
+ """Validate selection of the Next Timer ComboBoxEntry."""
+ modelfilter = self._presets_store.filter_new()
+ # Loop through all rows in ListStore
+ # TODO: Using a generator may be more memory efficient in this case.
+ for row in modelfilter:
+ # Check that name of preset is the exact match of the the text in
+ # the ComboBoxEntry
+ if widget.get_text() == row[0]:
+ # Yes, it matches! Make the auto-start checkbox sensitive
+ # (activate it).
+ self._auto_start_check.set_sensitive(True)
+ break
+ else:
+ # If value of ComboBoxEntry is None then de-activate the
+ # auto-start checkbox.
+ self._auto_start_check.set_sensitive(False)
+
diff --git a/timer-applet/src/timerapplet/ui/StatusButton.py b/timer-applet/src/timerapplet/ui/StatusButton.py
new file mode 100644
index 00000000..cde076fe
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/StatusButton.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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
+
+import gtk
+from PulseButton import PulseButton
+from PieMeter import PieMeter
+
+class StatusButton(PulseButton):
+ def __init__(self):
+ PulseButton.__init__(self)
+
+ self._tooltip = gtk.Tooltip()
+
+ self._icon_widget = gtk.Image()
+ self._pie_meter = PieMeter()
+ self._label_widget = gtk.Label()
+ self._visual_box = gtk.HBox()
+
+ self._visual_box.pack_start(self._icon_widget)
+ self._visual_box.pack_start(self._pie_meter)
+
+ self._layout_box = None
+ self._use_vertical = None
+ self.set_use_vertical_layout(False)
+
+ # _pie_meter will default to visible while
+ # _icon_widget will default to hidden.
+ self._pie_meter.show()
+ self._visual_box.show()
+ self._label_widget.show()
+
+ def set_tooltip(self, tip_text):
+ self._tooltip.set_text(tip_text)
+
+ def set_label(self, text):
+ self._label_widget.set_text(text)
+
+ def set_icon(self, image_path):
+ self._icon_widget.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(image_path, -1, 20))
+ #elf._icon_widget.set_from_file(image_path)
+
+ def set_use_icon(self, use_icon):
+ if use_icon:
+ self._pie_meter.hide()
+ self._icon_widget.show()
+ else:
+ self._pie_meter.show()
+ self._icon_widget.hide()
+
+ def set_sensitized(self, sensitized):
+ self._label_widget.props.sensitive = sensitized
+
+ def set_show_remaining_time(self, show_remaining_time):
+ if show_remaining_time:
+ self._label_widget.show()
+ else:
+ self._label_widget.hide()
+
+ def set_progress(self, progress):
+ self._pie_meter.set_progress(progress)
+
+ def set_use_vertical_layout(self, use_vertical):
+ if self._use_vertical == use_vertical:
+ return
+
+ self._use_vertical = use_vertical
+ if self._layout_box is not None:
+ self._layout_box.remove(self._visual_box)
+ self._layout_box.remove(self._label_widget)
+ self.remove(self._layout_box)
+ self._layout_box.destroy()
+ self._layout_box = None
+
+ new_layout_box = None
+ if self._use_vertical:
+ new_layout_box = gtk.VBox(False, 2)
+ else:
+ new_layout_box = gtk.HBox(False, 2)
+
+ new_layout_box.pack_start(self._visual_box, True, True, 0)
+ new_layout_box.pack_start(self._label_widget, False, False, 0)
+
+ self._layout_box = new_layout_box
+ self.add(self._layout_box)
+ self._layout_box.show()
+
+ def set_pie_fill_color(self, red, green, blue):
+ self._pie_meter.set_fill_color(red, green, blue)
diff --git a/timer-applet/src/timerapplet/ui/__init__.py b/timer-applet/src/timerapplet/ui/__init__.py
new file mode 100644
index 00000000..2b0b05ba
--- /dev/null
+++ b/timer-applet/src/timerapplet/ui/__init__.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+#
+# 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 AddEditPresetDialog import AddEditPresetDialog
+from ContinueTimerDialog import ContinueTimerDialog
+from DurationChooser import DurationChooser
+from ManagePresetsDialog import ManagePresetsDialog
+from Notifier import Notifier
+from PieMeter import PieMeter
+from PreferencesDialog import PreferencesDialog
+from PulseButton import PulseButton
+from ScrollableButtonList import ScrollableButtonList
+from StartTimerDialog import StartTimerDialog
+from StartNextTimerDialog import StartNextTimerDialog
+from StatusButton import StatusButton
diff --git a/timer-applet/src/timerapplet/utils.py b/timer-applet/src/timerapplet/utils.py
new file mode 100644
index 00000000..53bfdc21
--- /dev/null
+++ b/timer-applet/src/timerapplet/utils.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2008 Jimmy Do <[email protected]>
+# Copyright (C) 2010 Kenny Meyer <[email protected]>
+#
+# 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 gettext import gettext as _
+
+def is_valid_preset_name(name_str, preset_store, allowed_names=()):
+ if len(name_str) == 0:
+ return False
+
+ name_str = name_str.lower()
+ for allowed in allowed_names:
+ if name_str == allowed.lower():
+ return True
+
+ return not preset_store.preset_name_exists_case_insensitive(name_str)
+
+def seconds_to_hms(total_seconds):
+ (hours, remaining_seconds) = divmod(total_seconds, 3600)
+ (minutes, seconds) = divmod(remaining_seconds, 60)
+ return (hours, minutes, seconds)
+
+def hms_to_seconds(hours, minutes, seconds):
+ return hours * 3600 + minutes * 60 + seconds
+
+def construct_time_str(remaining_seconds, show_all=True):
+ """Return a user-friendly representation of remaining time based on the given number of seconds.
+
+ show_all specifies whether the returned string should show all time components.
+ If show_all is True (default), the returned string is in HH:MM:SS format.
+ If show_all is False, the returned string is in either HH:MM or MM:SS format,
+ depending on how much time is remaining. This avoids showing the user more
+ information than necessary.
+
+ """
+ hours, minutes, seconds = seconds_to_hms(remaining_seconds)
+ if show_all:
+ # HH:MM:SS
+ return _('%02d:%02d:%02d') % (hours, minutes, seconds)
+ else:
+ if hours > 0 or minutes > 14:
+ # HH:MM
+ return _('%02d:%02d') % (hours, minutes)
+ else:
+ # MM:SS
+ return _('%02d:%02d') % (minutes, seconds)
+
+def get_display_text_from_datetime(date_time):
+ return date_time.strftime('%X')
+
+def get_preset_display_text(presets_store, row_iter):
+ (name, hours, minutes, seconds, command, next_timer, auto_start) = \
+ presets_store.get_preset(row_iter)
+
+ # <preset name> (HH:MM:SS)
+ return _('%s (%02d:%02d:%02d)') % (name, hours, minutes, seconds)
+
+def serialize_bool(boolean):
+ if boolean:
+ return "1"
+ return "0"
+
+def deserialize_bool(string):
+ if string == "1":
+ return True
+ return False