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); + } +};