.dotfiles/.local/share/gnome-shell/extensions/quick-settings-audio-panel@.../libs/libpanel/main.js

954 lines
32 KiB
JavaScript

// Documentation: https://github.com/Rayzeq/libpanel/wiki
// Useful links:
// - Drag & Drop example: https://gitlab.com/justperfection.channel/how-to-create-a-gnome-shell-extension/-/blob/master/example11%40example11.com/extension.js
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';
import { BoxPointer } from 'resource:///org/gnome/shell/ui/boxpointer.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { PopupMenu } from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { QuickSettingsMenu } from 'resource:///org/gnome/shell/ui/quickSettings.js';
import { Patcher } from './patcher.js';
import {
add_named_connections,
array_insert,
array_remove,
find_panel,
get_extension_uuid,
get_settings,
rsplit,
set_style,
split
} from './utils.js';
const MenuManager = Main.panel.menuManager;
const QuickSettings = Main.panel.statusArea.quickSettings;
const QuickSettingsLayout = QuickSettings.menu._grid.layout_manager.constructor;
const VERSION = 1;
// The spacing between elements of the grid, in pixels.
const GRID_SPACING = 5;
function registerClass(metadata, klass) {
if (klass === undefined) {
klass = metadata;
metadata = {};
}
metadata.GTypeName = `${metadata.GTypeName || `LibPanel_${klass.name}`}_${get_extension_uuid().replace(/[^A-Za-z_-]/g, '-')}`;
return GObject.registerClass(metadata, klass);
}
const AutoHidable = superclass => {
// We need to cache the created classes or else we would register the same class name multiple times
if (AutoHidable.cache === undefined) AutoHidable.cache = {};
if (AutoHidable.cache[superclass.name] !== undefined) return AutoHidable.cache[superclass.name];
const klass = registerClass({
GTypeName: `LibPanel_AutoHidable_${superclass.name}`,
}, class extends superclass {
constructor(...args) {
const container = args.at(-1).container;
delete args.at(-1).container;
super(...args);
// We need to accept `null` as valid value here
// which is why we don't do `container || this`
this.container = container === undefined ? this : container;
}
get container() {
return this._lpah_container;
}
set container(value) {
if (this._lpah_container !== undefined) this.disconnect_named(this._lpah_container);
if (value !== null) {
this._lpah_container = value;
this.connect_named(this._lpah_container, 'actor-added', (_container, children) => {
this.connect_named(children, 'notify::visible', this._update_visibility.bind(this));
this._update_visibility();
});
this.connect_named(this._lpah_container, 'actor-removed', (_container, children) => {
this.disconnect_named(children);
this._update_visibility();
});
this._update_visibility();
}
}
_get_ah_children() {
return this._lpah_container.get_children();
}
_update_visibility() {
for (const child of this._get_ah_children()) {
if (child.visible) {
this.show();
return;
}
}
this.hide();
// Force the widget to take no space when hidden (this fixes some bugs but I don't know why)
this.queue_relayout();
}
});
AutoHidable.cache[superclass.name] = klass;
return klass;
};
const Semitransparent = superclass => {
// We need to cache the created classes or else we would register the same class name multiple times
if (Semitransparent.cache === undefined) Semitransparent.cache = {};
if (Semitransparent.cache[superclass.name] !== undefined) return Semitransparent.cache[superclass.name];
const klass = registerClass({
GTypeName: `LibPanel_Semitransparent_${superclass.name}`,
Properties: {
'transparent': GObject.ParamSpec.boolean(
'transparent',
'Transparent',
'Whether this widget is transparent to pointer events',
GObject.ParamFlags.READWRITE,
true
),
},
}, class extends superclass {
get transparent() {
if (this._transparent === undefined)
this._transparent = true;
return this._transparent;
}
set transparent(value) {
this._transparent = value;
this.notify('transparent');
}
vfunc_pick(context) {
if (!this.transparent) {
super.vfunc_pick(context);
}
for (const child of this.get_children()) {
child.pick(context);
}
}
});
Semitransparent.cache[superclass.name] = klass;
return klass;
};
const GridItem = superclass => {
// We need to cache the created classes or else we would register the same class name multiple times
if (GridItem.cache === undefined) GridItem.cache = {};
if (GridItem.cache[superclass.name] !== undefined) return GridItem.cache[superclass.name];
const klass = registerClass({
GTypeName: `LibPanel_GridItem_${superclass.name}`,
Properties: {
'draggable': GObject.ParamSpec.boolean(
'draggable',
'draggable',
'Whether this widget can be dragged',
GObject.ParamFlags.READWRITE,
true
),
},
}, class extends superclass {
constructor(panel_name, ...args) {
super(...args);
this.is_grid_item = true;
this.panel_name = panel_name;
this._drag_handle = DND.makeDraggable(this);
this.connect_named(this._drag_handle, 'drag-begin', () => {
QuickSettings.menu.transparent = false;
// Prevent the first column from disapearing if it only contains `this`
const column = this.get_parent();
this._source_column = column;
if (column.get_next_sibling() === null && column.get_children().length === 1) {
column._width_constraint.source = this;
column._inhibit_constraint_update = true;
}
this._dnd_placeholder?.destroy();
this._dnd_placeholder = new DropZone(this);
this._drag_monitor = {
dragMotion: this._on_drag_motion.bind(this),
};
DND.addDragMonitor(this._drag_monitor);
this._drag_orig_index = this.get_parent().get_children().indexOf(this);
// dirty fix for Catppuccin theme (because it relys on CSS inheriting)
// this may not work with custom grid items
this.add_style_class_name?.("popup-menu");
});
// This is emited BEFORE drag-end, which means that this._dnd_placeholder is still available
this.connect_named(this._drag_handle, 'drag-cancelled', () => {
// This stop the dnd system from doing anything with `this`, we want to manage ourselves what to do.
this._drag_handle._dragState = DND.DragState.CANCELLED;
if (this._dnd_placeholder.get_parent() !== null) {
this._dnd_placeholder.acceptDrop(this);
} else { // We manually reset the position of the panel because the dnd system will set it at the end of the column
this.get_parent().remove_child(this);
this._drag_handle._dragOrigParent.insert_child_at_index(this, this._drag_orig_index);
}
});
// This is called when the drag ends with a drop and when it's cancelled
this.connect_named(this._drag_handle, 'drag-end', (_drag_handle, _time, _cancelled) => {
QuickSettings.menu.transparent = true;
if (this._drag_monitor !== undefined) {
DND.removeDragMonitor(this._drag_monitor);
this._drag_monitor = undefined;
}
this._dnd_placeholder?.destroy();
this._dnd_placeholder = null;
const column = this._source_column;
if (!column._is_destroyed && column._width_constraint.source == this) {
column._width_constraint.source = column.get_next_sibling();
column._inhibit_constraint_update = false;
}
// Something, somewhere is setting a forced width & height for this actor,
// so we undo that
this.width = -1;
this.height = -1;
this.remove_style_class_name?.("popup-menu");
});
this.connect_named(this, 'destroy', () => {
if (this._drag_monitor !== undefined) {
DND.removeDragMonitor(this._drag_monitor);
this._drag_monitor = undefined;
}
});
}
get draggable() {
return this._drag_handle._disabled || false;
}
set draggable(value) {
this._drag_handle._disabled = value;
this.notify('draggable');
}
_on_drag_motion(event) {
if (event.source !== this) return DND.DragMotionResult.CONTINUE;
if (event.targetActor === this._dnd_placeholder) return DND.DragMotionResult.COPY_DROP;
const panel = find_panel(event.targetActor);
const previous_sibling = panel?.get_previous_sibling();
const target_pos = panel?.get_transformed_position();
const self_size = this.get_transformed_size();
this._dnd_placeholder.get_parent()?.remove_child(this._dnd_placeholder);
if (event.targetActor.is_panel_column) {
event.targetActor.add_child(this._dnd_placeholder);
} else if (panel !== undefined) {
const column = panel.get_parent();
if (previous_sibling === this._dnd_placeholder || event.y > (target_pos[1] + self_size[1])) {
column.insert_child_above(this._dnd_placeholder, panel);
} else {
column.insert_child_below(this._dnd_placeholder, panel);
}
}
return DND.DragMotionResult.NO_DROP;
}
});
GridItem.cache[superclass.name] = klass;
return klass;
};
const DropZone = registerClass(class LibPanel_DropZone extends St.Widget {
constructor(source) {
super({ style_class: source._drag_actor?.style_class || source.style_class, opacity: 127 });
this._delegate = this;
this._height_constraint = new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.WIDTH,
source: source,
});
this._width_constraint = new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.HEIGHT,
source: source,
});
this.add_constraint(this._height_constraint);
this.add_constraint(this._width_constraint);
}
acceptDrop(source, _actor, _x, _y, _time) {
if (!source.is_grid_item) return false;
source.get_parent().remove_child(source);
const column = this.get_parent();
column.replace_child(this, source);
column.get_parent()._delegate._cleanup();
LibPanel.get_instance()._save_layout();
return true;
}
});
class PanelGrid extends PopupMenu {
constructor(sourceActor) {
super(sourceActor, 0, St.Side.TOP);
// ==== We replace the BoxPointer with our own because we want to make it transparent ====
global.focus_manager.remove_group(this._boxPointer);
this._boxPointer.bin.set_child(null); // prevent `this.box` from being destroyed
this._boxPointer.destroy();
// The majority of this code has been copied from here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/popupMenu.js#L801
// We want to make the actor transparent
this._boxPointer = new (Semitransparent(BoxPointer))(this._arrowSide);
this.actor = this._boxPointer;
this.actor._delegate = this;
this.actor.style_class = 'popup-menu-boxpointer';
// Force the popup to take all the screen to allow drag and drop to empty spaces
this.actor.connect_after('parent-set', () => {
if (this._height_constraint) this.actor.remove_constraint(this._height_constraint);
const parent = this.actor.get_parent();
if (parent === null) {
this._height_constraint = undefined;
} else {
this._height_constraint = new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.HEIGHT,
source: parent,
});
this.actor.add_constraint(this._height_constraint);
}
});
// And manually add the bottom margin. This is useless as the grid is invisible,
// but in case something make it visible it looks nice
this.actor.connect_after('stage-views-changed', () => {
if (this.actor.get_stage() === null || this._height_constraint === undefined) return;
this._height_constraint.offset = -this.actor.getArrowHeight();
});
this._boxPointer.bin.set_child(this.box);
this.actor.add_style_class_name('popup-menu');
this.actor.add_style_class_name('QSAP-panel-grid');
global.focus_manager.add_group(this.actor);
this.actor.reactive = true;
// =======================================================================================
this.box._delegate = this; // this used so columns can get `this` using `column.get_parent()._delegate`
this.box.vertical = false;
this._panel_style_class = this.box.style_class; // we save the style class that's used to make a nice panel
this.box.style_class = ''; // and we remove it so it's invisible
this.box.style = `spacing: ${GRID_SPACING}px`;
this.actor.connect_after('notify::allocation', () => {
// The `setTimeout` fixes the following warning:
// Can't update stage views actor ... is on because it needs an allocation.
if (this.actor.x > 0)
this._timeout_id = setTimeout(() => {
this._timeout_id = null;
this._add_column();
}, 0);
});
this.actor.connect('destroy', () => {
if (this._timeout_id) clearTimeout(this._timeout_id);
});
}
get transparent() {
return this.actor.transparent;
}
set transparent(value) {
this.actor.transparent = value;
}
close(animate) {
for (const column of this.box.get_children()) {
column._close(animate);
}
super.close(animate);
}
_add_panel(panel) {
if (this.box.get_children().length === 0) {
this._add_column()._add_panel(panel);
return;
}
for (const column of this.box.get_children()) {
if (column._panel_layout.indexOf(panel.panel_name) > -1) {
column._add_panel(panel);
return;
}
}
// Everything here is really approximated because we can't have the allocations boxes at this point
// Most notably, `max_height` will be wrong
const max_height = this._height_constraint?.source?.height || this.actor.height;
let column;
for (const children of this.box.get_children().reverse()) {
if (this._get_column_height(children) < max_height) {
column = children;
break;
}
}
if (!column) column = this.box.first_child;
if (this._get_column_height(column) > max_height) {
column = this._add_column();
}
column._add_panel(panel);
}
_get_column_height(column) {
return column.get_children().reduce((acc, widget) => acc + widget.height, 0);
}
_add_column(layout = []) {
const column = new PanelColumn(layout);
this.actor.bind_property('transparent', column, 'transparent', GObject.BindingFlags.SYNC_CREATE);
this.box.insert_child_at_index(column, 0);
return column;
}
_get_panel_layout() {
return this.box.get_children().map(column => column._panel_layout);
}
_cleanup() {
while (this.box.last_child.get_children().length === 0) this.box.last_child.destroy();
}
_get_panels() {
return this.box.get_children().map(column => column.get_children()).flat();
}
}
const PanelColumn = registerClass(class LibPanel_PanelColumn extends Semitransparent(St.BoxLayout) {
constructor(layout = []) {
super({ vertical: true, style: `spacing: ${GRID_SPACING}px` });
this.is_panel_column = true; // since we can't use instanceof, we use this attribute
this._panel_layout = layout;
this._inhibit_constraint_update = false;
this._width_constraint = new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.WIDTH,
source: null,
});
this.add_constraint(this._width_constraint);
this.connect_after_named(this, 'actor-added', (_self, actor) => {
if (this.get_children().length === 1) this.remove_constraint(this._width_constraint);
if (!actor.is_grid_item) return;
const prev_index = this._panel_layout.indexOf(actor.get_previous_sibling()?.panel_name);
const index = this._panel_layout.indexOf(actor.panel_name);
const next_index = this._panel_layout.indexOf(actor.get_next_sibling()?.panel_name);
// `actor` is in the layout but is misplaced
if (index > -1 && ((prev_index > -1 && index < prev_index) || (next_index > -1 && next_index < index))) {
array_remove(this._panel_layout, actor.panel_name);
index = -1;
}
if (index < 0) { // `actor` is not in the layout
if (prev_index > -1)
array_insert(this._panel_layout, prev_index + 1, actor.panel_name);
else if (next_index > 0)
array_insert(this._panel_layout, next_index - 1, actor.panel_name);
else
array_insert(this._panel_layout, 0, actor.panel_name);
}
});
this.connect_after_named(this, 'actor-removed', (_self, actor) => {
if (this.get_children().length === 0) this.add_constraint(this._width_constraint);
if (actor._keep_layout || !actor.is_grid_item) return;
array_remove(this._panel_layout, actor.panel_name);
});
this.connect('destroy', () => this._is_destroyed = true);
this.connect_after_named(this, 'parent-set', (_self, old_parent) => {
if (old_parent !== null) this.disconnect_named(old_parent);
const parent = this.get_parent();
if (parent === null) return;
const update_source = (_parent, _actor) => {
// clutter is being dumb and emit this signal even though `_parent` and `this` are destroyed
// this fix it
if (this._is_destroyed || this._inhibit_constraint_update) return;
this._width_constraint.source = this.get_next_sibling();
};
this.connect_after_named(parent, 'actor-added', update_source);
this.connect_after_named(parent, 'actor-removed', update_source);
update_source();
});
}
_close(animate) {
for (const panel of this.get_children()) {
panel._close(animate);
}
}
_add_panel(panel) {
const index = this._panel_layout.indexOf(panel.panel_name);
if (index > -1) {
const panels = this.get_children().map(children => children.panel_name);
for (const panel_name of this._panel_layout.slice(0, index).reverse()) {
const children_index = panels.indexOf(panel_name);
if (children_index > -1) {
this.insert_child_at_index(panel, children_index + 1);
return;
}
}
this.insert_child_at_index(panel, 0);
} else {
this.add_child(panel);
}
}
});
export var Panel = registerClass(class LibPanel_Panel extends GridItem(AutoHidable(St.Widget)) {
constructor(panel_name, nColumns = 2) {
super(`${get_extension_uuid()}/${panel_name}`, {
// I have no idea why, but sometimes, a panel (not all of them) gets allocated too much space (behavior similar to `y-expand`)
// This prevent it from taking all available space
y_align: Clutter.ActorAlign.START,
// Enable this so the menu block any click event from propagating through
reactive: true,
// We want to set this later
container: null,
});
this._delegate = this;
// Overlay layer that will hold sub-popups
this._overlay = new Clutter.Actor({ layout_manager: new Clutter.BinLayout() });
// Placeholder to make empty space when opening a sub-popup
const placeholder = new Clutter.Actor({
// The placeholder have the same height as the overlay, which means
// it have the same height as the opened sub-popup
constraints: new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.HEIGHT,
source: this._overlay,
}),
});
// The grid holding every element
this._grid = new St.Widget({
style_class: LibPanel.get_instance()._panel_grid._panel_style_class + ' quick-settings quick-settings-grid',
layout_manager: new QuickSettingsLayout(placeholder, { nColumns }),
});
// Force the grid to take up all the available width. I'm using a constraint because x_expand don't work
this._grid.add_constraint(new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.WIDTH,
source: this,
}));
this.add_child(this._grid);
this.container = this._grid;
this._drag_actor = this._grid;
this._grid.add_child(placeholder);
this._dimEffect = new Clutter.BrightnessContrastEffect({ enabled: false });
this._grid.add_effect_with_name('dim', this._dimEffect);
this._overlay.add_constraint(new Clutter.BindConstraint({
coordinate: Clutter.BindCoordinate.WIDTH,
source: this._grid,
}));
this.add_child(this._overlay);
}
getItems() {
// Every child except the placeholder
return this._grid.get_children().filter(item => item != this._grid.layout_manager._overlay);
}
getFirstItem() {
return this.getItems[0];
}
addItem(item, colSpan = 1) {
this._grid.add_child(item);
this._completeAddItem(item, colSpan);
}
insertItemBefore(item, sibling, colSpan = 1) {
this._grid.insert_child_below(item, sibling);
this._completeAddItem(item, colSpan);
}
_completeAddItem(item, colSpan) {
this.setColumnSpan(item, colSpan);
if (item.menu) {
this._overlay.add_child(item.menu.actor);
this.connect_named(item.menu, 'open-state-changed', (_, isOpen) => {
this._setDimmed(isOpen);
this._activeMenu = isOpen ? item.menu : null;
// The sub-popup for the power menu is too high.
// I don't know if it's the real source of the issue, but I suspect that the constraint that fixes its y position
// isn't accounting for the padding of the grid, so we add it to the offset manually
// Later: I added the name check because it breaks on the audio panel
// so I'm almost certain that this is not a proper fix
if (isOpen && this.getItems().indexOf(item) == 0 && this.panel_name == "gnome@main") {
const constraint = item.menu.actor.get_constraints()[0];
constraint.offset =
// the offset is normally bound to the height of the source
constraint.source.height
+ this._grid.get_theme_node().get_padding(St.Side.TOP);
// note: we don't reset this property when the item is removed from this panel because
// we hope that it will reset itself (because it's bound to the height of the source),
// which in the case in my tests, but maybe some issue will arise because of this
}
});
}
if (item._menuButton) {
item._menuButton._libpanel_y_expand_backup = item._menuButton.y_expand;
item._menuButton.y_expand = false;
}
}
removeItem(item) {
if (!this._grid.get_children().includes(item)) console.error(`[LibPanel] ${get_extension_uuid()} tried to remove an item not in the panel`);
item.get_parent().remove_child(item);
if (item.menu) {
this.disconnect_named(item.menu);
item.menu.actor.get_parent().remove_child(item.menu.actor);
}
if (item._menuButton) {
item._menuButton.y_expand = item._menuButton._libpanel_y_expand_backup;
item._menuButton._libpanel_y_expand_backup = undefined;
}
}
getColumnSpan(item) {
if (!this._grid.get_children().includes(item)) console.error(`[LibPanel] ${get_extension_uuid()} tried to get the column span of an item not in the panel`);
const value = new GObject.Value();
this._grid.layout_manager.child_get_property(this._grid, item, 'column-span', value);
const column_span = value.get_int();
value.unset();
return column_span;
}
setColumnSpan(item, colSpan) {
if (!this._grid.get_children().includes(item)) console.error(`[LibPanel] ${get_extension_uuid()} tried to set the column span of an item not in the panel`);
this._grid.layout_manager.child_set_property(this._grid, item, 'column-span', colSpan);
}
_close(animate) {
this._activeMenu?.close(animate);
}
_get_ah_children() {
return this.getItems();
}
_setDimmed(dim) {
// copied from https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/quickSettings.js
const DIM_BRIGHTNESS = -0.4;
const POPUP_ANIMATION_TIME = 400;
const val = 127 * (1 + (dim ? 1 : 0) * DIM_BRIGHTNESS);
const color = Clutter.Color.new(val, val, val, 255);
this._grid.ease_property('@effects.dim.brightness', color, {
mode: Clutter.AnimationMode.LINEAR,
duration: POPUP_ANIMATION_TIME,
onStopped: () => (this._dimEffect.enabled = dim),
});
this._dimEffect.enabled = true;
}
});
// Patching the default to menu to have the exact same api as the one from `Panel`.
// This way, extensions can use them the same way.
QuickSettingsMenu.prototype.getItems = function () {
return this._grid.get_children().filter(item => item != this._grid.layout_manager._overlay);
};
QuickSettingsMenu.prototype.removeItem = function (item) {
this._grid.remove_child(item);
if (item.menu) {
// it seems that some menus don't have _signalConnectionsByName (probably custom menus)
// we check it exists before using it
if (item.menu._signalConnectionsByName) {
// Manually remove the connection since we don't have its id.
for (const id of item.menu._signalConnectionsByName["open-state-changed"]) {
if (item.menu._signalConnections[id].callback.toString().includes("this._setDimmed")) {
item.menu.disconnect(id);
}
}
}
this._overlay.remove_child(item.menu.actor);
}
};
QuickSettingsMenu.prototype.getColumnSpan = function (item) {
const value = new GObject.Value();
this._grid.layout_manager.child_get_property(this._grid, item, 'column-span', value);
const column_span = value.get_int();
value.unset();
return column_span;
};
QuickSettingsMenu.prototype.setColumnSpan = function (item, colSpan) {
this._grid.layout_manager.child_set_property(this._grid, item, 'column-span', colSpan);
};
export class LibPanel {
static _AutoHidable = AutoHidable;
static _Semitransparent = Semitransparent;
static _GridItem = GridItem;
static _DropZone = DropZone;
static _PanelGrid = PanelGrid;
static _PanelColumn = PanelColumn;
static get_instance() {
return Main.panel._libpanel;
}
static get VERSION() {
return LibPanel.get_instance()?.VERSION || VERSION;
}
// make the main panel available whether it's the gnome one or the libpanel one
static get main_panel() {
return LibPanel.get_instance()?._main_panel || QuickSettings.menu;
}
static get enabled() {
return LibPanel.enablers.length !== 0;
}
static get enablers() {
return LibPanel.get_instance()?._enablers || [];
}
static enable() {
let instance = LibPanel.get_instance();
if (!instance) {
instance = Main.panel._libpanel = new LibPanel();
instance._enable();
};
if (instance.constructor.VERSION != VERSION)
console.warn(`[LibPanel] ${get_extension_uuid()} depends on libpanel ${VERSION} but libpanel ${instance.constructor.VERSION} is loaded`);
const uuid = get_extension_uuid();
if (instance._enablers.indexOf(uuid) < 0) instance._enablers.push(uuid);
}
static disable() {
const instance = LibPanel.get_instance();
if (!instance) return;
const index = instance._enablers.indexOf(get_extension_uuid());
if (index > -1) instance._enablers.splice(index, 1);
if (instance._enablers.length === 0) {
instance._disable();
Main.panel._libpanel = undefined;
};
}
static addPanel(panel) {
const instance = LibPanel.get_instance();
if (!instance)
console.error(`[LibPanel] ${get_extension_uuid()} tried to add a panel, but the library is disabled.`);
if (instance._settings.get_boolean('padding-enabled'))
set_style(panel._grid, 'padding', `${instance._settings.get_int('padding')}px`);
if (instance._settings.get_boolean('row-spacing-enabled'))
set_style(panel._grid, 'spacing-rows', `${instance._settings.get_int('row-spacing')}px`);
if (instance._settings.get_boolean('column-spacing-enabled'))
set_style(panel._grid, 'spacing-columns', `${instance._settings.get_int('column-spacing')}px`);
instance._panel_grid._add_panel(panel);
instance._save_layout();
}
static removePanel(panel) {
panel._keep_layout = true;
panel.get_parent()?.remove_child(panel);
panel._keep_layout = undefined;
}
constructor() {
this._enablers = [];
this._patcher = null;
this._settings = null;
this._panel_grid = null;
this._old_menu = null;
}
_enable() {
const this_path = '/' + split(rsplit(import.meta.url, '/', 1)[0], '/', 3)[3];;
this._settings = get_settings(`${this_path}/org.gnome.shell.extensions.libpanel.gschema.xml`);
// ======================== Patching ========================
this._patcher = new Patcher();
// Permit disabling widget dragging
const _Draggable = DND.makeDraggable(new St.Widget()).constructor;
this._patcher.replace_method(_Draggable, function _grabActor(wrapped, device, touchSequence) {
if (this._disabled) return;
wrapped(device, touchSequence);
});
// Add named connections to objects
add_named_connections(this._patcher, GObject.Object);
// =================== Replacing the popup ==================
this._panel_grid = new PanelGrid(QuickSettings);
for (const column of this._settings.get_value("layout").recursiveUnpack().reverse()) {
this._panel_grid._add_column(column);
}
this._old_menu = this._replace_menu(this._panel_grid);
const new_menu = new Panel('', 2);
// we do that to prevent the name being this: `quick-settings-audio-panel@rayzeq.github.io/gnome@main`
new_menu.panel_name = 'gnome@main';
this._move_quick_settings(this._old_menu, new_menu);
LibPanel.addPanel(new_menu);
this._main_panel = new_menu;
// =================== Compatibility code ===================
//this._panel_grid.box = new_menu.box; // this would override existing properties
//this._panel_grid.actor = = new_menu.actor;
this._panel_grid._dimEffect = new_menu._dimEffect;
this._panel_grid._grid = new_menu._grid;
this._panel_grid._overlay = new_menu._overlay;
this._panel_grid._setDimmed = new_menu._setDimmed.bind(new_menu);
this._panel_grid.getFirstItem = new_menu.getFirstItem.bind(new_menu);
this._panel_grid.addItem = new_menu.addItem.bind(new_menu);
this._panel_grid.insertItemBefore = new_menu.insertItemBefore.bind(new_menu);
this._panel_grid._completeAddItem = new_menu._completeAddItem.bind(new_menu);
// ================== Visual customization ==================
const set_style_for_panels = (name, value) => {
for (const panel of this._panel_grid._get_panels()) {
set_style(panel._grid, name, value);
}
};
this._settings.connect('changed::padding-enabled', () => {
if (this._settings.get_boolean('padding-enabled'))
set_style_for_panels('padding', `${this._settings.get_int('padding')}px`);
else
set_style_for_panels('padding', null);
});
this._settings.connect('changed::padding', () => {
if (!this._settings.get_boolean('padding-enabled')) return;
set_style_for_panels('padding', `${this._settings.get_int('padding')}px`);
});
this._settings.connect('changed::row-spacing-enabled', () => {
if (this._settings.get_boolean('row-spacing-enabled'))
set_style_for_panels('spacing-rows', `${this._settings.get_int('row-spacing')}px`);
else
set_style_for_panels('spacing-rows', null);
});
this._settings.connect('changed::row-spacing', () => {
if (!this._settings.get_boolean('row-spacing-enabled')) return;
set_style_for_panels('spacing-rows', `${this._settings.get_int('row-spacing')}px`);
});
this._settings.connect('changed::column-spacing-enabled', () => {
if (this._settings.get_boolean('column-spacing-enabled'))
set_style_for_panels('spacing-columns', `${this._settings.get_int('column-spacing')}px`);
else
set_style_for_panels('spacing-columns', null);
});
this._settings.connect('changed::column-spacing', () => {
if (!this._settings.get_boolean('column-spacing-enabled')) return;
set_style_for_panels('spacing-columns', `${this._settings.get_int('column-spacing')}px`);
});
// https://gjs-docs.gnome.org/gio20~2.0/gio.settings#signal-changed
// "Note that @settings only emits this signal if you have read key at
// least once while a signal handler was already connected for key."
this._settings.get_boolean('padding-enabled');
this._settings.get_boolean('row-spacing-enabled');
this._settings.get_boolean('column-spacing-enabled');
this._settings.get_int('padding');
this._settings.get_int('row-spacing');
this._settings.get_int('column-spacing');
};
_disable() {
this._move_quick_settings(this._main_panel, this._old_menu);
this._replace_menu(this._old_menu);
this._old_menu = null;
this._panel_grid.destroy();
this._panel_grid = null;
this._settings = null;
this._patcher.unpatch_all();
this._patcher = null;
}
_replace_menu(new_menu) {
const old_menu = QuickSettings.menu;
MenuManager.removeMenu(old_menu);
Main.layoutManager.disconnectObject(old_menu);
QuickSettings.menu = null; // prevent old_menu from being destroyed
QuickSettings.setMenu(new_menu);
old_menu.actor.get_parent().remove_child(old_menu.actor);
MenuManager.addMenu(new_menu);
Main.layoutManager.connectObject('system-modal-opened', () => new_menu.close(), new_menu);
return old_menu;
}
_move_quick_settings(old_menu, new_menu) {
for (const item of old_menu.getItems()) {
const column_span = old_menu.getColumnSpan(item);
const visible = item.visible;
old_menu.removeItem(item);
new_menu.addItem(item, column_span);
item.visible = visible; // force reset of visibility
}
}
_save_layout() {
const layout = this._panel_grid._get_panel_layout();
// Remove leading empty columns
while (layout[0]?.length === 0) layout.shift();
this._settings.set_value(
"layout",
GLib.Variant.new_array(
GLib.VariantType.new('as'),
layout.map(column => GLib.Variant.new_strv(column))
)
);
}
};