diff options
46 files changed, 5192 insertions, 2 deletions
diff --git a/Makefile.am b/Makefile.am index b9c35b43..6fe592fc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -39,6 +39,10 @@ if BUILD_MIXER_APPLET mixer_applet_SUBDIR = mixer endif +if BUILD_TIMER_APPLET +timer_applet_SUBDIR = timer-applet +endif + always_built_SUBDIRS = \ charpick \ drivemount \ @@ -59,7 +63,8 @@ SUBDIRS = \ $(accessx_status_SUBDIR) \ $(invest_applet_SUBDIR) \ $(cpufreq_SUBDIR) \ - $(mixer_applet_SUBDIR) + $(mixer_applet_SUBDIR) \ + $(timer_applet_SUBDIR) DIST_SUBDIRS = \ po \ @@ -78,6 +83,7 @@ DIST_SUBDIRS = \ cpufreq \ invest-applet \ mixer \ + timer-applet \ null_applet EXTRA_DIST = \ diff --git a/configure.in b/configure.in index 32e4be9c..8ac89658 100644 --- a/configure.in +++ b/configure.in @@ -1,7 +1,7 @@ dnl *************************************************************************** dnl *** configure.in for MATE-APPLETS *** dnl *************************************************************************** -AC_INIT(mate-applets, 1.2.3) +AC_INIT(mate-applets, 1.3.0) AC_PREREQ(2.59) AM_CONFIG_HEADER(config.h) @@ -478,6 +478,15 @@ enable_stickynotes="yes" AM_CONDITIONAL(BUILD_STICKYNOTES_APPLET, test "x$enable_stickynotes" = "xyes") dnl *************************************************************************** +dnl *** Timer applet specific checks *** +dnl *************************************************************************** +AC_ARG_ENABLE([timer-applet], + AC_HELP_STRING([--enable-timer-applet], [Enable the timer applet.]), + enable_timerapplet=$enableval, + enable_timerapplet=no) +AM_CONDITIONAL(BUILD_TIMER_APPLET, test "x$enable_timerapplet" = "xyes") + +dnl *************************************************************************** dnl *** keyboard accessibility status applet check *** dnl *************************************************************************** @@ -740,6 +749,14 @@ cpufreq/src/Makefile cpufreq/src/cpufreq-selector/Makefile cpufreq/pixmaps/Makefile cpufreq/help/Makefile +timer-applet/Makefile +timer-applet/data/Makefile +timer-applet/images/Makefile +timer-applet/src/Makefile +timer-applet/src/timerapplet/Makefile +timer-applet/src/timerapplet/controllers/Makefile +timer-applet/src/timerapplet/core/Makefile +timer-applet/src/timerapplet/ui/Makefile null_applet/Makefile ]) @@ -773,6 +790,7 @@ mate-applets-$VERSION configure summary: - multiload $build_gtop_applets - stickynotes $enable_stickynotes - trashapplet always + - timer-applet $enable_timerapplet Using DBUS: $HAVE_DBUS Using NetworkManager: $HAVE_NETWORKMANAGER diff --git a/po/POTFILES.in b/po/POTFILES.in index 7d86c6bc..889ae781 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -98,6 +98,17 @@ stickynotes/stickynotes.schemas.in stickynotes/stickynotes_applet.c stickynotes/stickynotes_applet_callbacks.c stickynotes/stickynotes_callbacks.c +timer-applet/data/TimerApplet.server.in.in +timer-applet/data/TimerApplet.xml +timer-applet/data/timer-applet.glade +timer-applet/data/timer-applet.schemas.in +timer-applet/src/timerapplet/controllers/GlobalController.py +timer-applet/src/timerapplet/controllers/TimerApplet.py +timer-applet/src/timerapplet/ui/ContinueTimerDialog.py +timer-applet/src/timerapplet/ui/StartTimerDialog.py +timer-applet/src/timerapplet/ui/StartNextTimerDialog.py +timer-applet/src/timerapplet/ui/DurationChooser.py +timer-applet/src/timerapplet/utils.py [type: gettext/ini]trashapplet/org.mate.applets.TrashApplet.mate-panel-applet.in.in trashapplet/src/trashapplet.c trashapplet/src/trash-empty.c diff --git a/timer-applet/AUTHORS b/timer-applet/AUTHORS new file mode 100644 index 00000000..a12bc88c --- /dev/null +++ b/timer-applet/AUTHORS @@ -0,0 +1,2 @@ +Jimmy Do <[email protected]> +Stefano Karapetsas <[email protected]> diff --git a/timer-applet/Makefile.am b/timer-applet/Makefile.am new file mode 100644 index 00000000..106cc3f9 --- /dev/null +++ b/timer-applet/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = data images src diff --git a/timer-applet/data/Makefile.am b/timer-applet/data/Makefile.am new file mode 100644 index 00000000..043616ad --- /dev/null +++ b/timer-applet/data/Makefile.am @@ -0,0 +1,37 @@ +serverdir = $(libdir)/matecomponent/servers +server_in_files = TimerApplet.server.in +server_DATA = $(server_in_files:.server.in=.server) + +$(server_in_files): $(server_in_files:.server.in=.server.in.in) Makefile + sed -e "s|\@LIBEXECDIR\@|$(pkglibdir)|" $< > $@ + +schemasdir = $(MATECONF_SCHEMA_FILE_DIR) +schemas_in_files = timer-applet.schemas.in +schemas_DATA = $(schemas_in_files:.schemas.in=.schemas) + +pkgdata_DATA = \ + TimerApplet.xml \ + timer-applet.glade + +EXTRA_DIST = \ + TimerApplet.server.in.in \ + $(pkgdata_DATA) \ + $(schemas_in_files) + +CLEANFILES = \ + $(server_DATA) \ + $(server_in_files) \ + $(schemas_DATA) + +@INTLTOOL_SERVER_RULE@ +@INTLTOOL_SCHEMAS_RULE@ + +if MATECONF_SCHEMAS_INSTALL +install-data-local: + MATECONF_CONFIG_SOURCE=$(MATECONF_SCHEMA_CONFIG_SOURCE) \ + $(MATECONFTOOL) --makefile-install-rule $(schemas_DATA) + +uninstall-local: + MATECONF_CONFIG_SOURCE=$(MATECONF_SCHEMA_CONFIG_SOURCE) \ + $(MATECONFTOOL) --makefile-uninstall-rule $(schemas_DATA) +endif diff --git a/timer-applet/data/TimerApplet.server.in.in b/timer-applet/data/TimerApplet.server.in.in new file mode 100644 index 00000000..5c7a826d --- /dev/null +++ b/timer-applet/data/TimerApplet.server.in.in @@ -0,0 +1,22 @@ +<oaf_info> +<oaf_server iid="OAFIID:TimerApplet_Factory" type="exe" location="@LIBEXECDIR@/timer-applet"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:MateComponent/GenericFactory:1.0"/> + <item value="IDL:MateComponent/Unknown:1.0"/> + </oaf_attribute> + <oaf_attribute name="name" type="string" _value="Timer Applet Factory"/> + <oaf_attribute name="description" type="string" _value="A Timer Applet factory that creates Timer Applets"/> +</oaf_server> + +<oaf_server iid="OAFIID:TimerApplet" type="factory" location="OAFIID:TimerApplet_Factory"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:MATE/Vertigo/MatePanelAppletShell:1.0"/> + <item value="IDL:MateComponent/Control:1.0"/> + <item value="IDL:MateComponent/Unknown:1.0"/> + </oaf_attribute> + <oaf_attribute name="name" type="string" _value="Timer"/> + <oaf_attribute name="description" type="string" _value="Start a timer and receive a notification when it is finished"/> + <oaf_attribute name="panel:category" type="string" value="Accessories"/> + <oaf_attribute name="panel:icon" type="string" value="timer-applet.png"/> +</oaf_server> +</oaf_info> diff --git a/timer-applet/data/TimerApplet.xml b/timer-applet/data/TimerApplet.xml new file mode 100644 index 00000000..67df361c --- /dev/null +++ b/timer-applet/data/TimerApplet.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Root> + <popups> + <popup name="button3"> + <menuitem verb="PauseTimer" name="Pause Item" _label="Pa_use"/> + <menuitem verb="ContinueTimer" name="Continue Item" _label="_Continue"/> + <menuitem verb="StopTimer" name="Stop Item" _label="_Stop"/> + <menuitem verb="RestartTimer" name="Restart Item" _label="R_estart"/> + <menuitem verb="StartNextTimer" name="Start next item" _label="Next"/> + <submenu name="Presets" _label="Pre_sets"> + </submenu> + <separator name="Separator1"/> + <menuitem verb="ManagePresets" name="Manage Presets Item" _label="Ma_nage Presets"/> + <menuitem verb="Preferences" pixtype="stock" pixname="gtk-properties" name="Preferences Item" _label="_Preferences"/> + <menuitem verb="About" pixtype="stock" pixname="mate-stock-about" name="About Item" _label="_About"/> + </popup> + </popups> +</Root> + diff --git a/timer-applet/data/timer-applet.glade b/timer-applet/data/timer-applet.glade new file mode 100644 index 00000000..2ac443e6 --- /dev/null +++ b/timer-applet/data/timer-applet.glade @@ -0,0 +1,1302 @@ +<?xml version="1.0"?> +<glade-interface> + <!-- interface-requires gtk+ 2.6 --> + <!-- interface-naming-policy toplevel-contextual --> + <widget class="GtkDialog" id="start_timer_dialog"> + <property name="title" translatable="yes">Start Timer</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <property name="border_width">12</property> + <child> + <widget class="GtkVBox" id="duration_and_name_vbox"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkVBox" id="duration_chooser_container"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Name:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">name_entry</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkExpander" id="expander_command"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <child> + <widget class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <child> + <widget class="GtkComboBoxEntry" id="next_timer_combo_entry"> + <property name="visible">True</property> + </widget> + <packing> + <property name="y_padding">3</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_start_check"> + <property name="label" translatable="yes">Start automatically</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_padding">3</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Define next timer</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="border_width">3</property> + <child> + <widget class="GtkLabel" id="command_label"> + <property name="width_request">50</property> + <property name="visible">True</property> + <property name="has_tooltip">True</property> + <property name="ypad">5</property> + <property name="label" translatable="yes">Execute:</property> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="changed" handler="command_entry_changed_cb"/> + </widget> + <packing> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Run custom command</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="invalid_command_label"> + <property name="visible">True</property> + <property name="app_paintable">True</property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label_command"> + <property name="visible">True</property> + <property name="tooltip" translatable="yes">Run executable after timer finished.</property> + <property name="label" translatable="yes">Advanced</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="save_button"> + <property name="label" translatable="yes">S_ave as Preset</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <widget class="GtkFrame" id="presets_section"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox7"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="spacing">6</property> + <child> + <widget class="GtkVBox" id="presets_chooser_container"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="manage_presets_button"> + <property name="label" translatable="yes">Mana_ge Presets</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes"><b>_Presets</b></property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="cancelbutton1"> + <property name="label">gtk-cancel</property> + <property name="response_id">-6</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="ok_button"> + <property name="label" translatable="yes">S_tart Timer</property> + <property name="response_id">-5</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkDialog" id="manage_presets_dialog"> + <property name="title" translatable="yes">Manage Presets</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox2"> + <property name="visible">True</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox3"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <widget class="GtkTreeView" id="presets_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + </widget> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkButton" id="delete_button"> + <property name="label" translatable="yes">_Delete</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="edit_button"> + <property name="label" translatable="yes">_Edit...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="add_button"> + <property name="label" translatable="yes">_Add...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>_Presets</b></property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="close_button"> + <property name="label">gtk-close</property> + <property name="response_id">-7</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkDialog" id="preferences_dialog"> + <property name="title" translatable="yes">Timer Preferences</property> + <property name="default_height">189</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox3"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="border_width">12</property> + <child> + <widget class="GtkCheckButton" id="show_time_check"> + <property name="label" translatable="yes">_Show remaining time while timer is running</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox6"> + <property name="visible">True</property> + <child> + <widget class="GtkCheckButton" id="play_sound_check"> + <property name="label" translatable="yes">_Play notification sound</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="play_sound_box"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <widget class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <child> + <widget class="GtkRadioButton" id="use_default_sound_radio"> + <property name="label" translatable="yes">Use d_efault sound</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <child> + <widget class="GtkRadioButton" id="use_custom_sound_radio"> + <property name="label" translatable="yes">Use c_ustom sound</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">use_default_sound_radio</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkFileChooserButton" id="sound_chooser_button"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="title" translatable="yes">Choose A Sound File</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="popup_notification_check"> + <property name="label" translatable="yes">_Show popup notification</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip" translatable="yes">_Show popup notification after timer has ended.</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="pulsing_trayicon_check"> + <property name="label" translatable="yes">S_how pulsing panel icon</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip" translatable="yes">Show pulsing panel icon after timer has ended.</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area3"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="close_button"> + <property name="label">gtk-close</property> + <property name="response_id">-7</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkDialog" id="add_edit_preset_dialog"> + <property name="title" translatable="yes">Add Preset</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="border_width">12</property> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <widget class="GtkEntry" id="name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>_Name</b></property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="duration_chooser_container"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <placeholder/> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>_Duration</b></property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <widget class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="tooltip" translatable="yes">Run custom command after timer has ended.</property> + <property name="label" translatable="yes"><b>Custom command</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame4"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <child> + <widget class="GtkEntry" id="next_timer_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_start_check"> + <property name="label" translatable="yes">Initiate automatically</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Interval timer</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="cancel_button"> + <property name="label">gtk-cancel</property> + <property name="response_id">-6</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="ok_button"> + <property name="label">gtk-save</property> + <property name="response_id">-5</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkAboutDialog" id="about_dialog"> + <property name="window_position">center</property> + <property name="type_hint">normal</property> + <property name="copyright">Copyright (c) 2010 by Kenny Meyer. +Copyright (c) 2007 by Jimmy Do</property> + <property name="comments" translatable="yes">A timer applet for the perfect egg and beyond!</property> + <property name="website">https://launchpad.net/timer-applet</property> + <property name="website_label" translatable="yes">Timer Applet</property> + <property name="license"> GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geomraphical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 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., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License.</property> + <property name="authors">Jimmy Do <[email protected]></property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox5"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area5"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> diff --git a/timer-applet/data/timer-applet.schemas.in b/timer-applet/data/timer-applet.schemas.in new file mode 100644 index 00000000..eadc902a --- /dev/null +++ b/timer-applet/data/timer-applet.schemas.in @@ -0,0 +1,87 @@ +<mateconfschemafile> + <schemalist> + <schema> + <key>/schemas/apps/timer-applet/prefs/play_notification_sound</key> + <applyto>/apps/timer-applet/prefs/play_notification_sound</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Play notification sound</short> + <long>Play a notification sound when the timer finishes a countdown</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/use_custom_notification_sound</key> + <applyto>/apps/timer-applet/prefs/use_custom_notification_sound</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Use a custom notification sound</short> + <long>Use the custom notification sound specified in custom_notification_sound_path</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/custom_notification_sound_path</key> + <applyto>/apps/timer-applet/prefs/custom_notification_sound_path</applyto> + <owner>timer-applet</owner> + <type>string</type> + <default></default> + <locale name="C"> + <short>Path to a custom notification sound</short> + <long>Path to a sound file that will be played when the timer finishes a countdown</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/show_pulsing_icon</key> + <applyto>/apps/timer-applet/prefs/show_pulsing_icon</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Show Pulsing icon</short> + <long>Show pulsing tray icon when timer finished</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/show_popup_notification</key> + <applyto>/apps/timer-applet/prefs/show_popup_notification</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Show Popup notification</short> + <long>Show Popup notification in the notification area</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/show_remaining_time</key> + <applyto>/apps/timer-applet/prefs/show_remaining_time</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Show remaining time</short> + <long>Show the remaining time while the timer is running or paused</long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/timer-applet/prefs/play_beep</key> + <applyto>/apps/timer-applet/prefs/play_beep</applyto> + <owner>timer-applet</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Play a beep sound</short> + <long>Play a beep sound on a computer's internal speaker when the timer finishes. This may not have an effect on all computers.</long> + </locale> + </schema> + </schemalist> +</mateconfschemafile> diff --git a/timer-applet/images/Makefile.am b/timer-applet/images/Makefile.am new file mode 100644 index 00000000..fd039a73 --- /dev/null +++ b/timer-applet/images/Makefile.am @@ -0,0 +1,5 @@ +icondir = $(datadir)/pixmaps +icon_DATA = timer-applet.png + +EXTRA_DIST = $(icon_DATA) + diff --git a/timer-applet/images/timer-applet.png b/timer-applet/images/timer-applet.png Binary files differnew file mode 100644 index 00000000..cb54c3c6 --- /dev/null +++ b/timer-applet/images/timer-applet.png diff --git a/timer-applet/images/timer-applet.svg b/timer-applet/images/timer-applet.svg new file mode 100644 index 00000000..8378578e --- /dev/null +++ b/timer-applet/images/timer-applet.svg @@ -0,0 +1,725 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48" + height="48" + id="svg3319" + sodipodi:version="0.32" + inkscape:version="0.45" + version="1.0" + sodipodi:docbase="/home/jimmy/Desktop" + sodipodi:docname="drawing3.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true"> + <defs + id="defs3321"> + <linearGradient + id="linearGradient4195" + inkscape:collect="always"> + <stop + id="stop4197" + offset="0" + style="stop-color:#eeeeee;stop-opacity:1;" /> + <stop + id="stop4199" + offset="1" + style="stop-color:#a6a6a6;stop-opacity:1" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient4181"> + <stop + style="stop-color:#1e1e1e;stop-opacity:1" + offset="0" + id="stop4183" /> + <stop + style="stop-color:#000000;stop-opacity:0" + offset="1" + id="stop4185" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3760"> + <stop + style="stop-color:#969696;stop-opacity:1" + offset="0" + id="stop3762" /> + <stop + style="stop-color:#adadad;stop-opacity:1" + offset="1" + id="stop3764" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3752"> + <stop + style="stop-color:#414141;stop-opacity:1" + offset="0" + id="stop3754" /> + <stop + style="stop-color:#cacaca;stop-opacity:1" + offset="1" + id="stop3756" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3736"> + <stop + style="stop-color:#5c5f67;stop-opacity:1" + offset="0" + id="stop3738" /> + <stop + style="stop-color:#d6d7db;stop-opacity:1" + offset="1" + id="stop3740" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3728"> + <stop + style="stop-color:#54565e;stop-opacity:1;" + offset="0" + id="stop3730" /> + <stop + style="stop-color:#d9dadd;stop-opacity:1" + offset="1" + id="stop3732" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3629"> + <stop + style="stop-color:#eeeeee;stop-opacity:1;" + offset="0" + id="stop3631" /> + <stop + style="stop-color:#b1b1b1;stop-opacity:1" + offset="1" + id="stop3633" /> + </linearGradient> + <linearGradient + id="linearGradient3623" + inkscape:collect="always"> + <stop + id="stop3625" + offset="0" + style="stop-color:#535353;stop-opacity:1;" /> + <stop + id="stop3627" + offset="1" + style="stop-color:#eeeeee;stop-opacity:0.50282484" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3612"> + <stop + style="stop-color:#535353;stop-opacity:1;" + offset="0" + id="stop3614" /> + <stop + style="stop-color:#535353;stop-opacity:0;" + offset="1" + id="stop3616" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3604"> + <stop + style="stop-color:#535353;stop-opacity:1;" + offset="0" + id="stop3606" /> + <stop + style="stop-color:#535353;stop-opacity:0;" + offset="1" + id="stop3608" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3596"> + <stop + style="stop-color:#535353;stop-opacity:1;" + offset="0" + id="stop3598" /> + <stop + style="stop-color:#535353;stop-opacity:0;" + offset="1" + id="stop3600" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3588"> + <stop + style="stop-color:#535353;stop-opacity:1;" + offset="0" + id="stop3590" /> + <stop + style="stop-color:#535353;stop-opacity:0;" + offset="1" + id="stop3592" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3580"> + <stop + style="stop-color:#535353;stop-opacity:1;" + offset="0" + id="stop3582" /> + <stop + style="stop-color:#535353;stop-opacity:0;" + offset="1" + id="stop3584" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3504"> + <stop + style="stop-color:#353535;stop-opacity:1;" + offset="0" + id="stop3506" /> + <stop + style="stop-color:#656565;stop-opacity:1" + offset="1" + id="stop3508" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3460"> + <stop + style="stop-color:#2e3436;stop-opacity:1;" + offset="0" + id="stop3462" /> + <stop + style="stop-color:#7c7c7c;stop-opacity:1" + offset="1" + id="stop3464" /> + </linearGradient> + <linearGradient + id="linearGradient3452"> + <stop + id="stop3454" + offset="0" + style="stop-color:#6e6e6e;stop-opacity:1;" /> + <stop + style="stop-color:#b4b4b4;stop-opacity:1;" + offset="0.5" + id="stop3456" /> + <stop + id="stop3458" + offset="1" + style="stop-color:#6e6e6e;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3440"> + <stop + style="stop-color:#969696;stop-opacity:1;" + offset="0" + id="stop3442" /> + <stop + id="stop3450" + offset="0.5" + style="stop-color:#d2d2d2;stop-opacity:1;" /> + <stop + style="stop-color:#969696;stop-opacity:1;" + offset="1" + id="stop3444" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient3358"> + <stop + style="stop-color:#ebeceb;stop-opacity:1" + offset="0" + id="stop3360" /> + <stop + style="stop-color:#82847f;stop-opacity:1" + offset="1" + id="stop3362" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3358" + id="linearGradient3380" + gradientUnits="userSpaceOnUse" + x1="23.132887" + y1="9.5372581" + x2="24.583008" + y2="36.468082" + gradientTransform="matrix(1,0,0,-1,0.2614378,42.815504)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3358" + id="linearGradient3393" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,5.4277591e-2,37.567445)" + x1="23.132887" + y1="9.5372581" + x2="24.583008" + y2="36.468082" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3358" + id="linearGradient3406" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,5.4277591e-2,37.567445)" + x1="23.132887" + y1="9.5372581" + x2="24.583008" + y2="36.468082" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3358" + id="linearGradient3410" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,5.4277591e-2,37.567445)" + x1="23.132887" + y1="9.5372581" + x2="24.583008" + y2="36.468082" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3452" + id="linearGradient3438" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,0.8829184,34.874362)" + x1="3.2975602" + y1="-2.7816784" + x2="43.998512" + y2="-2.7816784" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3440" + id="linearGradient3446" + x1="7.5087595" + y1="22.312136" + x2="42.753796" + y2="22.312136" + gradientUnits="userSpaceOnUse" + spreadMethod="pad" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3460" + id="linearGradient3466" + x1="25.795841" + y1="28.020357" + x2="24.873999" + y2="3.4578571" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4195" + id="linearGradient3502" + x1="24.833334" + y1="25.916666" + x2="23.916668" + y2="15.041666" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3504" + id="linearGradient3510" + x1="27.875" + y1="29.25" + x2="24.138199" + y2="5.833334" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3580" + id="linearGradient3586" + x1="26.77684" + y1="28.044653" + x2="28.586987" + y2="29.712654" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3588" + id="linearGradient3594" + x1="26.763739" + y1="15.459818" + x2="28.632809" + y2="13.732889" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3596" + id="linearGradient3602" + x1="22.943235" + y1="15.459818" + x2="20.897388" + y2="13.497188" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3604" + id="linearGradient3610" + x1="22.956337" + y1="28.044653" + x2="21.146193" + y2="29.77158" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3612" + id="linearGradient3618" + x1="45.136982" + y1="14.765982" + x2="47.611858" + y2="21.012091" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3623" + id="linearGradient3621" + gradientUnits="userSpaceOnUse" + x1="25.691545" + y1="26.315392" + x2="30.05204" + y2="29.202744" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient3635" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9942059,0,0,1.1100452,0.144079,-2.2891677)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3728" + id="linearGradient3734" + x1="24.188944" + y1="13.705321" + x2="24.13002" + y2="14.97222" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3736" + id="linearGradient3742" + x1="24.875" + y1="29.084894" + x2="24.875" + y2="27.847456" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3752" + id="linearGradient3758" + x1="21.920311" + y1="27.99477" + x2="22.09375" + y2="16.828377" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3760" + id="linearGradient3766" + x1="27.606627" + y1="26.963573" + x2="27.625" + y2="16.828377" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4181" + id="radialGradient4187" + cx="25.573694" + cy="38.689762" + fx="25.573694" + fy="38.689762" + r="20.977501" + gradientTransform="matrix(1,0,0,0.1460674,0,33.038449)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4195" + id="linearGradient2246" + gradientUnits="userSpaceOnUse" + x1="24.833334" + y1="25.916666" + x2="23.916668" + y2="15.041666" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3504" + id="linearGradient2248" + gradientUnits="userSpaceOnUse" + x1="27.875" + y1="29.25" + x2="24.138199" + y2="5.833334" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2250" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9942059,0,0,1.1100452,0.144079,-2.2891677)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2247" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9605798,0,0,0.9793075,0.9789654,0.277812)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4181" + id="radialGradient2256" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.1460674,0,33.038449)" + cx="25.573694" + cy="38.689762" + fx="25.573694" + fy="38.689762" + r="20.977501" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3452" + id="linearGradient2258" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,0.8829184,34.874362)" + x1="3.2975602" + y1="-2.7816784" + x2="43.998512" + y2="-2.7816784" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3440" + id="linearGradient2260" + gradientUnits="userSpaceOnUse" + spreadMethod="pad" + x1="7.5087595" + y1="22.312136" + x2="42.753796" + y2="22.312136" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3460" + id="linearGradient2262" + gradientUnits="userSpaceOnUse" + x1="25.795841" + y1="28.020357" + x2="24.873999" + y2="3.4578571" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4195" + id="linearGradient2264" + gradientUnits="userSpaceOnUse" + x1="24.833334" + y1="25.916666" + x2="23.916668" + y2="15.041666" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3504" + id="linearGradient2266" + gradientUnits="userSpaceOnUse" + x1="27.875" + y1="29.25" + x2="24.138199" + y2="5.833334" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2268" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9605798,0,0,0.9793075,0.9789654,0.277812)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2271" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9605798,0,0,0.9793075,0.2110192,0.277812)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3460" + id="linearGradient2275" + gradientUnits="userSpaceOnUse" + x1="25.795841" + y1="28.020357" + x2="24.873999" + y2="3.4578571" + gradientTransform="translate(-0.7679462,0)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3440" + id="linearGradient2280" + gradientUnits="userSpaceOnUse" + spreadMethod="pad" + x1="7.5087595" + y1="22.312136" + x2="42.753796" + y2="22.312136" + gradientTransform="translate(-0.7679462,0)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3452" + id="linearGradient2283" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,0.1149722,34.874362)" + x1="3.2975602" + y1="-2.7816784" + x2="43.998512" + y2="-2.7816784" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4181" + id="radialGradient2286" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.1460674,0,33.038449)" + cx="25.573694" + cy="38.689762" + fx="25.573694" + fy="38.689762" + r="20.977501" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4195" + id="linearGradient2292" + gradientUnits="userSpaceOnUse" + x1="24.833334" + y1="25.916666" + x2="23.916668" + y2="15.041666" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3504" + id="linearGradient2294" + gradientUnits="userSpaceOnUse" + x1="27.875" + y1="29.25" + x2="24.138199" + y2="5.833334" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2296" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9605798,0,0,0.9793075,0.2110192,0.277812)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3629" + id="linearGradient2299" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.9605798,0,0,0.9793075,0.2197079,-0.3819568)" + x1="24.513035" + y1="16.85784" + x2="24.660349" + y2="25.696674" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + gridtolerance="10000" + guidetolerance="10" + objecttolerance="10" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="24" + inkscape:cx="25.678198" + inkscape:cy="30.891482" + inkscape:document-units="px" + inkscape:current-layer="layer1" + width="48px" + height="48px" + inkscape:window-width="1680" + inkscape:window-height="1000" + inkscape:window-x="0" + inkscape:window-y="0" /> + <metadata + id="metadata3324"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <path + sodipodi:type="arc" + style="fill:url(#radialGradient2286);fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path2237" + sodipodi:cx="25.573694" + sodipodi:cy="38.689762" + sodipodi:rx="20.977501" + sodipodi:ry="3.0641294" + d="M 46.551195 38.689762 A 20.977501 3.0641294 0 1 1 4.5961933,38.689762 A 20.977501 3.0641294 0 1 1 46.551195 38.689762 z" + transform="translate(-1.4676415,0.7071068)" /> + <path + style="fill:url(#linearGradient2283);fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" + d="M 3.7597844,35.215108 C 3.7592897,35.220202 3.7600001,35.337867 3.7597844,35.340108 L 3.7597844,35.371358 C 3.7597103,35.372133 3.7597844,35.433858 3.7597844,35.433858 L 3.7910344,35.433858 L 5.2285344,38.871358 C 11.911415,39.872542 18.429899,40.145888 24.041035,40.090108 C 24.111553,40.089409 24.189559,40.090913 24.259785,40.090108 C 29.870922,40.145891 36.389405,39.87254 43.072285,38.871358 L 44.509785,35.433858 L 44.541035,35.433858 C 44.541034,35.43386 44.541109,35.372131 44.541035,35.371358 L 44.541035,35.340108 C 44.540819,35.337867 44.54153,35.220202 44.541035,35.215108 L 3.7597844,35.215108 z " + id="path3378" /> + <path + style="fill:url(#linearGradient2280);fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" + d="M 22.07987,5.0113468 C 17.153472,5.1087058 12.536218,5.9642818 9.5798698,8.2300955 C 6.090401,11.046588 3.7593887,34.730192 3.7048693,35.292596 L 44.48612,35.292596 C 44.431602,34.730192 42.100588,11.046588 38.61112,8.2300955 C 35.654773,5.9642808 31.037517,5.1087058 26.11112,5.0113468 C 25.485073,4.9989748 24.841291,5.0010938 24.20487,5.0113468 C 24.132068,5.0098478 24.059152,5.0125238 23.98612,5.0113468 C 23.349699,5.0010928 22.705916,4.9989748 22.07987,5.0113468 z " + id="path3395" /> + <path + style="fill:#eeeeee;fill-opacity:0.92655364;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" + d="M 22.155713,5.3748772 C 17.350116,5.4708299 12.846082,6.3140491 9.9622258,8.5471361 C 6.5583227,11.322947 4.2539863,34.664469 4.2008038,35.21875 L 4.4141898,35.21875 C 4.7293798,32.10335 6.8404066,12.189785 10.023194,9.5942895 C 12.907049,7.3612026 17.411083,6.5179834 22.21668,6.4220306 C 22.827375,6.4098373 23.424888,6.4119247 24.045703,6.4220306 C 24.116945,6.4231906 24.218556,6.4205533 24.289573,6.4220306 C 24.910388,6.4119257 25.5079,6.4098373 26.118596,6.4220306 C 30.924193,6.5179834 35.428229,7.3612016 38.312083,9.5942895 C 41.49487,12.189785 43.605898,32.103352 43.921087,35.21875 L 43.982054,35.21875 C 43.928873,34.66447 41.655018,11.322947 38.251115,8.5471361 C 35.367261,6.3140482 30.863225,5.4708299 26.057629,5.3748772 C 25.446933,5.3626839 24.849421,5.3647723 24.228606,5.3748772 C 24.157589,5.3733998 24.055977,5.3760372 23.984736,5.3748772 C 23.363921,5.3647713 22.766408,5.3626839 22.155713,5.3748772 z " + id="path3476" /> + <path + style="fill:#5f5f5f;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" + d="M 43.856735,35.394922 L 42.566993,38.180445 C 35.999987,39.072866 29.594528,39.316522 24.080682,39.266799 C 24.011674,39.267517 23.93502,39.266176 23.865724,39.266799 C 18.351879,39.31652 11.94642,39.072868 5.379413,38.180445 L 4.1203784,35.450633 C 4.1204658,35.47767 4.1204845,35.505345 4.1203784,35.506344 L 4.1203784,35.534198 C 4.1203055,35.534889 4.1203784,35.589909 4.1203784,35.589909 L 4.1510866,35.589909 L 5.5636619,38.653984 C 12.130669,39.546407 18.536128,39.790059 24.049973,39.740338 C 24.119269,39.739715 24.195922,39.741056 24.264931,39.740338 C 29.778777,39.790061 36.184236,39.546405 42.751242,38.653984 L 44.163817,35.589909 L 44.194525,35.589909 C 44.194524,35.589911 44.194598,35.534888 44.194525,35.534198 L 44.194525,35.506344 C 44.194314,35.504346 44.195012,35.399463 44.194525,35.394922 L 43.856735,35.394922 z " + id="path3483" /> + <path + style="fill:none;fill-opacity:1;stroke:url(#linearGradient2275);stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" + d="M 22.090428,5.0211771 C 17.164031,5.1185361 12.546776,5.9741125 9.5904278,8.2399275 C 6.0803209,11.073079 3.7431507,35.108148 3.7154276,35.396178 L 3.7154276,35.427428 C 3.7153535,35.4282 3.7154276,35.489928 3.7154276,35.489928 L 3.7466776,35.489928 L 5.1841776,38.927428 C 11.867058,39.92861 18.385542,40.201957 23.996678,40.146178 C 24.067195,40.145476 24.145202,40.146983 24.215428,40.146178 C 29.826564,40.201957 36.345048,39.92861 43.027928,38.927428 L 44.465428,35.489928 L 44.496678,35.489928 C 44.496678,35.489928 44.496752,35.4282 44.496678,35.427428 L 44.496678,35.396178 C 44.468955,35.108148 42.131785,11.073079 38.621678,8.2399275 C 35.66533,5.9741126 31.048075,5.1185362 26.121678,5.0211771 C 25.495632,5.0088047 24.851849,5.0109237 24.215428,5.0211771 C 24.142627,5.019678 24.06971,5.0223537 23.996678,5.0211771 C 23.360258,5.0109236 22.716474,5.0088047 22.090428,5.0211771 z " + id="rect3335" /> + <path + sodipodi:type="arc" + style="fill:url(#linearGradient2292);fill-opacity:1;stroke:url(#linearGradient2294);stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path3494" + sodipodi:cx="24.833334" + sodipodi:cy="18.833334" + sodipodi:rx="12.5" + sodipodi:ry="12.5" + d="M 37.333334 18.833334 A 12.5 12.5 0 1 1 12.333334,18.833334 A 12.5 12.5 0 1 1 37.333334 18.833334 z" + transform="matrix(0.9615385,0,0,0.9615385,0.2278461,2.8076925)" /> + <rect + style="fill:url(#linearGradient2299);fill-opacity:1;stroke:#535353;stroke-width:0.48494926;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect3512" + width="4.1886005" + height="12.75308" + x="22.011753" + y="14.540128" /> + </g> +</svg> 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 |