diff --git a/data/dbus-interfaces/org.gnome.SettingsDaemon.Power.Keyboard.xml b/data/dbus-interfaces/org.gnome.SettingsDaemon.Power.Keyboard.xml new file mode 100644 index 000000000..75feb35ea --- /dev/null +++ b/data/dbus-interfaces/org.gnome.SettingsDaemon.Power.Keyboard.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml index 31d1f1266..c3954ba39 100644 --- a/data/gnome-shell-dbus-interfaces.gresource.xml +++ b/data/gnome-shell-dbus-interfaces.gresource.xml @@ -37,6 +37,7 @@ org.gnome.SessionManager.Presence.xml org.gnome.SessionManager.xml org.gnome.SettingsDaemon.Color.xml + org.gnome.SettingsDaemon.Power.Keyboard.xml org.gnome.SettingsDaemon.Power.Screen.xml org.gnome.SettingsDaemon.Rfkill.xml org.gnome.SettingsDaemon.Wacom.xml diff --git a/data/gnome-shell-icons.gresource.xml b/data/gnome-shell-icons.gresource.xml index a5dd1247b..ceec3b89d 100644 --- a/data/gnome-shell-icons.gresource.xml +++ b/data/gnome-shell-icons.gresource.xml @@ -22,6 +22,9 @@ scalable/actions/screenshot-ui-window-symbolic.svg scalable/actions/screenshot-recorded-symbolic.svg scalable/status/background-app-ghost-symbolic.svg + scalable/status/keyboard-brightness-high-symbolic.svg + scalable/status/keyboard-brightness-low-symbolic.svg + scalable/status/keyboard-brightness-medium-symbolic.svg scalable/status/keyboard-caps-lock-symbolic.svg scalable/status/keyboard-enter-symbolic.svg scalable/status/keyboard-hide-symbolic.svg diff --git a/data/icons/scalable/status/keyboard-brightness-high-symbolic.svg b/data/icons/scalable/status/keyboard-brightness-high-symbolic.svg new file mode 100644 index 000000000..ca654a9c6 --- /dev/null +++ b/data/icons/scalable/status/keyboard-brightness-high-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/icons/scalable/status/keyboard-brightness-low-symbolic.svg b/data/icons/scalable/status/keyboard-brightness-low-symbolic.svg new file mode 100644 index 000000000..15a86c99d --- /dev/null +++ b/data/icons/scalable/status/keyboard-brightness-low-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/icons/scalable/status/keyboard-brightness-medium-symbolic.svg b/data/icons/scalable/status/keyboard-brightness-medium-symbolic.svg new file mode 100644 index 000000000..bb51f14d1 --- /dev/null +++ b/data/icons/scalable/status/keyboard-brightness-medium-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/theme/gnome-shell-sass/widgets/_quick-settings.scss b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss index 88d938409..870dba47d 100644 --- a/data/theme/gnome-shell-sass/widgets/_quick-settings.scss +++ b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss @@ -163,6 +163,16 @@ .device-subtitle { color: transparentize($fg_color, 0.5); } +.keyboard-brightness-item .slider { + min-height: 16px; +} + +.keyboard-brightness-level { + spacing: $base_padding; + + .button:checked { @include button(default, $c:$selected_bg_color); } +} + // background apps .background-apps-quick-toggle { diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 1e36ed4a1..e72cd31e1 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -137,6 +137,7 @@ ui/status/accessibility.js ui/status/autoRotate.js ui/status/backgroundApps.js + ui/status/backlight.js ui/status/brightness.js ui/status/camera.js ui/status/darkMode.js diff --git a/js/ui/panel.js b/js/ui/panel.js index b44635f56..3a99c9e52 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -30,6 +30,7 @@ const SystemStatus = imports.ui.status.system; const LocationStatus = imports.ui.status.location; const NightLightStatus = imports.ui.status.nightLight; const DarkModeStatus = imports.ui.status.darkMode; +const BacklightStatus = imports.ui.status.backlight; const ThunderboltStatus = imports.ui.status.thunderbolt; const AutoRotateStatus = imports.ui.status.autoRotate; const BackgroundAppsStatus = imports.ui.status.backgroundApps; @@ -398,6 +399,7 @@ class QuickSettings extends PanelMenu.Button { this._thunderbolt = new ThunderboltStatus.Indicator(); this._nightLight = new NightLightStatus.Indicator(); this._darkMode = new DarkModeStatus.Indicator(); + this._backlight = new BacklightStatus.Indicator(); this._powerProfiles = new PowerProfileStatus.Indicator(); this._rfkill = new RFKillStatus.Indicator(); this._autoRotate = new AutoRotateStatus.Indicator(); @@ -414,6 +416,7 @@ class QuickSettings extends PanelMenu.Button { if (this._network) this._indicators.add_child(this._network); this._indicators.add_child(this._darkMode); + this._indicators.add_child(this._backlight); this._indicators.add_child(this._powerProfiles); if (this._bluetooth) this._indicators.add_child(this._bluetooth); @@ -439,6 +442,7 @@ class QuickSettings extends PanelMenu.Button { this._addItems(this._powerProfiles.quickSettingsItems); this._addItems(this._nightLight.quickSettingsItems); this._addItems(this._darkMode.quickSettingsItems); + this._addItems(this._backlight.quickSettingsItems); this._addItems(this._rfkill.quickSettingsItems); this._addItems(this._autoRotate.quickSettingsItems); this._addItems(this._unsafeMode.quickSettingsItems); diff --git a/js/ui/status/backlight.js b/js/ui/status/backlight.js new file mode 100644 index 000000000..cb2904ece --- /dev/null +++ b/js/ui/status/backlight.js @@ -0,0 +1,205 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Indicator */ + +const {Clutter, Gio, GObject, St} = imports.gi; + +const {QuickMenuToggle, SystemIndicator} = imports.ui.quickSettings; + +const PopupMenu = imports.ui.popupMenu; +const {Slider} = imports.ui.slider; + +const {loadInterfaceXML} = imports.misc.fileUtils; + +const BUS_NAME = 'org.gnome.SettingsDaemon.Power'; +const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power'; + +const BrightnessInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Power.Keyboard'); +const BrightnessProxy = Gio.DBusProxy.makeProxyWrapper(BrightnessInterface); + +const SliderItem = GObject.registerClass({ + Properties: { + 'value': GObject.ParamSpec.int( + 'value', '', '', + GObject.ParamFlags.READWRITE, + 0, 100, 0), + }, +}, class SliderItem extends PopupMenu.PopupBaseMenuItem { + constructor() { + super({ + activate: false, + style_class: 'keyboard-brightness-item', + }); + + this._slider = new Slider(0); + + this._sliderChangedId = this._slider.connect('notify::value', + () => this.notify('value')); + this._slider.accessible_name = _('Keyboard Brightness'); + + this.add_child(this._slider); + } + + get value() { + return this._slider.value * 100; + } + + set value(value) { + this._slider.block_signal_handler(this._sliderChangedId); + this._slider.value = value / 100; + this._slider.unblock_signal_handler(this._sliderChangedId); + } +}); + +const DiscreteItem = GObject.registerClass({ + Properties: { + 'value': GObject.ParamSpec.int( + 'value', '', '', + GObject.ParamFlags.READWRITE, + 0, 100, 0), + 'n-levels': GObject.ParamSpec.int( + 'n-levels', '', '', + GObject.ParamFlags.READWRITE, + 1, 3, 1), + }, +}, class DiscreteItem extends St.BoxLayout { + constructor() { + super({ + style_class: 'popup-menu-item', + reactive: true, + }); + + this._levelButtons = new Map(); + this._addLevelButton('off', _('Off'), 'keyboard-brightness-low-symbolic'); + this._addLevelButton('low', _('Low'), 'keyboard-brightness-medium-symbolic'); + this._addLevelButton('high', _('High'), 'keyboard-brightness-high-symbolic'); + + this.connect('notify::n-levels', () => this._syncLevels()); + this.connect('notify::value', () => this._syncChecked()); + this._syncLevels(); + } + + _valueToLevel(value) { + const checkedIndex = Math.round(value * (this.nLevels - 1) / 100); + if (checkedIndex === this.nLevels - 1) + return 'high'; + + return [...this._levelButtons.keys()].at(checkedIndex); + } + + _levelToValue(level) { + const keyIndex = [...this._levelButtons.keys()].indexOf(level); + return 100 * Math.min(keyIndex, this.nLevels - 1) / (this.nLevels - 1); + } + + _addLevelButton(key, label, iconName) { + const box = new St.BoxLayout({ + style_class: 'keyboard-brightness-level', + vertical: true, + x_expand: true, + }); + + box.button = new St.Button({ + styleClass: 'icon-button', + canFocus: true, + iconName, + }); + box.add_child(box.button); + + box.button.connect('clicked', () => { + this.value = this._levelToValue(key); + }); + + box.add_child(new St.Label({ + text: label, + x_align: Clutter.ActorAlign.CENTER, + })); + + this.add_child(box); + this._levelButtons.set(key, box); + } + + _syncLevels() { + this._levelButtons.get('off').visible = this.nLevels > 0; + this._levelButtons.get('high').visible = this.nLevels > 1; + this._levelButtons.get('low').visible = this.nLevels > 2; + } + + _syncChecked() { + const checkedKey = this._valueToLevel(this.value); + this._levelButtons.forEach((b, k) => { + b.button.checked = k === checkedKey; + }); + } +}); + +const KeyboardBrightnessToggle = GObject.registerClass( +class KeyboardBrightnessToggle extends QuickMenuToggle { + _init() { + super._init({ + title: _('Keyboard'), + iconName: 'display-brightness-symbolic', + }); + + this._proxy = new BrightnessProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH, + (proxy, error) => { + if (error) + console.error(error.message); + else + this._proxy.connect('g-properties-changed', () => this._sync()); + this._sync(); + }); + + this.connect('clicked', () => { + this._proxy.Brightness = this.checked ? 0 : 100; + }); + + this._sliderItem = new SliderItem(); + this.menu.box.add_child(this._sliderItem); + + this._discreteItem = new DiscreteItem(); + this.menu.box.add_child(this._discreteItem); + + this._sliderItem.bind_property('visible', + this._discreteItem, 'visible', + GObject.BindingFlags.INVERT_BOOLEAN | + GObject.BindingFlags.SYNC_CREATE); + + this._sliderItem.bind_property('value', + this._discreteItem, 'value', + GObject.BindingFlags.SYNC_CREATE); + + this._sliderItem.connect('notify::value', + () => (this._proxy.Brightness = this._sliderItem.value)); + + this._discreteItem.connect('notify::value', + () => (this._proxy.Brightness = this._discreteItem.value)); + } + + _sync() { + const brightness = this._proxy.Brightness; + const visible = Number.isInteger(brightness) && brightness >= 0; + this.visible = visible; + if (!visible) + return; + + this.checked = brightness > 0; + const useSlider = this._proxy.Steps >= 4; + + this._sliderItem.set({ + visible: useSlider, + value: brightness, + }); + + if (!useSlider) + this._discreteItem.nLevels = this._proxy.Steps; + } +}); + +var Indicator = GObject.registerClass( +class Indicator extends SystemIndicator { + _init() { + super._init(); + + this.quickSettingsItems.push(new KeyboardBrightnessToggle()); + } +}); diff --git a/po/POTFILES.in b/po/POTFILES.in index 098142518..74a521202 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -60,6 +60,7 @@ js/ui/shellMountOperation.js js/ui/status/accessibility.js js/ui/status/autoRotate.js js/ui/status/backgroundApps.js +js/ui/status/backlight.js js/ui/status/bluetooth.js js/ui/status/brightness.js js/ui/status/darkMode.js