#!/usr/bin/env python3 """Shared GUI Base Module for GTK3 Apps. Provides BaseApp class with: - Window setup (title, size, icon, HeaderBar, StatusBar) - Catppuccin theme via ClaudeCodePanel/theme.py - COSMIC dark/light auto-detection + watcher - Refresh timer management - Extendable CSS Usage: from gui_base import BaseApp class MyApp(BaseApp): def __init__(self): super().__init__("My App", 800, 600) # Add widgets to self.content_box label = Gtk.Label(label="Hello") self.content_box.pack_start(label, True, True, 0) """ import sys from pathlib import Path import gi gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") from gi.repository import Gdk, GLib, Gtk # Import theme module from ClaudeCodePanel sys.path.insert(0, str(Path.home() / "Projekte" / "ClaudeCodePanel")) from theme import build_css, get_palette, setup_theme_watcher # --------------------------------------------------------------------------- # Additional CSS for shared components # --------------------------------------------------------------------------- def _base_css(p: dict[str, str]) -> str: """Generate additional shared CSS classes.""" return f""" .base-card {{ border-radius: 8px; background: {p["card"]}; border: 1px solid {p["border"]}; padding: 10px; margin: 4px 0; }} .base-card:hover {{ background: {p["border"]}; }} .base-toolbar {{ background: {p["mantle"]}; border-radius: 8px; padding: 6px 10px; }} .base-badge {{ border-radius: 12px; padding: 2px 8px; font-size: 10px; font-weight: bold; }} .base-badge-ok {{ background: {p["green_hover_bg"]}; color: {p["green"]}; }} .base-badge-warn {{ background: {p["yellow_hover_bg"]}; color: {p["yellow"]}; }} .base-badge-error {{ background: rgba(243,139,168,0.2); color: {p["red"]}; }} .base-accent-btn {{ padding: 6px 16px; border-radius: 6px; background: {p["accent"]}; color: {p["resume_text"]}; font-weight: bold; border: none; }} .base-accent-btn:hover {{ background: {p["lavender"]}; }} """ class BaseApp(Gtk.Window): """Base class for standalone GTK3 apps with Catppuccin theming.""" def __init__( self, title: str, width: int = 800, height: int = 600, icon_name: str | None = None, ): super().__init__(title=title) self.set_default_size(width, height) if icon_name: self.set_icon_name(icon_name) self.connect("destroy", Gtk.main_quit) self._refresh_sources: list[int] = [] # --- Theme setup --- self._css_provider = Gtk.CssProvider() self._apply_full_css() Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), self._css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) setup_theme_watcher(self._css_provider) # --- Layout: Header + Content + StatusBar --- root_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add(root_box) # HeaderBar self._header = Gtk.HeaderBar() self._header.set_show_close_button(True) self._header.set_title(title) self.set_titlebar(self._header) # Content area (subclasses add widgets here) self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) self.content_box.set_margin_start(12) self.content_box.set_margin_end(12) self.content_box.set_margin_top(12) self.content_box.set_margin_bottom(6) root_box.pack_start(self.content_box, True, True, 0) # StatusBar self._status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self._status_box.get_style_context().add_class("status-bar") self._status_label = Gtk.Label(label="Ready") self._status_label.set_xalign(0) self._status_box.pack_start(self._status_label, True, True, 0) root_box.pack_end(self._status_box, False, False, 0) # --- Public API --- def set_subtitle(self, subtitle: str) -> None: """Set the HeaderBar subtitle.""" self._header.set_subtitle(subtitle) def add_header_widget(self, widget: Gtk.Widget, end: bool = True) -> None: """Add a widget to the HeaderBar (end=True for right side).""" if end: self._header.pack_end(widget) else: self._header.pack_start(widget) def set_status(self, text: str, css_class: str = "") -> None: """Update the status bar text and optional CSS class.""" self._status_label.set_text(text) ctx = self._status_label.get_style_context() for cls in ("status-saved", "status-error"): ctx.remove_class(cls) if css_class: ctx.add_class(css_class) def start_refresh(self, interval_seconds: int, callback) -> int: """Start a periodic refresh timer. Returns the source ID.""" source_id = GLib.timeout_add_seconds(interval_seconds, callback) self._refresh_sources.append(source_id) return source_id def stop_all_refresh(self) -> None: """Stop all refresh timers.""" for source_id in self._refresh_sources: GLib.source_remove(source_id) self._refresh_sources.clear() def add_css(self, css_string: str) -> None: """Add extra CSS rules. Call after __init__.""" palette = get_palette() full_css = build_css(palette).decode() + _base_css(palette) + css_string self._css_provider.load_from_data(full_css.encode()) def run(self) -> None: """Show all widgets and start the GTK main loop.""" self.show_all() Gtk.main() # --- Internal --- def _apply_full_css(self) -> None: """Load theme CSS + base CSS into the provider.""" palette = get_palette() full_css = build_css(palette).decode() + _base_css(palette) self._css_provider.load_from_data(full_css.encode())