1
0
mirror of https://github.com/gryf/pygtkworldclock.git synced 2025-12-17 03:20:23 +01:00
Files
pygtkworldclock/worldclock.py
2018-06-10 20:25:29 +02:00

260 lines
9.0 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):
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()