1
0
mirror of https://github.com/gryf/pygtkworldclock.git synced 2025-12-17 03:20:23 +01:00

Initial commit

This commit is contained in:
2018-06-10 20:25:29 +02:00
commit 2f9b928745
7 changed files with 431 additions and 0 deletions

139
README.rst Normal file
View File

@@ -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

33
example.yaml Normal file
View File

@@ -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

BIN
images/grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/horizontal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/single.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/vertical.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

259
worldclock.py Executable file
View File

@@ -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()