add tools
[hband-tools.git] / xgui-tools / xgcal
blobf40adfa44b16f95ba68f8cf894405d8ce842bd2f
1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
4 import os
5 import sys
6 import signal
7 import gtk
8 import gobject
9 import glib
10 import pango
11 import gettext
12 import re
13 import time
14 from datetime import datetime
15 from dateutil.relativedelta import relativedelta
16 import dbus
17 import dbus.service
18 import dbus.glib
19 import gio
20 import threading
21 import setproctitle
22 import subprocess
23 import traceback
24 import pytz
27 PROGNAME = 'xgcal'
28 APPNAME = 'hu.uucp.%s' % (PROGNAME,)
29 TASKBAR_HEIGHT = 32
30 PMU_CLOCK = "<span size='32000'><b>"
31 PMU_CLOCK_END = "</b></span>"
32 PMU_CLOCK_OTHERTZ = "<span size='32000' color='blue'><b>"
33 PMU_CLOCK_OTHERTZ_END = "</b></span>"
34 PMU_TIMEZONE = "<span color='blue'>"
35 PMU_TIMEZONE_END = "</span>"
36 PMU_SOLARCLOCK = "<span size='24000'>"
37 PMU_SOLARCLOCK_END = "</span>"
38 SUNCLOCK_WINDOW_BOTTOM_MARGIN = 15  # bottom margin (status bar of Sunclock app) is constant this many pixels tall
41 def add_key_binding(widget, keyname, callback):
42         accelgroup = gtk.AccelGroup()
43         key, modifier = gtk.accelerator_parse(keyname)
44         accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, callback)
45         widget.add_accel_group(accelgroup)
47 def display_error(e):
48         if threading.currentThread().name != 'MainThread':
49                 gtk.threads_enter()
50         text = None
51         if isinstance(e, OSError) or isinstance(e, IOError):
52                 text = '%s (#%d)\n%s' % (e.strerror, e.errno, e.filename)
53         elif isinstance(e, Exception):
54                 text = e.message
55         elif type(e) == type([]):
56                 text = ''.join(e)
57         if text is None:
58                 text = str(e)
59         dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
60         dlg.set_title(_("Error"))
61         dlg.run()
62         dlg.destroy()
63         if threading.currentThread().name != 'MainThread':
64                 gtk.threads_leave()
66 class DialogCancel(Exception):
67         pass
69 def question(msg, yes=None, parent=None, cancelable=False, default=gtk.RESPONSE_YES):
70         dlg = gtk.MessageDialog(parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
71         dlg.set_markup(msg)
72         dlg.set_title(_("Question"))
73         if yes is None:
74                 dlg.add_buttons(gtk.STOCK_YES, gtk.RESPONSE_YES)
75         else:
76                 if type(yes) in [type(()), type([])]:
77                         btn_yes = StockButton(label=yes[0], stock=yes[1])
78                         dlg.add_action_widget(btn_yes, gtk.RESPONSE_YES)
79                         btn_yes.show()
80                 else:
81                         dlg.add_buttons(yes, gtk.RESPONSE_YES)
82         dlg.add_buttons(gtk.STOCK_NO, gtk.RESPONSE_NO)
83         if cancelable:
84                 dlg.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
85                 add_key_binding(dlg, 'Escape', lambda a,b,c,d: dlg.response(gtk.RESPONSE_CANCEL) or True)
86         dlg.set_default_response(default)
87         resp = dlg.run()
88         dlg.destroy()
89         if resp == gtk.RESPONSE_CANCEL:
90                 raise DialogCancel()
91         return (resp == gtk.RESPONSE_YES)
93 def set_dialog_icon_name(dlg, icon):
94         img = dlg.get_content_area().children()[0].children()[0]
95         img.set_from_icon_name(icon, gtk.ICON_SIZE_DIALOG)
98 def act_button(func):
99         if type(func) != type(()):
100                 func = (func,)
101         if func[0] is not None:
102                 func[0](*func[1:])
104 def on_click_button(btn, func):
105         act_button(func)
106         return False
108 class StockButton(gtk.Button):
109         def __init__(self, label=None, stock=None, use_underline=True, icon_size=None, tooltip=None):
110                 if stock is not None and stock in gtk.stock_list_ids():
111                         stock_tmp = stock
112                 else:
113                         stock_tmp = gtk.STOCK_ABOUT
114                 super(self.__class__, self).__init__(stock=stock_tmp, use_underline=use_underline)
115                 if label is not None:
116                         self.set_markup(label)
117                 if stock is None:
118                         self.set_icon('')
119                 elif stock not in gtk.stock_list_ids():
120                         self.set_icon(stock)
121                 if icon_size is not None:
122                         self.set_icon(stock, icon_size)
123                 if tooltip is not None:
124                         self.set_tooltip_text(tooltip)
125         def __get_children(self):
126                 align = self.get_children()[0]
127                 hbox = align.get_children()[0]
128                 return hbox.get_children()
129         def set_label(self, label):
130                 x, lbl = self.__get_children()
131                 lbl.set_label(label)
132         def set_markup(self, label):
133                 x, lbl = self.__get_children()
134                 lbl.set_markup(label)
135         def set_icon(self, icon, size=gtk.ICON_SIZE_BUTTON):
136                 img, x = self.__get_children()
137                 if type(icon) == str:
138                         if icon == '':
139                                 img.props.visible = False
140                         else:
141                                 img.set_from_icon_name(icon, size)
142                                 img.props.visible = True
143                 else:
144                         img.set_from_pixbuf(icon)
145                         img.props.visible = True
147 class StockToolButton(gtk.ToolButton):
148         def __init__(self, label=None, stock=None, tooltip=None):
149                 super(self.__class__, self).__init__()
150                 if stock is not None:
151                         if stock in gtk.stock_list_ids():
152                                 if stock is not None: self.set_stock_id(stock)
153                         else:
154                                 self.set_icon_name(stock)
155                 if label is not None:
156                         self.set_label(label)
157                 if tooltip is not None:
158                         self.set_tooltip_text(tooltip)
159         def set_pixbuf(self, pxb):
160                 a = self.get_children()[0]
161                 a = a.get_children()[0]
162                 img, a = a.get_children()
163                 img.set_from_pixbuf(pxb)
164                 img.props.visible = True
165         def __get_children(self):
166                 align = self.get_children()[0]
167                 hbox = align.get_children()[0]
168                 return hbox.get_children()
169         def set_markup(self, markup):
170                 x, lbl = self.__get_children()
171                 lbl.set_markup(markup)
173 class StockMenuItem(gtk.ImageMenuItem):
174         def __init__(self, label=None, stock_id=None, accel_group=None):
175                 gtk.ImageMenuItem.__init__(self, stock_id, accel_group)
176                 if label is not None:
177                         self.set_label(label)
179 class Calendar(gtk.Calendar):
180         from datetime import datetime
181         from dateutil.relativedelta import relativedelta
182         __gsignals__ = {
183                 'marks-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
184                 'mark-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str,)),
185                 'mark-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str,)),
186                 'date-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
187                 'date-unselected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
188         }
189         date_format = '%04d-%02d-%02d'
190         def __init__(self):
191                 self._marks = {}
192                 self.selected_date = (0, 0, 0)
193                 super(self.__class__, self).__init__()
194                 self.set_display_options(self.get_display_options() | gtk.CALENDAR_WEEK_START_MONDAY)
195                 # create popup menu
196                 self.popup_menu = gtk.Menu()
197                 mi = gtk.ImageMenuItem(stock_id=gtk.STOCK_JUMP_TO)
198                 mi.set_label("Go Today")
199                 mi.connect('activate', self.gotoday)
200                 self.popup_menu.append(mi)
201                 self.popup_menu.show_all()
202                 # set initial state
203                 self.connect('key-press-event', self.on_key_press)
204                 self.connect('key-release-event', self.on_key_press_release)
205                 self._block_keypress_release_event = False
206                 self.connect('button-press-event', self.on_mouse_click)
207                 self.connect('button-release-event', self.on_mouse_clicked)
208                 self.connect('day-selected', self.on_selected_day)
209                 self._block_day_selected_event = False
210                 self.connect('day-selected-double-click', self.on_mark)
211                 self.connect('month-changed', self.on_paging)
212                 self.gotoday()
213         def on_selected_day(self, *X):
214                 if self._block_day_selected_event: return
215                 if self.date[2] != 0:
216                         if self.selected_date != self.date:
217                                 self.selected_date = self.date
218                                 self.emit('date-selected')
219         def act_unselect_day(self):
220                 self.selected_date = (0, 0, 0)
221                 self.emit('date-unselected')
222         def on_mouse_click(self, widget, event):
223                 if event.button == 3:
224                         self.popup_menu.popup(None, None, None, event.button, event.time)
225                         return True  # eat the event
226         def on_mouse_clicked(self, widget, event):
227                 if self.date[2] != 0:
228                         if event.button == 1 and event.state & gtk.gdk.SHIFT_MASK != 0:
229                                 self.select_day(0)
230                                 self.act_unselect_date()
231         def on_key_press(self, widget, event):
232                 # suppress day-selected event to handle it separately in on_key_press_release()
233                 self._block_day_selected_event = True
234         def on_key_press_release(self, widget, event):
235                 if self._block_keypress_release_event: return
236                 self._block_day_selected_event = False
237                 c_year, c_month, c_day = self.date
238                 if event.keyval == gtk.gdk.keyval_from_name('space'):
239                         if c_day == self.selected_date[2]:
240                                 self.select_day(0)
241                                 self.act_unselect_day()
242                         else:
243                                 self.on_selected_day()
244                 elif event.keyval in [gtk.gdk.keyval_from_name('Enter'), gtk.gdk.keyval_from_name('Return')]:
245                         s_year, s_month, s_day = self.selected_date[:]
246                         space_key_event = event.copy()
247                         space_key_event.keyval = int(gtk.gdk.keyval_from_name('space'))
248                         self._block_keypress_release_event = True
249                         self.emit('key-press-event', space_key_event)
250                         self.emit('key-release-event', space_key_event)
251                         self._block_keypress_release_event = False
252                         m_year, m_month, m_day = self.date
253                         if m_day == 0:
254                                 m_year, m_month, m_day = s_year, s_month, s_day
255                         self.do_mark(m_year, m_month, m_day)
256                         self.selected_date = (s_year, s_month, s_day)
257                         self.on_paging()
258                 elif event.keyval == gtk.gdk.keyval_from_name('Page_Up'):
259                         date = self.datetime.strptime('%d-%d'%(c_year,c_month), '%Y-%m') + self.relativedelta(months=-1)
260                         self.select_month(date.month - 1, date.year)
261                 elif event.keyval == gtk.gdk.keyval_from_name('Page_Down'):
262                         date = self.datetime.strptime('%d-%d'%(c_year,c_month), '%Y-%m') + self.relativedelta(months=+1)
263                         self.select_month(date.month - 1, date.year)
264         def gotoday(self, *X):
265                 now = time.localtime()
266                 self.select_month(now.tm_mon - 1, now.tm_year)
267                 self.select_day(now.tm_mday)
268                 self.on_paging()
269         @property
270         def date(self):
271                 y, m, d = super(self.__class__, self).get_date()
272                 return y, m+1, d
273         def set_marks(self, marks):
274                 self._marks = {}
275                 for mark in marks:
276                         self._marks[tuple(map(int, mark.split('-')))] = True
277                 self.on_paging()
278         def get_marks(self):
279                 response = []
280                 for year, month, day in self._marks.keys():
281                         response.append(date_format % (year, month, day))
282                 return response
283         def on_mark(self, *X):
284                 c_year, c_month, c_day = self.date
285                 self.do_mark(c_year, c_month, c_day)
286         def do_mark(self, c_year, c_month, c_day):
287                 was_marked = False
288                 for year, month, day in self._marks.keys():
289                         if year == c_year and month == c_month and c_day == day:
290                                 was_marked = True
291                                 del self._marks[(year, month, day)]
292                 if was_marked:
293                         self.emit('mark-removed', self.date_format % (c_year, c_month, c_day))
294                 else:
295                         self._marks[(c_year, c_month, c_day)] = True
296                         self.emit('mark-added', self.date_format % (c_year, c_month, c_day))
297                 self.on_paging()
298                 self.emit('marks-changed')
299         def on_paging(self, *X):
300                 self.clear_marks()
301                 self.select_day(0)
302                 c_year, c_month, c_day = self.date
303                 for year, month, day in self._marks:
304                         if (year == 0 or year == c_year) and month == c_month:
305                                 super(self.__class__, self).mark_day(day)
306                 if c_year == self.selected_date[0] and c_month == self.selected_date[1]:
307                         self.select_day(self.selected_date[2])
309 class EventImage(gtk.EventBox):
310         def __init__(self):
311                 super(self.__class__, self).__init__()
312                 self.image = gtk.Image()
313                 self.add(self.image)
314         def clear(self):
315                 return self.image.clear()
316         def set_from_pixbuf(self, *args):
317                 return self.image.set_from_pixbuf(*args)
318         def set_from_file(self, *args):
319                 return self.image.set_from_file(*args)
320         def set_from_file_at_size(self, path, w, h):
321                 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(path, w, h)
322                 self.image.set_from_pixbuf(pixbuf)
323         def set_size_request(self, *args):
324                 return self.image.set_size_request(*args)
325         @property
326         def size(self):
327                 pb = self.image.get_pixbuf()
328                 return pb.get_width(), pb.get_height()
329         @property
330         def width(self):
331                 return self.size[0]
332         @property
333         def height(self):
334                 return self.size[1]
338 ## OS functions
340 def stderr(s):
341         sys.stderr.write(s)
343 def run_managed(cmd, args):
344         pid = os.fork()
345         if pid == 0:
346                 args = args[:]
347                 args.insert(0, os.path.basename(cmd))
348                 try:
349                         os.execvp(cmd, args)
350                 except OSError:
351                         pass
352                 os._exit(127)
353         else:
354                 return pid
356 def run_detached(cmd, args):
357         args = args[:]
358         args.insert(0, os.path.basename(cmd))
359         pid1 = os.fork()
360         if pid1 == 0:
361                 os.closerange(0, 255)
362                 pid2 = os.fork()
363                 if pid2 == 0:
364                         try:
365                                 os.execvp(cmd, args)
366                         except OSError:
367                                 pass
368                         os._exit(127)
369                 else:
370                         os._exit(0)
371         else:
372                 os.waitpid(pid1, 0)
374 def mkdir_recursively(path):
375         parent = os.path.dirname(path)
376         if not os.path.exists(parent):
377                 mkdir_recursively(parent)
378         if not os.path.exists(path):
379                 os.mkdir(path)
381 def pidexists(pid):
382         try:
383                 os.kill(pid, 0)
384         except OSError as e:
385                 if e.errno == os.errno.ESRCH:
386                         return False
387                 else:
388                         return None
389         return True
393 def elhelyez(widget, rect):
394         w = rect.width
395         h = rect.height
396         W, H = widget.get_screen().get_root_window().get_size()
397         widget.move(W - w, H - h - TASKBAR_HEIGHT)
399 def win_main_show():
400         win_main.connect('size-allocate', elhelyez)
401         load_timezones()
403 def load_timezones():
404         for tz in pytz.all_timezones:
405                 timezone_store.append([tz, 0])
407 def act_main_toggle(*X):
408         if win_main.get_visible():
409                 win_main.hide()
410         else:
411                 win_main.show()
412                 cal0.gotoday()
414 def act_main_hide(*X):
415         win_main.hide()
417 def act_quit():
418         while RuntimeData.get('save_config_timer') is not None:
419                 glib.source_remove(RuntimeData.get('save_config_timer'))
420                 save_config()
421         RuntimeData['sunclock_thread'].stop()
422         gtk.main_quit()
423         return False
426 def event_masked(event):
427         return any(event.state & getattr(gtk.gdk, mask+'_MASK') for mask in ['CONTROL','SHIFT','MOD4','MOD5','META'])
429 def main_thread_alive():
430         alive = None
431         for thr in threading.enumerate():
432                 if thr.name == 'MainThread':
433                         alive = thr.is_alive()
434                         break
435         return alive
437 class xgCalService(dbus.service.Object):
438         def __init__(self, win_main):
439                 self.win = win_main
440                 bus_name = dbus.service.BusName(APPNAME, bus = dbus.SessionBus())
441                 dbus.service.Object.__init__(self, bus_name, '/'+APPNAME.replace('.', '/'))
442         @dbus.service.method(dbus_interface = APPNAME)
443         def terminate(self):
444                 act_quit()
445         @dbus.service.method(dbus_interface = APPNAME)
446         def toggle(self):
447                 act_main_toggle()
449 ## Private routines
452 def on_calendar_day_selected(*X):
453         if box_solartime.get_visible():
454                 show_solartime_panel_delayed()
455                 on_coordinates_changed()
456         update_date_info()
458 def on_coordinates_changed(*X):
459         update_solartime()
460         update_celestial_info(when=selected_date_timestamp())
461         if box_timezone.get_visible():
462                 set_timezone_by_coordinates()
464 def selected_date_timestamp():
465         y, m, d = cal0.selected_date
466         now = time.localtime()
467         if (y, m, d) == (now.tm_year, now.tm_mon, now.tm_mday):
468                 isDST = now.tm_isdst
469         else:
470                 then_noon = time.localtime(time.mktime((y, m, d, 12, 0, 0, -1, -1, -1)))
471                 isDST = then_noon.tm_isdst
472         return time.mktime((y, m, d, now.tm_hour, now.tm_min, now.tm_sec, -1, -1, isDST))
473         
474 def selected_date():
475         return time.localtime(selected_date_timestamp())
477 def update_date_info():
478         dt = selected_date()
479         
480         dayOfYear = time.strftime('%j', dt)
481         dayOfYear = re.sub(r'^0*', '', dayOfYear)
482         week = time.strftime('%W', dt)
483         isoweek = time.strftime('%V', dt)
484         if isoweek != week:
485                 isoweek_str = ' (ISO %s)' % isoweek
486         else:
487                 isoweek_str = ''
488         
489         with os.popen('hodie -v -d ymd %04d-%02d-%02d' % (dt[0], dt[1], dt[2])) as pipe:
490                 hodie = pipe.readline()
491                 hodie = re.sub(r'^Hodie ', '', hodie)
492                 hodie = re.sub(r'est$', '', hodie)
493                 hodie = hodie.replace('\n', '')
494         label_info.set_label(_("DoY %s, Week %s%s, %s") % (dayOfYear, week, isoweek_str, hodie))
496 def configfilename(option):
497         return os.path.expanduser(os.path.join('~', '.config', 'xgcal', option))
499 def save_config_queue():
500         if all(option.get('saved') == option['getter']() for option in Config.itervalues()):
501                 return
502         if RuntimeData.get('save_config_timer') is not None:
503                 return
504         RuntimeData['save_config_timer'] = glib.timeout_add(2000, save_config, priority=glib.PRIORITY_DEFAULT_IDLE)
506 def save_config():
507         RuntimeData['save_config_timer'] = None
508         for option_name, option in Config.iteritems():
509                 saved = option.get('saved')
510                 current = option['getter']()
511                 if saved != current:
512                         filename = configfilename(option_name)
513                         mkdir_recursively(os.path.dirname(filename))
514                         with open(filename, "w") as f:
515                                 f.write(str(current))
516                         option['saved'] = current
517         return False
519 def update_clock_continous(clock_label):
520         if os.environ.get('TZ') is None:
521                 tztext = ' '
522                 pmu_clock = PMU_CLOCK
523                 pmu_clock_end = PMU_CLOCK_END
524         else:
525                 tztext = time.strftime('%z (%Z)')
526                 pmu_clock = PMU_CLOCK_OTHERTZ
527                 pmu_clock_end = PMU_CLOCK_OTHERTZ_END
528         now = time.time()
529         s = time.strftime("%H:%M:%S", time.localtime(now))
530         clock_label.set_markup(pmu_clock + s + pmu_clock_end + '\n' + PMU_TIMEZONE + tztext + PMU_TIMEZONE_END)
531         
532         nextsecond = 1 - (now - int(now))
533         glib.timeout_add(int(nextsecond * 1000), update_clock_continous, clock_label, priority=glib.PRIORITY_DEFAULT_IDLE)
534         return False
536 def update_solartime_continous():
537         if box_solartime.get_visible():
538                 now = time.time()
539                 update_solartime()
540                 glib.timeout_add(60 * 1000, update_solartime_continous, priority=glib.PRIORITY_DEFAULT_IDLE)
541         return False
543 def update_solartime(*X):
544         lon = box_longitude.signed_value
545         save_config_queue()
546         # if solartime supports overrtiding current time by DATETIME_UTC env:
547         DATETIME_UTC = time.strftime('%F %T', time.gmtime(selected_date_timestamp()))
548         os.putenv('DATETIME_UTC', DATETIME_UTC)
549         cmd = 'solartime %s "%%a %%H:%%M"' % (lon,)
550         sys.stderr.write("DATETIME_UTC='%s' %s\n" % (DATETIME_UTC, cmd))
551         # otherwise use faketime:
552         #cmd = 'faketime "%s UTC" solartime %s "%%a %%H:%%M"' % (DATETIME_UTC, lon,)
553         with os.popen(cmd) as pipe:
554                 s = pipe.readline()
555                 s = s.replace('\n', '')
556         label_solartime.set_markup("%s☼ %s%s" % (PMU_SOLARCLOCK, s, PMU_SOLARCLOCK_END))
558 def toggle_solartime(btn):
559         if box_solartime.get_visible():
560                 box_coordinates.hide()
561                 box_solartime.hide()
562                 RuntimeData['sunclock_thread'].stop()
563                 sunclock_image.clear()
564         else:
565                 if box_timezone.get_visible():
566                         btn_timezone.set_active(False)
567                 show_solartime_panel()
568                 on_coordinates_changed()
570 def show_solartime_panel_delayed():
571         if RuntimeData.get('show_solartime_panel_timer') is None:
572                 sunclock_image.clear()
573                 RuntimeData['show_solartime_panel_timer'] = glib.timeout_add(1500, show_solartime_panel, priority=glib.PRIORITY_DEFAULT_IDLE)
575 def show_solartime_panel():
576         if RuntimeData.get('show_solartime_panel_timer') is not None:
577                 glib.source_remove(RuntimeData.get('show_solartime_panel_timer'))
578                 RuntimeData['show_solartime_panel_timer'] = None
579         try:
580                 box_coordinates.show()
581                 box_solartime.show()
582                 update_solartime_continous()
583                 
584                 RuntimeData['sunclock_thread'].stop()
585                 gtksocket = RuntimeData['sunclock_thread'].gtksocket
586                 del RuntimeData['sunclock_thread']
587                 RuntimeData['sunclock_thread'] = SunclockThread(gtksocket)
588                 RuntimeData['sunclock_thread'].start_longitude = box_longitude.signed_value
589                 RuntimeData['sunclock_thread'].start_jumpseconds = selected_date_timestamp() - time.time()
590                 win_main_width = win_main.get_size()[0]
591                 RuntimeData['sunclock_thread'].sunclock_geometry = (win_main_width, int(win_main_width/2)+SUNCLOCK_WINDOW_BOTTOM_MARGIN)
592                 RuntimeData['sunclock_thread'].start()
593         except:
594                 traceback.print_exc()
595         # clear glib timer by returning False:
596         RuntimeData['show_solartime_panel_timer'] = None
597         return False
599 def get_current_timezone_offset_minutes():
600         tz = time.strftime('%z')
601         return int('%s%s' % (tz[0], int(tz[1:3])*60 + int(tz[3:5])))
603 def update_celestial_info(when=None):
604         if when is None:
605                 when = time.time()
606         
607         with os.popen("pom %s" % (time.strftime('%Y%m%d%H', time.localtime(when)),)) as pipe:
608                 moon_phase = pipe.readline()
609                 moon_phase = re.sub(r'^.*The Moon (is|was|will be) ', '', moon_phase)
610         
611         tzmin = get_current_timezone_offset_minutes()
612         # ISO-6709:1983-co-ordinate
613         coords = '%+06.2f%+07.2f' % (float(box_latitude.signed_value), float(box_longitude.signed_value))
614         cmd = ['gcal','-c','-H','no','--suppress-date-part','--resource-file=/dev/null',
615                 '--text-variable=$C=%s:$T=%s' % (coords,tzmin),
616                 '--here=$c=$C,$T',
617                 '--here=0 sunrise: (%o6$c ) %o3$c',
618                 '--here=0 sunset: %s3$c  (%s6$c )',
619                 '--here=0 noon: %o1$c',
620                 '--here=0 moonrise: %(5$c',
621                 '--here=0 moonset: %)5$c',
622                 '%%%s' % time.strftime('%Y%m%d', time.localtime(when))]
623         sys.stderr.write(' '.join(["'"+arg.replace("'", "'\\''")+"'" for arg in cmd])+'\n')
624         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
625         stdout, Xstderr = proc.communicate()
626         del proc
627         moon_info = []
628         sun_info = []
629         for line in stdout.split('\n')[3:]:
630                 line = re.sub(r'(\d\d):(\d\d)', '\\1<sup>\\2</sup>', line)
631                 if re.search(r'^(moon)', line):
632                         moon_info.append(line)
633                 else:
634                         sun_info.append(line)
635         
636         label_celestial.set_markup('☼ ' + ' '.join(sun_info) +
637                 ' ☾ ' + ' '.join(moon_info) +
638                 ' ☾ ' + glib.markup_escape_text(moon_phase))
640 def sunclock_elhelyez(box, box_allocation):
641         if not box.get_data('size-allocate-signal-enable'):
642                 return
643         sunclock_xid = box.get_data('sunclock-xid')
644         #print box_allocation, sunclock_xid
645         if sunclock_xid:
646                 #box_allocation = box.get_allocation()
647                 #sunclock_win = gtk.gdk.window_foreign_new(sunclock_xid)
648                 #main_win_pos_x, main_win_pos_y = win_main.get_position()
649                 #sunclock_win.move(main_win_pos_x + box_allocation.x, main_win_pos_y + box_allocation.y)
650                 box.set_data('size-allocate-signal-enable', False)
651                 sunclock_socket.add_id(sunclock_xid)
653 class SunclockThread(threading.Thread):
654         def __init__(self, gtksocket):
655                 threading.Thread.__init__(self)
656                 self.pid = None
657                 self.gtksocket = gtksocket
658         def stop(self):
659                 if self.pid is not None:
660                         try:
661                                 os.kill(self.pid, signal.SIGTERM)
662                         except OSError:
663                                 pass
664                 self.pid = None
665         def run(self):
666                 geom = "%dx%d" % self.sunclock_geometry[:]
667                 cmd = ['-clock', '-clockimage', 'vmf/landwater.vmf', '-clockgeom', geom, '-shading', '2', '-nomoon', '-nosun', '-citymode', '0', '-position', '0 %f' % self.start_longitude, '-dateformat', '', '-jump', '%ds' % self.start_jumpseconds]
668                 sys.stderr.write('sunclock '+(' '.join(["'"+arg.replace("'", "'\\''")+"'" for arg in cmd]))+'\n')
669                 self.pid = run_managed('sunclock', cmd)
670                 xid = None
671                 while xid is None and pidexists(self.pid):
672                         time.sleep(0.2)
673                         gtk.threads_enter()
674                         xid = get_wid_by_class('sunclock')
675                         gtk.threads_leave()
676                 while xid is not None and self.pid is not None and pidexists(self.pid):
677                         gtk.threads_enter()
678                         if sunclock_socket.window.get_children():
679                                 gtk.threads_leave()
680                                 time.sleep(0.5)
681                                 continue
682                         try:
683                                 if sunclock_socket.get_parent() is None:
684                                         box_sunclock.pack_start(sunclock_socket, 0, 1)
685                                         sunclock_socket.show()
686                                 sunclock_socket.set_data('sunclock-xid', xid)
687                                 sunclock_socket.set_data('size-allocate-signal-enable', True)
688                                 sunclock_win = gtk.gdk.window_foreign_new(xid)
689                                 w, h = sunclock_win.get_size()
690                                 h -= SUNCLOCK_WINDOW_BOTTOM_MARGIN
691                                 sunclock_socket.set_size_request(w, h)
692                                 sunclock_win.reparent(sunclock_socket.window, 0, 0)
693                         finally:
694                                 gtk.threads_leave()
696 def on_sunclock_embedded(gtksocket):
697         # give some time to sunclock to paint itself
698         glib.timeout_add(150, screenshot_sunclock, priority=glib.PRIORITY_DEFAULT_IDLE)
700 def screenshot_sunclock():
701         xid = sunclock_socket.get_data('sunclock-xid')
702         if xid is not None:
703                 sunclock_win = gtk.gdk.window_foreign_new(xid)
704                 if sunclock_win is not None:
705                         w, h = sunclock_socket.get_size_request()
706                         #h -= SUNCLOCK_WINDOW_BOTTOM_MARGIN
707                         pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, w, h)
708                         screenshot = pixbuf.get_from_drawable(sunclock_win, sunclock_win.get_colormap(), 0, 0, 0, 0, w, h)
709                         for pixel in tuple(screenshot.get_pixels_array()[0][-1]), tuple(screenshot.get_pixels_array()[-1][-1]):
710                                 if pixel == (0, 0, 0) or pixel == (255, 255, 255):
711                                         # last pixel should not be black, it must be a wrong image;
712                                         # reschedule this function timer:
713                                         return True
714                         sunclock_image.set_from_pixbuf(screenshot)
715                         sunclock_image.set_size_request(w, h)
716                         sunclock_socket.hide()
717                         sunclock_image.show()
718                         RuntimeData['sunclock_thread'].stop()
719         return False
721 def get_world_map_coordinates(image, x, y):
722         lon_min = -180
723         lon_max = 180
724         lat_min = 90
725         lat_max = -90
726         w, h = image.size
727         lon = (x * (lon_max-lon_min) / w) + lon_min
728         lat = (y * (lat_max-lat_min) / h) + lat_min
729         return lon, lat
731 def on_sunclock_click(widget, event):
732         if event.button == 1:
733                 lon, lat = get_world_map_coordinates(widget, event.x, event.y)
734                 box_longitude.signed_value = lon
735                 box_latitude.signed_value = lat
737 def get_wid_by_class(wmclass):
738         root = gtk.gdk.get_default_root_window()
739         for xid in root.property_get('_NET_CLIENT_LIST')[2]:
740                 w = gtk.gdk.window_foreign_new(xid)
741                 if w:
742                         wm_class = w.property_get('WM_CLASS')
743                         if wm_class is not None:
744                                 thisclass = wm_class[2].split('\x00')[0]
745                                 if thisclass == wmclass:
746                                         return xid
747         return None
751 class CoordinateInput(gtk.HBox):
752         HEMISPHERE_WEST, HEMISPHERE_EAST, HEMISPHERE_SOUTH, HEMISPHERE_NORTH = range(4)
753         __gsignals__ = {
754                 'changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
755         }
756         def __init__(self, LONGITUDE=False, LATITUDE=False):
757                 assert (LONGITUDE or LATITUDE) and not (LONGITUDE and LATITUDE)
758                 self.is_latitude = LATITUDE
759                 self.is_longitude = LONGITUDE
760                 
761                 super(self.__class__, self).__init__()
762                 label_pre = gtk.Label(_("Longitude") if LONGITUDE else _("Latitude"))
763                 label_pre.set_alignment(0.0, 0.5)
764                 
765                 if LONGITUDE:
766                         degree_max = 180.0
767                 else:
768                         degree_max = 90.0
769                 self.adjuster = gtk.Adjustment(value=degree_max/2, lower=0.0, upper=degree_max, step_incr=1.00, page_incr=10.0)
770                 self.spinner = gtk.SpinButton(self.adjuster, digits=2)
771                 self.spinner.set_wrap(False)
772                 self.spinner.connect('output', self.on_change)
773                 
774                 label_post = gtk.Label(_("°"))
775                 self.btn_hemisphere = gtk.Button()
776                 self.btn_hemisphere.connect('clicked', self.switch_hemisphere)
777                 self.set_hemisphere(self.HEMISPHERE_EAST if LONGITUDE else self.HEMISPHERE_NORTH)
778                 
779                 self.pack_start(label_pre, 1, 1)
780                 self.pack_start(self.spinner, 0, 1)
781                 self.pack_start(label_post, 0, 0)
782                 self.pack_start(self.btn_hemisphere, 0, 1)
783         def set_hemisphere(self, hemisphere):
784                 assert (self.is_longitude and hemisphere in (self.HEMISPHERE_WEST, self.HEMISPHERE_EAST)) or (self.is_latitude and hemisphere in (self.HEMISPHERE_SOUTH, self.HEMISPHERE_NORTH))
785                 label = {self.HEMISPHERE_WEST:_("West"), self.HEMISPHERE_EAST:_("East"), self.HEMISPHERE_SOUTH:_("South"), self.HEMISPHERE_NORTH:_("North")}
786                 self.btn_hemisphere.set_label(label[hemisphere])
787                 self.btn_hemisphere.set_data('hemisphere', hemisphere)
788                 if hemisphere in (self.HEMISPHERE_WEST, self.HEMISPHERE_SOUTH):
789                         self.adjuster.set_page_increment(-abs(self.adjuster.get_page_increment()))
790                         self.adjuster.set_step_increment(-abs(self.adjuster.get_step_increment()))
791                 else:
792                         self.adjuster.set_page_increment(abs(self.adjuster.get_page_increment()))
793                         self.adjuster.set_step_increment(abs(self.adjuster.get_step_increment()))
794         def switch_hemisphere(self, *X):
795                 hemisphere = self.btn_hemisphere.get_data('hemisphere')
796                 if hemisphere == self.HEMISPHERE_EAST: hemisphere = self.HEMISPHERE_WEST
797                 elif hemisphere == self.HEMISPHERE_WEST: hemisphere = self.HEMISPHERE_EAST
798                 if hemisphere == self.HEMISPHERE_NORTH: hemisphere = self.HEMISPHERE_SOUTH
799                 elif hemisphere == self.HEMISPHERE_SOUTH: hemisphere = self.HEMISPHERE_NORTH
800                 self.set_hemisphere(hemisphere)
801                 self.emit('changed')
802         def on_change(self, *X):
803                 if self.spinner.get_value() in [0.0, 180.0]:
804                         self.switch_hemisphere()
805                 else:
806                         self.emit('changed')
807         @property
808         def signed_value(self):
809                 x = self.spinner.get_value()
810                 if self.btn_hemisphere.get_data('hemisphere') in (self.HEMISPHERE_WEST, self.HEMISPHERE_SOUTH): x = -x
811                 return x
812         @signed_value.setter
813         def signed_value(self, x):
814                 try:
815                         x = float(x)
816                 except ValueError:
817                         return x
818                 self.spinner.set_value(abs(x))
819                 if self.is_longitude:
820                         self.set_hemisphere(self.HEMISPHERE_EAST if x >= 0 else self.HEMISPHERE_WEST)
821                 else:
822                         self.set_hemisphere(self.HEMISPHERE_NORTH if x >= 0 else self.HEMISPHERE_SOUTH)
823                 return x
824         def get_signed_value(self):
825                 return self.signed_value
826         def set_signed_value(self, x):
827                 self.signed_value = x
829 def set_timezone(tz):
830         if tz is None or tz == '':
831                 if 'TZ' in os.environ:
832                         del os.environ['TZ']
833         else:
834                 os.environ['TZ'] = tz
835         time.tzset()
837 def on_click_timezone_btn(*X):
838         if box_timezone.get_visible():
839                 set_timezone(None)
840                 box_coordinates.hide()
841                 box_timezone.hide()
842         else:
843                 if box_solartime.get_visible():
844                         btn_solartime.set_active(False)
845                 box_coordinates.show()
846                 box_timezone.show()
847                 try:
848                         set_timezone_by_coordinates()
849                 except:
850                         on_timezone_string_entered()
852 def on_timezone_click(widget, event):
853         if event.button == 1:
854                 lon, lat = get_world_map_coordinates(widget, event.x, event.y)
855                 box_longitude.signed_value = lon
856                 box_latitude.signed_value = lat
858 from multiprocessing import Process, Queue
860 class TZWhere(object):
861         def __init__(self):
862                 self.qq = Queue()
863                 self.qa = Queue()
864                 self.loader = Process(target=self.subproc, args=(self.qq, self.qa))
865                 self.loader.daemon = True
866                 self.loader.start()
867         
868         def subproc(self, qq, qa):
869                 from tzwhere import tzwhere
870                 stderr("loading tzwhere database...\n")
871                 t0 = time.time()
872                 tzlookup = tzwhere.tzwhere(forceTZ=True)
873                 stderr("tzwhere loaded in %.3f sec\n" % (time.time() - t0,))
874                 while True:
875                         question = qq.get(block=True)
876                         try:
877                                 answer = tzlookup.tzNameAt(*question['pargs'], **question['kwargs'])
878                         except:
879                                 traceback.print_exc()
880                                 answer = None
881                         qa.put([question, answer])
882         
883         def tzNameAt(self, *pa, **kwa):
884                 question = {'pargs': pa, 'kwargs': kwa}
885                 self.qq.put(question)
886                 while True:
887                         answer = self.qa.get(block=True, timeout=0.25)
888                         if answer[0] == question:
889                                 answer = answer[1]
890                                 break
891                 return answer
893 def set_timezone_by_coordinates():
894         lat = float(box_latitude.signed_value)
895         lon = float(box_longitude.signed_value)
896         try:
897                 tzname = tzw.tzNameAt(lat, lon, forceTZ=True)
898         except Exception as e:
899                 traceback.print_exc(e)
900                 stderr(_("TZ database has not been loaded yet.")+'\n')
901                 return False
902         else:
903                 entry_timezone.set_text(tzname)
904                 on_timezone_string_entered()
906 def on_hover_timezone_image(image, event):
907         lat = float(-(2 *  90 * event.y / image.height) + 90)
908         lon = float((2 * 180 * event.x / image.width) - 180)
909         try:
910                 tzname = tzw.tzNameAt(lat, lon, forceTZ=True)
911                 image.window.set_cursor(None)
912                 image.set_tooltip_text(tzname)
913         except:
914                 cursor = gtk.gdk.Cursor(image.window.get_display(), gtk.gdk.WATCH)
915                 image.window.set_cursor(cursor)
916                 image.set_tooltip_text(_("(loading timezones…)"))
919 def on_timezone_string_entered(*X):
920         set_timezone(entry_timezone.get_text())
922 class RankedEntryCompletion(gtk.EntryCompletion):
923         def __init__(self, entry=None):
924                 super(self.__class__, self).__init__()
925                 self._rank_func = self._default_rank_func
926                 self.set_match_func(self._match_func)
927                 if entry is not None:
928                         self.entry = entry
929                 else:
930                         self.entry = gtk.Entry()
931                 self.entry.set_completion(self)
932                 self.entry.connect('changed', self.on_edit)
933                 self.set_popup_single_match(True)
934         def on_edit(self, *X):
935                 glib.timeout_add(100, self.on_after_edit, priority=glib.PRIORITY_DEFAULT_IDLE)
936         def on_after_edit(self):
937                 self.rerank(self.entry.get_text())
938         def set_rank_column(self, colnum):
939                 assert isinstance(colnum, int)
940                 assert colnum >= 0
941                 assert colnum < self.get_model().get_n_columns()
942                 self._rank_column = colnum
943                 self.get_model().set_sort_column_id(colnum, gtk.SORT_DESCENDING)
944         def set_rank_func(self, func):
945                 self._rank_func = func
946         def _default_rank_func(self, completion, key, row):
947                 return 1
948         def rerank(self, key):
949                 for row in self.get_model():
950                         row[self._rank_column] = self._rank_func(self, key, row)
951         def _match_func(self, completion, key, rowiter):
952                 row = self.get_model()[rowiter]
953                 return self._rank_func(self, key, row) > 0
955 def timezone_completion(completion, query, row):
956         tz = row[0]
957         tz = tz.lower()
958         query = query.lower()
959         if tz.startswith(query):
960                 return 3
961         if any([s.startswith(query) for s in tz.split('/')]):
962                 return 2
963         if tz.find(query) > -1:
964                 return 1
965         return 0
969 ### Main ###
971 setproctitle.setproctitle(PROGNAME)
972 gettext.textdomain(PROGNAME)
973 _ = gettext.gettext
974 RuntimeData = {
975         'save_config_timer': None,
976         'show_solartime_panel_timer': None,
977         'tz': None,
979 Config = {}
982 if any(x in sys.argv for x in ['--help', '-h', '-?']):
983         print """Usage: %s\n""" % (PROGNAME,)
984         raise SystemExit(0)
986 # Check if application is already running
987 dbus_app_session = dbus.SessionBus().request_name(APPNAME)
988 if dbus_app_session != dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER:
989         stderr(_("Already running; toggle window.")+'\n')
990         method = dbus.SessionBus().get_object(APPNAME, '/'+APPNAME.replace('.', '/')).get_dbus_method('toggle')
991         method()
992         raise SystemExit(0)
995 ### Build GUI ###
997 win_main = gtk.Window(gtk.WINDOW_TOPLEVEL)
998 box0 = gtk.VBox()
999 box1 = gtk.HBox()
1000 btn_timezone = gtk.ToggleButton(_("TZ"))
1001 btn_solartime = gtk.ToggleButton(_("Solar"))
1002 btn_stopper = gtk.Button(_("Timer"))
1003 btn_placeholder = gtk.HBox()
1004 btn_close = gtk.Button(_("Close"))
1005 box2 = gtk.HBox()
1006 label_clock = gtk.Label()
1007 label_info = gtk.Label()
1008 cal0 = Calendar()
1009 box_solartime = gtk.VBox()
1010 box_coordinates = gtk.VBox()
1011 box_longitude = CoordinateInput(LONGITUDE=True)
1012 box_latitude = CoordinateInput(LATITUDE=True)
1013 label_solartime = gtk.Label()
1014 box_sunclock = gtk.VBox()
1015 sunclock_socket = gtk.Socket()
1016 sunclock_image = EventImage()
1017 label_celestial = gtk.Label()
1018 box_timezone = gtk.VBox()
1019 timezone_image = EventImage()
1020 timezone_store = gtk.ListStore(str, int)
1021 entrycompletion_timezone = RankedEntryCompletion()
1022 entry_timezone = entrycompletion_timezone.entry
1024 # Configure main window's widgets
1025 win_main.set_decorated(False)
1026 win_main.set_size_request(256, -1)
1027 win_main.set_resizable(False)
1028 win_main.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_POPUP_MENU)
1029 win_main.set_keep_above(True)
1030 win_main.set_skip_taskbar_hint(True)
1031 win_main.set_icon_name('x-office-calendar')
1032 start_evt = win_main.connect('map-event', lambda w,e: (win_main.disconnect(start_evt), win_main_show()))
1033 win_main.connect('delete-event', lambda w,e: act_quit())
1034 add_key_binding(win_main, 'Escape', act_main_hide)
1036 label_solartime.set_markup(PMU_SOLARCLOCK + ':' + PMU_SOLARCLOCK_END)
1037 label_solartime.set_selectable(True)
1038 sunclock_socket.connect('size-allocate', sunclock_elhelyez)
1039 sunclock_socket.connect('plug-removed', lambda *X: True)
1040 sunclock_socket.connect('plug-added', on_sunclock_embedded)
1041 sunclock_image.connect('button-press-event', on_sunclock_click)
1042 label_celestial.set_line_wrap(True)
1043 label_celestial.set_size_request(win_main.get_size_request()[0], -1)
1044 label_celestial.set_selectable(True)
1046 timezone_image.set_from_file_at_size('/usr/share/xgcal/timezones.png', win_main.get_size_request()[0], -1)
1047 timezone_image.connect('button-press-event', on_timezone_click)
1048 timezone_image.add_events(gtk.gdk.POINTER_MOTION_MASK)
1049 timezone_image.connect('motion-notify-event', on_hover_timezone_image)
1050 entry_timezone.connect('changed', on_timezone_string_entered)
1051 entrycompletion_timezone.set_model(timezone_store)
1052 entrycompletion_timezone.set_text_column(0)
1053 entrycompletion_timezone.set_rank_column(1)
1054 entrycompletion_timezone.set_rank_func(timezone_completion)
1056 btn_timezone.connect('clicked', on_click_timezone_btn)
1057 btn_solartime.connect('clicked', toggle_solartime)
1058 btn_stopper.connect('clicked', lambda *X: run_detached('xstopper', []))
1059 btn_close.connect('clicked', act_main_hide)
1060 label_clock.set_markup(PMU_CLOCK + ':' + PMU_CLOCK_END + '\n' + PMU_TIMEZONE + ' ' + PMU_TIMEZONE_END)
1061 label_clock.set_padding(18, 0)
1062 label_clock.set_justify(gtk.JUSTIFY_CENTER)
1063 label_info.set_line_wrap(True)
1064 label_info.set_size_request(win_main.get_size_request()[0], -1)
1065 label_info.set_selectable(True)
1067 # Pack widgets
1068 win_main.add(box0)
1069 box0.pack_start(box_coordinates, 0, 1)
1070 box_coordinates.pack_start(box_longitude, 0, 1)
1071 box_coordinates.pack_start(box_latitude, 0, 1)
1072 box0.pack_start(box_solartime, 0, 1)
1073 box0.pack_start(box_timezone, 0, 1)
1074 box0.pack_start(box1, 0, 1)
1075 box0.pack_start(box2, 0, 1)
1076 box0.pack_start(label_info, 0, 1)
1077 box0.pack_start(cal0, 0, 1)
1079 box1.pack_start(btn_timezone, 0, 0)
1080 box1.pack_start(btn_solartime, 0, 0)
1081 box1.pack_start(btn_stopper, 0, 0)
1082 box1.pack_start(btn_placeholder, 1, 1)
1083 box1.pack_start(btn_close, 0, 0)
1084 box2.pack_start(label_clock, 1, 1)
1086 box_timezone.pack_start(timezone_image, 0, 1)
1087 box_timezone.pack_start(entry_timezone, 0, 0)
1089 box_solartime.pack_start(label_solartime, 0, 1)
1090 box_solartime.pack_start(box_sunclock, 0, 1)
1091 box_sunclock.pack_start(sunclock_image, 0, 1)
1092 box_sunclock.pack_start(sunclock_socket, 0, 1)
1093 box_solartime.pack_start(label_celestial, 0, 1)
1095 win_main.show_all()
1096 box_coordinates.hide()
1097 box_solartime.hide()
1098 sunclock_image.hide()
1099 box_timezone.hide()
1100 dbus_service = xgCalService(win_main)
1103 Config['longitude'] = {
1104         'getter': box_longitude.get_signed_value,
1105         'setter': box_longitude.set_signed_value,
1107 Config['latitude'] = {
1108         'getter': box_latitude.get_signed_value,
1109         'setter': box_latitude.set_signed_value,
1111 # Load config
1112 for option_name, option in Config.iteritems():
1113         filename = configfilename(option_name)
1114         if os.path.exists(filename):
1115                 with open(filename, "r") as f:
1116                         saved_raw = f.readline().strip()
1117                         option['saved'] = option['setter'](saved_raw)
1119 gtk.gdk.threads_init()
1120 RuntimeData['sunclock_thread'] = SunclockThread(sunclock_socket)
1121 tzw = TZWhere()
1122 update_clock_continous(label_clock)
1123 on_calendar_day_selected()
1125 box_longitude.connect('changed', on_coordinates_changed)
1126 box_latitude.connect('changed', on_coordinates_changed)
1127 cal0.connect('date-selected', on_calendar_day_selected)
1129 gtk.main()
1130 #os.kill(os.getpid(), signal.SIGQUIT)