✨ feat: implement core functionality and UI for Modus shell
This commit is contained in:
1 parent
5789013e2e
commit
acec6bdda7
129 files changed
+33424
-226
No files matched your search
@@ -0,0 +1,295 @@
|
||||
from fabric.bluetooth import BluetoothClient
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.svg import Svg
|
||||
|
||||
import utils.icons as icons
|
||||
from services.network import NetworkClient
|
||||
from gi.repository import GLib
|
||||
from services.battery import Battery as BatteryService
|
||||
|
||||
|
||||
class WifiIndicator(Button):
|
||||
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)
|
||||
|
||||
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.bluetooth.connect("device-added", self.on_device_added)
|
||||
self.bluetooth.connect("device-removed", self.on_device_removed)
|
||||
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
if not self.bluetooth.enabled:
|
||||
self.bt_icon.set_from_file(get_relative_path("../../../config/assets/icons/bluetooth-off.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/bluetooth.svg"))
|
||||
if len(connected_devices) == 1:
|
||||
device = connected_devices[0]
|
||||
tooltip = f"Connected to {device.alias}"
|
||||
if device.battery_percentage > 0:
|
||||
tooltip += f" ({device.battery_percentage:.0f}%)"
|
||||
else:
|
||||
tooltip = f"Connected to {len(connected_devices)} devices"
|
||||
else:
|
||||
self.bt_icon.set_from_file(get_relative_path("../../../config/assets/icons/bluetooth.svg"))
|
||||
tooltip = "No devices connected"
|
||||
|
||||
self.set_tooltip_text(tooltip)
|
||||
|
||||
def on_bluetooth_changed(self, *args):
|
||||
self.update_state()
|
||||
|
||||
def on_device_added(self, _, address):
|
||||
self.update_state()
|
||||
|
||||
def on_device_removed(self, _, address):
|
||||
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"
|
||||
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
|
||||
else:
|
||||
profile_icon = icons.power_balanced
|
||||
tooltip_points.append(f"• Profile: {profile_icon} {power_profile}")
|
||||
|
||||
temp = self._battery.temperature
|
||||
if temp != "N/A":
|
||||
tooltip_points.append(f"• Temperature: {temp}")
|
||||
|
||||
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}")
|
||||
elif state == "DISCHARGING":
|
||||
time_to_empty = self._battery.time_to_empty
|
||||
if time_to_empty:
|
||||
tooltip_points.append(f"• Time remaining: {time_to_empty}")
|
||||
|
||||
return "\n".join(tooltip_points)
|
||||
|
||||
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")
|
||||
else:
|
||||
self.icon.remove_style_class("alert")
|
||||
|
||||
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}"))
|
||||
|
||||
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,
|
||||
)
|
||||
self.show_all()
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
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}")
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,362 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
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
|
||||
|
||||
|
||||
class DropdownWindow(WaylandWindow):
|
||||
def __init__(self, x=0, y=0, parent_menubar=None, **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,
|
||||
**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,
|
||||
)
|
||||
self.add(self.content_box)
|
||||
|
||||
self.connect("enter-notify-event", self.on_mouse_enter)
|
||||
self.connect("leave-notify-event", self.on_mouse_leave)
|
||||
|
||||
self.auto_close_timer = None
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def auto_close_callback(self):
|
||||
if self.parent_menubar:
|
||||
self.parent_menubar.hide_dropdown()
|
||||
self.auto_close_timer = None
|
||||
return False
|
||||
|
||||
|
||||
class MenuBar(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="menubar",
|
||||
orientation="h",
|
||||
spacing=0,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.active_window = HyprlandActiveWindow(name="hyprland-window")
|
||||
self.active_window.connect("window-activated", self.on_window_changed)
|
||||
|
||||
self.current_window_class = None
|
||||
self.current_window_title = None
|
||||
|
||||
self.current_dropdown = None
|
||||
self.dropdown_window = None
|
||||
|
||||
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 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
|
||||
Reference in new issue
Block a user