feat(modules): restructure battery UI and add dynamic layout rebuilding
New Features - Introduce `get_battery_icon_file` to generate battery icons based on percentage and charging state. - Add fallback Bluetooth control using `rfkill` when Fabric Bluetooth fails. Enhancements - Refactor layout management with `_rebuild_layout_from_config` and dynamic indicator instantiation. - Replace large `EnergyModeButton` class with lightweight utility functions. - Simplify notification icon updates using `dynamic_file`. Bug Fixes - No explicit bug fixes; changes focus on feature additions and refactoring.
This commit is contained in:
18 files changed
+2373
-2233
No files matched your search
@@ -1,21 +1,17 @@
|
||||
"""
|
||||
Enhanced System Tray Icon Handling
|
||||
Simplified System Tray Icon Patch
|
||||
|
||||
This module provides enhanced icon loading capabilities for system tray items,
|
||||
including fallback mechanisms for file paths and common icon locations.
|
||||
Focuses on loading icons from absolute file paths and fixing tooltip naming.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GdkPixbuf, Gtk
|
||||
from pathlib import Path
|
||||
|
||||
from fabric.system_tray.widgets import SystemTrayItem
|
||||
|
||||
# FIX: the tooltip should show application names instead of unknown
|
||||
from fabric.utils import GdkPixbuf, GLib
|
||||
|
||||
|
||||
def patched_do_update_properties(self, *_):
|
||||
# Try default GTK theme first
|
||||
# Determine the icon name to use
|
||||
icon_name = self._item.icon_name
|
||||
attention_icon_name = self._item.attention_icon_name
|
||||
|
||||
@@ -24,142 +20,39 @@ def patched_do_update_properties(self, *_):
|
||||
else:
|
||||
preferred_icon_name = icon_name
|
||||
|
||||
# Try to load from default GTK theme
|
||||
if preferred_icon_name:
|
||||
try:
|
||||
default_theme = Gtk.IconTheme.get_default()
|
||||
if default_theme.has_icon(preferred_icon_name):
|
||||
pixbuf = default_theme.load_icon(
|
||||
preferred_icon_name, self._icon_size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
# 1. Try the standard fabric/GTK implementation first
|
||||
pixbuf = self._item.get_preferred_icon_pixbuf(self._icon_size)
|
||||
|
||||
# 2. If standard lookup fails, try your specific absolute path logic
|
||||
if pixbuf is None and preferred_icon_name:
|
||||
path = Path(preferred_icon_name)
|
||||
if path.is_absolute() and path.exists():
|
||||
try:
|
||||
target_size = self._icon_size if self._icon_size else 24
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
str(path),
|
||||
target_size,
|
||||
target_size,
|
||||
True, # Preserve aspect ratio
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
# Set tooltip
|
||||
tooltip = self._item.tooltip
|
||||
self.set_tooltip_markup(
|
||||
tooltip.description or tooltip.title or self._item.title.title()
|
||||
if self._item.title
|
||||
else "Unknown"
|
||||
)
|
||||
return
|
||||
except:
|
||||
pass
|
||||
except GLib.GError:
|
||||
pixbuf = None
|
||||
|
||||
# Enhanced fallback handling for file paths
|
||||
if preferred_icon_name and self._try_load_icon_from_path(preferred_icon_name):
|
||||
return
|
||||
# Apply the resulting pixbuf or the fallback "missing" icon
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
else:
|
||||
self._image.set_from_icon_name("image-missing", self._icon_size)
|
||||
|
||||
# Fallback to original implementation
|
||||
original_do_update_properties(self, *_)
|
||||
|
||||
|
||||
def _try_load_icon_from_path(self, icon_path):
|
||||
try:
|
||||
# Check if it's a file path and handle it directly
|
||||
if os.path.isabs(icon_path) or "/" in icon_path:
|
||||
# Try to load as SVG from the original path if it exists
|
||||
if os.path.exists(icon_path):
|
||||
if icon_path.lower().endswith(".svg"):
|
||||
# Load SVG directly
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
icon_path, self._icon_size, self._icon_size
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
self._set_tooltip()
|
||||
return True
|
||||
else:
|
||||
# Load other image formats
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
icon_path, self._icon_size, self._icon_size
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
self._set_tooltip()
|
||||
return True
|
||||
|
||||
# If it's a file path, try to extract just the filename for theme lookup
|
||||
filename = os.path.basename(icon_path)
|
||||
if filename:
|
||||
# Remove extension for theme lookup
|
||||
name_without_ext = os.path.splitext(filename)[0]
|
||||
default_theme = Gtk.IconTheme.get_default()
|
||||
|
||||
# Try filename without extension
|
||||
if default_theme.has_icon(name_without_ext):
|
||||
pixbuf = default_theme.load_icon(
|
||||
name_without_ext,
|
||||
self._icon_size,
|
||||
Gtk.IconLookupFlags.FORCE_SIZE,
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
self._set_tooltip()
|
||||
return True
|
||||
|
||||
# Try full filename
|
||||
if default_theme.has_icon(filename):
|
||||
pixbuf = default_theme.load_icon(
|
||||
filename, self._icon_size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
self._set_tooltip()
|
||||
return True
|
||||
|
||||
# If it looks like a file path but doesn't exist, try common icon locations
|
||||
if os.path.isabs(icon_path):
|
||||
common_icon_dirs = [
|
||||
"/usr/share/icons",
|
||||
"/usr/share/pixmaps",
|
||||
"/usr/local/share/icons",
|
||||
"/usr/local/share/pixmaps",
|
||||
os.path.expanduser("~/.local/share/icons"),
|
||||
os.path.expanduser("~/.icons"),
|
||||
]
|
||||
|
||||
filename = os.path.basename(icon_path)
|
||||
for icon_dir in common_icon_dirs:
|
||||
potential_path = os.path.join(icon_dir, filename)
|
||||
if os.path.exists(potential_path):
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
potential_path, self._icon_size, self._icon_size
|
||||
)
|
||||
if pixbuf:
|
||||
self._image.set_from_pixbuf(pixbuf)
|
||||
self._set_tooltip()
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _set_tooltip(self):
|
||||
tooltip = self._item.tooltip
|
||||
self.set_tooltip_markup(
|
||||
tooltip.description or tooltip.title or self._item.title.title()
|
||||
if self._item.title
|
||||
else "Unknown"
|
||||
tooltip.description
|
||||
or tooltip.title
|
||||
or (self._item.title.title() if self._item.title else None)
|
||||
or "Unknown"
|
||||
)
|
||||
|
||||
|
||||
def apply_enhanced_system_tray():
|
||||
# Store original method
|
||||
global original_do_update_properties
|
||||
original_do_update_properties = SystemTrayItem.do_update_properties
|
||||
|
||||
# Attach helper methods to SystemTrayItem class
|
||||
SystemTrayItem._try_load_icon_from_path = _try_load_icon_from_path
|
||||
SystemTrayItem._set_tooltip = _set_tooltip
|
||||
|
||||
# Replace the do_update_properties method
|
||||
# Replace the method on the class so all tray items use this logic
|
||||
SystemTrayItem.do_update_properties = patched_do_update_properties
|
||||
|
||||
|
||||
# Store reference to original method
|
||||
original_do_update_properties = None
|
||||
@@ -0,0 +1,338 @@
|
||||
from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow
|
||||
from fabric.utils import FormattedString, exec_shell_command_async
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from utils.app_name_resolver import format_window
|
||||
from utils.roam import modus_service
|
||||
from utils.utils import setup_cursor_hover
|
||||
from widgets.about import About, AboutApp
|
||||
from widgets.dropdown import ModusDropdown, dropdown_divider, dropdowns
|
||||
from widgets.mousecapture import DropDownMouseCapture
|
||||
|
||||
|
||||
def has_active_window():
|
||||
return (
|
||||
modus_service.current_active_app_name
|
||||
and modus_service.current_active_app_name != "Finder"
|
||||
)
|
||||
|
||||
|
||||
def create_menu_button(label, on_clicked=None):
|
||||
button_kwargs = {
|
||||
"label": label,
|
||||
"name": "global-menu",
|
||||
}
|
||||
|
||||
if on_clicked is not None:
|
||||
button_kwargs["on_clicked"] = on_clicked
|
||||
|
||||
button = Button(**button_kwargs)
|
||||
setup_cursor_hover(button, "pointer")
|
||||
return button
|
||||
|
||||
|
||||
def create_dropdown_with_capture(
|
||||
dropdown_id, parent, dropdown_children, layer="bottom"
|
||||
):
|
||||
dropdown = ModusDropdown(
|
||||
dropdown_id=dropdown_id,
|
||||
parent=parent,
|
||||
dropdown_children=dropdown_children,
|
||||
)
|
||||
mouse_capture = DropDownMouseCapture(layer=layer, child_window=dropdown)
|
||||
return mouse_capture
|
||||
|
||||
|
||||
def manage_button_style_classes(buttons, active_button=None, style_class="active"):
|
||||
for button in buttons:
|
||||
if button: # Check if button exists (some might be None)
|
||||
button.remove_style_class(style_class)
|
||||
|
||||
if active_button:
|
||||
active_button.add_style_class(style_class)
|
||||
|
||||
|
||||
def show_about_app(_=None):
|
||||
if not has_active_window():
|
||||
return
|
||||
|
||||
app_name = modus_service.current_active_app_name
|
||||
about_window = AboutApp(app_name=app_name, wmclass="")
|
||||
about_window.toggle(None)
|
||||
|
||||
|
||||
def on_click_subthread(button, on_clicked, on_click):
|
||||
if on_clicked:
|
||||
on_clicked(button)
|
||||
else:
|
||||
exec_shell_command_async(on_click, lambda *_: None)
|
||||
|
||||
for dropdown in dropdowns:
|
||||
if dropdown.is_visible():
|
||||
dropdown.hide_via_mousecapture()
|
||||
break
|
||||
|
||||
|
||||
def dropdown_option(
|
||||
label: str = "",
|
||||
keybind: str = "",
|
||||
on_click='echo "ModusPanelDropdown Action"',
|
||||
on_clicked=None,
|
||||
):
|
||||
btn = Button(
|
||||
child=CenterBox(
|
||||
start_children=[
|
||||
Label(label=label, h_align="start", name="dropdown-option-label"),
|
||||
],
|
||||
end_children=[
|
||||
Label(label=keybind, h_align="end", name="dropdown-option-keybind")
|
||||
],
|
||||
orientation="horizontal",
|
||||
h_align="fill",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
),
|
||||
name="dropdown-option",
|
||||
h_align="fill",
|
||||
on_clicked=lambda button: on_click_subthread(button, on_clicked, on_click),
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
setup_cursor_hover(btn, "pointer")
|
||||
return btn
|
||||
|
||||
|
||||
class SystemDropdown(ModusDropdown):
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(
|
||||
dropdown_id="os-menu",
|
||||
parent=parent,
|
||||
dropdown_children=[
|
||||
dropdown_option(
|
||||
"About this PC", on_clicked=lambda _: About().toggle(_)
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"System Settings...",
|
||||
# TODO: Open Modus own setting
|
||||
# on_click="xdg-open settings",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Force Quit", "", "hyprctl kill"),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Sleep", "", "systemctl suspend"),
|
||||
dropdown_option("Restart", "", "systemctl reboot"),
|
||||
dropdown_option("Shut Down", "", "shutdown now"),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Lock Screen", " L", "hyprlock"),
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class GlobalMenuDropdowns:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
self.system_dropdown = SystemDropdown(parent=parent)
|
||||
self.menu_button_dropdown = DropDownMouseCapture(
|
||||
layer="bottom", child_window=self.system_dropdown
|
||||
)
|
||||
self.menu_button = Button(
|
||||
label="Modus",
|
||||
name="global-menu",
|
||||
on_clicked=lambda _: self.menu_button_dropdown.toggle_mousecapture(),
|
||||
)
|
||||
self.menu_button_dropdown.child_window.set_pointing_to(self.menu_button)
|
||||
|
||||
self.global_title_menu_about = dropdown_option(
|
||||
f"About {modus_service.current_active_app_name}",
|
||||
on_clicked=show_about_app,
|
||||
)
|
||||
self.global_menu_title = create_dropdown_with_capture(
|
||||
"global-menu-title",
|
||||
parent,
|
||||
[self.global_title_menu_about],
|
||||
)
|
||||
|
||||
self.global_menu_view = create_dropdown_with_capture(
|
||||
"global-menu-view",
|
||||
parent,
|
||||
[
|
||||
dropdown_option(
|
||||
"Enter Full Screen",
|
||||
on_click="hyprctl dispatch fullscreen",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.global_menu_window = create_dropdown_with_capture(
|
||||
"global-menu-window",
|
||||
parent,
|
||||
[
|
||||
dropdown_option(
|
||||
"Zoom In",
|
||||
" +",
|
||||
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '.float * 1.1')",
|
||||
),
|
||||
dropdown_option(
|
||||
"Zoom Out",
|
||||
" -",
|
||||
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '(.float * 0.9) | if . < 1 then 1 else . end')",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"Move Window to Left",
|
||||
on_click="hyprctl dispatch movewindow l",
|
||||
),
|
||||
dropdown_option(
|
||||
"Move Window to Right",
|
||||
on_click="hyprctl dispatch movewindow r",
|
||||
),
|
||||
dropdown_option(
|
||||
"Cycle Through Windows",
|
||||
on_click="hyprctl dispatch cyclenext",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Float", on_click="hyprctl dispatch togglefloating"),
|
||||
dropdown_option("Quit", on_click="hyprctl dispatch killactive"),
|
||||
dropdown_option("Pseudo", on_click="hyprctl dispatch pseudo"),
|
||||
dropdown_option(
|
||||
"Toggle Split", on_click="hyprctl dispatch togglesplit"
|
||||
),
|
||||
dropdown_option("Center", on_click="hyprctl dispatch centerwindow"),
|
||||
dropdown_option("Group", on_click="hyprctl dispatch togglegroup"),
|
||||
dropdown_option(
|
||||
"Pin",
|
||||
on_clicked=lambda _: exec_shell_command_async(
|
||||
"bash ~/.config/scripts/winpin.sh", lambda *_: None
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self.global_menu_help = create_dropdown_with_capture(
|
||||
"global-menu-help",
|
||||
parent,
|
||||
[
|
||||
dropdown_option(
|
||||
"Modus",
|
||||
on_click="xdg-open https://github.com/S4NKALP/Modus/issues",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"Hyprland Wiki", on_click="xdg-open https://wiki.hyprland.org/"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
modus_service.connect(
|
||||
"current-active-app-name-changed", self._on_active_app_changed
|
||||
)
|
||||
|
||||
self.global_menu_button_title = Button(
|
||||
child=ActiveWindow(
|
||||
formatter=FormattedString(
|
||||
"{ format_window(win_title, win_class) }",
|
||||
format_window=format_window,
|
||||
)
|
||||
),
|
||||
name="global-menu",
|
||||
on_clicked=self._on_title_button_clicked,
|
||||
)
|
||||
|
||||
self.global_menu_title.child_window.set_pointing_to(
|
||||
self.global_menu_button_title
|
||||
)
|
||||
# File, Edit and Go buttons are placeholders - no dropdowns implemented yet
|
||||
self.global_menu_button_file = create_menu_button("File")
|
||||
self.global_menu_button_edit = create_menu_button("Edit")
|
||||
self.global_menu_button_go = create_menu_button("Go")
|
||||
|
||||
self.global_menu_button_view = create_menu_button(
|
||||
"View",
|
||||
lambda _: self.global_menu_view.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_view.child_window.set_pointing_to(self.global_menu_button_view)
|
||||
self.global_menu_button_window = create_menu_button(
|
||||
"Window",
|
||||
lambda _: self.global_menu_window.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_window.child_window.set_pointing_to(
|
||||
self.global_menu_button_window
|
||||
)
|
||||
self.global_menu_button_help = create_menu_button(
|
||||
"Help",
|
||||
lambda _: self.global_menu_help.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_help.child_window.set_pointing_to(self.global_menu_button_help)
|
||||
|
||||
self.all_menu_buttons = [
|
||||
self.menu_button,
|
||||
self.global_menu_button_title,
|
||||
self.global_menu_button_file,
|
||||
self.global_menu_button_edit,
|
||||
self.global_menu_button_view,
|
||||
self.global_menu_button_go,
|
||||
self.global_menu_button_window,
|
||||
self.global_menu_button_help,
|
||||
]
|
||||
|
||||
self.dropdown_button_map = {
|
||||
"os-menu": self.menu_button,
|
||||
"global-menu-title": self.global_menu_button_title,
|
||||
"global-menu-file": self.global_menu_button_file,
|
||||
"global-menu-edit": self.global_menu_button_edit,
|
||||
"global-menu-view": self.global_menu_button_view,
|
||||
"global-menu-go": self.global_menu_button_go,
|
||||
"global-menu-window": self.global_menu_button_window,
|
||||
"global-menu-help": self.global_menu_button_help,
|
||||
}
|
||||
|
||||
modus_service.connect("current-dropdown-changed", self.changed_dropdown)
|
||||
modus_service.connect("dropdowns-hide-changed", self.hide_dropdowns)
|
||||
|
||||
def _on_title_button_clicked(self, _):
|
||||
if has_active_window():
|
||||
self.global_menu_title.toggle_mousecapture()
|
||||
|
||||
def _on_active_app_changed(self, _, value):
|
||||
self.global_title_menu_about.set_property("label", f"About {value}")
|
||||
|
||||
def hide_dropdowns(self, *_):
|
||||
manage_button_style_classes(self.all_menu_buttons)
|
||||
|
||||
def changed_dropdown(self, _, dropdown_id):
|
||||
active_button = self.dropdown_button_map.get(dropdown_id)
|
||||
manage_button_style_classes(self.all_menu_buttons, active_button)
|
||||
|
||||
|
||||
class GlobalMenu(Box):
|
||||
def __init__(self, parent_window=None, **kwargs):
|
||||
if parent_window is None:
|
||||
parent_window = kwargs.pop("parent_window", None)
|
||||
|
||||
super().__init__(
|
||||
name="globalmenu", orientation="horizontal", spacing=0, **kwargs
|
||||
)
|
||||
|
||||
self.dropdown_system = GlobalMenuDropdowns(parent=parent_window)
|
||||
|
||||
self.children = [
|
||||
self.dropdown_system.global_menu_button_title,
|
||||
self.dropdown_system.global_menu_button_file,
|
||||
self.dropdown_system.global_menu_button_edit,
|
||||
self.dropdown_system.global_menu_button_view,
|
||||
self.dropdown_system.global_menu_button_go,
|
||||
self.dropdown_system.global_menu_button_window,
|
||||
self.dropdown_system.global_menu_button_help,
|
||||
]
|
||||
|
||||
def show_system_dropdown(self, imac_button):
|
||||
self.dropdown_system.menu_button_dropdown.child_window.set_pointing_to(
|
||||
imac_button
|
||||
)
|
||||
mouse_capture = self.dropdown_system.menu_button_dropdown
|
||||
mouse_capture.set_child_window_visible(not mouse_capture.is_visible())
|
||||
@@ -1,19 +1,59 @@
|
||||
from fabric.bluetooth import BluetoothClient
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
from widgets.battery_widget import BatteryControl
|
||||
|
||||
from modules.controlcenter.battery import BatteryControl
|
||||
from modules.controlcenter.bluetooth import BluetoothConnections
|
||||
from modules.controlcenter.wifi import WifiConnections
|
||||
from services.battery import Battery
|
||||
from services.battery import BatteryService, DeviceState
|
||||
from services.network import NetworkClient
|
||||
from utils.functions import format_duration, get_wifi_icon_for_strength
|
||||
from utils.roam import modus_service
|
||||
from utils.functions import get_wifi_icon_for_strength, get_wifi_connecting_icon
|
||||
from widgets.mousecapture import DropDownMouseCapture
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
def create_control_window(name_prefix):
|
||||
return Window(
|
||||
layer="overlay",
|
||||
title="modus",
|
||||
anchor="top right",
|
||||
margin="2px 10px 0px 0px",
|
||||
exclusivity="auto",
|
||||
keyboard_mode="on-demand",
|
||||
name=f"{name_prefix}-window",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
|
||||
def create_mouse_capture(window):
|
||||
return DropDownMouseCapture(layer="top", child_window=window)
|
||||
|
||||
|
||||
def setup_control_center(
|
||||
show_window, name_prefix, widget_class, parent, **widget_kwargs
|
||||
):
|
||||
if not show_window:
|
||||
return None, None, None
|
||||
|
||||
window = create_control_window(name_prefix)
|
||||
widget = widget_class(parent, **widget_kwargs)
|
||||
window.children = [widget]
|
||||
mouse_capture = create_mouse_capture(window)
|
||||
|
||||
return window, widget, mouse_capture
|
||||
|
||||
|
||||
def handle_indicator_click(mouse_capture):
|
||||
if mouse_capture:
|
||||
mouse_capture.toggle_mousecapture()
|
||||
|
||||
|
||||
def hide_control_center(mouse_capture):
|
||||
if mouse_capture:
|
||||
mouse_capture.hide_child_window()
|
||||
|
||||
|
||||
class BluetoothIndicator(Box):
|
||||
@@ -22,13 +62,7 @@ class BluetoothIndicator(Box):
|
||||
self.show_window = show_window
|
||||
|
||||
self.bluetooth = BluetoothClient()
|
||||
self.bt_icon = Svg(
|
||||
name="bt-icon",
|
||||
size=22,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
),
|
||||
)
|
||||
self.bt_icon = svg_file("applets/bluetooth-clear.svg", size=22)
|
||||
|
||||
self.bt_button = Button(
|
||||
name="bt-button", child=self.bt_icon, on_clicked=self.on_bluetooth_clicked
|
||||
@@ -36,30 +70,21 @@ class BluetoothIndicator(Box):
|
||||
|
||||
self.add(self.bt_button)
|
||||
|
||||
# Create Bluetooth control center widget only if show_window is True
|
||||
if self.show_window:
|
||||
self.bluetooth_window = Window(
|
||||
layer="overlay",
|
||||
title="modus",
|
||||
anchor="top right",
|
||||
margin="2px 10px 0px 0px",
|
||||
exclusivity="auto",
|
||||
keyboard_mode="on-demand",
|
||||
name="bluetooth-control-window",
|
||||
visible=False,
|
||||
)
|
||||
# Set pointer cursor on hover
|
||||
setup_cursor_hover(self.bt_button, "pointer")
|
||||
|
||||
self.bluetooth_widget = BluetoothConnections(self, show_back_button=False)
|
||||
self.bluetooth_window.children = [self.bluetooth_widget]
|
||||
|
||||
# Create mouse capture for Bluetooth widget
|
||||
self.bluetooth_mousecapture = DropDownMouseCapture(
|
||||
layer="top", child_window=self.bluetooth_window
|
||||
)
|
||||
else:
|
||||
self.bluetooth_window = None
|
||||
self.bluetooth_widget = None
|
||||
self.bluetooth_mousecapture = None
|
||||
# Setup control center using shared function
|
||||
(
|
||||
self.bluetooth_window,
|
||||
self.bluetooth_widget,
|
||||
self.bluetooth_mousecapture,
|
||||
) = setup_control_center(
|
||||
self.show_window,
|
||||
"bluetooth",
|
||||
BluetoothConnections,
|
||||
self,
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
modus_service.connect("bluetooth-changed", self.on_bluetooth_changed)
|
||||
self.bluetooth.connect("changed", self.on_bluetooth_direct_changed)
|
||||
@@ -71,26 +96,14 @@ class BluetoothIndicator(Box):
|
||||
|
||||
def update_state(self):
|
||||
if not self.bluetooth.enabled:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-off-clear.svg"
|
||||
)
|
||||
)
|
||||
self.bt_icon.dynamic_file("applets/bluetooth-off-clear.svg")
|
||||
tooltip = "Bluetooth disabled"
|
||||
else:
|
||||
connected_devices = self.bluetooth.connected_devices
|
||||
if connected_devices:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
)
|
||||
)
|
||||
self.bt_icon.dynamic_file("applets/bluetooth-clear.svg")
|
||||
if len(connected_devices) >= 1:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-paired.svg"
|
||||
)
|
||||
)
|
||||
self.bt_icon.dynamic_file("applets/bluetooth-paired.svg")
|
||||
device = connected_devices[0]
|
||||
tooltip = f"Connected to {device.alias}"
|
||||
if device.battery_percentage > 0:
|
||||
@@ -98,11 +111,7 @@ class BluetoothIndicator(Box):
|
||||
else:
|
||||
tooltip = f"Connected to {len(connected_devices)} devices"
|
||||
else:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
)
|
||||
)
|
||||
self.bt_icon.dynamic_file("applets/bluetooth-clear.svg")
|
||||
tooltip = "No devices connected"
|
||||
|
||||
self.bt_button.set_tooltip_text(tooltip)
|
||||
@@ -144,19 +153,13 @@ class BluetoothIndicator(Box):
|
||||
modus_service.bluetooth = bluetooth_state
|
||||
|
||||
def on_bluetooth_clicked(self, *args):
|
||||
"""Handle Bluetooth indicator click"""
|
||||
if self.show_window and self.bluetooth_mousecapture:
|
||||
self.bluetooth_mousecapture.toggle_mousecapture()
|
||||
handle_indicator_click(self.bluetooth_mousecapture)
|
||||
|
||||
def close_bluetooth(self, *args):
|
||||
"""Close Bluetooth control center"""
|
||||
if self.show_window and self.bluetooth_mousecapture:
|
||||
self.bluetooth_mousecapture.hide_child_window()
|
||||
hide_control_center(self.bluetooth_mousecapture)
|
||||
|
||||
def hide_controlcenter(self, *args):
|
||||
"""Hide Bluetooth control center"""
|
||||
if self.show_window and self.bluetooth_mousecapture:
|
||||
self.bluetooth_mousecapture.hide_child_window()
|
||||
hide_control_center(self.bluetooth_mousecapture)
|
||||
|
||||
|
||||
class NetworkIndicator(Box):
|
||||
@@ -166,13 +169,7 @@ class NetworkIndicator(Box):
|
||||
|
||||
self.network_service = NetworkClient()
|
||||
|
||||
self.network_icon = Svg(
|
||||
name="network-icon",
|
||||
size=22,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/wifi-clear.svg"
|
||||
),
|
||||
)
|
||||
self.network_icon = svg_file("applets/wifi-clear.svg", size=22)
|
||||
|
||||
self.network_button = Button(
|
||||
name="network-button",
|
||||
@@ -182,30 +179,21 @@ class NetworkIndicator(Box):
|
||||
|
||||
self.add(self.network_button)
|
||||
|
||||
# Create WiFi control center widget only if show_window is True
|
||||
if self.show_window:
|
||||
self.wifi_window = Window(
|
||||
layer="overlay",
|
||||
title="modus",
|
||||
anchor="top right",
|
||||
margin="2px 10px 0px 0px",
|
||||
exclusivity="auto",
|
||||
keyboard_mode="on-demand",
|
||||
name="wifi-control-window",
|
||||
visible=False,
|
||||
)
|
||||
# Set pointer cursor on hover
|
||||
setup_cursor_hover(self.network_button, "pointer")
|
||||
|
||||
self.wifi_widget = WifiConnections(self, show_back_button=False)
|
||||
self.wifi_window.children = [self.wifi_widget]
|
||||
|
||||
# Create mouse capture for WiFi widget
|
||||
self.wifi_mousecapture = DropDownMouseCapture(
|
||||
layer="top", child_window=self.wifi_window
|
||||
)
|
||||
else:
|
||||
self.wifi_window = None
|
||||
self.wifi_widget = None
|
||||
self.wifi_mousecapture = None
|
||||
# Setup control center using shared function
|
||||
(
|
||||
self.wifi_window,
|
||||
self.wifi_widget,
|
||||
self.wifi_mousecapture,
|
||||
) = setup_control_center(
|
||||
self.show_window,
|
||||
"wifi",
|
||||
WifiConnections,
|
||||
self,
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
modus_service.connect("wlan-changed", self.on_wlan_changed)
|
||||
self.network_service.connect("wifi-device-added", self.on_wifi_device_added)
|
||||
@@ -230,7 +218,6 @@ class NetworkIndicator(Box):
|
||||
self.update_state()
|
||||
|
||||
def on_ethernet_device_added(self, *args):
|
||||
"""Called when Ethernet device is added"""
|
||||
if self.network_service.ethernet_device:
|
||||
self.network_service.ethernet_device.connect(
|
||||
"changed", self.on_network_direct_changed
|
||||
@@ -249,7 +236,6 @@ class NetworkIndicator(Box):
|
||||
def update_modus_service_wlan_state(self):
|
||||
wlan_state = "disconnected"
|
||||
|
||||
# Check WiFi first (prioritize WiFi over Ethernet)
|
||||
if self.network_service.wifi_device:
|
||||
wifi = self.network_service.wifi_device
|
||||
if not wifi.wireless_enabled:
|
||||
@@ -315,25 +301,17 @@ class NetworkIndicator(Box):
|
||||
icon_file = "network-wired-offline.svg"
|
||||
tooltip = "Ethernet disconnected"
|
||||
|
||||
self.network_icon.set_from_file(
|
||||
get_relative_path(f"../../../config/assets/icons/applets/{icon_file}")
|
||||
)
|
||||
self.network_icon.dynamic_file(f"applets/{icon_file}")
|
||||
self.network_button.set_tooltip_text(tooltip)
|
||||
|
||||
def on_wifi_clicked(self, *args):
|
||||
"""Handle WiFi indicator click"""
|
||||
if self.show_window and self.wifi_mousecapture:
|
||||
self.wifi_mousecapture.toggle_mousecapture()
|
||||
handle_indicator_click(self.wifi_mousecapture)
|
||||
|
||||
def close_wifi(self, *args):
|
||||
"""Close WiFi control center"""
|
||||
if self.show_window and self.wifi_mousecapture:
|
||||
self.wifi_mousecapture.hide_child_window()
|
||||
hide_control_center(self.wifi_mousecapture)
|
||||
|
||||
def hide_controlcenter(self, *args):
|
||||
"""Hide WiFi control center"""
|
||||
if self.show_window and self.wifi_mousecapture:
|
||||
self.wifi_mousecapture.hide_child_window()
|
||||
hide_control_center(self.wifi_mousecapture)
|
||||
|
||||
|
||||
class BatteryIndicator(Box):
|
||||
@@ -341,15 +319,9 @@ class BatteryIndicator(Box):
|
||||
super().__init__(name="battery-indicator", orientation="h", **kwargs)
|
||||
self.show_window = show_window
|
||||
|
||||
self.battery_service = Battery()
|
||||
self.battery_service = BatteryService()
|
||||
|
||||
self.battery_icon = Svg(
|
||||
name="battery-icon",
|
||||
size=23,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/battery/battery-100.svg"
|
||||
),
|
||||
)
|
||||
self.battery_icon = svg_file("battery/battery-100.svg", size=23)
|
||||
|
||||
self.battery_button = Button(
|
||||
name="battery-button",
|
||||
@@ -362,30 +334,20 @@ class BatteryIndicator(Box):
|
||||
self.add(self.battery_label)
|
||||
self.add(self.battery_button)
|
||||
|
||||
# Create Battery control center widget only if show_window is True
|
||||
if self.show_window:
|
||||
self.battery_window = Window(
|
||||
layer="top",
|
||||
title="modus",
|
||||
anchor="top right",
|
||||
margin="2px 10px 0px 0px",
|
||||
exclusivity="auto",
|
||||
keyboard_mode="on-demand",
|
||||
name="battery-control-window",
|
||||
visible=False,
|
||||
)
|
||||
# Set pointer cursor on hover
|
||||
setup_cursor_hover(self.battery_button, "pointer")
|
||||
|
||||
self.battery_widget = BatteryControl(self, show_back_button=False)
|
||||
self.battery_window.children = [self.battery_widget]
|
||||
|
||||
# Create mouse capture for Battery widget
|
||||
self.battery_mousecapture = DropDownMouseCapture(
|
||||
layer="top", child_window=self.battery_window
|
||||
)
|
||||
else:
|
||||
self.battery_window = None
|
||||
self.battery_widget = None
|
||||
self.battery_mousecapture = None
|
||||
(
|
||||
self.battery_window,
|
||||
self.battery_widget,
|
||||
self.battery_mousecapture,
|
||||
) = setup_control_center(
|
||||
self.show_window,
|
||||
"battery",
|
||||
BatteryControl,
|
||||
self,
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
modus_service.connect("battery-changed", self.on_battery_changed)
|
||||
self.battery_service.connect("changed", self.on_battery_direct_changed)
|
||||
@@ -400,21 +362,46 @@ class BatteryIndicator(Box):
|
||||
self.update_modus_service_battery_state()
|
||||
self.update_state()
|
||||
|
||||
def _get_percentage(self):
|
||||
return int(self.battery_service.get_property("Percentage") or 0)
|
||||
|
||||
def _get_state(self):
|
||||
state_value = self.battery_service.get_property("State")
|
||||
return DeviceState.get(state_value, "UNKNOWN")
|
||||
|
||||
def _is_present(self):
|
||||
return bool(self.battery_service.get_property("IsPresent"))
|
||||
|
||||
def _get_time_to_empty(self):
|
||||
return int(self.battery_service.get_property("TimeToEmpty") or 0)
|
||||
|
||||
def _get_time_to_full(self):
|
||||
return int(self.battery_service.get_property("TimeToFull") or 0)
|
||||
|
||||
def _format_time(self, seconds: int) -> str:
|
||||
return format_duration(seconds)
|
||||
|
||||
def _get_battery_icon_file(self, percentage: int, is_charging: bool) -> str:
|
||||
clamped = max(0, min(100, percentage))
|
||||
step = (clamped // 10) * 10
|
||||
filename = f"battery-{step:03d}{'-charging' if is_charging else ''}.svg"
|
||||
return f"battery/{filename}"
|
||||
|
||||
def update_modus_service_battery_state(self):
|
||||
if not self.battery_service.is_present:
|
||||
if not self._is_present():
|
||||
battery_state = "not_present"
|
||||
else:
|
||||
percentage = self.battery_service.percentage
|
||||
state = self.battery_service.state.lower()
|
||||
percentage = self._get_percentage()
|
||||
state_str = self._get_state().lower()
|
||||
|
||||
battery_state = f"{state}:{percentage}%"
|
||||
battery_state = f"{state_str}:{percentage}%"
|
||||
|
||||
if state == "discharging":
|
||||
time_to_empty = self.battery_service.time_to_empty
|
||||
if state_str == "discharging":
|
||||
time_to_empty = self._format_time(self._get_time_to_empty())
|
||||
if time_to_empty != "N/A":
|
||||
battery_state += f":{time_to_empty}"
|
||||
elif state == "charging":
|
||||
time_to_full = self.battery_service.time_to_full
|
||||
elif state_str == "charging":
|
||||
time_to_full = self._format_time(self._get_time_to_full())
|
||||
if time_to_full != "N/A":
|
||||
battery_state += f":{time_to_full}"
|
||||
|
||||
@@ -425,11 +412,11 @@ class BatteryIndicator(Box):
|
||||
|
||||
if state == "CHARGING":
|
||||
tooltip += " (Charging)"
|
||||
time_to_full = self.battery_service.time_to_full
|
||||
time_to_full = self._format_time(self._get_time_to_full())
|
||||
if time_to_full != "N/A":
|
||||
tooltip += f" - {time_to_full} until full"
|
||||
elif state == "DISCHARGING":
|
||||
time_to_empty = self.battery_service.time_to_empty
|
||||
time_to_empty = self._format_time(self._get_time_to_empty())
|
||||
if time_to_empty != "N/A":
|
||||
tooltip += f" - {time_to_empty} remaining"
|
||||
elif state == "FULLY_CHARGED":
|
||||
@@ -438,40 +425,29 @@ class BatteryIndicator(Box):
|
||||
return tooltip
|
||||
|
||||
def update_state(self):
|
||||
if not self.battery_service.is_present:
|
||||
# Hide the entire battery component when no battery is present
|
||||
if not self._is_present():
|
||||
self.set_visible(False)
|
||||
return
|
||||
else:
|
||||
# Show the battery component when battery is present
|
||||
self.set_visible(True)
|
||||
|
||||
percentage = self.battery_service.percentage
|
||||
state = self.battery_service.state
|
||||
percentage = self._get_percentage()
|
||||
state = self._get_state()
|
||||
is_charging = state in ["CHARGING", "FULLY_CHARGED"]
|
||||
|
||||
icon_file = Battery.get_battery_icon_file(
|
||||
percentage, is_charging, base_path="../../../config/assets/icons/"
|
||||
)
|
||||
icon_file = self._get_battery_icon_file(percentage, is_charging)
|
||||
tooltip = self.get_battery_tooltip(percentage, state)
|
||||
percentage_text = f"{percentage}%"
|
||||
|
||||
# Update icon, tooltip, and percentage label
|
||||
self.battery_icon.set_from_file(get_relative_path(icon_file))
|
||||
self.battery_icon.dynamic_file(icon_file)
|
||||
self.battery_button.set_tooltip_text(tooltip)
|
||||
self.battery_label.set_label(percentage_text)
|
||||
|
||||
def on_battery_clicked(self, *args):
|
||||
"""Handle Battery indicator click"""
|
||||
if self.show_window and self.battery_mousecapture:
|
||||
self.battery_mousecapture.toggle_mousecapture()
|
||||
handle_indicator_click(self.battery_mousecapture)
|
||||
|
||||
def close_battery(self, *args):
|
||||
"""Close Battery control center"""
|
||||
if self.show_window and self.battery_mousecapture:
|
||||
self.battery_mousecapture.hide_child_window()
|
||||
hide_control_center(self.battery_mousecapture)
|
||||
|
||||
def hide_controlcenter(self, *args):
|
||||
"""Hide Battery control center"""
|
||||
if self.show_window and self.battery_mousecapture:
|
||||
self.battery_mousecapture.hide_child_window()
|
||||
hide_control_center(self.battery_mousecapture)
|
||||
@@ -1,411 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow
|
||||
from fabric.utils import FormattedString
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from modules.about import About, AboutApp
|
||||
from utils.roam import modus_service
|
||||
from widgets.dropdown import ModusDropdown, dropdown_divider
|
||||
from widgets.mousecapture import DropDownMouseCapture
|
||||
from utils.app_name_resolver import format_window
|
||||
|
||||
|
||||
def show_about_app():
|
||||
"""Show about dialog for current active application"""
|
||||
try:
|
||||
# Use modus_service's Hyprland connection to get current window info
|
||||
wmclass = ""
|
||||
title = ""
|
||||
|
||||
if (
|
||||
hasattr(modus_service, "_hyprland_connection")
|
||||
and modus_service._hyprland_connection
|
||||
):
|
||||
window_data = modus_service._hyprland_connection.send_command(
|
||||
"j/activewindow"
|
||||
).reply
|
||||
if window_data:
|
||||
window_info = json.loads(window_data.decode("utf-8"))
|
||||
wmclass = window_info.get("class", "")
|
||||
title = window_info.get("title", "")
|
||||
|
||||
# Don't show about dialog if there's no active window (Finder state)
|
||||
if not wmclass and not title:
|
||||
return
|
||||
|
||||
app_name = modus_service.current_active_app_name or "Finder"
|
||||
# Don't show about dialog for Finder
|
||||
if app_name == "Finder":
|
||||
return
|
||||
|
||||
about_window = AboutApp(app_name=app_name, wmclass=wmclass)
|
||||
about_window.toggle(None)
|
||||
except Exception:
|
||||
# Fallback: only show if we have a real app name
|
||||
app_name = modus_service.current_active_app_name or ""
|
||||
if app_name and app_name != "Finder":
|
||||
about_window = AboutApp(app_name=app_name, wmclass="")
|
||||
about_window.toggle(None)
|
||||
|
||||
|
||||
def dropdown_option(
|
||||
label: str = "",
|
||||
keybind: str = "",
|
||||
on_click='echo "ModusPanelDropdown Action"',
|
||||
on_clicked=None,
|
||||
):
|
||||
def on_click_subthread(button):
|
||||
# Execute the action first
|
||||
if on_clicked:
|
||||
on_clicked(button)
|
||||
else:
|
||||
subprocess.Popen(
|
||||
f"nohup {on_click} &",
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Hide dropdown by finding the current visible dropdown and calling its hide method
|
||||
from widgets.dropdown import dropdowns
|
||||
|
||||
for dropdown in dropdowns:
|
||||
if dropdown.is_visible() and hasattr(dropdown, "hide_via_mousecapture"):
|
||||
dropdown.hide_via_mousecapture()
|
||||
break
|
||||
|
||||
return Button(
|
||||
child=CenterBox(
|
||||
start_children=[
|
||||
Label(label=label, h_align="start", name="dropdown-option-label"),
|
||||
],
|
||||
end_children=[
|
||||
Label(label=keybind, h_align="end", name="dropdown-option-keybind")
|
||||
],
|
||||
orientation="horizontal",
|
||||
h_align="fill",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
),
|
||||
name="dropdown-option",
|
||||
h_align="fill",
|
||||
on_clicked=on_click_subthread,
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
|
||||
class SystemDropdown(ModusDropdown):
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(
|
||||
dropdown_id="os-menu",
|
||||
parent=parent,
|
||||
dropdown_children=[
|
||||
dropdown_option(
|
||||
"About this PC", on_clicked=lambda _: About().toggle(_)
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"System Settings...",
|
||||
# TODO: Open Modus own setting
|
||||
# on_click="xdg-open settings",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Force Quit", "", "hyprctl kill"),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Sleep", "", "systemctl suspend"),
|
||||
dropdown_option("Restart...", "", "systemctl reboot"),
|
||||
dropdown_option("Shut Down...", "", "shutdown now"),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option("Lock Screen", " L", "hyprlock"),
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class MenuBarDropdowns:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
# System dropdown
|
||||
self.system_dropdown = SystemDropdown(parent=parent)
|
||||
self.menu_button_dropdown = DropDownMouseCapture(
|
||||
layer="bottom", child_window=self.system_dropdown
|
||||
)
|
||||
self.menu_button = Button(
|
||||
label="Modus",
|
||||
name="menu-button",
|
||||
style_classes="button",
|
||||
on_clicked=lambda _: self.menu_button_dropdown.toggle_mousecapture(),
|
||||
)
|
||||
self.menu_button_dropdown.child_window.set_pointing_to(self.menu_button)
|
||||
|
||||
# Global menu dropdowns
|
||||
self.global_title_menu_about = dropdown_option(
|
||||
f"About {modus_service.current_active_app_name}",
|
||||
on_clicked=lambda _: show_about_app(),
|
||||
)
|
||||
self.global_menu_title = DropDownMouseCapture(
|
||||
layer="bottom",
|
||||
child_window=ModusDropdown(
|
||||
dropdown_id="global-menu-title",
|
||||
parent=parent,
|
||||
dropdown_children=[self.global_title_menu_about],
|
||||
),
|
||||
)
|
||||
|
||||
self.global_menu_file = None
|
||||
self.global_menu_edit = None
|
||||
self.global_menu_view = DropDownMouseCapture(
|
||||
layer="bottom",
|
||||
child_window=ModusDropdown(
|
||||
dropdown_id="global-menu-view",
|
||||
parent=parent,
|
||||
dropdown_children=[
|
||||
dropdown_option(
|
||||
"Enter Full Screen",
|
||||
on_click="hyprctl dispatch fullscreen",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
self.global_menu_go = None
|
||||
self.global_menu_window = DropDownMouseCapture(
|
||||
layer="bottom",
|
||||
child_window=ModusDropdown(
|
||||
dropdown_id="global-menu-window",
|
||||
parent=parent,
|
||||
dropdown_children=[
|
||||
dropdown_option(
|
||||
"Zoom In",
|
||||
" +",
|
||||
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '.float * 1.1')",
|
||||
),
|
||||
dropdown_option(
|
||||
"Zoom Out",
|
||||
" -",
|
||||
on_click="hyprctl -q keyword cursor:zoom_factor $(hyprctl getoption cursor:zoom_factor -j | jq '(.float * 0.9) | if . < 1 then 1 else . end')",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"Move Window to Left",
|
||||
on_click="hyprctl dispatch movewindow l",
|
||||
),
|
||||
dropdown_option(
|
||||
"Move Window to Right",
|
||||
on_click="hyprctl dispatch movewindow r",
|
||||
),
|
||||
dropdown_option(
|
||||
"Cycle Through Windows",
|
||||
on_click="hyprctl dispatch cyclenext",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"Float", on_click="hyprctl dispatch togglefloating"
|
||||
),
|
||||
dropdown_option("Quit", on_click="hyprctl dispatch killactive"),
|
||||
dropdown_option("Pseudo", on_click="hyprctl dispatch pseudo"),
|
||||
dropdown_option(
|
||||
"Toggle Split", on_click="hyprctl dispatch togglesplit"
|
||||
),
|
||||
dropdown_option("Center", on_click="hyprctl dispatch centerwindow"),
|
||||
dropdown_option("Group", on_click="hyprctl dispatch togglegroup"),
|
||||
dropdown_option(
|
||||
"Pin",
|
||||
on_clicked=lambda _: subprocess.run(
|
||||
"bash ~/.config/scripts/winpin.sh", shell=True
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
self.global_menu_help = DropDownMouseCapture(
|
||||
layer="bottom",
|
||||
child_window=ModusDropdown(
|
||||
dropdown_id="global-menu-help",
|
||||
parent=parent,
|
||||
dropdown_children=[
|
||||
dropdown_option(
|
||||
"Modus",
|
||||
on_click="xdg-open https://github.com/S4NKALP/Modus/issues",
|
||||
),
|
||||
dropdown_divider("---------------------"),
|
||||
dropdown_option(
|
||||
"Hyprland Wiki", on_click="xdg-open https://wiki.hyprland.org/"
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
# Create menu buttons
|
||||
modus_service.connect(
|
||||
"current-active-app-name-changed",
|
||||
lambda _, value: self.global_title_menu_about.set_property(
|
||||
"label", f"About {value}"
|
||||
),
|
||||
)
|
||||
|
||||
# Connect to active app name changes to update the title button
|
||||
modus_service.connect("current-active-app-name-changed", self._on_active_app_changed)
|
||||
|
||||
self.global_menu_button_title = Button(
|
||||
child=ActiveWindow(
|
||||
formatter=FormattedString(
|
||||
"{ format_window(win_title, win_class) }",
|
||||
format_window=format_window,
|
||||
)
|
||||
),
|
||||
name="global-title-button",
|
||||
style_classes="button",
|
||||
on_clicked=self._on_title_button_clicked,
|
||||
)
|
||||
|
||||
self.global_menu_title.child_window.set_pointing_to(
|
||||
self.global_menu_button_title
|
||||
)
|
||||
|
||||
self.global_menu_button_file = Button(
|
||||
label="File", name="global-menu-button-file", style_classes="button"
|
||||
)
|
||||
self.global_menu_button_edit = Button(
|
||||
label="Edit", name="global-menu-button-edit", style_classes="button"
|
||||
)
|
||||
self.global_menu_button_view = Button(
|
||||
label="View",
|
||||
name="global-menu-button-view",
|
||||
style_classes="button",
|
||||
on_clicked=lambda _: self.global_menu_view.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_view.child_window.set_pointing_to(self.global_menu_button_view)
|
||||
self.global_menu_button_go = Button(
|
||||
label="Go", name="global-menu-button-go", style_classes="button"
|
||||
)
|
||||
self.global_menu_button_window = Button(
|
||||
label="Window",
|
||||
name="global-menu-button-window",
|
||||
style_classes="button",
|
||||
on_clicked=lambda _: self.global_menu_window.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_window.child_window.set_pointing_to(
|
||||
self.global_menu_button_window
|
||||
)
|
||||
self.global_menu_button_help = Button(
|
||||
label="Help",
|
||||
name="global-menu-button-help",
|
||||
style_classes="button",
|
||||
on_clicked=lambda _: self.global_menu_help.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_help.child_window.set_pointing_to(self.global_menu_button_help)
|
||||
|
||||
modus_service.connect("current-dropdown-changed", self.changed_dropdown)
|
||||
modus_service.connect("dropdowns-hide-changed", self.hide_dropdowns)
|
||||
|
||||
def _on_title_button_clicked(self, _):
|
||||
"""Handle title button click - only show dropdown if there's an active window"""
|
||||
try:
|
||||
# Use modus_service's Hyprland connection to get current window info
|
||||
if (
|
||||
hasattr(modus_service, "_hyprland_connection")
|
||||
and modus_service._hyprland_connection
|
||||
):
|
||||
window_data = modus_service._hyprland_connection.send_command(
|
||||
"j/activewindow"
|
||||
).reply
|
||||
if window_data:
|
||||
window_info = json.loads(window_data.decode("utf-8"))
|
||||
wmclass = window_info.get("class", "")
|
||||
title = window_info.get("title", "")
|
||||
|
||||
# Only show dropdown if there's an active window (not Finder)
|
||||
if wmclass or title:
|
||||
self.global_menu_title.toggle_mousecapture()
|
||||
return
|
||||
|
||||
# Fallback: check if current_active_app_name is not "Finder"
|
||||
if (
|
||||
modus_service.current_active_app_name
|
||||
and modus_service.current_active_app_name != "Finder"
|
||||
):
|
||||
self.global_menu_title.toggle_mousecapture()
|
||||
except Exception:
|
||||
# If we can't get window info, don't show dropdown
|
||||
pass
|
||||
|
||||
def _on_active_app_changed(self, _, value):
|
||||
"""Handle active app name changes"""
|
||||
# Update the "About" menu item label
|
||||
self.global_title_menu_about.set_property("label", f"About {value}")
|
||||
|
||||
def hide_dropdowns(self, *_):
|
||||
self.menu_button.remove_style_class("active")
|
||||
self.global_menu_button_edit.remove_style_class("active")
|
||||
self.global_menu_button_file.remove_style_class("active")
|
||||
self.global_menu_button_go.remove_style_class("active")
|
||||
self.global_menu_button_help.remove_style_class("active")
|
||||
self.global_menu_button_title.remove_style_class("active")
|
||||
self.global_menu_button_view.remove_style_class("active")
|
||||
self.global_menu_button_window.remove_style_class("active")
|
||||
|
||||
def changed_dropdown(self, _, dropdown_id):
|
||||
self.hide_dropdowns(_, True)
|
||||
match dropdown_id:
|
||||
case "os-menu":
|
||||
self.menu_button.add_style_class("active")
|
||||
case "global-menu-edit":
|
||||
self.global_menu_button_edit.add_style_class("active")
|
||||
case "global-menu-file":
|
||||
self.global_menu_button_file.add_style_class("active")
|
||||
case "global-menu-go":
|
||||
self.global_menu_button_go.add_style_class("active")
|
||||
case "global-menu-help":
|
||||
self.global_menu_button_help.add_style_class("active")
|
||||
case "global-menu-title":
|
||||
self.global_menu_button_title.add_style_class("active")
|
||||
case "global-menu-view":
|
||||
self.global_menu_button_view.add_style_class("active")
|
||||
case "global-menu-window":
|
||||
self.global_menu_button_window.add_style_class("active")
|
||||
case _:
|
||||
pass
|
||||
|
||||
|
||||
class MenuBar(Box):
|
||||
"""Main MenuBar widget that contains all menu buttons"""
|
||||
|
||||
def __init__(self, parent_window=None, **kwargs):
|
||||
# Extract parent_window from kwargs if not provided as parameter
|
||||
if parent_window is None:
|
||||
parent_window = kwargs.pop("parent_window", None)
|
||||
|
||||
super().__init__(name="menubar", orientation="horizontal", spacing=0, **kwargs)
|
||||
|
||||
# Create the dropdown system
|
||||
self.dropdown_system = MenuBarDropdowns(parent=parent_window)
|
||||
|
||||
# Add all the menu buttons to the menubar
|
||||
self.children = [
|
||||
self.dropdown_system.global_menu_button_title,
|
||||
self.dropdown_system.global_menu_button_file,
|
||||
self.dropdown_system.global_menu_button_edit,
|
||||
self.dropdown_system.global_menu_button_view,
|
||||
self.dropdown_system.global_menu_button_go,
|
||||
self.dropdown_system.global_menu_button_window,
|
||||
self.dropdown_system.global_menu_button_help,
|
||||
]
|
||||
|
||||
def show_system_dropdown(self, imac_button):
|
||||
self.dropdown_system.menu_button_dropdown.child_window.set_pointing_to(
|
||||
imac_button
|
||||
)
|
||||
mouse_capture = self.dropdown_system.menu_button_dropdown
|
||||
if mouse_capture.is_visible():
|
||||
mouse_capture.set_child_window_visible(False)
|
||||
else:
|
||||
mouse_capture.set_child_window_visible(True)
|
||||
@@ -1,13 +1,12 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.utils import GLib, get_relative_path, os
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.svg import Svg
|
||||
from gi.repository import GLib
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
|
||||
from utils.functions import is_app_running, kill_process
|
||||
|
||||
|
||||
class RecordingIndicator(Button):
|
||||
@@ -24,13 +23,8 @@ class RecordingIndicator(Button):
|
||||
self.timer_timeout_id = None
|
||||
self.status_timeout_id = None
|
||||
|
||||
self.recording_icon = Svg(
|
||||
name="indicators-icon",
|
||||
size=24,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/misc/media-record.svg"
|
||||
),
|
||||
)
|
||||
self.recording_icon = svg_file("misc/media-record.svg", size=24)
|
||||
|
||||
self.time_label = Label(
|
||||
name="recording-time-label",
|
||||
markup="00:00",
|
||||
@@ -47,9 +41,19 @@ class RecordingIndicator(Button):
|
||||
|
||||
self.add(self.recording_box)
|
||||
|
||||
# Prevent container.show_all() from forcing this visible when no recording
|
||||
try:
|
||||
self.set_no_show_all(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.connect("clicked", self.on_stop_recording)
|
||||
try:
|
||||
setup_cursor_hover(self, "pointer")
|
||||
except Exception:
|
||||
pass
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.hide()
|
||||
self.set_visible(False)
|
||||
|
||||
GLib.timeout_add(100, self._delayed_init)
|
||||
|
||||
@@ -58,22 +62,13 @@ class RecordingIndicator(Button):
|
||||
return False
|
||||
|
||||
def is_recorder_running(self):
|
||||
# add more process names if needed
|
||||
# Check if any recorder process is running
|
||||
recorder_processes = ["wf-recorder", "gpu-screen-recorder"]
|
||||
|
||||
try:
|
||||
for proc in recorder_processes:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-x", proc],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True # Found a running recorder process
|
||||
return False # None found running
|
||||
except Exception:
|
||||
return False
|
||||
for proc in recorder_processes:
|
||||
if is_app_running(proc):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_recording_status(self):
|
||||
current_time = time.time()
|
||||
@@ -95,9 +90,9 @@ class RecordingIndicator(Button):
|
||||
|
||||
self.update_timer_display()
|
||||
else:
|
||||
if self.get_visible():
|
||||
self.set_visible(False)
|
||||
self.cleanup_recording_state()
|
||||
# Ensure it stays hidden when not recording
|
||||
self.set_visible(False)
|
||||
self.cleanup_recording_state()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error checking recording status: {e}")
|
||||
@@ -172,11 +167,9 @@ class RecordingIndicator(Button):
|
||||
|
||||
def send_stop_command():
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[self.script_path, "record", "stop"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
# Kill any running recorder processes
|
||||
kill_process("wf-recorder")
|
||||
kill_process("gpu-screen-recorder")
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error sending stop command: {e}")
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton
|
||||
from fabric.widgets.box import Box
|
||||
|
||||
from services.config import get_config, on_config_change
|
||||
from utils.functions import is_special_workspace_id
|
||||
from utils.utils import setup_cursor_hover
|
||||
|
||||
# TODO: Support multi-monitor setups
|
||||
|
||||
|
||||
class WorkspaceIndicator(Box):
|
||||
def __init__(self, **kwargs):
|
||||
Box.__init__(
|
||||
self, name="workspace-indicator", orientation="h", spacing=4, **kwargs
|
||||
)
|
||||
self._current_config = {"hide_special_workspace": True}
|
||||
on_config_change(self._on_config_changed)
|
||||
|
||||
self.workspaces = HyprlandWorkspaces(
|
||||
name="workspaces",
|
||||
spacing=4,
|
||||
buttons_factory=self._get_button_factory(),
|
||||
)
|
||||
|
||||
self.add(self.workspaces)
|
||||
self.show_all()
|
||||
|
||||
self._apply_initial_config()
|
||||
|
||||
def _apply_initial_config(self):
|
||||
new_value = get_config("hide_special_workspace", True)
|
||||
if new_value != self._current_config["hide_special_workspace"]:
|
||||
self._current_config["hide_special_workspace"] = new_value
|
||||
self.update_config({"hide_special_workspace": new_value})
|
||||
|
||||
def _on_config_changed(self, new_config: dict, old_config: dict):
|
||||
if "hide_special_workspace" in new_config and new_config.get(
|
||||
"hide_special_workspace"
|
||||
) != old_config.get("hide_special_workspace"):
|
||||
self._current_config["hide_special_workspace"] = new_config.get(
|
||||
"hide_special_workspace", True
|
||||
)
|
||||
self.update_config(
|
||||
{
|
||||
"hide_special_workspace": self._current_config[
|
||||
"hide_special_workspace"
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
def _get_button_factory(self):
|
||||
if self._current_config.get("hide_special_workspace", True):
|
||||
return self._create_filtered_button
|
||||
else:
|
||||
return lambda ws_id: self._create_button_with_hover(ws_id)
|
||||
|
||||
def _create_filtered_button(self, ws_id):
|
||||
if self._current_config.get(
|
||||
"hide_special_workspace", True
|
||||
) and is_special_workspace_id(ws_id):
|
||||
return None
|
||||
return self._create_button_with_hover(ws_id)
|
||||
|
||||
def _create_button_with_hover(self, ws_id):
|
||||
button = WorkspaceButton(id=ws_id, label=str(ws_id))
|
||||
setup_cursor_hover(button, "pointer")
|
||||
return button
|
||||
|
||||
def update_config(self, new_config: dict):
|
||||
if "hide_special_workspace" in new_config:
|
||||
button_factory = self._get_button_factory()
|
||||
self.remove(self.workspaces)
|
||||
self.workspaces = HyprlandWorkspaces(
|
||||
name="workspaces",
|
||||
spacing=4,
|
||||
buttons_factory=button_factory,
|
||||
)
|
||||
self.add(self.workspaces)
|
||||
self.workspaces.show_all()
|
||||
+113
-125
@@ -1,14 +1,14 @@
|
||||
from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton
|
||||
from fabric.system_tray.widgets import SystemTray
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.datetime import DateTime
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from modules.panel.components.globalmenu import GlobalMenu
|
||||
from modules.panel.components.workspace import WorkspaceIndicator
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
|
||||
import config.data as data
|
||||
from modules.controlcenter.main import ModusControlCenter
|
||||
from modules.notification.notification_center import NotificationCenter
|
||||
from modules.panel.components.enhanced_system_tray import apply_enhanced_system_tray
|
||||
@@ -17,53 +17,38 @@ from modules.panel.components.indicators import (
|
||||
BluetoothIndicator,
|
||||
NetworkIndicator,
|
||||
)
|
||||
from modules.panel.components.menubar import MenuBar
|
||||
from modules.panel.components.recording_indicator import RecordingIndicator
|
||||
from modules.todo.todo_widget import TodoListCapture
|
||||
from services.config import get_config, on_config_change
|
||||
from services.modus import notification_service
|
||||
from utils.functions import is_special_workspace_id
|
||||
from utils.roam import modus_service
|
||||
from widgets.mousecapture import MouseCapture
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
# Apply enhanced system tray icon handling
|
||||
apply_enhanced_system_tray()
|
||||
|
||||
|
||||
class Panel(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="bar",
|
||||
name="panel-window",
|
||||
title="modus",
|
||||
layer="top",
|
||||
anchor="left top right",
|
||||
exclusivity="auto",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
self.launcher = kwargs.get("launcher", None)
|
||||
self.menubar = MenuBar(parent_window=self)
|
||||
|
||||
self.workspace_indicator = HyprlandWorkspaces(
|
||||
name="workspaces",
|
||||
spacing=4,
|
||||
buttons_factory=lambda ws_id: (
|
||||
None
|
||||
if data.HIDE_SPECIAL_WORKSPACE and is_special_workspace_id(ws_id)
|
||||
else WorkspaceButton(id=ws_id, label=str(ws_id))
|
||||
),
|
||||
pass_through=False,
|
||||
visible=True,
|
||||
all_visible=False,
|
||||
)
|
||||
self.globalmenu = GlobalMenu(parent_window=self)
|
||||
|
||||
self.imac = Button(
|
||||
name="panel-button",
|
||||
child=Svg(
|
||||
size=16,
|
||||
svg_file=get_relative_path("../../config/assets/icons/misc/logo.svg"),
|
||||
),
|
||||
on_clicked=lambda *_: self.menubar.show_system_dropdown(self.imac),
|
||||
child=svg_file("misc/logo.svg", size=18),
|
||||
on_clicked=lambda *_: self.globalmenu.show_system_dropdown((self.imac)),
|
||||
)
|
||||
setup_cursor_hover(self.imac, "pointer")
|
||||
|
||||
self.tray = SystemTray(name="panel-button", spacing=4, icon_size=20)
|
||||
|
||||
self.tray = SystemTray(name="system-tray", spacing=4, icon_size=20)
|
||||
self.tray_revealer = Revealer(
|
||||
name="tray-revealer",
|
||||
child=self.tray,
|
||||
@@ -74,110 +59,67 @@ class Panel(Window):
|
||||
|
||||
self.chevron_button = Button(
|
||||
name="panel-button",
|
||||
child=Svg(
|
||||
size=16,
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/misc/chevron-right.svg"
|
||||
),
|
||||
),
|
||||
child=svg_file("misc/chevron-right.svg", size=16),
|
||||
on_clicked=self.toggle_tray,
|
||||
)
|
||||
setup_cursor_hover(self.chevron_button, "pointer")
|
||||
|
||||
self.indicators = Box(
|
||||
name="indicators",
|
||||
orientation="h",
|
||||
spacing=4,
|
||||
children=[
|
||||
BatteryIndicator(),
|
||||
NetworkIndicator(),
|
||||
BluetoothIndicator(),
|
||||
],
|
||||
)
|
||||
|
||||
self.search = Button(
|
||||
name="panel-button",
|
||||
on_clicked=lambda *_: self.search_apps(),
|
||||
child=Svg(
|
||||
size=22,
|
||||
svg_file=get_relative_path("../../config/assets/icons/misc/search.svg"),
|
||||
),
|
||||
name="panel-button", child=svg_file("misc/search.svg", size=22)
|
||||
)
|
||||
setup_cursor_hover(self.search, "pointer")
|
||||
|
||||
self.control_center = MouseCapture(
|
||||
layer="top", child_window=ModusControlCenter()
|
||||
)
|
||||
|
||||
self.control_center_button = Button(
|
||||
name="control-center-button",
|
||||
style_classes="button",
|
||||
self.control_center_btn = Button(
|
||||
name="panel-button",
|
||||
child=svg_file("misc/control-center.svg", size=22),
|
||||
on_clicked=self.control_center.toggle_mousecapture,
|
||||
child=Svg(
|
||||
size=22,
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/misc/control-center.svg"
|
||||
),
|
||||
),
|
||||
)
|
||||
setup_cursor_hover(self.control_center_btn, "pointer")
|
||||
|
||||
# Notification Center with MouseCapture
|
||||
self.notification_center = MouseCapture(
|
||||
layer="overlay", child_window=NotificationCenter()
|
||||
)
|
||||
|
||||
# Todo List with MouseCapture
|
||||
self.todo_list = TodoListCapture()
|
||||
|
||||
# Notification Center Icon
|
||||
self.notification_icon = Svg(
|
||||
size=22,
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/notifications/notification-inactive.svg"
|
||||
),
|
||||
self.notification_icon = svg_file(
|
||||
"notifications/notification-inactive.svg", size=22
|
||||
)
|
||||
|
||||
self.notification_center_icon_button = Button(
|
||||
name="notification-center-icon-button",
|
||||
self.notification_center_btn = Button(
|
||||
name="panel-button",
|
||||
child=self.notification_icon,
|
||||
on_clicked=self.on_notification_icon_clicked,
|
||||
)
|
||||
setup_cursor_hover(self.notification_center_btn, "pointer")
|
||||
|
||||
# Clickable DateTime for todo list
|
||||
self.datetime_button = Button(
|
||||
name="datetime-button",
|
||||
self.datetime_btn = Button(
|
||||
name="panel-button",
|
||||
child=DateTime(name="date-time", formatters=["%a %-d %b %I:%M %P"]),
|
||||
on_clicked=self.on_datetime_clicked,
|
||||
)
|
||||
setup_cursor_hover(self.datetime_btn, "pointer")
|
||||
|
||||
self.workspace_indicator = WorkspaceIndicator()
|
||||
self.recording_indicator = RecordingIndicator()
|
||||
|
||||
# Create boxes and mount, to allow live updates
|
||||
self.left_box = Box(name="modules-left")
|
||||
self.center_box = Box(name="modules-center", children=self.recording_indicator)
|
||||
self.right_box = Box(name="modules-right", spacing=4, orientation="h")
|
||||
|
||||
self.children = CenterBox(
|
||||
name="panel",
|
||||
start_children=Box(
|
||||
name="modules-left",
|
||||
children=[
|
||||
self.imac,
|
||||
self.menubar,
|
||||
],
|
||||
),
|
||||
center_children=Box(
|
||||
name="modules-center",
|
||||
children=self.recording_indicator,
|
||||
),
|
||||
end_children=Box(
|
||||
name="modules-right",
|
||||
spacing=4,
|
||||
orientation="h",
|
||||
children=[
|
||||
self.workspace_indicator,
|
||||
self.tray_revealer,
|
||||
self.chevron_button,
|
||||
self.indicators,
|
||||
self.search,
|
||||
self.control_center_button,
|
||||
self.datetime_button,
|
||||
self.notification_center_icon_button,
|
||||
],
|
||||
),
|
||||
start_children=self.left_box,
|
||||
center_children=self.center_box,
|
||||
end_children=self.right_box,
|
||||
)
|
||||
|
||||
# Connect to DND state changes for notification icon
|
||||
@@ -191,46 +133,28 @@ class Panel(Window):
|
||||
# Set initial notification icon state
|
||||
self.update_notification_icon()
|
||||
|
||||
return self.show_all()
|
||||
# Initial layout build
|
||||
self._rebuild_layout_from_config()
|
||||
|
||||
def search_apps(self):
|
||||
self.launcher.show_launcher()
|
||||
# Live updates
|
||||
on_config_change(self._on_config_changed)
|
||||
|
||||
def toggle_tray(self, *_):
|
||||
current_state = self.tray_revealer.child_revealed
|
||||
self.tray_revealer.child_revealed = not current_state
|
||||
|
||||
if self.tray_revealer.child_revealed:
|
||||
self.chevron_button.get_child().set_from_file(
|
||||
get_relative_path("../../config/assets/icons/misc/chevron-left.svg")
|
||||
)
|
||||
else:
|
||||
self.chevron_button.get_child().set_from_file(
|
||||
get_relative_path("../../config/assets/icons/misc/chevron-right.svg")
|
||||
)
|
||||
self.show_all()
|
||||
|
||||
def on_dnd_changed(self, _, dnd_state):
|
||||
"""Handle DND state changes from the service."""
|
||||
self.update_notification_icon() # Update notification icon when DND changes
|
||||
|
||||
def on_notification_count_changed(self, service, *args):
|
||||
"""Handle notification count changes from the service."""
|
||||
self.update_notification_icon()
|
||||
|
||||
def on_notification_icon_clicked(self, *args):
|
||||
"""Handle notification icon clicks - only open center if there are notifications."""
|
||||
count = notification_service.count
|
||||
if count > 0:
|
||||
# Only open notification center if there are notifications
|
||||
self.notification_center.toggle_mousecapture()
|
||||
# Do nothing if no notifications
|
||||
|
||||
def on_datetime_clicked(self, *args):
|
||||
"""Handle datetime button clicks - open todo list."""
|
||||
self.todo_list.toggle_mousecapture()
|
||||
|
||||
def update_notification_icon(self):
|
||||
"""Update the notification icon based on count and DND state."""
|
||||
count = notification_service.count
|
||||
dnd_enabled = modus_service.dont_disturb
|
||||
|
||||
@@ -244,7 +168,71 @@ class Panel(Window):
|
||||
# No notifications - show inactive icon
|
||||
icon_file = "notification-inactive.svg"
|
||||
|
||||
icon_path = get_relative_path(
|
||||
f"../../config/assets/icons/notifications/{icon_file}"
|
||||
)
|
||||
self.notification_icon.set_from_file(icon_path)
|
||||
self.notification_icon.dynamic_file(f"notifications/{icon_file}")
|
||||
|
||||
def _rebuild_layout_from_config(self):
|
||||
# Left
|
||||
left_children = []
|
||||
if get_config("imac_button", True):
|
||||
left_children.append(self.imac)
|
||||
if get_config("global_menu", True):
|
||||
left_children.append(self.globalmenu)
|
||||
self.left_box.children = left_children
|
||||
|
||||
# Indicators (create fresh instances on each rebuild)
|
||||
indicators_children = []
|
||||
if get_config("battery", True):
|
||||
indicators_children.append(BatteryIndicator())
|
||||
if get_config("network", True):
|
||||
indicators_children.append(NetworkIndicator())
|
||||
if get_config("bluetooth", True):
|
||||
indicators_children.append(BluetoothIndicator())
|
||||
self.indicators.children = indicators_children
|
||||
|
||||
# Right
|
||||
right_children = []
|
||||
if get_config("workspace_indicator", True):
|
||||
right_children.append(self.workspace_indicator)
|
||||
|
||||
if get_config("systray", True):
|
||||
right_children.extend([self.tray_revealer, self.chevron_button])
|
||||
|
||||
right_children.append(self.indicators)
|
||||
|
||||
if get_config("search", True):
|
||||
right_children.append(self.search)
|
||||
if get_config("control_center", True):
|
||||
right_children.append(self.control_center_btn)
|
||||
if get_config("date_time", True):
|
||||
right_children.append(self.datetime_btn)
|
||||
if get_config("notification_center", True):
|
||||
right_children.append(self.notification_center_btn)
|
||||
|
||||
self.right_box.children = right_children
|
||||
self.show_all()
|
||||
|
||||
def _on_config_changed(self, new_config, old_config):
|
||||
keys = {
|
||||
"imac_button",
|
||||
"global_menu",
|
||||
"workspace_indicator",
|
||||
"systray",
|
||||
"battery",
|
||||
"network",
|
||||
"bluetooth",
|
||||
"search",
|
||||
"control_center",
|
||||
"date_time",
|
||||
"notification_center",
|
||||
}
|
||||
if any(new_config.get(k) != old_config.get(k) for k in keys):
|
||||
self._rebuild_layout_from_config()
|
||||
|
||||
def toggle_tray(self, *_):
|
||||
current_state = self.tray_revealer.child_revealed
|
||||
self.tray_revealer.child_revealed = not current_state
|
||||
|
||||
if self.tray_revealer.child_revealed:
|
||||
self.chevron_button.get_child().dynamic_file("misc/chevron-left.svg")
|
||||
else:
|
||||
self.chevron_button.get_child().dynamic_file("misc/chevron-right.svg")
|
||||
Reference in new issue
Block a user