1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
14 from datetime import datetime, timedelta
15 from dateutil.relativedelta import relativedelta
24 if len(running_timers()) > 0:
31 def add_key_binding(widget, keyname, callback):
32 accelgroup = gtk.AccelGroup()
33 key, modifier = gtk.accelerator_parse(keyname)
34 accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, callback)
35 widget.add_accel_group(accelgroup)
38 class StockButton(gtk.Button):
39 def __init__(self, label=None, stock=None, use_underline=True, icon_size=None, tooltip=None):
40 if stock is not None and stock in gtk.stock_list_ids():
43 stock_tmp = gtk.STOCK_ABOUT
44 super(self.__class__, self).__init__(stock=stock_tmp, use_underline=use_underline)
46 self.set_markup(label)
49 elif stock not in gtk.stock_list_ids():
51 if icon_size is not None:
52 self.set_icon(stock, icon_size)
53 if tooltip is not None:
54 self.set_tooltip_text(tooltip)
55 def __get_children(self):
56 align = self.get_children()[0]
57 hbox = align.get_children()[0]
58 return hbox.get_children()
59 def set_label(self, label):
60 x, lbl = self.__get_children()
62 def set_markup(self, label):
63 x, lbl = self.__get_children()
65 def set_icon(self, icon, size=gtk.ICON_SIZE_BUTTON):
66 img, x = self.__get_children()
69 img.props.visible = False
71 img.set_from_icon_name(icon, size)
72 img.props.visible = True
74 img.set_from_pixbuf(icon)
75 img.props.visible = True
79 class Clock(gtk.Label):
83 super(gtk.Label, self).__init__()
84 self._format = '%H:%M:%S'
86 self._draining = False
93 def _next_update_interval_ms(self):
95 nextsecond = 1 - (now - int(now))
96 return int(nextsecond * 1000)
98 def update_display(self):
99 face = time.strftime(self._format, time.localtime(time.time()))
100 self.set_markup("<span size='32000'><b>" + face + "</b></span>")
103 if not self._draining:
104 self.update_display()
105 glib.timeout_add(self._next_update_interval_ms(), self._update, priority=glib.PRIORITY_DEFAULT_IDLE)
109 self._draining = True
112 class AlarmClock(gtk.HBox, Clock):
116 'turned-on': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
117 'turned-off': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
118 'alarm': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
122 self.face = AdjustableClock()
123 self.face.hours.range = (0, 23)
124 super(gtk.HBox, self).__init__()
125 super(Clock, self).__init__()
126 self.pack_start(self.face, expand=False, fill=False)
128 self.face.set(now.hour, now.minute, now.second)
133 h, m, s = self.face.components
134 set_datetime = now.replace(hour=h, minute=m, second=s)
135 if set_datetime < now:
136 set_datetime += timedelta(days=1)
137 self.set_time = int(set_datetime.strftime('%s'))
139 self.alarmer = Alarmer()
141 self.emit('turned-on')
144 self.face.format_labels(None, None, None)
146 self._running = False
147 self.emit('turned-off')
149 def update_display(self):
152 if self.set_time <= now:
153 if not self.alarmer.alarmed:
155 self.alarmer.text = "It's passed %02d:%02d:%02d !!" % self.face.components
158 self.face.format_labels(None, None, None, foreground='red')
161 class Stopper(Clock):
165 'started': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
166 'paused': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
171 self._behind_interval = 0
172 self._running = False
173 self._last_paused_time = 0
174 super(self.__class__, self).__init__()
177 if not self._running:
179 if self._start_time == 0:
180 self._start_time = now
181 if self._last_paused_time > 0:
182 self._behind_interval += now - self._last_paused_time
189 self._last_paused_time = time.time()
190 self._running = False
194 def _next_update_interval_ms(self):
197 def update_display(self):
198 if self._start_time > 0:
200 display_time = time.time() - self._start_time - self._behind_interval
202 display_time = self._last_paused_time - self._start_time - self._behind_interval
203 hours = display_time / 3600
204 face = '%s%02d:%02d.%02d' % (
205 ('%dh ' % hours) if hours >= 1 else '',
206 display_time / 60 % 60,
208 (display_time - int(display_time)) * 100,
212 self.set_markup("<span size='32000'><b>" + face + "</b></span>")
215 class SpinLabel(gtk.EventBox):
216 def __init__(self, start_value):
218 self._value = start_value
221 self.label = gtk.Label()
222 super(self.__class__, self).__init__()
224 self.connect('scroll-event', self._event_scroll)
225 self.connect('motion-notify-event', self._event_drag)
226 self.motion_last_pos = 0
227 self.drag_sensitivity = 30
231 return str(self._value)
240 if x > self.range[1]: x = self.range[0]
241 if x < self.range[0]: x = self.range[1]
248 def _event_scroll(self, X, event):
249 if self._locked: return
250 delta = +1 if event.direction == gtk.gdk.SCROLL_UP else -1
252 def _event_drag(self, X, event):
253 if self._locked: return
254 diff = event.y - self.motion_last_pos
255 if abs(diff) > self.drag_sensitivity:
256 delta = +1 if diff < 0 else -1
257 self.motion_last_pos = event.y
259 def _update_label(self):
260 self.label.set_markup(self.format % self._value)
263 class AdjustableClock(gtk.HBox):
264 def __init__(self, color=None):
265 self.hours = SpinLabel(0)
266 self.minutes = SpinLabel(0)
267 self.seconds = SpinLabel(0)
268 self.format_labels('%d:', '%02d:', '%02d')
269 super(gtk.HBox, self).__init__()
270 self.pack_start(self.hours, expand=False, fill=False)
271 self.pack_start(self.minutes, expand=False, fill=False)
272 self.pack_start(self.seconds, expand=False, fill=False)
273 def format_labels(self, fmt_h, fmt_m, fmt_s, **pango_attrs):
274 span_attr = ''.join([" %s='%s'" % (attr.replace('_', '-'), val) for attr, val in pango_attrs.iteritems()])
275 fmt = "<span size='32000'%s><b>%%s</b></span>" % (span_attr,)
276 if fmt_h is not None: self.fmt_h = fmt_h
277 if fmt_m is not None: self.fmt_m = fmt_m
278 if fmt_s is not None: self.fmt_s = fmt_s
279 self.hours.format = fmt % self.fmt_h
280 self.minutes.format = fmt % self.fmt_m
281 self.seconds.format = fmt % self.fmt_s
282 self.hours.value = self.hours.value
283 self.minutes.value = self.minutes.value
284 self.seconds.value = self.seconds.value
286 def components(self):
287 return int(self.hours), int(self.minutes), int(self.seconds)
289 for comp in self.hours, self.minutes, self.seconds:
292 for comp in self.hours, self.minutes, self.seconds:
294 def set(self, h, m, s):
296 self.minutes.value = m
297 self.seconds.value = s
300 class KitchenTimer(gtk.HBox, Clock):
304 'started': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
305 'stopped': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
306 'due': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, ()),
310 self.face = AdjustableClock()
311 self.alarmer = Alarmer()
312 super(gtk.HBox, self).__init__()
313 super(Clock, self).__init__()
314 self.pack_start(self.face, expand=False, fill=False)
318 def scheduled_time(self):
319 h, m, s = self.face.components
320 return h * 3600 + m * 60 + s
323 if not self._running and not self.alarmer.alarmed:
324 self.scheduled_time_components = self.face.components
326 self._future_time = time.time() + self.scheduled_time
331 self.face.format_labels(None, None, None)
332 self._running = False
335 def update_display(self):
337 display_time = self._future_time - time.time()
338 if display_time <= 0:
339 if not self.alarmer.alarmed:
341 self.alarmer.text = "Your %d:%02d:%02d timer is due!" % self.scheduled_time_components
343 self.face.format_labels('-%d:', None, None, foreground='red')
344 self.face.set(abs(display_time) / 3600, abs(display_time) / 60 % 60, abs(display_time) % 60)
347 class Alarmer(object):
354 dialog = gtk.MessageDialog(flags=gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_OK, message_format=self.title)
355 dialog.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLBAR)
356 dialog.set_keep_above(True)
357 dialog.set_skip_taskbar_hint(False)
358 dialog.set_icon_name('alarm-clock')
359 dialog.get_image().set_from_icon_name('alarm-clock', gtk.ICON_SIZE_DIALOG)
360 dialog.set_title(self.title)
361 dialog.format_secondary_text(self.text)
362 btn_ok = dialog.get_widget_for_response(gtk.RESPONSE_OK)
363 btn_ok.connect('clicked', lambda *X: dialog.destroy())
370 class ClockUI(Clock):
376 class StopperUI(gtk.HBox):
379 def __init__(self, *pargs, **kwargs):
380 super(gtk.HBox, self).__init__(*pargs, **kwargs)
381 self.clock = Stopper()
382 self.btn_start = StockButton(stock=gtk.STOCK_MEDIA_PLAY, label='')
383 self.btn_pause = StockButton(stock=gtk.STOCK_MEDIA_PAUSE, label='')
384 self.btn_start.connect('clicked', lambda *X: self.clock.run())
385 self.btn_pause.connect('clicked', lambda *X: self.clock.pause())
386 self.pack_start(self.clock, expand=True, fill=True)
387 self.pack_start(self.btn_start, expand=False, fill=False)
388 self.pack_start(self.btn_pause, expand=False, fill=False)
390 self.clock.connect('started', lambda *X: self.manage_button_states())
391 self.clock.connect('paused', lambda *X: self.manage_button_states())
392 self.manage_button_states()
394 def manage_button_states(self):
395 running = self.clock.is_running
396 self.btn_start.set_sensitive(not running)
397 self.btn_pause.set_sensitive(running)
400 class KitchenTimerUI(gtk.HBox):
401 name = KitchenTimer.name
403 def __init__(self, *pargs, **kwargs):
404 super(gtk.HBox, self).__init__(*pargs, **kwargs)
405 self.clock = KitchenTimer()
406 self.btn_start = StockButton(stock=gtk.STOCK_MEDIA_PLAY, label='')
407 self.btn_start.connect('clicked', lambda *X: self.clock.start())
408 self.btn_stop = StockButton(stock=gtk.STOCK_MEDIA_STOP, label='')
409 self.btn_stop.connect('clicked', lambda *X: self.clock.stop())
410 self.pack_start(self.clock, expand=True, fill=True)
411 self.pack_start(self.btn_start, expand=False, fill=False)
412 self.pack_start(self.btn_stop, expand=False, fill=False)
414 self.clock.connect('started', lambda *X: self.manage_button_states())
415 self.clock.connect('stopped', lambda *X: self.manage_button_states())
416 self.manage_button_states()
418 def manage_button_states(self):
419 running = self.clock.is_running
420 self.btn_start.set_sensitive(not running)
421 self.btn_stop.set_sensitive(running)
424 class AlarmClockUI(gtk.HBox):
425 name = AlarmClock.name
427 def __init__(self, *pargs, **kwargs):
428 super(gtk.HBox, self).__init__(*pargs, **kwargs)
429 self.clock = AlarmClock()
430 self.btn_set = gtk.ToggleButton()
432 img.set_from_icon_name('alarm-clock', size=gtk.ICON_SIZE_BUTTON)
433 self.btn_set.set_image(img)
434 self.btn_set.connect('clicked', lambda event: self.clock.turn_on() if self.btn_set.get_active() else self.clock.turn_off())
435 self.pack_start(self.clock, expand=True, fill=True)
436 self.pack_start(self.btn_set, expand=False, fill=False)
440 class MultiClassInstantiatorBox(gtk.HBox):
441 def __init__(self, classes):
442 super(self.__class__, self).__init__(homogeneous=True, spacing=2)
446 add_widget_button = StockButton(stock=gtk.STOCK_ADD, label=cls.name)
447 add_widget_button.connect('clicked', lambda X, column, cls: self.new_item(column, cls), column, cls)
448 column.pack_start(add_widget_button, expand=False, fill=False)
449 self.pack_start(column, expand=False, fill=False)
450 self.new_item(column, cls)
453 def new_item(self, column, cls):
455 self._items.append(item)
456 removable_box = gtk.HBox()
457 removable_box.inner_widget = item
458 button_remove = StockButton(stock=gtk.STOCK_REMOVE, label='')
459 button_remove.connect('clicked', lambda X, removable_box: self.remove_item(removable_box), removable_box)
460 removable_box.pack_start(item, expand=True, fill=True)
461 removable_box.pack_start(button_remove, expand=False, fill=False)
462 column.pack_start(removable_box, expand=False, fill=False)
469 def remove_item(self, removable_box):
470 item = removable_box.inner_widget
471 if not item.clock.is_running:
472 self.items.remove(item)
475 removable_box.destroy()
478 def running_timers():
479 return [item for item in multibox.items if item.clock.is_running]
482 win_main = gtk.Window(gtk.WINDOW_TOPLEVEL)
485 win_main.set_size_request(800, -1)
486 win_main.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLBAR)
487 win_main.set_keep_above(True)
488 win_main.set_icon_name('alarm-clock')
490 start_evt = win_main.connect('map-event', lambda w,e: (win_main.disconnect(start_evt), win_main_show()))
491 win_main.connect('delete-event', lambda w,e: act_quit())
492 add_key_binding(win_main, '<Ctrl><Shift>Q', lambda *x: act_quit())
494 box0.pack_start(ClockUI(), expand=False, fill=True)
495 multibox = MultiClassInstantiatorBox(classes=(StopperUI, KitchenTimerUI, AlarmClockUI))
496 box0.pack_start(multibox, expand=True, fill=True)