mirror of
https://github.com/gryf/pygtkworldclock.git
synced 2025-12-17 11:30:21 +01:00
269 lines
9.5 KiB
Python
Executable File
269 lines
9.5 KiB
Python
Executable File
#!/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, disable_resize=False):
|
|
super(Clocks, self).__init__()
|
|
self._conf = conf
|
|
self._clocks = []
|
|
self.size = size
|
|
self.show_seconds = not disable_seconds
|
|
self.disable_resize = disable_resize
|
|
|
|
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(not self.disable_resize)
|
|
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)
|
|
parser.add_argument("-r", "--disable-resize", help="Disable resize of "
|
|
"the window. Note, that with single clock you'll "
|
|
"have at least 200px width no matter if you put "
|
|
"something less in size option.", 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,
|
|
disable_resize=args.disable_resize)
|
|
clocks.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|