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