feat(root): implement dynamic todo list refresh and UI enhancements

This change adds a mechanism to refresh the todo list dynamically when the underlying service updates, improving UI responsiveness and user experience. It also updates documentation and removes deprecated files.

- New Features:
  - Added `_refresh_todos` method to rebuild the todo list when `get_todo_service().todos` changes.
  - Introduced `TodoListCapture` class for mouse capture handling of the todo list window.
- Enhancements:
  - Updated README with corrected image paths, installation tips, and manual installation commands.
  - Introduced `start.py` to launch the application or lock mode.
  - Removed deprecated `config.json` and `debug_memory.py`.
- Bug Fixes:
  - Fixed lock screen screenshot path in README.
This commit is contained in:
sankalp committed 2026-04-10 17:11:32 +05:45
1 parent 235f53b6a2
commit e4813f35a0
267 files changed
+24582 -394

No files matched your search

+387
View File
@@ -0,0 +1,387 @@
from fabric.utils import GLib
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.image import Image
from fabric.widgets.label import Label
from fabric.widgets.separator import Separator
from services.gamemode import check_gamemode, toggle_gamemode
from services.battery import BatteryService, DeviceState
from utils.functions import clear_children, format_duration
from utils.utils import svg_file
class EnergyModeButton(Box):
def __init__(
self,
profile_name: str,
display_name: str,
icon_name: str,
battery_service: BatteryService,
parent,
**kwargs,
):
super().__init__(name="energy-mode-button", **kwargs)
self.profile_name = profile_name
self.battery_service = battery_service
self.parent = parent
self.mode_icon_svg = svg_file(f"power_modes/battery-{icon_name}.svg", size=24)
self.mode_icon = Box(
children=[self.mode_icon_svg],
name="energy-mode-icon",
style_classes="battery-profile-icon",
)
self.mode_label = Label(
label=display_name,
style_classes="battery-power-mode",
h_align="start",
h_expand=True,
)
start_box = Box(
orientation="horizontal",
spacing=4,
children=[self.mode_icon, self.mode_label],
)
self.button = Button(
child=start_box,
h_expand=True,
name="energy-mode-button-clickable",
on_clicked=self.on_clicked,
style_classes="battery-profile-button",
)
self.children = [self.button]
self.update_state()
def on_clicked(self, *args):
success = self.battery_service.set_power_profile(self.profile_name)
if success:
# Update all profile buttons in parent
self.parent.update_energy_mode_buttons()
# Reset icon state after short delay
GLib.timeout_add(300, lambda: self._reset_icon_state())
def _reset_icon_state(self):
return False # Remove timeout
def update_state(self):
current_profile = self.battery_service.get_power_profile()
is_active = current_profile == self.profile_name
if is_active:
self.mode_icon.add_style_class("connected")
else:
self.mode_icon.remove_style_class("connected")
class GameModeButton(Box):
def __init__(self, parent, **kwargs):
super().__init__(name="energy-mode-button", h_expand=True, **kwargs)
self.parent = parent
self.game_icon = Image(
icon_name="applications-games-symbolic",
size=16,
name="game-mode-icon",
style_classes="battery-gamemode-icon",
)
self.game_label = Label(
label="Game Mode",
style_classes="gamemode-button",
h_align="start",
h_expand=True,
)
start_box = Box(
orientation="horizontal",
spacing=3,
children=[self.game_icon, self.game_label],
)
self.button = Button(
child=start_box,
name="game-mode-button-clickable",
on_clicked=self.on_clicked,
h_expand=True,
style_classes="battery-gamemode-button",
)
self.children = [self.button]
self.update_state()
def on_clicked(self, *args):
try:
toggle_gamemode()
GLib.timeout_add(500, lambda: self.update_state())
except Exception as e:
print(f"Failed to toggle game mode: {e}")
GLib.timeout_add(300, lambda: self._reset_icon_state())
def _reset_icon_state(self):
return False # Remove timeout
def update_state(self):
try:
is_active = check_gamemode() == "t"
if is_active:
self.game_icon.add_style_class("connected")
else:
self.game_icon.remove_style_class("connected")
except Exception as e:
print(f"Failed to check game mode status: {e}")
self.game_icon.remove_style_class("connected")
return False # Remove timeout if called from GLib.timeout_add
class BatteryControl(Box):
def __init__(self, parent, **kwargs):
super().__init__(
spacing=12,
orientation="vertical",
name="control-center-widgets",
**kwargs,
)
self.set_size_request(354, -1)
self.parent = parent
self.battery_service = BatteryService()
self.energy_mode_buttons = []
self.battery_widget = Box(
name="battery-widget",
orientation="vertical",
style_classes="battery-status-section",
h_expand=True,
spacing=8,
)
self.battery_title = Label(
label="Battery", style_classes="battery-main-title", h_align="start"
)
self.battery_percentage_label = Label(
label="80%", style_classes="battery-percentage", h_align="end"
)
self.battery_header = CenterBox(
start_children=self.battery_title,
end_children=self.battery_percentage_label,
name="battery-header",
)
self.power_source_label = Label(
label="Power Source: Power Adapter",
style_classes="battery-power-source",
h_align="start",
)
self.charging_time_label = Label(
label="1h 4m until fully charged",
style_classes="battery-power-source",
h_align="start",
)
self.energy_mode_section = Box(
orientation="vertical", spacing=8, name="energy-mode-section"
)
self.energy_mode_title = Label(
label="Energy Mode", style_classes="battery-section-title", h_align="start"
)
self.energy_modes_container = Box(
orientation="vertical", spacing=4, name="energy-modes-container"
)
self.game_mode_section = Box(
orientation="vertical", spacing=8, name="game-mode-section"
)
self.game_mode_title = Label(
label="Game Mode", style_classes="battery-section-title", h_align="start"
)
self.game_mode_container = Box(
orientation="vertical", spacing=4, name="game-mode-container"
)
self.battery_settings_button = Button(
v_align="center",
child=Label(
label="Battery Settings",
h_align="start",
),
style_classes="battery-settings-button",
on_clicked=self.open_battery_settings,
)
self.battery_widget.add(self.battery_header)
self.battery_widget.add(self.power_source_label)
self.battery_widget.add(self.charging_time_label)
separator1 = Separator(orientation="h", name="separator")
self.battery_widget.add(separator1)
self.energy_mode_section.add(self.energy_mode_title)
self.energy_mode_section.add(self.energy_modes_container)
self.battery_widget.add(self.energy_mode_section)
separator2 = Separator(orientation="h", name="separator")
self.battery_widget.add(separator2)
self.game_mode_section.add(self.game_mode_title)
self.game_mode_section.add(self.game_mode_container)
self.battery_widget.add(self.game_mode_section)
separator3 = Separator(orientation="h", name="separator")
self.battery_widget.add(separator3)
self.battery_widget.add(self.battery_settings_button)
self.add(self.battery_widget)
self.battery_service.connect("changed", self.on_battery_changed)
self.battery_service.connect("power_profile_changed", self.on_profile_changed)
# Initialize display
self.update_battery_info()
self.create_energy_mode_buttons()
self.create_game_mode_button()
def open_battery_settings(self, *args):
# TODO: Implement to open Battery Settings
pass
def create_energy_mode_buttons(self):
# Clear existing buttons
for button in self.energy_mode_buttons:
button.destroy()
self.energy_mode_buttons.clear()
# Get available profiles
available_profiles = self.battery_service.get_available_power_profiles() or []
if not available_profiles:
no_profiles_label = Label(
label="No energy modes available",
style_classes="battery-no-profiles",
h_align="start",
)
self.energy_modes_container.add(no_profiles_label)
return
# Define energy mode mappings with proper icon names
energy_mode_config = {
"balanced": {"display": "Automatic", "icon": "balanced"},
"power-saver": {"display": "Low Power", "icon": "power"},
"powersave": {"display": "Low Power", "icon": "power"},
"performance": {"display": "High Power", "icon": "performance"},
}
# Define the desired order for energy modes
desired_order = ["balanced", "power-saver", "powersave", "performance"]
# Create ordered list of available profiles
ordered_profiles = []
for profile_name in desired_order:
if profile_name in available_profiles:
ordered_profiles.append(profile_name)
# Add any remaining profiles not in the desired order
for profile in available_profiles:
if profile not in ordered_profiles:
ordered_profiles.append(profile)
# Create button for each available profile in the specified order
for profile in ordered_profiles:
config = energy_mode_config.get(
profile, {"display": profile.title(), "icon": "good"}
)
button = EnergyModeButton(
profile_name=profile,
display_name=config["display"],
icon_name=config["icon"],
battery_service=self.battery_service,
parent=self,
)
self.energy_mode_buttons.append(button)
self.energy_modes_container.add(button)
def update_energy_mode_buttons(self):
for button in self.energy_mode_buttons:
button.update_state()
def create_game_mode_button(self):
# Clear existing game mode button if any
clear_children(self.game_mode_container)
# Create game mode button
self.game_mode_button = GameModeButton(parent=self)
self.game_mode_container.add(self.game_mode_button)
def _format_time(self, seconds: int) -> str:
return format_duration(seconds)
def update_battery_info(self):
is_present = bool(self.battery_service.get_property("IsPresent"))
if not is_present:
self.battery_percentage_label.set_label("No Battery")
self.power_source_label.set_label("Power Source: Not Present")
self.charging_time_label.set_label("")
return
# Update percentage in header
percentage = int(self.battery_service.get_property("Percentage") or 0)
self.battery_percentage_label.set_label(f"{percentage}%")
# Update power source and charging info
state_code = self.battery_service.get_property("State")
state = DeviceState.get(state_code, "UNKNOWN")
if state in ["CHARGING", "PENDING_CHARGE"]:
self.power_source_label.set_label("Power Source: Power Adapter")
seconds_to_full = int(self.battery_service.get_property("TimeToFull") or 0)
time_to_full = self._format_time(seconds_to_full)
if time_to_full != "N/A" and time_to_full != "0m":
self.charging_time_label.set_label(
f"{time_to_full} until fully charged"
)
else:
self.charging_time_label.set_label("Charging...")
elif state == "FULLY_CHARGED":
self.power_source_label.set_label("Power Source: Power Adapter")
self.charging_time_label.set_label("Fully Charged")
elif state in ["DISCHARGING", "PENDING_DISCHARGE"]:
self.power_source_label.set_label("Power Source: Battery")
seconds_to_empty = int(
self.battery_service.get_property("TimeToEmpty") or 0
)
time_to_empty = self._format_time(seconds_to_empty)
if time_to_empty != "N/A" and not time_to_empty.startswith(
"4553h"
): # Filter out unrealistic times
self.charging_time_label.set_label(f"{time_to_empty} remaining")
else:
self.charging_time_label.set_label("On Battery Power")
elif state == "EMPTY":
self.power_source_label.set_label("Power Source: Battery")
self.charging_time_label.set_label("Battery Empty")
else:
self.power_source_label.set_label("Power Source: Unknown")
self.charging_time_label.set_label("")
def on_battery_changed(self, *args):
self.update_battery_info()
def on_profile_changed(self, service, *args):
self.update_energy_mode_buttons()
+90
View File
@@ -0,0 +1,90 @@
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.eventbox import EventBox
from utils.roam import modus_service
from shared.window.popup_window import PopupWindow
dropdowns = []
def dropdown_divider(comment):
return Box(
children=[Box(name="dropdown-divider", h_expand=True)],
name="dropdown-divider-box",
h_align="fill",
h_expand=True,
v_expand=True,
)
class ModusDropdown(PopupWindow):
def __init__(self, dropdown_children=None, dropdown_id=None, **kwargs):
super().__init__(
layer="top",
exclusivity="auto",
name="dropdown-menu",
title="modus",
keyboard_mode="none",
visible=False,
**kwargs,
)
self.id = dropdown_id or str(len(dropdowns))
dropdowns.append(self)
self._mousecapture_parent = None # Will be set by mousecapture
modus_service.connect("dropdowns-hide-changed", self.hide_dropdown)
self.dropdown = Box(
children=dropdown_children or [],
h_expand=True,
name="dropdown-options",
orientation="vertical",
)
self.child_box = CenterBox(start_children=[self.dropdown])
self.event_box = EventBox(
events=["enter-notify-event", "leave-notify-event"],
child=self.child_box,
all_visible=True,
)
self.children = [self.event_box]
self.add_keybinding("Escape", self.hide_dropdown)
def toggle_dropdown(self, button, parent=None):
self.set_visible(not self.is_visible())
modus_service.current_dropdown = self.id if self.is_visible() else None
def _init_mousecapture(self, mousecapture):
"""Store reference to mousecapture parent for hiding"""
self._mousecapture_parent = mousecapture
def hide_dropdown(self, widget, event):
if self.is_visible():
self.hide()
if str(modus_service.current_dropdown) == str(self.id):
modus_service.current_dropdown = None
def hide_via_mousecapture(self):
"""Hide dropdown via mousecapture parent"""
if self._mousecapture_parent:
self._mousecapture_parent.hide_child_window()
def _set_mousecapture(self, visible: bool) -> None:
self.set_visible(visible)
if visible:
modus_service.current_dropdown = self.id
else:
if str(modus_service.current_dropdown) == str(self.id):
modus_service.current_dropdown = None
def on_cursor_enter(self, *_):
self.set_visible(True)
def on_cursor_leave(self, *_):
if self.is_hovered():
return
self.set_visible(False)
modus_service.dropdowns_hide = not modus_service.dropdowns_hide
+301
View File
@@ -0,0 +1,301 @@
from fabric.utils import Any, Gdk, cairo
from fabric.widgets.eventbox import EventBox
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.widgets.widget import Widget
from gi.repository import GtkLayerShell # type: ignore
from utils.monitors import HyprlandWithMonitors
from utils.roam import modus_service
class MouseCapture(Window):
"""A background overlay that captures outside clicks without blocking child window interactions"""
def __init__(self, layer: str, child_window: Window, **kwargs):
super().__init__(
layer=layer, # Use the passed layer
anchor="top bottom left right",
exclusivity="auto",
title="modus",
name="MouseCapture",
keyboard_mode="none", # Don't steal keyboard
all_visible=False,
visible=False,
**kwargs,
)
GtkLayerShell.set_exclusive_zone(self, -1)
self.child_window = child_window
# Ensure child window is on overlay layer to be above this capture
if hasattr(self.child_window, "layer"):
self.child_window.layer = "overlay"
if hasattr(self.child_window, "_init_mousecapture"):
self.child_window._init_mousecapture(self)
# Create transparent event box that captures clicks
self.event_box = EventBox(
events=[
"button-press-event",
"button-release-event",
"pointer-motion-mask", # Listen for mouse movement
"enter-notify-mask",
],
all_visible=True,
)
self.event_box.connect("button-press-event", self.on_overlay_click)
self.event_box.connect("motion-notify-event", self.on_mouse_motion)
self.event_box.connect("enter-notify-event", self.on_mouse_motion)
self.children = [self.event_box]
self._hyprland = HyprlandWithMonitors()
self._cursor_pointer = Gdk.Cursor.new_from_name(
Gdk.Display.get_default(), "pointer"
)
self._cursor_default = None # Resets to default arrow
# Make the overlay transparent
self.set_app_paintable(True)
self.connect("draw", self.on_draw)
self.connect("size-allocate", lambda *_: self.update_input_region())
self.connect("map", lambda *_: self.update_input_region())
self.connect("notify::visible", lambda *_: self.update_input_region())
# Add escape key binding to child window
if hasattr(self.child_window, "add_keybinding"):
self.child_window.add_keybinding("Escape", self.hide_child_window)
def on_draw(self, _widget, cr):
"""Make overlay transparent"""
cr.set_source_rgba(0, 0, 0, 0) # Fully transparent
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
return False
def _get_child_window_bounds(self) -> tuple[int, int, int, int]:
"""Calculates absolute screen bounds for the child window using Layer Shell properties"""
try:
# Get monitor geometry
monitor = self._hyprland.display.get_monitor_at_window(
self.child_window.get_window()
)
if not monitor:
monitor = self._hyprland.display.get_default_monitor()
geom = monitor.get_geometry()
mx, my, mw, mh = geom.x, geom.y, geom.width, geom.height
# Get child window properties
alloc = self.child_window.get_allocation()
ww, wh = alloc.width, alloc.height
# fabric's WaylandWindow usually has anchor and margin accessible
anchor = self.child_window.anchor
margin = self.child_window.margin # (top, right, bottom, left)
# Calculation logic based on Layer Shell anchors
# X coordinate
if GtkLayerShell.Edge.LEFT in anchor and GtkLayerShell.Edge.RIGHT in anchor:
x = mx + margin[3]
elif GtkLayerShell.Edge.LEFT in anchor:
x = mx + margin[3]
elif GtkLayerShell.Edge.RIGHT in anchor:
x = mx + mw - ww - margin[1]
else: # Centered horizontally
x = mx + (mw - ww) // 2 + margin[3] - margin[1]
# Y coordinate
if GtkLayerShell.Edge.TOP in anchor and GtkLayerShell.Edge.BOTTOM in anchor:
y = my + margin[0]
elif GtkLayerShell.Edge.TOP in anchor:
y = my + margin[0]
elif GtkLayerShell.Edge.BOTTOM in anchor:
y = my + mh - wh - margin[2]
else: # Centered vertically
y = my + (mh - wh) // 2 + margin[0] - margin[2]
return x, y, ww, wh
except Exception as e:
print(f"Error calculating child window bounds: {e}")
# Fallback to allocation if possible, though likely (0,0)
alloc = self.child_window.get_allocation()
return 0, 0, alloc.width, alloc.height
def _get_widget_absolute_bounds(self, widget: Widget) -> tuple[int, int, int, int]:
"""Calculates absolute screen bounds for a widget in another window"""
try:
toplevel = widget.get_toplevel()
if not toplevel:
return 0, 0, 0, 0
# Get monitor geometry
monitor = self._hyprland.display.get_monitor_at_window(
toplevel.get_window()
)
if not monitor:
monitor = self._hyprland.display.get_default_monitor()
geom = monitor.get_geometry()
mx, my = geom.x, geom.y
# Get relative position within toplevel
alloc = widget.get_allocation()
x, y = widget.translate_coordinates(toplevel, 0, 0) or (0, 0)
# If the toplevel is a Layer Shell window, its (0,0) is monitor relative
# but we need to account for its own margin/anchor if it's not full screen.
# However, for the Panel (which is usually top-anchored), (0,0) is (monitor_x, monitor_y).
return mx + x, my + y, alloc.width, alloc.height
except Exception as e:
print(f"Error calculating widget absolute bounds: {e}")
return 0, 0, 0, 0
def update_input_region(self):
"""Punches holes in the input region for the trigger button and child window"""
window = self.get_window()
if not window:
return
try:
# Full screen region
alloc = self.get_allocation()
region = cairo.Region(cairo.RectangleInt(0, 0, alloc.width, alloc.height))
# Punch hole for trigger button (relative to this overlay window)
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
# get_widget_absolute_bounds returns screen-absolute coords.
# Since overlay is full screen on its monitor, we just need to subtract its monitor's X/Y
px, py, pw, ph = self._get_widget_absolute_bounds(pointing_widget)
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
mx, my = monitor.get_geometry().x, monitor.get_geometry().y
region.subtract(cairo.RectangleInt(px - mx, py - my, pw, ph))
# Punch hole for child window
cx, cy, cw, ch = self._get_child_window_bounds()
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
mx, my = monitor.get_geometry().x, monitor.get_geometry().y
region.subtract(cairo.RectangleInt(cx - mx, cy - my, cw, ch))
window.input_shape_combine_region(region, 0, 0)
except Exception as e:
print(f"Error updating input region: {e}")
def on_mouse_motion(self, _widget, event):
"""Update cursor feedback based on position"""
# (This is mostly redundant now with input regions but kept for extra safety)
if not self.child_window.is_visible():
return False
return False
def on_overlay_click(self, _widget, event):
"""Handle overlay clicks - check if click is outside child window"""
if not self.child_window.is_visible():
return False
# Support double and triple clicks too
if event.type not in [
Gdk.EventType.BUTTON_PRESS,
Gdk.EventType._2BUTTON_PRESS,
Gdk.EventType._3BUTTON_PRESS,
]:
return False
# Use window-local coordinates for robust detection
click_x = event.x
click_y = event.y
# Get window position relative to monitor for absolute -> local conversion
window = self.get_window()
mx, my = 0, 0
if window:
monitor = self._hyprland.display.get_monitor_at_window(window)
if monitor:
geom = monitor.get_geometry()
mx, my = geom.x, geom.y
# Check if click is on the "trigger" (pointing) widget
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
px_abs, py_abs, pw, ph = self._get_widget_absolute_bounds(pointing_widget)
px, py = px_abs - mx, py_abs - my
if px <= click_x <= px + pw and py <= click_y <= py + ph:
# Clicked the button that opened us - hide and consume
self.hide_child_window()
return True
# Get child window bounds (monitor-relative)
cx_abs, cy_abs, cw, ch = self._get_child_window_bounds()
cx, cy = cx_abs - mx, cy_abs - my
# Check if click is inside child window bounds
inside_child = cx <= click_x <= cx + cw and cy <= click_y <= cy + ch
if not inside_child:
# Click is outside child window - hide it immediately
# No delay to avoid race conditions with toggle buttons
self.hide_child_window()
return True # Consume the event
# Click is inside child window - don't consume event
return False
def show_child_window(self, widget: Widget = None, event: Any = None) -> None:
self.set_child_window_visible(True)
def hide_child_window(self, widget: Widget = None, event: Any = None) -> None:
self.set_child_window_visible(False)
def set_child_window_visible(self, visible: bool) -> None:
if visible:
self.child_window.show()
self.show()
else:
self.child_window.hide()
self.hide()
# Update styling on the trigger button
pointing_widget = getattr(self.child_window, "_pointing_widget", None)
if pointing_widget:
if visible:
pointing_widget.add_style_class("active")
else:
pointing_widget.remove_style_class("active")
if hasattr(self.child_window, "_set_mousecapture"):
self.child_window._set_mousecapture(visible)
def toggle_mousecapture(self, *_) -> None:
if self.is_visible():
self.set_child_window_visible(False)
else:
self.set_child_window_visible(True)
class DropDownMouseCapture(MouseCapture):
"""A specialized MouseCapture for dropdown menus with service integration"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
modus_service.connect("dropdowns-hide-changed", self.dropdowns_hide_changed)
def hide_child_window(self, widget: Widget = None, event: Any = None) -> None:
"""Hide child window and update dropdown service state"""
# Update service state before hiding to prevent conflicts
if hasattr(self.child_window, "id"):
if str(modus_service.current_dropdown) == str(self.child_window.id):
modus_service.current_dropdown = None
super().hide_child_window(widget, event)
def dropdowns_hide_changed(self, widget: Widget = None, event: Any = None) -> None:
"""Handle dropdown service hide changes"""
if hasattr(self.child_window, "id"):
if modus_service.current_dropdown == self.child_window.id:
return
return self.hide_child_window(widget, event)
+192
View File
@@ -0,0 +1,192 @@
import contextlib
from fabric.utils import Gdk, Gtk
from fabric.widgets.wayland import WaylandWindow
from gi.repository import GtkLayerShell
from utils.monitors import HyprlandWithMonitors
class PopupWindow(WaylandWindow):
"""A simple popover window that can point to a widget."""
def __init__(
self,
parent: WaylandWindow,
pointing_to: Gtk.Widget | None = None,
margin: tuple[int, ...] | str = "0px 0px 0px 0px",
enable_boundary_checking: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.exclusivity = "none"
self._is_centered = False
self._parent = parent
self._pointing_widget = pointing_to
self._hyprland = HyprlandWithMonitors()
self._base_margin = self.extract_margin(margin)
self.margin = self._base_margin.values()
self._enable_boundary_checking = enable_boundary_checking
self.connect("notify::visible", self.do_update_handlers)
def get_coords_for_widget(self, widget: Gtk.Widget) -> tuple[int, int]:
if not ((toplevel := widget.get_toplevel()) and toplevel.is_toplevel()): # type: ignore
return 0, 0
allocation = widget.get_allocation()
x, y = widget.translate_coordinates(toplevel, allocation.x, allocation.y) or (
0,
0,
)
return round(x / 2), round(y / 2)
def set_pointing_to(self, widget: Gtk.Widget | None):
if self._pointing_widget:
with contextlib.suppress(Exception):
self._pointing_widget.disconnect_by_func(self.do_handle_size_allocate)
self._pointing_widget = widget
return self.do_update_handlers()
def do_update_handlers(self, *_):
if not self._pointing_widget:
return
if not self.get_visible():
try:
self._pointing_widget.disconnect_by_func(self.do_handle_size_allocate)
self.disconnect_by_func(self.do_handle_size_allocate)
except Exception:
pass
return
self._pointing_widget.connect("size-allocate", self.do_handle_size_allocate)
self.connect("size-allocate", self.do_handle_size_allocate)
return self.do_handle_size_allocate()
def do_handle_size_allocate(self, *_):
return self.do_reposition(self.do_calculate_edges())
def do_calculate_edges(self):
move_axe = "x"
parent_anchor = self._parent.anchor
if len(parent_anchor) != 3:
self.anchor = "left bottom"
self._is_centered = True
return move_axe
if (
GtkLayerShell.Edge.LEFT in parent_anchor
and GtkLayerShell.Edge.RIGHT in parent_anchor
):
# horizontal -> move on x-axies
move_axe = "x"
if GtkLayerShell.Edge.TOP in parent_anchor:
self.anchor = "left top"
else:
self.anchor = "left bottom"
elif (
GtkLayerShell.Edge.TOP in parent_anchor
and GtkLayerShell.Edge.BOTTOM in parent_anchor
):
# vertical -> move on y-axies
move_axe = "y"
if GtkLayerShell.Edge.RIGHT in parent_anchor:
self.anchor = "top right"
else:
self.anchor = "top left"
self._is_centered = False
return move_axe
def do_reposition(self, move_axe: str):
parent_margin = self._parent.margin
parent_x_margin, parent_y_margin = parent_margin[0], parent_margin[3]
height = self.get_allocated_height()
width = self.get_allocated_width()
# Get monitor geometry for boundary checking
current_monitor_id = self._hyprland.get_current_gdk_monitor_id()
if current_monitor_id is not None:
monitor = self._hyprland.display.get_monitor(current_monitor_id)
monitor_geometry = monitor.get_geometry()
monitor_x, monitor_y = monitor_geometry.x, monitor_geometry.y
monitor_width, monitor_height = (
monitor_geometry.width,
monitor_geometry.height,
)
else:
# Fallback to default screen
screen = Gdk.Screen.get_default()
monitor_x, monitor_y = 0, 0
monitor_width, monitor_height = screen.get_width(), screen.get_height()
if self._pointing_widget:
coords = self.get_coords_for_widget(self._pointing_widget)
coords_centered = (
round(coords[0] + self._pointing_widget.get_allocated_width() / 2),
round(coords[1] + self._pointing_widget.get_allocated_height() / 2),
)
else:
coords_centered = (
round(self._parent.get_allocated_width() / 2),
round(self._parent.get_allocated_height() / 2),
)
if self._is_centered:
# Calculate centered position with boundary checking
centered_x = (
(monitor_width / 2 - self._parent.get_allocated_width() / 2) - width / 2
) + coords_centered[0]
# Apply boundary checking only if enabled
if self._enable_boundary_checking:
if centered_x < monitor_x:
centered_x = monitor_x
elif centered_x + width > monitor_x + monitor_width:
centered_x = monitor_x + monitor_width - width
self.margin = tuple(
a + b
for a, b in zip(
(0, 0, 0, centered_x),
self._base_margin.values(),
)
)
return
# Calculate position with boundary checking
if move_axe == "x":
# Horizontal positioning
calculated_x = round((parent_x_margin + coords_centered[0]) - (width / 2))
# Apply boundary checking only if enabled
if self._enable_boundary_checking:
if calculated_x < monitor_x:
calculated_x = monitor_x
elif calculated_x + width > monitor_x + monitor_width:
calculated_x = monitor_x + monitor_width - width
margin_values = (0, 0, 0, calculated_x)
else:
# Vertical positioning
calculated_y = round((parent_y_margin + coords_centered[1]) - (height / 2))
# Apply boundary checking only if enabled
if self._enable_boundary_checking:
if calculated_y < monitor_y:
calculated_y = monitor_y
elif calculated_y + height > monitor_y + monitor_height:
calculated_y = monitor_y + monitor_height - height
margin_values = (calculated_y, 0, 0, 0)
self.margin = tuple(
a + b
for a, b in zip(
margin_values,
self._base_margin.values(),
)
)