summaryrefslogtreecommitdiff
path: root/timer-applet/src/timerapplet/ui/StartTimerDialog.py
blob: 3d0b4f49f535fa59849bcb6abc0b96b017230fc1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# Copyright (C) 2008 Jimmy Do <jimmydo@users.sourceforge.net>
# Copyright (C) 2010 Kenny Meyer <knny.myer@gmail.com>
#
# 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 St, Fifth Floor, Boston, MA  02110-1301  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)