diff --git a/.editorconfig b/.editorconfig
index 2ec8a01..572b7d2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,3 +13,7 @@ trim_trailing_whitespace = true
[*.md]
indent_size = 4
trim_trailing_whitespace = false
+
+[*.ui]
+indent_size = 4
+trim_trailing_whitespace = true
diff --git a/src/prefs-box-order-item-row.ui b/src/prefs-box-order-item-row.ui
new file mode 100644
index 0000000..a82e225
--- /dev/null
+++ b/src/prefs-box-order-item-row.ui
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/prefs-box-order-list-empty-placeholder.ui b/src/prefs-box-order-list-empty-placeholder.ui
new file mode 100644
index 0000000..11803f8
--- /dev/null
+++ b/src/prefs-box-order-list-empty-placeholder.ui
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ 10
+ 10
+ 10
+ 10
+ 10
+
+
+ Drop items onto here to add them to this box.
+ start
+
+
+
+
+
+
+
diff --git a/src/prefs-widget.ui b/src/prefs-widget.ui
new file mode 100644
index 0000000..a153981
--- /dev/null
+++ b/src/prefs-widget.ui
@@ -0,0 +1,101 @@
+
+
+
+
+ vertical
+ 24
+ 36
+ 36
+ 36
+ 36
+
+
+ Simply use drag and drop to order the items any way you want.
+ start
+
+
+
+
+
+ vertical
+
+
+ Left Top Bar Box
+ start
+ 12
+
+
+
+
+
+
+
+ none
+ True
+
+
+
+
+
+
+ vertical
+
+
+ Center Top Bar Box
+ start
+ 12
+
+
+
+
+
+
+
+ none
+ True
+
+
+
+
+
+
+ vertical
+
+
+ Right Top Bar Box
+ start
+ 12
+
+
+
+
+
+
+
+ none
+ True
+
+
+
+
+
+
diff --git a/src/prefs.js b/src/prefs.js
new file mode 100644
index 0000000..0273332
--- /dev/null
+++ b/src/prefs.js
@@ -0,0 +1,238 @@
+/*
+ * This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
+ * organizing your Gnome Shell top bar).
+ * Copyright (C) 2021 Julian Schacher
+ *
+ * Top-Bar-Organizer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/* exported buildPrefsWidget, init */
+"use strict";
+
+const Gtk = imports.gi.Gtk;
+const Gdk = imports.gi.Gdk;
+const GObject = imports.gi.GObject;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+const Me = ExtensionUtils.getCurrentExtension();
+
+var PrefsBoxOrderListEmptyPlaceholder = GObject.registerClass({
+ GTypeName: "PrefsBoxOrderListEmptyPlaceholder",
+ Template: Me.dir.get_child("prefs-box-order-list-empty-placeholder.ui").get_uri()
+}, class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
+ _init(params = {}) {
+ super._init(params);
+
+ /// Make `this` accept drops by creating a drop target and adding it to
+ /// `this`.
+ let dropTarget = new Gtk.DropTarget();
+ dropTarget.set_gtypes([GObject.type_from_name("PrefsBoxOrderItemRow")]);
+ dropTarget.set_actions(Gdk.DragAction.MOVE);
+ // Handle a new drop on `this` properly.
+ // `value` is the thing getting dropped.
+ dropTarget.connect("drop", (target, value) => {
+ // Get the GtkListBoxes of `this` and the drop value.
+ const ownListBox = this.get_parent();
+ const valueListBox = value.get_parent();
+
+ // Remove the drop value from its list box.
+ valueListBox.remove(value);
+
+ // Insert the drop value into the list box of `this`.
+ ownListBox.insert(value, 0);
+
+ /// Finally save the box orders to settings.
+ const settings = ExtensionUtils.getSettings();
+
+ settings.set_strv(ownListBox.boxOrder, [value.item]);
+
+ let updatedBoxOrder = [ ];
+ for (let potentialListBoxRow of valueListBox) {
+ // Only process PrefsBoxOrderItemRows.
+ if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
+ continue;
+ }
+
+ const item = potentialListBoxRow.item;
+ updatedBoxOrder.push(item);
+ }
+ settings.set_strv(valueListBox.boxOrder, updatedBoxOrder);
+ });
+ this.add_controller(dropTarget);
+ }
+});
+
+var PrefsBoxOrderItemRow = GObject.registerClass({
+ GTypeName: "PrefsBoxOrderItemRow",
+ Template: Me.dir.get_child("prefs-box-order-item-row.ui").get_uri(),
+ Children: ["item-name-display-label"]
+}, class PrefsBoxOrderItemRow extends Gtk.ListBoxRow {
+ _init(params = {}) {
+ super._init(params);
+
+ // Make `this` draggable by creating a drag source and adding it to
+ // `this`.
+ let dragSource = new Gtk.DragSource();
+ dragSource.set_actions(Gdk.DragAction.MOVE);
+ dragSource.connect("prepare", () => {
+ return Gdk.ContentProvider.new_for_value(this);
+ });
+ this.add_controller(dragSource);
+
+ /// Make `this` accept drops by creating a drop target and adding it to
+ /// `this`.
+ let dropTarget = new Gtk.DropTarget();
+ dropTarget.set_gtypes([this.constructor.$gtype]);
+ dropTarget.set_actions(Gdk.DragAction.MOVE);
+ // Handle a new drop on `this` properly.
+ // `value` is the thing getting dropped.
+ dropTarget.connect("drop", (target, value) => {
+ // If `this` got dropped onto itself, do nothing.
+ if (value === this) {
+ return;
+ }
+
+ // Get the GtkListBoxes of `this` and the drop value.
+ const ownListBox = this.get_parent();
+ const valueListBox = value.get_parent();
+
+ // Get the position of `this` and the drop value.
+ const ownPosition = this.get_index();
+ const valuePosition = value.get_index();
+
+ // Remove the drop value from its list box.
+ valueListBox.remove(value);
+
+ // Since an element got potentially removed from the list of `this`,
+ // get the position of `this` again.
+ const updatedOwnPosition = this.get_index();
+
+ if (ownListBox !== valueListBox) {
+ // First handle the case where `this` and the drop value are in
+ // different list boxes.
+ if ((ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "left-box-order")
+ || (ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "center-box-order")
+ || (ownListBox.boxOrder === "center-box-order" && valueListBox.boxOrder === "left-box-order")) {
+ // If the list box of the drop value comes before the list
+ // box of `this`, add the drop value after `this`.
+ ownListBox.insert(value, updatedOwnPosition + 1);
+ } else {
+ // Otherwise, add the drop value where `this` currently is.
+ ownListBox.insert(value, updatedOwnPosition);
+ }
+ } else {
+ if (valuePosition < ownPosition) {
+ // If the drop value was before `this`, add the drop value
+ // after `this`.
+ ownListBox.insert(value, updatedOwnPosition + 1);
+ } else {
+ // Otherwise, add the drop value where `this` currently is.
+ ownListBox.insert(value, updatedOwnPosition);
+ }
+ }
+
+ /// Finally save the box orders to settings.
+ const settings = ExtensionUtils.getSettings();
+
+ let updatedBoxOrder = [ ];
+ for (let potentialListBoxRow of ownListBox) {
+ // Only process PrefsBoxOrderItemRows.
+ if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
+ continue;
+ }
+
+ const item = potentialListBoxRow.item;
+ updatedBoxOrder.push(item);
+ }
+ settings.set_strv(ownListBox.boxOrder, updatedBoxOrder);
+
+ // If the list boxes of `this` and the drop value were different,
+ // save an updated box order for the list were the drop value was in
+ // as well.
+ if (ownListBox !== valueListBox) {
+ let updatedBoxOrder = [ ];
+ for (let potentialListBoxRow of valueListBox) {
+ // Only process PrefsBoxOrderItemRows.
+ if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
+ continue;
+ }
+
+ const item = potentialListBoxRow.item;
+ updatedBoxOrder.push(item);
+ }
+ settings.set_strv(valueListBox.boxOrder, updatedBoxOrder);
+ }
+ });
+ this.add_controller(dropTarget);
+ }
+});
+
+var PrefsWidget = GObject.registerClass({
+ GTypeName: "PrefsWidget",
+ Template: Me.dir.get_child("prefs-widget.ui").get_uri(),
+ InternalChildren: [
+ "left-box-order",
+ "center-box-order",
+ "right-box-order"
+ ]
+}, class PrefsWidget extends Gtk.Box {
+ _init(params = {}) {
+ super._init(params);
+
+ this._settings = ExtensionUtils.getSettings();
+
+ // Initialize the given `gtkListBox`.
+ const initializeGtkListBox = (boxOrder, gtkListBox) => {
+ // Add the items of the given configured box order as
+ // GtkListBoxRows.
+ for (const item of boxOrder) {
+ const listBoxRow = new PrefsBoxOrderItemRow();
+
+ listBoxRow.item = item;
+ if (item.startsWith("appindicator-kstatusnotifieritem-")) {
+ // Set `item_name_display_label` of the `listBoxRow` to
+ // something nicer, if the associated item is an
+ // AppIndicator/KStatusNotifierItem item.
+ listBoxRow.item_name_display_label.set_label(item.replace("appindicator-kstatusnotifieritem-", ""));
+ } else {
+ // Otherwise just set the `item_name_display_label` of the
+ // `listBoxRow` to `item`.
+ listBoxRow.item_name_display_label.set_label(item);
+ }
+ gtkListBox.append(listBoxRow);
+ }
+
+ // Add a placeholder widget for the case, where `gtkListBox` doesn't
+ // have any GtkListBoxRows.
+ gtkListBox.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder());
+ };
+
+ initializeGtkListBox(this._settings.get_strv("left-box-order"), this._left_box_order);
+ initializeGtkListBox(this._settings.get_strv("center-box-order"), this._center_box_order);
+ initializeGtkListBox(this._settings.get_strv("right-box-order"), this._right_box_order);
+
+ // Set the box order each GtkListBox is associated with.
+ // This is needed by the reordering of the GtkListBoxRows, so that the
+ // updated box orders can be saved.
+ this._left_box_order.boxOrder = "left-box-order";
+ this._center_box_order.boxOrder = "center-box-order";
+ this._right_box_order.boxOrder = "right-box-order";
+ }
+});
+
+function buildPrefsWidget() {
+ return new PrefsWidget();
+}
+
+function init() {
+}