diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6d7940f06..6dc01b924 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -40,6 +40,7 @@
ui/animation.js
ui/appDisplay.js
ui/appFavorites.js
+ ui/appMenu.js
ui/audioDeviceSelection.js
ui/backgroundMenu.js
ui/background.js
diff --git a/js/ui/appMenu.js b/js/ui/appMenu.js
new file mode 100644
index 000000000..ae6aa16a0
--- /dev/null
+++ b/js/ui/appMenu.js
@@ -0,0 +1,137 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported AppMenu */
+const { Gio, GLib, Shell, St } = imports.gi;
+
+const PopupMenu = imports.ui.popupMenu;
+const Main = imports.ui.main;
+
+var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
+ /**
+ * @param {Clutter.Actor} sourceActor - actor the menu is attached to
+ */
+ constructor(sourceActor) {
+ super(sourceActor, 0.5, St.Side.TOP);
+
+ this.actor.add_style_class_name('app-menu');
+
+ this._app = null;
+ this._appSystem = Shell.AppSystem.get_default();
+
+ this._windowsChangedId = 0;
+
+ /* Translators: This is the heading of a list of open windows */
+ this._openWindowsHeader = new PopupMenu.PopupSeparatorMenuItem(_('Open Windows'));
+ this.addMenuItem(this._openWindowsHeader);
+
+ this._windowSection = new PopupMenu.PopupMenuSection();
+ this.addMenuItem(this._windowSection);
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._newWindowItem = this.addAction(_('New Window'), () => {
+ this._app.open_new_window(-1);
+ });
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._actionSection = new PopupMenu.PopupMenuSection();
+ this.addMenuItem(this._actionSection);
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._detailsItem = this.addAction(_('Show Details'), async () => {
+ const id = this._app.get_id();
+ const args = GLib.Variant.new('(ss)', [id, '']);
+ const bus = await Gio.DBus.get(Gio.BusType.SESSION, null);
+ bus.call(
+ 'org.gnome.Software',
+ '/org/gnome/Software',
+ 'org.gtk.Actions', 'Activate',
+ new GLib.Variant('(sava{sv})', ['details', [args], null]),
+ null, 0, -1, null);
+ });
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this.addAction(_('Quit'), () => this._app.request_quit());
+
+ this._appSystem.connect('installed-changed',
+ () => this._updateDetailsVisibility());
+ this._updateDetailsVisibility();
+ }
+
+ _updateDetailsVisibility() {
+ const sw = this._appSystem.lookup_app('org.gnome.Software.desktop');
+ this._detailsItem.visible = sw !== null;
+ }
+
+ /**
+ * @returns {bool} - true if the menu is empty
+ */
+ isEmpty() {
+ if (!this._app)
+ return true;
+ return super.isEmpty();
+ }
+
+ /**
+ * @param {Shell.App} app - the app the menu represents
+ */
+ setApp(app) {
+ if (this._app === app)
+ return;
+
+ if (this._windowsChangedId)
+ this._app.disconnect(this._windowsChangedId);
+ this._windowsChangedId = 0;
+
+ this._app = app;
+
+ if (app) {
+ this._windowsChangedId = app.connect('windows-changed', () => {
+ this._updateWindowsSection();
+ });
+ }
+
+ this._updateWindowsSection();
+
+ const appInfo = app?.app_info;
+ const actions = appInfo?.list_actions() ?? [];
+
+ this._actionSection.removeAll();
+ actions.forEach(action => {
+ const label = appInfo.get_action_name(action);
+ this._actionSection.addAction(label, event => {
+ this._app.launch_action(action, event.get_time(), -1);
+ });
+ });
+
+ this._newWindowItem.visible =
+ app && app.can_open_new_window() && !actions.includes('new-window');
+ }
+
+ _updateWindowsSection() {
+ this._windowSection.removeAll();
+ this._openWindowsHeader.hide();
+
+ if (!this._app)
+ return;
+
+ const windows = this._app.get_windows();
+ if (windows.length < 2)
+ return;
+
+ this._openWindowsHeader.show();
+
+ windows.forEach(window => {
+ const title = window.title || this._app.get_name();
+ const item = this._windowSection.addAction(title, event => {
+ Main.activateWindow(window, event.get_time());
+ });
+ const id = window.connect('notify::title', () => {
+ item.label.text = window.title || this._app.get_name();
+ });
+ item.connect('destroy', () => window.disconnect(id));
+ });
+ }
+};
diff --git a/js/ui/panel.js b/js/ui/panel.js
index c039112d7..89b082ad0 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -1,10 +1,11 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Panel */
-const { Atk, Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+const { Atk, Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
const Cairo = imports.cairo;
const Animation = imports.ui.animation;
+const { AppMenu } = imports.ui.appMenu;
const Config = imports.misc.config;
const CtrlAltTab = imports.ui.ctrlAltTab;
const DND = imports.ui.dnd;
@@ -18,131 +19,6 @@ var APP_MENU_ICON_MARGIN = 0;
var BUTTON_DND_ACTIVATION_TIMEOUT = 250;
-class AppMenu extends PopupMenu.PopupMenu {
- constructor(sourceActor) {
- super(sourceActor, 0.5, St.Side.TOP);
-
- this.actor.add_style_class_name('app-menu');
-
- this._app = null;
- this._appSystem = Shell.AppSystem.get_default();
-
- this._windowsChangedId = 0;
-
- /* Translators: This is the heading of a list of open windows */
- this._openWindowsHeader = new PopupMenu.PopupSeparatorMenuItem(_('Open Windows'));
- this.addMenuItem(this._openWindowsHeader);
-
- this._windowSection = new PopupMenu.PopupMenuSection();
- this.addMenuItem(this._windowSection);
-
- this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
-
- this._newWindowItem = this.addAction(_("New Window"), () => {
- this._app.open_new_window(-1);
- });
-
- this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
-
- this._actionSection = new PopupMenu.PopupMenuSection();
- this.addMenuItem(this._actionSection);
-
- this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
-
- this._detailsItem = this.addAction(_('Show Details'), async () => {
- let id = this._app.get_id();
- let args = GLib.Variant.new('(ss)', [id, '']);
- const bus = await Gio.DBus.get(Gio.BusType.SESSION, null);
- bus.call(
- 'org.gnome.Software',
- '/org/gnome/Software',
- 'org.gtk.Actions', 'Activate',
- new GLib.Variant('(sava{sv})', ['details', [args], null]),
- null, 0, -1, null);
- });
-
- this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
-
- this.addAction(_("Quit"), () => {
- this._app.request_quit();
- });
-
- this._appSystem.connect('installed-changed', () => {
- this._updateDetailsVisibility();
- });
- this._updateDetailsVisibility();
- }
-
- _updateDetailsVisibility() {
- let sw = this._appSystem.lookup_app('org.gnome.Software.desktop');
- this._detailsItem.visible = sw != null;
- }
-
- isEmpty() {
- if (!this._app)
- return true;
- return super.isEmpty();
- }
-
- setApp(app) {
- if (this._app == app)
- return;
-
- if (this._windowsChangedId)
- this._app.disconnect(this._windowsChangedId);
- this._windowsChangedId = 0;
-
- this._app = app;
-
- if (app) {
- this._windowsChangedId = app.connect('windows-changed', () => {
- this._updateWindowsSection();
- });
- }
-
- this._updateWindowsSection();
-
- const appInfo = app?.app_info;
- const actions = appInfo?.list_actions() ?? [];
-
- this._actionSection.removeAll();
- actions.forEach(action => {
- let label = appInfo.get_action_name(action);
- this._actionSection.addAction(label, event => {
- this._app.launch_action(action, event.get_time(), -1);
- });
- });
-
- this._newWindowItem.visible =
- app && app.can_open_new_window() && !actions.includes('new-window');
- }
-
- _updateWindowsSection() {
- this._windowSection.removeAll();
- this._openWindowsHeader.hide();
-
- if (!this._app)
- return;
-
- let windows = this._app.get_windows();
- if (windows.length < 2)
- return;
-
- this._openWindowsHeader.show();
-
- windows.forEach(window => {
- let title = window.title || this._app.get_name();
- let item = this._windowSection.addAction(title, event => {
- Main.activateWindow(window, event.get_time());
- });
- let id = window.connect('notify::title', () => {
- item.label.text = window.title || this._app.get_name();
- });
- item.connect('destroy', () => window.disconnect(id));
- });
- }
-}
-
/**
* AppMenuButton:
*
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8e763c8f2..6340bff17 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -15,6 +15,7 @@ js/portalHelper/main.js
js/ui/accessDialog.js
js/ui/appDisplay.js
js/ui/appFavorites.js
+js/ui/appMenu.js
js/ui/audioDeviceSelection.js
js/ui/backgroundMenu.js
js/ui/calendar.js