mirror of
https://github.com/gryf/pygtkworldclock.git
synced 2025-12-17 03:20:23 +01:00
Initial commit
This commit is contained in:
139
README.rst
Normal file
139
README.rst
Normal 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
33
example.yaml
Normal 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
BIN
images/grid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
images/horizontal.png
Normal file
BIN
images/horizontal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
images/single.png
Normal file
BIN
images/single.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
images/vertical.png
Normal file
BIN
images/vertical.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
259
worldclock.py
Executable file
259
worldclock.py
Executable 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()
|
||||||
Reference in New Issue
Block a user