feat(src/window):restructure dock UI and application switcher using config‑driven layout
This commit is contained in:
3 files changed
+647
-90
No files matched your search
+99
-58
@@ -20,7 +20,7 @@ from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
import shared.data as data
|
||||
from services.config import config, on_config_change
|
||||
from services.modus import modus_service
|
||||
from utils.functions import (
|
||||
clear_children,
|
||||
@@ -44,15 +44,10 @@ class AppBar(Box):
|
||||
self.pinned_items_pos = []
|
||||
self._parent = parent
|
||||
|
||||
# Set orientation based on dock position
|
||||
orientation = (
|
||||
"vertical" if data.DOCK_POSITION in ["Left", "Right"] else "horizontal"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
spacing=0,
|
||||
name="dock",
|
||||
orientation=orientation,
|
||||
orientation="horizontal",
|
||||
children=[],
|
||||
)
|
||||
self.icon_resolver = IconResolver()
|
||||
@@ -87,6 +82,39 @@ class AppBar(Box):
|
||||
self._app_monitor_timer_id = GLib.timeout_add(250, update_running_apps)
|
||||
GLib.idle_add(self.update_dock_apps)
|
||||
|
||||
def update_icon_size(self):
|
||||
"""Update all icons in the dock to the current config size"""
|
||||
desktop_apps = get_desktop_applications(include_hidden=False)
|
||||
|
||||
# Update pinned apps
|
||||
for app_id, button in self.pinned_buttons.items():
|
||||
if hasattr(button, "icon_image"):
|
||||
if app_id == "trash" or getattr(button, "is_trash", False):
|
||||
icon_size = config().get("dock_icon_size", 52)
|
||||
pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size)
|
||||
button.icon_image.set_from_pixbuf(pixbuf)
|
||||
else:
|
||||
# Find app data to get icon
|
||||
app_data = next(
|
||||
(
|
||||
a
|
||||
for a in self.pinned_apps
|
||||
if self._matches_app_identifier(a, app_id)
|
||||
),
|
||||
app_id,
|
||||
)
|
||||
app = self._find_desktop_app(app_id, desktop_apps)
|
||||
pixbuf = self._get_app_icon(app_data, app)
|
||||
button.icon_image.set_from_pixbuf(pixbuf)
|
||||
|
||||
# Update running apps
|
||||
for addr, button in self.client_buttons.items():
|
||||
if hasattr(button, "icon_image"):
|
||||
app_class = getattr(button, "app_class", "")
|
||||
app = self._find_desktop_app(app_class, desktop_apps)
|
||||
pixbuf = self._get_app_icon(app_class, app)
|
||||
button.icon_image.set_from_pixbuf(pixbuf)
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up timers and children"""
|
||||
if hasattr(self, "_app_monitor_timer_id") and self._app_monitor_timer_id:
|
||||
@@ -131,10 +159,11 @@ class AppBar(Box):
|
||||
|
||||
icon_pixbuf = self._get_app_icon(app_data, app)
|
||||
|
||||
icon_image = Image(name="dock_item_icon", pixbuf=icon_pixbuf)
|
||||
main_container = Box(
|
||||
name="dock_item_main_container",
|
||||
orientation="v",
|
||||
children=[Image(name="dock_item_icon", pixbuf=icon_pixbuf)],
|
||||
children=[icon_image],
|
||||
)
|
||||
|
||||
pinned_button = Button(
|
||||
@@ -153,14 +182,16 @@ class AppBar(Box):
|
||||
)
|
||||
|
||||
pinned_button.add_style_class("shown")
|
||||
pinned_button.icon_image = icon_image
|
||||
|
||||
self.pinned_buttons[app_identifier] = pinned_button
|
||||
self.pinned_apps_container.add(pinned_button)
|
||||
self.pinned_items_pos.append(pinned_button)
|
||||
|
||||
def _get_app_icon(self, app_data, app=None):
|
||||
icon_size = config().get("dock_icon_size", 52)
|
||||
if app:
|
||||
return app.get_icon_pixbuf(data.DOCK_ICON_SIZE)
|
||||
return app.get_icon_pixbuf(icon_size)
|
||||
|
||||
icon_name = ""
|
||||
if isinstance(app_data, dict):
|
||||
@@ -169,19 +200,19 @@ class AppBar(Box):
|
||||
icon_name = app_data
|
||||
|
||||
return self.icon_resolver.get_icon_pixbuf(
|
||||
icon_name or "application-x-executable", data.DOCK_ICON_SIZE
|
||||
icon_name or "application-x-executable", icon_size
|
||||
)
|
||||
|
||||
def _create_trash_button(self):
|
||||
"""Create a trash button that opens the trash in file manager"""
|
||||
trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(
|
||||
"user-trash", data.DOCK_ICON_SIZE
|
||||
)
|
||||
icon_size = config().get("dock_icon_size", 52)
|
||||
trash_icon_pixbuf = self.icon_resolver.get_icon_pixbuf("user-trash", icon_size)
|
||||
|
||||
trash_icon_image = Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf)
|
||||
main_container = Box(
|
||||
name="dock_item_main_container",
|
||||
orientation="v",
|
||||
children=[Image(name="dock_item_icon", pixbuf=trash_icon_pixbuf)],
|
||||
children=[trash_icon_image],
|
||||
)
|
||||
|
||||
trash_button = Button(
|
||||
@@ -199,6 +230,7 @@ class AppBar(Box):
|
||||
|
||||
trash_button.add_style_class("shown")
|
||||
trash_button.is_trash = True
|
||||
trash_button.icon_image = trash_icon_image
|
||||
|
||||
self.pinned_buttons["trash"] = trash_button
|
||||
self.pinned_apps_container.add(trash_button)
|
||||
@@ -646,6 +678,7 @@ class AppBar(Box):
|
||||
client_button.client_data = client
|
||||
client_button.app_class = app_class
|
||||
client_button.workspace_label = workspace_label
|
||||
client_button.icon_image = client_image
|
||||
client_button.add_style_class("shown")
|
||||
|
||||
self.client_buttons[instance_address] = client_button
|
||||
@@ -666,7 +699,7 @@ class AppBar(Box):
|
||||
return None
|
||||
|
||||
def _should_show_app_instance(self, client):
|
||||
if not data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS:
|
||||
if not config().get("dock_hide_special_workspace_apps", True):
|
||||
return True
|
||||
|
||||
workspace_id = self._get_workspace_id(client)
|
||||
@@ -755,19 +788,13 @@ class AppBar(Box):
|
||||
|
||||
class Dock(Window):
|
||||
def __init__(self):
|
||||
dock_config = self._get_dock_config()
|
||||
if not data.DOCK_ENABLED:
|
||||
super().__init__(layer="top", title="dock", anchor=dock_config["anchor"])
|
||||
self.children = Box() # Empty dock if disabled
|
||||
return
|
||||
|
||||
super().__init__(layer="top", anchor=dock_config["anchor"])
|
||||
super().__init__(layer="top", anchor="bottom center", title="modus-dock")
|
||||
|
||||
self.app_bar = AppBar(self)
|
||||
self.revealer = Revealer(
|
||||
child=Box(children=[self.app_bar], style="padding: 20px 50px 5px 50px;"),
|
||||
transition_duration=200,
|
||||
transition_type=dock_config["transition"],
|
||||
transition_type="slide-up",
|
||||
)
|
||||
|
||||
self.children = EventBox(
|
||||
@@ -777,17 +804,51 @@ class Dock(Window):
|
||||
on_leave_notify_event=lambda *_: self.on_hover_leave(),
|
||||
)
|
||||
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.app_bar.add_style_class("shown")
|
||||
self._update_visibility()
|
||||
|
||||
self.dock_height = 100
|
||||
self.is_hovered = False
|
||||
self.hide_ticket = 0
|
||||
|
||||
# Only setup occlusion monitoring if auto-hide is enabled
|
||||
if data.DOCK_AUTO_HIDE:
|
||||
# Subscribe to config changes for hot-reloading
|
||||
on_config_change(self._on_config_change)
|
||||
|
||||
if config().get("dock_auto_hide", True):
|
||||
self.setup_occlusion_monitoring()
|
||||
|
||||
def _on_config_change(self, new_config, old_config):
|
||||
# Handle dock visibility
|
||||
if config().has_changed("dock_enabled", old_config):
|
||||
self._update_visibility()
|
||||
|
||||
# Handle icon size changes
|
||||
if config().has_changed("dock_icon_size", old_config):
|
||||
self.app_bar.update_icon_size()
|
||||
|
||||
# Handle auto-hide changes
|
||||
if config().has_changed("dock_auto_hide", old_config):
|
||||
if new_config.get("dock_auto_hide", True):
|
||||
self.setup_occlusion_monitoring()
|
||||
else:
|
||||
if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id:
|
||||
GLib.source_remove(self._occlusion_timer_id)
|
||||
self._occlusion_timer_id = None
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.app_bar.add_style_class("shown")
|
||||
|
||||
# Handle special workspace apps visibility
|
||||
if config().has_changed("dock_hide_special_workspace_apps", old_config):
|
||||
self.app_bar.update_dock_apps()
|
||||
|
||||
def _update_visibility(self):
|
||||
enabled = config().get("dock_enabled", True)
|
||||
if enabled:
|
||||
self.show()
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.app_bar.add_style_class("shown")
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def on_hover_enter(self):
|
||||
self.is_hovered = True
|
||||
self.hide_ticket = random.getrandbits(32)
|
||||
@@ -800,14 +861,10 @@ class Dock(Window):
|
||||
|
||||
def delayed_hide(ticket):
|
||||
if ticket == self.hide_ticket and not self.is_hovered:
|
||||
# Only hide if we aren't occluded (or if occlusion logic says so)
|
||||
# For now, just trigger the basic hide if auto-hide is on
|
||||
if data.DOCK_AUTO_HIDE:
|
||||
# Check if actually occluded before hiding if DOCK_ALWAYS_OCCLUDED is false
|
||||
occlusion_pos = self._get_dock_config()["occlusion"]
|
||||
is_occluded = data.DOCK_ALWAYS_OCCLUDED or check_occlusion(
|
||||
(occlusion_pos, self.dock_height)
|
||||
)
|
||||
if config().get("dock_auto_hide", True):
|
||||
is_occluded = config().get(
|
||||
"dock_always_occluded", False
|
||||
) or check_occlusion(("bottom", self.dock_height))
|
||||
if is_occluded:
|
||||
self.revealer.set_reveal_child(False)
|
||||
self.app_bar.remove_style_class("shown")
|
||||
@@ -815,34 +872,18 @@ class Dock(Window):
|
||||
|
||||
GLib.timeout_add(500, delayed_hide, self.hide_ticket)
|
||||
|
||||
def _get_dock_config(self):
|
||||
config_map = {
|
||||
"Left": {
|
||||
"anchor": "left center",
|
||||
"transition": "slide-right",
|
||||
"occlusion": "left",
|
||||
},
|
||||
"Right": {
|
||||
"anchor": "right center",
|
||||
"transition": "slide-left",
|
||||
"occlusion": "right",
|
||||
},
|
||||
"Bottom": {
|
||||
"anchor": "bottom center",
|
||||
"transition": "slide-up",
|
||||
"occlusion": "bottom",
|
||||
},
|
||||
}
|
||||
return config_map.get(data.DOCK_POSITION, config_map["Bottom"])
|
||||
|
||||
def setup_occlusion_monitoring(self):
|
||||
# Remove existing timer if any
|
||||
if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id:
|
||||
GLib.source_remove(self._occlusion_timer_id)
|
||||
self._occlusion_timer_id = None
|
||||
|
||||
def check_dock_occlusion():
|
||||
try:
|
||||
if data.DOCK_ALWAYS_OCCLUDED:
|
||||
if config().get("dock_always_occluded", False):
|
||||
is_occluded = True
|
||||
else:
|
||||
occlusion_pos = self._get_dock_config()["occlusion"]
|
||||
is_occluded = check_occlusion((occlusion_pos, self.dock_height))
|
||||
is_occluded = check_occlusion(("bottom", self.dock_height))
|
||||
|
||||
if (
|
||||
is_occluded
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.entry import Entry
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from fabric.widgets.stack import Stack
|
||||
from fabric.utils import Gtk, GLib, Gdk
|
||||
|
||||
from services.config import config, on_config_change
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
|
||||
|
||||
class SettingsRow(CenterBox):
|
||||
def __init__(self, label: str, child=None, description: str = None, **kwargs):
|
||||
label_widget = Label(label=label, name="settings-row-label", h_align="start")
|
||||
|
||||
start_children = [label_widget]
|
||||
if description:
|
||||
description_widget = Label(
|
||||
label=description, name="settings-row-description", h_align="start"
|
||||
)
|
||||
start_children = [
|
||||
Box(
|
||||
orientation="v",
|
||||
spacing=2,
|
||||
children=[label_widget, description_widget],
|
||||
)
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
name="settings-row",
|
||||
start_children=start_children,
|
||||
end_children=[child] if child else [],
|
||||
**kwargs,
|
||||
)
|
||||
self.label_widget = label_widget
|
||||
if description:
|
||||
self.description_widget = description_widget
|
||||
|
||||
|
||||
class SettingsSwitch(Button):
|
||||
def __init__(self, config_key: str, **kwargs):
|
||||
self.config_key = config_key
|
||||
self.active = config().get(config_key, False)
|
||||
|
||||
super().__init__(name="settings-switch", on_clicked=self.toggle, **kwargs)
|
||||
self._update_style()
|
||||
setup_cursor_hover(self, "pointer")
|
||||
|
||||
# Sync with external config changes
|
||||
on_config_change(self._on_config_change)
|
||||
|
||||
def _on_config_change(self, new_config, old_config):
|
||||
if config().has_changed(self.config_key, old_config):
|
||||
self.active = new_config.get(self.config_key, False)
|
||||
self._update_style()
|
||||
|
||||
def toggle(self, *args):
|
||||
self.active = not self.active
|
||||
config().set(self.config_key, self.active)
|
||||
config().save()
|
||||
self._update_style()
|
||||
|
||||
def _update_style(self):
|
||||
if self.active:
|
||||
self.add_style_class("active")
|
||||
self.set_label("On")
|
||||
else:
|
||||
self.remove_style_class("active")
|
||||
self.set_label("Off")
|
||||
|
||||
|
||||
class SettingsEntry(Entry):
|
||||
def __init__(self, config_key: str, **kwargs):
|
||||
self.config_key = config_key
|
||||
initial_value = str(config().get(config_key, ""))
|
||||
|
||||
super().__init__(
|
||||
name="settings-entry",
|
||||
text=initial_value,
|
||||
on_activate=self.on_change,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Sync with external config changes
|
||||
on_config_change(self._on_config_change)
|
||||
|
||||
def _on_config_change(self, new_config, old_config):
|
||||
if config().has_changed(self.config_key, old_config):
|
||||
new_val = str(new_config.get(self.config_key, ""))
|
||||
if self.get_text() != new_val:
|
||||
self.set_text(new_val)
|
||||
|
||||
def on_change(self, entry, *args):
|
||||
value = entry.get_text()
|
||||
# Try to convert to int if possible
|
||||
try:
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
config().set(self.config_key, value)
|
||||
config().save()
|
||||
|
||||
|
||||
class SettingsComboBox(Gtk.ComboBoxText):
|
||||
def __init__(self, config_key: str, options: list, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_name("settings-combo")
|
||||
self.set_halign(Gtk.Align.END)
|
||||
self.config_key = config_key
|
||||
self.options = options
|
||||
|
||||
for opt in options:
|
||||
self.append_text(opt)
|
||||
|
||||
self._update_from_config()
|
||||
self.connect("changed", self.on_change)
|
||||
|
||||
# Sync with external config changes
|
||||
on_config_change(self._on_config_change)
|
||||
|
||||
def _update_from_config(self):
|
||||
current_val = str(config().get(self.config_key, ""))
|
||||
if current_val in self.options:
|
||||
self.set_active(self.options.index(current_val))
|
||||
|
||||
def _on_config_change(self, new_config, old_config):
|
||||
if config().has_changed(self.config_key, old_config):
|
||||
self._update_from_config()
|
||||
|
||||
def on_change(self, combo):
|
||||
value = combo.get_active_text()
|
||||
if value:
|
||||
config().set(self.config_key, value)
|
||||
config().save()
|
||||
|
||||
|
||||
class SettingsList(Box):
|
||||
"""A list of tags that can be added or removed"""
|
||||
|
||||
def __init__(self, config_key: str, **kwargs):
|
||||
super().__init__(name="settings-list", orientation="v", spacing=5, **kwargs)
|
||||
self.config_key = config_key
|
||||
|
||||
self.list_box = Box(spacing=5, name="settings-list-tags")
|
||||
self.entry = Entry(
|
||||
name="settings-list-entry",
|
||||
placeholder="Add new item...",
|
||||
on_activate=self.on_add,
|
||||
)
|
||||
|
||||
self.add(self.list_box)
|
||||
self.add(self.entry)
|
||||
|
||||
self._update_list()
|
||||
on_config_change(self._on_config_change)
|
||||
|
||||
def _on_config_change(self, new_config, old_config):
|
||||
if config().has_changed(self.config_key, old_config):
|
||||
self._update_list()
|
||||
|
||||
def _update_list(self):
|
||||
items = config().get(self.config_key, [])
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
|
||||
# Clear existing tags
|
||||
self.list_box.children = []
|
||||
|
||||
for item in items:
|
||||
tag = Button(
|
||||
name="settings-list-tag",
|
||||
child=Box(
|
||||
spacing=5,
|
||||
children=[
|
||||
Label(label=item),
|
||||
Label(label="✕", name="settings-list-tag-close"),
|
||||
],
|
||||
),
|
||||
on_clicked=lambda *_, i=item: self.on_remove(i),
|
||||
)
|
||||
setup_cursor_hover(tag, "pointer")
|
||||
self.list_box.pack_start(tag, False, False, 0)
|
||||
|
||||
self.list_box.show_all()
|
||||
|
||||
def on_add(self, entry):
|
||||
val = entry.get_text().strip()
|
||||
if not val:
|
||||
return
|
||||
|
||||
items = config().get(self.config_key, [])
|
||||
if val not in items:
|
||||
items.append(val)
|
||||
config().set(self.config_key, items)
|
||||
config().save()
|
||||
|
||||
entry.set_text("")
|
||||
self._update_list()
|
||||
|
||||
def on_remove(self, item):
|
||||
items = config().get(self.config_key, [])
|
||||
if item in items:
|
||||
items.remove(item)
|
||||
config().set(self.config_key, items)
|
||||
config().save()
|
||||
self._update_list()
|
||||
|
||||
|
||||
class SettingsPage(ScrolledWindow):
|
||||
def __init__(self, title: str, rows: list, **kwargs):
|
||||
container = Box(
|
||||
name="settings-page-container",
|
||||
orientation="v",
|
||||
spacing=10,
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[
|
||||
Label(label=title, name="settings-page-title", h_align="start"),
|
||||
Box(orientation="v", spacing=1, children=rows, h_expand=True),
|
||||
],
|
||||
)
|
||||
super().__init__(
|
||||
name="settings-page",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
propagate_width=True,
|
||||
propagate_height=True,
|
||||
min_content_width=600,
|
||||
min_content_height=500,
|
||||
**kwargs,
|
||||
)
|
||||
self.add(container)
|
||||
self.show_all()
|
||||
self._container = container
|
||||
|
||||
|
||||
class SettingsWindow(Gtk.Window):
|
||||
def __init__(self, **kwargs):
|
||||
GLib.set_prgname("modus-settings")
|
||||
super().__init__(title="Modus Settings", **kwargs)
|
||||
self.set_name("settings-window")
|
||||
self.set_wmclass("modus-settings", "Modus")
|
||||
self.set_default_size(850, 600)
|
||||
self.set_resizable(False)
|
||||
self.set_type_hint(Gtk.Window.get_type_hint(self) | Gdk.WindowTypeHint.DIALOG)
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.set_visible(False)
|
||||
|
||||
# Reset global singleton on destroy
|
||||
self.connect("destroy", self._on_window_destroy)
|
||||
|
||||
self.stack = Stack(
|
||||
name="settings-stack",
|
||||
transition_type="none",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
self.pages = {
|
||||
"general": self._create_general_page(),
|
||||
"dock": self._create_dock_page(),
|
||||
"panel": self._create_panel_page(),
|
||||
"notifications": self._create_notifications_page(),
|
||||
}
|
||||
|
||||
for name, page in self.pages.items():
|
||||
self.stack.add_named(page, name)
|
||||
|
||||
self.sidebar_buttons = {}
|
||||
self.sidebar_buttons["general"] = self._create_sidebar_button(
|
||||
"General", "general", "misc/logo.svg"
|
||||
)
|
||||
self.sidebar_buttons["dock"] = self._create_sidebar_button(
|
||||
"Dock", "dock", "misc/control.svg"
|
||||
)
|
||||
self.sidebar_buttons["panel"] = self._create_sidebar_button(
|
||||
"Panel", "panel", "misc/control-center.svg"
|
||||
)
|
||||
self.sidebar_buttons["notifications"] = self._create_sidebar_button(
|
||||
"Notifications", "notifications", "notifications/notification-active.svg"
|
||||
)
|
||||
|
||||
self.set_page("general")
|
||||
|
||||
self.sidebar = Box(
|
||||
name="settings-sidebar",
|
||||
orientation="v",
|
||||
spacing=5,
|
||||
children=list(self.sidebar_buttons.values()),
|
||||
)
|
||||
|
||||
self.main_box = Box(
|
||||
name="settings-main-box",
|
||||
orientation="h",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[
|
||||
self.sidebar,
|
||||
Box(
|
||||
name="settings-content-wrapper",
|
||||
children=[self.stack],
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self.add(self.main_box)
|
||||
|
||||
def _create_sidebar_button(self, label, page_name, icon_name):
|
||||
btn = Button(
|
||||
name="settings-sidebar-button",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
spacing=10,
|
||||
children=[svg_file(icon_name, size=16), Label(label=label)],
|
||||
),
|
||||
on_clicked=lambda *_: self.set_page(page_name),
|
||||
)
|
||||
setup_cursor_hover(btn, "pointer")
|
||||
return btn
|
||||
|
||||
def set_page(self, page_name):
|
||||
if page_name not in self.pages:
|
||||
return
|
||||
self.stack.set_visible_child(self.pages[page_name])
|
||||
for name, btn in self.sidebar_buttons.items():
|
||||
if name == page_name:
|
||||
btn.add_style_class("active")
|
||||
else:
|
||||
btn.remove_style_class("active")
|
||||
|
||||
def _create_general_page(self):
|
||||
return SettingsPage(
|
||||
"General Settings",
|
||||
[
|
||||
SettingsRow(
|
||||
"Wallpapers Directory",
|
||||
SettingsEntry("wallpapers_dir"),
|
||||
"Path to your wallpaper collection",
|
||||
),
|
||||
SettingsRow(
|
||||
"Weather Location",
|
||||
SettingsEntry("weather_location"),
|
||||
"City name for weather updates",
|
||||
),
|
||||
SettingsRow(
|
||||
"Keyboard Layouts",
|
||||
SettingsList("keyboard_layouts"),
|
||||
"Manage active keyboard input languages",
|
||||
),
|
||||
SettingsRow(
|
||||
"Window Switcher",
|
||||
SettingsSwitch("window_switcher"),
|
||||
"Enable the Alt-Tab window switcher",
|
||||
),
|
||||
SettingsRow(
|
||||
"Items Per Row",
|
||||
SettingsEntry("window_switcher_items_per_row"),
|
||||
"Maximum items in window switcher row",
|
||||
),
|
||||
SettingsRow(
|
||||
"Hide Special Workspace",
|
||||
SettingsSwitch("hide_special_workspace"),
|
||||
"Don't show special workspace in indicators",
|
||||
),
|
||||
SettingsRow(
|
||||
"OSD",
|
||||
SettingsSwitch("osd"),
|
||||
"On-screen display for volume/brightness",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def _create_dock_page(self):
|
||||
return SettingsPage(
|
||||
"Dock Settings",
|
||||
[
|
||||
SettingsRow(
|
||||
"Enabled",
|
||||
SettingsSwitch("dock_enabled"),
|
||||
"Show the application dock",
|
||||
),
|
||||
SettingsRow(
|
||||
"Auto Hide",
|
||||
SettingsSwitch("dock_auto_hide"),
|
||||
"Hide dock when not in use",
|
||||
),
|
||||
SettingsRow(
|
||||
"Always Occluded",
|
||||
SettingsSwitch("dock_always_occluded"),
|
||||
"Keep dock behind other windows",
|
||||
),
|
||||
SettingsRow(
|
||||
"Icon Size",
|
||||
SettingsEntry("dock_icon_size"),
|
||||
"Size of dock icons in pixels",
|
||||
),
|
||||
SettingsRow(
|
||||
"Hide Special Apps",
|
||||
SettingsSwitch("dock_hide_special_workspace_apps"),
|
||||
"Hide apps from special workspace in dock",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def _create_panel_page(self):
|
||||
# panel visibility settings
|
||||
rows = []
|
||||
panel_settings = [
|
||||
("iMac Button", "imac_button"),
|
||||
("Global Menu", "global_menu"),
|
||||
("Systray", "systray"),
|
||||
("Control Center", "control_center"),
|
||||
("Search", "search"),
|
||||
("Network", "network"),
|
||||
("Battery", "battery"),
|
||||
("Bluetooth", "bluetooth"),
|
||||
("Date & Time", "date_time"),
|
||||
("Workspace Indicator", "workspace_indicator"),
|
||||
("Notification Center", "notification_center"),
|
||||
]
|
||||
for label, key in panel_settings:
|
||||
rows.append(SettingsRow(label, SettingsSwitch(key)))
|
||||
|
||||
rows.append(
|
||||
SettingsRow(
|
||||
"Systray Ignore",
|
||||
SettingsList("systray_ignore"),
|
||||
"Icons to hide from the system tray",
|
||||
)
|
||||
)
|
||||
|
||||
return SettingsPage("Panel Settings", rows)
|
||||
|
||||
def _create_notifications_page(self):
|
||||
return SettingsPage(
|
||||
"Notification Settings",
|
||||
[
|
||||
SettingsRow(
|
||||
"Timeout",
|
||||
SettingsEntry("notification_timeout"),
|
||||
"How long notifications stay on screen (e.g. 5s)",
|
||||
),
|
||||
SettingsRow(
|
||||
"Ignored Apps",
|
||||
SettingsList("notification_ignored_apps"),
|
||||
"Apps that won't show notifications",
|
||||
),
|
||||
SettingsRow(
|
||||
"Limited History",
|
||||
SettingsList("notification_limited_apps_history"),
|
||||
"Apps with only the latest notification shown",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def _on_window_destroy(self, *args):
|
||||
global _settings_window
|
||||
_settings_window = None
|
||||
|
||||
def toggle(self):
|
||||
if not self.get_visible():
|
||||
self.show_all()
|
||||
self.present()
|
||||
|
||||
# Re-sync current page
|
||||
current_page = "general"
|
||||
for name, btn in self.sidebar_buttons.items():
|
||||
if "active" in btn.get_style_context().list_classes():
|
||||
current_page = name
|
||||
break
|
||||
self.set_page(current_page)
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
|
||||
_settings_window = None
|
||||
|
||||
|
||||
def get_settings_window():
|
||||
global _settings_window
|
||||
if _settings_window is None:
|
||||
_settings_window = SettingsWindow()
|
||||
return _settings_window
|
||||
+59
-32
@@ -8,7 +8,7 @@ from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
import shared.data as data
|
||||
from services.config import config, on_config_change
|
||||
from utils.functions import is_special_workspace
|
||||
from utils.icon_resolver import IconResolver
|
||||
|
||||
@@ -31,9 +31,10 @@ class ApplicationSwitcher(Window):
|
||||
self.windows = []
|
||||
self.current_index = 0
|
||||
self.tab_pressed = False
|
||||
self.items_per_row = data.WINDOW_SWITCHER_ITEMS_PER_ROW
|
||||
self.icon_size = 96
|
||||
self.items_per_row = 100 # Single row focus
|
||||
|
||||
# Subscribe to config changes
|
||||
on_config_change(self._on_config_changed)
|
||||
|
||||
container = Box(
|
||||
name="application-switcher-container",
|
||||
@@ -70,12 +71,25 @@ class ApplicationSwitcher(Window):
|
||||
style_classes=["switcher-workspace-label"],
|
||||
)
|
||||
container.add(self.workspace_label)
|
||||
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
self.connect("key-release-event", self.on_key_release)
|
||||
|
||||
self.show_all()
|
||||
self.hide()
|
||||
|
||||
@property
|
||||
def items_per_row(self):
|
||||
return config().get("window_switcher_items_per_row", 10)
|
||||
|
||||
def _on_config_changed(self, new_config, old_config):
|
||||
# Refresh windows if settings that affect display change
|
||||
if config().has_changed(
|
||||
"hide_special_workspace", old_config
|
||||
) or config().has_changed("window_switcher_items_per_row", old_config):
|
||||
if self.get_visible():
|
||||
self.update_windows()
|
||||
|
||||
def show_switcher(self) -> None:
|
||||
self.update_windows()
|
||||
if not self.windows:
|
||||
@@ -119,13 +133,13 @@ class ApplicationSwitcher(Window):
|
||||
|
||||
# Filter out hidden windows and optionally special workspace windows
|
||||
filtered_windows = []
|
||||
hide_special = config().get("hide_special_workspace", True)
|
||||
|
||||
for c in clients:
|
||||
if c.get("hidden", False):
|
||||
continue
|
||||
# Skip clients in special workspaces if the setting is enabled
|
||||
if data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS and self._is_special_workspace(
|
||||
c
|
||||
):
|
||||
if hide_special and self._is_special_workspace(c):
|
||||
continue
|
||||
filtered_windows.append(c)
|
||||
|
||||
@@ -143,16 +157,24 @@ class ApplicationSwitcher(Window):
|
||||
self.current_index = i
|
||||
break
|
||||
|
||||
current_row = Box(
|
||||
name="window-row",
|
||||
orientation="h",
|
||||
spacing=16,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
)
|
||||
self.view.add(current_row)
|
||||
# Create vertical container for rows
|
||||
rows_box = Box(orientation="v", spacing=16)
|
||||
self.view.add(rows_box)
|
||||
|
||||
current_row = None
|
||||
items_per_row = self.items_per_row
|
||||
|
||||
for i, window in enumerate(self.windows):
|
||||
if i % items_per_row == 0:
|
||||
current_row = Box(
|
||||
name="window-row",
|
||||
orientation="h",
|
||||
spacing=16,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
)
|
||||
rows_box.add(current_row)
|
||||
|
||||
# Create icon image for this window
|
||||
icon_image = self.create_icon_for_window(window)
|
||||
|
||||
@@ -256,25 +278,30 @@ class ApplicationSwitcher(Window):
|
||||
return False
|
||||
|
||||
def update_selection(self):
|
||||
for row in self.view.get_children():
|
||||
for i, child in enumerate(row.get_children()):
|
||||
index = self.view.get_children().index(row) * self.items_per_row + i
|
||||
if index == self.current_index:
|
||||
child.add_style_class("active")
|
||||
# Update selection label
|
||||
current_window = self.windows[self.current_index]
|
||||
app_class = current_window.get("class", "Unknown")
|
||||
# capitalize if it's all lowercase
|
||||
if app_class.islower():
|
||||
app_class = app_class.capitalize()
|
||||
self.selection_label.set_label(app_class)
|
||||
# Flatten all buttons from all rows to find them by index
|
||||
all_buttons = []
|
||||
rows_box = self.view.get_children()[0] if self.view.get_children() else None
|
||||
if rows_box:
|
||||
for row in rows_box.get_children():
|
||||
all_buttons.extend(row.get_children())
|
||||
|
||||
# Update workspace label
|
||||
workspace = current_window.get("workspace", {})
|
||||
workspace_name = workspace.get("name", "Unknown")
|
||||
self.workspace_label.set_label(f"Workspace {workspace_name}")
|
||||
else:
|
||||
child.remove_style_class("active")
|
||||
for i, child in enumerate(all_buttons):
|
||||
if i == self.current_index:
|
||||
child.add_style_class("active")
|
||||
# Update selection label
|
||||
current_window = self.windows[self.current_index]
|
||||
app_class = current_window.get("class", "Unknown")
|
||||
# capitalize if it's all lowercase
|
||||
if app_class.islower():
|
||||
app_class = app_class.capitalize()
|
||||
self.selection_label.set_label(app_class)
|
||||
|
||||
# Update workspace label
|
||||
workspace = current_window.get("workspace", {})
|
||||
workspace_name = workspace.get("name", "Unknown")
|
||||
self.workspace_label.set_label(f"Workspace {workspace_name}")
|
||||
else:
|
||||
child.remove_style_class("active")
|
||||
|
||||
def activate_selected(self):
|
||||
if not self.windows or self.current_index >= len(self.windows):
|
||||
|
||||
Reference in new issue
Block a user