diff --git a/scss/style/settings/dialog.scss b/scss/style/settings/dialog.scss index c917551..bde7907 100644 --- a/scss/style/settings/dialog.scss +++ b/scss/style/settings/dialog.scss @@ -355,3 +355,9 @@ dialog { color: $bar-menus-label; } } + +.unsaved-icon { + margin-right: 1em; + font-size: 1em; + color: $yellow; +} diff --git a/widget/settings/pages/config/bar/index.ts b/widget/settings/pages/config/bar/index.ts index 73dc4c5..957723c 100644 --- a/widget/settings/pages/config/bar/index.ts +++ b/widget/settings/pages/config/bar/index.ts @@ -6,7 +6,7 @@ import options from "options"; export const BarSettings = () => { return Widget.Scrollable({ vscroll: "always", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page paged-container", child: Widget.Box({ vertical: true, diff --git a/widget/settings/pages/config/menus/dashboard.ts b/widget/settings/pages/config/menus/dashboard.ts index 8fc9d2a..2d4d770 100644 --- a/widget/settings/pages/config/menus/dashboard.ts +++ b/widget/settings/pages/config/menus/dashboard.ts @@ -7,7 +7,7 @@ export const DashboardMenuSettings = () => { return Widget.Scrollable({ class_name: "bar-theme-page paged-container", vscroll: "always", - hscroll: "never", + hscroll: "automatic", vexpand: true, overlayScrolling: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/bar/index.ts b/widget/settings/pages/theme/bar/index.ts index 8af994a..37da420 100644 --- a/widget/settings/pages/theme/bar/index.ts +++ b/widget/settings/pages/theme/bar/index.ts @@ -6,7 +6,7 @@ import options from "options"; export const BarTheme = () => { return Widget.Scrollable({ vscroll: "always", - hscroll: "never", + hscroll: "automatic", class_name: "bar-theme-page paged-container", child: Widget.Box({ vertical: true, diff --git a/widget/settings/pages/theme/menus/battery.ts b/widget/settings/pages/theme/menus/battery.ts index d2b67c6..4be41c0 100644 --- a/widget/settings/pages/theme/menus/battery.ts +++ b/widget/settings/pages/theme/menus/battery.ts @@ -6,7 +6,7 @@ import options from "options"; export const BatteryMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page battery paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/bluetooth.ts b/widget/settings/pages/theme/menus/bluetooth.ts index 4993db9..a5580b3 100644 --- a/widget/settings/pages/theme/menus/bluetooth.ts +++ b/widget/settings/pages/theme/menus/bluetooth.ts @@ -6,7 +6,7 @@ import options from "options"; export const BluetoothMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page bluetooth paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/clock.ts b/widget/settings/pages/theme/menus/clock.ts index 26455db..f245e8c 100644 --- a/widget/settings/pages/theme/menus/clock.ts +++ b/widget/settings/pages/theme/menus/clock.ts @@ -6,7 +6,7 @@ import options from "options"; export const ClockMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page clock paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/dashboard.ts b/widget/settings/pages/theme/menus/dashboard.ts index e9e5b2f..b69a461 100644 --- a/widget/settings/pages/theme/menus/dashboard.ts +++ b/widget/settings/pages/theme/menus/dashboard.ts @@ -6,7 +6,7 @@ import options from "options"; export const DashboardMenuTheme = () => { return Widget.Scrollable({ vscroll: "always", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page dashboard paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/index.ts b/widget/settings/pages/theme/menus/index.ts index 929c729..8d65bc5 100644 --- a/widget/settings/pages/theme/menus/index.ts +++ b/widget/settings/pages/theme/menus/index.ts @@ -6,7 +6,7 @@ import options from "options"; export const MenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/media.ts b/widget/settings/pages/theme/menus/media.ts index 5e864bf..a019192 100644 --- a/widget/settings/pages/theme/menus/media.ts +++ b/widget/settings/pages/theme/menus/media.ts @@ -6,7 +6,7 @@ import options from "options"; export const MediaMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page media paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/network.ts b/widget/settings/pages/theme/menus/network.ts index d4dfa5f..58bdc8d 100644 --- a/widget/settings/pages/theme/menus/network.ts +++ b/widget/settings/pages/theme/menus/network.ts @@ -6,7 +6,7 @@ import options from "options"; export const NetworkMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page network paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/notifications.ts b/widget/settings/pages/theme/menus/notifications.ts index 30cb372..08d1bfc 100644 --- a/widget/settings/pages/theme/menus/notifications.ts +++ b/widget/settings/pages/theme/menus/notifications.ts @@ -6,7 +6,7 @@ import options from "options"; export const NotificationsMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page notifications paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/systray.ts b/widget/settings/pages/theme/menus/systray.ts index 147c6ea..ad0cde6 100644 --- a/widget/settings/pages/theme/menus/systray.ts +++ b/widget/settings/pages/theme/menus/systray.ts @@ -6,7 +6,7 @@ import options from "options"; export const SystrayMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page systray paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/menus/volume.ts b/widget/settings/pages/theme/menus/volume.ts index d5eba10..5505245 100644 --- a/widget/settings/pages/theme/menus/volume.ts +++ b/widget/settings/pages/theme/menus/volume.ts @@ -6,7 +6,7 @@ import options from "options"; export const VolumeMenuTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "menu-theme-page volume paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/notifications/index.ts b/widget/settings/pages/theme/notifications/index.ts index 8772d56..9f5d364 100644 --- a/widget/settings/pages/theme/notifications/index.ts +++ b/widget/settings/pages/theme/notifications/index.ts @@ -6,7 +6,7 @@ import options from "options"; export const NotificationsTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "notifications-theme-page paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/pages/theme/osd/index.ts b/widget/settings/pages/theme/osd/index.ts index 2283f56..566bb5a 100644 --- a/widget/settings/pages/theme/osd/index.ts +++ b/widget/settings/pages/theme/osd/index.ts @@ -6,7 +6,7 @@ import options from "options"; export const OsdTheme = () => { return Widget.Scrollable({ vscroll: "automatic", - hscroll: "never", + hscroll: "automatic", class_name: "osd-theme-page paged-container", vexpand: true, child: Widget.Box({ diff --git a/widget/settings/shared/Inputter.ts b/widget/settings/shared/Inputter.ts index 22059ba..be2ac4e 100644 --- a/widget/settings/shared/Inputter.ts +++ b/widget/settings/shared/Inputter.ts @@ -2,6 +2,7 @@ import { Opt } from "lib/option" import Gdk from "gi://Gdk" import icons from "lib/icons" import { RowProps } from "lib/types/options" +import { Variable } from "types/variable"; const EnumSetter = (opt: Opt, values: string[]) => { const lbl = Widget.Label({ label: opt.bind().as(v => `${v}`) }) @@ -34,33 +35,99 @@ export const Inputter = ({ min = 0, increment = 1 }: RowProps, - className: string + className: string, + isUnsaved: Variable ) => { return Widget.Box({ class_name: "inputter-container", setup: self => { switch (type) { - case "number": return self.child = Widget.SpinButton({ - setup(self) { - self.set_range(min, max) - self.set_increments(1 * increment, 5 * increment) - self.on("value-changed", () => opt.value = self.value as T) - self.hook(opt, () => self.value = opt.value as number) - }, - }) + case "number": return self.children = [ + Widget.Box({ + class_name: "unsaved-icon-container", + child: isUnsaved.bind("value").as(unsvd => { + if (unsvd) { + return Widget.Icon({ + class_name: "unsaved-icon", + icon: icons.ui.warning, + tooltipText: "Press 'Enter' to apply your changes." + }) + } + return Widget.Box(); + }), + }), + Widget.SpinButton({ + setup(self) { + self.set_range(min, max) + self.set_increments(1 * increment, 5 * increment) + self.on("value-changed", () => { + opt.value = self.value as T; + }) + self.hook(opt, () => { + self.value = opt.value as number; + isUnsaved.value = Number(self.text) !== opt.value as number; + }) + self.connect("key-release-event", () => { + isUnsaved.value = Number(self.text) !== opt.value as number; + }) + }, + }) + ] case "float": - case "object": return self.child = Widget.Entry({ - class_name: className, - on_accept: self => opt.value = JSON.parse(self.text || ""), - setup: self => self.hook(opt, () => self.text = JSON.stringify(opt.value)), - }) + case "object": return self.children = [ + Widget.Box({ + class_name: "unsaved-icon-container", + child: isUnsaved.bind("value").as(unsvd => { + if (unsvd) { + return Widget.Icon({ + class_name: "unsaved-icon", + icon: icons.ui.warning, + tooltipText: "Press 'Enter' to apply your changes." + }) + } + return Widget.Box(); + }), + }), + Widget.Entry({ + class_name: className, + on_change: self => isUnsaved.value = self.text !== JSON.stringify(opt.value), + on_accept: self => opt.value = JSON.parse(self.text || ""), + setup: self => self.hook(opt, () => { + self.text = JSON.stringify(opt.value); + isUnsaved.value = self.text !== JSON.stringify(opt.value); + }) + }) + ] - case "string": return self.child = Widget.Entry({ - on_accept: self => opt.value = self.text as T, - setup: self => self.hook(opt, () => self.text = opt.value as string), - }) + + case "string": return self.children = [ + Widget.Box({ + class_name: "unsaved-icon-container", + child: isUnsaved.bind("value").as(unsvd => { + if (unsvd) { + return Widget.Icon({ + class_name: "unsaved-icon", + icon: icons.ui.warning, + tooltipText: "Press 'Enter' to apply your changes." + }) + } + return Widget.Box(); + }), + }), + Widget.Entry({ + class_name: isUnsaved.bind("value").as(unsaved => unsaved ? "unsaved" : ""), + on_change: self => isUnsaved.value = self.text !== opt.value, + on_accept: self => { + opt.value = self.text as T; + }, + setup: self => self.hook(opt, () => { + isUnsaved.value = self.text !== opt.value; + self.text = opt.value as string; + }), + }) + ] case "enum": return self.child = EnumSetter(opt as unknown as Opt, enums!) case "boolean": return self.child = Widget.Switch() diff --git a/widget/settings/shared/Option.ts b/widget/settings/shared/Option.ts index 25d151e..201269c 100644 --- a/widget/settings/shared/Option.ts +++ b/widget/settings/shared/Option.ts @@ -8,7 +8,10 @@ type Option = { subtitle: string, } + export const Option = (props: RowProps, className: string = '') => { + const isUnsaved = Variable(false); + return Widget.Box({ class_name: "option-item", hexpand: true, @@ -19,7 +22,7 @@ export const Option = (props: RowProps, className: string = '') => { hexpand: true, child: Label(props.title, props.subtitle || ""), }), - Inputter(props, className), + Inputter(props, className, isUnsaved), Widget.Button({ vpack: "center", class_name: "reset-options",