363 lines
13 KiB
Python
363 lines
13 KiB
Python
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
|