diff --git a/config/settings/about_tab.py b/config/settings/about_tab.py new file mode 100644 index 0000000..b475efc --- /dev/null +++ b/config/settings/about_tab.py @@ -0,0 +1,40 @@ +from fabric.widgets.box import Box +from fabric.widgets.label import Label + +from config.data import APP_NAME_CAP + + +class AboutTab: + """About information tab for settings""" + + def __init__(self): + pass + + def create_about_tab(self): + """Create the About tab content""" + vbox = Box(orientation="v", spacing=18, style="margin: 30px;") + vbox.add( + Label( + markup=f"{APP_NAME_CAP}", + h_align="start", + style="font-size: 1.5em; margin-bottom: 8px;", + ) + ) + vbox.add( + Label( + label="A hackable shell for Hyprland, powered by Fabric.", + h_align="start", + style="margin-bottom: 12px;", + ) + ) + repo_box = Box(orientation="h", spacing=6, h_align="start") + repo_label = Label(label="GitHub:", h_align="start") + repo_link = Label( + markup='https://github.com/S4NKALP/Modus' + ) + repo_box.add(repo_label) + repo_box.add(repo_link) + vbox.add(repo_box) + + vbox.add(Box(v_expand=True)) + return vbox diff --git a/config/settings/appearance_tab.py b/config/settings/appearance_tab.py new file mode 100644 index 0000000..9873ce1 --- /dev/null +++ b/config/settings/appearance_tab.py @@ -0,0 +1,689 @@ +import os +import gi +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.image import Image as FabricImage +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import Gdk, GdkPixbuf, Gtk + +from config.data import NOTIF_POS_DEFAULT, NOTIF_POS_KEY +from config.settings.utils import bind_vars + +gi.require_version("Gtk", "3.0") + + +class AppearanceTab: + """Appearance and layout management tab for settings""" + + def __init__(self, themes, parent_window=None): + self.themes = themes + self.parent_window = parent_window + self.selected_face_icon = None + + # Widget references + self.wall_dir_chooser = None + self.face_image = None + self.face_status_label = None + self.ws_mode_combo = None + self.ws_chinese_switch = None + self.position_combo = None + self.dock_theme_combo = None + self.icon_size_scale = None + self.application_switcher_scale = None + self.dock_switch = None + self.dock_auto_hide_switch = None + self.dock_hover_switch = None + self.dock_hide_special_switch = None + self.dock_hide_special_apps_switch = None + self.notification_pos_combo = None + self.component_switches = {} + self.corners_switch = None + + def create_appearance_tab(self): + """Create the Appearance tab content""" + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=False, + v_expand=False, + propagate_width=False, + propagate_height=False, + ) + # Set fixed size to match tab stack dimensions + scrolled_window.set_size_request(580, 580) + + vbox = Box(orientation="v", spacing=15, style="margin: 15px;") + scrolled_window.add(vbox) + + # Create wallpaper and profile icon section + self._create_wallpaper_profile_section(vbox) + + # Add separator + separator1 = Box( + style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", + h_expand=True, + ) + vbox.add(separator1) + + # Create layout options section + self._create_layout_options_section(vbox) + + # Add separator + separator2 = Box( + style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", + h_expand=True, + ) + vbox.add(separator2) + + # Create modules section + self._create_modules_section(vbox) + + return scrolled_window + + def _create_wallpaper_profile_section(self, vbox): + """Create wallpaper and profile icon section""" + top_grid = Gtk.Grid() + top_grid.set_column_spacing(20) + top_grid.set_row_spacing(5) + top_grid.set_margin_bottom(10) + vbox.add(top_grid) + + # Wallpaper section + wall_header = Label(markup="Wallpapers", h_align="start") + top_grid.attach(wall_header, 0, 0, 1, 1) + wall_label = Label(label="Directory:", h_align="start", v_align="center") + top_grid.attach(wall_label, 0, 1, 1, 1) + + chooser_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + chooser_container.set_halign(Gtk.Align.START) + chooser_container.set_valign(Gtk.Align.CENTER) + self.wall_dir_chooser = Gtk.FileChooserButton( + title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER + ) + self.wall_dir_chooser.set_tooltip_text( + "Select the directory containing your wallpaper images" + ) + self.wall_dir_chooser.set_filename(bind_vars.get("wallpapers_dir", "")) + self.wall_dir_chooser.set_size_request(180, -1) + chooser_container.add(self.wall_dir_chooser) + top_grid.attach(chooser_container, 1, 1, 1, 1) + + # Profile icon section + face_header = Label(markup="Profile Icon", h_align="start") + top_grid.attach(face_header, 2, 0, 2, 1) + current_face = os.path.expanduser("~/.face.icon") + face_image_container = Box( + style_classes=["image-frame"], h_align="center", v_align="center" + ) + self.face_image = FabricImage(size=64) + try: + if os.path.exists(current_face): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) + self.face_image.set_from_pixbuf(pixbuf) + else: + self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) + except Exception as e: + print(f"Error loading face icon: {e}") + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + face_image_container.add(self.face_image) + top_grid.attach(face_image_container, 2, 1, 1, 1) + + browse_btn_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + browse_btn_container.set_halign(Gtk.Align.START) + browse_btn_container.set_valign(Gtk.Align.CENTER) + face_btn = Button( + label="Browse...", + tooltip_text="Select a square image for your profile icon", + on_clicked=self.on_select_face_icon, + ) + browse_btn_container.add(face_btn) + top_grid.attach(browse_btn_container, 3, 1, 1, 1) + self.face_status_label = Label(label="", h_align="start") + top_grid.attach(self.face_status_label, 2, 2, 2, 1) + + def _create_layout_options_section(self, vbox): + """Create layout options section""" + layout_header = Label(markup="Layout Options", h_align="start") + vbox.add(layout_header) + layout_grid = Gtk.Grid() + layout_grid.set_column_spacing(20) + layout_grid.set_row_spacing(10) + layout_grid.set_margin_start(10) + layout_grid.set_margin_top(5) + vbox.add(layout_grid) + + # Workspace mode + ws_mode_label = Label(label="Workspace Mode", h_align="start", v_align="center") + layout_grid.attach(ws_mode_label, 0, 0, 1, 1) + + ws_mode_combo_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.ws_mode_combo = Gtk.ComboBoxText() + self.ws_mode_combo.set_tooltip_text("Select how workspaces are displayed") + ws_modes = ["Dots", "Numbers", "Single Button"] + for mode in ws_modes: + self.ws_mode_combo.append_text(mode) + + # Set current mode based on config + current_dots = bind_vars.get("workspace_dots", True) + current_nums = bind_vars.get("workspace_nums", False) + if current_dots: + current_mode = "Dots" + elif current_nums: + current_mode = "Numbers" + else: + current_mode = "Single Button" + + try: + self.ws_mode_combo.set_active(ws_modes.index(current_mode)) + except ValueError: + self.ws_mode_combo.set_active(0) + + self.ws_mode_combo.connect("changed", self.on_ws_mode_changed) + ws_mode_combo_container.add(self.ws_mode_combo) + layout_grid.attach(ws_mode_combo_container, 1, 0, 1, 1) + + # Chinese numerals option + ws_chinese_label = Label( + label="Use Chinese Numerals", h_align="start", v_align="center" + ) + layout_grid.attach(ws_chinese_label, 2, 0, 1, 1) + ws_chinese_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.ws_chinese_switch = Gtk.Switch( + active=bind_vars.get("workspace_use_chinese_numerals", False), + sensitive=current_nums, # Only enabled when numbers mode is selected + ) + self.ws_chinese_switch.set_tooltip_text( + "Use Chinese numerals (一, 二, 三...) instead of Arabic numbers" + ) + ws_chinese_switch_container.add(self.ws_chinese_switch) + layout_grid.attach(ws_chinese_switch_container, 3, 0, 1, 1) + + # Dock position + position_label = Label(label="Dock Position", h_align="start", v_align="center") + layout_grid.attach(position_label, 0, 1, 1, 1) + position_combo_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.position_combo = Gtk.ComboBoxText() + self.position_combo.set_tooltip_text("Select the position of the dock") + positions = ["Bottom", "Top", "Left", "Right"] + for pos in positions: + self.position_combo.append_text(pos) + current_position = bind_vars.get("dock_position", "Bottom") + try: + self.position_combo.set_active(positions.index(current_position)) + except ValueError: + self.position_combo.set_active(0) + self.position_combo.connect("changed", self.on_position_changed) + position_combo_container.add(self.position_combo) + layout_grid.attach(position_combo_container, 1, 1, 1, 1) + + # Dock theme + dock_theme_label = Label(label="Dock Theme", h_align="start", v_align="center") + layout_grid.attach(dock_theme_label, 2, 1, 1, 1) + dock_theme_combo_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_theme_combo = Gtk.ComboBoxText() + self.dock_theme_combo.set_tooltip_text("Select the visual theme for the dock") + for theme in self.themes: + self.dock_theme_combo.append_text(theme) + current_dock_theme = bind_vars.get("dock_theme", "Pills") + try: + self.dock_theme_combo.set_active(self.themes.index(current_dock_theme)) + except ValueError: + self.dock_theme_combo.set_active(0) + dock_theme_combo_container.add(self.dock_theme_combo) + layout_grid.attach(dock_theme_combo_container, 3, 1, 1, 1) + + # Show Dock switch + dock_label = Label(label="Show Dock", h_align="start", v_align="center") + layout_grid.attach(dock_label, 0, 2, 1, 1) + dock_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_switch = Gtk.Switch(active=bind_vars.get("dock_enabled", True)) + self.dock_switch.connect("notify::active", self.on_dock_enabled_changed) + dock_switch_container.add(self.dock_switch) + layout_grid.attach(dock_switch_container, 1, 2, 1, 1) + + # Show Dock Only on Hover + dock_hover_label = Label( + label="Show Dock Only on Hover", h_align="start", v_align="center" + ) + layout_grid.attach(dock_hover_label, 2, 2, 1, 1) + dock_hover_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_hover_switch = Gtk.Switch( + active=bind_vars.get("dock_always_occluded", False), + sensitive=self.dock_switch.get_active(), + ) + dock_hover_switch_container.add(self.dock_hover_switch) + layout_grid.attach(dock_hover_switch_container, 3, 2, 1, 1) + + # Auto-hide Dock + dock_auto_hide_label = Label( + label="Auto-hide Dock", h_align="start", v_align="center" + ) + layout_grid.attach(dock_auto_hide_label, 0, 3, 1, 1) + dock_auto_hide_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_auto_hide_switch = Gtk.Switch( + active=bind_vars.get("dock_auto_hide", False), + sensitive=self.dock_switch.get_active(), + ) + dock_auto_hide_switch_container.add(self.dock_auto_hide_switch) + layout_grid.attach(dock_auto_hide_switch_container, 1, 3, 1, 1) + + # Hide Special Workspace + dock_hide_special_label = Label( + label="Hide Special Workspace", h_align="start", v_align="center" + ) + layout_grid.attach(dock_hide_special_label, 2, 3, 1, 1) + dock_hide_special_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_hide_special_switch = Gtk.Switch( + active=bind_vars.get("dock_hide_special_workspace", True), + sensitive=self.dock_switch.get_active(), + ) + self.dock_hide_special_switch.set_tooltip_text( + "Hide the dock when on special workspaces (scratchpad)" + ) + dock_hide_special_switch_container.add(self.dock_hide_special_switch) + layout_grid.attach(dock_hide_special_switch_container, 3, 3, 1, 1) + + # Icon size scale + icon_size_label = Label( + label="Taskbar Icon Size", h_align="start", v_align="center" + ) + layout_grid.attach(icon_size_label, 0, 4, 1, 1) + + icon_size_scale_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + hexpand=True, + ) + + self.icon_size_scale = Gtk.Scale.new_with_range( + Gtk.Orientation.HORIZONTAL, 20, 30, 2 + ) + self.icon_size_scale.set_value(bind_vars.get("dock_icon_size", 20)) + self.icon_size_scale.set_size_request(150, -1) + self.icon_size_scale.set_draw_value(True) + self.icon_size_scale.set_value_pos(Gtk.PositionType.RIGHT) + self.icon_size_scale.set_tooltip_text("Adjust the size of dock icons") + + icon_size_scale_container.add(self.icon_size_scale) + layout_grid.attach(icon_size_scale_container, 1, 4, 1, 1) + + # Application switcher items scale + application_switcher_label = Label( + label="Application Switcher Items", h_align="start", v_align="center" + ) + layout_grid.attach(application_switcher_label, 2, 4, 1, 1) + + application_switcher_scale_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + hexpand=True, + ) + + self.application_switcher_scale = Gtk.Scale.new_with_range( + Gtk.Orientation.HORIZONTAL, 5, 20, 1 + ) + self.application_switcher_scale.set_value( + bind_vars.get("window_switcher_items_per_row", 13) + ) + self.application_switcher_scale.set_size_request(150, -1) + self.application_switcher_scale.set_draw_value(True) + self.application_switcher_scale.set_value_pos(Gtk.PositionType.RIGHT) + self.application_switcher_scale.set_tooltip_text( + "Adjust the number of items per row in the application switcher" + ) + + application_switcher_scale_container.add(self.application_switcher_scale) + layout_grid.attach(application_switcher_scale_container, 3, 4, 1, 1) + + # Hide Special Workspace Apps + dock_hide_special_apps_label = Label( + label="Hide Special Workspace Apps", h_align="start", v_align="center" + ) + layout_grid.attach(dock_hide_special_apps_label, 2, 5, 1, 1) + dock_hide_special_apps_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.dock_hide_special_apps_switch = Gtk.Switch( + active=bind_vars.get("dock_hide_special_workspace_apps", True), + sensitive=self.dock_switch.get_active(), + ) + self.dock_hide_special_apps_switch.set_tooltip_text( + "Hide applications that are in special workspaces (scratchpad) from the dock" + ) + dock_hide_special_apps_switch_container.add(self.dock_hide_special_apps_switch) + layout_grid.attach(dock_hide_special_apps_switch_container, 3, 5, 1, 1) + + # Notification position + notification_pos_label = Label( + label="Notification Position", h_align="start", v_align="center" + ) + layout_grid.attach(notification_pos_label, 0, 8, 1, 1) + + notification_pos_combo_container = Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + + self.notification_pos_combo = Gtk.ComboBoxText() + self.notification_pos_combo.set_tooltip_text( + "Select where notifications appear on the screen." + ) + + notification_positions_list = ["Top", "Bottom"] + for pos in notification_positions_list: + self.notification_pos_combo.append_text(pos) + + current_notif_pos = bind_vars.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) + try: + self.notification_pos_combo.set_active( + notification_positions_list.index(current_notif_pos) + ) + except ValueError: + self.notification_pos_combo.set_active(0) + + self.notification_pos_combo.connect( + "changed", self.on_notification_position_changed + ) + + notification_pos_combo_container.add(self.notification_pos_combo) + layout_grid.attach(notification_pos_combo_container, 1, 8, 3, 1) + + def _create_modules_section(self, vbox): + """Create modules section""" + components_header = Label(markup="Modules", h_align="start") + vbox.add(components_header) + components_grid = Gtk.Grid() + components_grid.set_column_spacing(15) + components_grid.set_row_spacing(8) + components_grid.set_margin_start(10) + components_grid.set_margin_top(5) + vbox.add(components_grid) + + self.component_switches = {} + component_display_names = { + "workspace": "Workspaces", + "metrics": "System Metrics", + "date_time": "Date & Time", + "battery": "Battery Indicator", + "controls": "Control Panel", + "indicators": "Indicators", + "music_player": "Music Player", + "applications": "Taskbar", + "language": "Language Indicator", + } + + self.corners_switch = Gtk.Switch(active=bind_vars.get("corners_visible", True)) + num_components = len(component_display_names) + 1 + rows_per_column = (num_components + 1) // 2 + + corners_label = Label( + label="Rounded Corners", h_align="start", v_align="center" + ) + components_grid.attach(corners_label, 0, 0, 1, 1) + switch_container_corners = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + switch_container_corners.add(self.corners_switch) + components_grid.attach(switch_container_corners, 1, 0, 1, 1) + + item_idx = 0 + for name, display in component_display_names.items(): + if item_idx < (rows_per_column - 1): + row = item_idx + 1 + col = 0 + else: + row = item_idx - (rows_per_column - 1) + col = 2 + + component_label = Label(label=display, h_align="start", v_align="center") + components_grid.attach(component_label, col, row, 1, 1) + + switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + component_switch = Gtk.Switch( + active=bind_vars.get(f"dock_{name}_visible", True) + ) + switch_container.add(component_switch) + components_grid.attach(switch_container, col + 1, row, 1, 1) + self.component_switches[name] = component_switch + item_idx += 1 + + # Event handlers + def on_ws_mode_changed(self, combo): + """Handle workspace mode change""" + mode = combo.get_active_text() + is_numbers_mode = mode == "Numbers" + self.ws_chinese_switch.set_sensitive(is_numbers_mode) + if not is_numbers_mode: + self.ws_chinese_switch.set_active(False) + + def on_position_changed(self, _combo): + """Handle dock position change""" + # Note: centered_switch is not used in appearance tab, + # this method is kept for compatibility + pass + + def on_dock_enabled_changed(self, switch, _gparam): + """Handle dock enabled/disabled change""" + is_active = switch.get_active() + self.dock_hover_switch.set_sensitive(is_active) + self.dock_auto_hide_switch.set_sensitive(is_active) + self.dock_hide_special_switch.set_sensitive(is_active) + self.dock_hide_special_apps_switch.set_sensitive(is_active) + if not is_active: + self.dock_hover_switch.set_active(False) + self.dock_auto_hide_switch.set_active(False) + self.dock_hide_special_switch.set_active(False) + self.dock_hide_special_apps_switch.set_active(False) + + def on_notification_position_changed(self, combo): + """Handle notification position change""" + selected_text = combo.get_active_text() + if selected_text: + bind_vars[NOTIF_POS_KEY] = selected_text + print( + f"Notification position updated in bind_vars: { + bind_vars[NOTIF_POS_KEY] + }" + ) + + def on_select_face_icon(self, _widget): + """Handle face icon selection""" + dialog = Gtk.FileChooserDialog( + title="Select Face Icon", + transient_for=self.parent_window, + action=Gtk.FileChooserAction.OPEN, + ) + dialog.add_buttons( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK, + ) + image_filter = Gtk.FileFilter() + image_filter.set_name("Image files") + for mime in ["image/png", "image/jpeg"]: + image_filter.add_mime_type(mime) + for pattern in ["*.png", "*.jpg", "*.jpeg"]: + image_filter.add_pattern(pattern) + dialog.add_filter(image_filter) + if dialog.run() == Gtk.ResponseType.OK: + self.selected_face_icon = dialog.get_filename() + self.face_status_label.label = ( + f"Selected: {os.path.basename(self.selected_face_icon)}" + ) + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( + self.selected_face_icon, 64, 64 + ) + self.face_image.set_from_pixbuf(pixbuf) + except Exception as e: + print(f"Error loading selected face icon preview: {e}") + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + dialog.destroy() + + def get_appearance_values(self): + """Get current appearance values from widgets""" + values = {} + + values["wallpapers_dir"] = self.wall_dir_chooser.get_filename() + values["dock_position"] = self.position_combo.get_active_text() + values["vertical"] = values["dock_position"] in ["Left", "Right"] + values["dock_enabled"] = self.dock_switch.get_active() + values["dock_auto_hide"] = self.dock_auto_hide_switch.get_active() + values["dock_always_occluded"] = self.dock_hover_switch.get_active() + values["dock_hide_special_workspace"] = self.dock_hide_special_switch.get_active() + values["dock_hide_special_workspace_apps"] = self.dock_hide_special_apps_switch.get_active() + values["dock_icon_size"] = int(self.icon_size_scale.get_value()) + values["window_switcher_items_per_row"] = int(self.application_switcher_scale.get_value()) + values["corners_visible"] = self.corners_switch.get_active() + values["dock_theme"] = self.dock_theme_combo.get_active_text() + + selected_notif_pos_text = self.notification_pos_combo.get_active_text() + if selected_notif_pos_text: + values[NOTIF_POS_KEY] = selected_notif_pos_text + else: + values[NOTIF_POS_KEY] = NOTIF_POS_DEFAULT + + for component_name, switch in self.component_switches.items(): + values[f"dock_{component_name}_visible"] = switch.get_active() + + # Save workspace settings + ws_mode = self.ws_mode_combo.get_active_text() + values["workspace_dots"] = ws_mode == "Dots" + values["workspace_nums"] = ws_mode == "Numbers" + values["workspace_use_chinese_numerals"] = self.ws_chinese_switch.get_active() + + return values + + def get_selected_face_icon(self): + """Get the selected face icon path""" + return self.selected_face_icon + + def clear_selected_face_icon(self): + """Clear the selected face icon""" + self.selected_face_icon = None + self.face_status_label.label = "" + + def reset_to_defaults(self, defaults): + """Reset appearance settings to default values""" + self.wall_dir_chooser.set_filename(defaults.get("wallpapers_dir", "")) + + positions = ["Bottom", "Top", "Left", "Right"] + default_position = defaults.get("dock_position", "Bottom") + try: + self.position_combo.set_active(positions.index(default_position)) + except ValueError: + self.position_combo.set_active(0) + + self.dock_switch.set_active(defaults.get("dock_enabled", True)) + self.dock_auto_hide_switch.set_active(defaults.get("dock_auto_hide", False)) + self.dock_hover_switch.set_active(defaults.get("dock_always_occluded", False)) + self.dock_hide_special_switch.set_active(defaults.get("dock_hide_special_workspace", True)) + self.dock_hide_special_apps_switch.set_active(defaults.get("dock_hide_special_workspace_apps", True)) + + # Update sensitivity based on dock enabled state + dock_enabled = self.dock_switch.get_active() + self.dock_hover_switch.set_sensitive(dock_enabled) + self.dock_auto_hide_switch.set_sensitive(dock_enabled) + self.dock_hide_special_switch.set_sensitive(dock_enabled) + self.dock_hide_special_apps_switch.set_sensitive(dock_enabled) + + default_dock_theme_val = defaults.get("dock_theme", "Pills") + try: + self.dock_theme_combo.set_active(self.themes.index(default_dock_theme_val)) + except ValueError: + self.dock_theme_combo.set_active(0) + + default_notif_pos_val = defaults.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) + notification_positions_list = ["Top", "Bottom"] + try: + self.notification_pos_combo.set_active( + notification_positions_list.index(default_notif_pos_val) + ) + except ValueError: + self.notification_pos_combo.set_active(0) + + for name, switch in self.component_switches.items(): + switch.set_active(defaults.get(f"dock_{name}_visible", True)) + + # Reset workspace settings + default_dots = defaults.get("workspace_dots", True) + default_nums = defaults.get("workspace_nums", False) + if default_dots: + default_mode = "Dots" + elif default_nums: + default_mode = "Numbers" + else: + default_mode = "Single Button" + + ws_modes = ["Dots", "Numbers", "Single Button"] + try: + self.ws_mode_combo.set_active(ws_modes.index(default_mode)) + except ValueError: + self.ws_mode_combo.set_active(0) + + self.ws_chinese_switch.set_active(defaults.get("workspace_use_chinese_numerals", False)) + self.ws_chinese_switch.set_sensitive(default_nums) + self.corners_switch.set_active(defaults.get("corners_visible", True)) + + # Reset face icon + self.selected_face_icon = None + self.face_status_label.label = "" + current_face = os.path.expanduser("~/.face.icon") + try: + if os.path.exists(current_face): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) + self.face_image.set_from_pixbuf(pixbuf) + else: + self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) + except Exception as e: + print(f"Error loading face icon: {e}") + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + + # Reset scales + self.icon_size_scale.set_value(defaults.get("dock_icon_size", 20)) + self.application_switcher_scale.set_value(defaults.get("window_switcher_items_per_row", 13)) diff --git a/config/settings/gui.py b/config/settings/gui.py index f3ebb19..9cc4b29 100644 --- a/config/settings/gui.py +++ b/config/settings/gui.py @@ -16,10 +16,14 @@ from fabric.widgets.window import Window from gi.repository import Gdk, GdkPixbuf, GLib, Gtk from PIL import Image -from config.data import APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY -from config.settings.utils import backup_and_replace, bind_vars, start_config +from config.data import APP_NAME, APP_NAME_CAP +from config.settings.utils import backup_and_replace, start_config from config.settings.wifi_tab import WiFiTab from config.settings.bluetooth_tab import BluetoothTab +from config.settings.key_bindings_tab import KeyBindingsTab +from config.settings.appearance_tab import AppearanceTab +from config.settings.system_tab import SystemTab +from config.settings.about_tab import AboutTab gi.require_version("Gtk", "3.0") @@ -51,9 +55,13 @@ class HyprConfGUI(Window): self.show_lock_checkbox = show_lock_checkbox self.show_idle_checkbox = show_idle_checkbox - # Initialize WiFi and Bluetooth tabs + # Initialize all tabs self.wifi_tab = WiFiTab() self.bluetooth_tab = BluetoothTab() + self.key_bindings_tab = KeyBindingsTab() + self.appearance_tab = AppearanceTab(self.themes, self) + self.system_tab = SystemTab(show_lock_checkbox, show_idle_checkbox, self.enforce_window_size) + self.about_tab = AboutTab() # Pass window size enforcement method to tabs self.wifi_tab.set_window_size_enforcer(self.enforce_window_size) @@ -80,10 +88,10 @@ class HyprConfGUI(Window): self.wifi_tab_content = self.wifi_tab.create_wifi_tab() self.bluetooth_tab_content = self.bluetooth_tab.create_bluetooth_tab() - self.key_bindings_tab_content = self.create_key_bindings_tab() - self.appearance_tab_content = self.create_appearance_tab() - self.system_tab_content = self.create_system_tab() - self.about_tab_content = self.create_about_tab() + self.key_bindings_tab_content = self.key_bindings_tab.create_key_bindings_tab() + self.appearance_tab_content = self.appearance_tab.create_appearance_tab() + self.system_tab_content = self.system_tab.create_system_tab() + self.about_tab_content = self.about_tab.create_about_tab() self.tab_stack.add_titled( self.wifi_tab_content, "Wifi", "Wifi" @@ -137,967 +145,38 @@ class HyprConfGUI(Window): """Public method to enforce window size - can be called by tabs""" self._force_window_size(self) - def create_key_bindings_tab(self): - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=False, - v_expand=False, - propagate_width=False, - propagate_height=False, - ) - # Set fixed size to match tab stack dimensions - scrolled_window.set_size_request(580, 580) - main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;") - scrolled_window.add(main_vbox) - keybind_grid = Gtk.Grid() - keybind_grid.set_column_spacing(10) - keybind_grid.set_row_spacing(8) - keybind_grid.set_margin_start(5) - keybind_grid.set_margin_end(5) - keybind_grid.set_margin_top(5) - keybind_grid.set_margin_bottom(5) + # Removed create_appearance_tab - now using AppearanceTab class - action_label = Label( - markup="Action", h_align="start", style="margin-bottom: 5px;" - ) - modifier_label = Label( - markup="Modifier", h_align="start", style="margin-bottom: 5px;" - ) - separator_label = Label( - label="+", h_align="center", style="margin-bottom: 5px;" - ) - key_label = Label( - markup="Key", h_align="start", style="margin-bottom: 5px;" - ) + # on_notification_position_changed moved to AppearanceTab class - keybind_grid.attach(action_label, 0, 0, 1, 1) - keybind_grid.attach(modifier_label, 1, 0, 1, 1) - keybind_grid.attach(separator_label, 2, 0, 1, 1) - keybind_grid.attach(key_label, 3, 0, 1, 1) + # Removed create_system_tab - now using SystemTab class + # Removed create_about_tab - now using AboutTab class - self.entries = [] - bindings = [ - (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"), - ("Message", "prefix_msg", "suffix_msg"), - ( - "Application Switcher", - "prefix_application_switcher", - "suffix_application_switcher", - ), - ("Launcher", "prefix_launcher", "suffix_launcher"), - ("App Launcher", "prefix_app_launcher", "suffix_app_launcher"), - ("Clipboard History", "prefix_cliphist", "suffix_cliphist"), - ("Wallpapers", "prefix_wallpapers", "suffix_wallpapers"), - ("Random Wallpaper", "prefix_randwall", "suffix_randwall"), - ("Emoji Picker", "prefix_emoji", "suffix_emoji"), - ("Kanban", "prefix_kanban", "suffix_kanban"), - ("Power Menu", "prefix_power", "suffix_power"), - ("Toggle Caffeine", "prefix_caffeine", "suffix_caffeine"), - ("Settings", "prefix_settings", "suffix_settings"), - ( - "Restart with inspector", - "prefix_restart_inspector", - "suffix_restart_inspector", - ), - ] - for i, (label_text, prefix_key, suffix_key) in enumerate(bindings): - row = i + 1 - binding_label = Label(label=label_text, h_align="start") - keybind_grid.attach(binding_label, 0, row, 1, 1) - prefix_entry = Entry(text=bind_vars.get(prefix_key, "")) - keybind_grid.attach(prefix_entry, 1, row, 1, 1) - plus_label = Label(label="+", h_align="center") - keybind_grid.attach(plus_label, 2, row, 1, 1) - suffix_entry = Entry(text=bind_vars.get(suffix_key, "")) - keybind_grid.attach(suffix_entry, 3, row, 1, 1) - self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry)) + # Event handlers moved to respective tab classes - main_vbox.add(keybind_grid) - return scrolled_window - - def create_appearance_tab(self): - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=False, - v_expand=False, - propagate_width=False, - propagate_height=False, - ) - # Set fixed size to match tab stack dimensions - scrolled_window.set_size_request(580, 580) - - vbox = Box(orientation="v", spacing=15, style="margin: 15px;") - scrolled_window.add(vbox) - - top_grid = Gtk.Grid() - top_grid.set_column_spacing(20) - top_grid.set_row_spacing(5) - top_grid.set_margin_bottom(10) - vbox.add(top_grid) - - wall_header = Label(markup="Wallpapers", h_align="start") - top_grid.attach(wall_header, 0, 0, 1, 1) - wall_label = Label(label="Directory:", h_align="start", v_align="center") - top_grid.attach(wall_label, 0, 1, 1, 1) - - chooser_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - chooser_container.set_halign(Gtk.Align.START) - chooser_container.set_valign(Gtk.Align.CENTER) - self.wall_dir_chooser = Gtk.FileChooserButton( - title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER - ) - self.wall_dir_chooser.set_tooltip_text( - "Select the directory containing your wallpaper images" - ) - self.wall_dir_chooser.set_filename(bind_vars.get("wallpapers_dir", "")) - self.wall_dir_chooser.set_size_request(180, -1) - chooser_container.add(self.wall_dir_chooser) - top_grid.attach(chooser_container, 1, 1, 1, 1) - - face_header = Label(markup="Profile Icon", h_align="start") - top_grid.attach(face_header, 2, 0, 2, 1) - current_face = os.path.expanduser("~/.face.icon") - face_image_container = Box( - style_classes=["image-frame"], h_align="center", v_align="center" - ) - self.face_image = FabricImage(size=64) - try: - if os.path.exists(current_face): - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) - self.face_image.set_from_pixbuf(pixbuf) - else: - self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) - except Exception as e: - print(f"Error loading face icon: {e}") - self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) - face_image_container.add(self.face_image) - top_grid.attach(face_image_container, 2, 1, 1, 1) - - browse_btn_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - browse_btn_container.set_halign(Gtk.Align.START) - browse_btn_container.set_valign(Gtk.Align.CENTER) - face_btn = Button( - label="Browse...", - tooltip_text="Select a square image for your profile icon", - on_clicked=self.on_select_face_icon, - ) - browse_btn_container.add(face_btn) - top_grid.attach(browse_btn_container, 3, 1, 1, 1) - self.face_status_label = Label(label="", h_align="start") - top_grid.attach(self.face_status_label, 2, 2, 2, 1) - - separator1 = Box( - style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", - h_expand=True, - ) - vbox.add(separator1) - - layout_header = Label(markup="Layout Options", h_align="start") - vbox.add(layout_header) - layout_grid = Gtk.Grid() - layout_grid.set_column_spacing(20) - layout_grid.set_row_spacing(10) - layout_grid.set_margin_start(10) - layout_grid.set_margin_top(5) - vbox.add(layout_grid) - - # Workspace mode - ws_mode_label = Label(label="Workspace Mode", h_align="start", v_align="center") - layout_grid.attach(ws_mode_label, 0, 0, 1, 1) - - ws_mode_combo_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.ws_mode_combo = Gtk.ComboBoxText() - self.ws_mode_combo.set_tooltip_text("Select how workspaces are displayed") - ws_modes = ["Dots", "Numbers", "Single Button"] - for mode in ws_modes: - self.ws_mode_combo.append_text(mode) - - # Set current mode based on config - current_dots = bind_vars.get("workspace_dots", True) - current_nums = bind_vars.get("workspace_nums", False) - if current_dots: - current_mode = "Dots" - elif current_nums: - current_mode = "Numbers" - else: - current_mode = "Single Button" - - try: - self.ws_mode_combo.set_active(ws_modes.index(current_mode)) - except ValueError: - self.ws_mode_combo.set_active(0) - - self.ws_mode_combo.connect("changed", self.on_ws_mode_changed) - ws_mode_combo_container.add(self.ws_mode_combo) - layout_grid.attach(ws_mode_combo_container, 1, 0, 1, 1) - - # Chinese numerals option - ws_chinese_label = Label( - label="Use Chinese Numerals", h_align="start", v_align="center" - ) - layout_grid.attach(ws_chinese_label, 2, 0, 1, 1) - ws_chinese_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.ws_chinese_switch = Gtk.Switch( - active=bind_vars.get("workspace_use_chinese_numerals", False), - sensitive=current_nums, # Only enabled when numbers mode is selected - ) - self.ws_chinese_switch.set_tooltip_text( - "Use Chinese numerals (一, 二, 三...) instead of Arabic numbers" - ) - ws_chinese_switch_container.add(self.ws_chinese_switch) - layout_grid.attach(ws_chinese_switch_container, 3, 0, 1, 1) - - position_label = Label(label="Dock Position", h_align="start", v_align="center") - layout_grid.attach(position_label, 0, 1, 1, 1) - position_combo_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.position_combo = Gtk.ComboBoxText() - self.position_combo.set_tooltip_text("Select the position of the dock") - positions = ["Bottom", "Top", "Left", "Right"] - for pos in positions: - self.position_combo.append_text(pos) - current_position = bind_vars.get("dock_position", "Bottom") - try: - self.position_combo.set_active(positions.index(current_position)) - except ValueError: - self.position_combo.set_active(0) - self.position_combo.connect("changed", self.on_position_changed) - position_combo_container.add(self.position_combo) - layout_grid.attach(position_combo_container, 1, 1, 1, 1) - - dock_theme_label = Label(label="Dock Theme", h_align="start", v_align="center") - layout_grid.attach(dock_theme_label, 2, 1, 1, 1) - dock_theme_combo_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_theme_combo = Gtk.ComboBoxText() - self.dock_theme_combo.set_tooltip_text("Select the visual theme for the dock") - for theme in self.themes: - self.dock_theme_combo.append_text(theme) - current_dock_theme = bind_vars.get("dock_theme", "Pills") - try: - self.dock_theme_combo.set_active(self.themes.index(current_dock_theme)) - except ValueError: - self.dock_theme_combo.set_active(0) - dock_theme_combo_container.add(self.dock_theme_combo) - layout_grid.attach(dock_theme_combo_container, 3, 1, 1, 1) - - # Add icon size scale - icon_size_label = Label( - label="Taskbar Icon Size", h_align="start", v_align="center" - ) - layout_grid.attach(icon_size_label, 0, 4, 1, 1) - - icon_size_scale_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - hexpand=True, - ) - - self.icon_size_scale = Gtk.Scale.new_with_range( - Gtk.Orientation.HORIZONTAL, 20, 30, 2 - ) - self.icon_size_scale.set_value(bind_vars.get("dock_icon_size", 20)) - self.icon_size_scale.set_size_request(150, -1) - self.icon_size_scale.set_draw_value(True) - self.icon_size_scale.set_value_pos(Gtk.PositionType.RIGHT) - self.icon_size_scale.set_tooltip_text("Adjust the size of dock icons") - - icon_size_scale_container.add(self.icon_size_scale) - layout_grid.attach(icon_size_scale_container, 1, 4, 1, 1) - - # Add application switcher items scale - application_switcher_label = Label( - label="Application Switcher Items", h_align="start", v_align="center" - ) - layout_grid.attach(application_switcher_label, 2, 4, 1, 1) - - application_switcher_scale_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - hexpand=True, - ) - - self.application_switcher_scale = Gtk.Scale.new_with_range( - Gtk.Orientation.HORIZONTAL, 5, 20, 1 - ) - self.application_switcher_scale.set_value( - bind_vars.get("window_switcher_items_per_row", 13) - ) - self.application_switcher_scale.set_size_request(150, -1) - self.application_switcher_scale.set_draw_value(True) - self.application_switcher_scale.set_value_pos(Gtk.PositionType.RIGHT) - self.application_switcher_scale.set_tooltip_text( - "Adjust the number of items per row in the application switcher" - ) - - application_switcher_scale_container.add(self.application_switcher_scale) - layout_grid.attach(application_switcher_scale_container, 3, 4, 1, 1) - - dock_label = Label(label="Show Dock", h_align="start", v_align="center") - layout_grid.attach(dock_label, 0, 2, 1, 1) - dock_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_switch = Gtk.Switch(active=bind_vars.get("dock_enabled", True)) - self.dock_switch.connect("notify::active", self.on_dock_enabled_changed) - dock_switch_container.add(self.dock_switch) - layout_grid.attach(dock_switch_container, 1, 2, 1, 1) - - dock_auto_hide_label = Label( - label="Auto-hide Dock", h_align="start", v_align="center" - ) - layout_grid.attach(dock_auto_hide_label, 0, 3, 1, 1) - dock_auto_hide_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_auto_hide_switch = Gtk.Switch( - active=bind_vars.get("dock_auto_hide", False), - sensitive=self.dock_switch.get_active(), - ) - dock_auto_hide_switch_container.add(self.dock_auto_hide_switch) - layout_grid.attach(dock_auto_hide_switch_container, 1, 3, 1, 1) - - dock_hover_label = Label( - label="Show Dock Only on Hover", h_align="start", v_align="center" - ) - layout_grid.attach(dock_hover_label, 2, 2, 1, 1) - dock_hover_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_hover_switch = Gtk.Switch( - active=bind_vars.get("dock_always_occluded", False), - sensitive=self.dock_switch.get_active(), - ) - dock_hover_switch_container.add(self.dock_hover_switch) - layout_grid.attach(dock_hover_switch_container, 3, 2, 1, 1) - - # Add dock hide special workspace option - dock_hide_special_label = Label( - label="Hide Special Workspace", h_align="start", v_align="center" - ) - layout_grid.attach(dock_hide_special_label, 2, 3, 1, 1) - dock_hide_special_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_hide_special_switch = Gtk.Switch( - active=bind_vars.get("dock_hide_special_workspace", True), - sensitive=self.dock_switch.get_active(), - ) - self.dock_hide_special_switch.set_tooltip_text( - "Hide the dock when on special workspaces (scratchpad)" - ) - dock_hide_special_switch_container.add(self.dock_hide_special_switch) - layout_grid.attach(dock_hide_special_switch_container, 3, 3, 1, 1) - - # Add dock hide special workspace apps option - dock_hide_special_apps_label = Label( - label="Hide Special Workspace Apps", h_align="start", v_align="center" - ) - layout_grid.attach(dock_hide_special_apps_label, 2, 5, 1, 1) - dock_hide_special_apps_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.dock_hide_special_apps_switch = Gtk.Switch( - active=bind_vars.get("dock_hide_special_workspace_apps", True), - sensitive=self.dock_switch.get_active(), - ) - self.dock_hide_special_apps_switch.set_tooltip_text( - "Hide applications that are in special workspaces (scratchpad) from the dock" - ) - dock_hide_special_apps_switch_container.add(self.dock_hide_special_apps_switch) - layout_grid.attach(dock_hide_special_apps_switch_container, 3, 5, 1, 1) - - notification_pos_label = Label( - label="Notification Position", h_align="start", v_align="center" - ) - layout_grid.attach(notification_pos_label, 0, 8, 1, 1) - - notification_pos_combo_container = Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - - self.notification_pos_combo = Gtk.ComboBoxText() - self.notification_pos_combo.set_tooltip_text( - "Select where notifications appear on the screen." - ) - - notification_positions_list = ["Top", "Bottom"] - for pos in notification_positions_list: - self.notification_pos_combo.append_text(pos) - - current_notif_pos = bind_vars.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) - try: - self.notification_pos_combo.set_active( - notification_positions_list.index(current_notif_pos) - ) - except ValueError: - self.notification_pos_combo.set_active(0) - - self.notification_pos_combo.connect( - "changed", self.on_notification_position_changed - ) - - notification_pos_combo_container.add(self.notification_pos_combo) - layout_grid.attach(notification_pos_combo_container, 1, 8, 3, 1) - - separator2 = Box( - style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", - h_expand=True, - ) - vbox.add(separator2) - - components_header = Label(markup="Modules", h_align="start") - vbox.add(components_header) - components_grid = Gtk.Grid() - components_grid.set_column_spacing(15) - components_grid.set_row_spacing(8) - components_grid.set_margin_start(10) - components_grid.set_margin_top(5) - vbox.add(components_grid) - - self.component_switches = {} - component_display_names = { - "workspace": "Workspaces", - "metrics": "System Metrics", - "date_time": "Date & Time", - "battery": "Battery Indicator", - "controls": "Control Panel", - "indicators": "Indicators", - "music_player": "Music Player", - "applications": "Taskbar", - "language": "Language Indicator", - } - - self.corners_switch = Gtk.Switch(active=bind_vars.get("corners_visible", True)) - num_components = len(component_display_names) + 1 - rows_per_column = (num_components + 1) // 2 - - corners_label = Label( - label="Rounded Corners", h_align="start", v_align="center" - ) - components_grid.attach(corners_label, 0, 0, 1, 1) - switch_container_corners = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - switch_container_corners.add(self.corners_switch) - components_grid.attach(switch_container_corners, 1, 0, 1, 1) - - current_row = 0 - current_col = 0 - item_idx = 0 - for i, (name, display) in enumerate(component_display_names.items()): - if item_idx < (rows_per_column - 1): - row = item_idx + 1 - col = 0 - else: - row = item_idx - (rows_per_column - 1) - col = 2 - - component_label = Label(label=display, h_align="start", v_align="center") - components_grid.attach(component_label, col, row, 1, 1) - - switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - component_switch = Gtk.Switch( - active=bind_vars.get(f"dock_{name}_visible", True) - ) - switch_container.add(component_switch) - components_grid.attach(switch_container, col + 1, row, 1, 1) - self.component_switches[name] = component_switch - item_idx += 1 - - return scrolled_window - - def on_notification_position_changed(self, combo: Gtk.ComboBoxText): - selected_text = combo.get_active_text() - if selected_text: - bind_vars[NOTIF_POS_KEY] = selected_text - print( - f"Notification position updated in bind_vars: { - bind_vars[NOTIF_POS_KEY] - }" - ) - - def create_system_tab(self): - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=False, - v_expand=False, - propagate_width=False, - propagate_height=False, - ) - # Set fixed size to match tab stack dimensions - scrolled_window.set_size_request(580, 580) - - vbox = Box(orientation="v", spacing=15, style="margin: 15px;") - scrolled_window.add(vbox) - - system_grid = Gtk.Grid() - system_grid.set_column_spacing(20) - system_grid.set_row_spacing(10) - system_grid.set_margin_bottom(15) - vbox.add(system_grid) - - terminal_header = Label(markup="Terminal Settings", h_align="start") - system_grid.attach(terminal_header, 0, 0, 2, 1) - terminal_label = Label(label="Command:", h_align="start", v_align="center") - system_grid.attach(terminal_label, 0, 1, 1, 1) - self.terminal_entry = Entry( - text=bind_vars.get("terminal_command", "kitty -e"), - tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')", - h_expand=True, - ) - system_grid.attach(self.terminal_entry, 1, 1, 1, 1) - hint_label = Label( - markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'", - h_align="start", - ) - system_grid.attach(hint_label, 0, 2, 2, 1) - - hypr_header = Label(markup="Hyprland Integration", h_align="start") - system_grid.attach(hypr_header, 2, 0, 2, 1) - row = 1 - self.lock_switch = None - if self.show_lock_checkbox: - lock_label = Label( - label="Replace Hyprlock config", h_align="start", v_align="center" - ) - system_grid.attach(lock_label, 2, row, 1, 1) - lock_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.lock_switch = Gtk.Switch( - tooltip_text="Replace Hyprlock configuration with Modus's custom config" - ) - lock_switch_container.add(self.lock_switch) - system_grid.attach(lock_switch_container, 3, row, 1, 1) - row += 1 - self.idle_switch = None - if self.show_idle_checkbox: - idle_label = Label( - label="Replace Hypridle config", h_align="start", v_align="center" - ) - system_grid.attach(idle_label, 2, row, 1, 1) - idle_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - self.idle_switch = Gtk.Switch( - tooltip_text="Replace Hypridle configuration with Modus's custom config" - ) - idle_switch_container.add(self.idle_switch) - system_grid.attach(idle_switch_container, 3, row, 1, 1) - row += 1 - if self.show_lock_checkbox or self.show_idle_checkbox: - note_label = Label( - markup="Existing configs will be backed up", - h_align="start", - ) - system_grid.attach(note_label, 2, row, 2, 1) - - # Notifications app lists section - notifications_header = Label( - markup="Notification Settings", h_align="start" - ) - vbox.add(notifications_header) - - notif_grid = Gtk.Grid() - notif_grid.set_column_spacing(20) - notif_grid.set_row_spacing(10) - notif_grid.set_margin_start(10) - notif_grid.set_margin_top(5) - notif_grid.set_margin_bottom(15) - vbox.add(notif_grid) - - # Limited Apps History - limited_apps_label = Label( - label="Limited Apps History:", h_align="start", v_align="center" - ) - notif_grid.attach(limited_apps_label, 0, 0, 1, 1) - - limited_apps_list = bind_vars.get("limited_apps_history", ["Spotify"]) - limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list) - self.limited_apps_entry = Entry( - text=limited_apps_text, - tooltip_text='Enter app names separated by commas, e.g: "Spotify", "Discord"', - h_expand=True, - ) - notif_grid.attach(self.limited_apps_entry, 1, 0, 1, 1) - - limited_apps_hint = Label( - markup='Apps with limited notification history (format: "App1", "App2")', - h_align="start", - ) - notif_grid.attach(limited_apps_hint, 0, 1, 2, 1) - - # History Ignored Apps - ignored_apps_label = Label( - label="History Ignored Apps:", h_align="start", v_align="center" - ) - notif_grid.attach(ignored_apps_label, 0, 2, 1, 1) - - ignored_apps_list = bind_vars.get("history_ignored_apps", ["Modus"]) - ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list) - self.ignored_apps_entry = Entry( - text=ignored_apps_text, - tooltip_text='Enter app names separated by commas, e.g: "Modus", "Screenshot"', - h_expand=True, - ) - notif_grid.attach(self.ignored_apps_entry, 1, 2, 1, 1) - - ignored_apps_hint = Label( - markup='Apps whose notifications are ignored in history (format: "App1", "App2")', - h_align="start", - ) - notif_grid.attach(ignored_apps_hint, 0, 3, 2, 1) - - - metrics_header = Label(markup="System Metrics Options", h_align="start") - vbox.add(metrics_header) - metrics_grid = Gtk.Grid( - column_spacing=15, row_spacing=8, margin_start=10, margin_top=5 - ) - vbox.add(metrics_grid) - - self.metrics_switches = {} - metric_names = { - "cpu": "CPU", - "ram": "RAM", - "swap": "Swap", - "disk": "Disk", - "gpu": "GPU", - } - - metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 0, 0, 1, 1) - for i, (key, label_text) in enumerate(metric_names.items()): - switch = Gtk.Switch( - active=bind_vars.get("metrics_visible", {}).get(key, True) - ) - self.metrics_switches[key] = switch - metrics_grid.attach( - Label(label=label_text, h_align="start"), 0, i + 1, 1, 1 - ) - metrics_grid.attach(switch, 1, i + 1, 1, 1) - - def enforce_minimum_metrics(switch_dict): - enabled_switches = [s for s in switch_dict.values() if s.get_active()] - can_disable = len(enabled_switches) > 1 - for s in switch_dict.values(): - s.set_sensitive(True if can_disable or not s.get_active() else False) - - def on_metric_toggle(switch, gparam, switch_dict): - enforce_minimum_metrics(switch_dict) - - for k_s, s_s in self.metrics_switches.items(): - s_s.connect("notify::active", on_metric_toggle, self.metrics_switches) - enforce_minimum_metrics(self.metrics_switches) - - disks_label = Label( - label="Disk directories for Metrics", h_align="start", v_align="center" - ) - vbox.add(disks_label) - - # Create a scrolled container for disk entries to prevent overflow - disk_entries_scrolled = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=False, - v_expand=False, - propagate_width=False, - propagate_height=False, - ) - # Set fixed size for disk entries container - disk_entries_scrolled.set_size_request(550, 120) - - self.disk_entries = Box(orientation="v", spacing=8, h_align="start") - # Set size constraints for the disk entries box - self.disk_entries.set_size_request(530, -1) - disk_entries_scrolled.add(self.disk_entries) - - self._create_disk_edit_entry_func = lambda path: self._add_disk_entry_widget( - path - ) - - for p in bind_vars.get("metrics_disks", ["/"]): - self._create_disk_edit_entry_func(p) - vbox.add(disk_entries_scrolled) - - add_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - halign=Gtk.Align.START, - valign=Gtk.Align.CENTER, - ) - add_btn = Button( - label="Add new disk", - on_clicked=lambda _: self._create_disk_edit_entry_func("/"), - ) - add_container.add(add_btn) - vbox.add(add_container) - - return scrolled_window - - def _add_disk_entry_widget(self, path): - """Helper to add a disk entry row to the disk_entries Box.""" - bar = Box(orientation="h", spacing=10, h_align="start") - # Set fixed height for disk entry rows - bar.set_size_request(-1, 30) - entry = Entry(text=path, h_expand=True) - bar.add(entry) - x_btn = Button(label="X") - x_btn.connect( - "clicked", - lambda _, current_bar_to_remove=bar: self._remove_disk_entry(current_bar_to_remove), - ) - bar.add(x_btn) - self.disk_entries.add(bar) - self.disk_entries.show_all() - # Enforce window size after adding content - GLib.idle_add(self.enforce_window_size) - - def _remove_disk_entry(self, bar_to_remove): - """Helper to remove a disk entry and enforce window size""" - self.disk_entries.remove(bar_to_remove) - # Enforce window size after removing content - GLib.idle_add(self.enforce_window_size) - - def create_about_tab(self): - vbox = Box(orientation="v", spacing=18, style="margin: 30px;") - vbox.add( - Label( - markup=f"{APP_NAME_CAP}", - h_align="start", - style="font-size: 1.5em; margin-bottom: 8px;", - ) - ) - vbox.add( - Label( - label="A hackable shell for Hyprland, powered by Fabric.", - h_align="start", - style="margin-bottom: 12px;", - ) - ) - repo_box = Box(orientation="h", spacing=6, h_align="start") - repo_label = Label(label="GitHub:", h_align="start") - repo_link = Label( - markup='https://github.com/S4NKALP/Modus' - ) - repo_box.add(repo_label) - repo_box.add(repo_link) - vbox.add(repo_box) - - vbox.add(Box(v_expand=True)) - return vbox - - def on_ws_mode_changed(self, combo): - mode = combo.get_active_text() - is_numbers_mode = mode == "Numbers" - self.ws_chinese_switch.set_sensitive(is_numbers_mode) - if not is_numbers_mode: - self.ws_chinese_switch.set_active(False) - - def on_ws_num_changed(self, switch, gparam): - is_active = switch.get_active() - self.ws_chinese_switch.set_sensitive(is_active) - if not is_active: - self.ws_chinese_switch.set_active(False) - - def on_position_changed(self, combo): - position = combo.get_active_text() - is_vertical = position in ["Left", "Right"] - self.centered_switch.set_sensitive(is_vertical) - if not is_vertical: - self.centered_switch.set_active(False) - - def on_dock_enabled_changed(self, switch, gparam): - is_active = switch.get_active() - self.dock_hover_switch.set_sensitive(is_active) - self.dock_auto_hide_switch.set_sensitive(is_active) - self.dock_hide_special_switch.set_sensitive(is_active) - self.dock_hide_special_apps_switch.set_sensitive(is_active) - if not is_active: - self.dock_hover_switch.set_active(False) - self.dock_auto_hide_switch.set_active(False) - self.dock_hide_special_switch.set_active(False) - self.dock_hide_special_apps_switch.set_active(False) - - def on_select_face_icon(self, widget): - dialog = Gtk.FileChooserDialog( - title="Select Face Icon", - transient_for=self.get_toplevel(), - action=Gtk.FileChooserAction.OPEN, - ) - dialog.add_buttons( - Gtk.STOCK_CANCEL, - Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, - Gtk.ResponseType.OK, - ) - image_filter = Gtk.FileFilter() - image_filter.set_name("Image files") - for mime in ["image/png", "image/jpeg"]: - image_filter.add_mime_type(mime) - for pattern in ["*.png", "*.jpg", "*.jpeg"]: - image_filter.add_pattern(pattern) - dialog.add_filter(image_filter) - if dialog.run() == Gtk.ResponseType.OK: - self.selected_face_icon = dialog.get_filename() - self.face_status_label.label = ( - f"Selected: {os.path.basename(self.selected_face_icon)}" - ) - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( - self.selected_face_icon, 64, 64 - ) - self.face_image.set_from_pixbuf(pixbuf) - except Exception as e: - print(f"Error loading selected face icon preview: {e}") - self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) - dialog.destroy() - - def on_accept(self, widget): + def on_accept(self, _widget): current_bind_vars_snapshot = {} - for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: - current_bind_vars_snapshot[prefix_key] = prefix_entry.get_text() - current_bind_vars_snapshot[suffix_key] = suffix_entry.get_text() - current_bind_vars_snapshot["wallpapers_dir"] = ( - self.wall_dir_chooser.get_filename() - ) + # Get values from key bindings tab + current_bind_vars_snapshot.update(self.key_bindings_tab.get_key_binding_values()) - current_bind_vars_snapshot["dock_position"] = ( - self.position_combo.get_active_text() - ) - current_bind_vars_snapshot["vertical"] = current_bind_vars_snapshot[ - "dock_position" - ] in ["Left", "Right"] + # Get values from appearance tab + current_bind_vars_snapshot.update(self.appearance_tab.get_appearance_values()) - current_bind_vars_snapshot["dock_enabled"] = self.dock_switch.get_active() - current_bind_vars_snapshot["dock_auto_hide"] = ( - self.dock_auto_hide_switch.get_active() - ) - current_bind_vars_snapshot["dock_always_occluded"] = ( - self.dock_hover_switch.get_active() - ) - current_bind_vars_snapshot["dock_hide_special_workspace"] = ( - self.dock_hide_special_switch.get_active() - ) - current_bind_vars_snapshot["dock_hide_special_workspace_apps"] = ( - self.dock_hide_special_apps_switch.get_active() - ) - current_bind_vars_snapshot["dock_icon_size"] = int( - self.icon_size_scale.get_value() - ) - current_bind_vars_snapshot["window_switcher_items_per_row"] = int( - self.application_switcher_scale.get_value() - ) - current_bind_vars_snapshot["terminal_command"] = self.terminal_entry.get_text() - current_bind_vars_snapshot["corners_visible"] = self.corners_switch.get_active() - current_bind_vars_snapshot["dock_theme"] = ( - self.dock_theme_combo.get_active_text() - ) + # Get values from system tab + current_bind_vars_snapshot.update(self.system_tab.get_system_values()) - selected_notif_pos_text = self.notification_pos_combo.get_active_text() - if selected_notif_pos_text: - current_bind_vars_snapshot[NOTIF_POS_KEY] = selected_notif_pos_text - else: - current_bind_vars_snapshot[NOTIF_POS_KEY] = NOTIF_POS_DEFAULT + # Get face icon and hyprland switches + selected_icon_path = self.appearance_tab.get_selected_face_icon() + hyprland_switches = self.system_tab.get_hyprland_switches() + replace_lock = hyprland_switches["replace_lock"] + replace_idle = hyprland_switches["replace_idle"] - for component_name, switch in self.component_switches.items(): - current_bind_vars_snapshot[f"dock_{component_name}_visible"] = ( - switch.get_active() - ) - - # Save workspace settings - ws_mode = self.ws_mode_combo.get_active_text() - current_bind_vars_snapshot["workspace_dots"] = ws_mode == "Dots" - current_bind_vars_snapshot["workspace_nums"] = ws_mode == "Numbers" - current_bind_vars_snapshot["workspace_use_chinese_numerals"] = ( - self.ws_chinese_switch.get_active() - ) - - current_bind_vars_snapshot["metrics_visible"] = { - k: s.get_active() for k, s in self.metrics_switches.items() - } - current_bind_vars_snapshot["metrics_disks"] = [ - child.get_children()[0].get_text() - for child in self.disk_entries.get_children() - if isinstance(child, Gtk.Box) - and child.get_children() - and isinstance(child.get_children()[0], Entry) - ] - - # Parse notification app lists - def parse_app_list(text): - """Parse comma-separated app names with quotes""" - if not text.strip(): - return [] - apps = [] - for app in text.split(","): - app = app.strip() - if app.startswith('"') and app.endswith('"'): - app = app[1:-1] - elif app.startswith("'") and app.endswith("'"): - app = app[1:-1] - if app: - apps.append(app) - return apps - - current_bind_vars_snapshot["limited_apps_history"] = parse_app_list( - self.limited_apps_entry.get_text() - ) - current_bind_vars_snapshot["history_ignored_apps"] = parse_app_list( - self.ignored_apps_entry.get_text() - ) - - selected_icon_path = self.selected_face_icon - replace_lock = self.lock_switch and self.lock_switch.get_active() - replace_idle = self.idle_switch and self.idle_switch.get_active() - - if self.selected_face_icon: - self.selected_face_icon = None - self.face_status_label.label = "" + if selected_icon_path: + self.appearance_tab.clear_selected_face_icon() def _apply_and_reload_task_thread(): nonlocal current_bind_vars_snapshot @@ -1244,7 +323,7 @@ class HyprConfGUI(Window): self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) return GLib.SOURCE_REMOVE - def on_reset(self, widget): + def on_reset(self, _widget): dialog = Gtk.MessageDialog( transient_for=self.get_toplevel(), flags=0, @@ -1270,139 +349,15 @@ class HyprConfGUI(Window): utils.bind_vars.clear() utils.bind_vars.update(DEFAULTS.copy()) - for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: - prefix_entry.set_text(utils.bind_vars[prefix_key]) - suffix_entry.set_text(utils.bind_vars[suffix_key]) - - self.wall_dir_chooser.set_filename(utils.bind_vars["wallpapers_dir"]) - - positions = ["Bottom", "Top", "Left", "Right"] - default_position = DEFAULTS.get("dock_position", "Bottom") - try: - self.position_combo.set_active(positions.index(default_position)) - except ValueError: - self.position_combo.set_active(0) - - self.dock_switch.set_active(utils.bind_vars.get("dock_enabled", True)) - self.dock_auto_hide_switch.set_active( - utils.bind_vars.get("dock_auto_hide", True) - ) - self.dock_hover_switch.set_active( - utils.bind_vars.get("dock_always_occluded", False) - ) - self.dock_hide_special_switch.set_active( - utils.bind_vars.get("dock_hide_special_workspace", True) - ) - self.dock_hide_special_apps_switch.set_active( - utils.bind_vars.get("dock_hide_special_workspace_apps", True) - ) - self.dock_hover_switch.set_sensitive(self.dock_switch.get_active()) - self.dock_auto_hide_switch.set_sensitive(self.dock_switch.get_active()) - self.dock_hide_special_switch.set_sensitive(self.dock_switch.get_active()) - self.dock_hide_special_apps_switch.set_sensitive(self.dock_switch.get_active()) - self.terminal_entry.set_text(utils.bind_vars["terminal_command"]) - - default_dock_theme_val = DEFAULTS.get("dock_theme", "Pills") - try: - self.dock_theme_combo.set_active( - self.themes.index(default_dock_theme_val) - ) - except ValueError: - self.dock_theme_combo.set_active(0) - - default_notif_pos_val = DEFAULTS.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) - notification_positions_list = ["Top", "Bottom"] - try: - self.notification_pos_combo.set_active( - notification_positions_list.index(default_notif_pos_val) - ) - except ValueError: - self.notification_pos_combo.set_active(0) - - for name, switch in self.component_switches.items(): - switch.set_active(utils.bind_vars.get(f"dock_{name}_visible", True)) - - # Reset workspace settings - default_dots = DEFAULTS.get("workspace_dots", True) - default_nums = DEFAULTS.get("workspace_nums", False) - if default_dots: - default_mode = "Dots" - elif default_nums: - default_mode = "Numbers" - else: - default_mode = "Single Button" - - ws_modes = ["Dots", "Numbers", "Single Button"] - try: - self.ws_mode_combo.set_active(ws_modes.index(default_mode)) - except ValueError: - self.ws_mode_combo.set_active(0) - - self.ws_chinese_switch.set_active( - utils.bind_vars.get("workspace_use_chinese_numerals", False) - ) - self.ws_chinese_switch.set_sensitive(default_nums) - self.corners_switch.set_active(utils.bind_vars.get("corners_visible", True)) - - metrics_vis_defaults = DEFAULTS.get("metrics_visible", {}) - for k, s_widget in self.metrics_switches.items(): - s_widget.set_active(metrics_vis_defaults.get(k, True)) - - def enforce_minimum_metrics(switch_dict): - enabled_switches = [s for s in switch_dict.values() if s.get_active()] - can_disable = len(enabled_switches) > 1 - for s in switch_dict.values(): - s.set_sensitive( - True if can_disable or not s.get_active() else False - ) - - enforce_minimum_metrics(self.metrics_switches) - - for child in list(self.disk_entries.get_children()): - self.disk_entries.remove(child) - - for p in DEFAULTS.get("metrics_disks", ["/"]): - self._add_disk_edit_entry_func(p) - - # Reset notification app lists - limited_apps_list = DEFAULTS.get("limited_apps_history", ["Spotify"]) - limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list) - self.limited_apps_entry.set_text(limited_apps_text) - - ignored_apps_list = DEFAULTS.get("history_ignored_apps", ["Hyprshot"]) - ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list) - self.ignored_apps_entry.set_text(ignored_apps_text) + # Reset all tabs to defaults + self.key_bindings_tab.reset_to_defaults(utils.bind_vars) + self.appearance_tab.reset_to_defaults(utils.bind_vars) + self.system_tab.reset_to_defaults(utils.bind_vars) self._update_panel_position_sensitivity() - - self.selected_face_icon = None - self.face_status_label.label = "" - current_face = os.path.expanduser("~/.face.icon") - try: - pixbuf = ( - GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) - if os.path.exists(current_face) - else None - ) - if pixbuf: - self.face_image.set_from_pixbuf(pixbuf) - else: - self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) - except Exception: - self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) - - if self.lock_switch: - self.lock_switch.set_active(False) - if self.idle_switch: - self.idle_switch.set_active(False) - # Add reset for icon size scale - self.icon_size_scale.set_value(utils.bind_vars.get("dock_icon_size", 20)) - self.application_switcher_scale.set_value( - utils.bind_vars.get("window_switcher_items_per_row", 13) - ) print("Settings reset to defaults.") - def on_close(self, widget): + def on_close(self, _widget): if self.application: self.application.quit() else: diff --git a/config/settings/key_bindings_tab.py b/config/settings/key_bindings_tab.py new file mode 100644 index 0000000..e5d392d --- /dev/null +++ b/config/settings/key_bindings_tab.py @@ -0,0 +1,115 @@ +import gi +from fabric.widgets.box import Box +from fabric.widgets.entry import Entry +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import Gtk + +from config.data import APP_NAME_CAP +from config.settings.utils import bind_vars + +gi.require_version("Gtk", "3.0") + + +class KeyBindingsTab: + """Key bindings management tab for settings""" + + def __init__(self): + self.entries = [] + + def create_key_bindings_tab(self): + """Create the Key Bindings tab content""" + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=False, + v_expand=False, + propagate_width=False, + propagate_height=False, + ) + # Set fixed size to match tab stack dimensions + scrolled_window.set_size_request(580, 580) + + main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;") + scrolled_window.add(main_vbox) + + keybind_grid = Gtk.Grid() + keybind_grid.set_column_spacing(10) + keybind_grid.set_row_spacing(8) + keybind_grid.set_margin_start(5) + keybind_grid.set_margin_end(5) + keybind_grid.set_margin_top(5) + keybind_grid.set_margin_bottom(5) + + action_label = Label( + markup="Action", h_align="start", style="margin-bottom: 5px;" + ) + modifier_label = Label( + markup="Modifier", h_align="start", style="margin-bottom: 5px;" + ) + separator_label = Label( + label="+", h_align="center", style="margin-bottom: 5px;" + ) + key_label = Label( + markup="Key", h_align="start", style="margin-bottom: 5px;" + ) + + keybind_grid.attach(action_label, 0, 0, 1, 1) + keybind_grid.attach(modifier_label, 1, 0, 1, 1) + keybind_grid.attach(separator_label, 2, 0, 1, 1) + keybind_grid.attach(key_label, 3, 0, 1, 1) + + self.entries = [] + bindings = [ + (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"), + ("Message", "prefix_msg", "suffix_msg"), + ( + "Application Switcher", + "prefix_application_switcher", + "suffix_application_switcher", + ), + ("Launcher", "prefix_launcher", "suffix_launcher"), + ("App Launcher", "prefix_app_launcher", "suffix_app_launcher"), + ("Clipboard History", "prefix_cliphist", "suffix_cliphist"), + ("Wallpapers", "prefix_wallpapers", "suffix_wallpapers"), + ("Random Wallpaper", "prefix_randwall", "suffix_randwall"), + ("Emoji Picker", "prefix_emoji", "suffix_emoji"), + ("Kanban", "prefix_kanban", "suffix_kanban"), + ("Power Menu", "prefix_power", "suffix_power"), + ("Toggle Caffeine", "prefix_caffeine", "suffix_caffeine"), + ("Settings", "prefix_settings", "suffix_settings"), + ( + "Restart with inspector", + "prefix_restart_inspector", + "suffix_restart_inspector", + ), + ] + + for i, (label_text, prefix_key, suffix_key) in enumerate(bindings): + row = i + 1 + binding_label = Label(label=label_text, h_align="start") + keybind_grid.attach(binding_label, 0, row, 1, 1) + prefix_entry = Entry(text=bind_vars.get(prefix_key, "")) + keybind_grid.attach(prefix_entry, 1, row, 1, 1) + plus_label = Label(label="+", h_align="center") + keybind_grid.attach(plus_label, 2, row, 1, 1) + suffix_entry = Entry(text=bind_vars.get(suffix_key, "")) + keybind_grid.attach(suffix_entry, 3, row, 1, 1) + self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry)) + + main_vbox.add(keybind_grid) + return scrolled_window + + def get_key_binding_values(self): + """Get current key binding values from entries""" + values = {} + for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: + values[prefix_key] = prefix_entry.get_text() + values[suffix_key] = suffix_entry.get_text() + return values + + def reset_to_defaults(self, defaults): + """Reset key bindings to default values""" + for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: + prefix_entry.set_text(defaults.get(prefix_key, "")) + suffix_entry.set_text(defaults.get(suffix_key, "")) diff --git a/config/settings/system_tab.py b/config/settings/system_tab.py new file mode 100644 index 0000000..f3be6b4 --- /dev/null +++ b/config/settings/system_tab.py @@ -0,0 +1,382 @@ +import gi +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.entry import Entry +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import GLib, Gtk + +from config.settings.utils import bind_vars + +gi.require_version("Gtk", "3.0") + + +class SystemTab: + """System settings and metrics management tab for settings""" + + def __init__(self, show_lock_checkbox=False, show_idle_checkbox=False, window_size_enforcer=None): + self.show_lock_checkbox = show_lock_checkbox + self.show_idle_checkbox = show_idle_checkbox + self.window_size_enforcer = window_size_enforcer + + # Widget references + self.terminal_entry = None + self.lock_switch = None + self.idle_switch = None + self.limited_apps_entry = None + self.ignored_apps_entry = None + self.metrics_switches = {} + self.disk_entries = None + self._create_disk_edit_entry_func = None + + def create_system_tab(self): + """Create the System tab content""" + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=False, + v_expand=False, + propagate_width=False, + propagate_height=False, + ) + # Set fixed size to match tab stack dimensions + scrolled_window.set_size_request(580, 580) + + vbox = Box(orientation="v", spacing=15, style="margin: 15px;") + scrolled_window.add(vbox) + + # Create terminal and hyprland settings section + self._create_terminal_hyprland_section(vbox) + + # Create notification settings section + self._create_notification_settings_section(vbox) + + # Create system metrics section + self._create_system_metrics_section(vbox) + + return scrolled_window + + def _create_terminal_hyprland_section(self, vbox): + """Create terminal and Hyprland integration section""" + system_grid = Gtk.Grid() + system_grid.set_column_spacing(20) + system_grid.set_row_spacing(10) + system_grid.set_margin_bottom(15) + vbox.add(system_grid) + + # Terminal settings + terminal_header = Label(markup="Terminal Settings", h_align="start") + system_grid.attach(terminal_header, 0, 0, 2, 1) + terminal_label = Label(label="Command:", h_align="start", v_align="center") + system_grid.attach(terminal_label, 0, 1, 1, 1) + self.terminal_entry = Entry( + text=bind_vars.get("terminal_command", "kitty -e"), + tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')", + h_expand=True, + ) + system_grid.attach(self.terminal_entry, 1, 1, 1, 1) + hint_label = Label( + markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'", + h_align="start", + ) + system_grid.attach(hint_label, 0, 2, 2, 1) + + # Hyprland integration + hypr_header = Label(markup="Hyprland Integration", h_align="start") + system_grid.attach(hypr_header, 2, 0, 2, 1) + row = 1 + self.lock_switch = None + if self.show_lock_checkbox: + lock_label = Label( + label="Replace Hyprlock config", h_align="start", v_align="center" + ) + system_grid.attach(lock_label, 2, row, 1, 1) + lock_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.lock_switch = Gtk.Switch( + tooltip_text="Replace Hyprlock configuration with Modus's custom config" + ) + lock_switch_container.add(self.lock_switch) + system_grid.attach(lock_switch_container, 3, row, 1, 1) + row += 1 + self.idle_switch = None + if self.show_idle_checkbox: + idle_label = Label( + label="Replace Hypridle config", h_align="start", v_align="center" + ) + system_grid.attach(idle_label, 2, row, 1, 1) + idle_switch_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + self.idle_switch = Gtk.Switch( + tooltip_text="Replace Hypridle configuration with Modus's custom config" + ) + idle_switch_container.add(self.idle_switch) + system_grid.attach(idle_switch_container, 3, row, 1, 1) + row += 1 + if self.show_lock_checkbox or self.show_idle_checkbox: + note_label = Label( + markup="Existing configs will be backed up", + h_align="start", + ) + system_grid.attach(note_label, 2, row, 2, 1) + + def _create_notification_settings_section(self, vbox): + """Create notification settings section""" + notifications_header = Label( + markup="Notification Settings", h_align="start" + ) + vbox.add(notifications_header) + + notif_grid = Gtk.Grid() + notif_grid.set_column_spacing(20) + notif_grid.set_row_spacing(10) + notif_grid.set_margin_start(10) + notif_grid.set_margin_top(5) + notif_grid.set_margin_bottom(15) + vbox.add(notif_grid) + + # Limited Apps History + limited_apps_label = Label( + label="Limited Apps History:", h_align="start", v_align="center" + ) + notif_grid.attach(limited_apps_label, 0, 0, 1, 1) + + limited_apps_list = bind_vars.get("limited_apps_history", ["Spotify"]) + limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list) + self.limited_apps_entry = Entry( + text=limited_apps_text, + tooltip_text='Enter app names separated by commas, e.g: "Spotify", "Discord"', + h_expand=True, + ) + notif_grid.attach(self.limited_apps_entry, 1, 0, 1, 1) + + limited_apps_hint = Label( + markup='Apps with limited notification history (format: "App1", "App2")', + h_align="start", + ) + notif_grid.attach(limited_apps_hint, 0, 1, 2, 1) + + # History Ignored Apps + ignored_apps_label = Label( + label="History Ignored Apps:", h_align="start", v_align="center" + ) + notif_grid.attach(ignored_apps_label, 0, 2, 1, 1) + + ignored_apps_list = bind_vars.get("history_ignored_apps", ["Modus"]) + ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list) + self.ignored_apps_entry = Entry( + text=ignored_apps_text, + tooltip_text='Enter app names separated by commas, e.g: "Modus", "Screenshot"', + h_expand=True, + ) + notif_grid.attach(self.ignored_apps_entry, 1, 2, 1, 1) + + ignored_apps_hint = Label( + markup='Apps whose notifications are ignored in history (format: "App1", "App2")', + h_align="start", + ) + notif_grid.attach(ignored_apps_hint, 0, 3, 2, 1) + + def _create_system_metrics_section(self, vbox): + """Create system metrics section""" + metrics_header = Label(markup="System Metrics Options", h_align="start") + vbox.add(metrics_header) + metrics_grid = Gtk.Grid( + column_spacing=15, row_spacing=8, margin_start=10, margin_top=5 + ) + vbox.add(metrics_grid) + + self.metrics_switches = {} + metric_names = { + "cpu": "CPU", + "ram": "RAM", + "swap": "Swap", + "disk": "Disk", + "gpu": "GPU", + } + + metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 0, 0, 1, 1) + for i, (key, label_text) in enumerate(metric_names.items()): + switch = Gtk.Switch( + active=bind_vars.get("metrics_visible", {}).get(key, True) + ) + self.metrics_switches[key] = switch + metrics_grid.attach( + Label(label=label_text, h_align="start"), 0, i + 1, 1, 1 + ) + metrics_grid.attach(switch, 1, i + 1, 1, 1) + + def enforce_minimum_metrics(switch_dict): + enabled_switches = [s for s in switch_dict.values() if s.get_active()] + can_disable = len(enabled_switches) > 1 + for s in switch_dict.values(): + s.set_sensitive(True if can_disable or not s.get_active() else False) + + def on_metric_toggle(_switch, _gparam, switch_dict): + enforce_minimum_metrics(switch_dict) + + for s_s in self.metrics_switches.values(): + s_s.connect("notify::active", on_metric_toggle, self.metrics_switches) + enforce_minimum_metrics(self.metrics_switches) + + # Disk directories section + disks_label = Label( + label="Disk directories for Metrics", h_align="start", v_align="center" + ) + vbox.add(disks_label) + + # Create a scrolled container for disk entries to prevent overflow + disk_entries_scrolled = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=False, + v_expand=False, + propagate_width=False, + propagate_height=False, + ) + # Set fixed size for disk entries container + disk_entries_scrolled.set_size_request(550, 120) + + self.disk_entries = Box(orientation="v", spacing=8, h_align="start") + # Set size constraints for the disk entries box + self.disk_entries.set_size_request(530, -1) + disk_entries_scrolled.add(self.disk_entries) + + self._create_disk_edit_entry_func = lambda path: self._add_disk_entry_widget(path) + + for p in bind_vars.get("metrics_disks", ["/"]): + self._create_disk_edit_entry_func(p) + vbox.add(disk_entries_scrolled) + + add_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + ) + add_btn = Button( + label="Add new disk", + on_clicked=lambda _: self._create_disk_edit_entry_func("/"), + ) + add_container.add(add_btn) + vbox.add(add_container) + + def _add_disk_entry_widget(self, path): + """Helper to add a disk entry row to the disk_entries Box.""" + bar = Box(orientation="h", spacing=10, h_align="start") + # Set fixed height for disk entry rows + bar.set_size_request(-1, 30) + entry = Entry(text=path, h_expand=True) + bar.add(entry) + x_btn = Button(label="X") + x_btn.connect( + "clicked", + lambda _, current_bar_to_remove=bar: self._remove_disk_entry(current_bar_to_remove), + ) + bar.add(x_btn) + self.disk_entries.add(bar) + self.disk_entries.show_all() + # Enforce window size after adding content + if self.window_size_enforcer: + GLib.idle_add(self.window_size_enforcer) + + def _remove_disk_entry(self, bar_to_remove): + """Helper to remove a disk entry and enforce window size""" + self.disk_entries.remove(bar_to_remove) + # Enforce window size after removing content + if self.window_size_enforcer: + GLib.idle_add(self.window_size_enforcer) + + def get_system_values(self): + """Get current system values from widgets""" + values = {} + + values["terminal_command"] = self.terminal_entry.get_text() + + values["metrics_visible"] = { + k: s.get_active() for k, s in self.metrics_switches.items() + } + values["metrics_disks"] = [ + child.get_children()[0].get_text() + for child in self.disk_entries.get_children() + if isinstance(child, Gtk.Box) + and child.get_children() + and isinstance(child.get_children()[0], Entry) + ] + + # Parse notification app lists + def parse_app_list(text): + """Parse comma-separated app names with quotes""" + if not text.strip(): + return [] + apps = [] + for app in text.split(","): + app = app.strip() + if app.startswith('"') and app.endswith('"'): + app = app[1:-1] + elif app.startswith("'") and app.endswith("'"): + app = app[1:-1] + if app: + apps.append(app) + return apps + + values["limited_apps_history"] = parse_app_list( + self.limited_apps_entry.get_text() + ) + values["history_ignored_apps"] = parse_app_list( + self.ignored_apps_entry.get_text() + ) + + return values + + def get_hyprland_switches(self): + """Get the state of Hyprland integration switches""" + return { + "replace_lock": self.lock_switch and self.lock_switch.get_active(), + "replace_idle": self.idle_switch and self.idle_switch.get_active() + } + + def reset_to_defaults(self, defaults): + """Reset system settings to default values""" + self.terminal_entry.set_text(defaults.get("terminal_command", "kitty -e")) + + metrics_vis_defaults = defaults.get("metrics_visible", {}) + for k, s_widget in self.metrics_switches.items(): + s_widget.set_active(metrics_vis_defaults.get(k, True)) + + def enforce_minimum_metrics(switch_dict): + enabled_switches = [s for s in switch_dict.values() if s.get_active()] + can_disable = len(enabled_switches) > 1 + for s in switch_dict.values(): + s.set_sensitive( + True if can_disable or not s.get_active() else False + ) + + enforce_minimum_metrics(self.metrics_switches) + + # Reset disk entries + for child in list(self.disk_entries.get_children()): + self.disk_entries.remove(child) + + for p in defaults.get("metrics_disks", ["/"]): + self._create_disk_edit_entry_func(p) + + # Reset notification app lists + limited_apps_list = defaults.get("limited_apps_history", ["Spotify"]) + limited_apps_text = ", ".join(f'"{app}"' for app in limited_apps_list) + self.limited_apps_entry.set_text(limited_apps_text) + + ignored_apps_list = defaults.get("history_ignored_apps", ["Modus"]) + ignored_apps_text = ", ".join(f'"{app}"' for app in ignored_apps_list) + self.ignored_apps_entry.set_text(ignored_apps_text) + + # Reset Hyprland switches + if self.lock_switch: + self.lock_switch.set_active(False) + if self.idle_switch: + self.idle_switch.set_active(False)