From 47cb354e6c55fb29b1e8466fafb57a6ccc5acb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sun, 24 Jul 2022 19:29:50 +0200 Subject: [PATCH] quickSettings: Introduce QuickSettingsMenu The quick settings menu is a popover that arranges items in a reflowing, homogeneous grid. Grid children may span multiple columns, but not rows. For now the QuickSettingsMenu that contains the grid is just a convenience wrapper around the layout manager that does the heavy lifting. The two will become more intertwined when we add support for menu toggles though, so the custom menu type is unfortunately needed. Part-of: --- data/theme/gnome-shell-sass/_widgets.scss | 1 + .../widgets/_quick-settings.scss | 8 + data/theme/meson.build | 1 + js/js-resources.gresource.xml | 1 + js/ui/quickSettings.js | 207 ++++++++++++++++++ 5 files changed, 218 insertions(+) create mode 100644 data/theme/gnome-shell-sass/widgets/_quick-settings.scss create mode 100644 js/ui/quickSettings.js diff --git a/data/theme/gnome-shell-sass/_widgets.scss b/data/theme/gnome-shell-sass/_widgets.scss index 4ed082f1f..875baf582 100644 --- a/data/theme/gnome-shell-sass/_widgets.scss +++ b/data/theme/gnome-shell-sass/_widgets.scss @@ -33,6 +33,7 @@ // Panel @import 'widgets/panel'; @import 'widgets/corner-ripple'; +@import 'widgets/quick-settings'; // Overview @import 'widgets/overview'; @import 'widgets/window-picker'; diff --git a/data/theme/gnome-shell-sass/widgets/_quick-settings.scss b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss new file mode 100644 index 000000000..66a0ed308 --- /dev/null +++ b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss @@ -0,0 +1,8 @@ +.quick-settings { + padding: 4 * $base_padding; +} + +.quick-settings-grid { + spacing-rows: 3 * $base_padding; + spacing-columns: 2 * $base_padding; +} diff --git a/data/theme/meson.build b/data/theme/meson.build index 26471b679..fbf2938b8 100644 --- a/data/theme/meson.build +++ b/data/theme/meson.build @@ -29,6 +29,7 @@ theme_sources = files([ 'gnome-shell-sass/widgets/_overview.scss', 'gnome-shell-sass/widgets/_panel.scss', 'gnome-shell-sass/widgets/_popovers.scss', + 'gnome-shell-sass/widgets/_quick-settings.scss', 'gnome-shell-sass/widgets/_screen-shield.scss', 'gnome-shell-sass/widgets/_screenshot.scss', 'gnome-shell-sass/widgets/_scrollbars.scss', diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 642ad05c3..87adfd140 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -93,6 +93,7 @@ ui/pointerA11yTimeout.js ui/pointerWatcher.js ui/popupMenu.js + ui/quickSettings.js ui/remoteSearch.js ui/ripples.js ui/runDialog.js diff --git a/js/ui/quickSettings.js b/js/ui/quickSettings.js new file mode 100644 index 000000000..82a235850 --- /dev/null +++ b/js/ui/quickSettings.js @@ -0,0 +1,207 @@ +/* exported QuickSettingsMenu */ +const {Clutter, GLib, GObject, St} = imports.gi; + +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; + +const QuickSettingsLayoutMeta = GObject.registerClass({ + Properties: { + 'column-span': GObject.ParamSpec.int( + 'column-span', '', '', + GObject.ParamFlags.READWRITE, + 1, GLib.MAXINT32, 1), + }, +}, class QuickSettingsLayoutMeta extends Clutter.LayoutMeta {}); + +const QuickSettingsLayout = GObject.registerClass({ + Properties: { + 'row-spacing': GObject.ParamSpec.int( + 'row-spacing', 'row-spacing', 'row-spacing', + GObject.ParamFlags.READWRITE, + 0, GLib.MAXINT32, 0), + 'column-spacing': GObject.ParamSpec.int( + 'column-spacing', 'column-spacing', 'column-spacing', + GObject.ParamFlags.READWRITE, + 0, GLib.MAXINT32, 0), + 'n-columns': GObject.ParamSpec.int( + 'n-columns', 'n-columns', 'n-columns', + GObject.ParamFlags.READWRITE, + 1, GLib.MAXINT32, 1), + }, +}, class QuickSettingsLayout extends Clutter.LayoutManager { + _containerStyleChanged() { + const node = this._container.get_theme_node(); + + let changed = false; + let found, length; + [found, length] = node.lookup_length('spacing-rows', false); + changed ||= found; + if (found) + this.rowSpacing = length; + + [found, length] = node.lookup_length('spacing-columns', false); + changed ||= found; + if (found) + this.columnSpacing = length; + + if (changed) + this.layout_changed(); + } + + _getColSpan(container, child) { + const {columnSpan} = this.get_child_meta(container, child); + return Math.clamp(columnSpan, 1, this.nColumns); + } + + _getMaxChildWidth(container) { + let [minWidth, natWidth] = [0, 0]; + + for (const child of container) { + const [childMin, childNat] = child.get_preferred_width(-1); + const colSpan = this._getColSpan(container, child); + minWidth = Math.max(minWidth, childMin / colSpan); + natWidth = Math.max(natWidth, childNat / colSpan); + } + + return [minWidth, natWidth]; + } + + _getRows(container) { + const rows = []; + let lineIndex = 0; + let curRow; + + /** private */ + function appendRow() { + curRow = []; + rows.push(curRow); + lineIndex = 0; + } + + for (const child of container) { + if (!child.visible) + continue; + + if (lineIndex === 0) + appendRow(); + + const colSpan = this._getColSpan(container, child); + const fitsRow = lineIndex + colSpan <= this.nColumns; + + if (!fitsRow) + appendRow(); + + curRow.push(child); + lineIndex = (lineIndex + colSpan) % this.nColumns; + } + + return rows; + } + + _getRowHeight(children) { + let [minHeight, natHeight] = [0, 0]; + + children.forEach(child => { + const [childMin, childNat] = child.get_preferred_height(-1); + minHeight = Math.max(minHeight, childMin); + natHeight = Math.max(natHeight, childNat); + }); + + return [minHeight, natHeight]; + } + + vfunc_get_child_meta_type() { + return QuickSettingsLayoutMeta.$gtype; + } + + vfunc_set_container(container) { + this._container?.disconnectObject(this); + + this._container = container; + + this._container?.connectObject('style-changed', + () => this._containerStyleChanged(), this); + } + + vfunc_get_preferred_width(container, _forHeight) { + const [childMin, childNat] = this._getMaxChildWidth(container); + const spacing = (this.nColumns - 1) * this.column_spacing; + return [this.nColumns * childMin + spacing, this.nColumns * childNat + spacing]; + } + + vfunc_get_preferred_height(container, _forWidth) { + const rows = this._getRows(container); + + let [minHeight, natHeight] = [0, 0]; + + const spacing = (rows.length - 1) * this.row_spacing; + minHeight += spacing; + natHeight += spacing; + + rows.forEach(row => { + const [rowMin, rowNat] = this._getRowHeight(row); + minHeight += rowMin; + natHeight += rowNat; + }); + + return [minHeight, natHeight]; + } + + vfunc_allocate(container, box) { + const rows = this._getRows(container); + + const availWidth = box.get_width() - (this.nColumns - 1) * this.row_spacing; + const childWidth = availWidth / this.nColumns; + + const isRtl = container.text_direction === Clutter.TextDirection.RTL; + + const childBox = new Clutter.ActorBox(); + let y = box.y1; + rows.forEach(row => { + const [, rowNat] = this._getRowHeight(row); + + let lineIndex = 0; + row.forEach(child => { + const colSpan = this._getColSpan(container, child); + const width = + childWidth * colSpan + this.column_spacing * (colSpan - 1); + let x = box.x1 + lineIndex * (childWidth + this.column_spacing); + if (isRtl) + x = box.x2 - width - x; + + childBox.set_origin(x, y); + childBox.set_size(width, rowNat); + child.allocate(childBox); + + lineIndex = (lineIndex + colSpan) % this.nColumns; + }); + + y += rowNat + this.row_spacing; + }); + } +}); + +var QuickSettingsMenu = class extends PopupMenu.PopupMenu { + constructor(sourceActor, nColumns = 1) { + super(sourceActor, 0, St.Side.TOP); + + Main.layoutManager.connectObject('system-modal-opened', + () => this.close(), this); + + this.box.add_style_class_name('quick-settings'); + + this._grid = new St.Widget({ + style_class: 'quick-settings-grid', + layout_manager: new QuickSettingsLayout({ + nColumns, + }), + }); + this.box.add_child(this._grid); + } + + addItem(item, colSpan = 1) { + this._grid.add_child(item); + this._grid.layout_manager.child_set_property( + this._grid, item, 'column-span', colSpan); + } +};