1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
14 from datetime import datetime
15 from dateutil.relativedelta import relativedelta
28 APPNAME = 'hu.uucp.%s' % (PROGNAME,)
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)
48 if threading.currentThread().name != 'MainThread':
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):
55 elif type(e) == type([]):
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"))
63 if threading.currentThread().name != 'MainThread':
66 class DialogCancel(Exception):
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)
72 dlg.set_title(_("Question"))
74 dlg.add_buttons(gtk.STOCK_YES, gtk.RESPONSE_YES)
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)
81 dlg.add_buttons(yes, gtk.RESPONSE_YES)
82 dlg.add_buttons(gtk.STOCK_NO, gtk.RESPONSE_NO)
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)
89 if resp == gtk.RESPONSE_CANCEL:
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)
99 if type(func) != type(()):
101 if func[0] is not None:
104 def on_click_button(btn, func):
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():
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)
119 elif stock not in gtk.stock_list_ids():
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()
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:
139 img.props.visible = False
141 img.set_from_icon_name(icon, size)
142 img.props.visible = True
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)
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
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, ()),
189 date_format = '%04d-%02d-%02d'
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)
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()
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)
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:
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]:
241 self.act_unselect_day()
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
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)
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)
271 y, m, d = super(self.__class__, self).get_date()
273 def set_marks(self, marks):
276 self._marks[tuple(map(int, mark.split('-')))] = True
280 for year, month, day in self._marks.keys():
281 response.append(date_format % (year, month, day))
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):
288 for year, month, day in self._marks.keys():
289 if year == c_year and month == c_month and c_day == day:
291 del self._marks[(year, month, day)]
293 self.emit('mark-removed', self.date_format % (c_year, c_month, c_day))
295 self._marks[(c_year, c_month, c_day)] = True
296 self.emit('mark-added', self.date_format % (c_year, c_month, c_day))
298 self.emit('marks-changed')
299 def on_paging(self, *X):
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):
311 super(self.__class__, self).__init__()
312 self.image = gtk.Image()
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)
327 pb = self.image.get_pixbuf()
328 return pb.get_width(), pb.get_height()
343 def run_managed(cmd, args):
347 args.insert(0, os.path.basename(cmd))
356 def run_detached(cmd, args):
358 args.insert(0, os.path.basename(cmd))
361 os.closerange(0, 255)
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):
385 if e.errno == os.errno.ESRCH:
393 def elhelyez(widget, rect):
396 W, H = widget.get_screen().get_root_window().get_size()
397 widget.move(W - w, H - h - TASKBAR_HEIGHT)
400 win_main.connect('size-allocate', elhelyez)
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():
414 def act_main_hide(*X):
418 while RuntimeData.get('save_config_timer') is not None:
419 glib.source_remove(RuntimeData.get('save_config_timer'))
421 RuntimeData['sunclock_thread'].stop()
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():
431 for thr in threading.enumerate():
432 if thr.name == 'MainThread':
433 alive = thr.is_alive()
437 class xgCalService(dbus.service.Object):
438 def __init__(self, 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)
445 @dbus.service.method(dbus_interface = APPNAME)
452 def on_calendar_day_selected(*X):
453 if box_solartime.get_visible():
454 show_solartime_panel_delayed()
455 on_coordinates_changed()
458 def on_coordinates_changed(*X):
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):
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))
475 return time.localtime(selected_date_timestamp())
477 def update_date_info():
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)
485 isoweek_str = ' (ISO %s)' % isoweek
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()):
502 if RuntimeData.get('save_config_timer') is not None:
504 RuntimeData['save_config_timer'] = glib.timeout_add(2000, save_config, priority=glib.PRIORITY_DEFAULT_IDLE)
507 RuntimeData['save_config_timer'] = None
508 for option_name, option in Config.iteritems():
509 saved = option.get('saved')
510 current = option['getter']()
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
519 def update_clock_continous(clock_label):
520 if os.environ.get('TZ') is None:
522 pmu_clock = PMU_CLOCK
523 pmu_clock_end = PMU_CLOCK_END
525 tztext = time.strftime('%z (%Z)')
526 pmu_clock = PMU_CLOCK_OTHERTZ
527 pmu_clock_end = PMU_CLOCK_OTHERTZ_END
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)
532 nextsecond = 1 - (now - int(now))
533 glib.timeout_add(int(nextsecond * 1000), update_clock_continous, clock_label, priority=glib.PRIORITY_DEFAULT_IDLE)
536 def update_solartime_continous():
537 if box_solartime.get_visible():
540 glib.timeout_add(60 * 1000, update_solartime_continous, priority=glib.PRIORITY_DEFAULT_IDLE)
543 def update_solartime(*X):
544 lon = box_longitude.signed_value
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:
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()
562 RuntimeData['sunclock_thread'].stop()
563 sunclock_image.clear()
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
580 box_coordinates.show()
582 update_solartime_continous()
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()
594 traceback.print_exc()
595 # clear glib timer by returning False:
596 RuntimeData['show_solartime_panel_timer'] = None
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):
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)
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),
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()
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)
634 sun_info.append(line)
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'):
643 sunclock_xid = box.get_data('sunclock-xid')
644 #print box_allocation, 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)
657 self.gtksocket = gtksocket
659 if self.pid is not None:
661 os.kill(self.pid, signal.SIGTERM)
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)
671 while xid is None and pidexists(self.pid):
674 xid = get_wid_by_class('sunclock')
676 while xid is not None and self.pid is not None and pidexists(self.pid):
678 if sunclock_socket.window.get_children():
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)
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')
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:
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()
721 def get_world_map_coordinates(image, x, y):
727 lon = (x * (lon_max-lon_min) / w) + lon_min
728 lat = (y * (lat_max-lat_min) / h) + lat_min
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)
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:
751 class CoordinateInput(gtk.HBox):
752 HEMISPHERE_WEST, HEMISPHERE_EAST, HEMISPHERE_SOUTH, HEMISPHERE_NORTH = range(4)
754 'changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
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
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)
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)
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)
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()))
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)
802 def on_change(self, *X):
803 if self.spinner.get_value() in [0.0, 180.0]:
804 self.switch_hemisphere()
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
813 def signed_value(self, 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)
822 self.set_hemisphere(self.HEMISPHERE_NORTH if x >= 0 else self.HEMISPHERE_SOUTH)
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:
834 os.environ['TZ'] = tz
837 def on_click_timezone_btn(*X):
838 if box_timezone.get_visible():
840 box_coordinates.hide()
843 if box_solartime.get_visible():
844 btn_solartime.set_active(False)
845 box_coordinates.show()
848 set_timezone_by_coordinates()
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):
864 self.loader = Process(target=self.subproc, args=(self.qq, self.qa))
865 self.loader.daemon = True
868 def subproc(self, qq, qa):
869 from tzwhere import tzwhere
870 stderr("loading tzwhere database...\n")
872 tzlookup = tzwhere.tzwhere(forceTZ=True)
873 stderr("tzwhere loaded in %.3f sec\n" % (time.time() - t0,))
875 question = qq.get(block=True)
877 answer = tzlookup.tzNameAt(*question['pargs'], **question['kwargs'])
879 traceback.print_exc()
881 qa.put([question, answer])
883 def tzNameAt(self, *pa, **kwa):
884 question = {'pargs': pa, 'kwargs': kwa}
885 self.qq.put(question)
887 answer = self.qa.get(block=True, timeout=0.25)
888 if answer[0] == question:
893 def set_timezone_by_coordinates():
894 lat = float(box_latitude.signed_value)
895 lon = float(box_longitude.signed_value)
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')
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)
910 tzname = tzw.tzNameAt(lat, lon, forceTZ=True)
911 image.window.set_cursor(None)
912 image.set_tooltip_text(tzname)
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:
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)
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):
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):
958 query = query.lower()
959 if tz.startswith(query):
961 if any([s.startswith(query) for s in tz.split('/')]):
963 if tz.find(query) > -1:
971 setproctitle.setproctitle(PROGNAME)
972 gettext.textdomain(PROGNAME)
975 'save_config_timer': None,
976 'show_solartime_panel_timer': None,
982 if any(x in sys.argv for x in ['--help', '-h', '-?']):
983 print """Usage: %s\n""" % (PROGNAME,)
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')
997 win_main = gtk.Window(gtk.WINDOW_TOPLEVEL)
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"))
1006 label_clock = gtk.Label()
1007 label_info = gtk.Label()
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)
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)
1096 box_coordinates.hide()
1097 box_solartime.hide()
1098 sunclock_image.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,
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)
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)
1130 #os.kill(os.getpid(), signal.SIGQUIT)