Huge updates: ControlCenter, DropDown Menu
This commit is contained in:
34 files changed
+3261
-1440
No files matched your search
@@ -10,6 +10,9 @@ from gi.repository import Gtk, GdkPixbuf
|
||||
from fabric.system_tray.widgets import SystemTrayItem
|
||||
|
||||
|
||||
# FIX: the tooltip should show application names instead of unknown
|
||||
|
||||
|
||||
def patched_do_update_properties(self, *_):
|
||||
# Try default GTK theme first
|
||||
icon_name = self._item.icon_name
|
||||
|
||||
@@ -1,137 +1,34 @@
|
||||
from gi.repository import GLib
|
||||
|
||||
import utils.icons as icons
|
||||
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.svg import Svg
|
||||
from services.battery import Battery as BatteryService
|
||||
from services.network import NetworkClient
|
||||
from services.network import NetworkService
|
||||
from services.battery import Battery
|
||||
from utils.roam import modus_service
|
||||
|
||||
|
||||
class WifiIndicator(Button):
|
||||
class BluetoothIndicator(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.network = NetworkClient()
|
||||
self.wifi_icon = Svg(
|
||||
name="indicators-icon",
|
||||
size=24,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/wifi.svg"),
|
||||
)
|
||||
|
||||
self.add(self.wifi_icon)
|
||||
|
||||
self.network.connect("ready", self.on_network_ready)
|
||||
|
||||
if self.network.is_ready:
|
||||
self.setup_signals()
|
||||
|
||||
def on_network_ready(self, *args):
|
||||
self.setup_signals()
|
||||
|
||||
def setup_signals(self):
|
||||
self.network.connect("changed", self.on_network_changed)
|
||||
self.network.connect("wifi-device-added", self.on_wifi_device_added)
|
||||
self.network.connect("wifi-device-removed", self.on_wifi_device_removed)
|
||||
self.network.connect("ethernet-device-added", self.on_ethernet_device_added)
|
||||
self.network.connect("ethernet-device-removed", self.on_ethernet_device_removed)
|
||||
|
||||
if self.network.wifi_device:
|
||||
self.network.wifi_device.connect("changed", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-added", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-removed", self.on_wifi_changed)
|
||||
|
||||
if self.network.ethernet_device:
|
||||
self.network.ethernet_device.connect("changed", self.on_ethernet_changed)
|
||||
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
if (
|
||||
self.network.ethernet_device
|
||||
and self.network.ethernet_device.state == "activated"
|
||||
):
|
||||
self.wifi_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/wifi.svg")
|
||||
)
|
||||
tooltip = f"Connected to Ethernet ({self.network.ethernet_device.speed})"
|
||||
self.set_tooltip_text(tooltip)
|
||||
return
|
||||
|
||||
if not self.network.wifi_device:
|
||||
self.wifi_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/wifi-off.svg")
|
||||
)
|
||||
tooltip = "No WiFi device found"
|
||||
elif not self.network.wifi_device.wireless_enabled:
|
||||
self.wifi_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/wifi-off.svg")
|
||||
)
|
||||
tooltip = "WiFi disabled"
|
||||
else:
|
||||
active_ap = self.network.wifi_device.active_access_point
|
||||
if active_ap:
|
||||
strength = active_ap.strength
|
||||
# Use wifi.svg for all connected states since we don't have signal strength icons
|
||||
self.wifi_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/wifi.svg")
|
||||
)
|
||||
tooltip = f"Connected to {active_ap.ssid} ({strength}%)"
|
||||
else:
|
||||
self.wifi_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/wifi-off.svg")
|
||||
)
|
||||
tooltip = "Not connected"
|
||||
|
||||
self.set_tooltip_text(tooltip)
|
||||
|
||||
def on_wifi_device_added(self, *args):
|
||||
self.network.wifi_device.connect("changed", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-added", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-removed", self.on_wifi_changed)
|
||||
self.update_state()
|
||||
|
||||
def on_wifi_device_removed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
def on_network_changed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
def on_wifi_changed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
def on_ethernet_device_added(self, *args):
|
||||
if self.network.ethernet_device:
|
||||
self.network.ethernet_device.connect("changed", self.on_ethernet_changed)
|
||||
self.update_state()
|
||||
|
||||
def on_ethernet_device_removed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
def on_ethernet_changed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
|
||||
class BluetoothIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(name="bluetooth-indicator", orientation="h", **kwargs)
|
||||
|
||||
self.bluetooth = BluetoothClient()
|
||||
|
||||
# Use SVG icons instead of font icons
|
||||
self.bt_icon = Svg(
|
||||
name="bt-icon",
|
||||
size=20,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/bluetooth.svg"),
|
||||
)
|
||||
self.add(self.bt_icon)
|
||||
|
||||
self.bluetooth.connect("changed", self.on_bluetooth_changed)
|
||||
self.bt_button = Button(name="bt-button", child=self.bt_icon)
|
||||
|
||||
self.add(self.bt_button)
|
||||
|
||||
modus_service.connect("bluetooth-changed", self.on_bluetooth_changed)
|
||||
self.bluetooth.connect("changed", self.on_bluetooth_direct_changed)
|
||||
self.bluetooth.connect("device-added", self.on_device_added)
|
||||
self.bluetooth.connect("device-removed", self.on_device_removed)
|
||||
|
||||
self.update_modus_service_bluetooth_state()
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
@@ -159,161 +56,269 @@ class BluetoothIndicator(Button):
|
||||
)
|
||||
tooltip = "No devices connected"
|
||||
|
||||
self.set_tooltip_text(tooltip)
|
||||
self.bt_button.set_tooltip_text(tooltip)
|
||||
|
||||
def on_bluetooth_changed(self, *args):
|
||||
def on_bluetooth_changed(self, service, new_bluetooth_state):
|
||||
self.update_state()
|
||||
|
||||
def on_bluetooth_direct_changed(self, *args):
|
||||
self.update_modus_service_bluetooth_state()
|
||||
self.update_state()
|
||||
|
||||
def on_device_added(self, _, address):
|
||||
self.update_modus_service_bluetooth_state()
|
||||
self.update_state()
|
||||
|
||||
def on_device_removed(self, _, address):
|
||||
self.update_modus_service_bluetooth_state()
|
||||
self.update_state()
|
||||
|
||||
|
||||
class Battery(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(orientation="v", visible=True, **kwargs)
|
||||
|
||||
self._battery = BatteryService()
|
||||
self._battery.changed.connect(self.update_battery)
|
||||
|
||||
self.icon = Svg(
|
||||
name="indicators-icon",
|
||||
size=24,
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/battery/battery-100.svg"
|
||||
),
|
||||
)
|
||||
self.battery_button = Button(
|
||||
child=self.icon,
|
||||
)
|
||||
|
||||
self.battery_box = Box(
|
||||
name="battery-box",
|
||||
orientation="h",
|
||||
children=[self.battery_button],
|
||||
)
|
||||
|
||||
self.battery_button.set_has_tooltip(True)
|
||||
self.battery_box.set_has_tooltip(True)
|
||||
|
||||
self.battery_button.connect("query-tooltip", self.on_query_tooltip)
|
||||
self.battery_box.connect("query-tooltip", self.on_query_tooltip)
|
||||
|
||||
self.add(self.battery_box)
|
||||
|
||||
GLib.idle_add(self.update_battery)
|
||||
|
||||
def on_query_tooltip(self, *_):
|
||||
tooltip = _[-1]
|
||||
tooltip.set_markup(self.get_tooltip_text())
|
||||
return True
|
||||
|
||||
def get_tooltip_text(self):
|
||||
if not self._battery.is_present:
|
||||
return "No battery detected"
|
||||
|
||||
percentage = self._battery.percentage
|
||||
state = self._battery.state
|
||||
|
||||
capacity = int(float(self._battery.capacity.rstrip("%")))
|
||||
tooltip_points = []
|
||||
|
||||
if state == "FULLY_CHARGED":
|
||||
status = f"{icons.battery_full} Fully Charged"
|
||||
elif state == "CHARGING":
|
||||
status = f"{icons.bat_charging} Charging"
|
||||
elif percentage <= 15 and state == "DISCHARGING":
|
||||
status = f"{icons.bat_alert} Low Battery"
|
||||
elif state == "DISCHARGING":
|
||||
status = f"{icons.battery_0} Discharging"
|
||||
def update_modus_service_bluetooth_state(self):
|
||||
if not self.bluetooth.enabled:
|
||||
bluetooth_state = "disabled"
|
||||
else:
|
||||
status = "Battery"
|
||||
tooltip_points.append(f"• Status: {status}")
|
||||
|
||||
tooltip_points.append(f"• Level: {int(percentage)}%")
|
||||
tooltip_points.append(f"• Battery Health: {capacity}%")
|
||||
|
||||
power_profile = self._battery.power_profile
|
||||
if power_profile:
|
||||
if power_profile == "power-saver":
|
||||
profile_icon = icons.power_saving
|
||||
elif power_profile == "performance":
|
||||
profile_icon = icons.power_performance
|
||||
connected_devices = self.bluetooth.connected_devices
|
||||
if connected_devices:
|
||||
if len(connected_devices) == 1:
|
||||
device = connected_devices[0]
|
||||
bluetooth_state = f"connected:{device.alias}"
|
||||
if (
|
||||
hasattr(device, "battery_percentage")
|
||||
and device.battery_percentage > 0
|
||||
):
|
||||
bluetooth_state += f":{device.battery_percentage:.0f}%"
|
||||
else:
|
||||
bluetooth_state = f"connected:{len(connected_devices)}_devices"
|
||||
else:
|
||||
profile_icon = icons.power_balanced
|
||||
tooltip_points.append(f"• Profile: {profile_icon} {power_profile}")
|
||||
bluetooth_state = "enabled"
|
||||
|
||||
temp = self._battery.temperature
|
||||
if temp != "N/A":
|
||||
tooltip_points.append(f"• Temperature: {temp}")
|
||||
modus_service.bluetooth = bluetooth_state
|
||||
|
||||
|
||||
class NetworkIndicator(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="network-indicator", orientation="h", **kwargs)
|
||||
|
||||
self.network_service = NetworkService()
|
||||
|
||||
self.network_icon = Svg(
|
||||
name="network-icon",
|
||||
size=25,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/wifi.svg"),
|
||||
)
|
||||
|
||||
self.network_button = Button(name="network-button", child=self.network_icon)
|
||||
|
||||
self.add(self.network_button)
|
||||
modus_service.connect("wlan-changed", self.on_wlan_changed)
|
||||
self.network_service.connect("device-ready", self.on_device_ready)
|
||||
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
def on_wlan_changed(self, service, new_wlan_state):
|
||||
self.update_state()
|
||||
|
||||
def on_device_ready(self, *args):
|
||||
if self.network_service.wifi_device:
|
||||
self.network_service.wifi_device.connect(
|
||||
"changed", self.on_network_direct_changed
|
||||
)
|
||||
|
||||
if self.network_service.ethernet_device:
|
||||
self.network_service.ethernet_device.connect(
|
||||
"changed", self.on_network_direct_changed
|
||||
)
|
||||
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
def on_network_direct_changed(self, *args):
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
def on_network_changed(self, *args):
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
def update_modus_service_wlan_state(self):
|
||||
primary_device = self.network_service.primary_device
|
||||
wlan_state = "disconnected"
|
||||
|
||||
if primary_device == "wifi" and self.network_service.wifi_device:
|
||||
wifi = self.network_service.wifi_device
|
||||
if not wifi.enabled:
|
||||
wlan_state = "disabled"
|
||||
elif wifi.internet == "activated":
|
||||
wlan_state = f"connected:{wifi.ssid}"
|
||||
if wifi.strength >= 0:
|
||||
wlan_state += f":{wifi.strength}%"
|
||||
elif wifi.internet == "activating":
|
||||
wlan_state = f"connecting:{wifi.ssid}"
|
||||
else:
|
||||
wlan_state = "enabled"
|
||||
|
||||
elif primary_device == "wired" and self.network_service.ethernet_device:
|
||||
ethernet = self.network_service.ethernet_device
|
||||
if ethernet.internet == "activated":
|
||||
wlan_state = "ethernet:connected"
|
||||
if hasattr(ethernet, "speed") and ethernet.speed > 0:
|
||||
wlan_state += f":{ethernet.speed}Mbps"
|
||||
elif ethernet.internet == "activating":
|
||||
wlan_state = "ethernet:connecting"
|
||||
else:
|
||||
wlan_state = "ethernet:disconnected"
|
||||
|
||||
modus_service.wlan = wlan_state
|
||||
|
||||
def update_state(self):
|
||||
primary_device = self.network_service.primary_device
|
||||
tooltip = "No network connection"
|
||||
icon_file = "wifi-off.svg"
|
||||
|
||||
if primary_device == "wifi" and self.network_service.wifi_device:
|
||||
wifi = self.network_service.wifi_device
|
||||
if not wifi.enabled:
|
||||
icon_file = "wifi-off.svg"
|
||||
tooltip = "WiFi disabled"
|
||||
elif wifi.internet == "activated":
|
||||
icon_file = "wifi.svg"
|
||||
tooltip = f"Connected to {wifi.ssid}"
|
||||
if wifi.strength >= 0:
|
||||
tooltip += f" ({wifi.strength}%)"
|
||||
elif wifi.internet == "activating":
|
||||
icon_file = "wifi.svg"
|
||||
tooltip = f"Connecting to {wifi.ssid}..."
|
||||
else:
|
||||
icon_file = "wifi-off.svg"
|
||||
tooltip = "WiFi disconnected"
|
||||
|
||||
elif primary_device == "wired" and self.network_service.ethernet_device:
|
||||
ethernet = self.network_service.ethernet_device
|
||||
if ethernet.internet == "activated":
|
||||
icon_file = "ethernet.svg"
|
||||
tooltip = "Ethernet connected"
|
||||
if hasattr(ethernet, "speed") and ethernet.speed > 0:
|
||||
tooltip += f" ({ethernet.speed} Mbps)"
|
||||
elif ethernet.internet == "activating":
|
||||
icon_file = "wifi.svg"
|
||||
tooltip = "Ethernet connecting..."
|
||||
else:
|
||||
icon_file = "wifi-off.svg"
|
||||
tooltip = "Ethernet disconnected"
|
||||
|
||||
self.network_icon.set_from_file(
|
||||
get_relative_path(f"../../../config/assets/icons/{icon_file}")
|
||||
)
|
||||
self.network_button.set_tooltip_text(tooltip)
|
||||
|
||||
|
||||
class BatteryIndicator(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="battery-indicator", orientation="h", **kwargs)
|
||||
|
||||
self.battery_service = Battery()
|
||||
|
||||
self.battery_icon = Svg(
|
||||
name="battery-icon",
|
||||
size=25,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/battery.svg"),
|
||||
)
|
||||
|
||||
self.battery_button = Button(name="battery-button", child=self.battery_icon)
|
||||
|
||||
self.add(self.battery_button)
|
||||
modus_service.connect("battery-changed", self.on_battery_changed)
|
||||
self.battery_service.connect("changed", self.on_battery_direct_changed)
|
||||
|
||||
self.update_modus_service_battery_state()
|
||||
self.update_state()
|
||||
|
||||
def on_battery_changed(self, service, new_battery_state):
|
||||
self.update_state()
|
||||
|
||||
def on_battery_direct_changed(self, *args):
|
||||
self.update_modus_service_battery_state()
|
||||
self.update_state()
|
||||
|
||||
def update_modus_service_battery_state(self):
|
||||
if not self.battery_service.is_present:
|
||||
battery_state = "not_present"
|
||||
else:
|
||||
percentage = self.battery_service.percentage
|
||||
state = self.battery_service.state.lower()
|
||||
|
||||
battery_state = f"{state}:{percentage}%"
|
||||
|
||||
if state == "discharging":
|
||||
time_to_empty = self.battery_service.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
|
||||
if time_to_full != "N/A":
|
||||
battery_state += f":{time_to_full}"
|
||||
|
||||
modus_service.battery = battery_state
|
||||
|
||||
def get_battery_icon_level(self, percentage):
|
||||
if percentage >= 90:
|
||||
return "100"
|
||||
elif percentage >= 80:
|
||||
return "090"
|
||||
elif percentage >= 70:
|
||||
return "080"
|
||||
elif percentage >= 60:
|
||||
return "070"
|
||||
elif percentage >= 50:
|
||||
return "060"
|
||||
elif percentage >= 40:
|
||||
return "050"
|
||||
elif percentage >= 30:
|
||||
return "040"
|
||||
elif percentage >= 20:
|
||||
return "030"
|
||||
elif percentage >= 10:
|
||||
return "020"
|
||||
else:
|
||||
return "010"
|
||||
|
||||
def get_battery_icon_file(self, percentage, is_charging):
|
||||
level = self.get_battery_icon_level(percentage)
|
||||
suffix = "-charging" if is_charging else ""
|
||||
return f"battery/battery-{level}{suffix}.svg"
|
||||
|
||||
def get_battery_tooltip(self, percentage, state):
|
||||
tooltip = f"Battery: {percentage}%"
|
||||
|
||||
if state == "CHARGING":
|
||||
time_to_full = self._battery.time_to_full
|
||||
if time_to_full:
|
||||
tooltip_points.append(f"• Time until full: {time_to_full}")
|
||||
tooltip += " (Charging)"
|
||||
time_to_full = self.battery_service.time_to_full
|
||||
if time_to_full != "N/A":
|
||||
tooltip += f" - {time_to_full} until full"
|
||||
elif state == "DISCHARGING":
|
||||
time_to_empty = self._battery.time_to_empty
|
||||
if time_to_empty:
|
||||
tooltip_points.append(f"• Time remaining: {time_to_empty}")
|
||||
time_to_empty = self.battery_service.time_to_empty
|
||||
if time_to_empty != "N/A":
|
||||
tooltip += f" - {time_to_empty} remaining"
|
||||
elif state == "FULLY_CHARGED":
|
||||
tooltip += " (Fully charged)"
|
||||
|
||||
return "\n".join(tooltip_points)
|
||||
return tooltip
|
||||
|
||||
def update_battery(self, *_):
|
||||
if not self._battery.is_present:
|
||||
self.set_visible(False)
|
||||
return True
|
||||
|
||||
percentage = self._battery.percentage
|
||||
state = self._battery.state
|
||||
charging = state in ["CHARGING", "FULLY_CHARGED"]
|
||||
|
||||
if percentage <= 15 and not charging:
|
||||
self.icon.add_style_class("alert")
|
||||
def update_state(self):
|
||||
if not self.battery_service.is_present:
|
||||
icon_file = "battery.svg"
|
||||
tooltip = "No battery detected"
|
||||
else:
|
||||
self.icon.remove_style_class("alert")
|
||||
percentage = self.battery_service.percentage
|
||||
state = self.battery_service.state
|
||||
is_charging = state in ["CHARGING", "FULLY_CHARGED"]
|
||||
|
||||
if state == "FULLY_CHARGED" or (percentage >= 100 and state == "CHARGING"):
|
||||
self.icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/battery/battery-100-charging.svg"
|
||||
)
|
||||
)
|
||||
elif state == "CHARGING":
|
||||
# Calculate battery level and use corresponding charging icon
|
||||
level = min(100, max(0, int(percentage // 10) * 10))
|
||||
if level == 0 and percentage > 0:
|
||||
level = 10
|
||||
icon_name = f"battery-{level:03d}-charging.svg"
|
||||
self.icon.set_from_file(
|
||||
get_relative_path(f"../../../config/assets/icons/battery/{icon_name}")
|
||||
)
|
||||
else:
|
||||
# Calculate battery level and use corresponding icon
|
||||
level = min(100, max(0, int(percentage // 10) * 10))
|
||||
if level == 0 and percentage > 0:
|
||||
level = 10
|
||||
icon_name = f"battery-{level:03d}.svg"
|
||||
self.icon.set_from_file(
|
||||
get_relative_path(f"../../../config/assets/icons/battery/{icon_name}")
|
||||
)
|
||||
icon_file = self.get_battery_icon_file(percentage, is_charging)
|
||||
tooltip = self.get_battery_tooltip(percentage, state)
|
||||
|
||||
self.set_visible(True)
|
||||
return True
|
||||
|
||||
|
||||
class Indicators(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="indicators",
|
||||
orientation="h",
|
||||
spacing=10,
|
||||
children=[
|
||||
Battery(),
|
||||
WifiIndicator(),
|
||||
BluetoothIndicator(),
|
||||
],
|
||||
**kwargs,
|
||||
# Update icon and tooltip
|
||||
self.battery_icon.set_from_file(
|
||||
get_relative_path(f"../../../config/assets/icons/{icon_file}")
|
||||
)
|
||||
self.show_all()
|
||||
self.battery_button.set_tooltip_text(tooltip)
|
||||
@@ -1,93 +0,0 @@
|
||||
"""
|
||||
Menu action handlers for the macOS-style menu bar.
|
||||
This file contains all the implementations for menu item actions.
|
||||
"""
|
||||
|
||||
from fabric.hyprland.service import Hyprland
|
||||
from fabric.utils import exec_shell_command
|
||||
|
||||
|
||||
class MenuActionHandler:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def execute_action(self, action):
|
||||
try:
|
||||
# Window menu actions
|
||||
if action == "Move Window to Left":
|
||||
Hyprland.send_command("dispatch movewindow l")
|
||||
elif action == "Move Window to Right":
|
||||
Hyprland.send_command("dispatch movewindow r")
|
||||
elif action == "Cycle Through Windows":
|
||||
Hyprland.send_command("dispatch cyclenext")
|
||||
elif action == "Float":
|
||||
Hyprland.send_command("dispatch togglefloating")
|
||||
elif action == "Pseudo":
|
||||
Hyprland.send_command("dispatch pseudo")
|
||||
elif action == "Center":
|
||||
Hyprland.send_command("dispatch centerwindow")
|
||||
elif action == "Group":
|
||||
Hyprland.send_command("dispatch togglegroup")
|
||||
elif action == "Pin":
|
||||
Hyprland.send_command("dispatch pin")
|
||||
elif action == "Quit":
|
||||
Hyprland.send_command("dispatch killactive")
|
||||
|
||||
# Go menu actions
|
||||
elif action == "Back":
|
||||
Hyprland.send_command("dispatch workspace e-1")
|
||||
elif action == "Forward":
|
||||
Hyprland.send_command("dispatch workspace e+1")
|
||||
|
||||
# Edit menu actions
|
||||
|
||||
# View menu actions
|
||||
elif action == "Full Screen":
|
||||
Hyprland.send_command("dispatch fullscreen")
|
||||
elif action == "Zoom In":
|
||||
exec_shell_command("wtype -k ctrl+plus")
|
||||
elif action == "Zoom Out":
|
||||
exec_shell_command("wtype -k ctrl+minus")
|
||||
elif action == "Actual Size":
|
||||
exec_shell_command("wtype -k ctrl+0")
|
||||
|
||||
# Help menu actions
|
||||
elif action == "Hyprland Help":
|
||||
exec_shell_command("xdg-open https://wiki.hyprland.org/")
|
||||
elif action == "Arch Wiki":
|
||||
exec_shell_command("xdg-open https://wiki.archlinux.org/")
|
||||
elif action == "Report a Bug...":
|
||||
exec_shell_command("xdg-open https://github.com/S4NKALP/Modus/issues")
|
||||
|
||||
# System menu actions
|
||||
elif action == "About This PC":
|
||||
exec_shell_command("gnome-system-monitor")
|
||||
elif action == "Force Quit":
|
||||
exec_shell_command("gnome-system-monitor")
|
||||
elif action == "Shutdown":
|
||||
exec_shell_command("systemctl poweroff")
|
||||
elif action == "Restart":
|
||||
exec_shell_command("systemctl reboot")
|
||||
elif action == "Sleep":
|
||||
exec_shell_command("systemctl suspend")
|
||||
elif action == "Lock":
|
||||
exec_shell_command("hyprlock")
|
||||
|
||||
# App menu actions (for hiding apps)
|
||||
elif action.startswith("Hide ") and not action == "Hide Others":
|
||||
action.replace("Hide ", "")
|
||||
Hyprland.send_command("dispatch movetoworkspacesilent special:hidden")
|
||||
elif action == "Hide Others":
|
||||
Hyprland.send_command(
|
||||
"dispatch movetoworkspacesilent special:hidden,^(activewindow)"
|
||||
)
|
||||
elif action == "Show All":
|
||||
Hyprland.send_command("dispatch movetoworkspace e+0,special:hidden")
|
||||
elif action.startswith("Quit "):
|
||||
action.replace("Quit ", "")
|
||||
Hyprland.send_command("dispatch killactive")
|
||||
else:
|
||||
print(f"No implementation for action: {action}")
|
||||
except Exception as e:
|
||||
print(f"Error executing command for '{action}': {e}")
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
Menu contents definitions for the macOS-style menu bar.
|
||||
This file contains all the menu items and their organization.
|
||||
"""
|
||||
|
||||
|
||||
def get_default_menu_contents():
|
||||
return {
|
||||
"Hyprland": [],
|
||||
"File": [],
|
||||
"Edit": [],
|
||||
"View": ["Full Screen", "---", "Zoom In", "Zoom Out", "Actual Size"],
|
||||
"Go": [
|
||||
"Back",
|
||||
"Forward",
|
||||
],
|
||||
"Window": [
|
||||
"Move Window to Left",
|
||||
"Move Window to Right",
|
||||
"Cycle Through Windows",
|
||||
"---",
|
||||
"Float",
|
||||
"Pseudo",
|
||||
"Center",
|
||||
"Group",
|
||||
"Pin",
|
||||
"Quit",
|
||||
],
|
||||
"Help": [
|
||||
"---",
|
||||
"Hyprland Help",
|
||||
"Arch Wiki",
|
||||
"Keyboard Shortcuts",
|
||||
"---",
|
||||
"Report a Bug...",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_app_menu_template():
|
||||
return ["About {app_name}", "---", "Hide {app_name}", "---", "Quit {app_name}"]
|
||||
|
||||
|
||||
def create_app_menu(app_name):
|
||||
template = get_app_menu_template()
|
||||
return [
|
||||
item.format(app_name=app_name) if "{app_name}" in item else item
|
||||
for item in template
|
||||
]
|
||||
|
||||
|
||||
def get_menu_contents_for_app(app_name):
|
||||
contents = get_default_menu_contents().copy()
|
||||
|
||||
if app_name and app_name != "Hyprland":
|
||||
# Replace the Hyprland menu with app-specific menu
|
||||
contents["Hyprland"] = create_app_menu(app_name)
|
||||
|
||||
return contents
|
||||
|
||||
|
||||
def get_app_specific_menu_contents(app_name):
|
||||
return get_menu_contents_for_app(app_name)
|
||||
+347
-339
@@ -1,364 +1,372 @@
|
||||
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 fabric.hyprland.widgets import HyprlandActiveWindow
|
||||
from utils.wayland import WaylandWindow
|
||||
from gi.repository import Gtk, GLib
|
||||
from .menu_contents import get_default_menu_contents, get_app_specific_menu_contents
|
||||
from .menu_actions import MenuActionHandler
|
||||
|
||||
from modules.about import About
|
||||
from utils.roam import modus_service
|
||||
from widgets.dropdown import ModusDropdown, dropdown_divider
|
||||
from widgets.mousecapture import DropDownMouseCapture
|
||||
|
||||
|
||||
class DropdownWindow(WaylandWindow):
|
||||
def __init__(self, x=0, y=0, parent_menubar=None, **kwargs):
|
||||
class AppName:
|
||||
def __init__(self, path="/usr/share/applications"):
|
||||
self.files = os.listdir(path) if os.path.exists(path) else []
|
||||
self.path = path
|
||||
|
||||
def get_app_name(self, wmclass):
|
||||
desktop_file = ""
|
||||
for f in self.files:
|
||||
if f.startswith(wmclass + ".desktop"):
|
||||
desktop_file = f
|
||||
|
||||
desktop_app_name = wmclass
|
||||
|
||||
if desktop_file == "":
|
||||
return wmclass
|
||||
try:
|
||||
with open(os.path.join(self.path, desktop_file), "r") as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if line.startswith("Name="):
|
||||
desktop_app_name = line.split("=")[1].strip()
|
||||
break
|
||||
except:
|
||||
return wmclass
|
||||
return desktop_app_name
|
||||
|
||||
def format_app_name(self, title, wmclass, update=False):
|
||||
name = wmclass
|
||||
if name == "":
|
||||
name = title
|
||||
|
||||
# Try to get the proper app name from desktop file
|
||||
name = self.get_app_name(wmclass=wmclass)
|
||||
|
||||
# Smart title formatting (capitalize first letter)
|
||||
name = str(name).title()
|
||||
if "." in name:
|
||||
name = name.split(".")[-1]
|
||||
|
||||
if update:
|
||||
modus_service.current_active_app_name = name
|
||||
return name
|
||||
|
||||
|
||||
app_name_class = AppName()
|
||||
|
||||
|
||||
def format_window(title, wmclass):
|
||||
name = app_name_class.format_app_name(title, wmclass, True)
|
||||
if not name or name == "":
|
||||
return "Desktop"
|
||||
return name
|
||||
|
||||
|
||||
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__(
|
||||
name="dropdown-window",
|
||||
title="Menu Dropdown",
|
||||
layer="overlay",
|
||||
anchor="top left",
|
||||
exclusivity="none",
|
||||
margin=f"{y}px 0px 0px {x}px",
|
||||
visible=False,
|
||||
all_visible=True,
|
||||
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,
|
||||
)
|
||||
|
||||
# Store reference to parent menubar for closing dropdown
|
||||
self.parent_menubar = parent_menubar
|
||||
|
||||
self.content_box = Box(
|
||||
name="dropdown-content",
|
||||
orientation="v",
|
||||
spacing=2,
|
||||
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.add(self.content_box)
|
||||
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)
|
||||
|
||||
self.connect("enter-notify-event", self.on_mouse_enter)
|
||||
self.connect("leave-notify-event", self.on_mouse_leave)
|
||||
# Global menu dropdowns
|
||||
self.global_title_menu_about = dropdown_option(
|
||||
f"About {modus_service.current_active_app_name}"
|
||||
)
|
||||
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.auto_close_timer = None
|
||||
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",
|
||||
on_clicked=lambda _: subprocess.run(
|
||||
"bash ~/.config/scripts/zoomer.sh", shell=True
|
||||
),
|
||||
),
|
||||
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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def on_mouse_enter(self, widget, event):
|
||||
# Only handle if the event is for the main dropdown window, not child widgets
|
||||
if event.detail != 2: # 2 = Gdk.NotifyType.INFERIOR (child widget)
|
||||
# Cancel auto-close timer when mouse enters
|
||||
if self.auto_close_timer:
|
||||
GLib.source_remove(self.auto_close_timer)
|
||||
self.auto_close_timer = None
|
||||
return False
|
||||
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/"
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def on_mouse_leave(self, widget, event):
|
||||
# Only handle if actually leaving the dropdown window, not entering child widgets
|
||||
if event.detail != 2: # 2 = Gdk.NotifyType.INFERIOR (child widget)
|
||||
# Start auto-close timer when mouse leaves
|
||||
if not self.auto_close_timer:
|
||||
self.auto_close_timer = GLib.timeout_add(
|
||||
300, self.auto_close_callback
|
||||
) # 300ms delay
|
||||
return False
|
||||
# Create menu buttons
|
||||
modus_service.connect(
|
||||
"current-active-app-name-changed",
|
||||
lambda _, value: self.global_title_menu_about.set_property(
|
||||
"label", f"About {value}"
|
||||
),
|
||||
)
|
||||
self.global_menu_button_title = Button(
|
||||
child=ActiveWindow(
|
||||
formatter=FormattedString(
|
||||
"{ format_window('None', 'Hyprland') if win_title == '' and win_class == '' else format_window(win_title, win_class) }",
|
||||
format_window=format_window,
|
||||
)
|
||||
),
|
||||
name="global-title-button",
|
||||
style_classes="button",
|
||||
on_clicked=lambda _: self.global_menu_title.toggle_mousecapture(),
|
||||
)
|
||||
self.global_menu_title.child_window.set_pointing_to(
|
||||
self.global_menu_button_title
|
||||
)
|
||||
|
||||
def auto_close_callback(self):
|
||||
if self.parent_menubar:
|
||||
self.parent_menubar.hide_dropdown()
|
||||
self.auto_close_timer = None
|
||||
return False
|
||||
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 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):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="menubar",
|
||||
orientation="h",
|
||||
spacing=0,
|
||||
**kwargs,
|
||||
)
|
||||
"""Main MenuBar widget that contains all menu buttons"""
|
||||
|
||||
self.active_window = HyprlandActiveWindow(name="hyprland-window")
|
||||
self.active_window.connect("window-activated", self.on_window_changed)
|
||||
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)
|
||||
|
||||
self.current_window_class = None
|
||||
self.current_window_title = None
|
||||
super().__init__(name="menubar", orientation="horizontal", spacing=0, **kwargs)
|
||||
|
||||
self.current_dropdown = None
|
||||
self.dropdown_window = None
|
||||
# Create the dropdown system
|
||||
self.dropdown_system = MenuBarDropdowns(parent=parent_window)
|
||||
|
||||
self.action_handler = MenuActionHandler()
|
||||
|
||||
# Menu items in macOS style order (excluding the first item which will be dynamic)
|
||||
self.static_menu_items = ["File", "Edit", "View", "Go", "Window", "Help"]
|
||||
|
||||
self.menu_contents = get_default_menu_contents()
|
||||
|
||||
self.app_button = Button(
|
||||
name="menubar-button",
|
||||
child=Label(name="activewindow-label", label="Hyprland"),
|
||||
)
|
||||
self.app_button.connect(
|
||||
"clicked", lambda widget: self.toggle_dropdown("Hyprland", widget)
|
||||
)
|
||||
self.add(self.app_button)
|
||||
|
||||
self.menu_buttons = []
|
||||
for item in self.static_menu_items:
|
||||
button = Button(
|
||||
name="menubar-button",
|
||||
child=Label(name="menubar-label", label=item),
|
||||
)
|
||||
|
||||
button.connect(
|
||||
"clicked",
|
||||
lambda widget, menu_item=item: self.on_menu_button_clicked(
|
||||
menu_item, widget
|
||||
),
|
||||
)
|
||||
self.menu_buttons.append(button)
|
||||
self.add(button)
|
||||
|
||||
self.update_menu_button_states()
|
||||
|
||||
self.show_all()
|
||||
|
||||
def get_button_for_item(self, menu_item):
|
||||
if menu_item == "Hyprland":
|
||||
return self.app_button
|
||||
|
||||
for i, item in enumerate(self.static_menu_items):
|
||||
if item == menu_item:
|
||||
return self.menu_buttons[i]
|
||||
return None
|
||||
|
||||
def on_menu_button_clicked(self, menu_item, button):
|
||||
# Check if we should allow this menu to be opened
|
||||
if not self.should_menu_be_active(menu_item):
|
||||
return
|
||||
|
||||
# Menu is allowed, proceed with normal toggle
|
||||
self.toggle_dropdown(menu_item, button)
|
||||
|
||||
def should_menu_be_active(self, menu_item):
|
||||
# These menus should only work when there's an active window
|
||||
context_dependent_menus = ["File", "Edit", "View", "Go"]
|
||||
|
||||
if menu_item in context_dependent_menus:
|
||||
# Only allow if there's an active window (not just showing "Hyprland")
|
||||
has_active_window = (
|
||||
self.current_window_title
|
||||
and self.current_window_title.strip()
|
||||
and self.app_button.get_child().get_label() != "Hyprland"
|
||||
)
|
||||
return has_active_window
|
||||
|
||||
# Window and Help menus are always available
|
||||
return True
|
||||
|
||||
def update_menu_button_states(self):
|
||||
context_dependent_menus = ["File", "Edit", "View", "Go"]
|
||||
|
||||
for i, menu_item in enumerate(self.static_menu_items):
|
||||
if menu_item in context_dependent_menus:
|
||||
button = self.menu_buttons[i]
|
||||
is_active = self.should_menu_be_active(menu_item)
|
||||
|
||||
# Keep buttons sensitive for hovering, but add visual styling for inactive state
|
||||
# Don't use set_sensitive(False) as it prevents hovering
|
||||
if is_active:
|
||||
button.get_style_context().remove_class("inactive")
|
||||
else:
|
||||
button.get_style_context().add_class("inactive")
|
||||
|
||||
def toggle_dropdown(self, menu_item, button):
|
||||
# If same menu is clicked and dropdown is visible, hide it
|
||||
if (
|
||||
self.current_dropdown == menu_item
|
||||
and self.dropdown_window
|
||||
and self.dropdown_window.get_visible()
|
||||
):
|
||||
self.hide_dropdown()
|
||||
return
|
||||
|
||||
# (show_dropdown will handle hiding the previous one)
|
||||
self.show_dropdown(menu_item, button)
|
||||
|
||||
def show_dropdown(self, menu_item, button):
|
||||
# Hide existing dropdown if any and wait for it to be completely hidden
|
||||
if self.dropdown_window:
|
||||
self.dropdown_window.set_visible(False)
|
||||
self.dropdown_window.destroy()
|
||||
self.dropdown_window = None
|
||||
# Wait a short moment to ensure the previous dropdown is completely gone
|
||||
GLib.timeout_add(150, self._create_and_show_dropdown, menu_item, button)
|
||||
else:
|
||||
# No existing dropdown, show immediately
|
||||
self._create_and_show_dropdown(menu_item, button)
|
||||
|
||||
def _create_and_show_dropdown(self, menu_item, button):
|
||||
# Calculate position first
|
||||
x, y = self.calculate_dropdown_position(button)
|
||||
|
||||
# Create new dropdown window at the calculated position
|
||||
self.dropdown_window = DropdownWindow(x=x, y=y, parent_menubar=self)
|
||||
|
||||
# Get menu items for this menu
|
||||
menu_items = self.menu_contents.get(menu_item, [])
|
||||
|
||||
# Add menu items to dropdown
|
||||
for item in menu_items:
|
||||
if item == "---":
|
||||
# Add separator
|
||||
separator = Box(
|
||||
name="menu-separator",
|
||||
size=(150, 1),
|
||||
style="background-color: #444; margin: 2px 0;",
|
||||
)
|
||||
self.dropdown_window.content_box.add(separator)
|
||||
else:
|
||||
# Add menu item button
|
||||
menu_button = Button(
|
||||
name="dropdown-item",
|
||||
child=Label(
|
||||
name="dropdown-label",
|
||||
label=item,
|
||||
h_align="start", # Align text to the left
|
||||
),
|
||||
on_clicked=lambda *_, action=item: self.on_menu_action(action),
|
||||
)
|
||||
self.dropdown_window.content_box.add(menu_button)
|
||||
|
||||
self.dropdown_window.set_size_request(150, -1)
|
||||
|
||||
self.current_dropdown = menu_item
|
||||
self.dropdown_window.set_visible(True)
|
||||
|
||||
return False
|
||||
|
||||
def calculate_dropdown_position(self, button):
|
||||
# Get button allocation
|
||||
allocation = button.get_allocation()
|
||||
|
||||
print(
|
||||
f"Button allocation: x={allocation.x}, y={allocation.y}, width={allocation.width}, height={allocation.height}"
|
||||
)
|
||||
|
||||
# Use the exact button position for perfect alignment
|
||||
dropdown_x = allocation.x
|
||||
dropdown_y = allocation.y + 3 # 3px gap below button
|
||||
|
||||
print(f"Perfect dropdown position: x={dropdown_x}, y={dropdown_y}")
|
||||
|
||||
return dropdown_x, dropdown_y
|
||||
|
||||
def hide_dropdown(self):
|
||||
if self.dropdown_window:
|
||||
self.dropdown_window.set_visible(False)
|
||||
self.dropdown_window.destroy()
|
||||
self.dropdown_window = None
|
||||
self.current_dropdown = None
|
||||
|
||||
def on_menu_action(self, action):
|
||||
# Hide dropdown after action
|
||||
self.hide_dropdown()
|
||||
|
||||
# Handle window management actions
|
||||
self.execute_menu_action(action)
|
||||
|
||||
def execute_menu_action(self, action):
|
||||
self.action_handler.execute_action(action)
|
||||
|
||||
def on_window_changed(self, widget, window_class, window_title):
|
||||
self.current_window_class = window_class
|
||||
self.current_window_title = window_title
|
||||
|
||||
# Update the first button label and menu contents
|
||||
if window_title and window_title.strip():
|
||||
# Show the window title/app name when there's an active window
|
||||
display_name = window_class if window_class else window_title
|
||||
# Capitalize the first letter
|
||||
display_name = display_name.capitalize() if display_name else "Hyprland"
|
||||
self.app_button.get_child().set_label(display_name)
|
||||
|
||||
# Update the app menu contents dynamically
|
||||
self.update_app_menu(display_name)
|
||||
else:
|
||||
# Show "Hyprland" when no active window
|
||||
self.app_button.get_child().set_label("Hyprland")
|
||||
self.update_app_menu("Hyprland")
|
||||
|
||||
# Update menu button states based on new context
|
||||
self.update_menu_button_states()
|
||||
|
||||
def update_app_menu(self, app_name):
|
||||
# Get app-specific menu contents (includes app-specific overrides if available)
|
||||
self.menu_contents = get_app_specific_menu_contents(app_name)
|
||||
|
||||
def show_system_dropdown(self, button):
|
||||
# If same system dropdown is open, hide it (toggle behavior)
|
||||
if (
|
||||
self.current_dropdown == "System"
|
||||
and self.dropdown_window
|
||||
and self.dropdown_window.get_visible()
|
||||
):
|
||||
self.hide_dropdown()
|
||||
return
|
||||
|
||||
# Hide existing dropdown if any and show system dropdown
|
||||
if self.dropdown_window:
|
||||
self.dropdown_window.set_visible(False)
|
||||
self.dropdown_window.destroy()
|
||||
self.dropdown_window = None
|
||||
# Wait a short moment to ensure the previous dropdown is completely gone
|
||||
GLib.timeout_add(150, self._create_and_show_system_dropdown, button)
|
||||
else:
|
||||
# No existing dropdown, show immediately
|
||||
self._create_and_show_system_dropdown(button)
|
||||
|
||||
def _create_and_show_system_dropdown(self, button):
|
||||
# Calculate position first
|
||||
x, y = self.calculate_dropdown_position(button)
|
||||
|
||||
# Create new dropdown window at the calculated position
|
||||
self.dropdown_window = DropdownWindow(x=x, y=y, parent_menubar=self)
|
||||
|
||||
# System menu items
|
||||
system_menu_items = [
|
||||
"About This PC",
|
||||
"---",
|
||||
"Force Quit",
|
||||
"---",
|
||||
"Shutdown",
|
||||
"Restart",
|
||||
"Sleep",
|
||||
"Lock",
|
||||
# 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,
|
||||
]
|
||||
|
||||
# Add menu items to dropdown
|
||||
for item in system_menu_items:
|
||||
if item == "---":
|
||||
# Add separator
|
||||
separator = Box(
|
||||
name="menu-separator",
|
||||
size=(150, 1),
|
||||
style="background-color: #444; margin: 2px 0;",
|
||||
)
|
||||
self.dropdown_window.content_box.add(separator)
|
||||
else:
|
||||
# Add menu item button
|
||||
menu_button = Button(
|
||||
name="dropdown-item",
|
||||
child=Label(
|
||||
name="dropdown-label",
|
||||
label=item,
|
||||
h_align="start", # Align text to the left
|
||||
),
|
||||
on_clicked=lambda *_, action=item: self.on_menu_action(action),
|
||||
)
|
||||
self.dropdown_window.content_box.add(menu_button)
|
||||
|
||||
# Set dropdown window size
|
||||
self.dropdown_window.set_size_request(150, -1)
|
||||
|
||||
# Show the dropdown without animations
|
||||
self.current_dropdown = "System"
|
||||
# Use set_visible(True) for immediate display without animations
|
||||
self.dropdown_window.set_visible(True)
|
||||
|
||||
# Return False to stop the timeout
|
||||
return False
|
||||
def show_system_dropdown(self, _):
|
||||
"""Show the system dropdown menu"""
|
||||
self.dropdown_system.menu_button_dropdown.toggle_mousecapture()
|
||||
@@ -9,6 +9,8 @@ from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.svg import Svg
|
||||
|
||||
# FIX: Make the timer consistent with the recording start time
|
||||
|
||||
|
||||
class RecordingIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -26,14 +28,14 @@ class RecordingIndicator(Button):
|
||||
name="recording-time-label",
|
||||
markup="00:00",
|
||||
max_width_chars=5,
|
||||
ellipsize="none"
|
||||
ellipsize="none",
|
||||
)
|
||||
|
||||
self.recording_box = Box(
|
||||
orientation="h",
|
||||
spacing=2,
|
||||
children=[self.recording_icon, self.time_label],
|
||||
size=(80, -1),
|
||||
size=(80, -1),
|
||||
)
|
||||
|
||||
self.add(self.recording_box)
|
||||
|
||||
Reference in new issue
Block a user