#!/usr/bin/env python

# World Clock Screenlet v 0.9.2
#
#
# COPYRIGHT NOTICE:
# 
# Copyright (c) 2008 jsf (aka Joe Forbes) <joe.hormel@gmail.com>
# All Rights Reserved
# Licensed under GPL v2.0
# 
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
# 
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
# 
#
# INFO:
# - adaptation of RYX's ClockScreenlet with added zoneinfo support
# - based on version 0.5 of ClockScreenlet.
# - Changes made:
#	* user can select a timezone rather than an offset from local
#		* ensures proper handling of DST for selected timezone
#	* caption uses wildcards to support:
#		* digital time display
#		* timezone code display
#		* configurable date display
#		* multiline caption
#
#	
# TODO: 
# Add choices list for face_text?
# Make update interval configurable?
# Re-factor based on work in sensor screenlets?
#
#
# CREDITS:
#
# Based on Clock Screenlet (c) RYX (aka Rico Pfaus) 2007 <ryx@ryxperience.com>


import screenlets
from screenlets import Screenlet
from screenlets.options import IntOption, BoolOption
from screenlets.options import StringOption, FontOption, ColorOption
from screenlets.services import ScreenletService

import pygtk
pygtk.require('2.0')
import gtk
import math
import cairo
import pango
from datetime import datetime
import gobject

# jsf additional imports needed for tzinfo support
from datetime import *
from dateutil.tz import *
from dateutil import zoneinfo
import os
# use gettext for translation
import gettext

_ = screenlets.utils.get_translator(__file__)

def tdoc(obj):
	obj.__doc__ = _(obj.__doc__)
	return obj

@tdoc
# the service that implements the remote-actions for this screenlet
class WorldClockService (ScreenletService):
	"""A service for remote-controlling the WorldClockScreenlet. Defines custom
	actions and signals this Screenlet offers to the outer world."""
	
	# define our custom interface here
	IFACE 	= 'org.screenlets.WorldClock'
	
	# constructor
	def __init__ (self, worldClock):
		ScreenletService.__init__(self, worldClock, 'WorldClock')
	
	# defining an action (with support for multiple instances)
	@screenlets.services.action(IFACE)
	def get_time (self, id):
		"""This method returns the current time as string."""
		# get the instance with id
		sl = self.screenlet.session.get_instance_by_id(id)
		if sl:
			# and return its time
			return sl.get_time()
	
	# defining an action (with support for multiple instances)
	@screenlets.services.action(IFACE)
	def get_date (self, id):
		"""This method returns the current date as string."""
		sl = self.screenlet.session.get_instance_by_id(id)
		if sl:
			return sl.get_date()
	
	# defining a signal (can be just an empty function)
	@screenlets.services.signal(IFACE)
	def alarm_start (self, id):
		"""This signal is emitted whenever the Alarm starts."""

	# defining a signal (can be just an empty function)
	@screenlets.services.signal(IFACE)
	def alarm_stop (self, id):
		"""This signal is emitted whenever the Alarm ends."""

# use gettext for translation
import gettext

_ = screenlets.utils.get_translator(__file__)

def tdoc(obj):
	obj.__doc__ = _(obj.__doc__)
	return obj

@tdoc

class WorldClockScreenlet(Screenlet):
	"""A version of RYX's Screenlet-version of MacSlow\'s cairo-clock with 
	timezone selected from a list, supporting automatic daylight saving time 
	(DST) adjustment based on the rules for the selected timezone. Also supports
	custom multi-line date/time text display."""

	
	# default meta-info for Screenlets
	__name__ = 'WorldClockScreenlet'
	__version__ = '0.9.3'
	__author__ = 'jsf (aka Joe Forbes)'
	__desc__	= __doc__
	
	# internal vars
	__timeout		= None
	__buffer_back	= None
	__buffer_fore	= None
	__time = datetime.now()
	__alarm_running	= False
	__alarm_state	= 0
	__alarm_count	= 0

	__timezone_names = [] 					# the list of values for the timezone option
	__tzinfo = None  					# the actual tzinfo used in calculating time and date
	__last_timezone_name = "original_last"			# keeps track so we know when to re-cache __tzinfo
	__pango_layout 	= None

	# editable options
	# timezone_name is used to retrieve a tzinfo file from /usr/share/zoneinfo
	timezone_name = "America/Chicago"
	face_text = 'Austin%n%n%n%n%A, %b %e%n%T %Z'
	face_text_x		= 0
	face_text_y		= 19
	face_text_color	= (1.0, 1.0, 1.0, 1.0)
	face_text_font	= "Sans Medium 5"
	alarm_activated	= False
	alarm_time_h	= 10
	alarm_time_m	= 30
	alarm_time_s	= 0
	alarm_length	= 500	# times to blink before auto-stop
	hour_format		= "12"
	show_seconds_hand = True
	alarm_command = 'firefox'
	run_command = False
	text_alignment	= _('Center')

	# constructor
	def __init__ (self, parent_window=None, **keyword_args):
		"""Create a new WorldClockScreenlet instance."""
		# call super (we define to use our own service here)
		Screenlet.__init__(self, uses_theme=True, service_class=WorldClockService,
			**keyword_args)
		# set default theme for this Screenlet (causes redraw)
		# TODO: check, if theme is valid??
		self.theme_name = "default"
		self.scale = 2.0
		self.update()
		# update the clock once a second
		self.__timeout = gobject.timeout_add(1000, self.update)

		# jsf load timezone names from /usr/share/zoneinfo
		self.get_timezone_names() 

		# add default menuitems
		self.add_default_menuitems()
		# add settings-groups
		self.add_options_group(_('Clock'), _('Clock-specific settings.'))
		self.add_options_group(_('Alarm'), _('Settings for the Alarm-function.'))
		self.add_options_group(_('Face'), 
			_('Additional settings for the face-layout ...'))
		# add editable settings to this Screenlet
		
		self.add_option(StringOption(_('Clock'), 'timezone_name', 
			self.timezone_name, _('Timezone'), _('The timezone for this ')+\
			_('World Clock instance. Daylight saving adjustments are ')+\
			'made automatically using zoneinfo files in '+\
			'/usr/share/zoneinfo.', choices = self.__timezone_names
			))
		self.add_option(StringOption(_('Clock'), 'hour_format', 
			self.hour_format, _('Hour-Format'), 
			_('The hour-format (12/24) ...'), choices=['12', '24']))
		self.add_option(BoolOption(_('Clock'), 'show_seconds_hand', 
			self.show_seconds_hand, _('Show seconds-hand'), 
			_('Show/Hide the seconds-hand ...')))
		self.add_option(BoolOption(_('Alarm'), 'alarm_activated', 
			self.alarm_activated, _('Activate Alarm'), 
			_('Activate the alarm for this clock-instance ...')))
		self.add_option(BoolOption(_('Alarm'), 'run_command', 
			self.run_command, _('Run a command'), 
			_('Run a command when the alarm is activated...')))
		self.add_option(StringOption(_('Alarm'), 'alarm_command', 
			self.alarm_command, _('Alarm command'), 
			_('The command that should be run when the alarm goes off...')))
 		self.add_option(IntOption(_('Alarm'), 'alarm_time_h', 
 			self.alarm_time_h, _('Alarm-Time (Hour)'), 
 			_('The hour of the alarm-time ...'),min=0, max=23))
		self.add_option(IntOption(_('Alarm'), 'alarm_time_h', 
			self.alarm_time_h, _('Alarm-Time (Hour)'), 
			_('The hour of the alarm-time ...'),min=0, max=23))
		self.add_option(IntOption(_('Alarm'), 'alarm_time_m', 
			self.alarm_time_m, _('Alarm-Time (Minute)'), 
			_('The minute of the alarm-time ...'), min=0, max=59))
		self.add_option(IntOption(_('Alarm'), 'alarm_time_s', 
			self.alarm_time_s, _('Alarm-Time (Second)'), 
			_('The second of the alarm-time ...'), min=0, max=59))
		self.add_option(IntOption(_('Alarm'), 'alarm_length', 
			self.alarm_length, _('Alarm stops after'), 
			_('The times the clock shall blink before auto-stopped. ') + \
			_('Divide the number by two to get the seconds ...'), 
			min=0, max=5000))
		self.add_option(StringOption(_('Face'), 'face_text', 
			self.face_text, _('Face-Text'), 
			_('The text/Pango-Markup to be placed on the clock\'s face ...')))
		self.add_option(FontOption(_('Face'), 'face_text_font', 
			self.face_text_font, _('Text-Font'), 
			_('The font of the text (when no Markup is used) ...')))
		self.add_option(ColorOption(_('Face'), 'face_text_color', 
			self.face_text_color, _('Text-Color'), 
			_('The color of the text (when no Markup is used) ...')))
		self.add_option(IntOption(_('Face'), 'face_text_x', 
			self.face_text_x, _('X-Position of Text'), 
			_('The X-Position of the text-rectangle\'s upper left corner ...'), 
			min=0, max=100))
		self.add_option(IntOption(_('Face'), 'face_text_y', 
			self.face_text_y, _('Y-Position of Text'), 
			_('The Y-Position of the text-rectangle\'s upper left corner ...'), 
			min=0, max=100))
		self.add_option(StringOption(_('Face'), 'text_alignment', 
			self.text_alignment, _('Text alignment'), 
			_('The alignment of the text.'),
			choices = [_('Left'), _('Right'), _('Center')]))

	def __setattr__ (self, name, value):
		super(WorldClockScreenlet, self).__setattr__(name, value)
		if name == "timezone_name":
			self.update_timezone()
		elif name == "alarm_activated" and value==False:
			if self.__alarm_running:
				self.stop_alarm()
		elif name == 'show_seconds_hand':
			if value == True:
				self.set_update_interval(1000)
			else:
				self.set_update_interval(20000)
			self.redraw_canvas()
	
	def get_date (self):
		"""Only needed for the service."""
		return self.__time.strftime("%d/%m/%Y")

	def get_time (self):
		"""Only needed for the service."""
		return self.__time.strftime("%h/%i/%s")
	
	def on_load_theme (self): 
		"""A Callback to do special actions when the theme gets reloaded.
		(called AFTER loading theme and BEFORE redrawing shape/canvas)"""
		self.init_buffers()
		self.redraw_foreground()
		self.redraw_background()
	
	def on_scale (self):
		"""Called when the scale-attribute changes."""
		if self.window:
			self.init_buffers()
			self.redraw_foreground()
			self.redraw_background()
	
	def init_buffers (self):
		"""(Re-)Create back-/foreground buffers"""
		if self.window is None or self.window.window is None:
			return
		self.__buffer_back = gtk.gdk.Pixmap(self.window.window, 
			int(self.width * self.scale), int(self.height * self.scale), -1)
		self.__buffer_fore = gtk.gdk.Pixmap(self.window.window, 
			int(self.width * self.scale), int(self.height * self.scale), -1)
		
	def redraw_foreground (self):
		"""Redraw the foreground-buffer (face-shadow, glass, frame)."""
		if self.window is None or self.window.window is None:
			return
		# create context from fg-buffer
		ctx_fore = self.__buffer_fore.cairo_create()
		# clear context
		self.clear_cairo_context(ctx_fore)
		# and compose foreground
		ctx_fore.scale(self.scale, self.scale)
		self.theme['clock-face-shadow.svg'].render_cairo(ctx_fore)
		self.theme['clock-glass.svg'].render_cairo(ctx_fore)
		self.theme['clock-frame.svg'].render_cairo(ctx_fore)
	
	def redraw_background (self):
		"""Redraw the background-buffer (drop-shadow, face, marks)."""
		if self.window is None or self.window.window is None:
			return
		# create context
		ctx_back = self.__buffer_back.cairo_create()
		# clear context
		self.clear_cairo_context(ctx_back)
		# compose background
		ctx_back.set_operator(cairo.OPERATOR_OVER)
		ctx_back.scale(self.scale, self.scale)
		self.theme['clock-drop-shadow.svg'].render_cairo(ctx_back)
		self.theme['clock-face.svg'].render_cairo(ctx_back)

		self.theme['clock-marks.svg'].render_cairo(ctx_back)
	
	def start_alarm (self):
		"""Start the alarm-animation."""
		self.__alarm_running = True
		self.__alarm_count = self.alarm_length
		self.set_update_interval(500)
		if self.run_command == True and self.alarm_command != '':
			os.system(self.alarm_command)
		# send signal over service
		self.service.alarm_start(self.id)
	
	def stop_alarm (self):
		"""Stop the alarm-animation."""
		self.__alarm_running = False
		self.__alarm_count = 0
		self.set_update_interval(1000)
		# send signal over service
		self.service.alarm_stop(self.id)
	
	def set_update_interval (self, interval):
		"""Set the update-time in milliseconds."""
		if self.__timeout:
			gobject.source_remove(self.__timeout)
		self.__timeout = gobject.timeout_add(interval, self.update)
		
	def check_alarm (self):
		"""Checks current time with alarm-time and start alarm on match."""
		if self.__time.hour == self.alarm_time_h and \
			self.__time.minute == self.alarm_time_m and \
			self.__time.second == self.alarm_time_s:
			self.start_alarm()
				
	def update (self):
		"""Update the time and redraw the canvas"""
		if self.__last_timezone_name != self.timezone_name:
		  self.update_timezone()

		if self.__tzinfo is None:
		  os.environ['TZ'] = self.timezone_name
		  self.__time = datetime.now()
		else:
		  self.__time = datetime.now(self.__tzinfo)
		  
		if self.alarm_activated:
			self.check_alarm()

		self.redraw_canvas()
		return True # keep running this event
	
	def on_init (self):
		print "OK - Clock has been initialized."
	
	def on_draw (self, ctx):
		# no theme? no drawing
		if self.theme==None:
			return
		# get dimensions
		x = (self.theme.width / 2.0) * self.scale
		y = (self.theme.height / 2.0) * self.scale
		radius = min(self.theme.width / 2.0, self.theme.height / 2.0) - 5
		# render background buffer to context
		if self.__buffer_back:
			ctx.set_operator(cairo.OPERATOR_OVER)
			ctx.set_source_pixmap(self.__buffer_back, 0, 0)
			ctx.paint()
		# calc. scale relative to theme proportions
		ctx_w = self.scale
		ctx_h = self.scale
		# init time-vars
		hours = self.__time.hour
		minutes = self.__time.minute
		seconds = self.__time.second
		# TODO: use better shadow-placing
		shadow_offset_x = 1
		shadow_offset_y = 1
		# set hour-format specific vars
		if self.hour_format=="24":
			hf = 12.0
			hr = 720.0
		else:
			hf = 6.0
			hr = 360.0
		ctx.set_operator(cairo.OPERATOR_OVER)
		# render hour-hand-shadow
		ctx.save()
		ctx.translate (x+shadow_offset_x, y+shadow_offset_y)
		ctx.rotate(-math.pi/2.0)
		ctx.scale(ctx_w, ctx_h)
		ctx.rotate ((math.pi/hf) * hours + (math.pi/hr) * minutes)
		self.theme['clock-hour-hand-shadow.svg'].render_cairo(ctx)
		ctx.restore()
		# render hour-hand
		ctx.save()
		ctx.translate (x, y)
		ctx.rotate(-math.pi/2.0)
		ctx.scale(ctx_w, ctx_h)
		ctx.rotate ((math.pi/hf) * hours + (math.pi/hr) * minutes)
		self.theme['clock-hour-hand.svg'].render_cairo(ctx)
		ctx.restore()
		# render minutes-hand-shadow
		ctx.save()
		ctx.translate (x+shadow_offset_x, y+shadow_offset_y)
		ctx.rotate(-math.pi/2.0)
		ctx.scale(ctx_w, ctx_h)
		ctx.rotate((math.pi/30.0) * minutes)
		self.theme['clock-minute-hand-shadow.svg'].render_cairo(ctx)
		ctx.restore()
		# render minutes-hand
		ctx.save()
		ctx.translate(x, y);
		ctx.rotate(-math.pi/2.0)
		ctx.scale(ctx_w, ctx_h)
		ctx.rotate((math.pi/30.0) * minutes)
		self.theme['clock-minute-hand.svg'].render_cairo(ctx)
		ctx.restore()
		# render seconds-hand
		if self.show_seconds_hand:
			ctx.save()
			ctx.translate(x, y);
			ctx.rotate(-math.pi/2.0)
			ctx.set_source_rgba(0, 0, 0, 0.3)
			ctx.scale(ctx_w, ctx_h)
			ctx.rotate((math.pi/30.0) * seconds)
			ctx.translate(-shadow_offset_x, -shadow_offset_y)
			ctx.set_operator(cairo.OPERATOR_OVER)
			self.theme['clock-second-hand-shadow.svg'].render_cairo(ctx)
			ctx.translate(shadow_offset_x, shadow_offset_y)
			self.theme['clock-second-hand.svg'].render_cairo(ctx)
			ctx.restore()

		# render text
		self.redraw_text(ctx)

		# render foreground-buffer to context
		if self.__buffer_fore:
			ctx.set_operator(cairo.OPERATOR_OVER)
			ctx.set_source_pixmap(self.__buffer_fore, 0, 0)
			ctx.paint()
		# alarm-function
		if self.alarm_activated:
			if self.__alarm_running:
				ctx.set_operator(cairo.OPERATOR_ATOP)
				if self.__alarm_state == 1:
					ctx.set_source_rgba(1, 1, 1, 0.5)
					self.__alarm_state = 0
				else:
					ctx.set_source_rgba(0, 0, 0, 0.1)
					self.__alarm_state = 1
				ctx.paint()
				self.__alarm_count -= 1
				if self.__alarm_count == 0:
					self.stop_alarm()
			
	def on_draw_shape (self,ctx):
		if self.__buffer_back:
			ctx.set_operator(cairo.OPERATOR_OVER)
			ctx.set_source_pixmap(self.__buffer_back, 0, 0)
			ctx.paint()
			ctx.set_source_pixmap(self.__buffer_fore, 0, 0)
			ctx.paint()

	def get_timezone_names(self):
	# calls a recursive routine to get timezone filenames from /usr/share/zoneinfo, then sorts names
		self.__timezone_names = []
		# limit list to continents and oceans folders
		self.add_timezone_filenames("/usr/share/zoneinfo/Africa")
		self.add_timezone_filenames("/usr/share/zoneinfo/America")
		self.add_timezone_filenames("/usr/share/zoneinfo/Antarctica")
		self.add_timezone_filenames("/usr/share/zoneinfo/Arctic")
		self.add_timezone_filenames("/usr/share/zoneinfo/Asia")
		self.add_timezone_filenames("/usr/share/zoneinfo/Atlantic")
		self.add_timezone_filenames("/usr/share/zoneinfo/Australia")
		self.add_timezone_filenames("/usr/share/zoneinfo/Etc") # jsf adding UTC support by pulling in this directory
		self.add_timezone_filenames("/usr/share/zoneinfo/Europe")
		self.add_timezone_filenames("/usr/share/zoneinfo/Indian")
		self.add_timezone_filenames("/usr/share/zoneinfo/Pacific")
		self.__timezone_names.sort()
		return

	def add_timezone_filenames(self, path):
	# recursive routine to list zoneinfo filenames.
		for filename in os.listdir(path):
		# get all filenames, including subfolder names
			filepath = os.path.join(path, filename)
			if os.path.isdir(filepath):
				self.add_timezone_filenames(filepath)
			else:
				self.__timezone_names.append(filepath[len("/usr/share/zoneinfo") + 1:])

	def update_timezone(self):
		self.__tzinfo = zoneinfo.gettz(self.timezone_name) 
		self.__last_timezone_name = self.timezone_name

	def get_pango_layout(self, ctx):
		if self.__pango_layout == None:
			self.__pango_layout = ctx.create_layout()
		else:
			ctx.update_layout(self.__pango_layout)

	def redraw_text(self, ctx):
		try:
			txt = self.__time.strftime(self.face_text)
		except ValueError:
		# trap error with face_text that is invalid 
			# use face_text as-is
			txt = self.face_text

		if txt == '':
			return

		ctx.save()
		
		self.get_pango_layout(ctx)

		self.__pango_layout.set_width((self.width * pango.SCALE))

		if self.text_alignment == _('Left'):
			self.__pango_layout.set_alignment(pango.ALIGN_LEFT)
		elif self.text_alignment == _('Right'):
			self.__pango_layout.set_alignment(pango.ALIGN_RIGHT)
		elif self.text_alignment == _('Center'):
			self.__pango_layout.set_alignment(pango.ALIGN_CENTER)
		else:
			self.__pango_layout.set_alignment(pango.ALIGN_LEFT)
			print 'invalid alignment (' + str(self.text_alignment) + ')'

		om = '<span font_desc="'+self.face_text_font+'">'
		cm = '</span>'
		self.__pango_layout.set_markup(om + txt + cm)

		ctx.scale(self.scale, self.scale)
		ctx.translate(self.face_text_x, self.face_text_y)
		ctx.set_source_rgba(self.face_text_color[0], 
			self.face_text_color[1], self.face_text_color[2], 
			self.face_text_color[3])
		ctx.show_layout(self.__pango_layout)
		ctx.fill()

		ctx.restore()
	
# If the program is run directly or passed as an argument to the python
# interpreter then create a Screenlet instance and show it
if __name__ == "__main__":
	# create new session
	import screenlets.session
	screenlets.session.create_session(WorldClockScreenlet)

