diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a6d38dc..92f7bcc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug Report about: Report a bug from noctalia-shell -title: "[Bug]: " +title: "[Bug] " labels: bug assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5322498..43a8170 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature Request about: Suggest a new feature or improvement -title: "[Feature]: " +title: "[Feature] " labels: enhancement assignees: '' --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfbea23 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.qmlls.ini diff --git a/Assets/ColorScheme/Catppuccin.json b/Assets/ColorScheme/Catppuccin.json index 3977069..725f183 100644 --- a/Assets/ColorScheme/Catppuccin.json +++ b/Assets/ColorScheme/Catppuccin.json @@ -4,7 +4,7 @@ "mOnPrimary": "#11111b", "mSecondary": "#fab387", "mOnSecondary": "#11111b", - "mTertiary": "#a6e3a1", + "mTertiary": "#94e2d5", "mOnTertiary": "#11111b", "mError": "#f38ba8", "mOnError": "#11111b", @@ -16,19 +16,19 @@ "mShadow": "#11111b" }, "light": { - "mPrimary": "#9349ef", + "mPrimary": "#8839ef", "mOnPrimary": "#eff1f5", - "mSecondary": "#f67525", + "mSecondary": "#fe640b", "mOnSecondary": "#eff1f5", - "mTertiary": "#40b635", + "mTertiary": "#40a02b", "mOnTertiary": "#eff1f5", - "mError": "#f38ba8", - "mOnError": "#11111b", + "mError": "#d20f39", + "mOnError": "#dce0e8", "mSurface": "#eff1f5", "mOnSurface": "#4c4f69", "mSurfaceVariant": "#ccd0da", "mOnSurfaceVariant": "#6c6f85", - "mOutline": "#aeb5c4", + "mOutline": "#a5adcb", "mShadow": "#dce0e8" } } diff --git a/Assets/ColorScheme/Everforest.json b/Assets/ColorScheme/Everforest.json new file mode 100644 index 0000000..618ab50 --- /dev/null +++ b/Assets/ColorScheme/Everforest.json @@ -0,0 +1,34 @@ +{ + "dark": { + "mPrimary": "#D3C6AA", + "mOnPrimary": "#232A2E", + "mSecondary": "#D3C6AA", + "mOnSecondary": "#232A2E", + "mTertiary": "#9DA9A0", + "mOnTertiary": "#232A2E", + "mError": "#E67E80", + "mOnError": "#232A2E", + "mSurface": "#232A2E", + "mOnSurface": "#859289", + "mSurfaceVariant": "#2D353B", + "mOnSurfaceVariant": "#D3C6AA", + "mOutline": "#D3C6AA", + "mShadow": "#475258" + }, + "light": { + "mPrimary": "#434F55", + "mOnPrimary": "#D3C6AA", + "mSecondary": "#232a2e", + "mOnSecondary": "#D3C6AA", + "mTertiary": "#333c43", + "mOnTertiary": "#9DA9A0", + "mError": "#E66868", + "mOnError": "#9DA9A0", + "mSurface": "#BEC5B2", + "mOnSurface": "#333C43", + "mSurfaceVariant": "#9DA9A0", + "mOnSurfaceVariant": "#232A2E", + "mOutline": "#232A2E", + "mShadow": "#ECF5ED" + } +} diff --git a/Assets/ColorScheme/Monochrome.json b/Assets/ColorScheme/Monochrome.json new file mode 100644 index 0000000..1bca771 --- /dev/null +++ b/Assets/ColorScheme/Monochrome.json @@ -0,0 +1,34 @@ +{ + "dark": { + "mPrimary": "#aaaaaa", + "mOnPrimary": "#111111", + "mSecondary": "#a7a7a7", + "mOnSecondary": "#111111", + "mTertiary": "#cccccc", + "mOnTertiary": "#111111", + "mError": "#dddddd", + "mOnError": "#111111", + "mSurface": "#111111", + "mOnSurface": "#828282", + "mSurfaceVariant": "#191919", + "mOnSurfaceVariant": "#5d5d5d", + "mOutline": "#3c3c3c", + "mShadow": "#000000" + }, + "light": { + "mPrimary": "#555555", + "mOnPrimary": "#eeeeee", + "mSecondary": "#505058", + "mOnSecondary": "#eeeeee", + "mTertiary": "#333333", + "mOnTertiary": "#eeeeee", + "mError": "#222222", + "mOnError": "#efefef", + "mSurface": "#d4d4d4", + "mOnSurface": "#696969", + "mSurfaceVariant": "#e8e8e8", + "mOnSurfaceVariant": "#9e9e9e", + "mOutline": "#c3c3c3", + "mShadow": "#fafafa" + } +} diff --git a/Assets/ColorScheme/Rosepine.json b/Assets/ColorScheme/Rosepine.json index 547bb73..7590858 100644 --- a/Assets/ColorScheme/Rosepine.json +++ b/Assets/ColorScheme/Rosepine.json @@ -4,8 +4,8 @@ "mOnPrimary": "#191724", "mSecondary": "#9ccfd8", "mOnSecondary": "#191724", - "mTertiary": "#f6c177", - "mOnTertiary": "#191724", + "mTertiary": "#524f67", + "mOnTertiary": "#e0def4", "mError": "#eb6f92", "mOnError": "#191724", "mSurface": "#191724", @@ -16,19 +16,19 @@ "mShadow": "#191724" }, "light": { - "mPrimary": "#d46e6b", + "mPrimary": "#d7827e", "mOnPrimary": "#faf4ed", "mSecondary": "#56949f", "mOnSecondary": "#faf4ed", - "mTertiary": "#31748f", - "mOnTertiary": "#232136", + "mTertiary": "#cecacd", + "mOnTertiary": "#575279", "mError": "#b4637a", - "mOnError": "#f2e9e1", - "mSurface": "#e0def4", - "mOnSurface": "#232136", - "mSurfaceVariant": "#bcb8e7", + "mOnError": "#faf4ed", + "mSurface": "#faf4ed", + "mOnSurface": "#575279", + "mSurfaceVariant": "#f2e9e1", "mOnSurfaceVariant": "#797593", - "mOutline": "#9893a5", - "mShadow": "#575279" + "mOutline": "#dfdad9", + "mShadow": "#faf4ed" } } diff --git a/Assets/ColorScheme/Tokyo Night.json b/Assets/ColorScheme/Tokyo Night.json index bee47d2..520f3b1 100644 --- a/Assets/ColorScheme/Tokyo Night.json +++ b/Assets/ColorScheme/Tokyo Night.json @@ -1,34 +1,34 @@ { "dark": { - "mPrimary": "#ff9e64", - "mOnPrimary": "#1a1b26", - "mSecondary": "#e0af68", - "mOnSecondary": "#1a1b26", - "mTertiary": "#7aa2f7", - "mOnTertiary": "#1a1b26", + "mPrimary": "#7aa2f7", + "mOnPrimary": "#16161e", + "mSecondary": "#bb9af7", + "mOnSecondary": "#16161e", + "mTertiary": "#9ece6a", + "mOnTertiary": "#16161e", "mError": "#f7768e", - "mOnError": "#1a1b26", + "mOnError": "#16161e", "mSurface": "#1a1b26", - "mOnSurface": "#a9b1d6", - "mSurfaceVariant": "#292e42", - "mOnSurfaceVariant": "#787c99", - "mOutline": "#3d4462", - "mShadow": "#1a1b26" + "mOnSurface": "#c0caf5", + "mSurfaceVariant": "#24283b", + "mOnSurfaceVariant": "#9aa5ce", + "mOutline": "#565f89", + "mShadow": "#15161e" }, "light": { - "mPrimary": "#fd5d00", - "mOnPrimary": "#e6e7ed", - "mSecondary": "#bb8027", - "mOnSecondary": "#e6e7ed", - "mTertiary": "#4a80f4", - "mOnTertiary": "#e6e7ed", - "mError": "#965027", - "mOnError": "#e6e7ed", - "mSurface": "#e6e7ed", - "mOnSurface": "#343b58", - "mSurfaceVariant": "#d5d6db", - "mOnSurfaceVariant": "#40434f", - "mOutline": "#babbc3", - "mShadow": "#c0caf5" + "mPrimary": "#2e7de9", + "mOnPrimary": "#e1e2e7", + "mSecondary": "#9854f1", + "mOnSecondary": "#e1e2e7", + "mTertiary": "#587539", + "mOnTertiary": "#e1e2e7", + "mError": "#f52a65", + "mOnError": "#e1e2e7", + "mSurface": "#e1e2e7", + "mOnSurface": "#3760bf", + "mSurfaceVariant": "#d0d5e3", + "mOnSurfaceVariant": "#6172b0", + "mOutline": "#b4b5b9", + "mShadow": "#a8aecb" } } diff --git a/Assets/Fonts/tabler/tabler-icons.ttf b/Assets/Fonts/tabler/tabler-icons.ttf new file mode 100644 index 0000000..8626fa1 Binary files /dev/null and b/Assets/Fonts/tabler/tabler-icons.ttf differ diff --git a/Assets/Fonts/tabler/tabler-icons.woff2 b/Assets/Fonts/tabler/tabler-icons.woff2 deleted file mode 100644 index a58679f..0000000 Binary files a/Assets/Fonts/tabler/tabler-icons.woff2 and /dev/null differ diff --git a/Assets/Matugen/Matugen.qml b/Assets/Matugen/Matugen.qml index 7f662dc..009349e 100644 --- a/Assets/Matugen/Matugen.qml +++ b/Assets/Matugen/Matugen.qml @@ -51,28 +51,31 @@ Singleton { lines.push("\n[templates.ghostty]") lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"') lines.push('output_path = "~/.config/ghostty/themes/noctalia"') - lines.push( - "post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"") + lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"") } if (Settings.data.matugen.foot) { lines.push("\n[templates.foot]") lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"') lines.push('output_path = "~/.config/foot/themes/noctalia"') - lines.push( - 'post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"') + lines.push('post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"') } if (Settings.data.matugen.fuzzel) { lines.push("\n[templates.fuzzel]") lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"') lines.push('output_path = "~/.config/fuzzel/themes/noctalia"') - lines.push( - 'post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"') + lines.push('post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"') } if (Settings.data.matugen.vesktop) { lines.push("\n[templates.vesktop]") lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"') lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"') } + if (Settings.data.matugen.pywalfox) { + lines.push("\n[templates.pywalfox]") + lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/pywalfox.json"') + lines.push('output_path = "~/.cache/wal/colors.json"') + lines.push('post_hook = "pywalfox update"') + } return lines.join("\n") + "\n" } diff --git a/Assets/Matugen/templates/pywalfox.json b/Assets/Matugen/templates/pywalfox.json new file mode 100644 index 0000000..a2a9087 --- /dev/null +++ b/Assets/Matugen/templates/pywalfox.json @@ -0,0 +1,22 @@ +{ + "wallpaper": "{{image}}", + "alpha": "100", + "colors": { + "color0": "{{colors.background.default.hex}}", + "color1": "", + "color2": "", + "color3": "", + "color4": "", + "color5": "", + "color6": "", + "color7": "", + "color8": "", + "color9": "", + "color10": "{{colors.primary.default.hex}}", + "color11": "", + "color12": "", + "color13": "{{colors.surface_bright.default.hex}}", + "color14": "", + "color15": "{{colors.on_surface.default.hex}}" + } + } \ No newline at end of file diff --git a/Assets/Matugen/templates/vesktop.css b/Assets/Matugen/templates/vesktop.css index ac5b166..40e8c73 100644 --- a/Assets/Matugen/templates/vesktop.css +++ b/Assets/Matugen/templates/vesktop.css @@ -1,572 +1,113 @@ -/* -* Vesktop Theme -* Generated with Matugen -* Base was taken from https://github.com/catppuccin/discord <3 +/** + * @name noctalia + * @description Original theme: midnight | A dark, rounded discord theme. + * @author refact0r + * @version 1.6.2 + * @invite nz87hXyvcy + * @website https://github.com/refact0r/midnight-discord + * @source https://github.com/refact0r/midnight-discord/blob/master/midnight.theme.css + * @authorId 508863359777505290 + * @authorLink https://www.refact0r.dev */ +/* IMPORTANT: make sure to enable dark mode in discord settings for the theme to apply properly!!! */ -/* Dark Theme */ -.visual-refresh.theme-dark, -.visual-refresh .theme-dark { - /* Brand Colors */ - --brand-experiment: {{colors.primary.default.hex}}; - --bg-brand: {{colors.primary.default.hex}}; - --brand-500: {{colors.primary.default.hex}} !important; - --text-link: {{colors.primary.default.hex}} !important; - --text-brand: {{colors.primary.default.hex}}; - --control-brand-foreground: {{colors.primary.default.hex}}; - --control-brand-foreground-new: {{colors.primary.default.hex}}; - --mention-foreground: {{colors.primary.default.hex}}; - --mention-background: {{colors.primary.default.hex}}20; - --focus-primary: {{colors.primary.default.hex}}; - --logo-primary: {{colors.on_surface.default.hex}}; - --badge-brand-bg: {{colors.primary.default.hex}}; - --badge-brand-text: {{colors.on_primary.default.hex}}; +@import url('https://refact0r.github.io/midnight-discord/build/midnight.css'); - /* Text Colors */ - --header-primary: {{colors.on_surface.default.hex}} !important; - --header-secondary: {{colors.on_surface_variant.default.hex}} !important; - --text-normal: {{colors.on_surface.default.hex}} !important; - --text-default: {{colors.on_surface.default.hex}}; - --text-muted: {{colors.on_surface_variant.default.hex}} !important; - --text-primary: {{colors.on_surface.default.hex}}; - --text-secondary: {{colors.on_surface_variant.default.hex}}; - --text-tertiary: {{colors.on_surface_variant.default.hex}} !important; - --interactive-normal: {{colors.on_surface.default.hex}} !important; - --interactive-muted: {{colors.on_surface_variant.default.hex}}; - --interactive-hover: {{colors.on_surface.default.hex}}; - --interactive-active: {{colors.on_surface.default.hex}}; +/* customize things here */ +:root { + /* font, change to 'gg sans' for default discord font*/ + --font: 'figtree'; - /* Main Background Colors - Bar color (mSurface) colors.surface.default.hex*/ - --background-primary: {{colors.surface_variant.default.hex}} !important; - --background-floating: {{colors.surface_variant.default.hex}} !important; - --background-surface-high: {{colors.surface_variant.default.hex}} !important; - --modal-background: {{colors.surface_variant.default.hex}} !important; - --app-background-frame: {{colors.surface_variant.default.hex}} !important; - --home-background: {{colors.surface_variant.default.hex}} !important; - --chat-background: {{colors.surface_variant.default.hex}} !important; - --chat-background-default: {{colors.surface_variant.default.hex}} !important; - --chat-input-container-background: {{colors.surface_container.default.hex}} !important; - - /* Secondary Background Colors - Workspace color (mSurfaceVariant) */ - --background-secondary: {{colors.surface.default.hex}} !important; - --background-secondary-alt: {{colors.surface.default.hex}} !important; - --background-surface-higher: {{colors.surface.default.hex}} !important; - --background-base-low: {{colors.surface.default.hex}} !important; - --background-base-lower: {{colors.surface.default.hex}} !important; - --channeltextarea-background: {{colors.surface_container.default.hex}} !important; - --modal-footer-background: {{colors.surface.default.hex}} !important; - - /* New Messages Banner */ - --background-mentioned: {{colors.primary.default.hex}}15 !important; - --background-mentioned-hover: {{colors.primary.default.hex}}20 !important; - --text-mentioned: {{colors.on_surface.default.hex}} !important; - --text-mentioned-hover: {{colors.on_surface.default.hex}} !important; - --text-mentioned-link: {{colors.primary.default.hex}} !important; - - /* Additional Discord-specific variables for new messages banner */ - --background-message-automod: {{colors.primary.default.hex}}15 !important; - --background-message-automod-hover: {{colors.primary.default.hex}}20 !important; - --background-message-highlight: {{colors.primary.default.hex}}15 !important; - --background-message-highlight-hover: {{colors.primary.default.hex}}20 !important; - - /* Discord unread messages banner specific variables */ - --background-mentioned: {{colors.primary.default.hex}}15 !important; - --background-mentioned-hover: {{colors.primary.default.hex}}20 !important; - --text-mentioned: {{colors.on_surface.default.hex}} !important; - --text-mentioned-hover: {{colors.on_surface.default.hex}} !important; - --text-mentioned-link: {{colors.primary.default.hex}} !important; - - /* Additional Discord banner text variables */ - --text-normal: {{colors.on_surface.default.hex}} !important; - --text-default: {{colors.on_surface.default.hex}} !important; - --text-primary: {{colors.on_surface.default.hex}} !important; - --text-secondary: {{colors.on_surface_variant.default.hex}} !important; - --text-tertiary: {{colors.on_surface_variant.default.hex}} !important; - --text-muted: {{colors.on_surface_variant.default.hex}} !important; - --interactive-normal: {{colors.on_surface.default.hex}} !important; - --interactive-muted: {{colors.on_surface_variant.default.hex}} !important; - - /* Additional Discord banner variables */ - --background-message-automod: {{colors.primary.default.hex}}15 !important; - --background-message-automod-hover: {{colors.primary.default.hex}}20 !important; - --background-message-highlight: {{colors.primary.default.hex}}15 !important; - --background-message-highlight-hover: {{colors.primary.default.hex}}20 !important; - --background-message-hover: {{colors.surface_variant.default.hex}}50 !important; - --background-modifier-hover: {{colors.surface_variant.default.hex}}80 !important; - --background-modifier-selected: {{colors.primary.default.hex}}20 !important; - --background-modifier-accent: {{colors.primary.default.hex}}30 !important; - --background-modifier-active: {{colors.primary.default.hex}}25 !important; - - /* Chat Input Improvements */ - --text-input-background: {{colors.surface_container.default.hex}} !important; - --text-input-border: {{colors.outline.default.hex}} !important; - --text-input-border-hover: {{colors.primary.default.hex}} !important; - - /* Additional Discord-specific input variables */ - --deprecated-text-input-bg: {{colors.surface_container.default.hex}} !important; - --deprecated-text-input-border: {{colors.outline.default.hex}} !important; - --deprecated-text-input-border-hover: {{colors.primary.default.hex}} !important; - --input-background: {{colors.surface_container.default.hex}} !important; - --input-border: {{colors.outline.default.hex}} !important; - --input-placeholder-text: {{colors.on_surface_variant.default.hex}} !important; - - /* Elevated/Container Backgrounds */ - --background-tertiary: {{colors.surface_container.default.hex}} !important; - --background-accent: {{colors.surface_container.default.hex}} !important; - --background-surface-highest: {{colors.surface_container_high.default.hex}} !important; - --background-base-lowest: {{colors.surface_container.default.hex}} !important; + /* top left corner text */ + --corner-text: 'Midnight'; - /* Border Colors */ - --border-faint: {{colors.outline_variant.default.hex}}; - --border-strong: {{colors.surface_container.default.hex}}; - --border-normal: {{colors.surface_container_high.default.hex}}; - --border-subtle: {{colors.surface.default.hex}} !important; - --chat-border: {{colors.surface_container_high.default.hex}}; + /* color of status indicators and window controls */ + --online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */ + --dnd-indicator: {{colors.error.default.hex}}; /* change to #f13f43 for default red */ + --idle-indicator: {{colors.tertiary_container.default.hex}}; /* change to #f0b232 for default yellow */ + --streaming-indicator: {{colors.on_primary.default.hex}}; /* change to #593695 for default purple */ - /* Status Colors */ - --status-positive: {{colors.tertiary.default.hex}}; - --status-positive-background: {{colors.tertiary.default.hex}}; - --status-positive-text: {{colors.on_tertiary.default.hex}}; - --text-positive: {{colors.tertiary.default.hex}}; - --text-feedback-positive: {{colors.tertiary.default.hex}}; - --background-feedback-positive: {{colors.tertiary.default.hex}}20; - --info-positive-background: {{colors.tertiary.default.hex}}20; - --info-positive-foreground: {{colors.tertiary.default.hex}}; - --info-positive-text: {{colors.on_surface.default.hex}}; + /* accent colors */ + --accent-1: {{colors.tertiary.default.hex}}; /* links */ + --accent-2: {{colors.primary.default.hex}}; /* general unread/mention elements, some icons when active */ + --accent-3: {{colors.primary.default.hex}}; /* accent buttons */ + --accent-4: {{colors.surface_bright.default.hex}}; /* accent buttons when hovered */ + --accent-5: {{colors.primary_fixed_dim.default.hex}}; /* accent buttons when clicked */ + --mention: {{colors.surface.default.hex}}; /* mentions & mention messages */ + --mention-hover: {{colors.surface_bright.default.hex}}; /* mentions & mention messages when hovered */ - --status-warning: {{colors.secondary.default.hex}}; - --status-warning-background: {{colors.secondary.default.hex}}; - --status-warning-text: {{colors.on_secondary.default.hex}}; - --text-warning: {{colors.secondary.default.hex}}; - --text-feedback-warning: {{colors.secondary.default.hex}}; - --background-feedback-warning: {{colors.secondary.default.hex}}20; - --info-warning-background: {{colors.secondary.default.hex}}20; - --info-warning-foreground: {{colors.secondary.default.hex}}; - --info-warning-text: {{colors.on_surface.default.hex}}; + /* text colors */ + --text-0: {{colors.surface.default.hex}}; /* text on colored elements */ + --text-1: {{colors.on_surface.default.hex}}; /* other normally white text */ + --text-2: {{colors.on_surface.default.hex}}; /* headings and important text */ + --text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */ + --text-4: {{colors.on_surface_variant.default.hex}}; /* icon buttons and channels */ + --text-5: {{colors.outline.default.hex}}; /* muted channels/chats and timestamps */ - --status-danger: {{colors.error.default.hex}}; - --status-danger-background: {{colors.error.default.hex}}; - --status-danger-text: {{colors.on_error.default.hex}}; - --text-danger: {{colors.error.default.hex}}; - --text-feedback-critical: {{colors.error.default.hex}}; - --background-feedback-critical: {{colors.error.default.hex}}20; - --info-danger-background: {{colors.error.default.hex}}20; - --info-danger-foreground: {{colors.error.default.hex}}; - --info-danger-text: {{colors.on_surface.default.hex}}; + /* background and dark colors */ + --bg-1: {{colors.primary.default.hex}}; /* dark buttons when clicked */ + --bg-2: {{colors.surface_container_high.default.hex}}; /* dark buttons */ + --bg-3: {{colors.surface_container_low.default.hex}}; /* spacing, secondary elements */ + --bg-4: {{colors.surface.default.hex}}; /* main background color */ + --hover: {{colors.surface_bright.default.hex}}; /* channels and buttons when hovered */ + --active: {{colors.surface_bright.default.hex}}; /* channels and buttons when clicked or selected */ + --message-hover: {{colors.surface_bright.default.hex}}; /* messages when hovered */ - /* Button Colors */ - --button-secondary-background: {{colors.surface_variant.default.hex}} !important; - --button-secondary-background-hover: {{colors.surface_container.default.hex}}; - --button-secondary-background-active: {{colors.surface_container.default.hex}}; - --button-secondary-background-disabled: {{colors.surface_variant.default.hex}}; - --button-secondary-text: {{colors.on_surface.default.hex}} !important; + /* amount of spacing and padding */ + --spacing: 12px; - --button-filled-brand-text: {{colors.on_primary.default.hex}}; - --button-filled-brand-background: {{colors.primary.default.hex}}; - --button-filled-brand-background-hover: {{colors.primary.default.hex}}; - --button-filled-brand-background-active: {{colors.primary.default.hex}}; + /* animations */ + /* ALL ANIMATIONS CAN BE DISABLED WITH REDUCED MOTION IN DISCORD SETTINGS */ + --list-item-transition: 0.2s ease; /* channels/members/settings hover transition */ + --unread-bar-transition: 0.2s ease; /* unread bar moving into view transition */ + --moon-spin-transition: 0.4s ease; /* moon icon spin */ + --icon-spin-transition: 1s ease; /* round icon button spin (settings, emoji, etc.) */ - /* Input Colors */ - --input-background: {{colors.surface_container.default.hex}}; - --input-border: {{colors.outline.default.hex}}; - --input-placeholder-text: {{colors.on_surface_variant.default.hex}}; + /* corner roundness (border-radius) */ + --roundness-xl: 22px; /* roundness of big panel outer corners */ + --roundness-l: 20px; /* popout panels */ + --roundness-m: 16px; /* smaller panels, images, embeds */ + --roundness-s: 12px; /* members, settings inputs */ + --roundness-xs: 10px; /* channels, buttons */ + --roundness-xxs: 8px; /* searchbar, small elements */ - /* Scrollbar Colors */ - --scrollbar-thin-thumb: {{colors.primary.default.hex}}; - --scrollbar-thin-track: transparent; - --scrollbar-auto-thumb: {{colors.primary.default.hex}}; - --scrollbar-auto-track: {{colors.surface_container_high.default.hex}}; - --scrollbar-auto-scrollbar-color-thumb: {{colors.primary.default.hex}}; - --scrollbar-auto-scrollbar-color-track: {{colors.surface_container_high.default.hex}}; + /* direct messages moon icon */ + /* change to block to show, none to hide */ + --discord-icon: none; /* discord icon */ + --moon-icon: block; /* moon icon */ + --moon-icon-url: url('https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg'); /* custom icon url */ + --moon-icon-size: auto; - /* Icon Colors */ - --icon-muted: {{colors.on_surface_variant.default.hex}}; - --icon-default: {{colors.on_surface.default.hex}}; - --icon-primary: {{colors.on_surface.default.hex}}; - --icon-secondary: {{colors.on_surface_variant.default.hex}}; - --icon-tertiary: {{colors.on_surface_variant.default.hex}} !important; - - /* Channel Colors */ - --channels-default: {{colors.on_surface_variant.default.hex}} !important; - --channel-icon: {{colors.on_surface_variant.default.hex}} !important; - --channel-text-area-placeholder: {{colors.on_surface.default.hex}}80; - - /* Selection and Hover States */ - --background-modifier-hover: {{colors.surface_variant.default.hex}}80; - --background-modifier-selected: {{colors.primary.default.hex}}20 !important; - --background-modifier-accent: {{colors.primary.default.hex}}30; - --background-modifier-active: {{colors.primary.default.hex}}25 !important; - --background-message-hover: {{colors.surface_variant.default.hex}}50 !important; - --background-message-highlight: {{colors.primary.default.hex}}15; - --background-message-highlight-hover: {{colors.primary.default.hex}}20; - - /* Code Block - Use workspace background */ - --background-code: {{colors.surface_container.default.hex}}; - --textbox-markdown-syntax: {{colors.on_surface_variant.default.hex}}; - - /* Spoiler */ - --spoiler-revealed-background: {{colors.surface_container.default.hex}}; - --spoiler-hidden-background: {{colors.surface_variant.default.hex}}; - - /* White/Black Overrides */ - --white: {{colors.on_surface.default.hex}}; - --white-400: {{colors.on_surface.default.hex}}; - --white-500: {{colors.on_surface.default.hex}}; - --white-600: {{colors.on_surface_variant.default.hex}}; - --white-700: {{colors.on_surface_variant.default.hex}}; - --black-500: {{colors.surface_container_high.default.hex}}; - - /* Force styling for Discord unread messages banner */ - --unread-bar-background: {{colors.primary.default.hex}}15 !important; - --unread-bar-text: {{colors.on_surface.default.hex}} !important; - --unread-bar-hover: {{colors.primary.default.hex}}20 !important; - - /* Additional Discord unread bar variables */ - --background-mentioned: {{colors.primary.default.hex}}15 !important; - --background-mentioned-hover: {{colors.primary.default.hex}}20 !important; - --text-mentioned: {{colors.on_surface.default.hex}} !important; - --text-mentioned-hover: {{colors.on_surface.default.hex}} !important; - --text-mentioned-link: {{colors.primary.default.hex}} !important; - - /* Discord banner specific variables */ - --background-message-automod: {{colors.primary.default.hex}}15 !important; - --background-message-automod-hover: {{colors.primary.default.hex}}20 !important; - --background-message-highlight: {{colors.primary.default.hex}}15 !important; - --background-message-highlight-hover: {{colors.primary.default.hex}}20 !important; - - /* Discord unread bar specific variables */ - --background-mentioned: {{colors.primary.default.hex}}15 !important; - --background-mentioned-hover: {{colors.primary.default.hex}}20 !important; - --text-mentioned: {{colors.on_surface.default.hex}} !important; - --text-mentioned-hover: {{colors.on_surface.default.hex}} !important; - --text-mentioned-link: {{colors.primary.default.hex}} !important; - - /* Additional Discord text variables that might affect the banner */ - --text-normal: {{colors.on_surface.default.hex}} !important; - --text-default: {{colors.on_surface.default.hex}} !important; - --text-primary: {{colors.on_surface.default.hex}} !important; - --text-secondary: {{colors.on_surface_variant.default.hex}} !important; - --text-tertiary: {{colors.on_surface_variant.default.hex}} !important; - --text-muted: {{colors.on_surface_variant.default.hex}} !important; - --interactive-normal: {{colors.on_surface.default.hex}} !important; - --interactive-muted: {{colors.on_surface_variant.default.hex}} !important; - - /* Force styling for Discord chat input */ - --chat-input-background: {{colors.surface_container.default.hex}} !important; - --chat-input-placeholder: {{colors.on_surface_variant.default.hex}} !important; - - /* Discord unread messages banner specific variables */ - --new-messages-bar-background: {{colors.surface_container.default.hex}} !important; - --new-messages-bar-text: {{colors.on_surface.default.hex}} !important; - --new-messages-bar-hover: {{colors.surface_container_high.default.hex}} !important; - --bar-button-background: {{colors.surface_container.default.hex}} !important; - --bar-button-text: {{colors.on_surface.default.hex}} !important; - --bar-button-hover: {{colors.surface_container_high.default.hex}} !important; + /* filter uncolorable elements to fit theme */ + /* (just set to none, they're too much work to configure) */ + --login-bg-filter: saturate(0.3) hue-rotate(-15deg) brightness(0.4); /* login background artwork */ + --green-to-accent-3-filter: hue-rotate(56deg) saturate(1.43); /* add friend page explore icon */ + --blurple-to-accent-3-filter: hue-rotate(304deg) saturate(0.84) brightness(1.2); /* add friend page school icon */ } -.visual-refresh.theme-dark ::selection, -.visual-refresh .theme-dark ::selection { - background-color: {{colors.primary.default.hex}}; +/* Selected chat/friend text */ +.selected_f5eb4b, +.selected_f6f816 .link_d8bfb3 { + color: var(--text-0) !important; + background: var(--accent-3) !important; } -/* Force Discord unread messages banner styling */ -.visual-refresh.theme-dark .newMessagesBar__0f481, -.visual-refresh.theme-dark .barButtonMain__0f481, -.visual-refresh.theme-dark .barButtonBase__0f481, -.visual-refresh.theme-dark .span__0f481 { - background-color: {{colors.surface_container.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; +.selected_f6f816 .link_d8bfb3 * { + color: var(--text-0) !important; + fill: var(--text-0) !important; } -.visual-refresh.theme-dark .newMessagesBar__0f481:hover, -.visual-refresh.theme-dark .barButtonMain__0f481:hover, -.visual-refresh.theme-dark .barButtonBase__0f481:hover { - background-color: {{colors.surface_container_high.default.hex}} !important; +/* Make channel name text less visible (darker) */ +.name__2ea32 { + color: var(--text-5) !important; + opacity: 0.7 !important; } -/* Force Discord chat input styling */ -.visual-refresh.theme-dark .channelTextArea-rNsIhG, -.visual-refresh.theme-dark .channelTextArea-rNsIhG *, -.visual-refresh.theme-dark .scrollableContainer-2NUZem, -.visual-refresh.theme-dark [data-slate-editor="true"] { - background-color: {{colors.surface_container.default.hex}} !important; +/* Make unread channel names brighter */ +.link__2ea32[aria-label*="unread"] .name__2ea32 { + color: var(--text-2) !important; + opacity: 1 !important; + font-weight: 600 !important; } -.visual-refresh.theme-dark [data-slate-editor="true"]::placeholder, -.visual-refresh.theme-dark .channelTextArea-rNsIhG [data-slate-editor="true"]::placeholder { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -/* Discord Emoji Picker Theming */ -.visual-refresh.theme-dark .contentWrapper__08434, -.visual-refresh.theme-dark .emojiPicker_c0e32c, -.visual-refresh.theme-dark .wrapper_c0e32c { - background-color: {{colors.surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .nav__08434, -.visual-refresh.theme-dark .navList__08434 { - background-color: {{colors.surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .navButton__08434 { - background-color: {{colors.surface.default.hex}} !important; - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .navButtonActive__08434 { - background-color: {{colors.surface.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .searchBar_c0e32c, -.visual-refresh.theme-dark .input_a45028 { - background-color: {{colors.surface.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .input_a45028::placeholder { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .header_c656ac, -.visual-refresh.theme-dark .header__14245, -.visual-refresh.theme-dark .wrapper__14245 { - background-color: {{colors.surface_variant.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .headerLabel__14245 { - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .interactive__14245 { - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .header__14245 { - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .header__14245 * { - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .headerIcon__14245 svg, -.visual-refresh.theme-dark .headerCollapseIcon__14245 svg { - color: {{colors.on_surface.default.hex}} !important; - fill: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .emojiItem_fc7141 { - background-color: transparent !important; -} - -.visual-refresh.theme-dark .emojiItem_fc7141:hover { - background-color: {{colors.surface_container.default.hex}} !important; -} - -.visual-refresh.theme-dark .emojiItemSelected_fc7141 { - background-color: {{colors.primary.default.hex}}20 !important; -} - -.visual-refresh.theme-dark .inspector_aeaaeb { - background-color: {{colors.surface_container.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .categoryList_c0e32c { - background-color: {{colors.surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .categoryItem_b9ee0c { - background-color: transparent !important; -} - -.visual-refresh.theme-dark .categoryItem_b9ee0c:hover { - background-color: {{colors.surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .categoryItemDefaultCategorySelected_b9ee0c { - background-color: {{colors.surface_variant.default.hex}} !important; -} - -/* Additional Discord emoji picker elements */ -.visual-refresh.theme-dark .navItem__08434 { - background-color: {{colors.surface_variant.default.hex}} !important; - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .navItem__08434:hover { - background-color: {{colors.surface_container.default.hex}} !important; -} - -.visual-refresh.theme-dark .stickersNavItem__08434 { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .wrapper__14245 { - background-color: {{colors.surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .headerLabel__14245 { - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .headerIcon__14245 svg, -.visual-refresh.theme-dark .headerCollapseIcon__14245 svg { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .interactive__14245:hover { - background-color: {{colors.surface_container.default.hex}} !important; -} - -/* Chat input styling */ -.visual-refresh.theme-dark .scrollableContainer__74017, -.visual-refresh.theme-dark .themedBackground__74017, -.visual-refresh.theme-dark .inner__74017, -.visual-refresh.theme-dark .textArea__74017, -.visual-refresh.theme-dark .slateContainer_ec4baf, -.visual-refresh.theme-dark .markup__75297, -.visual-refresh.theme-dark .editor__1b31f, -.visual-refresh.theme-dark .slateTextArea_ec4baf { - background-color: {{colors.surface_container.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .emptyText__1464f { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .placeholder__1b31f { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -/* Message content styling */ -.visual-refresh.theme-dark .messageContent_c19a55 { - color: {{colors.on_surface.default.hex}} !important; - background-color: {{colors.surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .messageContent_c19a55 .markup__75297 { - color: {{colors.on_surface.default.hex}} !important; - background-color: {{colors.surface.default.hex}} !important; -} - -/* Message background styling */ -.visual-refresh.theme-dark .message__5126c, -.visual-refresh.theme-dark .cozyMessage__5126c, -.visual-refresh.theme-dark .wrapper_c19a55, -.visual-refresh.theme-dark .contents_c19a55 { - background-color: {{colors.surface.default.hex}} !important; -} - -/* Message hover effects */ -.visual-refresh.theme-dark .message__5126c:hover { - background-color: {{colors.surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .message__5126c:hover * { - color: {{colors.on_surface.default.hex}} !important; -} - -/* Remove Discord's native quote/reply bar */ -.visual-refresh.theme-dark .message__5126c::before { - display: none !important; -} - -.visual-refresh.theme-dark .message__5126c.hasReply_c19a55::before { - display: none !important; -} - -/* Channel styling - darker text for read channels */ -.visual-refresh.theme-dark .link__2ea32 .name__2ea32 { - color: {{colors.outline.default.hex}} !important; -} - -/* Unread channels keep normal color */ -.visual-refresh.theme-dark .link__2ea32[aria-label*="unread"] .name__2ea32 { - color: {{colors.on_surface.default.hex}} !important; -} - -/* Search input styling */ -.visual-refresh.theme-dark .inner_a45028 { - background-color: {{colors.surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .input_a45028 { - background-color: transparent !important; -} - -.visual-refresh.theme-dark .input_a45028::placeholder { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -/* Chat input placeholder styling */ -.visual-refresh.theme-dark .emptyText__1464f { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before { - content: "send a message" !important; - color: {{colors.on_surface_variant.default.hex}} !important; -} - -/* Hide placeholder when input is focused */ -.visual-refresh.theme-dark .slateTextArea_ec4baf:focus .emptyText__1464f::before, -.visual-refresh.theme-dark .markup__75297:focus .emptyText__1464f::before { - display: none !important; -} - - - -.visual-refresh.theme-dark .message__5126c:hover .messageContent_c19a55, -.visual-refresh.theme-dark .message__5126c:hover .markup__75297, -.visual-refresh.theme-dark .message__5126c:hover .header_c19a55, -.visual-refresh.theme-dark .message__5126c:hover .headerText_c19a55, -.visual-refresh.theme-dark .message__5126c:hover .username_c19a55, -.visual-refresh.theme-dark .message__5126c:hover .timestamp_c19a55 { - background-color: {{colors.surface_variant.default.hex}} !important; -} - - - -.visual-refresh.theme-dark .categoryIcon_b9ee0c svg { - color: {{colors.on_surface_variant.default.hex}} !important; -} - -.visual-refresh.theme-dark .unicodeShortcut_b9ee0c { - background-color: {{colors.surface_container.default.hex}} !important; - color: {{colors.on_surface.default.hex}} !important; -} - -.visual-refresh.theme-dark .unicodeShortcut_b9ee0c:hover { - background-color: {{colors.surface_container_high.default.hex}} !important; -} - -.visual-refresh.theme-dark .unicodeShortcut_b9ee0c svg { - color: {{colors.on_surface.default.hex}} !important; -} - -/* Number badge styling */ -.visual-refresh.theme-dark .numberBadge__2b1f5 { - color: {{colors.surface.default.hex}} !important; - background-color: {{colors.primary.default.hex}} !important; -} - -/* New badge styling */ -.visual-refresh.theme-dark .newBadge__4ed1a { - color: {{colors.surface.default.hex}} !important; - background-color: {{colors.primary.default.hex}} !important; -} - - - - - - - - - diff --git a/Assets/Screenshots/launcher.png b/Assets/Screenshots/launcher.png deleted file mode 100644 index 5a143bd..0000000 Binary files a/Assets/Screenshots/launcher.png and /dev/null differ diff --git a/Assets/Screenshots/light-mode.png b/Assets/Screenshots/light-mode.png deleted file mode 100644 index 3d97379..0000000 Binary files a/Assets/Screenshots/light-mode.png and /dev/null differ diff --git a/Assets/Screenshots/noctalia-dark-1.png b/Assets/Screenshots/noctalia-dark-1.png new file mode 100644 index 0000000..cff528d Binary files /dev/null and b/Assets/Screenshots/noctalia-dark-1.png differ diff --git a/Assets/Screenshots/noctalia-dark-2.png b/Assets/Screenshots/noctalia-dark-2.png new file mode 100644 index 0000000..cab6eb5 Binary files /dev/null and b/Assets/Screenshots/noctalia-dark-2.png differ diff --git a/Assets/Screenshots/noctalia-dark-3.png b/Assets/Screenshots/noctalia-dark-3.png new file mode 100644 index 0000000..11190b0 Binary files /dev/null and b/Assets/Screenshots/noctalia-dark-3.png differ diff --git a/Assets/Screenshots/noctalia-light-1.png b/Assets/Screenshots/noctalia-light-1.png new file mode 100644 index 0000000..327dfb9 Binary files /dev/null and b/Assets/Screenshots/noctalia-light-1.png differ diff --git a/Assets/Screenshots/noctalia-light-2.png b/Assets/Screenshots/noctalia-light-2.png new file mode 100644 index 0000000..0d1c46e Binary files /dev/null and b/Assets/Screenshots/noctalia-light-2.png differ diff --git a/Assets/Screenshots/noctalia-light-3.png b/Assets/Screenshots/noctalia-light-3.png new file mode 100644 index 0000000..171fbf6 Binary files /dev/null and b/Assets/Screenshots/noctalia-light-3.png differ diff --git a/Assets/Screenshots/settings-panel.png b/Assets/Screenshots/settings-panel.png deleted file mode 100644 index 5124868..0000000 Binary files a/Assets/Screenshots/settings-panel.png and /dev/null differ diff --git a/Bin/qmlfmt.sh b/Bin/qmlfmt.sh index 2af1996..a7c6b54 100755 --- a/Bin/qmlfmt.sh +++ b/Bin/qmlfmt.sh @@ -4,4 +4,4 @@ # Can be installed from AUR "qmlfmt-git" # Requires qt6-5compat -find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; +find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \; diff --git a/Commons/AppIcons.qml b/Commons/AppIcons.qml index 2a9aee1..aa8c4ae 100644 --- a/Commons/AppIcons.qml +++ b/Commons/AppIcons.qml @@ -34,8 +34,7 @@ Singleton { try { if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) return iconFromName(fallback, fallback) - const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup( - appId) : DesktopEntries.byId(appId) + const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId) const name = entry && entry.icon ? entry.icon : "" return iconFromName(name || fallback, fallback) } catch (e) { diff --git a/Commons/Icons.qml b/Commons/Icons.qml index 354d963..93ed5d9 100644 --- a/Commons/Icons.qml +++ b/Commons/Icons.qml @@ -14,7 +14,7 @@ Singleton { readonly property string defaultIcon: TablerIcons.defaultIcon readonly property var icons: TablerIcons.icons readonly property var aliases: TablerIcons.aliases - readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.woff2" + readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf" Component.onCompleted: { Logger.log("Icons", "Service started") diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/IconsSets/TablerIcons.qml index dc782e6..71d8e6c 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/IconsSets/TablerIcons.qml @@ -20,7 +20,8 @@ Singleton { "folder-open": "folder-open", "download": "download", "toast-notice": "circle-check", - "toast-warning": "exclamation-circle", + "toast-warning": "alert-circle", + "toast-error": "circle-x", "question-mark": "question-mark", "search": "search", "warning": "exclamation-circle", @@ -39,21 +40,20 @@ Singleton { "balanced": "scale", "powersaver": "leaf", "storage": "database", - "ethernet": "sitemap-filled", + "ethernet": "sitemap", "keyboard": "keyboard", "shutdown": "power", - "lock": "lock-filled", + "lock": "lock", "logout": "logout", "reboot": "refresh", - "suspend": "player-pause-filled", - "nightlight-on": "moon-filled", + "suspend": "player-pause", + "nightlight-on": "moon", "nightlight-off": "moon-off", "nightlight-forced": "moon-stars", "bell": "bell", "bell-off": "bell-off", "keep-awake-on": "mug", "keep-awake-off": "mug-off", - "panel": "clipboard-filled", "disc": "disc-filled", "image": "photo", "dark-mode": "contrast-filled", @@ -84,41 +84,47 @@ Singleton { "volume-zero": "volume-3", "volume-low": "volume-2", "volume-high": "volume", - "weather-sun": "sun-filled", - "weather-cloud-sun": "sun-wind", + "weather-sun": "sun", "weather-cloud": "cloud", "weather-cloud-haze": "cloud-fog", + "weather-cloud-lightning": "cloud-bolt", "weather-cloud-rain": "cloud-rain", "weather-cloud-snow": "cloud-snow", - "weather-cloud-lightning": "cloud-bolt", + "weather-cloud-sun": "cloud-sun", "brightness-low": "brightness-down-filled", "brightness-high": "brightness-up-filled", "settings-general": "adjustments-horizontal", "settings-bar": "capsule-horizontal", + "settings-dock": "layout-bottombar", "settings-launcher": "rocket", "settings-audio": "device-speaker", "settings-display": "device-desktop", - "settings-network": "sitemap-filled", - "settings-brightness": "brightness-up-filled", - "settings-weather": "cloud-rain", + "settings-network": "sitemap", + "settings-brightness": "brightness-up", + "settings-weather": "cloud-sun", "settings-color-scheme": "palette", "settings-wallpaper": "paint", "settings-wallpaper-selector": "library-photo", "settings-screen-recorder": "video", "settings-hooks": "link", + "settings-notification": "bell", "settings-about": "info-square-rounded", "bluetooth": "bluetooth", "bt-device-generic": "bluetooth", - "bt-device-headphones": "headphones-filled", + "bt-device-headphones": "headphones", "bt-device-mouse": "mouse-2", "bt-device-keyboard": "bluetooth", - "bt-device-phone": "device-mobile-filled", + "bt-device-phone": "device-mobile", "bt-device-watch": "device-watch", "bt-device-speaker": "device-speaker", - "bt-device-tv": "device-tv" + "bt-device-tv": "device-tv", + "noctalia": "noctalia" } - // Fonts Codepoints - do not change + // Fonts Codepoints - do not change! + // Some icons have been disabled because Qt's text rendering engine recognizes + // some ranges as special Unicode characters at a very low level. + // ex: fe00-fe2f readonly property var icons: { "123": "\u{f554}", "360": "\u{f62f}", @@ -255,8 +261,8 @@ Singleton { "align-left": "\u{ea09}", "align-left-2": "\u{ff00}", "align-right": "\u{ea0a}", - "align-right-2": "\u{feff}", - "alpha": "\u{f543}", + "alpha"//"align-right-2": "\u{feff}", + : "\u{f543}", "alphabet-arabic": "\u{ff2f}", "alphabet-bangla": "\u{ff2e}", "alphabet-cyrillic": "\u{f1df}", @@ -2044,6 +2050,7 @@ Singleton { "cloud-snow": "\u{ea73}", "cloud-star": "\u{f85b}", "cloud-storm": "\u{ea74}", + "cloud-sun": "\u{ea7a}", "cloud-up": "\u{f85c}", "cloud-upload": "\u{ea75}", "cloud-x": "\u{f85d}", @@ -3087,8 +3094,8 @@ Singleton { "friends": "\u{eab0}", "friends-off": "\u{f136}", "frustum": "\u{fa9f}", - "frustum-off": "\u{fa9d}", - "frustum-plus": "\u{fa9e}", + "frustum-plus"//"frustum-off": "\u{fa9d}", + : "\u{fa9e}", "function": "\u{f225}", "function-filled": "\u{fc2b}", "function-off": "\u{f3f0}", @@ -3347,13 +3354,13 @@ Singleton { "hexagon-letter-x": "\u{f479}", "hexagon-letter-x-filled": "\u{fe30}", "hexagon-letter-y": "\u{f47a}", - "hexagon-letter-y-filled": "\u{fe2f}", - "hexagon-letter-z": "\u{f47b}", - "hexagon-letter-z-filled": "\u{fe2e}", - "hexagon-minus": "\u{fc8f}", + "hexagon-letter-z"//"hexagon-letter-y-filled": "\u{fe2f}", + : "\u{f47b}", + "hexagon-minus"//"hexagon-letter-z-filled": "\u{fe2e}", + : "\u{fc8f}", "hexagon-minus-2": "\u{fc8e}", - "hexagon-minus-filled": "\u{fe2d}", - "hexagon-number-0": "\u{f459}", + "hexagon-number-0"//"hexagon-minus-filled": "\u{fe2d}", + : "\u{f459}", "hexagon-number-0-filled": "\u{f74c}", "hexagon-number-1": "\u{f45a}", "hexagon-number-1-filled": "\u{f74d}", @@ -3376,8 +3383,8 @@ Singleton { "hexagon-off": "\u{ee9c}", "hexagon-plus": "\u{fc45}", "hexagon-plus-2": "\u{fc90}", - "hexagon-plus-filled": "\u{fe2c}", - "hexagonal-prism": "\u{faa5}", + "hexagonal-prism"//"hexagon-plus-filled": "\u{fe2c}", + : "\u{faa5}", "hexagonal-prism-off": "\u{faa3}", "hexagonal-prism-plus": "\u{faa4}", "hexagonal-pyramid": "\u{faa8}", @@ -3407,8 +3414,8 @@ Singleton { "home-eco": "\u{f351}", "home-edit": "\u{f352}", "home-exclamation": "\u{f33c}", - "home-filled": "\u{fe2b}", - "home-hand": "\u{f504}", + "home-hand"//"home-filled": "\u{fe2b}", + : "\u{f504}", "home-heart": "\u{f353}", "home-infinity": "\u{f505}", "home-link": "\u{f354}", @@ -3525,8 +3532,8 @@ Singleton { "ironing-2-filled": "\u{1006e}", "ironing-3": "\u{f2f6}", "ironing-3-filled": "\u{1006d}", - "ironing-filled": "\u{fe2a}", - "ironing-off": "\u{f2f7}", + "ironing-off"//"ironing-filled": "\u{fe2a}", + : "\u{f2f7}", "ironing-steam": "\u{f2f9}", "ironing-steam-filled": "\u{1006c}", "ironing-steam-off": "\u{f2f8}", @@ -3536,8 +3543,8 @@ Singleton { "italic": "\u{eb93}", "jacket": "\u{f661}", "jetpack": "\u{f581}", - "jetpack-filled": "\u{fe29}", - "jewish-star": "\u{f3ff}", + "jewish-star"//"jetpack-filled": "\u{fe29}", + : "\u{f3ff}", "jewish-star-filled": "\u{f67e}", "join-bevel": "\u{ff4c}", "join-round": "\u{ff4b}", @@ -3551,8 +3558,8 @@ Singleton { "kering": "\u{efb8}", "kerning": "\u{efb8}", "key": "\u{eac7}", - "key-filled": "\u{fe28}", - "key-off": "\u{f14b}", + "key-off"//"key-filled": "\u{fe28}", + : "\u{f14b}", "keyboard": "\u{ebd6}", "keyboard-filled": "\u{100a2}", "keyboard-hide": "\u{ec7e}", @@ -3608,20 +3615,20 @@ Singleton { "layers-union": "\u{eacb}", "layout": "\u{eadb}", "layout-2": "\u{eacc}", - "layout-2-filled": "\u{fe27}", - "layout-align-bottom": "\u{eacd}", - "layout-align-bottom-filled": "\u{fe26}", - "layout-align-center": "\u{eace}", - "layout-align-center-filled": "\u{fe25}", - "layout-align-left": "\u{eacf}", - "layout-align-left-filled": "\u{fe24}", - "layout-align-middle": "\u{ead0}", - "layout-align-middle-filled": "\u{fe23}", - "layout-align-right": "\u{ead1}", - "layout-align-right-filled": "\u{fe22}", - "layout-align-top": "\u{ead2}", - "layout-align-top-filled": "\u{fe21}", - "layout-board": "\u{ef95}", + "layout-align-left"//"layout-2-filled": "\u{fe27}", + // "layout-align-bottom": "\u{eacd}", + //"layout-align-bottom-filled": "\u{fe26}", + // "layout-align-center": "\u{eace}", + //"layout-align-center-filled": "\u{fe25}", + : "\u{eacf}", + "layout-align-middle"// "layout-align-left-filled": "\u{fe24}", + : "\u{ead0}", + "layout-align-right"//"layout-align-middle-filled": "\u{fe23}", + : "\u{ead1}", + "layout-align-top"//"layout-align-right-filled": "\u{fe22}", + : "\u{ead2}", + "layout-board"//"layout-align-top-filled": "\u{fe21}", + : "\u{ef95}", "layout-board-filled": "\u{10182}", "layout-board-split": "\u{ef94}", "layout-board-split-filled": "\u{10183}", @@ -3633,8 +3640,8 @@ Singleton { "layout-bottombar-filled": "\u{fc37}", "layout-bottombar-inactive": "\u{fd45}", "layout-cards": "\u{ec13}", - "layout-cards-filled": "\u{fe20}", - "layout-collage": "\u{f389}", + "layout-collage"// "layout-cards-filled": "\u{fe20}", + : "\u{f389}", "layout-columns": "\u{ead4}", "layout-dashboard": "\u{f02c}", "layout-dashboard-filled": "\u{fe1f}", @@ -4115,14 +4122,14 @@ Singleton { "microphone": "\u{eaf0}", "microphone-2": "\u{ef2c}", "microphone-2-off": "\u{f40d}", - "microphone-filled": "\u{fe0f}", - "microphone-off": "\u{ed16}", + "microphone-off"//"microphone-filled": "\u{fe0f}", + : "\u{ed16}", "microscope": "\u{ef64}", "microscope-filled": "\u{10166}", "microscope-off": "\u{f40e}", "microwave": "\u{f248}", - "microwave-filled": "\u{fe0e}", - "microwave-off": "\u{f264}", + "microwave-off"//"microwave-filled": "\u{fe0e}", + : "\u{f264}", "military-award": "\u{f079}", "military-rank": "\u{efcf}", "military-rank-filled": "\u{ff5e}", @@ -4295,6 +4302,7 @@ Singleton { "news-off": "\u{f167}", "nfc": "\u{eeb7}", "nfc-off": "\u{f168}", + "noctalia": "\u{ec33}", "no-copyright": "\u{efb9}", "no-creative-commons": "\u{efba}", "no-derivatives": "\u{efbb}", @@ -4354,18 +4362,18 @@ Singleton { "number-4-small": "\u{fcf9}", "number-40-small": "\u{fffa}", "number-41-small": "\u{fff9}", - "number-42-small": "\u{fff8}", - "number-43-small": "\u{fff7}", - "number-44-small": "\u{fff6}", - "number-45-small": "\u{fff5}", - "number-46-small": "\u{fff4}", - "number-47-small": "\u{fff3}", - "number-48-small": "\u{fff2}", - "number-49-small": "\u{fff1}", - "number-5": "\u{edf5}", + "number-5"//"number-42-small": "\u{fff8}", + // "number-43-small": "\u{fff7}", + // "number-44-small": "\u{fff6}", + // "number-45-small": "\u{fff5}", + // "number-46-small": "\u{fff4}", + // "number-47-small": "\u{fff3}", + // "number-48-small": "\u{fff2}", + // "number-49-small": "\u{fff1}", + : "\u{edf5}", "number-5-small": "\u{fcfa}", - "number-50-small": "\u{fff0}", - "number-51-small": "\u{ffef}", + "number-51-small"// "number-50-small": "\u{fff0}", + : "\u{ffef}", "number-52-small": "\u{ffee}", "number-53-small": "\u{ffed}", "number-54-small": "\u{ffec}", @@ -4805,11 +4813,11 @@ Singleton { "quote": "\u{efbe}", "quote-filled": "\u{1009c}", "quote-off": "\u{f188}", - "quotes": "\u{fb1e}", - "radar": "\u{f017}", + "radar"//"quotes": "\u{fb1e}", + : "\u{f017}", "radar-2": "\u{f016}", - "radar-filled": "\u{fe0d}", - "radar-off": "\u{f41f}", + "radar-off"//"radar-filled": "\u{fe0d}", + : "\u{f41f}", "radio": "\u{ef2d}", "radio-off": "\u{f420}", "radioactive": "\u{ecc0}", @@ -4869,12 +4877,12 @@ Singleton { "regex-off": "\u{f421}", "registered": "\u{eb14}", "relation-many-to-many": "\u{ed7f}", - "relation-many-to-many-filled": "\u{fe0c}", - "relation-one-to-many": "\u{ed80}", - "relation-one-to-many-filled": "\u{fe0b}", - "relation-one-to-one": "\u{ed81}", - "relation-one-to-one-filled": "\u{fe0a}", - "reload": "\u{f3ae}", + "relation-one-to-many"//"relation-many-to-many-filled": "\u{fe0c}", + : "\u{ed80}", + "relation-one-to-one"//"relation-one-to-many-filled": "\u{fe0b}", + : "\u{ed81}", + "reload"//"relation-one-to-one-filled": "\u{fe0a}", + : "\u{f3ae}", "reorder": "\u{fc15}", "repeat": "\u{eb72}", "repeat-off": "\u{f18e}", @@ -5026,8 +5034,8 @@ Singleton { "search": "\u{eb1c}", "search-off": "\u{f19c}", "section": "\u{eed5}", - "section-filled": "\u{fe09}", - "section-sign": "\u{f019}", + "section-sign"//"section-filled": "\u{fe09}", + : "\u{f019}", "seeding": "\u{ed51}", "seeding-filled": "\u{10006}", "seeding-off": "\u{f19d}", @@ -5235,8 +5243,8 @@ Singleton { "sort-z-a": "\u{f550}", "sos": "\u{f24a}", "soup": "\u{ef2e}", - "soup-filled": "\u{fe08}", - "soup-off": "\u{f42d}", + "soup-off"//"soup-filled": "\u{fe08}", + : "\u{f42d}", "source-code": "\u{f4a2}", "space": "\u{ec0c}", "space-off": "\u{f1aa}", @@ -5329,22 +5337,22 @@ Singleton { "square-half": "\u{effb}", "square-key": "\u{f638}", "square-letter-a": "\u{f47c}", - "square-letter-a-filled": "\u{fe07}", - "square-letter-b": "\u{f47d}", - "square-letter-b-filled": "\u{fe06}", - "square-letter-c": "\u{f47e}", - "square-letter-c-filled": "\u{fe05}", - "square-letter-d": "\u{f47f}", - "square-letter-d-filled": "\u{fe04}", - "square-letter-e": "\u{f480}", - "square-letter-e-filled": "\u{fe03}", - "square-letter-f": "\u{f481}", - "square-letter-f-filled": "\u{fe02}", - "square-letter-g": "\u{f482}", - "square-letter-g-filled": "\u{fe01}", - "square-letter-h": "\u{f483}", - "square-letter-h-filled": "\u{fe00}", - "square-letter-i": "\u{f484}", + "square-letter-b"//"square-letter-a-filled": "\u{fe07}", + : "\u{f47d}", + "square-letter-c"//"square-letter-b-filled": "\u{fe06}", + : "\u{f47e}", + "square-letter-d"//"square-letter-c-filled": "\u{fe05}", + : "\u{f47f}", + "square-letter-e"//"square-letter-d-filled": "\u{fe04}", + : "\u{f480}", + "square-letter-f"//"square-letter-e-filled": "\u{fe03}", + : "\u{f481}", + "square-letter-g"//"square-letter-f-filled": "\u{fe02}", + : "\u{f482}", + "square-letter-h"//"square-letter-g-filled": "\u{fe01}", + : "\u{f483}", + "square-letter-i"//"square-letter-h-filled": "\u{fe00}", + : "\u{f484}", "square-letter-i-filled": "\u{fdff}", "square-letter-j": "\u{f485}", "square-letter-j-filled": "\u{fdfe}", diff --git a/Commons/KeyboardLayout.qml b/Commons/KeyboardLayout.qml new file mode 100644 index 0000000..38d08aa --- /dev/null +++ b/Commons/KeyboardLayout.qml @@ -0,0 +1,205 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + // Comprehensive language name to ISO code mapping + property var languageMap: { + "english"// English variants + : "us", + "american": "us", + "united states": "us", + "us english": "us", + "british": "gb", + "uk": "ua", + "united kingdom"// FIXED: Ukrainian language code should map to Ukraine + : "gb", + "english (uk)": "gb", + "canadian": "ca", + "canada": "ca", + "canadian english": "ca", + "australian": "au", + "australia": "au", + "swedish"// Nordic countries + : "se", + "svenska": "se", + "sweden": "se", + "norwegian": "no", + "norsk": "no", + "norway": "no", + "danish": "dk", + "dansk": "dk", + "denmark": "dk", + "finnish": "fi", + "suomi": "fi", + "finland": "fi", + "icelandic": "is", + "íslenska": "is", + "iceland": "is", + "german"// Western/Central European Germanic + : "de", + "deutsch": "de", + "germany": "de", + "austrian": "at", + "austria": "at", + "österreich": "at", + "swiss": "ch", + "switzerland": "ch", + "schweiz": "ch", + "suisse": "ch", + "dutch": "nl", + "nederlands": "nl", + "netherlands": "nl", + "holland": "nl", + "belgian": "be", + "belgium": "be", + "belgië": "be", + "belgique": "be", + "french"// Romance languages (Western/Southern Europe) + : "fr", + "français": "fr", + "france": "fr", + "canadian french": "ca", + "spanish": "es", + "español": "es", + "spain": "es", + "castilian": "es", + "italian": "it", + "italiano": "it", + "italy": "it", + "portuguese": "pt", + "português": "pt", + "portugal": "pt", + "catalan": "ad", + "català": "ad", + "andorra": "ad", + "romanian"// Eastern European Romance + : "ro", + "română": "ro", + "romania": "ro", + "russian"// Slavic languages (Eastern Europe) + : "ru", + "русский": "ru", + "russia": "ru", + "polish": "pl", + "polski": "pl", + "poland": "pl", + "czech": "cz", + "čeština": "cz", + "czech republic": "cz", + "slovak": "sk", + "slovenčina": "sk", + "slovakia": "sk", + "uk": "ua", + "ukrainian"// Ukrainian language code + : "ua", + "українська": "ua", + "ukraine": "ua", + "bulgarian": "bg", + "български": "bg", + "bulgaria": "bg", + "serbian": "rs", + "srpski": "rs", + "serbia": "rs", + "croatian": "hr", + "hrvatski": "hr", + "croatia": "hr", + "slovenian": "si", + "slovenščina": "si", + "slovenia": "si", + "bosnian": "ba", + "bosanski": "ba", + "bosnia": "ba", + "macedonian": "mk", + "македонски": "mk", + "macedonia": "mk", + "irish"// Celtic languages (Western Europe) + : "ie", + "gaeilge": "ie", + "ireland": "ie", + "welsh": "gb", + "cymraeg": "gb", + "wales": "gb", + "scottish": "gb", + "gàidhlig": "gb", + "scotland": "gb", + "estonian"// Baltic languages (Northern Europe) + : "ee", + "eesti": "ee", + "estonia": "ee", + "latvian": "lv", + "latviešu": "lv", + "latvia": "lv", + "lithuanian": "lt", + "lietuvių": "lt", + "lithuania": "lt", + "hungarian"// Other European languages + : "hu", + "magyar": "hu", + "hungary": "hu", + "greek": "gr", + "ελληνικά": "gr", + "greece": "gr", + "albanian": "al", + "shqip": "al", + "albania": "al", + "maltese": "mt", + "malti": "mt", + "malta": "mt", + "turkish"// West/Southwest Asian languages + : "tr", + "türkçe": "tr", + "turkey": "tr", + "arabic": "ar", + "العربية": "ar", + "arab": "ar", + "hebrew": "il", + "עברית": "il", + "israel": "il", + "brazilian"// South American languages + : "br", + "brazilian portuguese": "br", + "brasil": "br", + "brazil": "br", + "japanese"// East Asian languages + : "jp", + "日本語": "jp", + "japan": "jp", + "korean": "kr", + "한국어": "kr", + "korea": "kr", + "south korea": "kr", + "chinese": "cn", + "中文": "cn", + "china": "cn", + "simplified chinese": "cn", + "traditional chinese": "tw", + "taiwan": "tw", + "繁體中文": "tw", + "thai"// Southeast Asian languages + : "th", + "ไทย": "th", + "thailand": "th", + "vietnamese": "vn", + "tiếng việt": "vn", + "vietnam": "vn", + "hindi"// South Asian languages + : "in", + "हिन्दी": "in", + "india": "in", + "afrikaans"// African languages + : "za", + "south africa": "za", + "south african": "za", + "qwerty"// Layout variants + : "us", + "dvorak": "us", + "colemak": "us", + "workman": "us", + "azerty": "fr", + "norman": "fr", + "qwertz": "de" + } +} diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 762a92c..7d3d885 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -13,11 +13,8 @@ Singleton { // Default config directory: ~/.config/noctalia // Default cache directory: ~/.cache/noctalia property string shellName: "noctalia" - property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") - || Quickshell.env( - "HOME") + "/.config") + "/" + shellName + "/" - property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( - "HOME") + "/.cache") + "/" + shellName + "/" + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/" property string cacheDirImages: cacheDir + "images/" property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") @@ -58,8 +55,7 @@ Singleton { } } if (!hasValidBarMonitor) { - Logger.warn("Settings", - "No configured bar monitors found on system, clearing bar monitor list to show on all screens") + Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens") adapter.bar.monitors = [] } else { @@ -138,13 +134,18 @@ Singleton { widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon break case "Battery": - widget.alwaysShowPercentage = widget.alwaysShowPercentage - !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage + widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage break case "Clock": - widget.showDate = widget.showDate !== undefined ? widget.showDate : adapter.location.showDateWithClock widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth + if (widget.showDate !== undefined) { + widget.displayFormat = "time-date" + } else if (widget.showSeconds) { + widget.displayFormat = "time-seconds" + } + delete widget.showDate + delete widget.showSeconds break case "MediaMini": widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt @@ -174,7 +175,7 @@ Singleton { } } - // Backup the widget definition before altering + // Compare settings, to detect if something has been upgraded const widgetAfter = JSON.stringify(widget) return (widgetAfter !== widgetBefore) } @@ -258,14 +259,19 @@ Singleton { JsonAdapter { id: adapter - property int settingsVersion: 1 + property int settingsVersion: 2 // bar property JsonObject bar: JsonObject { - property string position: "top" // "top" or "bottom" + property string position: "top" // "top", "bottom", "left", or "right" property real backgroundOpacity: 1.0 property list monitors: [] + // Floating bar settings + property bool floating: false + property real marginVertical: 0.25 + property real marginHorizontal: 0.25 + property bool showActiveWindowIcon: true // TODO: delete property bool alwaysShowBatteryPercentage: false // TODO: delete property bool showNetworkStats: false // TODO: delete @@ -316,7 +322,9 @@ Singleton { property string avatarImage: defaultAvatar property bool dimDesktop: false property bool showScreenCorners: false + property bool forceBlackScreenCorners: false property real radiusRatio: 1.0 + property real screenRadiusRatio: 1.0 // Animation speed multiplier (0.1x - 2.0x) property real animationSpeed: 1.0 } @@ -376,6 +384,7 @@ Singleton { property bool autoHide: false property bool exclusive: false property real backgroundOpacity: 1.0 + property real floatingRatio: 1.0 property list monitors: [] } @@ -391,6 +400,10 @@ Singleton { property list monitors: [] // Last time the user opened the notification history (ms since epoch) property real lastSeenTs: 0 + // Duration settings for different urgency levels (in seconds) + property int lowUrgencyDuration: 3 + property int normalUrgencyDuration: 8 + property int criticalUrgencyDuration: 15 } // audio @@ -437,6 +450,7 @@ Singleton { property bool foot: false property bool fuzzel: false property bool vesktop: false + property bool pywalfox: false property bool enableUserTemplates: false } diff --git a/Commons/Style.qml b/Commons/Style.qml index 6c059e7..99d88e6 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -35,6 +35,9 @@ Singleton { property int radiusM: 16 * Settings.data.general.radiusRatio property int radiusL: 20 * Settings.data.general.radiusRatio + //screen Radii + property int screenRadius: 20 * Settings.data.general.screenRadiusRatio + // Border property int borderS: 3 property int borderM: 3 @@ -63,9 +66,9 @@ Singleton { property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed) // Dimensions - property int barHeight: 36 + property int barHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37 property int capsuleHeight: (barHeight * 0.73) - property int baseWidgetSize: 32 + property int baseWidgetSize: (barHeight * 0.9) property int sliderWidth: 200 // Delays diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 61d4fa4..7f588e7 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -43,9 +43,7 @@ Variants { // Fillmode default is "crop" property real fillMode: 1.0 - property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, - Settings.data.wallpaper.fillColor.g, - Settings.data.wallpaper.fillColor.b, 1.0) + property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0) // On startup assign wallpaper immediately Component.onCompleted: { @@ -229,8 +227,7 @@ Variants { from: 0.0 to: 1.0 // The stripes shader feels faster visually, we make it a bit slower here. - duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration - * 1.6 : Settings.data.wallpaper.transitionDuration + duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration * 1.6 : Settings.data.wallpaper.transitionDuration easing.type: Easing.InOutCubic onFinished: { // Swap images after transition completes diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index b971ebe..0828be9 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -60,6 +60,7 @@ Variants { MultiEffect { anchors.fill: parent source: bgImage + autoPaddingEnabled: false blurEnabled: true blur: 0.48 blurMax: 128 @@ -68,9 +69,7 @@ Variants { // Make the overview darker Rectangle { anchors.fill: parent - color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, - Style.opacityMedium) : Qt.alpha(Color.mOnSurface, - Style.opacityMedium) + color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium) } } } diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index ae1709e..e5008a0 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -19,9 +19,9 @@ Loader { property real scaling: ScalingService.getScreenScale(screen) screen: modelData - property color cornerColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) - property real cornerRadius: 20 * scaling - property real cornerSize: 20 * scaling + property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + property real cornerRadius: Style.screenRadius * scaling + property real cornerSize: Style.screenRadius * scaling Connections { target: ScalingService @@ -46,12 +46,12 @@ Loader { } margins { - top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) - || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" - && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 - bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) - || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" - && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 + // When bar is floating, corners should be at screen edges (no margins) + // When bar is not floating, respect bar margins as before + top: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 + bottom: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 + left: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 + right: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0 } mask: Region {} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index dfcb721..11a618d 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -27,118 +27,222 @@ Variants { } } - active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) : false + active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false sourceComponent: PanelWindow { screen: modelData || null WlrLayershell.namespace: "noctalia-bar" - implicitHeight: Math.round(Style.barHeight * scaling) + implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling) + implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width color: Color.transparent anchors { - top: Settings.data.bar.position === "top" - bottom: Settings.data.bar.position === "bottom" - left: true - right: true + top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + } + + // Floating bar margins - only apply when floating is enabled + margins { + top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0 + bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0 + left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0 + right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0 } Item { anchors.fill: parent clip: true - // Background fill + // Background fill with shadow Rectangle { id: bar anchors.fill: parent color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + + // Floating bar rounded corners + radius: Settings.data.bar.floating ? Style.radiusL : 0 } - // ------------------------------ - // Left Section - Dynamic Widgets - Row { - id: leftSection - objectName: "leftSection" + // For vertical bars, use a single column layout + Loader { + id: verticalBarLayout + anchors.fill: parent + visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + sourceComponent: verticalBarComponent + } - height: parent.height - anchors.left: parent.left - anchors.leftMargin: Style.marginS * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling + // For horizontal bars, use the original three-section layout + Loader { + id: horizontalBarLayout + anchors.fill: parent + visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + sourceComponent: horizontalBarComponent + } - Repeater { - model: Settings.data.bar.widgets.left - delegate: NWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - widgetProps: { - "screen": root.modelData || null, - "scaling": ScalingService.getScreenScale(screen), - "widgetId": modelData.id, - "barSection": parent.objectName, - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length + // Main layout components + Component { + id: verticalBarComponent + Item { + anchors.fill: parent + + // Top section (left widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Style.marginM * root.scaling + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.left + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + } + Layout.alignment: Qt.AlignHCenter + } } + } + + // Center section (center widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.center + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + } + Layout.alignment: Qt.AlignHCenter + } + } + } + + // Bottom section (right widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginM * root.scaling + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.right + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + } + Layout.alignment: Qt.AlignHCenter + } + } } } } - // ------------------------------ - // Center Section - Dynamic Widgets - Row { - id: centerSection - objectName: "centerSection" + Component { + id: horizontalBarComponent + Item { + anchors.fill: parent - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - - Repeater { - model: Settings.data.bar.widgets.center - delegate: NWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - widgetProps: { - "screen": root.modelData || null, - "scaling": ScalingService.getScreenScale(screen), - "widgetId": modelData.id, - "barSection": parent.objectName, - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } + // Left Section + RowLayout { + id: leftSection + objectName: "leftSection" + anchors.left: parent.left + anchors.leftMargin: Style.marginS * root.scaling anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.left + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + } + Layout.alignment: Qt.AlignVCenter + } + } } - } - } - // ------------------------------ - // Right Section - Dynamic Widgets - Row { - id: rightSection - objectName: "rightSection" - - height: parent.height - anchors.right: bar.right - anchors.rightMargin: Style.marginS * scaling - anchors.verticalCenter: bar.verticalCenter - spacing: Style.marginS * scaling - - Repeater { - model: Settings.data.bar.widgets.right - delegate: NWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - widgetProps: { - "screen": root.modelData || null, - "scaling": ScalingService.getScreenScale(screen), - "widgetId": modelData.id, - "barSection": parent.objectName, - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.right.length - } + // Center Section + RowLayout { + id: centerSection + objectName: "centerSection" + anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.center + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + } + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Right Section + RowLayout { + id: rightSection + objectName: "rightSection" + anchors.right: parent.right + anchors.rightMargin: Style.marginS * root.scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * root.scaling + + Repeater { + model: Settings.data.bar.widgets.right + delegate: NWidgetLoader { + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + } + Layout.alignment: Qt.AlignVCenter + } + } } } } diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml index c1582ec..be43621 100644 --- a/Modules/Bar/Extras/TrayMenu.qml +++ b/Modules/Bar/Extras/TrayMenu.qml @@ -31,8 +31,7 @@ PopupWindow { implicitWidth: menuWidth * scaling // Use the content height of the Flickable for implicit height - implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, - flickable.contentHeight + (Style.marginS * 2 * scaling)) + implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling)) visible: false color: Color.transparent anchor.item: anchorItem @@ -159,8 +158,7 @@ PopupWindow { NText { id: text Layout.fillWidth: true - color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant + color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..." font.pointSize: Style.fontSizeS * scaling verticalAlignment: Text.AlignVCenter @@ -180,7 +178,7 @@ PopupWindow { font.pointSize: Style.fontSizeS * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false - color: Color.mOnSurface + color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) } } @@ -222,9 +220,32 @@ PopupWindow { const submenuWidth = menuWidth * scaling // Assuming a similar width as the parent const overlap = 4 * scaling // A small overlap to bridge the mouse path - // Check if there's enough space on the right + // Determine submenu opening direction based on bar position and available space + let openLeft = false + + // Check bar position first + const barPosition = Settings.data.bar.position const globalPos = entry.mapToGlobal(0, 0) - const openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width)) + + if (barPosition === "right") { + // Bar is on the right, prefer opening submenus to the left + openLeft = true + } else if (barPosition === "left") { + // Bar is on the left, prefer opening submenus to the right + openLeft = false + } else { + // Bar is horizontal (top/bottom) or undefined, use space-based logic + openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width)) + + // Secondary check: ensure we don't open off-screen + if (openLeft && globalPos.x - submenuWidth < 0) { + // Would open off the left edge, force right opening + openLeft = false + } else if (!openLeft && globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width)) { + // Would open off the right edge, force left opening + openLeft = true + } + } // Position with overlap const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index ed657b3..e9bb66d 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -8,20 +8,19 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Item { id: root property ShellScreen screen property real scaling: 1.0 // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -37,34 +36,85 @@ RowLayout { readonly property real minWidth: Math.max(1, screen.width * 0.06) readonly property real maxWidth: minWidth * 2 + readonly property string barPosition: Settings.data.bar.position + implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) + implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) + function getTitle() { - return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" + try { + return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" + } catch (e) { + Logger.warn("ActiveWindow", "Error getting title:", e) + return "" + } } - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling visible: getTitle() !== "" + function calculatedVerticalHeight() { + // Use standard widget height like other widgets + return Math.round(Style.capsuleHeight * scaling) + } + + function calculatedHorizontalWidth() { + let total = Style.marginM * 2 * scaling // internal padding + + if (showIcon) { + total += Style.baseWidgetSize * 0.5 * scaling + 2 * scaling // icon + spacing + } + + // Calculate actual text width more accurately + const title = getTitle() + if (title !== "") { + // Estimate text width: average character width * number of characters + const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate + const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling) + total += titleWidth + } + + // Row layout handles spacing between widgets + return Math.max(total, Style.capsuleHeight * scaling) // Minimum width + } + function getAppIcon() { - // Try CompositorService first - const focusedWindow = CompositorService.getFocusedWindow() - if (focusedWindow && focusedWindow.appId) { - const idValue = focusedWindow.appId - const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue) - return AppIcons.iconForAppId(normalizedId.toLowerCase()) - } - - // Fallback to ToplevelManager - if (ToplevelManager && ToplevelManager.activeToplevel) { - const activeToplevel = ToplevelManager.activeToplevel - if (activeToplevel.appId) { - const idValue2 = activeToplevel.appId - const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2) - return AppIcons.iconForAppId(normalizedId2.toLowerCase()) + try { + // Try CompositorService first + const focusedWindow = CompositorService.getFocusedWindow() + if (focusedWindow && focusedWindow.appId) { + try { + const idValue = focusedWindow.appId + const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue) + const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase()) + if (iconResult && iconResult !== "") { + return iconResult + } + } catch (iconError) { + Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError) + } } - } - return "" + // Fallback to ToplevelManager + if (ToplevelManager && ToplevelManager.activeToplevel) { + try { + const activeToplevel = ToplevelManager.activeToplevel + if (activeToplevel.appId) { + const idValue2 = activeToplevel.appId + const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2) + const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase()) + if (iconResult2 && iconResult2 !== "") { + return iconResult2 + } + } + } catch (fallbackError) { + Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError) + } + } + + return "" + } catch (e) { + Logger.warn("ActiveWindow", "Error in getAppIcon:", e) + return "" + } } // A hidden text element to safely measure the full title width @@ -79,27 +129,31 @@ RowLayout { Rectangle { id: windowTitleRect visible: root.visible - Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) + height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant Item { id: mainContainer anchors.fill: parent - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling + anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling + anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling clip: true + // Horizontal layout for top/bottom bars RowLayout { - id: contentLayout + id: horizontalLayout anchors.centerIn: parent - spacing: Style.marginS * scaling + spacing: 2 * scaling + visible: barPosition === "top" || barPosition === "bottom" // Window icon Item { - Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 - Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 + Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling Layout.alignment: Qt.AlignVCenter visible: getTitle() !== "" && showIcon @@ -110,16 +164,28 @@ RowLayout { asynchronous: true smooth: true visible: source !== "" + + // Handle loading errors gracefully + onStatusChanged: { + if (status === Image.Error) { + Logger.warn("ActiveWindow", "Failed to load icon:", source) + } + } } } NText { id: titleText Layout.preferredWidth: { - if (mouseArea.containsMouse) { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) - } else { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) + try { + if (mouseArea.containsMouse) { + return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) + } else { + return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars + } + } catch (e) { + Logger.warn("ActiveWindow", "Error calculating width:", e) + return 80 * scaling } } Layout.alignment: Qt.AlignVCenter @@ -141,12 +207,65 @@ RowLayout { } } + // Vertical layout for left/right bars - icon only + Item { + id: verticalLayout + anchors.centerIn: parent + width: parent.width - Style.marginXS * scaling * 2 + height: parent.height - Style.marginXS * scaling * 2 + visible: barPosition === "left" || barPosition === "right" + + // Window icon + Item { + width: Style.baseWidgetSize * 0.5 * scaling + height: Style.baseWidgetSize * 0.5 * scaling + anchors.centerIn: parent + visible: getTitle() !== "" && showIcon + + IconImage { + id: windowIconVertical + anchors.fill: parent + source: getAppIcon() + asynchronous: true + smooth: true + visible: source !== "" + + // Handle loading errors gracefully + onStatusChanged: { + if (status === Image.Error) { + Logger.warn("ActiveWindow", "Failed to load icon:", source) + } + } + } + } + } + // Mouse area for hover detection MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + onEntered: { + if (barPosition === "left" || barPosition === "right") { + tooltip.show() + } + } + onExited: { + if (barPosition === "left" || barPosition === "right") { + tooltip.hide() + } + } + } + + // Hover tooltip with full title (only for vertical bars) + NTooltip { + id: tooltip + target: verticalLayout + text: getTitle() + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + delay: 500 } } } @@ -154,10 +273,20 @@ RowLayout { Connections { target: CompositorService function onActiveWindowChanged() { - windowIcon.source = Qt.binding(getAppIcon) + try { + windowIcon.source = Qt.binding(getAppIcon) + windowIconVertical.source = Qt.binding(getAppIcon) + } catch (e) { + Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e) + } } function onWindowListChanged() { - windowIcon.source = Qt.binding(getAppIcon) + try { + windowIcon.source = Qt.binding(getAppIcon) + windowIconVertical.source = Qt.binding(getAppIcon) + } catch (e) { + Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e) + } } } } diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index c76722d..fa2705b 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -14,13 +14,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -30,21 +29,18 @@ Item { return {} } - // Resolve settings: try user settings or defaults from BarWidgetRegistry - readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage - !== undefined ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage - readonly property real warningThreshold: widgetSettings.warningThreshold - !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold // Test mode readonly property bool testMode: false - readonly property int testPercent: 90 + readonly property int testPercent: 100 readonly property bool testCharging: false // Main properties readonly property var battery: UPower.displayDevice - readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery - && battery.isPresent) + readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) property bool hasNotifiedLowBattery: false @@ -89,11 +85,12 @@ Item { id: pill rightOpen: BarWidgetRegistry.getNPillDirection(root) - icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, - charging, isReady) - text: (isReady || testMode) ? Math.round(percent) + "%" : "-" + icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady) + text: (isReady || testMode) ? Math.round(percent) : "-" + suffix: "%" autoHide: false - forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage + forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) tooltipText: { let lines = [] @@ -113,8 +110,7 @@ Item { if (battery.changeRate !== undefined) { const rate = battery.changeRate if (rate > 0) { - lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed( - 2) + " W.") + lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.") } else if (rate < 0) { lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.") } else { diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index 246852f..41588bf 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -21,5 +21,6 @@ NIconButton { icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off" tooltipText: "Bluetooth devices." - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this) + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) } diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 477cf19..0ca072e 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -13,13 +13,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -29,8 +28,8 @@ Item { return {} } - readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false @@ -82,15 +81,16 @@ Item { autoHide: false // Important to be false so we can hover as long as we want text: { var monitor = getMonitor() - return monitor ? (Math.round(monitor.brightness * 100) + "%") : "" + return monitor ? Math.round(monitor.brightness * 100) : "" } - forceOpen: userAlwaysShowPercentage + suffix: text.length > 0 ? "%" : "-" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" tooltipText: { var monitor = getMonitor() if (!monitor) return "" - return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method - + "\nLeft click for advanced settings.\nScroll up/down to change brightness." + return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nRight click for settings.\nScroll to modify brightness." } onWheel: function (angle) { @@ -106,8 +106,14 @@ Item { onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Brightness - settingsPanel.open(screen) + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.open() + } + + onRightClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.open() } } } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 71526e2..45ebece 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services @@ -12,13 +13,12 @@ Rectangle { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -28,45 +28,178 @@ Rectangle { return {} } - // Resolve settings: try user settings or defaults from BarWidgetRegistry - readonly property bool showDate: widgetSettings.showDate !== undefined ? widgetSettings.showDate : widgetMetadata.showDate - readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock - readonly property bool showSeconds: widgetSettings.showSeconds !== undefined ? widgetSettings.showSeconds : widgetMetadata.showSeconds - readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth - !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth + readonly property string barPosition: Settings.data.bar.position - implicitWidth: clock.width + Style.marginM * 2 * scaling - implicitHeight: Math.round(Style.capsuleHeight * scaling) - radius: Math.round(Style.radiusM * scaling) + // Resolve settings: try user settings or defaults from BarWidgetRegistry + readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock + readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth + readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat + + // Use compact mode for vertical bars + readonly property bool verticalMode: barPosition === "left" || barPosition === "right" + + implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling) + implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.baseWidgetSize * 0.8 * scaling) // Match NPill + + radius: Math.round(Style.radiusS * scaling) color: Color.mSurfaceVariant - // Clock Icon with attached calendar - NText { - id: clock - text: { - const now = Time.date - const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm") - const timeString = Qt.formatDateTime(now, timeFormat) + Item { + id: clockContainer + anchors.fill: parent + anchors.margins: Style.marginXS * scaling - if (showDate) { - let dayName = now.toLocaleDateString(Qt.locale(), "ddd") - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) - let day = now.getDate() - let month = now.toLocaleDateString(Qt.locale(), "MMM") - return timeString + " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) + ColumnLayout { + id: layout + anchors.centerIn: parent + spacing: verticalMode ? -2 * scaling : -3 * scaling + + // Compact mode for vertical bars - Time section (HH, MM) + Repeater { + model: verticalMode ? 2 : 1 + NText { + readonly property bool showSeconds: (displayFormat === "time-seconds") + readonly property bool inlineDate: (displayFormat === "time-date") + readonly property var now: Time.date + + text: { + if (verticalMode) { + // Compact mode: time section (first 2 lines) + switch (index) { + case 0: + // Hours + if (use12h) { + const hours = now.getHours() + const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours) + return displayHours.toString().padStart(2, '0') + } else { + return now.getHours().toString().padStart(2, '0') + } + case 1: + // Minutes + return now.getMinutes().toString().padStart(2, '0') + default: + return "" + } + } else { + // Normal mode: single line with time + let timeStr = "" + + if (use12h) { + // 12-hour format with proper padding and consistent spacing + const hours = now.getHours() + const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours) + const paddedHours = displayHours.toString().padStart(2, '0') + const minutes = now.getMinutes().toString().padStart(2, '0') + const ampm = hours < 12 ? 'AM' : 'PM' + + if (showSeconds) { + const seconds = now.getSeconds().toString().padStart(2, '0') + timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}` + } else { + timeStr = `${paddedHours}:${minutes} ${ampm}` + } + } else { + // 24-hour format with padding + const hours = now.getHours().toString().padStart(2, '0') + const minutes = now.getMinutes().toString().padStart(2, '0') + + if (showSeconds) { + const seconds = now.getSeconds().toString().padStart(2, '0') + timeStr = `${hours}:${minutes}:${seconds}` + } else { + timeStr = `${hours}:${minutes}` + } + } + + // Add inline date if needed + if (inlineDate) { + let dayName = now.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + const day = now.getDate().toString().padStart(2, '0') + let month = now.toLocaleDateString(Qt.locale(), "MMM") + timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) + } + + return timeStr + } + } + + font.family: Settings.data.ui.fontFixed + font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + } + + // Separator line for compact mode (between time and date) + Rectangle { + visible: verticalMode + Layout.preferredWidth: 20 * scaling + Layout.preferredHeight: 2 * scaling + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 3 * scaling + Layout.bottomMargin: 3 * scaling + color: Color.mPrimary + opacity: 0.3 + radius: 1 * scaling + } + + // Compact mode for vertical bars - Date section (DD, MM) + Repeater { + model: verticalMode ? 2 : 0 + NText { + readonly property var now: Time.date + + text: { + if (verticalMode) { + // Compact mode: date section (last 2 lines) + switch (index) { + case 0: + // Day + return now.getDate().toString().padStart(2, '0') + case 1: + // Month + return (now.getMonth() + 1).toString().padStart(2, '0') + default: + return "" + } + } + return "" + } + + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + } + + // Second line for normal mode (date) + NText { + visible: !verticalMode && (displayFormat === "time-date-short") + text: { + const now = Time.date + const day = now.getDate().toString().padStart(2, '0') + const month = (now.getMonth() + 1).toString().padStart(2, '0') + return reverseDayMonth ? `${month}/${day}` : `${day}/${month}` + } + + // Enable fixed-width font for consistent spacing + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightRegular + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } - return timeString } - anchors.centerIn: parent - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary } NTooltip { id: tooltip text: `${Time.formatDate(reverseDayMonth)}.` - target: clock + target: clockContainer positionAbove: Settings.data.bar.position === "bottom" } @@ -85,7 +218,7 @@ Rectangle { } onClicked: { tooltip.hide() - PanelService.getPanel("calendarPanel")?.toggle(screen, this) + PanelService.getPanel("calendarPanel")?.toggle(this) } } } diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 8027135..9ab1b11 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -1,12 +1,13 @@ import QtQuick import QtQuick.Layouts import Quickshell +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets import qs.Modules.SettingsPanel -NIconButton { +Item { id: root // Widget properties passed from Bar.qml @@ -15,13 +16,12 @@ NIconButton { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -36,33 +36,80 @@ NIconButton { readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec + readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "") + readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000) readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec) - enabled: hasExec - allowClickWhenDisabled: true // we want to be able to open config with left click when its not setup properly - colorBorder: Color.transparent - colorBorderHover: Color.transparent - sizeRatio: 0.8 - icon: customIcon - tooltipText: { - if (!hasExec) { - return "Custom Button - Configure in settings" - } else { - var lines = [] - if (leftClickExec !== "") { - lines.push(`Left click: ${leftClickExec}.`) + implicitWidth: pill.width + implicitHeight: pill.height + + NPill { + id: pill + + rightOpen: BarWidgetRegistry.getNPillDirection(root) + icon: customIcon + text: _dynamicText + autoHide: false + forceOpen: _dynamicText !== "" + forceClose: false + disableOpen: true + tooltipText: { + if (!hasExec) { + return "Custom Button - Configure in settings" + } else { + var lines = [] + if (leftClickExec !== "") { + lines.push(`Left click: ${leftClickExec}.`) + } + if (rightClickExec !== "") { + lines.push(`Right click: ${rightClickExec}.`) + } + if (middleClickExec !== "") { + lines.push(`Middle click: ${middleClickExec}.`) + } + return lines.join("\n") } - if (rightClickExec !== "") { - lines.push(`Right click: ${rightClickExec}.`) - } - if (middleClickExec !== "") { - lines.push(`Middle click: ${middleClickExec}.`) - } - return lines.join("
") + } + + onClicked: root.onClicked() + onRightClicked: root.onRightClicked() + onMiddleClicked: root.onMiddleClicked() + } + + // Internal state for dynamic text + property string _dynamicText: "" + + // Periodically run the text command (if set) + Timer { + id: refreshTimer + interval: Math.max(250, textIntervalMs) + repeat: true + running: (textCommand && textCommand.length > 0) + triggeredOnStart: true + onTriggered: { + if (!textCommand || textCommand.length === 0) + return + if (textProc.running) + return + textProc.command = ["sh", "-lc", textCommand] + textProc.running = true } } - onClicked: { + Process { + id: textProc + stdout: StdioCollector {} + stderr: StdioCollector {} + onExited: (exitCode, exitStatus) => { + var out = String(stdout.text || "").trim() + if (out.indexOf("\n") !== -1) { + out = out.split("\n")[0] + } + _dynamicText = out + } + } + + function onClicked() { if (leftClickExec) { Quickshell.execDetached(["sh", "-c", leftClickExec]) Logger.log("CustomButton", `Executing command: ${leftClickExec}`) @@ -70,18 +117,18 @@ NIconButton { // No script was defined, open settings var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.Bar - settingsPanel.open(screen) + settingsPanel.open() } } - onRightClicked: { + function onRightClicked() { if (rightClickExec) { Quickshell.execDetached(["sh", "-c", rightClickExec]) Logger.log("CustomButton", `Executing command: ${rightClickExec}`) } } - onMiddleClicked: { + function onMiddleClicked() { if (middleClickExec) { Quickshell.execDetached(["sh", "-c", middleClickExec]) Logger.log("CustomButton", `Executing command: ${middleClickExec}`) diff --git a/Modules/Bar/Widgets/DarkModeToggle.qml b/Modules/Bar/Widgets/DarkModeToggle.qml index b01d383..cf68958 100644 --- a/Modules/Bar/Widgets/DarkModeToggle.qml +++ b/Modules/Bar/Widgets/DarkModeToggle.qml @@ -17,7 +17,5 @@ NIconButton { colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary colorBorder: Color.transparent colorBorderHover: Color.transparent - - anchors.verticalCenter: parent.verticalCenter onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode } diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index a09d1e6..87aef27 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -13,6 +13,25 @@ Item { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode + // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout @@ -26,9 +45,10 @@ Item { rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: "keyboard" autoHide: false // Important to be false so we can hover as long as we want - text: currentLayout - tooltipText: "Keyboard layout: " + currentLayout - + text: currentLayout.toUpperCase() + tooltipText: "Keyboard layout: " + currentLayout.toUpperCase() + forceOpen: root.displayMode === "forceOpen" + forceClose: root.displayMode === "alwaysHide" onClicked: { // You could open keyboard settings here if needed diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 8d48a85..5cdf228 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Item { id: root property ShellScreen screen @@ -15,13 +15,12 @@ RowLayout { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -31,12 +30,11 @@ RowLayout { return {} } - readonly property bool showAlbumArt: (widgetSettings.showAlbumArt - !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt - readonly property bool showVisualizer: (widgetSettings.showVisualizer - !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer - readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType - !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType + readonly property string barPosition: Settings.data.bar.position + + readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt + readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer + readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType // 6% of total width readonly property real minWidth: Math.max(1, screen.width * 0.06) @@ -46,10 +44,26 @@ RowLayout { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") } - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling + function calculatedVerticalHeight() { + return Math.round(Style.baseWidgetSize * 0.8 * scaling) + } + + function calculatedHorizontalWidth() { + let total = Style.marginM * 2 * scaling // internal padding + if (showAlbumArt) { + total += 18 * scaling + 2 * scaling // album art + spacing + } else { + total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing + } + total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text + // Row layout handles spacing between widgets + return total + } + + implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0 + implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0 + visible: MediaService.currentPlayer !== null && MediaService.canPlay - Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 // A hidden text element to safely measure the full title width NText { @@ -61,12 +75,12 @@ RowLayout { Rectangle { id: mediaMini - - Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - - radius: Math.round(Style.radiusM * scaling) + visible: root.visible + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling) + height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling) + radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant // Used to anchor the tooltip, so the tooltip does not move when the content expands @@ -79,8 +93,8 @@ RowLayout { Item { id: mainContainer anchors.fill: parent - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling + anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling + anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling Loader { anchors.verticalCenter: parent.verticalCenter @@ -127,10 +141,12 @@ RowLayout { } } + // Horizontal layout for top/bottom bars RowLayout { id: rowLayout anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling + visible: barPosition === "top" || barPosition === "bottom" z: 1 // Above the visualizer NIcon { @@ -191,6 +207,33 @@ RowLayout { } } + // Vertical layout for left/right bars - icon only + Item { + id: verticalLayout + anchors.centerIn: parent + width: parent.width - Style.marginM * scaling * 2 + height: parent.height - Style.marginM * scaling * 2 + visible: barPosition === "left" || barPosition === "right" + z: 1 // Above the visualizer + + // Media icon + Item { + width: Style.baseWidgetSize * 0.5 * scaling + height: Style.baseWidgetSize * 0.5 * scaling + anchors.centerIn: parent + visible: getTitle() !== "" + + NIcon { + id: mediaIconVertical + anchors.fill: parent + icon: MediaService.isPlaying ? "media-pause" : "media-play" + font.pointSize: Style.fontSizeL * scaling + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + } + // Mouse area for hover detection MouseArea { id: mouseArea @@ -213,12 +256,18 @@ RowLayout { } onEntered: { - if (tooltip.text !== "") { + if (barPosition === "left" || barPosition === "right") { + tooltip.show() + } else if (tooltip.text !== "") { tooltip.show() } } onExited: { - tooltip.hide() + if (barPosition === "left" || barPosition === "right") { + tooltip.hide() + } else { + tooltip.hide() + } } } } @@ -227,16 +276,23 @@ RowLayout { NTooltip { id: tooltip text: { - var str = "" - if (MediaService.canGoNext) { - str += "Right click for next.\n" + if (barPosition === "left" || barPosition === "right") { + return getTitle() + } else { + var str = "" + if (MediaService.canGoNext) { + str += "Right click for next.\n" + } + if (MediaService.canGoPrevious) { + str += "Middle click for previous." + } + return str } - if (MediaService.canGoPrevious) { - str += "Middle click for previous." - } - return str } - target: anchor + target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor + positionLeft: barPosition === "right" + positionRight: barPosition === "left" positionAbove: Settings.data.bar.position === "bottom" + delay: 500 } } diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 4e61a66..3844260 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -15,13 +15,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -31,8 +30,8 @@ Item { return {} } - readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode // Used to avoid opening the pill on Quickshell startup property bool firstInputVolumeReceived: false @@ -89,14 +88,14 @@ Item { NPill { id: pill - rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want - text: Math.floor(AudioService.inputVolume * 100) + "%" - forceOpen: alwaysShowPercentage - tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) - + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + text: Math.floor(AudioService.inputVolume * 100) + suffix: "%" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" + tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume." onWheel: function (delta) { wheelAccumulator += delta @@ -109,12 +108,12 @@ Item { } } onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Audio - settingsPanel.open(screen) + AudioService.setInputMuted(!AudioService.inputMuted) } onRightClicked: { - AudioService.setInputMuted(!AudioService.inputMuted) + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Audio + settingsPanel.open() } onMiddleClicked: { Quickshell.execDetached(["pwvucontrol"]) diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 9cd2e74..43028a1 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -15,8 +15,8 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - colorBg: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? Color.mTertiary : Color.mPrimary) : Color.mSurfaceVariant - colorFg: Settings.data.nightLight.enabled ? Color.mOnPrimary : Color.mOnSurface + colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.mSurfaceVariant + colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent @@ -36,7 +36,7 @@ NIconButton { onRightClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Brightness - settingsPanel.open(screen) + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.open() } } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 94d3a7a..3021b47 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -15,13 +15,12 @@ NIconButton { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -30,10 +29,8 @@ NIconButton { } return {} } - readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge - !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge - readonly property bool hideWhenZero: (widgetSettings.hideWhenZero - !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero + readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge + readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero function lastSeenTs() { return Settings.data.notifications?.lastSeenTs || 0 @@ -62,7 +59,7 @@ NIconButton { onClicked: { var panel = PanelService.getPanel("notificationHistoryPanel") - panel?.toggle(screen, this) + panel?.toggle(this) Settings.data.notifications.lastSeenTs = Time.timestamp * 1000 } diff --git a/Modules/Bar/Widgets/PowerToggle.qml b/Modules/Bar/Widgets/PowerToggle.qml index 25e380c..eccecf3 100644 --- a/Modules/Bar/Widgets/PowerToggle.qml +++ b/Modules/Bar/Widgets/PowerToggle.qml @@ -19,5 +19,5 @@ NIconButton { colorFg: Color.mError colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("powerPanel")?.toggle(screen) + onClicked: PanelService.getPanel("powerPanel")?.toggle() } diff --git a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml index 00a785b..efa8299 100644 --- a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml +++ b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml @@ -16,6 +16,5 @@ NIconButton { sizeRatio: 0.8 colorBg: Color.mPrimary colorFg: Color.mOnPrimary - anchors.verticalCenter: parent.verticalCenter onClicked: ScreenRecorderService.toggleRecording() } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 00343be..c3347a8 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -14,13 +14,12 @@ NIconButton { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -30,39 +29,27 @@ NIconButton { return {} } - readonly property bool useDistroLogo: (widgetSettings.useDistroLogo - !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo + readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo - icon: useDistroLogo ? "" : "apps" + icon: useDistroLogo ? "" : "noctalia" tooltipText: "Open side panel." - sizeRatio: 0.8 + sizeRatio: 0.85 colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface + colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary colorBorder: Color.transparent - colorBorderHover: Color.transparent - - anchors.verticalCenter: parent.verticalCenter - onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this) - onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen) + colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent + onClicked: PanelService.getPanel("sidePanel")?.toggle(this) + onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() IconImage { id: logo anchors.centerIn: parent - width: root.width * 0.6 + width: root.width * 0.85 height: width source: useDistroLogo ? DistroLogoService.osLogo : "" visible: useDistroLogo && source !== "" smooth: true } - - MultiEffect { - anchors.fill: logo - source: logo - //visible: logo.visible - colorization: 1 - brightness: 1 - saturation: 1 - colorizationColor: root.hovering ? Color.mSurfaceVariant : Color.mOnSurface - } } diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml index fcb8cfe..8f1378e 100644 --- a/Modules/Bar/Widgets/Spacer.qml +++ b/Modules/Bar/Widgets/Spacer.qml @@ -14,13 +14,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 155786b..37cdc42 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -5,7 +5,7 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Rectangle { id: root property ShellScreen screen @@ -13,13 +13,12 @@ RowLayout { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -29,245 +28,419 @@ RowLayout { return {} } - readonly property bool showCpuUsage: (widgetSettings.showCpuUsage - !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage + readonly property string barPosition: Settings.data.bar.position + + readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp - readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage - !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage - readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent - !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent - readonly property bool showNetworkStats: (widgetSettings.showNetworkStats - !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats - readonly property bool showDiskUsage: (widgetSettings.showDiskUsage - !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage - readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : (widgetMetadata.showGpuTemp - || false) + readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage + readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats + readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling + anchors.centerIn: parent + implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) + implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(verticalLayout.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling) + radius: Math.round(Style.radiusM * scaling) + color: Color.mSurfaceVariant - Rectangle { - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 - Layout.alignment: Qt.AlignVCenter + // Compact speed formatter for vertical bar display + function formatCompactSpeed(bytesPerSecond) { + if (!bytesPerSecond || bytesPerSecond <= 0) + return "0" + const units = ["", "k", "M", "G"] + let value = bytesPerSecond + let unitIndex = 0 + while (value >= 1024 && unitIndex < units.length - 1) { + value = value / 1024.0 + unitIndex++ + } + // Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded) + if (unitIndex < units.length - 1 && value >= 100) { + value = value / 1024.0 + unitIndex++ + } + const display = (value >= 10) ? Math.round(value).toString() : value.toFixed(1) + return display + units[unitIndex] + } - radius: Math.round(Style.radiusM * scaling) - color: Color.mSurfaceVariant + // Horizontal layout for top/bottom bars + RowLayout { + id: horizontalLayout + anchors.centerIn: parent + anchors.leftMargin: Style.marginM * scaling + anchors.rightMargin: Style.marginM * scaling + spacing: Style.marginXS * scaling + visible: barPosition === "top" || barPosition === "bottom" - RowLayout { - id: mainLayout - anchors.centerIn: parent // Better centering than margins - width: parent.width - Style.marginM * scaling * 2 - spacing: Style.marginS * scaling + // CPU Usage Component + Item { + Layout.preferredWidth: cpuUsageRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showCpuUsage - // CPU Usage Component - Item { - Layout.preferredWidth: cpuUsageRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showCpuUsage + RowLayout { + id: cpuUsageRow + anchors.centerIn: parent + spacing: Style.marginXXS * scaling - RowLayout { - id: cpuUsageRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + NIcon { + icon: "cpu-usage" + font.pointSize: Style.fontSizeM * scaling + Layout.alignment: Qt.AlignVCenter + } - NIcon { - icon: "cpu-usage" - font.pointSize: Style.fontSizeM * scaling - Layout.alignment: Qt.AlignVCenter - } - - NText { - text: `${SystemStatService.cpuUsage}%` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: `${SystemStatService.cpuUsage}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } - // CPU Temperature Component - Item { - Layout.preferredWidth: cpuTempRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showCpuTemp + // CPU Temperature Component + Item { + Layout.preferredWidth: cpuTempRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showCpuTemp - RowLayout { - id: cpuTempRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + RowLayout { + id: cpuTempRow + anchors.centerIn: parent + spacing: Style.marginXXS * scaling - NIcon { - icon: "cpu-temperature" - // Fire is so tall, we need to make it smaller - font.pointSize: Style.fontSizeS * scaling - Layout.alignment: Qt.AlignVCenter - } + NIcon { + icon: "cpu-temperature" + // Fire is so tall, we need to make it smaller + font.pointSize: Style.fontSizeS * scaling + Layout.alignment: Qt.AlignVCenter + } - NText { - text: `${SystemStatService.cpuTemp}°C` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: `${SystemStatService.cpuTemp}°C` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } - // GPU Temperature Component - Item { - Layout.preferredWidth: gpuTempRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showGpuTemp + // Memory Usage Component + Item { + Layout.preferredWidth: memoryUsageRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showMemoryUsage - RowLayout { - id: gpuTempRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + RowLayout { + id: memoryUsageRow + anchors.centerIn: parent + spacing: Style.marginXXS * scaling - NIcon { - icon: "gpu-temperature" - font.pointSize: Style.fontSizeS * scaling - Layout.alignment: Qt.AlignVCenter - } + NIcon { + icon: "memory" + font.pointSize: Style.fontSizeM * scaling + Layout.alignment: Qt.AlignVCenter + } - NText { - text: `${SystemStatService.gpuTemp}°C` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } - // Memory Usage Component - Item { - Layout.preferredWidth: memoryUsageRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showMemoryUsage + // Network Download Speed Component + Item { + Layout.preferredWidth: networkDownloadRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showNetworkStats - RowLayout { - id: memoryUsageRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + RowLayout { + id: networkDownloadRow + anchors.centerIn: parent + spacing: Style.marginXS * scaling - NIcon { - icon: "memory" - font.pointSize: Style.fontSizeM * scaling - Layout.alignment: Qt.AlignVCenter - } + NIcon { + icon: "download-speed" + font.pointSize: Style.fontSizeM * scaling + Layout.alignment: Qt.AlignVCenter + } - NText { - text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } - // Network Download Speed Component - Item { - Layout.preferredWidth: networkDownloadRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showNetworkStats + // Network Upload Speed Component + Item { + Layout.preferredWidth: networkUploadRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showNetworkStats - RowLayout { - id: networkDownloadRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + RowLayout { + id: networkUploadRow + anchors.centerIn: parent + spacing: Style.marginXS * scaling - NIcon { - icon: "download-speed" - font.pointSize: Style.fontSizeM * scaling - Layout.alignment: Qt.AlignVCenter - } + NIcon { + icon: "upload-speed" + font.pointSize: Style.fontSizeM * scaling + Layout.alignment: Qt.AlignVCenter + } - NText { - text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } - // Network Upload Speed Component - Item { - Layout.preferredWidth: networkUploadRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showNetworkStats + // Disk Usage Component (primary drive) + Item { + Layout.preferredWidth: diskUsageRow.implicitWidth + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.alignment: Qt.AlignVCenter + visible: showDiskUsage - RowLayout { - id: networkUploadRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + RowLayout { + id: diskUsageRow + anchors.centerIn: parent + spacing: Style.marginXS * scaling - NIcon { - icon: "upload-speed" - font.pointSize: Style.fontSizeM * scaling - Layout.alignment: Qt.AlignVCenter - } + NIcon { + icon: "storage" + font.pointSize: Style.fontSizeM * scaling + Layout.alignment: Qt.AlignVCenter + } - NText { - text: SystemStatService.formatSpeed(SystemStatService.txSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: `${SystemStatService.diskPercent}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } + } + } - // Disk Usage Component (primary drive) - Item { - Layout.preferredWidth: diskUsageRow.implicitWidth - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.alignment: Qt.AlignVCenter - visible: showDiskUsage + // Vertical layout for left/right bars + ColumnLayout { + id: verticalLayout + anchors.centerIn: parent + anchors.topMargin: Style.marginS * scaling + anchors.bottomMargin: Style.marginS * scaling + width: Math.round(28 * scaling) + spacing: Style.marginS * scaling + visible: barPosition === "left" || barPosition === "right" - RowLayout { - id: diskUsageRow - anchors.centerIn: parent - spacing: Style.marginXS * scaling + // CPU Usage Component + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showCpuUsage - NIcon { - icon: "storage" - font.pointSize: Style.fontSizeM * scaling - Layout.alignment: Qt.AlignVCenter - } + Column { + id: cpuUsageRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling - NText { - text: `${SystemStatService.diskPercent}%` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - Layout.alignment: Qt.AlignVCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NText { + text: `${Math.round(SystemStatService.cpuUsage)}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + color: Color.mPrimary + } + + NIcon { + icon: "cpu-usage" + font.pointSize: Style.fontSizeS * scaling + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + // CPU Temperature Component + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showCpuTemp + + Column { + id: cpuTempRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + text: `${SystemStatService.cpuTemp}°` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + color: Color.mPrimary + } + + NIcon { + icon: "cpu-temperature" + // Fire is so tall, we need to make it smaller + font.pointSize: Style.fontSizeXS * scaling + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + // Memory Usage Component + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showMemoryUsage + + Column { + id: memoryUsageRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${Math.round(SystemStatService.memGb)}G` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + color: Color.mPrimary + } + + NIcon { + icon: "memory" + font.pointSize: Style.fontSizeS * scaling + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + // Network Download Speed Component + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showNetworkStats + + Column { + id: networkDownloadRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: formatCompactSpeed(SystemStatService.rxSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + color: Color.mPrimary + } + + NIcon { + icon: "download-speed" + font.pointSize: Style.fontSizeS * scaling + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + // Network Upload Speed Component + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showNetworkStats + + Column { + id: networkUploadRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: formatCompactSpeed(SystemStatService.txSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + color: Color.mPrimary + } + + NIcon { + icon: "upload-speed" + font.pointSize: Style.fontSizeS * scaling + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + // Disk Usage Component (primary drive) + Item { + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: Math.round(28 * scaling) + Layout.alignment: Qt.AlignHCenter + visible: showDiskUsage + + ColumnLayout { + id: diskUsageRowVertical + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + text: `${SystemStatService.diskPercent}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + color: Color.mPrimary + } + + NIcon { + icon: "storage" + font.pointSize: Style.fontSizeS * scaling + Layout.alignment: Qt.AlignHCenter } } } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 5c1b090..daa4c80 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -17,36 +17,37 @@ Rectangle { property real scaling: 1.0 readonly property real itemSize: 24 * scaling + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVertical: barPosition === "left" || barPosition === "right" function onLoaded() { - // When the widget is fully initialized with its props - // set the screen for the trayMenu + // When the widget is fully initialized with its props set the screen for the trayMenu if (trayMenu.item) { trayMenu.item.screen = screen } } visible: SystemTray.items.values.length > 0 - implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2 - implicitHeight: Math.round(Style.capsuleHeight * scaling) + implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2) + implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant Layout.alignment: Qt.AlignVCenter - RowLayout { - id: trayLayout + Flow { + id: trayFlow anchors.centerIn: parent spacing: Style.marginS * scaling + flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight Repeater { id: repeater model: SystemTray.items delegate: Item { - Layout.preferredWidth: itemSize - Layout.preferredHeight: itemSize - Layout.alignment: Qt.AlignCenter + width: itemSize + height: itemSize visible: modelData IconImage { @@ -111,9 +112,21 @@ Rectangle { if (modelData.hasMenu && modelData.menu && trayMenu.item) { trayPanel.open() - // Anchor the menu to the tray icon item (parent) and position it below the icon - const menuX = (width / 2) - (trayMenu.item.width / 2) - const menuY = Math.round(Style.barHeight * scaling) + // Position menu based on bar position + let menuX, menuY + if (barPosition === "left") { + // For left bar: position menu to the right of the bar + menuX = width + Style.marginM * scaling + menuY = 0 + } else if (barPosition === "right") { + // For right bar: position menu to the left of the bar + menuX = -trayMenu.item.width - Style.marginM * scaling + menuY = 0 + } else { + // For horizontal bars: center horizontally and position below + menuX = (width / 2) - (trayMenu.item.width / 2) + menuY = Math.round(Style.barHeight * scaling) + } trayMenu.item.menu = modelData.menu trayMenu.item.showAt(parent, menuX, menuY) } else { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index a0d40f2..15e1ddc 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -15,13 +15,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -31,8 +30,8 @@ Item { return {} } - readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false @@ -78,10 +77,11 @@ Item { rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want - text: Math.floor(AudioService.volume * 100) + "%" - forceOpen: alwaysShowPercentage - tooltipText: "Volume: " + Math.round(AudioService.volume * 100) - + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + text: Math.floor(AudioService.volume * 100) + suffix: "%" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" + tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume." onWheel: function (delta) { wheelAccumulator += delta @@ -94,12 +94,12 @@ Item { } } onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Audio - settingsPanel.open(screen) + AudioService.setOutputMuted(!AudioService.muted) } onRightClicked: { - AudioService.setMuted(!AudioService.muted) + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Audio + settingsPanel.open() } onMiddleClicked: { Quickshell.execDetached(["pwvucontrol"]) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 4148f0a..74b3e73 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -41,5 +41,6 @@ NIconButton { } } tooltipText: "Manage Wi-Fi." - onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this) + onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index d8bb543..8d668a9 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -16,13 +16,12 @@ Item { // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" - property string barSection: "" + property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { - var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { var widgets = Settings.data.bar.widgets[section] if (widgets && sectionWidgetIndex < widgets.length) { @@ -32,7 +31,10 @@ Item { return {} } + readonly property string barPosition: Settings.data.bar.position + readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied property bool isDestroying: false property bool hovered: false @@ -47,17 +49,8 @@ Item { signal workspaceChanged(int workspaceId, color accentColor) - implicitHeight: Math.round(Style.barHeight * scaling) - implicitWidth: { - let total = 0 - for (var i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i) - total += calculatedWsWidth(ws) - } - total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills - total += horizontalPadding * 2 - return total - } + implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) + implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth() function calculatedWsWidth(ws) { if (ws.isFocused) @@ -68,6 +61,37 @@ Item { return Math.round(20 * scaling) } + function calculatedWsHeight(ws) { + if (ws.isFocused) + return Math.round(44 * scaling) + else if (ws.isActive) + return Math.round(28 * scaling) + else + return Math.round(20 * scaling) + } + + function calculatedVerticalHeight() { + let total = 0 + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + total += calculatedWsHeight(ws) + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills + total += horizontalPadding * 2 + return total + } + + function calculatedHorizontalWidth() { + let total = 0 + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + total += calculatedWsWidth(ws) + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills + total += horizontalPadding * 2 + return total + } + Component.onCompleted: { refreshWorkspaces() } @@ -77,6 +101,7 @@ Item { } onScreenChanged: refreshWorkspaces() + onHideUnoccupiedChanged: refreshWorkspaces() Connections { target: WorkspaceService @@ -91,11 +116,15 @@ Item { for (var i = 0; i < WorkspaceService.workspaces.count; i++) { const ws = WorkspaceService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) { + continue + } localWorkspaces.append(ws) } } } - workspaceRepeater.model = localWorkspaces + workspaceRepeaterHorizontal.model = localWorkspaces + workspaceRepeaterVertical.model = localWorkspaces updateWorkspaceFocus() } @@ -144,9 +173,8 @@ Item { Rectangle { id: workspaceBackground - width: parent.width - - height: Math.round(Style.capsuleHeight * scaling) + width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width + height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant @@ -154,14 +182,17 @@ Item { anchors.verticalCenter: parent.verticalCenter } + // Horizontal layout for top/bottom bars Row { id: pillRow spacing: spacingBetweenPills anchors.verticalCenter: workspaceBackground.verticalCenter width: root.width - horizontalPadding * 2 x: horizontalPadding + visible: barPosition === "top" || barPosition === "bottom" + Repeater { - id: workspaceRepeater + id: workspaceRepeaterHorizontal model: localWorkspaces Item { id: workspacePillContainer @@ -197,8 +228,6 @@ Item { return Color.mOnError if (model.isActive || model.isOccupied) return Color.mOnSecondary - if (model.isUrgent) - return Color.mOnError return Color.mOnSurface } @@ -214,8 +243,6 @@ Item { return Color.mError if (model.isActive || model.isOccupied) return Color.mSecondary - if (model.isUrgent) - return Color.mError return Color.mOutline } @@ -299,4 +326,149 @@ Item { } } } + + // Vertical layout for left/right bars + Column { + id: pillColumn + spacing: spacingBetweenPills + anchors.horizontalCenter: workspaceBackground.horizontalCenter + height: root.height - horizontalPadding * 2 + y: horizontalPadding + visible: barPosition === "left" || barPosition === "right" + + Repeater { + id: workspaceRepeaterVertical + model: localWorkspaces + Item { + id: workspacePillContainerVertical + width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) + height: root.calculatedWsHeight(model) + + Rectangle { + id: pillVertical + anchors.fill: parent + + Loader { + active: (labelMode !== "none") + sourceComponent: Component { + Text { + x: (pillVertical.width - width) / 2 + y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2 + text: { + if (labelMode === "name" && model.name && model.name.length > 0) { + return model.name.substring(0, 2) + } else { + return model.idx.toString() + } + } + font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling + font.capitalization: Font.AllUppercase + font.family: Settings.data.ui.fontFixed + font.weight: Style.fontWeightBold + wrapMode: Text.Wrap + color: { + if (model.isFocused) + return Color.mOnPrimary + if (model.isUrgent) + return Color.mOnError + if (model.isActive || model.isOccupied) + return Color.mOnSecondary + + return Color.mOnSurface + } + } + } + } + + radius: width * 0.5 + color: { + if (model.isFocused) + return Color.mPrimary + if (model.isUrgent) + return Color.mError + if (model.isActive || model.isOccupied) + return Color.mSecondary + + return Color.mOutline + } + scale: model.isFocused ? 1.0 : 0.9 + z: 0 + + MouseArea { + id: pillMouseAreaVertical + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + WorkspaceService.switchToWorkspace(model.idx) + } + hoverEnabled: true + } + // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + } + + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + // Burst effect overlay for focused pill (smaller outline) + Rectangle { + id: pillBurstVertical + anchors.centerIn: workspacePillContainerVertical + width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale + height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale + radius: width / 2 + color: Color.transparent + border.color: root.effectColor + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling)) + opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + visible: root.effectsActive && model.isFocused + z: 1 + } + } + } + } } diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/BluetoothPanel/BluetoothDevicesList.qml index 05fd4d6..fec1ffd 100644 --- a/Modules/BluetoothPanel/BluetoothDevicesList.qml +++ b/Modules/BluetoothPanel/BluetoothDevicesList.qml @@ -116,8 +116,7 @@ ColumnLayout { NText { visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked - text: (modelData.signalStrength !== undefined - && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" font.pointSize: Style.fontSizeXS * scaling color: getContentColor(Color.mOnSurface) } diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml index ea6a64c..12a5ce0 100644 --- a/Modules/BluetoothPanel/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -11,8 +11,9 @@ import qs.Widgets NPanel { id: root - panelWidth: 380 * scaling - panelHeight: 500 * scaling + preferredWidth: 380 + preferredHeight: 500 + panelKeyboardFocus: true panelContent: Rectangle { color: Color.transparent @@ -42,7 +43,7 @@ NPanel { } NToggle { - id: wifiSwitch + id: bluetoothSwitch checked: Settings.data.network.bluetoothEnabled onToggled: checked => BluetoothService.setBluetoothEnabled(checked) baseSize: Style.baseWidgetSize * 0.65 * scaling @@ -62,7 +63,7 @@ NPanel { NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.8 onClicked: { root.close() @@ -75,7 +76,7 @@ NPanel { } Rectangle { - visible: !Settings.data.network.bluetoothEnabled + visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled) Layout.fillWidth: true Layout.fillHeight: true color: Color.transparent @@ -108,12 +109,12 @@ NPanel { } } - ScrollView { + NScrollView { visible: BluetoothService.adapter && BluetoothService.adapter.enabled Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded clip: true contentWidth: availableWidth @@ -142,8 +143,7 @@ NPanel { property var items: { if (!BluetoothService.adapter || !Bluetooth.devices) return [] - var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected - && (dev.paired || dev.trusted)) + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)) return BluetoothService.sortDevices(filtered) } model: items @@ -175,10 +175,7 @@ NPanel { } var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing - && !dev.blocked - && (dev.signalStrength === undefined - || dev.signalStrength > 0) + return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0) }).length return (availableCount === 0) } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 12c9684..80c6fd3 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -10,9 +10,9 @@ import qs.Widgets NPanel { id: root - panelWidth: 340 * scaling - panelHeight: 320 * scaling - panelAnchorRight: true + preferredWidth: 340 + preferredHeight: 320 + panelAnchorRight: Settings.data.bar.position === "right" // Main Column panelContent: ColumnLayout { diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 44d1845..cec00ca 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -12,10 +12,10 @@ import qs.Widgets Variants { model: Quickshell.screens - delegate: Loader { - + delegate: Item { required property ShellScreen modelData property real scaling: ScalingService.getScreenScale(modelData) + Connections { target: ScalingService function onScaleChanged(screenName, scale) { @@ -25,318 +25,368 @@ Variants { } } - active: Settings.isLoaded && modelData ? Settings.data.dock.monitors.includes(modelData.name) : false + // Shared properties between peek and dock windows + readonly property bool autoHide: Settings.data.dock.autoHide + readonly property int hideDelay: 500 + readonly property int showDelay: 100 + readonly property int hideAnimationDuration: Style.animationFast + readonly property int showAnimationDuration: Style.animationFast + readonly property int peekHeight: 1 // no scaling for peek + readonly property int iconSize: 36 * scaling + readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling - sourceComponent: PanelWindow { - id: dockWindow + // Bar detection and positioning properties + readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" + readonly property int barHeight: Style.barHeight * scaling - screen: modelData + // Shared state between windows + property bool dockHovered: false + property bool anyAppHovered: false + property bool hidden: autoHide + property bool peekHovered: false - WlrLayershell.namespace: "noctalia-dock" + // Separate property to control Loader - stays true during animations + property bool dockLoaded: !autoHide // Start loaded if autoHide is off - readonly property bool autoHide: Settings.data.dock.autoHide - readonly property int hideDelay: 500 - readonly property int showDelay: 100 - readonly property int hideAnimationDuration: Style.animationFast - readonly property int showAnimationDuration: Style.animationFast - readonly property int peekHeight: 7 * scaling - readonly property int fullHeight: dockContainer.height - readonly property int iconSize: 36 * scaling - readonly property int floatingMargin: 12 * scaling // Margin to make dock float + // Timer to unload dock after hide animation completes + Timer { + id: unloadTimer + interval: hideAnimationDuration + 50 // Add small buffer + onTriggered: { + if (hidden && autoHide) { + dockLoaded = false + } + } + } - // Bar detection and positioning properties - readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) : false - readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" - readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top" - readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0 - readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge - - // Track hover state - property bool dockHovered: false - property bool anyAppHovered: false - property bool hidden: autoHide - - // Dock is positioned at the bottom - anchors.bottom: true - - // Dock should be above bar but not create its own exclusion zone - exclusionMode: ExclusionMode.Ignore - focusable: false - - // Make the window transparent - color: Color.transparent - - // Set the window size - include extra height only if bar is at bottom - implicitWidth: dockContainer.width + (floatingMargin * 2) - implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0) - - // Position the entire window above the bar only when bar is at bottom - margins.bottom: barAtBottom ? barHeight : 0 - - // Watch for autoHide setting changes - onAutoHideChanged: { - if (!autoHide) { - // If auto-hide is disabled, show the dock - hidden = false - hideTimer.stop() - showTimer.stop() - } else { - // If auto-hide is enabled, start hidden + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: { + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { hidden = true + unloadTimer.restart() // Start unload timer when hiding } } + } - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: { - if (autoHide && !dockHovered && !anyAppHovered && !peekArea.containsMouse) { - hidden = true - } + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: { + if (autoHide) { + dockLoaded = true // Load dock immediately + hidden = false // Then trigger show animation + unloadTimer.stop() // Cancel any pending unload } } + } - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: { - if (autoHide) { - hidden = false - } - } + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + hidden = false + dockLoaded = true + hideTimer.stop() + showTimer.stop() + unloadTimer.stop() + } else { + hidden = true + unloadTimer.restart() // Schedule unload after animation } + } - // Peek area that remains visible when dock is hidden - MouseArea { - id: peekArea - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0) - hoverEnabled: autoHide - visible: autoHide + // PEEK WINDOW - Always visible when auto-hide is enabled + Loader { + active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide - onEntered: { - if (autoHide && hidden) { - showTimer.start() - } - } + sourceComponent: PanelWindow { + id: peekWindow - onExited: { - if (autoHide && !hidden && !dockHovered && !anyAppHovered) { - hideTimer.restart() - } - } - } + screen: modelData + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: Color.transparent - Rectangle { - id: dockContainer - width: dockLayout.implicitWidth + Style.marginL * scaling * 2 - height: Math.round(iconSize * 1.6) - color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0) - radius: Style.radiusL * scaling - border.width: Math.max(1, Style.borderS * scaling) - border.color: Color.mOutline + WlrLayershell.namespace: "noctalia-dock-peek" + WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive - // Fade and zoom animation properties - opacity: hidden ? 0 : 1 - scale: hidden ? 0.85 : 1 + implicitHeight: peekHeight - Behavior on opacity { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } - - Behavior on scale { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: hidden ? Easing.InQuad : Easing.OutBack - easing.overshoot: hidden ? 0 : 1.05 - } + Rectangle { + anchors.fill: parent + color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent } MouseArea { - id: dockMouseArea + id: peekArea anchors.fill: parent hoverEnabled: true onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - if (hidden) { - hidden = false - } + peekHovered = true + if (hidden) { + showTimer.start() } } onExited: { - dockHovered = false - // Only start hide timer if we're not hovering over any app or the peek area - if (autoHide && !anyAppHovered && !peekArea.containsMouse) { + peekHovered = false + if (!hidden && !dockHovered && !anyAppHovered) { hideTimer.restart() } } } + } + } + // DOCK WINDOW + Loader { + active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0) + + sourceComponent: PanelWindow { + id: dockWindow + + screen: modelData + + focusable: false + color: Color.transparent + + WlrLayershell.namespace: "noctalia-dock-main" + WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore + + // Size to fit the dock container exactly + implicitWidth: dockContainerWrapper.width + implicitHeight: dockContainerWrapper.height + + // Position above the bar if it's at bottom + anchors.bottom: true + + margins.bottom: { + switch (Settings.data.bar.position) { + case "bottom": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin) + default: + return floatingMargin + } + } + + // Rectangle { + // anchors.fill: parent + // color: "#000FF0" + // z: -1 + // } + + // Wrapper item for scale/opacity animations Item { - id: dock - width: dockLayout.implicitWidth - height: parent.height - (Style.marginM * 2 * scaling) - anchors.centerIn: parent + id: dockContainerWrapper + width: dockContainer.width + height: dockContainer.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) - return "" - return AppIcons.iconForAppId(toplevel.appId?.toLowerCase()) + // Apply animations to this wrapper + opacity: hidden ? 0 : 1 + scale: hidden ? 0.85 : 1 + + Behavior on opacity { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } } - RowLayout { - id: dockLayout - spacing: Style.marginL * scaling - Layout.preferredHeight: parent.height + Behavior on scale { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: hidden ? Easing.InQuad : Easing.OutBack + easing.overshoot: hidden ? 0 : 1.05 + } + } + + Rectangle { + id: dockContainer + width: dockLayout.implicitWidth + Style.marginM * scaling * 2 + height: Math.round(iconSize * 1.5) + color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.centerIn: parent + radius: Style.radiusL * scaling + border.width: Math.max(1, Style.borderS * scaling) + border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity) - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true - delegate: Item { - id: appButton - Layout.preferredWidth: iconSize - Layout.preferredHeight: iconSize - Layout.alignment: Qt.AlignCenter - - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" - - // Individual tooltip for this app - NTooltip { - id: appTooltip - target: appButton - positionAbove: true - visible: false + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering } + } - // The icon with better quality settings - Image { - id: appIcon - width: iconSize - height: iconSize - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - sourceSize.width: iconSize * 2 - sourceSize.height: iconSize * 2 - smooth: true - mipmap: true - antialiasing: true - fillMode: Image.PreserveAspectFit - cache: true + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !peekHovered) { + hideTimer.restart() + } + } + } - scale: appButton.hovered ? 1.15 : 1.0 + Item { + id: dock + width: dockLayout.implicitWidth + height: parent.height - (Style.marginM * 2 * scaling) + anchors.centerIn: parent - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutBack - easing.overshoot: 1.2 + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + return AppIcons.iconForAppId(toplevel.appId?.toLowerCase()) + } + + RowLayout { + id: dockLayout + spacing: Style.marginM * scaling + Layout.preferredHeight: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Item { + id: appButton + Layout.preferredWidth: iconSize + Layout.preferredHeight: iconSize + Layout.alignment: Qt.AlignCenter + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + // Individual tooltip for this app + NTooltip { + id: appTooltip + target: appButton + positionAbove: true + visible: false } - } - } - // Fall back if no icon - NIcon { - anchors.centerIn: parent - visible: !appIcon.visible - icon: "question-mark" - font.pointSize: iconSize * 0.7 - color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant - scale: appButton.hovered ? 1.15 : 1.0 + Image { + id: appIcon + width: iconSize + height: iconSize + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + sourceSize.width: iconSize * 2 + sourceSize.height: iconSize * 2 + smooth: true + mipmap: true + antialiasing: true + fillMode: Image.PreserveAspectFit + cache: true - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutBack - easing.overshoot: 1.2 - } - } - } + scale: appButton.hovered ? 1.15 : 1.0 - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.isVisible = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - if (hidden) { - hidden = false + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } } } - } - onExited: { - anyAppHovered = false - appTooltip.hide() - // Only start hide timer if we're not hovering over the dock or peek area - if (autoHide && !dockHovered && !peekArea.containsMouse) { - hideTimer.restart() - } - } + // Fall back if no icon + NIcon { + anchors.centerIn: parent + visible: !appIcon.visible + icon: "question-mark" + font.pointSize: iconSize * 0.7 + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + scale: appButton.hovered ? 1.15 : 1.0 - onClicked: function (mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() - } - } - } - // Active indicator - Rectangle { - visible: isActive - width: iconSize * 0.2 - height: iconSize * 0.1 - color: Color.mPrimary - radius: Style.radiusXS * scaling - anchors.top: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Style.marginXXS * 1.5 * scaling + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton - // Pulse animation for active indicator - SequentialAnimation on opacity { - running: isActive - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering app + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !peekHovered) { + hideTimer.restart() + } + } + + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + } } - NumberAnimation { - to: 1.0 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad + + // Active indicator + Rectangle { + visible: isActive + width: iconSize * 0.2 + height: iconSize * 0.1 + color: Color.mPrimary + radius: Style.radiusXS * scaling + anchors.top: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + // Pulse animation for active indicator + SequentialAnimation on opacity { + running: isActive + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } + } } } } diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 204dbbe..e9bfac5 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -8,17 +8,6 @@ import qs.Services Item { id: root - // Using Wayland protocols to get focused window then determine which screen it's on. - function getActiveScreen() { - const activeWindow = ToplevelManager.activeToplevel - if (activeWindow && activeWindow.screens.length > 0) { - return activeWindow.screens[0] - } - - // Fall back to the primary screen - return Quickshell.screens[0] - } - IpcHandler { target: "screenRecorder" function toggle() { @@ -31,14 +20,14 @@ Item { IpcHandler { target: "settings" function toggle() { - settingsPanel.toggle(getActiveScreen()) + settingsPanel.toggle() } } IpcHandler { target: "notifications" function toggleHistory() { - notificationHistoryPanel.toggle(getActiveScreen()) + notificationHistoryPanel.toggle() } function toggleDND() { Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb @@ -55,15 +44,15 @@ Item { IpcHandler { target: "launcher" function toggle() { - launcherPanel.toggle(getActiveScreen()) + launcherPanel.toggle() } function clipboard() { launcherPanel.setSearchText(">clip ") - launcherPanel.toggle(getActiveScreen()) + launcherPanel.toggle() } function calculator() { launcherPanel.setSearchText(">calc ") - launcherPanel.toggle(getActiveScreen()) + launcherPanel.toggle() } } @@ -110,7 +99,7 @@ Item { AudioService.decreaseVolume() } function muteOutput() { - AudioService.setMuted(!AudioService.muted) + AudioService.setOutputMuted(!AudioService.muted) } function muteInput() { if (AudioService.source?.ready && AudioService.source?.audio) { @@ -122,14 +111,14 @@ Item { IpcHandler { target: "powerPanel" function toggle() { - powerPanel.toggle(getActiveScreen()) + powerPanel.toggle() } } IpcHandler { target: "sidePanel" function toggle() { - sidePanel.toggle(getActiveScreen()) + sidePanel.toggle() } } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index a309913..8789ee2 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -11,16 +11,10 @@ NPanel { id: root // Panel configuration - panelWidth: { - var w = Math.round(Math.max(screen?.width * 0.3, 500) * scaling) - w = Math.min(w, screen?.width - Style.marginL * 2) - return w - } - panelHeight: { - var h = Math.round(Math.max(screen?.height * 0.5, 600) * scaling) - h = Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2) - return h - } + preferredWidth: 500 + preferredWidthRatio: 0.3 + preferredHeight: 600 + preferredHeightRatio: 0.5 panelKeyboardFocus: true panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) @@ -287,9 +281,12 @@ NPanel { } // Results list - ListView { + NListView { id: resultsList + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + Layout.fillWidth: true Layout.fillHeight: true spacing: Style.marginXXS * scaling @@ -306,10 +303,6 @@ NPanel { } } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - delegate: Rectangle { id: entry diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml index 05f086f..e6eced4 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -37,8 +37,7 @@ Item { if (!query || query.trim() === "") { // Return all apps alphabetically - return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map( - app => createResultEntry(app)) + return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app)) } // Use fuzzy search if available, fallback to simple search @@ -57,8 +56,7 @@ Item { const name = (app.name || "").toLowerCase() const comment = (app.comment || "").toLowerCase() const generic = (app.genericName || "").toLowerCase() - return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes( - searchTerm) + return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) }).sort((a, b) => { // Prioritize name matches const aName = a.name.toLowerCase() @@ -85,7 +83,10 @@ Item { if (Settings.data.appLauncher.useApp2Unit && app.id) { Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`) - Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]) + if (app.runInTerminal) + Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]) + else + Quickshell.execDetached(["app2unit", "--"].concat(app.command)) } else if (app.execute) { app.execute() } else if (app.exec) { diff --git a/Modules/Launcher/Plugins/CalculatorPlugin.qml b/Modules/Launcher/Plugins/CalculatorPlugin.qml index 292517f..2029870 100644 --- a/Modules/Launcher/Plugins/CalculatorPlugin.qml +++ b/Modules/Launcher/Plugins/CalculatorPlugin.qml @@ -8,8 +8,7 @@ Item { function handleCommand(query) { // Handle >calc command or direct math expressions after > - return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression( - query.substring(1))) + return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(query.substring(1))) } function commands() { diff --git a/Modules/LockScreen/LockContext.qml b/Modules/LockScreen/LockContext.qml index 985bcd4..b766191 100644 --- a/Modules/LockScreen/LockContext.qml +++ b/Modules/LockScreen/LockContext.qml @@ -12,6 +12,7 @@ Scope { property bool unlockInProgress: false property bool showFailure: false property string errorMessage: "" + property string infoMessage: "" property bool pamAvailable: typeof PamContext !== "undefined" onCurrentTextChanged: { @@ -28,12 +29,6 @@ Scope { return } - if (currentText === "") { - errorMessage = "Password required" - showFailure = true - return - } - root.unlockInProgress = true errorMessage = "" showFailure = false @@ -48,11 +43,12 @@ Scope { user: Quickshell.env("USER") onPamMessage: { - Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", - responseRequired) + Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired) if (messageIsError) { errorMessage = message + } else { + infoMessage = message } if (responseRequired) { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 2c1c956..190b9a1 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -62,8 +62,7 @@ Loader { Item { id: keyboardLayout - property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' - && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" + property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" } Image { @@ -227,12 +226,10 @@ Loader { Repeater { model: CavaService.values.length * 2 Rectangle { - property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length - * 2 - 1 - index) + property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length * 2 - 1 - index) property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI property real mirroredRadius: 70 * scaling - property real mirroredBarLength: Math.max( - 2, CavaService.values[mirroredValueIndex] * 30 * scaling) + property real mirroredBarLength: Math.max(2, CavaService.values[mirroredValueIndex] * 30 * scaling) property real mirroredBarWidth: 3 * scaling width: mirroredBarWidth height: mirroredBarLength @@ -428,8 +425,7 @@ Loader { spacing: Style.marginS * scaling visible: batteryIndicator.batteryVisible NIcon { - icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, - batteryIndicator.isReady) + icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, batteryIndicator.isReady) font.pointSize: Style.fontSizeM * scaling color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface rotation: -90 @@ -515,6 +511,7 @@ Loader { width: 0 height: 0 visible: false + enabled: !lockContext.unlockInProgress font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling color: Color.mOnSurface @@ -544,7 +541,7 @@ Loader { color: Color.mOnSurface font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus + visible: passwordInput.activeFocus && !lockContext.unlockInProgress SequentialAnimation { id: typingEffect @@ -588,7 +585,7 @@ Loader { NText { text: { if (lockContext.unlockInProgress) - return "Authenticating..." + return lockContext.infoMessage || "Authenticating..." if (lockContext.showFailure && lockContext.errorMessage) return lockContext.errorMessage if (lockContext.showFailure) @@ -750,7 +747,7 @@ Loader { id: shutdownTooltipText anchors.margins: Style.marginM * scaling anchors.fill: parent - text: "Shut down the computer." + text: "Shut down." font.pointSize: Style.fontSizeM * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -801,7 +798,7 @@ Loader { id: restartTooltipText anchors.margins: Style.marginM * scaling anchors.fill: parent - text: "Restart the computer." + text: "Restart." font.pointSize: Style.fontSizeM * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -853,7 +850,7 @@ Loader { id: suspendTooltipText anchors.margins: Style.marginM * scaling anchors.fill: parent - text: "Suspend the system." + text: "Suspend." font.pointSize: Style.fontSizeM * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 61809e5..940c6d6 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -25,10 +25,7 @@ Variants { property var removingNotifications: ({}) // If no notification display activated in settings, then show them all - active: Settings.isLoaded && modelData - && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes( - modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false + active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false visible: (NotificationService.notificationModel.count > 0) @@ -36,13 +33,50 @@ Variants { screen: modelData color: Color.transparent - // Position based on bar location - anchors.top: Settings.data.bar.position === "top" - anchors.bottom: Settings.data.bar.position === "bottom" - anchors.right: true - margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0 - margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0 - margins.right: Style.marginM * scaling + // Position based on bar location - always at top + anchors.top: true + anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + anchors.left: Settings.data.bar.position === "left" + + margins.top: { + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return Style.marginM * scaling + } + } + + margins.bottom: { + switch (Settings.data.bar.position) { + case "bottom": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return 0 + } + } + + margins.left: { + switch (Settings.data.bar.position) { + case "left": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0) + default: + return 0 + } + } + + margins.right: { + switch (Settings.data.bar.position) { + case "right": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0) + case "top": + case "bottom": + return Style.marginM * scaling + default: + return 0 + } + } + implicitWidth: 360 * scaling implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling) //WlrLayershell.layer: WlrLayer.Overlay @@ -80,10 +114,10 @@ Variants { // Main notification container ColumnLayout { id: notificationStack - // Position based on bar location - anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined - anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined - anchors.right: parent.right + // Position based on bar location - always at top + anchors.top: parent.top + anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined + anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined spacing: Style.marginS * scaling width: 360 * scaling visible: true @@ -181,8 +215,7 @@ Variants { spacing: Style.marginS * scaling NText { - text: `${(model.appName || model.desktopEntry) - || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` color: Color.mSecondary font.pointSize: Style.fontSizeXS * scaling } @@ -249,8 +282,7 @@ Variants { RowLayout { Layout.fillWidth: true spacing: Style.marginS * scaling - visible: model.rawNotification && model.rawNotification.actions - && model.rawNotification.actions.length > 0 + visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0 property var notificationActions: model.rawNotification ? model.rawNotification.actions : [] @@ -293,7 +325,7 @@ Variants { // Close button positioned absolutely NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.6 anchors.top: parent.top anchors.topMargin: Style.marginM * scaling diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index df84c87..0a9e852 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -12,9 +12,10 @@ import qs.Widgets NPanel { id: root - panelWidth: 380 * scaling - panelHeight: 500 * scaling - panelAnchorRight: true + preferredWidth: 380 + preferredHeight: 500 + panelAnchorRight: Settings.data.bar.position === "right" + panelKeyboardFocus: true panelContent: Rectangle { id: notificationRect @@ -56,12 +57,15 @@ NPanel { icon: "trash" tooltipText: "Clear history" sizeRatio: 0.8 - onClicked: NotificationService.clearHistory() + onClicked: { + NotificationService.clearHistory() + root.close() + } } NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.8 onClicked: { root.close() @@ -115,10 +119,13 @@ NPanel { } // Notification list - ListView { + NListView { id: notificationList Layout.fillWidth: true Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + model: NotificationService.historyModel spacing: Style.marginM * scaling clip: true @@ -129,7 +136,7 @@ NPanel { width: notificationList.width height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant + color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) border.width: Math.max(1, Style.borderS * scaling) @@ -145,13 +152,7 @@ NPanel { Layout.preferredHeight: 28 * scaling Layout.alignment: Qt.AlignVCenter // Prefer stable themed icons over transient image paths - imagePath: (appIcon - && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") - || appIcon) : ((AppIcons.iconForAppId(desktopEntry - || appName, "application-x-executable") - || (image && image - !== "" ? image : AppIcons.iconFromName("application-x-executable", - "application-x-executable")))) + imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable")))) borderColor: Color.transparent borderWidth: 0 visible: true @@ -168,7 +169,7 @@ NPanel { text: (summary || "No summary").substring(0, 100) font.pointSize: Style.fontSizeM * scaling font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary + color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mPrimary wrapMode: Text.Wrap Layout.fillWidth: true maximumLineCount: 2 @@ -178,7 +179,7 @@ NPanel { NText { text: (body || "").substring(0, 150) font.pointSize: Style.fontSizeXS * scaling - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface wrapMode: Text.Wrap Layout.fillWidth: true maximumLineCount: 3 @@ -189,7 +190,7 @@ NPanel { NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeXS * scaling - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface Layout.fillWidth: true } } diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index b9380fb..e81fd5e 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -13,8 +13,8 @@ import qs.Widgets NPanel { id: root - panelWidth: 440 * scaling - panelHeight: 380 * scaling + preferredWidth: 440 + preferredHeight: 410 panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true panelKeyboardFocus: true @@ -224,6 +224,7 @@ NPanel { root.close() } } + context: Qt.WidgetShortcut enabled: root.opened } @@ -262,8 +263,7 @@ NPanel { Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling NText { - text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil( - timeRemaining / 1000)} seconds...` : "Power Options" + text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu" font.weight: Style.fontWeightBold font.pointSize: Style.fontSizeL * scaling color: timerActive ? Color.mPrimary : Color.mOnSurface @@ -292,6 +292,10 @@ NPanel { } } + NDivider { + Layout.fillWidth: true + } + // Power options ColumnLayout { Layout.fillWidth: true @@ -337,7 +341,7 @@ NPanel { return Qt.alpha(Color.mPrimary, 0.08) } if (isSelected || mouseArea.containsMouse) { - return Color.mSecondary + return Color.mTertiary } return Color.transparent } @@ -367,7 +371,7 @@ NPanel { if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + return Color.mOnTertiary return Color.mOnSurface } font.pointSize: Style.fontSizeXXXL * scaling @@ -401,7 +405,7 @@ NPanel { if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + return Color.mOnTertiary return Color.mOnSurface } @@ -426,7 +430,7 @@ NPanel { if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError if (buttonRoot.isSelected || mouseArea.containsMouse) - return Color.mOnSecondary + return Color.mOnTertiary return Color.mOnSurfaceVariant } opacity: Style.opacityHeavy diff --git a/Modules/SettingsPanel/Bar/BarSectionEditor.qml b/Modules/SettingsPanel/Bar/BarSectionEditor.qml index bdadc28..0c1919b 100644 --- a/Modules/SettingsPanel/Bar/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Bar/BarSectionEditor.qml @@ -19,10 +19,8 @@ NBox { signal reorderWidget(string section, int fromIndex, int toIndex) signal updateWidgetSettings(string section, int index, var settings) signal dragPotentialStarted - // Emitted when a widget is pressed (potential drag start) signal dragPotentialEnded - // Emitted when interaction ends (drag or click) color: Color.mSurface Layout.fillWidth: true Layout.minimumHeight: { @@ -43,17 +41,19 @@ NBox { const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => { return acc + character.charCodeAt(0) }, 0) - switch (totalSum % 5) { + switch (totalSum % 6) { case 0: - return Color.mPrimary + return [Color.mPrimary, Color.mOnPrimary] case 1: - return Color.mSecondary + return [Color.mSecondary, Color.mOnSecondary] case 2: - return Color.mTertiary + return [Color.mTertiary, Color.mOnTertiary] case 3: - return Color.mError + return [Color.mError, Color.mOnError] case 4: - return Color.mOnSurface + return [Color.mOnSurface, Color.mSurface] + case 5: + return [Color.mOnSurfaceVariant, Color.mSurfaceVariant] } } @@ -131,7 +131,7 @@ NBox { width: widgetContent.implicitWidth + Style.marginL * scaling height: Style.baseWidgetSize * 1.15 * scaling radius: Style.radiusL * scaling - color: root.getWidgetColor(modelData) + color: root.getWidgetColor(modelData)[0] border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -147,12 +147,12 @@ NBox { Behavior on opacity { NumberAnimation { - duration: 150 + duration: Style.animationFast } } Behavior on scale { NumberAnimation { - duration: 150 + duration: Style.animationFast } } @@ -164,7 +164,7 @@ NBox { NText { text: modelData.id font.pointSize: Style.fontSizeS * scaling - color: Color.mOnPrimary + color: root.getWidgetColor(modelData)[1] horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight Layout.preferredWidth: 80 * scaling @@ -240,7 +240,7 @@ NBox { width: 0 height: Style.baseWidgetSize * 1.15 * scaling radius: Style.radiusL * scaling - color: "transparent" + color: Color.transparent border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) opacity: 0.7 @@ -305,7 +305,7 @@ NBox { acceptedButtons: Qt.LeftButton preventStealing: false - propagateComposedEvents: !dragStarted + propagateComposedEvents: false hoverEnabled: true // Always track mouse for drag operations property point startPos: Qt.point(0, 0) @@ -339,12 +339,10 @@ NBox { continue // Check distance to left edge (insert before) - const leftDist = Math.sqrt(Math.pow(mouseX - widget.x, - 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2)) + const leftDist = Math.sqrt(Math.pow(mouseX - widget.x, 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2)) // Check distance to right edge (insert after) - const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width), - 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2)) + const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width), 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2)) if (leftDist < minDistance) { minDistance = leftDist @@ -355,8 +353,7 @@ NBox { if (rightDist < minDistance) { minDistance = rightDist bestIndex = i + 1 - bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2, - widget.y) + bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2, widget.y) } } @@ -368,8 +365,7 @@ NBox { if (dist < minDistance && mouseX < firstWidget.x + firstWidget.width / 2) { minDistance = dist bestIndex = 0 - bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling), - firstWidget.y) + bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling), firstWidget.y) } } } @@ -419,8 +415,7 @@ NBox { for (var i = 0; i < widgetModel.length; i++) { const widget = widgetFlow.children[i] if (widget && widget.widgetIndex !== undefined) { - if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y - && mouse.y <= widget.y + widget.height) { + if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y && mouse.y <= widget.y + widget.height) { const localX = mouse.x - widget.x const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth) @@ -458,7 +453,7 @@ NBox { // Setup ghost widget if (draggedWidget) { dragGhost.width = draggedWidget.width - dragGhost.color = root.getWidgetColor(draggedModelData) + dragGhost.color = root.getWidgetColor(draggedModelData)[0] ghostText.text = draggedModelData.id } } diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml index 775b148..3acdf94 100644 --- a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml @@ -19,7 +19,7 @@ Popup { x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 - width: 420 * scaling + width: 500 * scaling height: content.implicitHeight + padding * 2 padding: Style.marginXL * scaling modal: true @@ -46,6 +46,7 @@ Popup { "Brightness": "WidgetSettings/BrightnessSettings.qml", "Clock": "WidgetSettings/ClockSettings.qml", "CustomButton": "WidgetSettings/CustomButtonSettings.qml", + "KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml", "MediaMini": "WidgetSettings/MediaMiniSettings.qml", "Microphone": "WidgetSettings/MicrophoneSettings.qml", "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml index 4e66f65..04b8748 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml @@ -14,22 +14,36 @@ ColumnLayout { property var widgetMetadata: null // Local state - property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage - !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage - property int valueWarningThreshold: widgetData.warningThreshold - !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property int valueWarningThreshold: widgetData.warningThreshold !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.alwaysShowPercentage = valueAlwaysShowPercentage + settings.displayMode = valueDisplayMode settings.warningThreshold = valueWarningThreshold return settings } - NToggle { - label: "Always show percentage" - checked: root.valueAlwaysShowPercentage - onToggled: checked => root.valueAlwaysShowPercentage = checked + NComboBox { + label: "Display mode" + description: "Choose how you'd like this value to appear." + minimumWidth: 134 * scaling + model: ListModel { + ListElement { + key: "onhover" + name: "On Hover" + } + ListElement { + key: "alwaysShow" + name: "Always Show" + } + ListElement { + key: "alwaysHide" + name: "Always Hide" + } + } + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key } NSpinBox { diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml index 6054e9c..bb43bd8 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml @@ -14,18 +14,33 @@ ColumnLayout { property var widgetMetadata: null // Local state - property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage - !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.alwaysShowPercentage = valueAlwaysShowPercentage + settings.displayMode = valueDisplayMode return settings } - NToggle { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked + NComboBox { + label: "Display mode" + description: "Choose how you'd like this value to appear." + minimumWidth: 134 * scaling + model: ListModel { + ListElement { + key: "onhover" + name: "On Hover" + } + ListElement { + key: "alwaysShow" + name: "Always Show" + } + ListElement { + key: "alwaysHide" + name: "Always Hide" + } + } + currentKey: valueDisplayMode + onSelected: key => valueDisplayMode = key } } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml index cef94a8..3af8282 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml @@ -14,24 +14,41 @@ ColumnLayout { property var widgetMetadata: null // Local state - property bool valueShowDate: widgetData.showDate !== undefined ? widgetData.showDate : widgetMetadata.showDate + property string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock - property bool valueShowSeconds: widgetData.showSeconds !== undefined ? widgetData.showSeconds : widgetMetadata.showSeconds property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.showDate = valueShowDate + settings.displayFormat = valueDisplayFormat settings.use12HourClock = valueUse12h - settings.showSeconds = valueShowSeconds settings.reverseDayMonth = valueReverseDayMonth return settings } - NToggle { - label: "Show date" - checked: valueShowDate - onToggled: checked => valueShowDate = checked + NComboBox { + label: "Display Format" + model: ListModel { + ListElement { + key: "time" + name: "HH:mm" + } + ListElement { + key: "time-seconds" + name: "HH:mm:ss" + } + ListElement { + key: "time-date" + name: "HH:mm - Date" + } + ListElement { + key: "time-date-short" + name: "HH:mm - Short Date" + } + } + currentKey: valueDisplayFormat + onSelected: key => valueDisplayFormat = key + minimumWidth: 230 * scaling } NToggle { @@ -40,12 +57,6 @@ ColumnLayout { onToggled: checked => valueUse12h = checked } - NToggle { - label: "Show seconds" - checked: valueShowSeconds - onToggled: checked => valueShowSeconds = checked - } - NToggle { label: "Reverse day and month" checked: valueReverseDayMonth diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml index 101c475..371d845 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml @@ -19,6 +19,8 @@ ColumnLayout { settings.leftClickExec = leftClickExecInput.text settings.rightClickExec = rightClickExecInput.text settings.middleClickExec = middleClickExecInput.text + settings.textCommand = textCommandInput.text + settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10) return settings } @@ -48,18 +50,16 @@ ColumnLayout { Popup { id: iconPicker modal: true - property real panelWidth: { + width: { var w = Math.round(Math.max(Screen.width * 0.35, 900) * scaling) w = Math.min(w, Screen.width - Style.marginL * 2) return w } - property real panelHeight: { + height: { var h = Math.round(Math.max(Screen.height * 0.65, 700) * scaling) h = Math.min(h, Screen.height - Style.barHeight * scaling - Style.marginL * 2) return h } - width: panelWidth - height: panelHeight anchors.centerIn: Overlay.overlay padding: Style.marginXL * scaling @@ -117,10 +117,12 @@ ColumnLayout { } // Icon grid - ScrollView { + NScrollView { Layout.fillWidth: true Layout.fillHeight: true clip: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOn GridView { id: grid @@ -228,4 +230,33 @@ ColumnLayout { placeholderText: "Enter command to execute (app or custom script)" text: widgetData.middleClickExec || widgetMetadata.middleClickExec } + + NDivider { + Layout.fillWidth: true + } + + NText { + text: "Dynamic Text" + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NTextInput { + id: textCommandInput + Layout.fillWidth: true + label: "Text Command" + description: "Shell command to run periodically (first line becomes the text)." + placeholderText: "echo \"Hello World\"" + text: widgetData?.textCommand || widgetMetadata.textCommand + } + + NTextInput { + id: textIntervalInput + Layout.fillWidth: true + label: "Refresh Interval" + description: "Interval in milliseconds." + placeholderText: String(widgetMetadata.textIntervalMs || 3000) + text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : "" + } } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/KeyboardLayoutSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/KeyboardLayoutSettings.qml new file mode 100644 index 0000000..6a0c832 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/KeyboardLayoutSettings.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.displayMode = valueDisplayMode + return settings + } + + NComboBox { + label: "Display mode" + description: "Choose how you'd like this value to appear." + minimumWidth: 134 * scaling + model: ListModel { + ListElement { + key: "onhover" + name: "On Hover" + } + ListElement { + key: "forceOpen" + name: "Force Open" + } + ListElement { + key: "alwaysHide" + name: "Always Hide" + } + } + currentKey: valueDisplayMode + onSelected: key => valueDisplayMode = key + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml index 6054e9c..bb43bd8 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml @@ -14,18 +14,33 @@ ColumnLayout { property var widgetMetadata: null // Local state - property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage - !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.alwaysShowPercentage = valueAlwaysShowPercentage + settings.displayMode = valueDisplayMode return settings } - NToggle { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked + NComboBox { + label: "Display mode" + description: "Choose how you'd like this value to appear." + minimumWidth: 134 * scaling + model: ListModel { + ListElement { + key: "onhover" + name: "On Hover" + } + ListElement { + key: "alwaysShow" + name: "Always Show" + } + ListElement { + key: "alwaysHide" + name: "Always Hide" + } + } + currentKey: valueDisplayMode + onSelected: key => valueDisplayMode = key } } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml index 0c9b9bb..d2cee14 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -16,20 +16,15 @@ ColumnLayout { // Local, editable state for checkboxes property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp - property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : (widgetMetadata.showGpuTemp - || false) property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage - property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent - !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent - property bool valueShowNetworkStats: widgetData.showNetworkStats - !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats + property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage function saveSettings() { var settings = Object.assign({}, widgetData || {}) settings.showCpuUsage = valueShowCpuUsage settings.showCpuTemp = valueShowCpuTemp - settings.showGpuTemp = valueShowGpuTemp settings.showMemoryUsage = valueShowMemoryUsage settings.showMemoryAsPercent = valueShowMemoryAsPercent settings.showNetworkStats = valueShowNetworkStats @@ -53,14 +48,6 @@ ColumnLayout { onToggled: checked => valueShowCpuTemp = checked } - NToggle { - id: showGpuTemp - Layout.fillWidth: true - label: "GPU temperature" - checked: valueShowGpuTemp - onToggled: checked => valueShowGpuTemp = checked - } - NToggle { id: showMemoryUsage Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml index 6054e9c..bb43bd8 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml @@ -14,18 +14,33 @@ ColumnLayout { property var widgetMetadata: null // Local state - property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage - !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.alwaysShowPercentage = valueAlwaysShowPercentage + settings.displayMode = valueDisplayMode return settings } - NToggle { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked + NComboBox { + label: "Display mode" + description: "Choose how you'd like this value to appear." + minimumWidth: 134 * scaling + model: ListModel { + ListElement { + key: "onhover" + name: "On Hover" + } + ListElement { + key: "alwaysShow" + name: "Always Show" + } + ListElement { + key: "alwaysHide" + name: "Always Hide" + } + } + currentKey: valueDisplayMode + onSelected: key => valueDisplayMode = key } } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml index 1e44dae..a72c91c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml @@ -16,6 +16,7 @@ ColumnLayout { function saveSettings() { var settings = Object.assign({}, widgetData || {}) settings.labelMode = labelModeCombo.currentKey + settings.hideUnoccupied = hideUnoccupiedToggle.checked return settings } @@ -41,4 +42,12 @@ ColumnLayout { onSelected: key => labelModeCombo.currentKey = key minimumWidth: 200 * scaling } + + NToggle { + id: hideUnoccupiedToggle + label: "Hide unoccupied" + description: "Don't display workspaces without windows." + checked: widgetData.hideUnoccupied + onToggled: checked => hideUnoccupiedToggle.checked = checked + } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 2938dbc..37d54a5 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -11,16 +11,11 @@ import qs.Widgets NPanel { id: root - panelWidth: { - var w = Math.round(Math.max(screen?.width * 0.4, 1000) * scaling) - w = Math.min(w, screen?.width - Style.marginL * 2) - return w - } - panelHeight: { - var h = Math.round(Math.max(screen?.height * 0.75, 800) * scaling) - h = Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2) - return h - } + preferredWidth: 1000 + preferredHeight: 1000 + preferredWidthRatio: 0.4 + preferredHeightRatio: 0.75 + panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true @@ -31,13 +26,14 @@ NPanel { About, Audio, Bar, + Dock, Hooks, Launcher, - Brightness, ColorScheme, Display, General, Network, + Notification, ScreenRecorder, Weather, Wallpaper, @@ -72,15 +68,10 @@ NPanel { id: barTab Tabs.BarTab {} } - Component { id: audioTab Tabs.AudioTab {} } - Component { - id: brightnessTab - Tabs.BrightnessTab {} - } Component { id: displayTab Tabs.DisplayTab {} @@ -117,6 +108,14 @@ NPanel { id: hooksTab Tabs.HooksTab {} } + Component { + id: dockTab + Tabs.DockTab {} + } + Component { + id: notificationTab + Tabs.NotificationTab {} + } // Order *DOES* matter function updateTabsModel() { @@ -130,6 +129,11 @@ NPanel { "label": "Bar", "icon": "settings-bar", "source": barTab + }, { + "id": SettingsPanel.Tab.Dock, + "label": "Dock", + "icon": "settings-dock", + "source": dockTab }, { "id": SettingsPanel.Tab.Launcher, "label": "Launcher", @@ -145,16 +149,16 @@ NPanel { "label": "Display", "icon": "settings-display", "source": displayTab + }, { + "id": SettingsPanel.Tab.Notification, + "label": "Notification", + "icon": "settings-notification", + "source": notificationTab }, { "id": SettingsPanel.Tab.Network, "label": "Network", "icon": "settings-network", "source": networkTab - }, { - "id": SettingsPanel.Tab.Brightness, - "label": "Brightness", - "icon": "settings-brightness", - "source": brightnessTab }, { "id": SettingsPanel.Tab.Weather, "label": "Weather", @@ -223,8 +227,7 @@ NPanel { if (activeScrollView && activeScrollView.ScrollBar.vertical) { const scrollBar = activeScrollView.ScrollBar.vertical const stepSize = activeScrollView.height * 0.1 // Scroll 10% of viewport - scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, - 1.0 - scrollBar.size) + scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size) } } @@ -240,8 +243,7 @@ NPanel { if (activeScrollView && activeScrollView.ScrollBar.vertical) { const scrollBar = activeScrollView.ScrollBar.vertical const pageSize = activeScrollView.height * 0.9 // Scroll 90% of viewport - scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, - 1.0 - scrollBar.size) + scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size) } } @@ -466,7 +468,7 @@ NPanel { NIcon { icon: root.tabsModel[currentTabIndex]?.icon color: Color.mPrimary - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: Style.fontSizeXXL * scaling } // Main title @@ -482,7 +484,7 @@ NPanel { // Close button NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." Layout.alignment: Qt.AlignVCenter onClicked: root.close() } @@ -522,11 +524,11 @@ NPanel { anchors.fill: parent pressDelay: 200 - ScrollView { + NScrollView { id: scrollView anchors.fill: parent - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded padding: Style.marginL * scaling clip: true diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index 24890a1..b169e67 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -10,107 +10,108 @@ import qs.Widgets ColumnLayout { id: root + spacing: Style.marginL * scaling property string latestVersion: GitHubService.latestVersion property string currentVersion: UpdateService.currentVersion property var contributors: GitHubService.contributors - NText { - text: "Noctalia Shell" - font.pointSize: Style.fontSizeXXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Noctalia Shell" + description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell." } - // Versions - GridLayout { - Layout.alignment: Qt.AlignCenter - columns: 2 - rowSpacing: Style.marginXS * scaling - columnSpacing: Style.marginS * scaling + RowLayout { + spacing: Style.marginL * scaling - NText { - text: "Latest Version:" - color: Color.mOnSurface - Layout.alignment: Qt.AlignRight - } + // Versions + GridLayout { + columns: 2 + rowSpacing: Style.marginXS * scaling + columnSpacing: Style.marginS * scaling - NText { - text: root.latestVersion - color: Color.mOnSurface - font.weight: Style.fontWeightBold - } - - NText { - text: "Installed Version:" - color: Color.mOnSurface - Layout.alignment: Qt.AlignRight - } - - NText { - text: root.currentVersion - color: Color.mOnSurface - font.weight: Style.fontWeightBold - } - } - - // Updater - Rectangle { - Layout.alignment: Qt.AlignCenter - Layout.topMargin: Style.marginS * scaling - Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) - Layout.preferredHeight: Math.round(Style.barHeight * scaling) - radius: Style.radiusL * scaling - color: updateArea.containsMouse ? Color.mPrimary : Color.transparent - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS * scaling) - visible: { - if (root.latestVersion === "Unknown") - return false - - const latest = root.latestVersion.replace("v", "").split(".") - const current = root.currentVersion.replace("v", "").split(".") - for (var i = 0; i < Math.max(latest.length, current.length); i++) { - const l = parseInt(latest[i] || "0") - const c = parseInt(current[i] || "0") - if (l > c) - return true - - if (l < c) - return false - } - return false - } - - RowLayout { - id: updateRow - anchors.centerIn: parent - spacing: Style.marginS * scaling - - NIcon { - icon: "download" - font.pointSize: Style.fontSizeXXL * scaling - color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + NText { + text: "Latest Version:" + color: Color.mOnSurface } NText { - id: updateText - text: "Download latest release" - font.pointSize: Style.fontSizeL * scaling - color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + text: root.latestVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + + NText { + text: "Installed Version:" + color: Color.mOnSurface + } + + NText { + text: root.currentVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold } } - MouseArea { - id: updateArea + Item { + Layout.fillWidth: true + } - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + // Update button + Rectangle { + Layout.alignment: Qt.AlignRight + Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) + Layout.preferredHeight: Math.round(Style.barHeight * scaling) + radius: Style.radiusL * scaling + color: updateArea.containsMouse ? Color.mPrimary : Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS * scaling) + visible: { + if (root.latestVersion === "Unknown") + return false + + const latest = root.latestVersion.replace("v", "").split(".") + const current = root.currentVersion.replace("v", "").split(".") + for (var i = 0; i < Math.max(latest.length, current.length); i++) { + const l = parseInt(latest[i] || "0") + const c = parseInt(current[i] || "0") + if (l > c) + return true + + if (l < c) + return false + } + return false + } + + RowLayout { + id: updateRow + anchors.centerIn: parent + spacing: Style.marginS * scaling + + NIcon { + icon: "download" + font.pointSize: Style.fontSizeXXL * scaling + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + } + + NText { + id: updateText + text: "Download latest release" + font.pointSize: Style.fontSizeL * scaling + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary + } + } + + MouseArea { + id: updateArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + } } } } @@ -121,17 +122,13 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL * scaling } - NText { - text: `Shout-out to our ${root.contributors.length} awesome contributors!` - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignCenter + NHeader { + label: "Contributors" + description: `Shout-out to our ${root.contributors.length} awesome contributors!` } GridView { id: contributorsGrid - - Layout.topMargin: Style.marginL * scaling Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns Layout.preferredHeight: { @@ -192,7 +189,7 @@ ColumnLayout { NText { text: modelData.login || "Unknown" font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface + color: contributorArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -200,7 +197,7 @@ ColumnLayout { NText { text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeXS * scaling - color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface + color: contributorArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface } } } diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index d60ae38..b0d42ac 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -2,12 +2,18 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.Pipewire -import qs.Widgets import qs.Commons import qs.Services +import qs.Widgets ColumnLayout { id: root + spacing: Style.marginL * scaling + + NHeader { + label: "Volumes" + description: "Configure volume controls and audio levels." + } property real localVolume: AudioService.volume @@ -20,7 +26,7 @@ ColumnLayout { // Master Volume ColumnLayout { - spacing: Style.marginS * scaling + spacing: Style.marginXXS * scaling Layout.fillWidth: true NLabel { @@ -28,37 +34,29 @@ ColumnLayout { description: "System-wide volume level." } - RowLayout { - // Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily - // Probably because they have some quick fades in and out to avoid clipping - // We use a timer to space out the updates, to avoid lock up - Timer { - interval: Style.animationFast - running: true - repeat: true - onTriggered: { - if (Math.abs(localVolume - AudioService.volume) >= 0.01) { - AudioService.setVolume(localVolume) - } + // Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily + // Probably because they have some quick fades in and out to avoid clipping + // We use a timer to space out the updates, to avoid lock up + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (Math.abs(localVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(localVolume) } } + } - NSlider { - Layout.fillWidth: true - from: 0 - to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0 - value: localVolume - stepSize: 0.01 - onMoved: { - localVolume = value - } - } - - NText { - text: Math.floor(AudioService.volume * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface + NValueSlider { + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0 + value: localVolume + stepSize: 0.01 + text: Math.floor(AudioService.volume * 100) + "%" + onMoved: { + localVolume = value } } } @@ -67,7 +65,6 @@ ColumnLayout { ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling NToggle { label: "Mute Audio Output" @@ -83,33 +80,22 @@ ColumnLayout { // Input Volume ColumnLayout { - spacing: Style.marginS * scaling + spacing: Style.marginXS * scaling Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling NLabel { label: "Input Volume" description: "Microphone input volume level." } - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 1.0 - value: AudioService.inputVolume - stepSize: 0.01 - onMoved: { - AudioService.setInputVolume(value) - } - } - - NText { - text: Math.floor(AudioService.inputVolume * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1.0 + value: AudioService.inputVolume + stepSize: 0.01 + text: Math.floor(AudioService.inputVolume * 100) + "%" + onMoved: value => AudioService.setInputVolume(value) } } @@ -117,7 +103,6 @@ ColumnLayout { ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling NToggle { label: "Mute Audio Input" @@ -131,7 +116,6 @@ ColumnLayout { ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling NSpinBox { Layout.fillWidth: true @@ -142,9 +126,7 @@ ColumnLayout { value: Settings.data.audio.volumeStep stepSize: 1 suffix: "%" - onValueChanged: { - Settings.data.audio.volumeStep = value - } + onValueChanged: Settings.data.audio.volumeStep = value } } @@ -158,12 +140,9 @@ ColumnLayout { ColumnLayout { spacing: Style.marginS * scaling - NText { - text: "Audio Devices" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Audio Devices" + description: "Configure audio input and output devices." } // ------------------------------- @@ -203,7 +182,6 @@ ColumnLayout { ColumnLayout { spacing: Style.marginXS * scaling Layout.fillWidth: true - Layout.bottomMargin: Style.marginL * scaling NLabel { label: "Input Device" @@ -234,12 +212,9 @@ ColumnLayout { ColumnLayout { spacing: Style.marginL * scaling - NText { - text: "Media Player" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Media Player" + description: "Configure your favorite media players." } // Preferred player @@ -360,12 +335,9 @@ ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - NText { - text: "Audio Visualizer" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Audio Visualizer" + description: "Customize visual effects that respond to audio playback." } // AudioService Visualizer section diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 864bc0a..1b4353c 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import qs.Commons import qs.Services import qs.Widgets @@ -8,6 +9,20 @@ import qs.Modules.SettingsPanel.Bar ColumnLayout { id: root + spacing: Style.marginL * scaling + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice() + if (!arr.includes(name)) + arr.push(name) + return arr + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name + }) + } // Handler for drag start - disables panel background clicks function handleDragStart() { @@ -25,65 +40,153 @@ ColumnLayout { } } - ColumnLayout { - spacing: Style.marginL * scaling + NHeader { + label: "Appearance" + description: "Configure bar appearance and positioning." + } - RowLayout { - NComboBox { - Layout.fillWidth: true - label: "Bar Position" - description: "Choose where to place the bar on the screen." - model: ListModel { - ListElement { - key: "top" - name: "Top" - } - ListElement { - key: "bottom" - name: "Bottom" - } + RowLayout { + NComboBox { + Layout.fillWidth: true + label: "Bar Position" + description: "Choose where to place the bar on the screen." + model: ListModel { + ListElement { + key: "top" + name: "Top" + } + ListElement { + key: "bottom" + name: "Bottom" + } + ListElement { + key: "left" + name: "Left" + } + ListElement { + key: "right" + name: "Right" } - currentKey: Settings.data.bar.position - onSelected: key => Settings.data.bar.position = key } + currentKey: Settings.data.bar.position + onSelected: key => Settings.data.bar.position = key + } + } + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NLabel { + label: "Background Opacity" + description: "Adjust the background opacity of the bar." } - ColumnLayout { - spacing: Style.marginXXS * scaling + NValueSlider { Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.bar.backgroundOpacity + onMoved: value => Settings.data.bar.backgroundOpacity = value + text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%" + } + } + NToggle { + Layout.fillWidth: true + label: "Floating Bar" + description: "Make the bar float with rounded corners and margins. Screen corners will move to screen edges." + checked: Settings.data.bar.floating + onToggled: checked => Settings.data.bar.floating = checked + } - NText { - text: "Background Opacity" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } + // Floating bar options - only show when floating is enabled + ColumnLayout { + visible: Settings.data.bar.floating + spacing: Style.marginS * scaling + Layout.fillWidth: true - NText { - text: "Adjust the background opacity of the bar." - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } + NLabel { + label: "Margins" + description: "Adjust the margins around the floating bar." + } - RowLayout { - NSlider { + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL * scaling + + ColumnLayout { + spacing: Style.marginXXS * scaling + + NText { + text: "Vertical" + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + } + + NValueSlider { Layout.fillWidth: true from: 0 to: 1 stepSize: 0.01 - value: Settings.data.bar.backgroundOpacity - onMoved: Settings.data.bar.backgroundOpacity = value - cutoutColor: Color.mSurface + value: Settings.data.bar.marginVertical + onMoved: value => Settings.data.bar.marginVertical = value + text: Math.round(Settings.data.bar.marginVertical * 100) + "%" } + } + + ColumnLayout { + spacing: Style.marginXXS * scaling NText { - text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface + text: "Horizontal" + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant } + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.bar.marginHorizontal + onMoved: value => Settings.data.bar.marginHorizontal = value + text: Math.round(Settings.data.bar.marginHorizontal * 100) + "%" + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Monitor Configuration + ColumnLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NHeader { + label: "Monitors Configuration" + description: "Choose which monitors should display the bar." + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}` + description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})` + checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } } } } @@ -99,20 +202,9 @@ ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true - NText { - text: "Widgets Positioning" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling - } - - NText { - text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true + NHeader { + label: "Widgets Positioning" + description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets." } // Bar Sections @@ -201,8 +293,7 @@ ColumnLayout { } function _reorderWidgetInSection(section, fromIndex, toIndex) { - if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 - && toIndex < Settings.data.bar.widgets[section].length) { + if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.bar.widgets[section].length) { // Create a new array to avoid modifying the original var newArray = Settings.data.bar.widgets[section].slice() diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml deleted file mode 100644 index a8f0135..0000000 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ /dev/null @@ -1,341 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Io -import qs.Commons -import qs.Services -import qs.Widgets - -ColumnLayout { - id: root - - // Time dropdown options (00:00 .. 23:30) - ListModel { - id: timeOptions - } - Component.onCompleted: { - for (var h = 0; h < 24; h++) { - for (var m = 0; m < 60; m += 30) { - var hh = ("0" + h).slice(-2) - var mm = ("0" + m).slice(-2) - var key = hh + ":" + mm - timeOptions.append({ - "key": key, - "name": key - }) - } - } - } - - // Check for wlsunset availability when enabling Night Light - Process { - id: wlsunsetCheck - command: ["which", "wlsunset"] - running: false - - onExited: function (exitCode) { - if (exitCode === 0) { - Settings.data.nightLight.enabled = true - NightLightService.apply() - ToastService.showNotice("Night Light", "Enabled") - } else { - Settings.data.nightLight.enabled = false - ToastService.showWarning("Night Light", "wlsunset not installed") - } - } - - stdout: StdioCollector {} - stderr: StdioCollector {} - } - - spacing: Style.marginL * scaling - - // Brightness Step Section - ColumnLayout { - spacing: Style.marginS * scaling - Layout.fillWidth: true - - NSpinBox { - Layout.fillWidth: true - label: "Brightness Step Size" - description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)." - minimum: 1 - maximum: 50 - value: Settings.data.brightness.brightnessStep - stepSize: 1 - suffix: "%" - onValueChanged: { - Settings.data.brightness.brightnessStep = value - } - } - } - - // Monitor Overview Section - ColumnLayout { - spacing: Style.marginL * scaling - - NLabel { - label: "Monitors Brightness Control" - description: "Current brightness levels for all detected monitors." - } - - // Single monitor display using the same data source as the bar icon - Repeater { - model: BrightnessService.monitors - Rectangle { - Layout.fillWidth: true - radius: Style.radiusM * scaling - color: Color.mSurface - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling - - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: `${model.modelData.name} [${model.modelData.model}]` - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - Item { - Layout.fillWidth: true - } - - NText { - text: model.method - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignRight - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: "Brightness:" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - } - - NSlider { - Layout.fillWidth: true - from: 0 - to: 1 - value: model.brightness - stepSize: 0.05 - onPressedChanged: { - if (!pressed) { - var monitor = BrightnessService.getMonitorForScreen(model.modelData) - monitor.setBrightness(value) - } - } - } - - NText { - text: Math.round(model.brightness * 100) + "%" - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.alignment: Qt.AlignRight - } - } - } - } - } - } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling - } - - // Night Light Section - ColumnLayout { - spacing: Style.marginXS * scaling - Layout.fillWidth: true - - NText { - text: "Night Light" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NText { - text: "Reduce blue light emission to help you sleep better and reduce eye strain." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - - NToggle { - label: "Enable Night Light" - description: "Apply a warm color filter to reduce blue light emission." - checked: Settings.data.nightLight.enabled - onToggled: checked => { - if (checked) { - // Verify wlsunset exists before enabling - wlsunsetCheck.running = true - } else { - Settings.data.nightLight.enabled = false - Settings.data.nightLight.forced = false - NightLightService.apply() - ToastService.showNotice("Night Light", "Disabled") - } - } - } - - // Temperature - ColumnLayout { - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignVCenter - - NLabel { - label: "Color temperature" - description: "Choose two temperatures in Kelvin." - } - - RowLayout { - visible: Settings.data.nightLight.enabled - spacing: Style.marginM * scaling - Layout.fillWidth: false - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter - - NText { - text: "Night" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignVCenter - } - - NTextInput { - text: Settings.data.nightLight.nightTemp - inputMethodHints: Qt.ImhDigitsOnly - Layout.alignment: Qt.AlignVCenter - onEditingFinished: { - var nightTemp = parseInt(text) - var dayTemp = parseInt(Settings.data.nightLight.dayTemp) - if (!isNaN(nightTemp) && !isNaN(dayTemp)) { - // Clamp value between [1000 .. (dayTemp-500)] - var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp)) - text = Settings.data.nightLight.nightTemp = clampedValue.toString() - } - } - } - - Item {} - - NText { - text: "Day" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignVCenter - } - NTextInput { - text: Settings.data.nightLight.dayTemp - inputMethodHints: Qt.ImhDigitsOnly - Layout.alignment: Qt.AlignVCenter - onEditingFinished: { - var dayTemp = parseInt(text) - var nightTemp = parseInt(Settings.data.nightLight.nightTemp) - if (!isNaN(nightTemp) && !isNaN(dayTemp)) { - // Clamp value between [(nightTemp+500) .. 6500] - var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp)) - text = Settings.data.nightLight.dayTemp = clampedValue.toString() - } - } - } - } - } - - NToggle { - label: "Automatic Scheduling" - description: `Based on the sunset and sunrise time in ${LocationService.stableName} - recommended.` - checked: Settings.data.nightLight.autoSchedule - onToggled: checked => Settings.data.nightLight.autoSchedule = checked - visible: Settings.data.nightLight.enabled - } - - // Schedule settings - ColumnLayout { - spacing: Style.marginXS * scaling - visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule - && !Settings.data.nightLight.forced - - RowLayout { - Layout.fillWidth: false - spacing: Style.marginM * scaling - - NLabel { - label: "Manual Scheduling" - } - - Item {// add a little more spacing - } - - NText { - text: "Sunrise Time" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - } - - NComboBox { - model: timeOptions - currentKey: Settings.data.nightLight.manualSunrise - placeholder: "Select start time" - onSelected: key => Settings.data.nightLight.manualSunrise = key - minimumWidth: 120 * scaling - } - - Item {// add a little more spacing - } - - NText { - text: "Sunset Time" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - } - NComboBox { - model: timeOptions - currentKey: Settings.data.nightLight.manualSunset - placeholder: "Select stop time" - onSelected: key => Settings.data.nightLight.manualSunset = key - minimumWidth: 120 * scaling - } - } - } - - // Force activation toggle - NToggle { - label: "Force activation" - description: "Immediately apply night temperature without scheduling or fade." - checked: Settings.data.nightLight.forced - onToggled: checked => { - Settings.data.nightLight.forced = checked - if (checked && !Settings.data.nightLight.enabled) { - // Ensure enabled when forcing - wlsunsetCheck.running = true - } else { - NightLightService.apply() - } - } - visible: Settings.data.nightLight.enabled - } -} diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 5708d01..d752639 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -8,7 +8,7 @@ import qs.Widgets ColumnLayout { id: root - spacing: 0 + spacing: Style.marginL * scaling // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) @@ -105,39 +105,39 @@ ColumnLayout { } // Main Toggles - Dark Mode / Matugen - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true + NHeader { + label: "Behavior" + description: "Main settings for Noctalia's colors." + } - // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) - NToggle { - label: "Dark Mode" - description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available." - checked: Settings.data.colorSchemes.darkMode - enabled: true - onToggled: checked => Settings.data.colorSchemes.darkMode = checked - } + // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) + NToggle { + label: "Dark Mode" + description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available." + checked: Settings.data.colorSchemes.darkMode + enabled: true + onToggled: checked => Settings.data.colorSchemes.darkMode = checked + } - // Use Matugen - NToggle { - label: "Enable Matugen" - description: "Automatically generate colors based on your active wallpaper." - checked: Settings.data.colorSchemes.useWallpaperColors - onToggled: checked => { - if (checked) { - // Check if matugen is installed - matugenCheck.running = true - } else { - Settings.data.colorSchemes.useWallpaperColors = false - ToastService.showNotice("Matugen", "Disabled") + // Use Matugen + NToggle { + label: "Enable Matugen" + description: "Automatically generate colors based on your active wallpaper." + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + if (checked) { + // Check if matugen is installed + matugenCheck.running = true + } else { + Settings.data.colorSchemes.useWallpaperColors = false + ToastService.showNotice("Matugen", "Disabled") - if (Settings.data.colorSchemes.predefinedScheme) { + if (Settings.data.colorSchemes.predefinedScheme) { - ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) - } + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) } } - } + } } NDivider { @@ -151,19 +151,9 @@ ColumnLayout { spacing: Style.marginM * scaling Layout.fillWidth: true - NText { - text: "Predefined Color Schemes" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NText { - text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - wrapMode: Text.WordWrap + NHeader { + label: "Predefined Color Schemes" + description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper." } // Color Schemes Grid @@ -186,9 +176,7 @@ ColumnLayout { radius: Style.radiusM * scaling color: getSchemeColor(modelData, "mSurface") border.width: Math.max(1, Style.borderL * scaling) - border.color: (!Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === modelData.split("/").pop().replace( - ".json", ""))) ? Color.mPrimary : Color.mOutline + border.color: (!Settings.data.colorSchemes.useWallpaperColors && (Settings.data.colorSchemes.predefinedScheme === modelData.split("/").pop().replace(".json", ""))) ? Color.mSecondary : Color.mOutline scale: root.cardScaleLow // Mouse area for selection @@ -281,23 +269,21 @@ ColumnLayout { // Selection indicator (Checkmark) Rectangle { - visible: !Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === schemePath.split("/").pop().replace(".json", - "")) + visible: !Settings.data.colorSchemes.useWallpaperColors && (Settings.data.colorSchemes.predefinedScheme === schemePath.split("/").pop().replace(".json", "")) anchors.right: parent.right anchors.top: parent.top anchors.margins: Style.marginS * scaling - width: 24 * scaling - height: 24 * scaling + width: 28 * scaling + height: 28 * scaling radius: width * 0.5 - color: Color.mPrimary + color: Color.mSecondary - NText { - anchors.centerIn: parent - text: "✓" - font.pointSize: Style.fontSizeXS * scaling + NIcon { + icon: "check" + font.pointSize: Style.fontSizeM * scaling font.weight: Style.fontWeightBold - color: Color.mOnPrimary + color: Color.mOnSecondary + anchors.centerIn: parent } } @@ -332,146 +318,187 @@ ColumnLayout { visible: Settings.data.colorSchemes.useWallpaperColors } - // Matugen template toggles (moved from MatugenTab) + // Matugen template toggles organized by category ColumnLayout { - spacing: Style.marginL * scaling Layout.fillWidth: true visible: Settings.data.colorSchemes.useWallpaperColors + spacing: Style.marginL * scaling - ColumnLayout { - spacing: Style.marginS * scaling + // UI Components + NCollapsible { Layout.fillWidth: true + label: "UI" + description: "Desktop environment and UI toolkit theming." + defaultExpanded: false - NText { - text: "Matugen Templates" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NCheckbox { + label: "GTK 4 (libadwaita)" + description: "Write ~/.config/gtk-4.0/gtk.css" + checked: Settings.data.matugen.gtk4 + onToggled: checked => { + Settings.data.matugen.gtk4 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } } - NText { - text: "Select which external components Matugen should apply theming to." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - wrapMode: Text.WordWrap + NCheckbox { + label: "GTK 3" + description: "Write ~/.config/gtk-3.0/gtk.css" + checked: Settings.data.matugen.gtk3 + onToggled: checked => { + Settings.data.matugen.gtk3 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + + NCheckbox { + label: "Qt6ct" + description: "Write ~/.config/qt6ct/colors/noctalia.conf" + checked: Settings.data.matugen.qt6 + onToggled: checked => { + Settings.data.matugen.qt6 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + + NCheckbox { + label: "Qt5ct" + description: "Write ~/.config/qt5ct/colors/noctalia.conf" + checked: Settings.data.matugen.qt5 + onToggled: checked => { + Settings.data.matugen.qt5 = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } } } - NCheckbox { - label: "GTK 4 (libadwaita)" - description: "Write ~/.config/gtk-4.0/gtk.css" - checked: Settings.data.matugen.gtk4 - onToggled: checked => { - Settings.data.matugen.gtk4 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "GTK 3" - description: "Write ~/.config/gtk-3.0/gtk.css" - checked: Settings.data.matugen.gtk3 - onToggled: checked => { - Settings.data.matugen.gtk3 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Qt6ct" - description: "Write ~/.config/qt6ct/colors/noctalia.conf" - checked: Settings.data.matugen.qt6 - onToggled: checked => { - Settings.data.matugen.qt6 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Qt5ct" - description: "Write ~/.config/qt5ct/colors/noctalia.conf" - checked: Settings.data.matugen.qt5 - onToggled: checked => { - Settings.data.matugen.qt5 = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Kitty" - description: "Write ~/.config/kitty/themes/noctalia.conf and reload" - checked: Settings.data.matugen.kitty - onToggled: checked => { - Settings.data.matugen.kitty = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Ghostty" - description: "Write ~/.config/ghostty/themes/noctalia and reload" - checked: Settings.data.matugen.ghostty - onToggled: checked => { - Settings.data.matugen.ghostty = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Foot" - description: "Write ~/.config/foot/themes/noctalia and reload" - checked: Settings.data.matugen.foot - onToggled: checked => { - Settings.data.matugen.foot = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Fuzzel" - description: "Write ~/.config/fuzzel/themes/noctalia and reload" - checked: Settings.data.matugen.fuzzel - onToggled: checked => { - Settings.data.matugen.fuzzel = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NCheckbox { - label: "Vesktop" - description: "Write ~/.config/vesktop/themes/noctalia.theme.css" - checked: Settings.data.matugen.vesktop - onToggled: checked => { - Settings.data.matugen.vesktop = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } - } - - NDivider { + // Terminal Emulators + NCollapsible { Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling - Layout.bottomMargin: Style.marginM * scaling + label: "Terminal" + description: "Terminal emulator theming." + defaultExpanded: false + + NCheckbox { + label: "Kitty" + description: ProgramCheckerService.kittyAvailable ? "Write ~/.config/kitty/themes/noctalia.conf and reload" : "Requires kitty terminal to be installed" + checked: Settings.data.matugen.kitty + enabled: ProgramCheckerService.kittyAvailable + opacity: ProgramCheckerService.kittyAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.kittyAvailable) { + Settings.data.matugen.kitty = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Ghostty" + description: ProgramCheckerService.ghosttyAvailable ? "Write ~/.config/ghostty/themes/noctalia and reload" : "Requires ghostty terminal to be installed" + checked: Settings.data.matugen.ghostty + enabled: ProgramCheckerService.ghosttyAvailable + opacity: ProgramCheckerService.ghosttyAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.ghosttyAvailable) { + Settings.data.matugen.ghostty = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Foot" + description: ProgramCheckerService.footAvailable ? "Write ~/.config/foot/themes/noctalia and reload" : "Requires foot terminal to be installed" + checked: Settings.data.matugen.foot + enabled: ProgramCheckerService.footAvailable + opacity: ProgramCheckerService.footAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.footAvailable) { + Settings.data.matugen.foot = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } } - NCheckbox { - label: "User Templates" - description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml" - checked: Settings.data.matugen.enableUserTemplates - onToggled: checked => { - Settings.data.matugen.enableUserTemplates = checked - if (Settings.data.colorSchemes.useWallpaperColors) - MatugenService.generateFromWallpaper() - } + // Applications + NCollapsible { + Layout.fillWidth: true + label: "Programs" + description: "Application-specific theming." + defaultExpanded: false + + NCheckbox { + label: "Fuzzel" + description: ProgramCheckerService.fuzzelAvailable ? "Write ~/.config/fuzzel/themes/noctalia and reload" : "Requires fuzzel launcher to be installed" + checked: Settings.data.matugen.fuzzel + enabled: ProgramCheckerService.fuzzelAvailable + opacity: ProgramCheckerService.fuzzelAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.fuzzelAvailable) { + Settings.data.matugen.fuzzel = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Vesktop" + description: ProgramCheckerService.vesktopAvailable ? "Write ~/.config/vesktop/themes/noctalia.theme.css" : "Requires vesktop Discord client to be installed" + checked: Settings.data.matugen.vesktop + enabled: ProgramCheckerService.vesktopAvailable + opacity: ProgramCheckerService.vesktopAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.vesktopAvailable) { + Settings.data.matugen.vesktop = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + + NCheckbox { + label: "Pywalfox (Firefox)" + description: ProgramCheckerService.pywalfoxAvailable ? "Write ~/.cache/wal/colors.json and run pywalfox update" : "Requires pywalfox package to be installed" + checked: Settings.data.matugen.pywalfox + enabled: ProgramCheckerService.pywalfoxAvailable + opacity: ProgramCheckerService.pywalfoxAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.pywalfoxAvailable) { + Settings.data.matugen.pywalfox = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } + } + } + + // Miscellaneous + NCollapsible { + Layout.fillWidth: true + label: "Misc" + description: "Additional configuration options." + defaultExpanded: false + + NCheckbox { + label: "User Templates" + description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml" + checked: Settings.data.matugen.enableUserTemplates + onToggled: checked => { + Settings.data.matugen.enableUserTemplates = checked + if (Settings.data.colorSchemes.useWallpaperColors) + MatugenService.generateFromWallpaper() + } + } } } } diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 9a5c4e3..b5e41f8 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets @@ -9,49 +10,68 @@ import qs.Widgets ColumnLayout { id: root - // Helper functions to update arrays immutably - function addMonitor(list, name) { - const arr = (list || []).slice() - if (!arr.includes(name)) - arr.push(name) - return arr + // Time dropdown options (00:00 .. 23:30) + ListModel { + id: timeOptions } - function removeMonitor(list, name) { - return (list || []).filter(function (n) { - return n !== name - }) + Component.onCompleted: { + for (var h = 0; h < 24; h++) { + for (var m = 0; m < 60; m += 30) { + var hh = ("0" + h).slice(-2) + var mm = ("0" + m).slice(-2) + var key = hh + ":" + mm + timeOptions.append({ + "key": key, + "name": key + }) + } + } } - NText { - text: "Monitor-specific configuration" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold + // Check for wlsunset availability when enabling Night Light + Process { + id: wlsunsetCheck + command: ["which", "wlsunset"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + Settings.data.nightLight.enabled = true + NightLightService.apply() + ToastService.showNotice("Night Light", "Enabled") + } else { + Settings.data.nightLight.enabled = false + ToastService.showWarning("Night Light", "wlsunset not installed") + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} } - NText { - text: "Bars and notifications appear on all displays by default. Choose specific displays below to limit where they're shown." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true + spacing: Style.marginL * scaling + + NHeader { + label: "Monitor-specific configuration" + description: "Configure scaling and brightness settings individually for each connected display." } ColumnLayout { spacing: Style.marginL * scaling - Layout.topMargin: Style.marginL * scaling Repeater { model: Quickshell.screens || [] delegate: Rectangle { Layout.fillWidth: true - Layout.minimumWidth: 550 * scaling + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling radius: Style.radiusM * scaling - color: Color.mSurface + color: Color.mSurfaceVariant border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling property real localScaling: ScalingService.getScreenScale(modelData) + property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData) + Connections { target: ScalingService function onScaleChanged(screenName, scale) { @@ -68,122 +88,100 @@ ColumnLayout { spacing: Style.marginXXS * scaling NText { - text: (modelData.name || "Unknown") - font.pointSize: Style.fontSizeXL * scaling + text: (`${modelData.name}: ${modelData.model}` || "Unknown") + font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold - color: Color.mSecondary + color: Color.mPrimary } NText { - text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` + text: `Resolution: ${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})` font.pointSize: Style.fontSizeXS * scaling color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap Layout.fillWidth: true } + // Scale ColumnLayout { - spacing: Style.marginL * scaling + spacing: Style.marginS * scaling Layout.fillWidth: true - NToggle { - Layout.fillWidth: true - label: "Bar" - description: "Enable the bar on this monitor." - checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 - onToggled: checked => { - if (checked) { - Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) - } else { - Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) - } - } - } - - NToggle { - Layout.fillWidth: true - label: "Notifications" - description: "Enable notifications on this monitor." - checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 - onToggled: checked => { - if (checked) { - Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, - modelData.name) - } else { - Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, - modelData.name) - } - } - } - - NToggle { - Layout.fillWidth: true - label: "Dock" - description: "Enable the dock on this monitor." - checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 - onToggled: checked => { - if (checked) { - Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) - } else { - Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) - } - } - } - - ColumnLayout { - spacing: Style.marginS * scaling + RowLayout { + spacing: Style.marginM * scaling Layout.fillWidth: true - RowLayout { - Layout.fillWidth: true - spacing: Style.marginL * scaling - - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true - - NText { - text: "Scale" - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - NText { - text: "Scale the user interface on this monitor." - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - - NText { - text: `${Math.round(localScaling * 100)}%` - Layout.alignment: Qt.AlignVCenter - Layout.minimumWidth: 50 * scaling - horizontalAlignment: Text.AlignRight - } + NText { + text: "Scale" + Layout.preferredWidth: 80 * scaling } - RowLayout { - spacing: Style.marginS * scaling + NValueSlider { + id: scaleSlider + from: 0.7 + to: 1.8 + stepSize: 0.01 + value: localScaling + onPressedChanged: (pressed, value) => ScalingService.setScreenScale(modelData, value) + text: `${Math.round(localScaling * 100)}%` Layout.fillWidth: true + } - NSlider { - id: scaleSlider - from: 0.7 - to: 1.8 - stepSize: 0.01 - value: localScaling - onPressedChanged: ScalingService.setScreenScale(modelData, value) - Layout.fillWidth: true - Layout.minimumWidth: 150 * scaling - } + // Reset button container + Item { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 30 * scaling NIconButton { icon: "refresh" + sizeRatio: 0.8 tooltipText: "Reset scaling" onClicked: ScalingService.setScreenScale(modelData, 1.0) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + // Brightness + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + visible: brightnessMonitor !== undefined && brightnessMonitor !== null + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: "Brightness" + Layout.preferredWidth: 80 * scaling + } + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1 + value: brightnessMonitor ? brightnessMonitor.brightness : 0.5 + stepSize: 0.01 + onPressedChanged: (pressed, value) => brightnessMonitor.setBrightness(value) + text: brightnessMonitor ? Math.round(brightnessMonitor.brightness * 100) + "%" : "N/A" + } + + // Empty container to match scale row layout + Item { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 30 * scaling + + // Method text positioned in the button area + NText { + text: brightnessMonitor ? brightnessMonitor.method : "" + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignRight } } } @@ -192,4 +190,216 @@ ColumnLayout { } } } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Brightness Section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NHeader { + label: "Brightness" + description: "Adjust brightness related settings." + } + + // Brightness Step Section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NSpinBox { + Layout.fillWidth: true + label: "Brightness Step Size" + description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)." + minimum: 1 + maximum: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + suffix: "%" + onValueChanged: Settings.data.brightness.brightnessStep = value + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Night Light Section + ColumnLayout { + spacing: Style.marginXS * scaling + Layout.fillWidth: true + + NHeader { + label: "Night Light" + description: "Reduce blue light emission to help you sleep better and reduce eye strain." + } + } + + NToggle { + label: "Enable Night Light" + description: "Apply a warm color filter to reduce blue light emission." + checked: Settings.data.nightLight.enabled + onToggled: checked => { + if (checked) { + // Verify wlsunset exists before enabling + wlsunsetCheck.running = true + } else { + Settings.data.nightLight.enabled = false + Settings.data.nightLight.forced = false + NightLightService.apply() + ToastService.showNotice("Night Light", "Disabled") + } + } + } + + // Temperature + ColumnLayout { + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + + NLabel { + label: "Color temperature" + description: "Choose two temperatures in Kelvin." + } + + RowLayout { + visible: Settings.data.nightLight.enabled + spacing: Style.marginM * scaling + Layout.fillWidth: false + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + + NText { + text: "Night" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + + NTextInput { + text: Settings.data.nightLight.nightTemp + inputMethodHints: Qt.ImhDigitsOnly + Layout.alignment: Qt.AlignVCenter + onEditingFinished: { + var nightTemp = parseInt(text) + var dayTemp = parseInt(Settings.data.nightLight.dayTemp) + if (!isNaN(nightTemp) && !isNaN(dayTemp)) { + // Clamp value between [1000 .. (dayTemp-500)] + var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp)) + text = Settings.data.nightLight.nightTemp = clampedValue.toString() + } + } + } + + Item {} + + NText { + text: "Day" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + NTextInput { + text: Settings.data.nightLight.dayTemp + inputMethodHints: Qt.ImhDigitsOnly + Layout.alignment: Qt.AlignVCenter + onEditingFinished: { + var dayTemp = parseInt(text) + var nightTemp = parseInt(Settings.data.nightLight.nightTemp) + if (!isNaN(nightTemp) && !isNaN(dayTemp)) { + // Clamp value between [(nightTemp+500) .. 6500] + var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp)) + text = Settings.data.nightLight.dayTemp = clampedValue.toString() + } + } + } + } + } + + NToggle { + label: "Automatic Scheduling" + description: `Based on the sunset and sunrise time in ${LocationService.stableName} - recommended.` + checked: Settings.data.nightLight.autoSchedule + onToggled: checked => Settings.data.nightLight.autoSchedule = checked + visible: Settings.data.nightLight.enabled + } + + // Schedule settings + ColumnLayout { + spacing: Style.marginXS * scaling + visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced + + RowLayout { + Layout.fillWidth: false + spacing: Style.marginM * scaling + + NLabel { + label: "Manual Scheduling" + } + + Item {// add a little more spacing + } + + NText { + text: "Sunrise Time" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: timeOptions + currentKey: Settings.data.nightLight.manualSunrise + placeholder: "Select start time" + onSelected: key => Settings.data.nightLight.manualSunrise = key + minimumWidth: 120 * scaling + } + + Item {// add a little more spacing + } + + NText { + text: "Sunset Time" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + NComboBox { + model: timeOptions + currentKey: Settings.data.nightLight.manualSunset + placeholder: "Select stop time" + onSelected: key => Settings.data.nightLight.manualSunset = key + minimumWidth: 120 * scaling + } + } + } + + // Force activation toggle + NToggle { + label: "Force activation" + description: "Immediately apply night temperature without scheduling or fade." + checked: Settings.data.nightLight.forced + onToggled: checked => { + Settings.data.nightLight.forced = checked + if (checked && !Settings.data.nightLight.enabled) { + // Ensure enabled when forcing + wlsunsetCheck.running = true + } else { + NightLightService.apply() + } + } + visible: Settings.data.nightLight.enabled + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } } diff --git a/Modules/SettingsPanel/Tabs/DockTab.qml b/Modules/SettingsPanel/Tabs/DockTab.qml new file mode 100644 index 0000000..293378c --- /dev/null +++ b/Modules/SettingsPanel/Tabs/DockTab.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL * scaling + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice() + if (!arr.includes(name)) + arr.push(name) + return arr + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name + }) + } + + NHeader { + label: "Appearance" + description: "Configure dock behavior and appearance." + } + + NToggle { + label: "Auto-hide" + description: "Automatically hide when not in use." + checked: Settings.data.dock.autoHide + onToggled: checked => Settings.data.dock.autoHide = checked + } + + NToggle { + label: "Exclusive Zone" + description: "Ensure windows don't open underneath." + checked: Settings.data.dock.exclusive + onToggled: checked => Settings.data.dock.exclusive = checked + } + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + NLabel { + label: "Background Opacity" + description: "Adjust the background opacity." + } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.dock.backgroundOpacity + onMoved: value => Settings.data.dock.backgroundOpacity = value + text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" + } + } + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NLabel { + label: "Dock Floating Distance" + description: "Adjust the floating distance from the screen edge." + } + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 4 + stepSize: 0.01 + value: Settings.data.dock.floatingRatio + onMoved: value => Settings.data.dock.floatingRatio = value + text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%" + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Monitor Configuration + ColumnLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NHeader { + label: "Monitors Configuration" + description: "Choose which monitors should display the dock." + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}` + description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})` + checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } +} diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index fe4dbcd..84afd87 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -9,6 +9,11 @@ import qs.Widgets ColumnLayout { id: root + NHeader { + label: "Profile" + description: "Configure your user profile and avatar settings." + } + // Profile section RowLayout { Layout.fillWidth: true @@ -48,19 +53,9 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "User Interface" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling - } - - NToggle { - label: "Show Corners" - description: "Display rounded corners on the edge of the screen." - checked: Settings.data.general.showScreenCorners - onToggled: checked => Settings.data.general.showScreenCorners = checked + NHeader { + label: "User Interface" + description: "Main settings for the user interface." } NToggle { @@ -79,23 +74,14 @@ ColumnLayout { description: "Adjust the rounded border of all UI elements." } - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 1 - stepSize: 0.01 - value: Settings.data.general.radiusRatio - onMoved: Settings.data.general.radiusRatio = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.floor(Settings.data.general.radiusRatio * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.general.radiusRatio + onMoved: value => Settings.data.general.radiusRatio = value + text: Math.floor(Settings.data.general.radiusRatio * 100) + "%" } } @@ -109,26 +95,18 @@ ColumnLayout { description: "Adjust global animation speed." } - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0.1 - to: 2.0 - stepSize: 0.01 - value: Settings.data.general.animationSpeed - onMoved: Settings.data.general.animationSpeed = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.round(Settings.data.general.animationSpeed * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } + NValueSlider { + Layout.fillWidth: true + from: 0.1 + to: 2.0 + stepSize: 0.01 + value: Settings.data.general.animationSpeed + onMoved: value => Settings.data.general.animationSpeed = value + text: Math.round(Settings.data.general.animationSpeed * 100) + "%" } } } + NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginXL * scaling @@ -139,57 +117,42 @@ ColumnLayout { ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Dock" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Screen Corners" + description: "Customize screen corner rounding and visual effects." } NToggle { - label: "Auto-hide Dock" - description: "Automatically hide the dock when not in use." - checked: Settings.data.dock.autoHide - onToggled: checked => Settings.data.dock.autoHide = checked + label: "Show Screen Corners" + description: "Display rounded corners on the edge of the screen." + checked: Settings.data.general.showScreenCorners + onToggled: checked => Settings.data.general.showScreenCorners = checked + } + + NToggle { + label: "Solid Black Corners" + description: "Force screen corners to always render as solid black." + checked: Settings.data.general.forceBlackScreenCorners + onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked } ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true - NText { - text: "Dock Background Opacity" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface + NLabel { + label: "Screen Corners Radius" + description: "Adjust the rounded corners of the screen." } - NText { - text: "Adjust the background opacity of the dock." - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap + NValueSlider { Layout.fillWidth: true - } - - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 1 - stepSize: 0.01 - value: Settings.data.dock.backgroundOpacity - onMoved: Settings.data.dock.backgroundOpacity = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } + from: 0 + to: 2 + stepSize: 0.01 + value: Settings.data.general.screenRadiusRatio + onMoved: value => Settings.data.general.screenRadiusRatio = value + text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%" } } } @@ -203,12 +166,10 @@ ColumnLayout { ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Fonts" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + + NHeader { + label: "Fonts" + description: "Configure interface typography." } // Font configuration section @@ -216,12 +177,13 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NComboBox { + NSearchableComboBox { label: "Default Font" description: "Main font used throughout the interface." model: FontService.availableFonts currentKey: Settings.data.ui.fontDefault placeholder: "Select default font..." + searchPlaceholder: "Search fonts..." popupHeight: 420 * scaling minimumWidth: 300 * scaling onSelected: function (key) { @@ -229,12 +191,13 @@ ColumnLayout { } } - NComboBox { + NSearchableComboBox { label: "Fixed Width Font" description: "Monospace font used for terminal and code display." model: FontService.monospaceFonts currentKey: Settings.data.ui.fontFixed placeholder: "Select monospace font..." + searchPlaceholder: "Search monospace fonts..." popupHeight: 320 * scaling minimumWidth: 300 * scaling onSelected: function (key) { @@ -242,12 +205,13 @@ ColumnLayout { } } - NComboBox { + NSearchableComboBox { label: "Billboard Font" description: "Large font used for clocks and prominent displays." model: FontService.displayFonts currentKey: Settings.data.ui.fontBillboard placeholder: "Select display font..." + searchPlaceholder: "Search display fonts..." popupHeight: 320 * scaling minimumWidth: 300 * scaling onSelected: function (key) { diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml index 461a4b8..b3c789d 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -10,6 +10,11 @@ ColumnLayout { spacing: Style.marginL * scaling width: root.width + NHeader { + label: "System Hooks" + description: "Configure commands to be executed when system events occur." + } + // Enable/Disable Toggle NToggle { label: "Enable Hooks" diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 28bb9f0..616583d 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -7,104 +7,97 @@ import qs.Widgets ColumnLayout { id: root + spacing: Style.marginL * scaling + + NHeader { + label: "Appearance" + description: "Configure the launcher behavior and appearance." + } + + NComboBox { + id: launcherPosition + label: "Position" + description: "Choose where the Launcher panel appears." + Layout.fillWidth: true + model: ListModel { + ListElement { + key: "center" + name: "Center (default)" + } + ListElement { + key: "top_left" + name: "Top Left" + } + ListElement { + key: "top_right" + name: "Top Right" + } + ListElement { + key: "bottom_left" + name: "Bottom Left" + } + ListElement { + key: "bottom_right" + name: "Bottom Right" + } + ListElement { + key: "bottom_center" + name: "Bottom Center" + } + ListElement { + key: "top_center" + name: "Top Center" + } + } + currentKey: Settings.data.appLauncher.position + onSelected: function (key) { + Settings.data.appLauncher.position = key + } + } ColumnLayout { - spacing: Style.marginL * scaling + spacing: Style.marginXXS * scaling + Layout.fillWidth: true - NComboBox { - id: launcherPosition - label: "Position" - description: "Choose where the Launcher panel appears." + NText { + text: "Background Opacity" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Adjust the background opacity of the launcher." + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap Layout.fillWidth: true - model: ListModel { - ListElement { - key: "center" - name: "Center (default)" - } - ListElement { - key: "top_left" - name: "Top Left" - } - ListElement { - key: "top_right" - name: "Top Right" - } - ListElement { - key: "bottom_left" - name: "Bottom Left" - } - ListElement { - key: "bottom_right" - name: "Bottom Right" - } - ListElement { - key: "bottom_center" - name: "Bottom Center" - } - ListElement { - key: "top_center" - name: "Top Center" - } - } - currentKey: Settings.data.appLauncher.position - onSelected: function (key) { - Settings.data.appLauncher.position = key - } } - ColumnLayout { - spacing: Style.marginXXS * scaling + NValueSlider { + id: launcherBgOpacity Layout.fillWidth: true - - NText { - text: "Background Opacity" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - - NText { - text: "Adjust the background opacity of the launcher." - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - NSlider { - id: launcherBgOpacity - Layout.fillWidth: true - from: 0.0 - to: 1.0 - stepSize: 0.01 - value: Settings.data.appLauncher.backgroundOpacity - onMoved: Settings.data.appLauncher.backgroundOpacity = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.floor(Settings.data.appLauncher.backgroundOpacity * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } - } + from: 0.0 + to: 1.0 + stepSize: 0.01 + value: Settings.data.appLauncher.backgroundOpacity + onMoved: value => Settings.data.appLauncher.backgroundOpacity = value + text: Math.floor(Settings.data.appLauncher.backgroundOpacity * 100) + "%" } + } - NToggle { - label: "Enable Clipboard History" - description: "Show clipboard history in the launcher." - checked: Settings.data.appLauncher.enableClipboardHistory - onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked - } + NToggle { + label: "Enable Clipboard History" + description: "Show clipboard history in the launcher." + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + } - NToggle { - label: "Use App2Unit for Launching" - description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." - checked: Settings.data.appLauncher.useApp2Unit - onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked - } + NToggle { + label: "Use App2Unit for Launching" + description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." + checked: Settings.data.appLauncher.useApp2Unit + onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked } NDivider { diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index c4ac87a..5feb6fa 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -11,6 +11,11 @@ ColumnLayout { id: root spacing: Style.marginL * scaling + NHeader { + label: "Network Settings" + description: "Configure Wi-Fi and Bluetooth connectivity options." + } + NToggle { label: "Enable Wi-Fi" description: "Enable Wi-Fi connectivity." diff --git a/Modules/SettingsPanel/Tabs/NotificationTab.qml b/Modules/SettingsPanel/Tabs/NotificationTab.qml new file mode 100644 index 0000000..fa6d268 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/NotificationTab.qml @@ -0,0 +1,162 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice() + if (!arr.includes(name)) + arr.push(name) + return arr + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name + }) + } + + // General Notification Settings + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + + NHeader { + label: "Appearance" + description: "Configure notifications appearance and behavior." + } + + NToggle { + label: "Do Not Disturb" + description: "Disable all notification popups when enabled." + checked: Settings.data.notifications.doNotDisturb + onToggled: checked => Settings.data.notifications.doNotDisturb = checked + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Monitor Configuration + ColumnLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NHeader { + label: "Monitors Configuration" + description: "Choose which monitors should display notifications." + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}` + description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})` + checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name) + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Notification Duration Settings + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + + NHeader { + label: "Notification Duration" + description: "Configure how long notifications stay visible based on their urgency level." + } + + // Low Urgency Duration + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NLabel { + label: "Low Urgency Duration" + description: "How long low priority notifications stay visible." + } + + NValueSlider { + Layout.fillWidth: true + from: 1 + to: 30 + stepSize: 1 + value: Settings.data.notifications.lowUrgencyDuration + onMoved: value => Settings.data.notifications.lowUrgencyDuration = value + text: Settings.data.notifications.lowUrgencyDuration + "s" + } + } + + // Normal Urgency Duration + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NLabel { + label: "Normal Urgency Duration" + description: "How long normal priority notifications stay visible." + } + + NValueSlider { + Layout.fillWidth: true + from: 1 + to: 30 + stepSize: 1 + value: Settings.data.notifications.normalUrgencyDuration + onMoved: value => Settings.data.notifications.normalUrgencyDuration = value + text: Settings.data.notifications.normalUrgencyDuration + "s" + } + } + + // Critical Urgency Duration + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NLabel { + label: "Critical Urgency Duration" + description: "How long critical priority notifications stay visible." + } + + NValueSlider { + Layout.fillWidth: true + from: 1 + to: 30 + stepSize: 1 + value: Settings.data.notifications.criticalUrgencyDuration + onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value + text: Settings.data.notifications.criticalUrgencyDuration + "s" + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } +} diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 58f1e8d..05cb66a 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -10,11 +10,15 @@ ColumnLayout { spacing: Style.marginL * scaling + NHeader { + label: "General Settings" + description: "Configure screen recording output and content." + } + // Output Directory ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - Layout.topMargin: Style.marginS * scaling NTextInput { label: "Output Directory" @@ -53,12 +57,8 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Video Settings" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Video Settings" } // Source @@ -203,12 +203,8 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Audio Settings" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.bottomMargin: Style.marginS * scaling + NHeader { + label: "Audio Settings" } // Audio Source diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index f82c4c9..a6d340a 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -9,7 +9,6 @@ import qs.Widgets ColumnLayout { id: root width: parent.width - spacing: Style.marginL * scaling property list wallpapersList: [] @@ -42,11 +41,9 @@ ColumnLayout { } // Current wallpaper display - NText { - text: "Current Wallpaper" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NHeader { + label: "Current Wallpaper" + description: "Preview and manage your desktop background." } Rectangle { @@ -80,18 +77,9 @@ ColumnLayout { Layout.fillWidth: true // Wallpaper grid - NText { - text: "Wallpaper Selector" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NText { - text: "Click on a wallpaper to set it as your current wallpaper." - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true + NHeader { + label: "Wallpaper Selector" + description: "Click on a wallpaper to set it as your current wallpaper." } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 0d40d33..ea4ddc3 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -9,6 +9,12 @@ import qs.Widgets ColumnLayout { id: root + spacing: Style.marginL * scaling + + NHeader { + label: "Wallpaper Settings" + description: "Control how wallpapers are managed and displayed." + } NToggle { label: "Enable Wallpaper Management" @@ -22,6 +28,7 @@ ColumnLayout { visible: Settings.data.wallpaper.enabled spacing: Style.marginL * scaling Layout.fillWidth: true + NTextInput { label: "Wallpaper Directory" description: "Path to your common wallpaper directory." @@ -61,7 +68,7 @@ ColumnLayout { delegate: RowLayout { NText { text: (modelData.name || "Unknown") - color: Color.mSecondary + color: Color.mPrimary font.weight: Style.fontWeightBold Layout.preferredWidth: 90 * scaling } @@ -89,11 +96,8 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Look & Feel" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NHeader { + label: "Look & Feel" } // Fill Mode @@ -134,21 +138,14 @@ ColumnLayout { description: "Duration of transition animations in seconds." } - RowLayout { - spacing: Style.marginL * scaling - NSlider { - Layout.fillWidth: true - from: 500 - to: 10000 - stepSize: 100 - value: Settings.data.wallpaper.transitionDuration - onMoved: Settings.data.wallpaper.transitionDuration = value - cutoutColor: Color.mSurface - } - NText { - text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s" - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - } + NValueSlider { + Layout.fillWidth: true + from: 500 + to: 10000 + stepSize: 100 + value: Settings.data.wallpaper.transitionDuration + onMoved: value => Settings.data.wallpaper.transitionDuration = value + text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s" } } @@ -159,20 +156,13 @@ ColumnLayout { description: "Duration of transition animations in seconds." } - RowLayout { - spacing: Style.marginL * scaling - NSlider { - Layout.fillWidth: true - from: 0.0 - to: 1.0 - value: Settings.data.wallpaper.transitionEdgeSmoothness - onMoved: Settings.data.wallpaper.transitionEdgeSmoothness = value - cutoutColor: Color.mSurface - } - NText { - text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%" - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - } + NValueSlider { + Layout.fillWidth: true + from: 0.0 + to: 1.0 + value: Settings.data.wallpaper.transitionEdgeSmoothness + onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value + text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%" } } } @@ -189,11 +179,8 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Automation" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NHeader { + label: "Automation" } // Random Wallpaper diff --git a/Modules/SettingsPanel/Tabs/WeatherTab.qml b/Modules/SettingsPanel/Tabs/WeatherTab.qml index 667aca5..68676f7 100644 --- a/Modules/SettingsPanel/Tabs/WeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/WeatherTab.qml @@ -7,6 +7,12 @@ import qs.Widgets ColumnLayout { id: root + spacing: Style.marginL * scaling + + NHeader { + label: "Your Location" + description: "Set your location for weather, time zones, and scheduling." + } // Location section RowLayout { @@ -57,11 +63,9 @@ ColumnLayout { spacing: Style.marginM * scaling Layout.fillWidth: true - NText { - text: "Weather" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary + NHeader { + label: "Weather" + description: "Configure weather display preferences and temperature units." } NToggle { @@ -71,4 +75,10 @@ ColumnLayout { onToggled: checked => Settings.data.location.useFahrenheit = checked } } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index d4207e6..66db286 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -10,12 +10,8 @@ import qs.Widgets NBox { id: root - Layout.fillWidth: true - Layout.fillHeight: true - ColumnLayout { anchors.fill: parent - Layout.fillHeight: true anchors.margins: Style.marginL * scaling // No media player detected @@ -236,9 +232,7 @@ NBox { return 0 return Math.max(0, Math.min(1, r)) } - property real effectiveRatio: (MediaService.isSeeking - && localSeekRatio >= 0) ? Math.max(0, Math.min(1, - localSeekRatio)) : progressRatio + property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio // Debounced backend seek during drag Timer { @@ -248,8 +242,7 @@ NBox { onTriggered: { if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) { const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio)) - if (progressWrapper.lastSentSeekRatio < 0 || Math.abs( - next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) { + if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) { MediaService.seekByRatio(next) progressWrapper.lastSentSeekRatio = next } @@ -265,7 +258,6 @@ NBox { stepSize: 0 snapAlways: false enabled: MediaService.trackLength > 0 && MediaService.canSeek - cutoutColor: Color.mSurface heightRatio: 0.65 onMoved: { diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 63efa31..bbfe070 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -9,13 +9,11 @@ import qs.Widgets // Power Profiles: performance, balanced, eco NBox { - Layout.fillWidth: true - Layout.preferredWidth: 1 - implicitHeight: powerRow.implicitHeight + Style.marginM * 2 * scaling + + property real spacing: 0 // Centralized service readonly property bool hasPP: PowerProfileService.available - property real spacing: 0 RowLayout { id: powerRow @@ -31,8 +29,7 @@ NBox { tooltipText: "Set performance power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled - && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant + colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary onClicked: { if (enabled) { @@ -46,8 +43,7 @@ NBox { tooltipText: "Set balanced power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled - && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant + colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary onClicked: { if (enabled) { @@ -61,8 +57,7 @@ NBox { tooltipText: "Set eco power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled - && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant + colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary onClicked: { if (enabled) { diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index c883ef7..34bb0ce 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -16,10 +16,6 @@ NBox { property string uptimeText: "--" - Layout.fillWidth: true - // Height driven by content - implicitHeight: content.implicitHeight + Style.marginM * 2 * scaling - RowLayout { id: content anchors.left: parent.left @@ -63,7 +59,7 @@ NBox { tooltipText: "Open settings." onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General - settingsPanel.open(screen) + settingsPanel.open() } } @@ -72,7 +68,7 @@ NBox { icon: "power" tooltipText: "Power menu." onClicked: { - powerPanel.open(screen) + powerPanel.open() sidePanel.close() } } diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index dc58845..f4c615c 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -8,9 +8,6 @@ import qs.Widgets NBox { id: root - Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling - implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling - ColumnLayout { id: content anchors.left: parent.left diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 623d3fd..fd23adf 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -12,9 +12,6 @@ NBox { property real spacing: 0 - Layout.fillWidth: true - Layout.preferredWidth: 1 - implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling RowLayout { id: utilRow anchors.fill: parent @@ -61,7 +58,7 @@ NBox { onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector - settingsPanel.open(screen) + settingsPanel.open() } onRightClicked: { WallpaperService.setRandomWallpaper() diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 5f3f137..160c48c 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -11,11 +11,6 @@ NBox { readonly property bool weatherReady: (LocationService.data.weather !== null) - // TBC weatherReady is not turning to false when we reset weather... - Layout.fillWidth: true - // Height driven by content - implicitHeight: content.implicitHeight + Style.marginL * 2 * scaling - ColumnLayout { id: content anchors.left: parent.left @@ -28,8 +23,7 @@ NBox { spacing: Style.marginS * scaling NIcon { Layout.alignment: Qt.AlignVCenter - icon: weatherReady ? LocationService.weatherSymbolFromCode( - LocationService.data.weather.current_weather.weathercode) : "" + icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "" font.pointSize: Style.fontSizeXXXL * 1.75 * scaling color: Color.mPrimary } @@ -84,7 +78,7 @@ NBox { RowLayout { visible: weatherReady Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter spacing: Style.marginL * scaling Repeater { model: weatherReady ? LocationService.data.weather.daily.time : [] diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 4dba15a..bfd4e85 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -10,32 +10,15 @@ import qs.Widgets NPanel { id: root - panelWidth: 460 * scaling - panelHeight: contentHeight - - // Default height, will be modified via binding when the content is fully loaded - property real contentHeight: 720 * scaling + preferredWidth: 460 + preferredHeight: 734 + panelKeyboardFocus: true panelContent: Item { id: content property real cardSpacing: Style.marginL * scaling - width: root.panelWidth - implicitHeight: layout.implicitHeight + (2 * cardSpacing) - height: implicitHeight - - // Update parent's contentHeight whenever our height changes - onHeightChanged: { - root.contentHeight = height - } - - onImplicitHeightChanged: { - if (implicitHeight > 0) { - root.contentHeight = implicitHeight - } - } - // Layout content ColumnLayout { id: layout @@ -46,57 +29,52 @@ NPanel { // Cards (consistent inter-card spacing via ColumnLayout spacing) ProfileCard { - id: profileCard Layout.fillWidth: true + Layout.preferredHeight: Math.max(64 * scaling) } WeatherCard { - id: weatherCard Layout.fillWidth: true + Layout.preferredHeight: Math.max(220 * scaling) } // Middle section: media + stats column RowLayout { - id: middleRow Layout.fillWidth: true - Layout.minimumHeight: 280 * scaling - Layout.preferredHeight: Math.max(280 * scaling, statsCard.implicitHeight) + Layout.preferredHeight: Math.max(310 * scaling) spacing: content.cardSpacing // Media card MediaCard { - id: mediaCard Layout.fillWidth: true Layout.fillHeight: true } // System monitors combined in one card SystemMonitorCard { - id: statsCard - Layout.alignment: Qt.AlignTop + Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling + Layout.fillHeight: true } } // Bottom actions (two grouped rows of round buttons) RowLayout { - id: bottomRow Layout.fillWidth: true - Layout.minimumHeight: 60 * scaling - Layout.preferredHeight: Math.max(60 * scaling, powerProfilesCard.implicitHeight, utilitiesCard.implicitHeight) + Layout.preferredHeight: Math.max(60 * scaling) spacing: content.cardSpacing // Power Profiles switcher PowerProfilesCard { - id: powerProfilesCard - spacing: content.cardSpacing Layout.fillWidth: true + Layout.fillHeight: true + spacing: content.cardSpacing } // Utilities buttons UtilitiesCard { - id: utilitiesCard - spacing: content.cardSpacing Layout.fillWidth: true + Layout.fillHeight: true + spacing: content.cardSpacing } } } diff --git a/Modules/Toast/SimpleToast.qml b/Modules/Toast/SimpleToast.qml new file mode 100644 index 0000000..bc51858 --- /dev/null +++ b/Modules/Toast/SimpleToast.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Rectangle { + id: root + + property string message: "" + property string description: "" + property string type: "notice" + property int duration: 3000 + readonly property real initialScale: 0.7 + + signal hidden + + width: Math.min(500 * scaling, parent.width * 0.8) + height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling) + radius: Style.radiusL * scaling + visible: false + opacity: 0 + scale: initialScale + + // Clean surface background like NToast + color: Color.mSurface + + // Colored border based on type + border.color: { + switch (type) { + case "warning": + return Color.mPrimary + case "error": + return Color.mError + default: + return Color.mOutline + } + } + border.width: Math.max(2, Style.borderM * scaling) + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Timer { + id: hideTimer + interval: root.duration + onTriggered: root.hide() + } + + Timer { + id: hideAnimation + interval: Style.animationFast + onTriggered: { + root.visible = false + root.hidden() + } + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginL * scaling + + // Icon + NIcon { + id: icon + icon: { + switch (type) { + case "warning": + return "toast-warning" + case "error": + return "toast-error" + default: + return "toast-notice" + } + } + color: { + switch (type) { + case "warning": + return Color.mPrimary + case "error": + return Color.mError + default: + return Color.mOnSurface + } + } + font.pointSize: Style.fontSizeXXL * 1.5 * scaling + Layout.alignment: Qt.AlignVCenter + } + + // Label and description + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + NText { + Layout.fillWidth: true + text: root.message + color: Color.mOnSurface + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + wrapMode: Text.WordWrap + visible: text.length > 0 + } + + NText { + Layout.fillWidth: true + text: root.description + color: Color.mOnSurface + font.pointSize: Style.fontSizeM * scaling + wrapMode: Text.WordWrap + visible: text.length > 0 + } + } + + // Close button + NIconButton { + id: closeButton + icon: "close" + + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.mOutline + + sizeRatio: 0.8 + Layout.alignment: Qt.AlignTop + + onClicked: root.hide() + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: root.hide() + cursorShape: Qt.PointingHandCursor + } + + function show(msg, desc, msgType, msgDuration) { + message = msg + description = desc || "" + type = msgType || "notice" + duration = msgDuration || 3000 + + visible = true + opacity = 1 + scale = 1.0 + + hideTimer.restart() + } + + function hide() { + hideTimer.stop() + opacity = 0 + scale = initialScale + hideAnimation.start() + } + + function hideImmediately() { + opacity = 0 + scale = initialScale + root.visible = false + root.hidden() + } +} diff --git a/Modules/Toast/ToastOverlay.qml b/Modules/Toast/ToastOverlay.qml index d86a549..78cb670 100644 --- a/Modules/Toast/ToastOverlay.qml +++ b/Modules/Toast/ToastOverlay.qml @@ -9,70 +9,13 @@ import qs.Widgets Variants { model: Quickshell.screens - delegate: Loader { + delegate: ToastScreen { required property ShellScreen modelData - property real scaling: ScalingService.getScreenScale(modelData) - Connections { - target: ScalingService - function onScaleChanged(screenName, scale) { - if (screenName === modelData.name) { - scaling = scale - } - } - } + screen: modelData + scaling: ScalingService.getScreenScale(modelData) - // Only show on screens that have notifications enabled - active: Settings.isLoaded && modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false - - sourceComponent: PanelWindow { - id: root - - screen: modelData - - // Position based on bar location, like Notification popup does - anchors { - top: Settings.data.bar.position === "top" - bottom: Settings.data.bar.position === "bottom" - } - - // Set a width instead of anchoring left/right so we can click on the side of the toast - implicitWidth: 500 * scaling - - // Small height when hidden, appropriate height when visible - implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1) - - // Set margins based on bar position - margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginS) * scaling : 0 - margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS) * scaling : 0 - - // Transparent background - color: Color.transparent - - // Overlay layer to appear above other panels - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - exclusionMode: PanelWindow.ExclusionMode.Ignore - - NToast { - id: toast - screen: modelData - - // Simple positioning - margins already account for bar - targetY: Style.marginS * scaling - - // Hidden position based on bar location - hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20 - - Component.onCompleted: { - // Register this toast with the service - ToastService.allToasts.push(toast) - - // Connect dismissal signal - toast.dismissed.connect(ToastService.onToastDismissed) - } - } - } + // Only activate on enabled screens + active: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || Settings.data.notifications.monitors.length === 0) } } diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml new file mode 100644 index 0000000..a2e0eca --- /dev/null +++ b/Modules/Toast/ToastScreen.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +Loader { + id: root + + required property ShellScreen screen + required property real scaling + required property bool active + + // Local queue for this screen only + property var messageQueue: [] + property bool isShowingToast: false + + // If true, immediately show new toasts + property bool replaceOnNew: true + + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if (screenName === root.screen.name) { + root.scaling = scale + } + } + } + + Connections { + target: ToastService + enabled: root.active + + function onNotify(message, description, type, duration) { + root.enqueueToast({ + "message": message, + "description": description, + "type": type, + "duration": duration, + "timestamp": Date.now() + }) + } + } + + function enqueueToast(toastData) { + if (replaceOnNew && isShowingToast) { + // Cancel current toast and clear queue for latest toast + messageQueue = [] // Clear existing queue + messageQueue.push(toastData) + + // Hide current toast immediately + if (item) { + hideTimer.stop() + item.hideToast() // Need to add this method to PanelWindow + } + + // Process new toast after a brief delay + isShowingToast = false + quickSwitchTimer.restart() + } else { + // Original behavior - queue the toast + messageQueue.push(toastData) + processQueue() + } + } + + Timer { + id: quickSwitchTimer + interval: 50 // Brief delay for smooth transition + onTriggered: root.processQueue() + } + + function processQueue() { + if (!active || !item || messageQueue.length === 0 || isShowingToast) { + return + } + + var data = messageQueue.shift() + isShowingToast = true + + // Show the toast + item.showToast(data.message, data.description, data.type, data.duration) + } + + function onToastHidden() { + isShowingToast = false + // Small delay before next toast + hideTimer.restart() + } + + Timer { + id: hideTimer + interval: 200 + onTriggered: root.processQueue() + } + + sourceComponent: PanelWindow { + id: panel + + screen: root.screen + + anchors { + top: true + } + + implicitWidth: 500 * root.scaling + implicitHeight: Math.round(toastItem.visible ? toastItem.height + Style.marginM * root.scaling : 1) + + // Set margins based on bar position + margins.top: { + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return Style.marginL * scaling + } + } + + color: Color.transparent + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: PanelWindow.ExclusionMode.Ignore + + function showToast(message, description, type, duration) { + toastItem.show(message, description, type, duration) + } + + // Add method to immediately hide toast + function hideToast() { + toastItem.hideImmediately() + } + + SimpleToast { + id: toastItem + + anchors.horizontalCenter: parent.horizontalCenter + onHidden: root.onToastHidden() + } + } +} diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 7d49a37..bfda627 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -10,8 +10,8 @@ import qs.Widgets NPanel { id: root - panelWidth: 400 * scaling - panelHeight: 500 * scaling + preferredWidth: 400 + preferredHeight: 500 panelKeyboardFocus: true property string passwordSsid: "" @@ -64,7 +64,7 @@ NPanel { NIconButton { icon: "close" - tooltipText: "Close" + tooltipText: "Close." sizeRatio: 0.8 onClicked: root.close() } @@ -156,8 +156,7 @@ NPanel { // Scanning state ColumnLayout { - visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys( - NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 anchors.fill: parent spacing: Style.marginL * scaling @@ -185,12 +184,11 @@ NPanel { } // Networks list container - ScrollView { - visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys( - NetworkService.networks).length > 0) + NScrollView { + visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) anchors.fill: parent - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded clip: true ColumnLayout { @@ -217,11 +215,9 @@ NPanel { radius: Style.radiusM * scaling // Add opacity for operations in progress - opacity: (NetworkService.disconnectingFrom === modelData.ssid - || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0 + opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0 - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, - 0.05) : Color.mSurface + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline @@ -338,9 +334,7 @@ NPanel { } Rectangle { - visible: modelData.cached && !modelData.connected - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid + visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid color: Color.transparent border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -364,19 +358,14 @@ NPanel { spacing: Style.marginS * scaling NBusyIndicator { - visible: NetworkService.connectingTo === modelData.ssid - || NetworkService.disconnectingFrom === modelData.ssid - || NetworkService.forgettingNetwork === modelData.ssid + visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid running: visible color: Color.mPrimary size: Style.baseWidgetSize * 0.5 * scaling } NIconButton { - visible: (modelData.existing || modelData.cached) && !modelData.connected - && NetworkService.connectingTo !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid + visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid icon: "trash" tooltipText: "Forget network" sizeRatio: 0.7 @@ -384,10 +373,7 @@ NPanel { } NButton { - visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid - && passwordSsid !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid - && NetworkService.disconnectingFrom !== modelData.ssid + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid text: { if (modelData.existing || modelData.cached) return "Connect" @@ -422,8 +408,7 @@ NPanel { // Password input Rectangle { - visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid + visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: passwordRow.implicitHeight + Style.marginS * scaling * 2 color: Color.mSurfaceVariant @@ -504,8 +489,7 @@ NPanel { // Forget network Rectangle { - visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid - && NetworkService.forgettingNetwork !== modelData.ssid + visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: forgetRow.implicitHeight + Style.marginS * 2 * scaling color: Color.mSurfaceVariant @@ -561,8 +545,7 @@ NPanel { // Empty state when no networks ColumnLayout { - visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys( - NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 anchors.fill: parent spacing: Style.marginL * scaling diff --git a/README.md b/README.md index 58166e3..0429709 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,20 @@ Features a modern modular architecture with a status bar, notification system, c ## Preview -![Launcher](/Assets/Screenshots/launcher.png) +https://github.com/user-attachments/assets/72c6d6dc-48b0-48a0-bd8b-c7e70990edc4 -![SettingsPanel](/Assets/Screenshots/settings-panel.png?v=2) +
+Screenshots -![SidePanel](/Assets/Screenshots/light-mode.png?v=2) +![Dark 1](/Assets/Screenshots/noctalia-dark-1.png) +![Dark 2](/Assets/Screenshots/noctalia-dark-2.png) +![Dark 3](/Assets/Screenshots/noctalia-dark-3.png) + +![Light 1](/Assets/Screenshots/noctalia-light-1.png) +![Light 2](/Assets/Screenshots/noctalia-light-2.png) +![Light 3](/Assets/Screenshots/noctalia-light-3.png) + +
--- @@ -68,7 +77,7 @@ Features a modern modular architecture with a status bar, notification system, c - `quickshell-git` - Core shell framework - `ttf-roboto` - The default font used for most of the UI - `inter-font` - The default font used for Headers (ex: clock on the LockScreen) -- `gpu-screen-recorder` - Screen recording functionality +- `gpu-screen-recorder` - Screen recording functionality (Flatpak also supported) - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) @@ -83,15 +92,6 @@ Features a modern modular architecture with a status bar, notification system, c > There is one more optional dependency. > `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder. -If you want to use the `ArchUpdater` widget, you will have to set your `TERMINAL` environment variable. - -Example command (you can edit the /etc/environment file manually too): - -`sudo sed -i '/^TERMINAL=/d' /etc/environment && echo 'TERMINAL=/usr/bin/kitty' | sudo tee -a /etc/environment -` - -Please do not forget to edit `TERMINAL=/usr/bin/kitty` to match your terminal. - --- ## Quick Start @@ -154,16 +154,17 @@ Alternatively, you can add it to your NixOS configuration or flake: inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - - noctalia = { - url = "github:noctalia-dev/noctalia-shell"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + # you need nixpkgs unstable quickshell = { url = "github:outfoxxed/quickshell"; inputs.nixpkgs.follows = "nixpkgs"; - inputs.quickshell.follows = "quickshell" + }; + + noctalia = { + url = "github:noctalia-dev/noctalia-shell"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.quickshell.follows = "quickshell"; }; }; @@ -191,6 +192,7 @@ Alternatively, you can add it to your NixOS configuration or flake: ### Usage +Start the Shell with: `qs -c noctalia-shell` `noctalia-shell` offers many IPC calls for your convenience, so you can add them to your favorite keybinds or scripts. diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml deleted file mode 100644 index 9d33f2b..0000000 --- a/Services/ArchUpdaterService.qml +++ /dev/null @@ -1,705 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons - -Singleton { - id: updateService - - // ============================================================================ - // CORE PROPERTIES - // ============================================================================ - - // Package data - property var repoPackages: [] - property var aurPackages: [] - property var selectedPackages: [] - property int selectedPackagesCount: 0 - property string allUpdatesOutput: "" - - // Update state - property bool updateInProgress: false - property bool updateFailed: false - property string lastUpdateError: "" - property bool checkFailed: false - property string lastCheckError: "" - - // Monitoring state - property string capturedErrorText: "" - property string capturedSuccessText: "" - - // Computed properties - readonly property bool aurBusy: checkAurUpdatesProcess.running || checkAurOnlyProcess.running - readonly property int updates: repoPackages.length - readonly property int aurUpdates: aurPackages.length - readonly property int totalUpdates: updates + aurUpdates - - // Terminal validation - readonly property bool terminalAvailable: Quickshell.env("TERMINAL") !== "" - readonly property string terminalError: "TERMINAL environment variable not set" - - // AUR helper validation - readonly property bool aurHelperAvailable: cachedAurHelper !== "" - readonly property string aurHelperError: "No AUR helper found (yay or paru not installed)" - - // Polling cooldown (prevent excessive polling) - property int lastPollTime: 0 - readonly property int pollCooldownMs: 5 * 60 * 1000 // 5 minutes - readonly property bool canPoll: (Date.now() - lastPollTime) > pollCooldownMs - - // ============================================================================ - // TIMERS - // ============================================================================ - - // Refresh timer for post-update polling - Timer { - id: refreshTimer - interval: 5000 - repeat: false - onTriggered: { - doPoll() - } - } - - // Timer to mark update as complete - with error handling - Timer { - id: updateCompleteTimer - interval: 30000 // Increased to 30 seconds to allow more time - repeat: false - onTriggered: { - checkForUpdateFailures() - } - } - - // Timer to check if update processes are still running - Timer { - id: updateMonitorTimer - interval: 2000 - repeat: true - running: updateInProgress - onTriggered: { - // Check if any update-related processes might still be running - checkUpdateStatus() - } - } - - // ============================================================================ - // MONITORING PROCESSES - // ============================================================================ - - // Process to monitor update completion - Process { - id: updateStatusProcess - command: ["pgrep", "-f", "(yay|paru).*(-S|-Syu)"] - onExited: function (exitCode) { - if (exitCode !== 0 && updateInProgress) { - // No update processes found, update likely completed - updateInProgress = false - updateMonitorTimer.stop() - errorCheckTimer.stop() - successCheckTimer.stop() - - // Don't stop the complete timer - let it handle failures - // If the update actually failed, the timer will trigger and set updateFailed = true - - // Refresh package lists after a short delay - Qt.callLater(() => { - doPoll() - }, 2000) - } - } - } - - // Process to check for errors in log file (only when update is in progress) - Process { - id: errorCheckProcess - command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'failed to build\\|could not resolve\\|unable to satisfy\\|failed to install\\|failed to upgrade\\|error:' /tmp/archupdater_output.log | grep -v 'ERROR_DETECTED' | tail -1; fi"] - onExited: function (exitCode) { - if (exitCode === 0 && updateInProgress && capturedErrorText.trim() !== "") { - // Error found in log - updateInProgress = false - updateFailed = true - updateCompleteTimer.stop() - updateMonitorTimer.stop() - errorCheckTimer.stop() - successCheckTimer.stop() - lastUpdateError = "Build or update error detected" - - // Refresh to check actual state - Qt.callLater(() => { - doPoll() - }, 1000) - } - } - stdout: StdioCollector { - onStreamFinished: { - capturedErrorText = text || "" - } - } - } - - // Process to check for successful completion - Process { - id: successCheckProcess - command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'Update complete!\\|:: Running post-transaction hooks\\|:: Processing package changes\\|upgrading.*\\.\\.\\.\\|installing.*\\.\\.\\.\\|removing.*\\.\\.\\.' /tmp/archupdater_output.log | tail -1; fi"] - onExited: function (exitCode) { - if (exitCode === 0 && updateInProgress && capturedSuccessText.trim() !== "") { - // Success indicators found - updateInProgress = false - updateFailed = false - updateCompleteTimer.stop() - updateMonitorTimer.stop() - errorCheckTimer.stop() - successCheckTimer.stop() - lastUpdateError = "" - - // Refresh to check actual state - Qt.callLater(() => { - doPoll() - }, 1000) - } - } - stdout: StdioCollector { - onStreamFinished: { - capturedSuccessText = text || "" - } - } - } - - // Timer to check for success more frequently when update is in progress - Timer { - id: successCheckTimer - interval: 5000 // Check every 5 seconds - repeat: true - running: updateInProgress - onTriggered: { - if (updateInProgress && !successCheckProcess.running) { - successCheckProcess.running = true - } - } - } - - // Timer to check for errors more frequently when update is in progress - Timer { - id: errorCheckTimer - interval: 5000 // Check every 5 seconds - repeat: true - running: updateInProgress - onTriggered: { - if (updateInProgress && !errorCheckProcess.running) { - errorCheckProcess.running = true - } - } - } - - // ============================================================================ - // MONITORING FUNCTIONS - // ============================================================================ - function checkUpdateStatus() { - if (updateInProgress && !updateStatusProcess.running) { - updateStatusProcess.running = true - } - } - - function checkForUpdateFailures() { - updateInProgress = false - updateFailed = true - updateCompleteTimer.stop() - updateMonitorTimer.stop() - - // Refresh to check actual state after a delay - Qt.callLater(() => { - doPoll() - }, 2000) - } - - // Initial check - Component.onCompleted: { - // Start AUR helper detection - getAurHelper() - - // Set up a fallback timer in case detection takes too long - Qt.callLater(() => { - if (cachedAurHelper === "" && !yayCheckProcess.running && !paruCheckProcess.running) { - // No AUR helper found after reasonable time and processes have finished - checkFailed = true - lastCheckError = "No AUR helper found (yay or paru not installed)" - Logger.warn("ArchUpdater", "No AUR helper found (yay or paru)") - } - }, 5000) // 5 second fallback - } - - // ============================================================================ - // PACKAGE CHECKING PROCESSES - // ============================================================================ - - // Process for checking all updates with AUR helper (repo + AUR) - Process { - id: checkAurUpdatesProcess - command: [] - onExited: function (exitCode) { - // For both yay and paru: exit code 0 = updates available, exit code 1 = no updates - if (exitCode !== 0 && exitCode !== 1) { - Logger.warn("ArchUpdater", "AUR helper check failed (code:", exitCode, ")") - checkFailed = true - lastCheckError = "Failed to check for updates (exit code: " + exitCode + ")" - aurPackages = [] - repoPackages = [] - } - // Don't clear checkFailed here - wait for the second process to complete - } - stdout: StdioCollector { - onStreamFinished: { - allUpdatesOutput = text - // Now get AUR-only updates to compare - checkAurOnlyProcess.running = true - } - } - } - - // Process for checking AUR-only updates (to separate from repo updates) - Process { - id: checkAurOnlyProcess - command: [] - onExited: function (exitCode) { - // For both yay and paru: exit code 0 = updates available, exit code 1 = no updates - if (exitCode !== 0 && exitCode !== 1) { - Logger.warn("ArchUpdater", "AUR helper AUR-only check failed (code:", exitCode, ")") - checkFailed = true - lastCheckError = "Failed to check AUR updates (exit code: " + exitCode + ")" - aurPackages = [] - repoPackages = [] - } else { - // Only clear checkFailed if both processes succeeded - // Check if the first process also succeeded (no error was set) - if (!checkFailed) { - checkFailed = false - lastCheckError = "" - } - } - } - stdout: StdioCollector { - onStreamFinished: { - parseAllUpdatesOutput(allUpdatesOutput, text) - Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) and", aurPackages.length, - "AUR package(s) to upgrade") - } - } - } - - // ============================================================================ - // PARSING FUNCTIONS - // ============================================================================ - - // Generic package parsing function - function parsePackageOutput(output, source) { - const lines = output.trim().split('\n').filter(line => line.trim()) - const packages = [] - - for (const line of lines) { - const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/) - if (m) { - packages.push({ - "name": m[1], - "oldVersion": m[2], - "newVersion": m[3], - "description": `${m[1]} ${m[2]} -> ${m[3]}`, - "source": source - }) - } - } - - // Only update if we have new data or if this is a fresh check - if (packages.length > 0 || output.trim() === "") { - if (source === "repo") { - repoPackages = packages - } else { - aurPackages = packages - } - } - } - - // Parse all updates output (repo + AUR packages) - function parseAllUpdatesOutput(allOutput, aurOnlyOutput) { - const allLines = allOutput.trim().split('\n').filter(line => line.trim()) - const aurOnlyLines = aurOnlyOutput.trim().split('\n').filter(line => line.trim()) - - // Create a set of AUR package names for quick lookup - const aurPackageNames = new Set() - for (const line of aurOnlyLines) { - const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/) - if (m) { - aurPackageNames.add(m[1]) - } - } - - const repoPackages = [] - const aurPackages = [] - - for (const line of allLines) { - const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/) - if (m) { - const packageInfo = { - "name": m[1], - "oldVersion": m[2], - "newVersion": m[3], - "description": `${m[1]} ${m[2]} -> ${m[3]}` - } - - // Check if this package is in the AUR-only list - if (aurPackageNames.has(m[1])) { - packageInfo.source = "aur" - aurPackages.push(packageInfo) - } else { - packageInfo.source = "repo" - repoPackages.push(packageInfo) - } - } - } - - // Update the package lists - if (repoPackages.length > 0 || aurPackages.length > 0 || allOutput.trim() === "") { - updateService.repoPackages = repoPackages - updateService.aurPackages = aurPackages - } - } - - function doPoll() { - // Prevent excessive polling - if (aurBusy || !canPoll) { - return - } - - // Check if we have a cached AUR helper - if (cachedAurHelper !== "") { - // Clear error state when helper is available - if (checkFailed && lastCheckError.includes("No AUR helper found")) { - checkFailed = false - lastCheckError = "" - } - - checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"] - checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()] - - // Start AUR updates check (includes both repo and AUR packages) - checkAurUpdatesProcess.running = true - lastPollTime = Date.now() - } else { - // AUR helper detection is still in progress or failed - // Try to detect again if not already in progress - if (!yayCheckProcess.running && !paruCheckProcess.running) { - getAurHelper() - } - Logger.warn("ArchUpdater", "AUR helper detection in progress or failed") - } - } - - // ============================================================================ - // UPDATE FUNCTIONS - // ============================================================================ - - // Helper function to generate update command with error detection - function generateUpdateCommand(baseCommand) { - return baseCommand + " 2>&1 | tee /tmp/archupdater_output.log; if [ $? -ne 0 ]; then echo 'ERROR_DETECTED'; fi; echo 'Update complete! Press Enter to close...'; read -p 'Press Enter to continue...'" - } - - // Update all packages (repo + AUR) - function runUpdate() { - if (totalUpdates === 0) { - doPoll() - return - } - - // Reset any previous error states - updateFailed = false - lastUpdateError = "" - updateInProgress = true - capturedErrorText = "" - capturedSuccessText = "" - - const terminal = Quickshell.env("TERMINAL") - if (!terminal) { - updateInProgress = false - updateFailed = true - lastUpdateError = "TERMINAL environment variable not set" - ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set") - return - } - - // Check if we have an AUR helper for full system update - if (cachedAurHelper !== "" && (aurUpdates > 0 || updates > 0)) { - // Use AUR helper for full system update (handles both repo and AUR) - const command = generateUpdateCommand(cachedAurHelper + " -Syu") - Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) - } else if (cachedAurHelper === "") { - // No AUR helper found - updateInProgress = false - updateFailed = true - lastUpdateError = "No AUR helper found (yay or paru not installed)" - Logger.warn("ArchUpdater", "No AUR helper found for update") - } - - // Start monitoring and timeout timers - refreshTimer.start() - updateCompleteTimer.start() - updateMonitorTimer.start() - } - - // Update selected packages - function runSelectiveUpdate() { - if (selectedPackages.length === 0) - return - - // Reset any previous error states - updateFailed = false - lastUpdateError = "" - updateInProgress = true - capturedErrorText = "" - capturedSuccessText = "" - - const terminal = Quickshell.env("TERMINAL") - if (!terminal) { - updateInProgress = false - updateFailed = true - lastUpdateError = "TERMINAL environment variable not set" - ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set") - return - } - - // Update all packages with AUR helper (handles both repo and AUR) - if (selectedPackages.length > 0) { - if (cachedAurHelper !== "") { - const packageList = selectedPackages.join(" ") - - // Handle ghostty terminal differently due to command parsing issues - if (terminal.includes("ghostty")) { - const simpleCommand = cachedAurHelper + " -S " + packageList - Quickshell.execDetached([terminal, "-e", simpleCommand]) - } else { - const command = generateUpdateCommand(cachedAurHelper + " -S " + packageList) - Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) - } - } else { - updateInProgress = false - updateFailed = true - lastUpdateError = "No AUR helper found (yay or paru not installed)" - Logger.warn("ArchUpdater", "No AUR helper found for packages:", selectedPackages.join(", ")) - } - } - - // Start monitoring and timeout timers - refreshTimer.start() - updateCompleteTimer.start() - updateMonitorTimer.start() - } - - // Reset update state (useful for manual recovery) - function resetUpdateState() { - // Clear all update states - updateInProgress = false - updateFailed = false - lastUpdateError = "" - checkFailed = false - lastCheckError = "" - updateCompleteTimer.stop() - updateMonitorTimer.stop() - refreshTimer.stop() - errorCheckTimer.stop() - successCheckTimer.stop() - - // Refresh to get current state - doPoll() - } - - // Manual refresh function (bypasses cooldown) - function forceRefresh() { - // Prevent multiple simultaneous refreshes - if (aurBusy) { - return - } - - // Clear error states when refreshing - updateFailed = false - lastUpdateError = "" - checkFailed = false - lastCheckError = "" - - // Check if we have a cached AUR helper - if (cachedAurHelper !== "") { - // Clear error state when helper is available - if (checkFailed && lastCheckError.includes("No AUR helper found")) { - checkFailed = false - lastCheckError = "" - } - - checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"] - checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()] - - // Force refresh by bypassing cooldown - checkAurUpdatesProcess.running = true - lastPollTime = Date.now() - } else { - // AUR helper detection is still in progress or failed - // Try to detect again if not already in progress - if (!yayCheckProcess.running && !paruCheckProcess.running) { - getAurHelper() - } - Logger.warn("ArchUpdater", "AUR helper detection in progress or failed") - } - } - - // ============================================================================ - // UTILITY PROCESSES - // ============================================================================ - - // Process for checking yay availability - Process { - id: yayCheckProcess - command: ["which", "yay"] - onExited: function (exitCode) { - if (exitCode === 0) { - cachedAurHelper = "yay" - Logger.log("ArchUpdater", "Found yay AUR helper (preferred)") - // Clear error state when helper is found - if (checkFailed && lastCheckError.includes("No AUR helper found")) { - checkFailed = false - lastCheckError = "" - } - // Trigger initial check when helper is found - triggerInitialCheck() - } - } - } - - // Process for checking paru availability - Process { - id: paruCheckProcess - command: ["which", "paru"] - onExited: function (exitCode) { - if (exitCode === 0) { - // Only use paru if yay wasn't found (yay is preferred) - if (cachedAurHelper === "") { - cachedAurHelper = "paru" - Logger.log("ArchUpdater", "Found paru AUR helper") - // Clear error state when helper is found - if (checkFailed && lastCheckError.includes("No AUR helper found")) { - checkFailed = false - lastCheckError = "" - } - // Trigger initial check when helper is found - triggerInitialCheck() - } else { - Logger.log("ArchUpdater", "Found paru but using", cachedAurHelper, "(preferred)") - } - } - } - } - - // Cached AUR helper detection - property string cachedAurHelper: "" - - // Helper function to detect AUR helper - function getAurHelper() { - // Return cached result if available - if (cachedAurHelper !== "") { - return cachedAurHelper - } - - // Check for AUR helpers using Process objects - Logger.log("ArchUpdater", "Detecting AUR helper...") - - // Start the detection processes - yayCheckProcess.running = true - paruCheckProcess.running = true - - // Return empty string to indicate no helper found yet - // The processes will update cachedAurHelper when they complete - return "" - } - - // Helper function to get the correct AUR-only flag for the detected helper - function getAurOnlyFlag() { - if (cachedAurHelper === "yay") { - return "-Qua" - } else if (cachedAurHelper === "paru") { - return "-Qua" // paru uses the same flag but different exit code behavior - } - return "-Qua" // fallback - } - - // Helper function to trigger the initial package check - function triggerInitialCheck() { - // Only trigger if this is the first time (no packages have been checked yet) - if (repoPackages.length === 0 && aurPackages.length === 0 && !aurBusy) { - // Clear any previous error state - checkFailed = false - lastCheckError = "" - - // Wait a bit for the system to be ready before the first check - Qt.callLater(() => { - checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"] - checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()] - checkAurUpdatesProcess.running = true - lastPollTime = Date.now() - }, 1000) - } - } - - // ============================================================================ - // PACKAGE SELECTION FUNCTIONS - // ============================================================================ - function togglePackageSelection(packageName) { - const index = selectedPackages.indexOf(packageName) - if (index > -1) { - selectedPackages.splice(index, 1) - } else { - selectedPackages.push(packageName) - } - selectedPackagesCount = selectedPackages.length - } - - function selectAllPackages() { - selectedPackages = [...repoPackages.map(pkg => pkg.name), ...aurPackages.map(pkg => pkg.name)] - selectedPackagesCount = selectedPackages.length - } - - function deselectAllPackages() { - selectedPackages = [] - selectedPackagesCount = 0 - } - - function isPackageSelected(packageName) { - return selectedPackages.indexOf(packageName) > -1 - } - - // ============================================================================ - // REFRESH FUNCTIONS - // ============================================================================ - - // ============================================================================ - // UTILITY FUNCTIONS - // ============================================================================ - - // Notification helper - function notify(title, body) { - Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]) - } - - // ============================================================================ - // AUTO-POLL TIMER - // ============================================================================ - - // Auto-poll every 15 minutes (respects cooldown) - Timer { - interval: 15 * 60 * 1000 // 15 minutes - repeat: true - running: true - onTriggered: { - if (!updateInProgress && canPoll) { - doPoll() - } - } - } -} diff --git a/Services/AudioService.qml b/Services/AudioService.qml index 9f526ea..8438d03 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -62,7 +62,6 @@ Singleton { function onMutedChanged() { root._muted = (sink?.audio.muted ?? true) Logger.log("AudioService", "OnMuteChanged:", root._muted) - // Toast: audio output mute toggle ToastService.showNotice("Audio Output", root._muted ? "Muted" : "Unmuted") } } @@ -81,7 +80,6 @@ Singleton { function onMutedChanged() { root._inputMuted = (source?.audio.muted ?? true) Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted) - // Toast: microphone mute toggle ToastService.showNotice("Microphone", root._inputMuted ? "Muted" : "Unmuted") } } @@ -105,7 +103,7 @@ Singleton { } } - function setMuted(muted: bool) { + function setOutputMuted(muted: bool) { if (sink?.ready && sink?.audio) { sink.audio.muted = muted } else { diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index f104d07..392b570 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -43,18 +43,17 @@ Singleton { }, "Battery": { "allowUserSettings": true, - "alwaysShowPercentage": false, + "displayMode": "onhover", "warningThreshold": 30 }, "Brightness": { "allowUserSettings": true, - "alwaysShowPercentage": false + "displayMode": "onhover" }, "Clock": { "allowUserSettings": true, - "showDate": false, + "displayFormat": "time-date-short", "use12HourClock": false, - "showSeconds": false, "reverseDayMonth": true }, "CustomButton": { @@ -62,11 +61,13 @@ Singleton { "icon": "heart", "leftClickExec": "", "rightClickExec": "", - "middleClickExec": "" + "middleClickExec": "", + "textCommand": "", + "textIntervalMs": 3000 }, "Microphone": { "allowUserSettings": true, - "alwaysShowPercentage": false + "displayMode": "onhover" }, "NotificationHistory": { "allowUserSettings": true, @@ -81,7 +82,6 @@ Singleton { "allowUserSettings": true, "showCpuUsage": true, "showCpuTemp": true, - "showGpuTemp": false, "showMemoryUsage": true, "showMemoryAsPercent": false, "showNetworkStats": false, @@ -89,7 +89,8 @@ Singleton { }, "Workspace": { "allowUserSettings": true, - "labelMode": "index" + "labelMode": "index", + "hideUnoccupied": false }, "MediaMini": { "allowUserSettings": true, @@ -103,7 +104,11 @@ Singleton { }, "Volume": { "allowUserSettings": true, - "alwaysShowPercentage": false + "displayMode": "onhover" + }, + "KeyboardLayout": { + "allowUserSettings": true, + "displayMode": "onhover" } }) @@ -204,9 +209,9 @@ Singleton { function getNPillDirection(widget) { try { - if (widget.barSection === "leftSection") { + if (widget.section === "left") { return true - } else if (widget.barSection === "rightSection") { + } else if (widget.section === "right") { return false } else { // middle section diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 9bbc55b..1f773eb 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -32,6 +32,16 @@ Singleton { function init() { Logger.log("Bluetooth", "Service initialized") + delaySyncState.running = true + } + + Timer { + id: delaySyncState + interval: 1000 + repeat: false + onTriggered: { + Settings.data.network.bluetoothEnabled = adapter.enabled + } } Timer { @@ -81,8 +91,7 @@ Singleton { var name = (device.name || device.deviceName || "").toLowerCase() var icon = (device.icon || "").toLowerCase() - if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") - || name.includes("headset") || name.includes("arctis")) { + if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") || name.includes("headset") || name.includes("arctis")) { return "bt-device-headphones" } @@ -92,8 +101,7 @@ Singleton { if (icon.includes("keyboard") || name.includes("keyboard")) { return "bt-device-keyboard" } - if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") - || name.includes("samsung")) { + if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung")) { return "bt-device-phone" } if (icon.includes("watch") || name.includes("watch")) { @@ -194,8 +202,7 @@ Singleton { return false } - return device.pairing || device.state === BluetoothDeviceState.Disconnecting - || device.state === BluetoothDeviceState.Connecting + return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting } function connectDeviceWithTrust(device) { diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index d14b166..923c20e 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -84,8 +84,7 @@ Singleton { var ddcModel = ddcModelMatc ? ddcModelMatc.length > 0 : false var model = modelMatch ? modelMatch[1] : "Unknown" var bus = busMatch ? busMatch[1] : "Unknown" - Logger.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", - !ddcModel) + Logger.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel) return { "model": model, "busNum": bus, @@ -261,9 +260,7 @@ Singleton { } else { // Internal backlight - find the first available backlight device and get its info // This now returns: device_path, current_brightness, max_brightness (on separate lines) - initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " - + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " - + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"] + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"] } initProc.running = true } diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml index 8e2df62..d76c8b9 100644 --- a/Services/ClipboardService.qml +++ b/Services/ClipboardService.qml @@ -70,10 +70,7 @@ Singleton { root.cliphistAvailable = false // Show toast notification if feature is enabled but cliphist is missing if (Settings.data.appLauncher.enableClipboardHistory) { - ToastService.showWarning( - "Clipboard History Unavailable", - "The 'cliphist' application is not installed. Please install it to use clipboard history features.", - false, 6000) + ToastService.showWarning("Clipboard History Unavailable", "The 'cliphist' application is not installed. Please install it to use clipboard history features.", false, 6000) } } } diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 7e46e31..615cb82 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -15,6 +15,8 @@ Singleton { property bool isHyprland: false property bool isNiri: false + readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") + // Generic workspace and window data property ListModel workspaces: ListModel {} property var windows: [] @@ -29,47 +31,78 @@ Singleton { signal windowListChanged signal windowTitleChanged + // Debounce timer for updates + property Timer updateTimer: Timer { + interval: 50 // 50ms debounce + repeat: false + onTriggered: { + try { + updateHyprlandWindows() + updateHyprlandWorkspaces() + windowListChanged() + } catch (e) { + Logger.error("Compositor", "Error in debounced update:", e) + } + } + } + // Compositor detection Component.onCompleted: { detectCompositor() } // Hyprland connections - Connections { - target: Hyprland.workspaces - enabled: isHyprland - function onValuesChanged() { - updateHyprlandWorkspaces() - workspaceChanged() - } - } + Loader { + active: isHyprland + sourceComponent: Component { + Item { + Connections { + target: Hyprland.workspaces + enabled: isHyprland + function onValuesChanged() { + try { + updateHyprlandWorkspaces() + workspaceChanged() + } catch (e) { + Logger.error("Compositor", "Error in workspaces onValuesChanged:", e) + } + } + } - Connections { - target: Hyprland.toplevels - enabled: isHyprland - function onValuesChanged() { - updateHyprlandWindows() - // Keep workspace occupancy up to date when windows change - updateHyprlandWorkspaces() - windowListChanged() - } - } + Connections { + target: Hyprland.toplevels + enabled: isHyprland + function onValuesChanged() { + try { + // Use debounced update to prevent too frequent calls + updateTimer.restart() + } catch (e) { + Logger.error("Compositor", "Error in toplevels onValuesChanged:", e) + } + } + } - Connections { - target: Hyprland - enabled: isHyprland - function onRawEvent(event) { - updateHyprlandWorkspaces() - workspaceChanged() - updateHyprlandWindows() - windowListChanged() + Connections { + target: Hyprland + enabled: isHyprland + function onRawEvent(event) { + try { + updateHyprlandWorkspaces() + workspaceChanged() + updateTimer.restart() + } catch (e) { + Logger.error("Compositor", "Error in rawEvent:", e) + } + } + } + } } } function detectCompositor() { try { // Try Hyprland first - if (Hyprland.eventSocketPath) { + if (hyprlandSignature && hyprlandSignature.length > 0) { compositorType = "hyprland" isHyprland = true isNiri = false @@ -121,34 +154,52 @@ Singleton { workspaces.clear() try { const hlWorkspaces = Hyprland.workspaces.values + // Determine occupied workspace ids from current toplevels const occupiedIds = {} try { const hlToplevels = Hyprland.toplevels.values for (var t = 0; t < hlToplevels.length; t++) { - const tws = hlToplevels[t].workspace?.id - if (tws !== undefined && tws !== null) { - occupiedIds[tws] = true + const toplevel = hlToplevels[t] + if (toplevel) { + try { + const tws = toplevel.workspace?.id + if (tws !== undefined && tws !== null) { + occupiedIds[tws] = true + } + } catch (toplevelError) { + // Ignore errors from individual toplevels + continue + } } } } catch (e2) { // ignore occupancy errors; fall back to false } + for (var i = 0; i < hlWorkspaces.length; i++) { const ws = hlWorkspaces[i] - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - workspaces.append({ - "id": i, - "idx": ws.id, - "name": ws.name || "", - "output": ws.monitor?.name || "", - "isActive": ws.active === true, - "isFocused": ws.focused === true, - "isUrgent": ws.urgent === true, - "isOccupied": occupiedIds[ws.id] === true - }) + if (!ws) + continue + + try { + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + workspaces.append({ + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true, + "isOccupied": occupiedIds[ws.id] === true + }) + } + } catch (workspaceError) { + Logger.warn("Compositor", "Error processing workspace at index", i, ":", workspaceError) + continue } } } catch (e) { @@ -167,39 +218,84 @@ Singleton { for (var i = 0; i < hlToplevels.length; i++) { const toplevel = hlToplevels[i] - // Try to get appId from various sources - let appId = "" - - // First try the direct properties - if (toplevel.class) { - appId = toplevel.class - } else if (toplevel.initialClass) { - appId = toplevel.initialClass - } else if (toplevel.appId) { - appId = toplevel.appId + // Skip if toplevel is null or invalid + if (!toplevel) { + continue } - // If still no appId, try to get it from the lastIpcObject - if (!appId && toplevel.lastIpcObject) { + try { + // Try to get appId from various sources with proper null checks + let appId = "" + + // First try the direct properties with null/undefined checks try { - const ipcData = toplevel.lastIpcObject - // Try different possible property names for the application identifier - appId = ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "" - } catch (e) { + if (toplevel.class !== undefined && toplevel.class !== null) { + appId = String(toplevel.class) + } else if (toplevel.initialClass !== undefined && toplevel.initialClass !== null) { + appId = String(toplevel.initialClass) + } else if (toplevel.appId !== undefined && toplevel.appId !== null) { + appId = String(toplevel.appId) + } + } catch (propertyError) { - // Ignore errors when accessing lastIpcObject + // Ignore property access errors and continue with empty appId } - } - windowsList.push({ - "id": (toplevel.address !== undefined - && toplevel.address !== null) ? String(toplevel.address) : "", - "title": (toplevel.title !== undefined && toplevel.title !== null) ? String( - toplevel.title) : "", - "appId": (appId !== undefined && appId !== null) ? String(appId) : "", - "workspaceId": toplevel.workspace?.id || null, - "isFocused": toplevel.activated === true - }) + // If still no appId, try to get it from the lastIpcObject + if (!appId) { + try { + const ipcData = toplevel.lastIpcObject + if (ipcData) { + appId = String(ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "") + } + } catch (ipcError) { + + // Ignore errors when accessing lastIpcObject + } + } + + // Safely get other properties with fallbacks + let windowId = "" + let windowTitle = "" + let workspaceId = null + let isActivated = false + + try { + windowId = (toplevel.address !== undefined && toplevel.address !== null) ? String(toplevel.address) : "" + } catch (e) { + windowId = "" + } + + try { + windowTitle = (toplevel.title !== undefined && toplevel.title !== null) ? String(toplevel.title) : "" + } catch (e) { + windowTitle = "" + } + + try { + workspaceId = toplevel.workspace?.id || null + } catch (e) { + workspaceId = null + } + + try { + isActivated = toplevel.activated === true + } catch (e) { + isActivated = false + } + + windowsList.push({ + "id": windowId, + "title": windowTitle, + "appId": appId, + "workspaceId": workspaceId, + "isFocused": isActivated + }) + } catch (toplevelError) { + // Log the error but continue processing other toplevels + Logger.warn("Compositor", "Error processing toplevel at index", i, ":", toplevelError) + continue + } } windows = windowsList @@ -217,6 +313,7 @@ Singleton { activeWindowChanged() } catch (e) { Logger.error("Compositor", "Error updating Hyprland windows:", e) + // Don't crash, just keep the previous windows list } } @@ -281,7 +378,7 @@ Singleton { if (a.output !== b.output) { return a.output.localeCompare(b.output) } - return a.id - b.id + return a.idx - b.idx }) // Update the workspaces ListModel diff --git a/Services/FontService.qml b/Services/FontService.qml index ebf03e3..d63de0d 100644 --- a/Services/FontService.qml +++ b/Services/FontService.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import QtQuick.Controls import Quickshell +import Quickshell.Io import qs.Commons Singleton { @@ -12,11 +13,17 @@ Singleton { property ListModel monospaceFonts: ListModel {} property ListModel displayFonts: ListModel {} property bool fontsLoaded: false + property var fontconfigMonospaceFonts: [] // ------------------------------------------- function init() { Logger.log("Font", "Service started") - loadSystemFonts() + loadFontconfigMonospaceFonts() + } + + function loadFontconfigMonospaceFonts() { + fontconfigProcess.command = ["fc-list", ":mono", "family"] + fontconfigProcess.running = true } function loadSystemFonts() { @@ -57,48 +64,42 @@ Singleton { sortModel(displayFonts) if (monospaceFonts.count === 0) { - Logger.log("Font", "No monospace fonts detected, adding fallbacks") - addFallbackFonts( - monospaceFonts, - ["DejaVu Sans Mono", "Liberation Mono", "Courier New", "Courier", "Monaco", "Consolas", "Lucida Console", "Monaco", "Andale Mono"]) + addFallbackFonts(monospaceFonts, ["DejaVu Sans Mono"]) } if (displayFonts.count === 0) { - Logger.log("Font", "No display fonts detected, adding fallbacks") - addFallbackFonts( - displayFonts, - ["Inter", "Roboto", "Open Sans", "Arial", "Helvetica", "Verdana", "Segoe UI", "SF Pro Display", "Ubuntu", "Noto Sans"]) + addFallbackFonts(displayFonts, ["Inter", "Roboto", "DejaVu Sans"]) } fontsLoaded = true - Logger.log("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", - displayFonts.count, "display") + Logger.log("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display") } function isMonospaceFont(fontName) { - var patterns = ["mono", "monospace", "fixed", "console", "terminal", "typewriter", "courier", "dejavu", "liberation", "source code", "fira code", "jetbrains", "cascadia", "hack", "inconsolata", "roboto mono", "ubuntu mono", "menlo", "consolas", "monaco", "andale mono"] - var lowerFontName = fontName.toLowerCase() - - for (var i = 0; i < patterns.length; i++) { - if (lowerFontName.includes(patterns[i])) - return true + // First, check if fontconfig detected this as monospace + if (fontconfigMonospaceFonts.indexOf(fontName) !== -1) { + return true } - var commonFonts = ["DejaVu Sans Mono", "Liberation Mono", "Source Code Pro", "Fira Code", "JetBrains Mono", "Cascadia Code", "Hack", "Inconsolata", "Roboto Mono", "Ubuntu Mono", "Menlo", "Consolas", "Monaco", "Andale Mono", "Courier New", "Courier", "Lucida Console", "Monaco", "MS Gothic", "MS Mincho"] - return commonFonts.includes(fontName) + // Minimal fallback: only check for basic monospace patterns + var lowerFontName = fontName.toLowerCase() + if (lowerFontName.includes("mono") || lowerFontName.includes("monospace")) { + return true + } + + return false } function isDisplayFont(fontName) { - var patterns = ["display", "headline", "title", "hero", "showcase", "brand", "inter", "roboto", "open sans", "lato", "montserrat", "poppins", "raleway", "nunito", "source sans", "ubuntu", "noto sans", "work sans", "dm sans", "manrope", "plus jakarta", "figtree"] + // Minimal fallback: only check for basic display patterns var lowerFontName = fontName.toLowerCase() - - for (var i = 0; i < patterns.length; i++) { - if (lowerFontName.includes(patterns[i])) - return true + if (lowerFontName.includes("display") || lowerFontName.includes("headline") || lowerFontName.includes("title")) { + return true } - var commonFonts = ["Inter", "Roboto", "Open Sans", "Lato", "Montserrat", "Poppins", "Raleway", "Nunito", "Source Sans Pro", "Ubuntu", "Noto Sans", "Work Sans", "DM Sans", "Manrope", "Plus Jakarta Sans", "Figtree", "SF Pro Display", "Segoe UI", "Arial", "Helvetica", "Verdana"] - return commonFonts.includes(fontName) + // Essential fallback fonts only + var essentialFonts = ["Inter", "Roboto", "DejaVu Sans"] + return essentialFonts.includes(fontName) } function sortModel(model) { @@ -158,4 +159,36 @@ Singleton { return results } + + // Process for fontconfig commands + Process { + id: fontconfigProcess + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (this.text !== "") { + var lines = this.text.split('\n') + fontconfigMonospaceFonts = [] + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + if (line && line !== "") { + if (fontconfigMonospaceFonts.indexOf(line) === -1) { + fontconfigMonospaceFonts.push(line) + } + } + } + } + loadSystemFonts() + } + } + + onExited: function (exitCode, exitStatus) { + if (exitCode !== 0) { + fontconfigMonospaceFonts = [] + } + loadSystemFonts() + } + } } diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 99dc283..1c5583f 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -149,8 +149,7 @@ Singleton { Logger.log("GitHub", "Raw contributors response length:", response ? response.length : 0) if (response && response.trim()) { const data = JSON.parse(response) - Logger.log("GitHub", "Parsed contributors data type:", typeof data, "length:", - Array.isArray(data) ? data.length : "not array") + Logger.log("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array") root.data.contributors = data || [] root.contributors = root.data.contributors Logger.log("GitHub", "Contributors fetched from GitHub:", root.contributors.length) diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml index 3a9aedb..f0c8420 100644 --- a/Services/IdleInhibitorService.qml +++ b/Services/IdleInhibitorService.qml @@ -131,8 +131,7 @@ Singleton { // Systemd inhibition using systemd-inhibit function startSystemdInhibition() { - inhibitorProcess.command = ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why=" - + reason, "--mode=block", "sleep", "infinity"] + inhibitorProcess.command = ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why=" + reason, "--mode=block", "sleep", "infinity"] inhibitorProcess.running = true } diff --git a/Services/KeyboardLayoutService.qml b/Services/KeyboardLayoutService.qml index c66e5c9..c10f697 100644 --- a/Services/KeyboardLayoutService.qml +++ b/Services/KeyboardLayoutService.qml @@ -9,7 +9,6 @@ import qs.Services Singleton { id: root - property string currentLayout: "Unknown" property int updateInterval: 1000 // Update every second @@ -34,7 +33,7 @@ Singleton { try { const data = JSON.parse(text) const layoutName = data.names[data.current_idx] - root.currentLayout = mapLayoutNameToCode(layoutName) + root.currentLayout = extractLayoutCode(layoutName) } catch (e) { root.currentLayout = "Unknown" } @@ -54,7 +53,7 @@ Singleton { // Find the main keyboard and get its active keymap const mainKeyboard = data.keyboards.find(kb => kb.main === true) if (mainKeyboard && mainKeyboard.active_keymap) { - root.currentLayout = mapLayoutNameToCode(mainKeyboard.active_keymap) + root.currentLayout = extractLayoutCode(mainKeyboard.active_keymap) } else { root.currentLayout = "Unknown" } @@ -65,34 +64,141 @@ Singleton { } } - // Layout name to ISO code mapping - property var layoutMap: { - "German": "de", - "English (US)": "us", - "English (UK)": "gb", - "French": "fr", - "Spanish": "es", - "Italian": "it", - "Portuguese (Brazil)": "br", - "Portuguese": "pt", - "Russian": "ru", - "Polish": "pl", - "Swedish": "se", - "Norwegian": "no", - "Danish": "dk", - "Finnish": "fi", - "Hungarian": "hu", - "Turkish": "tr", - "Czech": "cz", - "Slovak": "sk", - "Japanese": "jp", - "Korean": "kr", - "Chinese": "cn" + // Process for X11 systems using setxkbmap + Process { + id: x11LayoutProcess + running: false + command: ["setxkbmap", "-query"] + stdout: StdioCollector { + onStreamFinished: { + try { + const lines = text.split('\n') + for (const line of lines) { + if (line.startsWith('layout:')) { + const layout = line.split(':')[1].trim() + root.currentLayout = layout + return + } + } + root.currentLayout = "Unknown" + } catch (e) { + root.currentLayout = "Unknown" + } + } + } } - // Map layout names to ISO codes - function mapLayoutNameToCode(layoutName) { - return layoutMap[layoutName] || layoutName // fallback to raw name if not found + // Process for general Wayland using localectl (systemd) + Process { + id: localectlProcess + running: false + command: ["localectl", "status"] + stdout: StdioCollector { + onStreamFinished: { + try { + const lines = text.split('\n') + for (const line of lines) { + if (line.includes('X11 Layout:')) { + const layout = line.split(':')[1].trim() + if (layout && layout !== "n/a") { + root.currentLayout = layout + return + } + } + if (line.includes('VC Keymap:')) { + const keymap = line.split(':')[1].trim() + if (keymap && keymap !== "n/a") { + root.currentLayout = extractLayoutCode(keymap) + return + } + } + } + root.currentLayout = "Unknown" + } catch (e) { + root.currentLayout = "Unknown" + } + } + } + } + + // Process for generic keyboard layout detection using gsettings (GNOME-based) + Process { + id: gsettingsProcess + running: false + command: ["gsettings", "get", "org.gnome.desktop.input-sources", "current"] + stdout: StdioCollector { + onStreamFinished: { + try { + const currentIndex = parseInt(text.trim()) + gsettingsSourcesProcess.running = true + } catch (e) { + fallbackToLocalectl() + } + } + } + } + + Process { + id: gsettingsSourcesProcess + running: false + command: ["gsettings", "get", "org.gnome.desktop.input-sources", "sources"] + stdout: StdioCollector { + onStreamFinished: { + try { + // Parse the sources array and extract layout codes + const sourcesText = text.trim() + const matches = sourcesText.match(/\('xkb', '([^']+)'\)/g) + if (matches && matches.length > 0) { + // Get the first layout as default + const layoutMatch = matches[0].match(/\('xkb', '([^']+)'\)/) + if (layoutMatch) { + root.currentLayout = layoutMatch[1].split('+')[0] // Take first part before any variants + } + } else { + fallbackToLocalectl() + } + } catch (e) { + fallbackToLocalectl() + } + } + } + } + + function fallbackToLocalectl() { + localectlProcess.running = true + } + + // Extract layout code from various format strings using Commons data + function extractLayoutCode(layoutString) { + if (!layoutString) + return "Unknown" + + const str = layoutString.toLowerCase() + + // If it's already a short code (2-3 chars), return as-is + if (/^[a-z]{2,3}(\+.*)?$/.test(str)) { + return str.split('+')[0] + } + + // Extract from parentheses like "English (US)" + const parenMatch = str.match(/\(([a-z]{2,3})\)/) + if (parenMatch) { + return parenMatch[1] + } + + // Check for exact matches or partial matches in language map from Commons + const entries = Object.entries(KeyboardLayout.languageMap) + for (var i = 0; i < entries.length; i++) { + const lang = entries[i][0] + const code = entries[i][1] + if (str.includes(lang)) { + return code + } + } + + // If nothing matches, try first 2-3 characters if they look like a code + const codeMatch = str.match(/^([a-z]{2,3})/) + return codeMatch ? codeMatch[1] : "unknown" } Component.onCompleted: { @@ -101,12 +207,27 @@ Singleton { } function updateLayout() { + // Try compositor-specific methods first if (CompositorService.isHyprland) { hyprlandLayoutProcess.running = true } else if (CompositorService.isNiri) { niriLayoutProcess.running = true } else { - currentLayout = "Unknown" + // Try detection methods in order of preference + if (Qt.platform.os === "linux") { + // Check if we're in X11 or Wayland + const sessionType = Qt.application.arguments.find(arg => arg.includes("QT_QPA_PLATFORM")) || process.env.XDG_SESSION_TYPE + + if (sessionType && sessionType.includes("xcb") || process.env.DISPLAY) { + // X11 system + x11LayoutProcess.running = true + } else { + // Wayland or unknown - try gsettings first, then localectl + gsettingsProcess.running = true + } + } else { + currentLayout = "Unknown" + } } } } diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 87de8fd..79b4aa5 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -119,9 +119,7 @@ Singleton { return } - if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") - || (adapter.longitude === "") || (adapter.name !== Settings.data.location.name) - || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency)) { + if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") || (adapter.longitude === "") || (adapter.name !== Settings.data.location.name) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -161,8 +159,7 @@ Singleton { // -------------------------------- function _geocodeLocation(locationName, callback, errorCallback) { Logger.log("Location", "Geocoding location name") - var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent( - locationName) + "&language=en&format=json" + var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json" var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { @@ -189,8 +186,7 @@ Singleton { // -------------------------------- function _fetchWeather(latitude, longitude, errorCallback) { Logger.log("Location", "Fetching weather from api.open-meteo.com") - var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude - + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" + var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { diff --git a/Services/MatugenService.qml b/Services/MatugenService.qml index ef80b7d..3d090c1 100644 --- a/Services/MatugenService.qml +++ b/Services/MatugenService.qml @@ -61,10 +61,8 @@ Singleton { var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''") // Build the main script - var script = "cat > '" + pathEsc + "' << 'EOF'\n" + content + "EOF\n" + "for d in '" + extraRepo + "' '" + extraUser - + "'; do\n" + " if [ -d \"$d\" ]; then\n" - + " for f in \"$d\"/*.toml; do\n" + " [ -f \"$f\" ] && { echo; echo \"# extra: $f\"; cat \"$f\"; } >> '" - + pathEsc + "'\n" + " done\n" + " fi\n" + "done\n" + "matugen image '" + wp + "' --config '" + pathEsc + "' --mode " + mode + var script = "cat > '" + pathEsc + "' << 'EOF'\n" + content + "EOF\n" + "for d in '" + extraRepo + "' '" + extraUser + "'; do\n" + " if [ -d \"$d\" ]; then\n" + " for f in \"$d\"/*.toml; do\n" + " [ -f \"$f\" ] && { echo; echo \"# extra: $f\"; cat \"$f\"; } >> '" + pathEsc + "'\n" + " done\n" + " fi\n" + "done\n" + "matugen image '" + + wp + "' --config '" + pathEsc + "' --mode " + mode // Add user config execution if enabled if (Settings.data.matugen.enableUserTemplates) { diff --git a/Services/MediaService.qml b/Services/MediaService.qml index a37e85b..0eae65e 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -13,8 +13,7 @@ Singleton { property real currentPosition: 0 property bool isSeeking: false property int selectedPlayerIndex: 0 - property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing - || currentPlayer.isPlaying) : false + property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing || currentPlayer.isPlaying) : false property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" @@ -40,8 +39,7 @@ Singleton { let controllablePlayers = [] // Apply blacklist and controllable filter - const blacklist = (Settings.data.audio - && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : [] + const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : [] for (var i = 0; i < allPlayers.length; i++) { let player = allPlayers[i] if (!player) @@ -52,8 +50,7 @@ Singleton { const idKey = identity.toLowerCase() const match = blacklist.find(b => { const s = String(b || "").toLowerCase() - return s && (idKey.includes(s) || busName.toLowerCase().includes(s) - || desktop.toLowerCase().includes(s)) + return s && (idKey.includes(s) || busName.toLowerCase().includes(s) || desktop.toLowerCase().includes(s)) }) if (match) continue @@ -159,12 +156,10 @@ Singleton { Timer { id: positionTimer interval: 1000 - running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0 - && currentPlayer.playbackState === MprisPlaybackState.Playing + running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0 && currentPlayer.playbackState === MprisPlaybackState.Playing repeat: true onTriggered: { - if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying - && currentPlayer.playbackState === MprisPlaybackState.Playing) { + if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { currentPosition = currentPlayer.position } else { running = false diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 31f9a9e..2287fc4 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -491,7 +491,8 @@ Singleton { root.connecting = false root.connectingTo = "" - Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) + Logger.log("Network", `Connected to network: '${connectProcess.ssid}'`) + ToastService.showNotice("Wi-Fi", `Connected to '${connectProcess.ssid}'`) // Still do a scan to get accurate signal and security info delayedScanTimer.interval = 5000 @@ -531,7 +532,8 @@ Singleton { stdout: StdioCollector { onStreamFinished: { - Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`) + Logger.log("Network", `Disconnected from network: '${disconnectProcess.ssid}'`) + ToastService.showNotice("Wi-Fi", `Disconnected from '${disconnectProcess.ssid}'`) // Immediately update UI on successful disconnect root.updateNetworkStatus(disconnectProcess.ssid, false) diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index bf66182..98ad57b 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -57,8 +57,7 @@ Singleton { property int maxHistory: 100 // Cached history file path - property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") - || (Settings.cacheDir + "notifications.json") + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") // Persisted storage for history property FileView historyFileView: FileView { @@ -88,9 +87,26 @@ Singleton { // Maximum visible notifications property int maxVisible: 5 + // Function to get duration based on urgency + function getDurationForUrgency(urgency) { + switch (urgency) { + case 0: + // Low urgency + return (Settings.data.notifications.lowUrgencyDuration || 3) * 1000 + case 1: + // Normal urgency + return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000 + case 2: + // Critical urgency + return (Settings.data.notifications.criticalUrgencyDuration || 15) * 1000 + default: + return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000 + } + } + // Auto-hide timer property Timer hideTimer: Timer { - interval: 8000 // 8 seconds - longer display time + interval: 1000 // Check every second repeat: true running: notificationModel.count > 0 @@ -99,11 +115,26 @@ Singleton { return } - // Remove the oldest notification (last in the list) - let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification - if (oldestNotification) { - // Trigger animation signal instead of direct dismiss - animateAndRemove(oldestNotification, notificationModel.count - 1) + // Check each notification for expiration + for (var i = notificationModel.count - 1; i >= 0; i--) { + let notificationData = notificationModel.get(i) + if (notificationData && notificationData.rawNotification) { + let notification = notificationData.rawNotification + let urgency = notificationData.urgency + let timestamp = notificationData.timestamp + + // Calculate if this notification should be removed + let duration = getDurationForUrgency(urgency) + let now = new Date() + let elapsed = now.getTime() - timestamp.getTime() + + if (elapsed >= duration) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(notification, i) + break + // Only remove one notification per check to avoid conflicts + } + } } } } @@ -280,9 +311,7 @@ Singleton { "appIcon": n.appIcon, "urgency": n.urgency, "timestamp"// Always persist in milliseconds - : (n.timestamp instanceof Date) ? n.timestamp.getTime( - ) : (typeof n.timestamp === "number" - && n.timestamp < 1e12 ? n.timestamp * 1000 : n.timestamp) + : (n.timestamp instanceof Date) ? n.timestamp.getTime() : (typeof n.timestamp === "number" && n.timestamp < 1e12 ? n.timestamp * 1000 : n.timestamp) }) } historyAdapter.history = arr diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml new file mode 100644 index 0000000..f8e4fa9 --- /dev/null +++ b/Services/ProgramCheckerService.qml @@ -0,0 +1,118 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Service to check if various programs are available on the system +Singleton { + id: root + + // Program availability properties + property bool matugenAvailable: false + property bool pywalfoxAvailable: false + property bool kittyAvailable: false + property bool ghosttyAvailable: false + property bool footAvailable: false + property bool fuzzelAvailable: false + property bool vesktopAvailable: false + property bool gpuScreenRecorderAvailable: false + + // Signal emitted when all checks are complete + signal checksCompleted + + // Programs to check - maps property names to commands + readonly property var programsToCheck: ({ + "matugenAvailable": ["which", "matugen"], + "pywalfoxAvailable": ["which", "pywalfox"], + "kittyAvailable": ["which", "kitty"], + "ghosttyAvailable": ["which", "ghostty"], + "footAvailable": ["which", "foot"], + "fuzzelAvailable": ["which", "fuzzel"], + "vesktopAvailable": ["which", "vesktop"], + "gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"] + }) + + // Internal tracking + property int completedChecks: 0 + property int totalChecks: Object.keys(programsToCheck).length + + // Single reusable Process object + Process { + id: checker + running: false + + property string currentProperty: "" + + onExited: function (exitCode) { + // Set the availability property + root[currentProperty] = (exitCode === 0) + + // Stop the process to free resources + running = false + + // Track completion + root.completedChecks++ + + // Check next program or emit completion signal + if (root.completedChecks >= root.totalChecks) { + root.checksCompleted() + } else { + root.checkNextProgram() + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Queue of programs to check + property var checkQueue: [] + property int currentCheckIndex: 0 + + // Function to check the next program in the queue + function checkNextProgram() { + if (currentCheckIndex >= checkQueue.length) + return + + var propertyName = checkQueue[currentCheckIndex] + var command = programsToCheck[propertyName] + + checker.currentProperty = propertyName + checker.command = command + checker.running = true + + currentCheckIndex++ + } + + // Function to run all program checks + function checkAllPrograms() { + // Reset state + completedChecks = 0 + currentCheckIndex = 0 + checkQueue = Object.keys(programsToCheck) + + // Start first check + if (checkQueue.length > 0) { + checkNextProgram() + } + } + + // Function to check a specific program + function checkProgram(programProperty) { + if (!programsToCheck.hasOwnProperty(programProperty)) { + Logger.warn("ProgramChecker", "Unknown program property:", programProperty) + return + } + + checker.currentProperty = programProperty + checker.command = programsToCheck[programProperty] + checker.running = true + } + + // Initialize checks when service is created + Component.onCompleted: { + checkAllPrograms() + } +} diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 7642542..8a5512b 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -13,16 +13,13 @@ Singleton { property bool isRecording: false property bool isPending: false property string outputPath: "" - property bool isAvailable: false + property bool isAvailable: ProgramCheckerService.gpuScreenRecorderAvailable - Component.onCompleted: { - checkAvailability() - } - - function checkAvailability() { - // Detect native or Flatpak gpu-screen-recorder - availabilityCheckProcess.command = ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"] - availabilityCheckProcess.running = true + // Update availability when ProgramCheckerService completes its checks + Connections { + target: ProgramCheckerService + function onChecksCompleted() {// Availability is now automatically updated via property binding + } } // Start or Stop recording @@ -74,8 +71,7 @@ Singleton { return } - Quickshell.execDetached( - ["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"]) + Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"]) isRecording = false isPending = false @@ -102,18 +98,6 @@ Singleton { } } - // Availability check process - Process { - id: availabilityCheckProcess - command: ["sh", "-c", "true"] - onExited: function (exitCode, exitStatus) { - // exitCode 0 means available, non-zero means unavailable - root.isAvailable = (exitCode === 0) - } - stdout: StdioCollector {} - stderr: StdioCollector {} - } - Timer { id: pendingTimer interval: 2000 // Wait 2 seconds to see if process stays alive @@ -152,8 +136,7 @@ Singleton { running: false repeat: false onTriggered: { - Quickshell.execDetached( - ["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || pkill -9 -f 'com.dec05eba.gpu_screen_recorder' 2>/dev/null || true"]) + Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || pkill -9 -f 'com.dec05eba.gpu_screen_recorder' 2>/dev/null || true"]) } } } diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index b796d85..a8d37d1 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -12,7 +12,6 @@ Singleton { // Public values property real cpuUsage: 0 property real cpuTemp: 0 - property real gpuTemp: 0 property real memGb: 0 property real memPercent: 0 property real diskPercent: 0 @@ -36,12 +35,6 @@ Singleton { readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] property string cpuTempSensorName: "" property string cpuTempHwmonPath: "" - // Gpu temperature (simple hwmon read if available) - readonly property var supportedTempGpuSensorNames: ["amdgpu", "nvidia", "radeon"] - property string gpuTempSensorName: "" - property string gpuTempHwmonPath: "" - property bool gpuIsDedicated: false - property string _gpuPendingAmdPath: "" // For Intel coretemp averaging of all cores/sensors property var intelTempValues: [] property int intelTempFilesChecked: 0 @@ -73,7 +66,6 @@ Singleton { dfProcess.running = true updateCpuTemperature() - updateGpuTemperature() } } @@ -192,108 +184,6 @@ Singleton { } } - // -------------------------------------------- - // -------------------------------------------- - // ---- GPU temperature detection (hwmon) - FileView { - id: gpuTempNameReader - property int currentIndex: 0 - printErrors: false - - function checkNext() { - if (currentIndex >= 16) { - // Check up to hwmon10 - Logger.warn("SystemStat", "No supported GPU temperature sensor found") - return - } - - gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name` - gpuTempNameReader.reload() - } - - Component.onCompleted: checkNext() - - onLoaded: { - const name = text().trim() - if (root.supportedTempGpuSensorNames.includes(name)) { - const hwPath = `/sys/class/hwmon/hwmon${currentIndex}` - if (name === "nvidia") { - // Treat NVIDIA as dedicated by default - root.gpuTempSensorName = name - root.gpuTempHwmonPath = hwPath - root.gpuIsDedicated = true - Logger.log("SystemStat", `Selected NVIDIA GPU thermal sensor at ${root.gpuTempHwmonPath}`) - } else if (name === "amdgpu") { - // Probe VRAM to distinguish dGPU vs iGPU - root._gpuPendingAmdPath = hwPath - vramReader.requestCheck(hwPath) - } else if (!root.gpuTempHwmonPath) { - // Fallback to first supported sensor (e.g., radeon) - root.gpuTempSensorName = name - root.gpuTempHwmonPath = hwPath - Logger.log("SystemStat", `Selected GPU thermal sensor at ${root.gpuTempHwmonPath}`) - } - } else { - currentIndex++ - Qt.callLater(() => { - checkNext() - }) - } - } - - onLoadFailed: function (error) { - currentIndex++ - Qt.callLater(() => { - checkNext() - }) - } - } - - // Reader to detect AMD dGPU by checking VRAM presence - FileView { - id: vramReader - property string targetHwmonPath: "" - function requestCheck(hwPath) { - targetHwmonPath = hwPath - vramReader.path = `${hwPath}/device/mem_info_vram_total` - vramReader.reload() - } - printErrors: false - onLoaded: { - const val = parseInt(text().trim()) - // If VRAM present (>0), prefer this as dGPU - if (!isNaN(val) && val > 0) { - root.gpuTempSensorName = "amdgpu" - root.gpuTempHwmonPath = targetHwmonPath - root.gpuIsDedicated = true - Logger.log("SystemStat", - `Selected AMD dGPU (VRAM=${Math.round(val / (1024 * 1024 * 1024))}GB) at ${root.gpuTempHwmonPath}`) - } else if (!root.gpuTempHwmonPath) { - // Use as fallback iGPU if nothing selected yet - root.gpuTempSensorName = "amdgpu" - root.gpuTempHwmonPath = targetHwmonPath - root.gpuIsDedicated = false - Logger.log("SystemStat", `Selected AMD GPU (no VRAM) at ${root.gpuTempHwmonPath}`) - } - // Continue scanning other hwmon entries - gpuTempNameReader.currentIndex++ - Qt.callLater(() => { - gpuTempNameReader.checkNext() - }) - } - onLoadFailed: function (error) { - // If failed to read VRAM, consider as fallback if none selected - if (!root.gpuTempHwmonPath) { - root.gpuTempSensorName = "amdgpu" - root.gpuTempHwmonPath = targetHwmonPath - } - gpuTempNameReader.currentIndex++ - Qt.callLater(() => { - gpuTempNameReader.checkNext() - }) - } - } - // ------------------------------------------------------- // ------------------------------------------------------- // Parse memory info from /proc/meminfo @@ -459,26 +349,6 @@ Singleton { } } - // ------------------------------------------------------- - // Function to start/refresh the GPU temperature - function updateGpuTemperature() { - if (!root.gpuTempHwmonPath) - return - gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input` - gpuTempReader.reload() - } - - FileView { - id: gpuTempReader - printErrors: false - onLoaded: { - const data = parseInt(text().trim()) - if (!isNaN(data)) { - root.gpuTemp = Math.round(data / 1000.0) - } - } - } - // ------------------------------------------------------- // Function to check next Intel temperature sensor function checkNextIntelTemp() { diff --git a/Services/ToastService.qml b/Services/ToastService.qml index ee9fb24..df4cc25 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -2,247 +2,23 @@ pragma Singleton import QtQuick import Quickshell -import Quickshell.Io -import qs.Commons Singleton { id: root - // Queue of pending toast messages - property var messageQueue: [] - property bool isShowingToast: false + // Simple signal-based notification system + signal notify(string message, string description, string type, int duration) - // Reference to all toast instances (set by ToastOverlay) - property var allToasts: [] - - // Properties for command checking - property var commandCheckCallback: null - property string commandCheckSuccessMessage: "" - property string commandCheckFailMessage: "" - - // Properties for command running - property var commandRunCallback: null - property string commandRunSuccessMessage: "" - property string commandRunFailMessage: "" - - // Properties for delayed toast - property string delayedToastMessage: "" - property string delayedToastType: "notice" - - // Process for command checking - Process { - id: commandCheckProcess - command: ["which", "test"] - onExited: function (exitCode) { - if (exitCode === 0) { - showNotice(commandCheckSuccessMessage) - if (commandCheckCallback) - commandCheckCallback() - } else { - showWarning(commandCheckFailMessage) - } - } - stdout: StdioCollector {} - stderr: StdioCollector {} + // Convenience methods + function showNotice(message, description = "", duration = 3000) { + notify(message, description, "notice", duration) } - // Process for command running - Process { - id: commandRunProcess - command: ["echo", "test"] - onExited: function (exitCode) { - if (exitCode === 0) { - showNotice(commandRunSuccessMessage) - if (commandRunCallback) - commandRunCallback() - } else { - showWarning(commandRunFailMessage) - } - } - stdout: StdioCollector {} - stderr: StdioCollector {} + function showWarning(message, description = "", duration = 4000) { + notify(message, description, "warning", duration) } - // Timer for delayed toast - Timer { - id: delayedToastTimer - interval: 1000 - repeat: false - onTriggered: { - showToast(delayedToastMessage, delayedToastType) - } - } - - // Methods to show different types of messages - function showNotice(label, description = "", persistent = false, duration = 3000) { - showToast(label, description, "notice", persistent, duration) - } - - function showWarning(label, description = "", persistent = false, duration = 4000) { - showToast(label, description, "warning", persistent, duration) - } - - // Utility function to check if a command exists and show appropriate toast - function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) { - // Store callback for use in the process - commandCheckCallback = onSuccess - commandCheckSuccessMessage = successMessage - commandCheckFailMessage = failMessage - - // Start the command check process - commandCheckProcess.command = ["which", command] - commandCheckProcess.running = true - } - - // Simple function to show a random toast (useful for testing or fun messages) - function showRandomToast() { - var messages = [{ - "type": "notice", - "text": "Everything is working smoothly!" - }, { - "type": "notice", - "text": "Noctalia is looking great today!" - }, { - "type": "notice", - "text": "Your desktop setup is amazing!" - }, { - "type": "warning", - "text": "Don't forget to take a break!" - }, { - "type": "notice", - "text": "Configuration saved successfully!" - }, { - "type": "warning", - "text": "Remember to backup your settings!" - }] - - var randomMessage = messages[Math.floor(Math.random() * messages.length)] - showToast(randomMessage.text, randomMessage.type) - } - - // Convenience function for quick notifications - function quickNotice(message) { - showNotice(message, false, 2000) // Short duration - } - - function quickWarning(message) { - showWarning(message, false, 3000) // Medium duration - } - - // Generic command runner with toast feedback - function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) { - // Store callback for use in the process - commandRunCallback = onSuccess - commandRunSuccessMessage = successMessage - commandRunFailMessage = failMessage - - // Start the command run process - commandRunProcess.command = [command].concat(args || []) - commandRunProcess.running = true - } - - // Check if a file/directory exists - function checkPathAndToast(path, successMessage, failMessage, onSuccess = null) { - runCommandWithToast("test", ["-e", path], successMessage, failMessage, onSuccess) - } - - // Show toast after a delay (useful for delayed feedback) - function delayedToast(message, type = "notice", delayMs = 1000) { - delayedToastMessage = message - delayedToastType = type - delayedToastTimer.interval = delayMs - delayedToastTimer.restart() - } - - // Generic method to show a toast - function showToast(label, description = "", type = "notice", persistent = false, duration = 3000) { - var toastData = { - "label": label, - "description": description, - "type": type, - "persistent": persistent, - "duration": duration, - "timestamp": Date.now() - } - - // If there's already a toast showing, instantly start hide animation and show new one - if (isShowingToast) { - // Instantly start hide animation of current toast - for (var i = 0; i < allToasts.length; i++) { - allToasts[i].hide() - } - // Clear the queue since we're showing the new toast immediately - messageQueue = [] - } - - // Add to queue - messageQueue.push(toastData) - - // Always process immediately for instant display - processQueue() - } - - // Process the message queue - function processQueue() { - if (messageQueue.length === 0 || allToasts.length === 0) { - // Added this so we don't accidentally get duplicate toasts - // if it causes issues, remove it and we'll find a different solution - if (allToasts.length === 0 && messageQueue.length > 0) { - messageQueue = [] - } - isShowingToast = false - return - } - - var toastData = messageQueue.shift() - isShowingToast = true - - // Configure and show toast on all screens - for (var i = 0; i < allToasts.length; i++) { - var toast = allToasts[i] - toast.label = toastData.label - toast.description = toastData.description - toast.type = toastData.type - toast.persistent = toastData.persistent - toast.duration = toastData.duration - - toast.show() - } - } - - // Called when a toast is dismissed - function onToastDismissed() { - // Check if all toasts are dismissed - var allDismissed = true - for (var i = 0; i < allToasts.length; i++) { - if (allToasts[i].visible) { - allDismissed = false - break - } - } - - if (allDismissed) { - isShowingToast = false - - // Small delay before showing next toast - Qt.callLater(function () { - processQueue() - }) - } - } - - // Clear all pending messages - function clearQueue() { - - messageQueue = [] - } - - // Hide current toast - function hideCurrentToast() { - if (isShowingToast) { - for (var i = 0; i < allToasts.length; i++) { - allToasts[i].hide() - } - } + function showError(message, description = "", duration = 5000) { + notify(message, description, "error", duration) } } diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 4a4d291..c56a489 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.8.0" + property string baseVersion: "2.9.2" property bool isDevelopment: false property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index c1eba0a..3a0756e 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -97,8 +97,7 @@ Singleton { // All transition keys but filter out "none" and "random" so we are left with the real transitions readonly property var allTransitions: Array.from({ "length": transitionsModel.count - }, (_, i) => transitionsModel.get(i).key).filter( - key => key !== "random" && key != "none") + }, (_, i) => transitionsModel.get(i).key).filter(key => key !== "random" && key != "none") property var wallpaperLists: ({}) property int scanningCount: 0 diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index 000eb43..b8fe113 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -33,8 +33,7 @@ Rectangle { // Dimensions implicitWidth: customWidth > 0 ? customWidth : contentRow.implicitWidth + (Style.marginL * 2 * scaling) - implicitHeight: customHeight > 0 ? customHeight : Math.max(Style.baseWidgetSize * scaling, - contentRow.implicitHeight + (Style.marginM * scaling)) + implicitHeight: customHeight > 0 ? customHeight : Math.max(Style.baseWidgetSize * scaling, contentRow.implicitHeight + (Style.marginM * scaling)) // Appearance radius: Style.radiusS * scaling diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index a48db95..b9c2ae6 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -13,7 +13,7 @@ RowLayout { property bool hovering: false property color activeColor: Color.mPrimary property color activeOnColor: Color.mOnPrimary - property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) + property int baseSize: Style.baseWidgetSize * 0.7 signal toggled(bool checked) signal entered @@ -35,12 +35,12 @@ RowLayout { Rectangle { id: box - implicitWidth: root.baseSize * scaling - implicitHeight: root.baseSize * scaling + implicitWidth: Math.round(root.baseSize * scaling) + implicitHeight: Math.round(root.baseSize * scaling) radius: Style.radiusXS * scaling color: root.checked ? root.activeColor : Color.mSurface - border.color: root.checked ? root.activeColor : Color.mOutline - border.width: Math.max(1, Style.borderM * scaling) + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) Behavior on color { ColorAnimation { @@ -57,9 +57,11 @@ RowLayout { NIcon { visible: root.checked anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 * scaling icon: "check" color: root.activeOnColor - font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling + font.pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5) * scaling + font.weight: Style.fontWeightBold } MouseArea { diff --git a/Widgets/NCollapsible.qml b/Widgets/NCollapsible.qml new file mode 100644 index 0000000..1403cf3 --- /dev/null +++ b/Widgets/NCollapsible.qml @@ -0,0 +1,193 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + property string label: "" + property string description: "" + property bool expanded: false + property bool defaultExpanded: false + property real contentSpacing: Style.marginM * scaling + signal toggled(bool expanded) + + Layout.fillWidth: true + spacing: 0 + + // Default property to accept children + default property alias content: contentLayout.children + + // Header with clickable area + Rectangle { + id: headerContainer + Layout.fillWidth: true + Layout.preferredHeight: headerContent.implicitHeight + (Style.marginL * scaling * 2) + + // Material 3 style background + color: root.expanded ? Color.mSecondary : Color.mSurfaceVariant + radius: Style.radiusL * scaling + + // Subtle border + border.color: root.expanded ? Color.mOnSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Smooth color transitions + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + MouseArea { + id: headerArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: { + root.expanded = !root.expanded + root.toggled(root.expanded) + } + + // Hover effect overlay + Rectangle { + anchors.fill: parent + color: headerArea.containsMouse ? Color.mOnSurface : Color.transparent + opacity: headerArea.containsMouse ? 0.08 : 0 + radius: headerContainer.radius // Reference the container's radius directly + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + RowLayout { + id: headerContent + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // Expand/collapse icon with rotation animation + NIcon { + id: chevronIcon + icon: "chevron-right" + font.pointSize: Style.fontSizeL * scaling + color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + + rotation: root.expanded ? 90 : 0 + Behavior on rotation { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + // Header text content - properly contained + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginXXS * scaling + + NText { + text: root.label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightSemiBold + color: root.expanded ? Color.mOnSecondary : Color.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + NText { + text: root.description + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightRegular + color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: root.description !== "" + opacity: 0.87 + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + } + } + } + + // Collapsible content with Material 3 styling + Rectangle { + id: contentContainer + Layout.fillWidth: true + Layout.topMargin: Style.marginS * scaling + + visible: root.expanded + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Dynamic height based on content + Layout.preferredHeight: visible ? contentLayout.implicitHeight + (Style.marginL * scaling * 2) : 0 + + // Smooth height animation + Behavior on Layout.preferredHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Content layout + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: root.contentSpacing + + // Clip content during animation + clip: true + } + + // Fade in animation for content + opacity: root.expanded ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + // Initialize expanded state + Component.onCompleted: { + root.expanded = root.defaultExpanded + } +} diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml index cc246c6..b20b59f 100644 --- a/Widgets/NColorPickerDialog.qml +++ b/Widgets/NColorPickerDialog.qml @@ -110,12 +110,12 @@ Popup { border.width: Math.max(1, Style.borderM * scaling) } - ScrollView { + NScrollView { id: scrollView anchors.fill: parent - ScrollBar.vertical.policy: ScrollBar.AlwaysOff - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + horizontalPolicy: ScrollBar.AlwaysOff clip: true ColumnLayout { @@ -180,8 +180,7 @@ Popup { } NText { - text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round( - root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" + text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" @@ -244,25 +243,19 @@ Popup { Layout.preferredWidth: 20 * scaling } - NSlider { + NValueSlider { id: redSlider Layout.fillWidth: true from: 0 to: 255 value: Math.round(root.selectedColor.r * 255) - onMoved: { - root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(redSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling + onMoved: value => { + root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + text: Math.round(value) } } @@ -276,26 +269,20 @@ Popup { Layout.preferredWidth: 20 * scaling } - NSlider { + NValueSlider { id: greenSlider Layout.fillWidth: true from: 0 to: 255 value: Math.round(root.selectedColor.g * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(greenSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling + onMoved: value => { + root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + text: Math.round(value) } } @@ -309,26 +296,20 @@ Popup { Layout.preferredWidth: 20 * scaling } - NSlider { + NValueSlider { id: blueSlider Layout.fillWidth: true from: 0 to: 255 value: Math.round(root.selectedColor.b * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(blueSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling + onMoved: value => { + root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + text: Math.round(value) } } @@ -342,38 +323,31 @@ Popup { Layout.preferredWidth: 80 * scaling } - NSlider { + NValueSlider { id: brightnessSlider Layout.fillWidth: true from: 0 to: 100 value: { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) return hsv[2] } - onMoved: { - var hue = root.currentHue - var saturation = root.currentSaturation + onMoved: value => { + var hue = root.currentHue + var saturation = root.currentSaturation - if (hue === 0 && saturation === 0) { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - hue = hsv[0] - saturation = hsv[1] - root.currentHue = hue - root.currentSaturation = saturation - } + if (hue === 0 && saturation === 0) { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + hue = hsv[0] + saturation = hsv[1] + root.currentHue = hue + root.currentSaturation = saturation + } - var rgb = root.hsvToRgb(hue, saturation, value) - root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) - } - } - - NText { + var rgb = root.hsvToRgb(hue, saturation, value) + root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) + } text: Math.round(brightnessSlider.value) + "%" - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 40 * scaling } } } @@ -416,8 +390,7 @@ Popup { cursorShape: Qt.PointingHandCursor onClicked: { root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) root.currentHue = hsv[0] root.currentSaturation = hsv[1] } @@ -459,16 +432,14 @@ Popup { radius: Style.radiusXXS * scaling color: modelData border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: Math.max( - 1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling) + border.width: Math.max(1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling) MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) root.currentHue = hsv[0] root.currentSaturation = hsv[1] } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 52c6eb4..c0f096a 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -76,10 +76,8 @@ RowLayout { font.pointSize: Style.fontSizeM * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - color: (combo.currentIndex >= 0 - && combo.currentIndex < root.model.count) ? Color.mOnSurface : Color.mOnSurfaceVariant - text: (combo.currentIndex >= 0 - && combo.currentIndex < root.model.count) ? root.model.get(combo.currentIndex).name : root.placeholder + color: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? Color.mOnSurface : Color.mOnSurfaceVariant + text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get(combo.currentIndex).name : root.placeholder } indicator: NIcon { diff --git a/Widgets/NHeader.qml b/Widgets/NHeader.qml new file mode 100644 index 0000000..16180dc --- /dev/null +++ b/Widgets/NHeader.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM * scaling + + NText { + text: root.label + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + font.capitalization: Font.Capitalize + color: Color.mSecondary + visible: root.title !== "" + } + + NText { + text: root.description + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + visible: root.description !== "" + } +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 6caf79b..07f2798 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -9,7 +9,6 @@ Rectangle { // Multiplier to control how large the button container is relative to Style.baseWidgetSize property real sizeRatio: 1.0 - readonly property real size: Style.baseWidgetSize * sizeRatio * scaling property string icon property string tooltipText @@ -19,8 +18,8 @@ Rectangle { property color colorBg: Color.mSurfaceVariant property color colorFg: Color.mPrimary - property color colorBgHover: Color.mPrimary - property color colorFgHover: Color.mOnPrimary + property color colorBgHover: Color.mTertiary + property color colorFgHover: Color.mOnTertiary property color colorBorder: Color.mOutline property color colorBorderHover: Color.mOutline @@ -30,8 +29,8 @@ Rectangle { signal rightClicked signal middleClicked - implicitWidth: size - implicitHeight: size + implicitWidth: Math.round(Style.baseWidgetSize * scaling * sizeRatio) + implicitHeight: Math.round(Style.baseWidgetSize * scaling * sizeRatio) opacity: root.enabled ? Style.opacityFull : Style.opacityMedium color: root.enabled && root.hovering ? colorBgHover : colorBg diff --git a/Widgets/NListView.qml b/Widgets/NListView.qml new file mode 100644 index 0000000..6731d7c --- /dev/null +++ b/Widgets/NListView.qml @@ -0,0 +1,203 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Commons + +Item { + id: root + + property color handleColor: Qt.alpha(Color.mTertiary, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: Color.transparent + property real handleWidth: 6 * scaling + property real handleRadius: Style.radiusM * scaling + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + + // Forward ListView properties + property alias model: listView.model + property alias delegate: listView.delegate + property alias spacing: listView.spacing + property alias orientation: listView.orientation + property alias currentIndex: listView.currentIndex + property alias count: listView.count + property alias contentHeight: listView.contentHeight + property alias contentWidth: listView.contentWidth + property alias contentY: listView.contentY + property alias contentX: listView.contentX + property alias currentItem: listView.currentItem + property alias highlightItem: listView.highlightItem + property alias headerItem: listView.headerItem + property alias footerItem: listView.footerItem + property alias section: listView.section + property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem + property alias highlightMoveDuration: listView.highlightMoveDuration + property alias highlightMoveVelocity: listView.highlightMoveVelocity + property alias preferredHighlightBegin: listView.preferredHighlightBegin + property alias preferredHighlightEnd: listView.preferredHighlightEnd + property alias highlightRangeMode: listView.highlightRangeMode + property alias snapMode: listView.snapMode + property alias keyNavigationWraps: listView.keyNavigationWraps + property alias cacheBuffer: listView.cacheBuffer + property alias displayMarginBeginning: listView.displayMarginBeginning + property alias displayMarginEnd: listView.displayMarginEnd + property alias layoutDirection: listView.layoutDirection + property alias effectiveLayoutDirection: listView.effectiveLayoutDirection + property alias verticalLayoutDirection: listView.verticalLayoutDirection + property alias boundsBehavior: listView.boundsBehavior + property alias flickableDirection: listView.flickableDirection + property alias interactive: listView.interactive + property alias moving: listView.moving + property alias flicking: listView.flicking + property alias dragging: listView.dragging + property alias horizontalVelocity: listView.horizontalVelocity + property alias verticalVelocity: listView.verticalVelocity + + // Forward ListView methods + function positionViewAtIndex(index, mode) { + listView.positionViewAtIndex(index, mode) + } + + function positionViewAtBeginning() { + listView.positionViewAtBeginning() + } + + function positionViewAtEnd() { + listView.positionViewAtEnd() + } + + function forceLayout() { + listView.forceLayout() + } + + function cancelFlick() { + listView.cancelFlick() + } + + function flick(xVelocity, yVelocity) { + listView.flick(xVelocity, yVelocity) + } + + function incrementCurrentIndex() { + listView.incrementCurrentIndex() + } + + function decrementCurrentIndex() { + listView.decrementCurrentIndex() + } + + function indexAt(x, y) { + return listView.indexAt(x, y) + } + + function itemAt(x, y) { + return listView.itemAt(x, y) + } + + function itemAtIndex(index) { + return listView.itemAtIndex(index) + } + + // Set reasonable implicit sizes for Layout usage + implicitWidth: 200 + implicitHeight: 200 + + ListView { + id: listView + anchors.fill: parent + + // Enable clipping to keep content within bounds + clip: true + + // Enable flickable for smooth scrolling + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: ScrollBar { + parent: listView + x: listView.mirrored ? 0 : listView.width - width + y: 0 + height: listView.height + active: listView.ScrollBar.horizontal.active + policy: root.verticalPolicy + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + ScrollBar.horizontal: ScrollBar { + id: horizontalScrollBar + parent: listView + x: 0 + y: listView.height - height + width: listView.width + active: listView.ScrollBar.vertical.active + policy: root.horizontalPolicy + + contentItem: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 4cf48a5..ef286f1 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -7,24 +7,14 @@ import qs.Services Loader { id: root - active: false - asynchronous: true - property ShellScreen screen - property real scaling: ScalingService.getScreenScale(screen) - - Connections { - target: ScalingService - function onScaleChanged(screenName, scale) { - if ((screen !== null) && (screenName === screen.name)) { - scaling = scale - } - } - } + property real scaling: 1.0 property Component panelContent: null - property int panelWidth: 1500 - property int panelHeight: 400 + property real preferredWidth: 700 + property real preferredHeight: 900 + property real preferredWidthRatio + property real preferredHeightRatio property color panelBackgroundColor: Color.mSurface property bool panelAnchorHorizontalCenter: false @@ -50,14 +40,14 @@ Loader { property real opacityValue: originalOpacity property alias isClosing: hideTimer.running - readonly property real barHeight: Math.round(Style.barHeight * scaling) - readonly property bool barAtBottom: Settings.data.bar.position === "bottom" - readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) - || (Settings.data.bar.monitors.length === 0)) + readonly property string barPosition: Settings.data.bar.position signal opened signal closed + active: false + asynchronous: true + Component.onCompleted: { PanelService.registerPanel(root) } @@ -81,32 +71,16 @@ Loader { } // ----------------------------------------- - function toggle(aScreen, buttonItem) { - // Don't toggle if screen is null or invalid - if (!aScreen || !aScreen.name) { - Logger.warn("NPanel", "Cannot toggle panel: invalid screen object") - return - } - + function toggle(buttonItem) { if (!active || isClosing) { - open(aScreen, buttonItem) + open(buttonItem) } else { close() } } // ----------------------------------------- - function open(aScreen, buttonItem) { - // Don't open if screen is null or invalid - if (!aScreen || !aScreen.name) { - Logger.warn("NPanel", "Cannot open panel: invalid screen object") - return - } - - if (aScreen !== null) { - screen = aScreen - } - + function open(buttonItem) { // Get the button position if provided if (buttonItem !== undefined && buttonItem !== null) { useButtonPosition = true @@ -165,11 +139,38 @@ Loader { PanelWindow { id: panelWindow + // PanelWindow has its own screen property inherited of QsWindow + property real scaling: ScalingService.getScreenScale(screen) + readonly property real barHeight: Math.round(Style.barHeight * scaling) + readonly property real barWidth: Math.round(Style.barHeight * scaling) + readonly property bool barAtBottom: Settings.data.bar.position === "bottom" + readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) + + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if ((screen !== null) && (screenName === screen.name)) { + root.scaling = scaling = scale + } + } + } + + Connections { + target: panelWindow + function onScreenChanged() { + root.screen = screen + root.scaling = scaling = ScalingService.getScreenScale(screen) + + // It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched + panelContentLoader.active = false + panelContentLoader.active = true + } + } + visible: true // Dim desktop if required - color: (root.active && !root.isClosing - && Settings.data.general.dimDesktop) ? Qt.alpha(Color.mShadow, Style.opacityHeavy) : Color.transparent + color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Qt.alpha(Color.mShadow, Style.opacityHeavy) : Color.transparent WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" @@ -185,8 +186,29 @@ Loader { anchors.left: true anchors.right: true anchors.bottom: true - margins.top: (barIsVisible && !barAtBottom) ? barHeight : 0 - margins.bottom: (barIsVisible && barAtBottom) ? barHeight : 0 + margins.top: { + if (!barIsVisible || barAtBottom) { + return 0 + } + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return Style.marginM * scaling + } + } + + margins.bottom: { + if (!barIsVisible || !barAtBottom) { + return 0 + } + switch (Settings.data.bar.position) { + case "bottom": + return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return 0 + } + } // Close any panel with Esc without requiring focus Shortcut { @@ -209,8 +231,27 @@ Loader { radius: Style.radiusL * scaling border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - width: panelWidth - height: panelHeight + width: { + var w + if (preferredWidthRatio !== undefined) { + w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) * scaling) + } else { + w = preferredWidth * scaling + } + // Clamp width so it is never bigger than the screen + return Math.min(w, screen?.width - Style.marginL * 2) + } + height: { + var h + if (preferredHeightRatio !== undefined) { + h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) * scaling) + } else { + h = preferredHeight * scaling + } + + // Clamp width so it is never bigger than the screen + return Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2) + } scale: root.scaleValue opacity: root.opacityValue @@ -219,37 +260,125 @@ Loader { y: calculatedY property int calculatedX: { - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (panelWidth / 2) + var barPosition = Settings.data.bar.position - // Keep panel within screen bounds - var maxX = panelWindow.width - panelWidth - (Style.marginS * scaling) - var minX = Style.marginS * scaling - - return Math.round(Math.max(minX, Math.min(targetX, maxX))) - } else if (!panelAnchorHorizontalCenter && panelAnchorLeft) { + // Check anchor properties first, even when using button positioning + if (!panelAnchorHorizontalCenter && panelAnchorLeft) { return Math.round(Style.marginS * scaling) } else if (!panelAnchorHorizontalCenter && panelAnchorRight) { - return Math.round(panelWindow.width - panelWidth - (Style.marginS * scaling)) + // For right anchor, consider bar position + if (barPosition === "right") { + // If bar is on right, position panel to the left of the bar + var maxX = panelWindow.width - barWidth - panelBackground.width - (Style.marginS * scaling) + + // If we have button position, position close to the button like working panels + if (root.useButtonPosition) { + // Use the same logic as working panels - position at edge of bar with spacing + var maxXWithSpacing = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxXWithSpacing -= Style.marginL * scaling + } else { + maxXWithSpacing -= Style.marginM * scaling + } + return Math.round(maxXWithSpacing) + } else { + return Math.round(maxX) + } + } else { + // Default right positioning + var rightX = panelWindow.width - panelBackground.width - (Style.marginS * scaling) + return Math.round(rightX) + } + } else if (root.useButtonPosition) { + // Position panel relative to button (only if no explicit anchoring) + var targetX + + // For vertical bars, position panel close to the button + if (barPosition === "left") { + // Position panel to the right of the left bar, close to the button + var minX = barWidth + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + minX += Style.marginL * scaling + } else { + minX += Style.marginM * scaling + } + targetX = minX + } else if (barPosition === "right") { + // Position panel to the left of the right bar, close to the button + var maxX = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxX -= Style.marginL * scaling + } else { + maxX -= Style.marginM * scaling + } + targetX = maxX + } else { + // For horizontal bars, center panel on button + targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (panelBackground.width / 2) + } + + // Keep panel within screen bounds + var maxScreenX = panelWindow.width - panelBackground.width - (Style.marginS * scaling) + var minScreenX = Style.marginS * scaling + + return Math.round(Math.max(minScreenX, Math.min(targetX, maxScreenX))) } else { - return Math.round((panelWindow.width - panelWidth) / 2) + // For vertical bars, center but avoid bar overlap + var centerX = (panelWindow.width - panelBackground.width) / 2 + if (barPosition === "left") { + var minX = barWidth + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + minX += Style.marginL * scaling + } else { + minX += Style.marginM * scaling + } + centerX = Math.max(centerX, minX) + } else if (barPosition === "right") { + // For right bar, center but ensure it doesn't overlap with the bar + var maxX = panelWindow.width - barWidth - panelBackground.width + // Add spacing - more if screen corners are disabled, less if enabled + if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) { + maxX -= Style.marginL * scaling + } else { + maxX -= Style.marginM * scaling + } + centerX = Math.min(centerX, maxX) + } + return Math.round(centerX) } } property int calculatedY: { - if (panelAnchorVerticalCenter) { - return Math.round((panelWindow.height - panelHeight) / 2) + var barPosition = Settings.data.bar.position + + if (root.useButtonPosition) { + // Position panel relative to button + var targetY = root.buttonPosition.y + (root.buttonHeight / 2) - (panelBackground.height / 2) + + // Keep panel within screen bounds + var maxY = panelWindow.height - panelBackground.height - (Style.marginS * scaling) + var minY = Style.marginS * scaling + + return Math.round(Math.max(minY, Math.min(targetY, maxY))) + } else if (panelAnchorVerticalCenter) { + return Math.round((panelWindow.height - panelBackground.height) / 2) } else if (panelAnchorBottom) { - return Math.round(panelWindow.height - panelHeight - (Style.marginS * scaling)) + return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling)) } else if (panelAnchorTop) { return Math.round(Style.marginS * scaling) + } else if (barPosition === "left" || barPosition === "right") { + // For vertical bars, center vertically + return Math.round((panelWindow.height - panelBackground.height) / 2) } else if (!barAtBottom) { // Below the top bar return Math.round(Style.marginS * scaling) } else { // Above the bottom bar - return Math.round(panelWindow.height - panelHeight - (Style.marginS * scaling)) + return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling)) } } @@ -280,6 +409,7 @@ Loader { } Loader { + id: panelContentLoader anchors.fill: parent sourceComponent: root.panelContent } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 7b57ad8..d6f0fac 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -8,16 +8,18 @@ Item { property string icon: "" property string text: "" + property string suffix: "" property string tooltipText: "" property real sizeRatio: 0.8 property bool autoHide: false property bool forceOpen: false + property bool forceClose: false property bool disableOpen: false property bool rightOpen: false property bool hovered: false - // Effective shown state (true if hovered/animated open or forced) - readonly property bool revealed: forceOpen || showPill + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" signal shown signal hidden @@ -28,258 +30,81 @@ Item { signal middleClicked signal wheel(int delta) - // Internal state - property bool showPill: false - property bool shouldAnimateHide: false + // Dynamic sizing based on loaded component + width: pillLoader.item ? pillLoader.item.width : 0 + height: pillLoader.item ? pillLoader.item.height : 0 - // Exposed width logic - readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) - readonly property int pillHeight: iconSize - readonly property int pillPaddingHorizontal: Style.marginS * scaling - readonly property int pillOverlap: iconSize * 0.5 - readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + // Loader to switch between vertical and horizontal pill implementations + Loader { + id: pillLoader + sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent - width: iconSize + Math.max(0, pill.width - pillOverlap) - height: pillHeight - - Rectangle { - id: pill - width: revealed ? maxPillWidth : 1 - height: pillHeight - - x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right - (iconCircle.x + iconCircle.width / 2) - width // Opens left - - opacity: revealed ? Style.opacityFull : Style.opacityNone - color: Color.mSurfaceVariant - - topLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - topRightRadius: rightOpen ? pillHeight * 0.5 : 0 - bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0 - anchors.verticalCenter: parent.verticalCenter - - NText { - id: textItem - anchors.verticalCenter: parent.verticalCenter - x: { - // Little tweak to have a better text horizontal centering - var centerX = (parent.width - width) / 2 - var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling - return centerX + offset - } - text: root.text - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - visible: revealed - } - - Behavior on width { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - Behavior on opacity { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - } - - Rectangle { - id: iconCircle - width: iconSize - height: iconSize - radius: width * 0.5 - color: hovered && !forceOpen ? Color.mPrimary : Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - - x: rightOpen ? 0 : (parent.width - width) - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - easing.type: Easing.InOutQuad + Component { + id: verticalPillComponent + NPillVertical { + icon: root.icon + text: root.text + suffix: root.suffix + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + forceClose: root.forceClose + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: delta => root.wheel(delta) } } - NIcon { - icon: root.icon - font.pointSize: Style.fontSizeM * scaling - color: hovered && !forceOpen ? Color.mOnPrimary : Color.mOnSurface - // Center horizontally - x: (iconCircle.width - width) / 2 - // Center vertically accounting for font metrics - y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 - } - } - - ParallelAnimation { - id: showAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: 1 - to: maxPillWidth - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 0 - to: 1 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - onStarted: { - showPill = true - } - onStopped: { - delayedHideAnim.start() - root.shown() - } - } - - SequentialAnimation { - id: delayedHideAnim - running: false - PauseAnimation { - duration: 2500 - } - ScriptAction { - script: if (shouldAnimateHide) { - hideAnim.start() - } - } - } - - ParallelAnimation { - id: hideAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: maxPillWidth - to: 1 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 1 - to: 0 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - onStopped: { - showPill = false - shouldAnimateHide = false - root.hidden() - } - } - - NTooltip { - id: tooltip - positionAbove: Settings.data.bar.position === "bottom" - target: pill - delay: Style.tooltipDelayLong - text: root.tooltipText - } - - Timer { - id: showTimer - interval: Style.pillDelay - onTriggered: { - if (!showPill) { - showAnim.start() + Component { + id: horizontalPillComponent + NPillHorizontal { + icon: root.icon + text: root.text + suffix: root.suffix + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + forceClose: root.forceClose + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: delta => root.wheel(delta) } } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onEntered: { - hovered = true - root.entered() - tooltip.show() - if (disableOpen) { - return - } - if (!forceOpen) { - showDelayed() - } - } - onExited: { - hovered = false - root.exited() - if (!forceOpen) { - hide() - } - tooltip.hide() - } - onClicked: function (mouse) { - if (mouse.button === Qt.LeftButton) { - root.clicked() - } else if (mouse.button === Qt.RightButton) { - root.rightClicked() - } else if (mouse.button === Qt.MiddleButton) { - root.middleClicked() - } - } - onWheel: wheel => { - root.wheel(wheel.angleDelta.y) - } - } - function show() { - if (!showPill) { - shouldAnimateHide = autoHide - showAnim.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() + if (pillLoader.item && pillLoader.item.show) { + pillLoader.item.show() } } function hide() { - if (forceOpen) { - return + if (pillLoader.item && pillLoader.item.hide) { + pillLoader.item.hide() } - if (showPill) { - hideAnim.start() - } - showTimer.stop() } function showDelayed() { - if (!showPill) { - shouldAnimateHide = autoHide - showTimer.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() - } - } - - onForceOpenChanged: { - if (forceOpen) { - // Immediately lock open without animations - showAnim.stop() - hideAnim.stop() - delayedHideAnim.stop() - showPill = true - } else { - hide() + if (pillLoader.item && pillLoader.item.showDelayed) { + pillLoader.item.showDelayed() } } } diff --git a/Widgets/NPillHorizontal.qml b/Widgets/NPillHorizontal.qml new file mode 100644 index 0000000..89888ba --- /dev/null +++ b/Widgets/NPillHorizontal.qml @@ -0,0 +1,290 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string suffix: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool forceClose: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + + // Effective shown state (true if hovered/animated open or forced) + readonly property bool revealed: forceOpen || showPill + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Exposed width logic + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillHeight: iconSize + readonly property int pillPaddingHorizontal: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + + width: iconSize + Math.max(0, pill.width - pillOverlap) + height: pillHeight + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: pillHeight + + x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right + (iconCircle.x + iconCircle.width / 2) - width // Opens left + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + + topLeftRadius: rightOpen ? 0 : pillHeight * 0.5 + bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5 + topRightRadius: rightOpen ? pillHeight * 0.5 : 0 + bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0 + anchors.verticalCenter: parent.verticalCenter + + NText { + id: textItem + anchors.verticalCenter: parent.verticalCenter + x: { + // Better text horizontal centering + var centerX = (parent.width - width) / 2 + var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling + if (forceOpen) { + // If its force open, the icon disc background is the same color as the bg pill move text slightly + offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling + } + return centerX + offset + } + text: root.text + root.suffix + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: forceOpen ? Color.mOnSurface : Color.mPrimary + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered ? Color.mTertiary : Color.mSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + + x: rightOpen ? 0 : (parent.width - width) + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered ? Color.mOnTertiary : Color.mOnSurface + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + positionAbove: Settings.data.bar.position === "bottom" + target: pill + delay: Style.tooltipDelayLong + text: root.tooltipText + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => root.wheel(wheel.angleDelta.y) + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NPillVertical.qml b/Widgets/NPillVertical.qml new file mode 100644 index 0000000..fb5037a --- /dev/null +++ b/Widgets/NPillVertical.qml @@ -0,0 +1,331 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string suffix: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool forceClose: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + + // Bar position detection for pill direction + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Determine pill direction based on section position + readonly property bool openDownward: rightOpen + readonly property bool openUpward: !rightOpen + + // Effective shown state (true if animated open or forced, but not if force closed) + readonly property bool revealed: !forceClose && (forceOpen || showPill) + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for vertical bars + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillHeight: iconSize + readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: iconSize + readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4) + + // For vertical bars: width is just icon size, height includes pill space + width: iconSize + height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: 0 + y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2) + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + + // Radius logic for vertical expansion - rounded on the side that connects to icon + topLeftRadius: openUpward ? iconSize * 0.5 : 0 + bottomLeftRadius: openDownward ? iconSize * 0.5 : 0 + topRightRadius: openUpward ? iconSize * 0.5 : 0 + bottomRightRadius: openDownward ? iconSize * 0.5 : 0 + + anchors.horizontalCenter: parent.horizontalCenter + + NText { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: { + var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75 + if (forceOpen) { + // If its force open, the icon disc background is the same color as the bg pill move text slightly + offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling + } + return offset + } + text: root.text + root.suffix + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightMedium + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: forceOpen ? Color.mOnSurface : Color.mPrimary + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered ? Color.mTertiary : Color.mSurfaceVariant + + // Icon positioning based on direction + x: 0 + y: openUpward ? (parent.height - height) : 0 + anchors.horizontalCenter: parent.horizontalCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered ? Color.mOnTertiary : Color.mOnSurface + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + target: pill + text: root.tooltipText + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelayLong + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen || forceClose) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen && !forceClose) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => root.wheel(wheel.angleDelta.y) + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NScrollView.qml b/Widgets/NScrollView.qml new file mode 100644 index 0000000..1edca1c --- /dev/null +++ b/Widgets/NScrollView.qml @@ -0,0 +1,106 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Commons + +T.ScrollView { + id: root + + property color handleColor: Qt.alpha(Color.mTertiary, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: Color.transparent + property real handleWidth: 6 * scaling + property real handleRadius: Style.radiusM * scaling + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + ScrollBar.vertical: ScrollBar { + parent: root + x: root.mirrored ? 0 : root.width - width + y: root.topPadding + height: root.availableHeight + active: root.ScrollBar.horizontal.active + policy: root.verticalPolicy + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + ScrollBar.horizontal: ScrollBar { + parent: root + x: root.leftPadding + y: root.height - height + width: root.availableWidth + active: root.ScrollBar.vertical.active + policy: root.horizontalPolicy + + contentItem: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } +} diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml new file mode 100644 index 0000000..3f91321 --- /dev/null +++ b/Widgets/NSearchableComboBox.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets +import "../Helpers/FuzzySort.js" as Fuzzysort + +RowLayout { + id: root + + property real minimumWidth: 280 * scaling + property real popupHeight: 180 * scaling + + property string label: "" + property string description: "" + property ListModel model: { + + } + property string currentKey: "" + property string placeholder: "" + property string searchPlaceholder: "Search..." + + readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling + + signal selected(string key) + + spacing: Style.marginL * scaling + Layout.fillWidth: true + + // Filtered model for search results + property ListModel filteredModel: ListModel {} + property string searchText: "" + + function findIndexByKey(key) { + for (var i = 0; i < root.model.count; i++) { + if (root.model.get(i).key === key) { + return i + } + } + return -1 + } + + function findIndexByKeyInFiltered(key) { + for (var i = 0; i < root.filteredModel.count; i++) { + if (root.filteredModel.get(i).key === key) { + return i + } + } + return -1 + } + + function filterModel() { + filteredModel.clear() + + if (searchText.trim() === "") { + // If no search text, show all items + for (var i = 0; i < root.model.count; i++) { + filteredModel.append(root.model.get(i)) + } + } else { + // Convert ListModel to array for fuzzy search + var items = [] + for (var i = 0; i < root.model.count; i++) { + items.push(root.model.get(i)) + } + + // Use fuzzy search if available, fallback to simple search + if (typeof Fuzzysort !== 'undefined') { + var fuzzyResults = Fuzzysort.go(searchText, items, { + "key": "name", + "threshold": -1000, + "limit": 50 + }) + + // Add results in order of relevance + for (var j = 0; j < fuzzyResults.length; j++) { + filteredModel.append(fuzzyResults[j].obj) + } + } else { + // Fallback to simple search + var searchLower = searchText.toLowerCase() + for (var i = 0; i < items.length; i++) { + var item = items[i] + if (item.name.toLowerCase().includes(searchLower)) { + filteredModel.append(item) + } + } + } + } + } + + onSearchTextChanged: filterModel() + onModelChanged: filterModel() + + NLabel { + label: root.label + description: root.description + } + + Item { + Layout.fillWidth: true + } + + ComboBox { + id: combo + + Layout.minimumWidth: root.minimumWidth + Layout.preferredHeight: root.preferredHeight + model: filteredModel + currentIndex: findIndexByKeyInFiltered(currentKey) + onActivated: { + if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) { + root.selected(filteredModel.get(combo.currentIndex).key) + } + } + + background: Rectangle { + implicitWidth: Style.baseWidgetSize * 3.75 * scaling + implicitHeight: preferredHeight + color: Color.mSurface + border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: NText { + leftPadding: Style.marginL * scaling + rightPadding: combo.indicator.width + Style.marginL * scaling + font.pointSize: Style.fontSizeM * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant + text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder + } + + indicator: NIcon { + x: combo.width - width - Style.marginM * scaling + y: combo.topPadding + (combo.availableHeight - height) / 2 + icon: "caret-down" + font.pointSize: Style.fontSizeL * scaling + } + + popup: Popup { + y: combo.height + width: combo.width + height: root.popupHeight + 60 * scaling + padding: Style.marginM * scaling + + contentItem: ColumnLayout { + spacing: Style.marginS * scaling + + // Search input + NTextInput { + id: searchInput + Layout.fillWidth: true + placeholderText: root.searchPlaceholder + text: root.searchText + onTextChanged: root.searchText = text + fontSize: Style.fontSizeS * scaling + } + + // Font list + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: combo.popup.visible ? filteredModel : null + ScrollIndicator.vertical: ScrollIndicator {} + + delegate: ItemDelegate { + width: listView.width + hoverEnabled: true + highlighted: ListView.view.currentIndex === index + + onHoveredChanged: { + if (hovered) { + ListView.view.currentIndex = index + } + } + + onClicked: { + root.selected(filteredModel.get(index).key) + combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key) + combo.popup.close() + } + + contentItem: NText { + text: name + font.pointSize: Style.fontSizeM * scaling + color: highlighted ? Color.mSurface : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + width: listView.width * scaling + color: highlighted ? Color.mTertiary : Color.transparent + radius: Style.radiusS * scaling + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + } + } + + // Update the currentIndex if the currentKey is changed externally + Connections { + target: root + function onCurrentKeyChanged() { + combo.currentIndex = root.findIndexByKeyInFiltered(currentKey) + } + } + + // Focus search input when popup opens + Connections { + target: combo.popup + function onVisibleChanged() { + if (combo.popup.visible) { + // Small delay to ensure the popup is fully rendered + Qt.callLater(function () { + if (searchInput && searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + } + } +} diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index c4bd096..a0edbbb 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -7,14 +7,13 @@ import qs.Services Slider { id: root - // Optional color to cut the track beneath the knob (should match surrounding background) - property var cutoutColor + property var cutoutColor: Color.mSurface property bool snapAlways: true property real heightRatio: 0.75 - readonly property real knobDiameter: Style.baseWidgetSize * heightRatio * scaling - readonly property real trackHeight: knobDiameter * 0.5 - readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling + readonly property real knobDiameter: Math.round(Style.baseWidgetSize * heightRatio * scaling) + readonly property real trackHeight: knobDiameter * 0.4 + readonly property real cutoutExtra: Math.round(Style.baseWidgetSize * 0.1 * scaling) snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease implicitHeight: Math.max(trackHeight, knobDiameter) @@ -26,15 +25,54 @@ Slider { implicitHeight: trackHeight width: root.availableWidth height: implicitHeight - radius: height / 2 - color: Color.mSurface + radius: 0 + color: Qt.alpha(Color.mSurface, 0.5) + border.color: Qt.alpha(Color.mOutline, 0.5) + border.width: Math.max(1, Style.borderS * scaling) + // Animated gradient active track Rectangle { id: activeTrack width: root.visualPosition * parent.width height: parent.height - color: Color.mPrimary radius: parent.radius + + // Animated gradient fill + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0.0 + color: Qt.darker(Color.mPrimary, 1.2) + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } + GradientStop { + position: 0.5 + color: Color.mPrimary + SequentialAnimation on position { + loops: Animation.Infinite + NumberAnimation { + from: 0.3 + to: 0.7 + duration: 2000 + easing.type: Easing.InOutSine + } + NumberAnimation { + from: 0.7 + to: 0.3 + duration: 2000 + easing.type: Easing.InOutSine + } + } + } + GradientStop { + position: 1.0 + color: Qt.lighter(Color.mPrimary, 1.2) + } + } } // Circular cutout @@ -44,54 +82,32 @@ Slider { height: knobDiameter + cutoutExtra radius: width / 2 color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface - x: Math.max(0, Math.min(parent.width - width, - root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) - y: (parent.height - height) / 2 + x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra / 2 + anchors.verticalCenter: parent.verticalCenter } } handle: Item { width: knob.implicitWidth height: knob.implicitHeight - x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + x: root.leftPadding + Math.round(root.visualPosition * (root.availableWidth - width)) y: root.topPadding + root.availableHeight / 2 - height / 2 - // Subtle shadow for a more polished look - MultiEffect { - anchors.fill: knob - source: knob - shadowEnabled: true - shadowColor: Color.mShadow - shadowOpacity: 0.25 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 1 - shadowBlur: 8 - } - Rectangle { id: knob implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: root.pressed ? Color.mSurfaceVariant : Color.mSurface + color: root.pressed ? Color.mTertiary : Color.mSurface border.color: Color.mPrimary border.width: Math.max(1, Style.borderL * scaling) + anchors.centerIn: parent Behavior on color { ColorAnimation { duration: Style.animationFast } } - - // Press feedback halo (using accent color, low opacity) - Rectangle { - anchors.centerIn: parent - width: parent.width + 8 * scaling - height: parent.height + 8 * scaling - radius: width / 2 - color: Color.mPrimary - opacity: root.pressed ? 0.16 : 0.0 - } } } } diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml deleted file mode 100644 index 3c559cb..0000000 --- a/Widgets/NToast.qml +++ /dev/null @@ -1,193 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Effects -import Quickshell -import qs.Commons -import qs.Widgets -import qs.Services - -Item { - id: root - - property string label: "" - property string description: "" - property string type: "notice" // "notice", "warning" - property int duration: 5000 // Auto-hide after 5 seconds, 0 = no auto-hide - property bool persistent: false // If true, requires manual dismiss - - required property ShellScreen screen - property real scaling: 1.0 - - // Animation properties - property real targetY: 0 - property real hiddenY: -height - 20 - - signal dismissed - - width: Math.min(500 * scaling, parent.width * 0.8) - height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling) - - // Position at top center of parent - anchors.horizontalCenter: parent.horizontalCenter - y: hiddenY - z: 1000 // High z-index to appear above everything - - function show() { - // NToast updates its scaling when showing. - scaling = ScalingService.getScreenScale(screen) - - // Stop any running animations and reset state - showAnimation.stop() - hideAnimation.stop() - autoHideTimer.stop() - - // Ensure we start from the hidden position - y = hiddenY - visible = true - - // Start the show animation - showAnimation.start() - if (duration > 0 && !persistent) { - autoHideTimer.start() - } - } - - function hide() { - hideAnimation.start() - } - - // Auto-hide timer - Timer { - id: autoHideTimer - interval: root.duration - onTriggered: hide() - } - - // Show animation - PropertyAnimation { - id: showAnimation - target: root - property: "y" - to: targetY - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - - // Hide animation - PropertyAnimation { - id: hideAnimation - target: root - property: "y" - to: hiddenY - duration: Style.animationNormal - easing.type: Easing.InCubic - onFinished: { - root.visible = false - root.dismissed() - } - } - - // Main toast container - Rectangle { - anchors.fill: parent - radius: Style.radiusL * scaling - - // Clean surface background - color: Color.mSurface - - // Simple colored border all around - border.color: { - switch (root.type) { - case "warning": - return Color.mError - case "notice": - return Color.mPrimary - default: - return Color.mOutline - } - } - border.width: Math.max(2, Style.borderM * scaling) - - RowLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling - - // Icon - NIcon { - id: icon - icon: (root.type == "warning") ? "toast-warning" : "toast-notice" - color: { - switch (root.type) { - case "warning": - return Color.mError - case "notice": - return Color.mPrimary - default: - return Color.mPrimary - } - } - - font.pointSize: Style.fontSizeXXL * 1.5 * scaling // 150% size to cover two lines - Layout.alignment: Qt.AlignVCenter - } - - // Label and description - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - - NText { - Layout.fillWidth: true - text: root.label - color: Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - wrapMode: Text.WordWrap - visible: text.length > 0 - } - - NText { - Layout.fillWidth: true - text: root.description - color: Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - wrapMode: Text.WordWrap - visible: text.length > 0 - } - } - - // Close button (only if persistent or manual dismiss needed) - NIconButton { - icon: "close" - visible: root.persistent || root.duration === 0 - - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.mOutline - - sizeRatio: 0.8 - Layout.alignment: Qt.AlignTop - - onClicked: hide() - } - } - - // Click to dismiss (if not persistent) - MouseArea { - anchors.fill: parent - enabled: !root.persistent - onClicked: hide() - cursorShape: Qt.PointingHandCursor - } - } - - // Initial state - Component.onCompleted: { - visible = false - } -} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index f04d117..1381213 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -11,7 +11,7 @@ RowLayout { property string description: "" property bool checked: false property bool hovering: false - property int baseSize: Style.baseWidgetSize + property int baseSize: Math.round(Style.baseWidgetSize * 0.8) signal toggled(bool checked) signal entered @@ -27,12 +27,12 @@ RowLayout { Rectangle { id: switcher - implicitWidth: root.baseSize * 1.625 * scaling - implicitHeight: root.baseSize * scaling + implicitWidth: Math.round(root.baseSize * 1.625 * scaling) + implicitHeight: Math.round(root.baseSize * scaling) radius: height * 0.5 color: root.checked ? Color.mPrimary : Color.mSurface - border.color: root.checked ? Color.mPrimary : Color.mOutline - border.width: Math.max(1, Style.borderM * scaling) + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) Behavior on color { ColorAnimation { @@ -47,14 +47,15 @@ RowLayout { } Rectangle { - implicitWidth: (root.baseSize - 5) * scaling - implicitHeight: (root.baseSize - 5) * scaling + implicitWidth: Math.round((root.baseSize * 0.8) * scaling) + implicitHeight: Math.round((root.baseSize * 0.8) * scaling) radius: height * 0.5 color: root.checked ? Color.mOnPrimary : Color.mPrimary border.color: root.checked ? Color.mSurface : Color.mSurface border.width: Math.max(1, Style.borderM * scaling) - y: 2 * scaling - x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 0 + x: root.checked ? switcher.width - width - 3 * scaling : 3 * scaling Behavior on x { NumberAnimation { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 168d7ee..eb8afd1 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -13,6 +13,8 @@ Window { property bool positionLeft: false property bool positionRight: false + readonly property string barPosition: Settings.data.bar.position + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: Color.transparent visible: false @@ -46,17 +48,34 @@ Window { return } - if (positionLeft) { + // Auto-detect positioning based on bar position if not explicitly set + var shouldPositionLeft = positionLeft + var shouldPositionRight = positionRight + var shouldPositionAbove = positionAbove + + // If no explicit positioning is set, auto-detect based on bar position + if (!positionLeft && !positionRight && !positionAbove) { + if (barPosition === "left") { + shouldPositionRight = true + } else if (barPosition === "right") { + shouldPositionLeft = true + } else if (barPosition === "bottom") { + shouldPositionAbove = true + } + // For "top" bar, default to below (no change needed) + } + + if (shouldPositionLeft) { // Position tooltip to the left of the target var pos = target.mapToGlobal(0, 0) x = pos.x - width - 12 // 12 px margin to the left y = pos.y - height / 2 + target.height / 2 - } else if (positionRight) { + } else if (shouldPositionRight) { // Position tooltip to the right of the target var pos = target.mapToGlobal(target.width, 0) x = pos.x + 12 // 12 px margin to the right y = pos.y - height / 2 + target.height / 2 - } else if (positionAbove) { + } else if (shouldPositionAbove) { // Position tooltip above the target var pos = target.mapToGlobal(0, 0) x = pos.x - width / 2 + target.width / 2 diff --git a/Widgets/NValueSlider.qml b/Widgets/NValueSlider.qml new file mode 100644 index 0000000..4f06954 --- /dev/null +++ b/Widgets/NValueSlider.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +RowLayout { + id: root + + property real from: 0 + property real to: 1 + property real value: 0 + property real stepSize: 0.01 + property var cutoutColor: Color.mSurface + property bool snapAlways: true + property real heightRatio: 0.75 + property string text: "" + + // Signals + signal moved(real value) + signal pressedChanged(bool pressed, real value) + + spacing: Style.marginL * scaling + + NSlider { + id: slider + Layout.fillWidth: true + from: root.from + to: root.to + value: root.value + stepSize: root.stepSize + cutoutColor: root.cutoutColor + snapAlways: root.snapAlways + heightRatio: root.heightRatio + onMoved: root.moved(value) + onPressedChanged: root.pressedChanged(pressed, value) + } + + NText { + visible: root.text !== "" + text: root.text + font.family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 40 * scaling + horizontalAlignment: Text.AlignRight + } +} diff --git a/flake.nix b/flake.nix index 29a5921..b2daca6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,6 @@ { - description = "Noctalia shell - a Wayland desktop shell built with Quickshell"; + description = + "Noctalia shell - a Wayland desktop shell built with Quickshell"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -11,81 +12,73 @@ }; }; - outputs = { - self, - nixpkgs, - systems, - quickshell, - ... - }: let - eachSystem = nixpkgs.lib.genAttrs (import systems); - in { - formatter = eachSystem ( - system: - nixpkgs.legacyPackages.${system}.alejandra - ); + outputs = { self, nixpkgs, systems, quickshell, ... }: + let eachSystem = nixpkgs.lib.genAttrs (import systems); + in { + formatter = + eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); - packages = eachSystem ( - system: let - pkgs = nixpkgs.legacyPackages.${system}; - qs = quickshell.packages.${system}.default.override { - withX11 = false; - withI3 = false; - }; - - runtimeDeps = with pkgs; [ - bash - bluez - brightnessctl - cava - cliphist - coreutils - ddcutil - file - findutils - gpu-screen-recorder - libnotify - matugen - networkmanager - wl-clipboard - ]; - - fontconfig = pkgs.makeFontsConf { - fontDirectories = [ - pkgs.roboto - pkgs.inter-nerdfont - ]; - }; - in { - default = pkgs.stdenv.mkDerivation { - pname = "noctalia-shell"; - version = self.rev or self.dirtyRev or "dirty"; - src = ./.; - - nativeBuildInputs = [pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook]; - buildInputs = [qs pkgs.xkeyboard-config pkgs.qt6.qtbase]; - propagatedBuildInputs = runtimeDeps; - - installPhase = '' - mkdir -p $out/share/noctalia-shell - cp -r ./* $out/share/noctalia-shell - - makeWrapper ${qs}/bin/qs $out/bin/noctalia-shell \ - --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" \ - --set FONTCONFIG_FILE "${fontconfig}" \ - --add-flags "-p $out/share/noctalia-shell" - ''; - - meta = { - description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; - homepage = "https://github.com/noctalia-dev/noctalia-shell"; - license = pkgs.lib.licenses.mit; - mainProgram = "noctalia-shell"; + packages = eachSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + qs = quickshell.packages.${system}.default.override { + withX11 = false; + withI3 = false; }; - }; - } - ); - defaultPackage = eachSystem (system: self.packages.${system}.default); - }; -} \ No newline at end of file + runtimeDeps = with pkgs; + [ + bash + bluez + brightnessctl + cava + cliphist + coreutils + ddcutil + file + findutils + libnotify + matugen + networkmanager + wl-clipboard + ] ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) + [ gpu-screen-recorder ]; + + fontconfig = pkgs.makeFontsConf { + fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ]; + }; + in { + default = pkgs.stdenv.mkDerivation { + pname = "noctalia-shell"; + version = self.rev or self.dirtyRev or "dirty"; + src = ./.; + + nativeBuildInputs = + [ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ]; + buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ]; + propagatedBuildInputs = runtimeDeps; + + installPhase = '' + mkdir -p $out/share/noctalia-shell + cp -r ./* $out/share/noctalia-shell + + makeWrapper ${qs}/bin/qs $out/bin/noctalia-shell \ + --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" \ + --set FONTCONFIG_FILE "${fontconfig}" \ + --add-flags "-p $out/share/noctalia-shell" + ''; + + meta = { + description = + "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; + homepage = "https://github.com/noctalia-dev/noctalia-shell"; + license = pkgs.lib.licenses.mit; + mainProgram = "noctalia-shell"; + }; + }; + }); + + defaultPackage = eachSystem (system: self.packages.${system}.default); + }; +} +