__init__.py
Python script, ASCII text executable
1""" 2Clock applet for the Panorama panel. 3Copyright 2025, roundabout-host.com <vlad@roundabout-host.com> 4 5This program is free software: you can redistribute it and/or modify 6it under the terms of the GNU General Public Licence as published by 7the Free Software Foundation, either version 3 of the Licence, or 8(at your option) any later version. 9 10This program is distributed in the hope that it will be useful, 11but WITHOUT ANY WARRANTY; without even the implied warranty of 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13GNU General Public Licence for more details. 14 15You should have received a copy of the GNU General Public Licence 16along with this program. If not, see <https://www.gnu.org/licenses/>. 17""" 18 19import os 20from pathlib import Path 21import panorama_panel 22 23import gi 24gi.require_version("Gtk", "4.0") 25 26from gi.repository import Gtk, GLib, Gio, Gdk 27 28 29SECOND_PLACEHOLDERS = ("%c", "%s", "%S", "%T", "%X") 30 31 32module_directory = Path(__file__).resolve().parent 33 34 35@Gtk.Template(filename=str(module_directory / "panorama-clock-options.ui")) 36class ClockOptions(Gtk.Window): 37__gtype_name__ = "ClockOptions" 38format_entry: Gtk.Entry = Gtk.Template.Child() 39 40def __init__(self, **kwargs): 41super().__init__(**kwargs) 42 43self.connect("close-request", lambda *args: self.destroy()) 44 45 46class ClockApplet(panorama_panel.Applet): 47name = "Clock" 48description = "Read the current time and date" 49 50def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 51super().__init__(orientation=orientation, config=config) 52if config is None: 53config = {} 54self.button = Gtk.MenuButton() 55self.button.set_has_frame(False) # flat look 56self.label = Gtk.Label() 57self.button.set_child(self.label) 58 59# Create the monthly calendar 60self.popover = Gtk.Popover() 61panorama_panel.track_popover(self.popover) 62self.calendar = Gtk.Calendar() 63self.calendar.set_show_week_numbers(True) 64self.popover.set_child(self.calendar) 65self.button.set_popover(self.popover) 66 67self.append(self.button) 68 69self.formatting = config.get("formatting", "%c") 70# Some placeholders require second precision, but not all of them. If not required, 71# use minute precision 72self.has_second_precision = any(placeholder in self.formatting for placeholder in SECOND_PLACEHOLDERS) 73self.next_update = None 74self.set_time() 75 76self.context_menu = self.make_context_menu() 77panorama_panel.track_popover(self.context_menu) 78 79right_click_controller = Gtk.GestureClick() 80right_click_controller.set_button(3) 81right_click_controller.connect("pressed", self.show_context_menu) 82 83self.add_controller(right_click_controller) 84 85action_group = Gio.SimpleActionGroup() 86options_action = Gio.SimpleAction.new("options", None) 87options_action.connect("activate", self.show_options) 88action_group.add_action(options_action) 89self.insert_action_group("applet", action_group) 90 91self.options_window = None 92 93def make_context_menu(self): 94menu = Gio.Menu() 95menu.append("Clock _options", "applet.options") 96context_menu = Gtk.PopoverMenu.new_from_model(menu) 97context_menu.set_has_arrow(False) 98context_menu.set_parent(self) 99context_menu.set_halign(Gtk.Align.START) 100context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 101return context_menu 102 103def show_context_menu(self, gesture, n_presses, x, y): 104rect = Gdk.Rectangle() 105rect.x = int(x) 106rect.y = int(y) 107rect.width = 1 108rect.height = 1 109 110self.context_menu.set_pointing_to(rect) 111self.context_menu.popup() 112 113def update_formatting(self, entry): 114self.formatting = entry.get_text() 115# Some placeholders require second precision, but not all of them. If not required, 116# use minute precision 117self.has_second_precision = any(placeholder in self.formatting for placeholder in SECOND_PLACEHOLDERS) 118if self.next_update is not None: 119GLib.source_remove(self.next_update) 120self.next_update = None 121self.set_time() 122self.emit("config-changed") 123 124def show_options(self, _0=None, _1=None): 125if self.options_window is None: 126self.options_window = ClockOptions() 127self.options_window.format_entry.set_text(self.formatting) 128self.options_window.format_entry.connect("changed", self.update_formatting) 129 130def reset_window(*args): 131self.options_window = None 132 133self.options_window.connect("close-request", reset_window) 134self.options_window.present() 135 136def set_time(self): 137datetime = GLib.DateTime.new_now_local() 138formatted_time = datetime.format(self.formatting) 139if formatted_time is not None: 140self.label.set_text(datetime.format(self.formatting)) 141else: 142self.label.set_text("Invalid time formatting") 143return False 144 145if self.has_second_precision: 146current_ms = GLib.DateTime.new_now_local().get_microsecond() // 1000 147self.next_update = GLib.timeout_add(1000 - current_ms + 1, self.set_time) # 1ms is added to ensure the clock is updated 148else: 149now = GLib.DateTime.new_now_local() 150current_ms = now.get_second() * 1000 + now.get_microsecond() // 1000 151self.next_update = GLib.timeout_add(60000 - current_ms + 1, self.set_time) 152return False # Do not rerun the current timeout; a new one has been scheduled 153 154def get_config(self): 155return {"formatting": self.formatting} 156 157def set_panel_position(self, position): 158self.popover.set_position(panorama_panel.OPPOSITE_POSITION[position]) 159self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]]) 160