commit 2f9b9287450b07655f9882328e4ed6e31a97c787 Author: gryf Date: Sun Jun 10 20:25:29 2018 +0200 Initial commit diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1842e49 --- /dev/null +++ b/README.rst @@ -0,0 +1,139 @@ +pyGTKWorldClock +=============== + +This is simple world clock application written in Python and Cairo. + +Requirement +----------- + +- Python3 +- PyCairo +- PyGObject + +Configuration +------------- + +See `example.yaml`_ for reference. Basically config file is a YAML file, which +contain a definition of timezone and the label, which in the end maps as python +dictionary: + +.. code:: python + + {'tz': 'string of timezone' + 'label': 'string of some label'} + +For example, single clock can be represended as: + +.. code:: yaml + + --- + - + tz: UTC + label: The real world clock + +which would have an effect: + +.. image:: /images/single.png + :alt: single clock + +Note, that it have to be a list in YAML format (line started with ``-``) +followed by a definition of key-value of timezone and a label. Analogically, +several clocks (let's take an example of US timezones) can be vertically +arranged by providing a *list* of *key-values*: + +.. code:: yaml + + --- + - + tz: US/Hawaii + label: Honolulu, Hawaii, US + - + tz: US/Alaska + label: Anchorage, Alaska, US + - + tz: US/Pacific + label: Portland, Oregon, US + - + tz: US/Mountain + label: Salt Lake City, Utah, US + - + tz: US/Central + label: Austin, Texas, US + - + tz: US/Eastern + label: New York, US + +and the result: + +.. image:: /images/vertical.png + :alt: single clock + +Same in horizontal arragement: + +.. code:: yaml + + --- + - + - + tz: US/Hawaii + label: Honolulu, Hawaii, US + - + tz: US/Alaska + label: Anchorage, Alaska, US + - + tz: US/Pacific + label: Portland, Oregon, US + - + tz: US/Mountain + label: Salt Lake City, Utah, US + - + tz: US/Central + label: Austin, Texas, US + - + tz: US/Eastern + label: New York, US + +obviously the result would be: + +.. image:: /images/horizontal.png + :alt: single clock + +And finally the same in two rows, three columns: + +.. code:: yaml + + --- + - + - + tz: US/Hawaii + label: Honolulu, Hawaii, US + - + tz: US/Alaska + label: Anchorage, Alaska, US + - + tz: US/Pacific + label: Portland, Oregon, US + - + - + tz: US/Mountain + label: Salt Lake City, Utah, US + - + tz: US/Central + label: Austin, Texas, US + - + tz: US/Eastern + label: New York, US + +which will look like that: + +.. image:: /images/grid.png + :alt: single clock + +You can experiment to get the layout of your choice. + +License +------- + + + +.. _example.yaml: example.yaml diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..aec9f66 --- /dev/null +++ b/example.yaml @@ -0,0 +1,33 @@ +--- +- + - + tz: US/Pacific + label: Portland Oregon, US + - + tz: US/Central + label: Austin Texas, US + - + tz: US/Eastern + label: New York, US + - + tz: UTC + label: UTC + - + tz: Europe/London + label: London, UK +- + - + tz: Europe/Warsaw + label: Warsaw, Poland + - + tz: Europe/Moscow + label: Moscow, Russia + - + tz: Asia/Shanghai + label: Shanghai, China + - + tz: Asia/Tokyo + label: Tokyo, Japan + - + tz: Australia/Melbourne + label: Melbourne, Australia diff --git a/images/grid.png b/images/grid.png new file mode 100644 index 0000000..354326e Binary files /dev/null and b/images/grid.png differ diff --git a/images/horizontal.png b/images/horizontal.png new file mode 100644 index 0000000..1f797b0 Binary files /dev/null and b/images/horizontal.png differ diff --git a/images/single.png b/images/single.png new file mode 100644 index 0000000..aebfc8e Binary files /dev/null and b/images/single.png differ diff --git a/images/vertical.png b/images/vertical.png new file mode 100644 index 0000000..ba3df9f Binary files /dev/null and b/images/vertical.png differ diff --git a/worldclock.py b/worldclock.py new file mode 100755 index 0000000..7b5f9d6 --- /dev/null +++ b/worldclock.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Simple Cairo world clock. Even if it's using GTK, it's ignore most of the +possible goodies, which GTK provides, because of limitations, that DrawingArea +and container managers in GTK have. + +Putting DA in grid for example, will kind of work, but, you'll end up with +this: + - every single DA in a grid cells will have 1x1 px dimension, + - unless you initialize DA with some values for its width and height, but + you'll loose scalability of the widget, + - every thing you draw on DA will be "invisible", since coordinates, which + we can get are limited to the current widget (DrawingArea descendant) + which will get you relative coordinates, and there is no way to get + coordinates of the grid cell, so that you'll end up with several + DrawingAreas, which have absolute coordinates just like the first one, + and the others just will be hidden behind the first one in the best + case, and in worst - view window od the DrawingArea will be shifted by + the selected cell, which eventually end up with no drawings at all. +As for Box manager, you'll suffer from the last item of the grid list above, +which will end up with "invisible" or misplaced widget in the box itself. + +Due to this stupid design, DrawingArea descendant object should be placed +directly in window object. Using single clock in container manager (like grid, +table, box) will make you hurt, and you'll be swearing a lot like I did. Don't +do that. It's a waste of time. +""" +import argparse +from datetime import datetime +import math +import os +import sys + +import cairo +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk +from gi.repository import GObject +import pytz +import yaml + + +CLOCK_DA_DIAMETER = 140 +DATE_FMT = '%Y-%m-%d %H:%M %Z' +CFG_GNAME = "gtkworldclock.yaml" + + +class Clock: + """Clock class for conveniently keeping it's state""" + def __init__(self, conf, row=0, col=0, size=CLOCK_DA_DIAMETER, + show_seconds=True): + self.tz = pytz.timezone(conf['tz']) + self.label = conf['label'] + self.row = row + self.col = col + self.now = None + self.size = size + self.show_seconds = show_seconds + + self._calculate_coordinates() + + def _calculate_coordinates(self): + self.x = self.col * self.size + self.size / 2 + self.y = self.row * self.size + self.size / 2 + self.y = self.y + self.row * self.size / 4 + self.radius = self.size / 2 - 5 + + def _show_caption(self, ctx): + self._calculate_coordinates() + ctx.save() + ctx.set_font_size(self.radius / 7) + x, y, width, height, dx, dy = ctx.text_extents(self.label) + ctx.move_to(self.x - x/2 - width/2, + self.y + self.radius + 2 * self.radius / 10) + ctx.show_text(self.label) + + date = self.now.strftime(DATE_FMT) + + x, y, width, height, dx, dy = ctx.text_extents(date) + ctx.move_to(self.x - x/2 - width/2, + self.y + self.radius + 2 * self.radius / 10 + + self.size / 10) + ctx.show_text(date) + ctx.restore() + + def _draw_face(self, ctx): + self._calculate_coordinates() + ctx.move_to(self.x + self.radius * math.cos(0 * math.pi/30), + self.y + self.radius * math.sin(0 * math.pi/30)) + ctx.arc(self.x, self.y, self.radius, 0, 2 * math.pi) + ctx.set_source_rgb(1, 1, 1) + ctx.fill_preserve() + ctx.set_source_rgb(0, 0, 0) + ctx.stroke() + + def _draw_ticks(self, ctx): + self._calculate_coordinates() + for i in range(180): + ctx.save() + if i % 5 == 0: + if i % 3 == 0: + # hours: 12, 3, 6, and 9 + inset = 0.15 * self.radius + else: + # all other hours + inset = 0.1 * self.radius + ctx.set_line_width(0.5 * ctx.get_line_width()) + else: + # seconds + inset = 0.05 * self.radius + ctx.set_line_width(0.25 * ctx.get_line_width()) + + ctx.move_to(self.x + (self.radius-inset) * + math.cos(i * math.pi/30), + self.y + (self.radius-inset) * + math.sin(i * math.pi/30)) + ctx.line_to(self.x + self.radius * math.cos(i * math.pi/30), + self.y + self.radius * math.sin(i * math.pi/30)) + ctx.stroke() + ctx.restore() + + def _draw_hands(self, ctx): + self._calculate_coordinates() + self.now = datetime.now(self.tz) + hours = self.now.hour * math.pi / 6 + minutes = self.now.minute * math.pi / 30 + seconds = self.now.second * math.pi / 30 + + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + + # draw the hours hand + ctx.save() + ctx.set_line_width(3) + ctx.move_to(self.x, self.y) + ctx.line_to(self.x + math.sin(hours + minutes/12) * + (self.radius * 0.5), + self.y - math.cos(hours + minutes/12) * + (self.radius * 0.5)) + ctx.stroke() + ctx.restore() + + # draw the minutes hand + ctx.save() + ctx.set_line_width(2) + ctx.move_to(self.x, self.y) + ctx.line_to(self.x + math.sin(minutes + seconds/60) * + (self.radius * 0.8), + self.y - math.cos(minutes + seconds/60) * + (self.radius * 0.8)) + ctx.stroke() + ctx.restore() + + if not self.show_seconds: + return + + # draw the seconds hand + ctx.save() + ctx.set_line_width(1) + ctx.move_to(self.x, self.y) + ctx.line_to(self.x + math.sin(seconds) * (self.radius * 0.9), + self.y - math.cos(seconds) * (self.radius * 0.9)) + ctx.stroke() + ctx.restore() + + +class Clocks(Gtk.DrawingArea): + + def __init__(self, conf=None, size=CLOCK_DA_DIAMETER, + disable_seconds=False): + super(Clocks, self).__init__() + self._conf = conf + self._clocks = [] + self.size = size + self.show_seconds = not disable_seconds + + self._parse_conf() + + def _parse_conf(self): + self.height = len(self._conf) * (self.size + self.size / 4) + width = 0 + if isinstance(self._conf[0], dict): + self.width = self.size + for row_no, conf in enumerate(self._conf): + self._clocks.append(Clock(conf, row_no, 0, self.size, + self.show_seconds)) + return + + for row_no, row in enumerate(self._conf): + width = len(row) if len(row) > width else width + for col_no, conf in enumerate(row): + self._clocks.append(Clock(conf, row_no, col_no, self.size, + self.show_seconds)) + + self.width = self.size * width + + def run(self): + self.connect('draw', self._draw) + GObject.timeout_add(100, self.on_timeout) + win = Gtk.Window() + win.set_title('World Clock') + # win.set_resizable(False) + win.connect('destroy', lambda w: Gtk.main_quit()) + win.set_default_size(int(self.width), int(self.height)) + win.resize(int(self.width), int(self.height)) + win.add(self) + win.show_all() + Gtk.main() + + def on_timeout(self): + """Tic-toc""" + win = self.get_window() + rect = self.get_allocation() + win.invalidate_rect(rect, True) + return True + + def _draw(self, da, cairo_ctx): + for clock in self._clocks: + clock._draw_face(cairo_ctx) + clock._draw_ticks(cairo_ctx) + clock._draw_hands(cairo_ctx) + clock._show_caption(cairo_ctx) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--config", help="Provide configuration as YAML" + "file.") + parser.add_argument("-s", "--size", help="Size of the clock faces, " + "default %d" % CLOCK_DA_DIAMETER, type=int, + default=CLOCK_DA_DIAMETER) + parser.add_argument("-d", "--disable-seconds", help="Disable seconds in " + "clock face", action="store_true", default=False) + args = parser.parse_args() + args = parser.parse_args() + + conf = None + xdg_conf = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) + + if not args.config: + conf_path = os.path.join(xdg_conf, CFG_GNAME) + if not os.path.exists(conf_path): + print("Cannot find proper configuration for the World Clock. " + "Please provide proper configuration in %s file, or use " + "`--config` switch for providing it at the " + "commandline." % conf_path) + sys.exit(1) + else: + conf_path = args.config + + with open(conf_path) as fobj: + conf = yaml.load(fobj) + + clocks = Clocks(conf, size=args.size, disable_seconds=args.disable_seconds) + clocks.run() + + +if __name__ == "__main__": + main()