From 69343573af6946f86e27c040523a6786b1ef779f Mon Sep 17 00:00:00 2001 From: oxmc7769 Date: Sat, 4 Apr 2026 06:01:59 -0700 Subject: [PATCH] Add basic app menu --- debian/changelog | 49 ++ debian/rules | 2 + ...ll.extensions.vesperos-taskbar.gschema.xml | 5 + src/startMenu.js | 742 ++++++++++++++++++ src/stylesheet.css | 170 ++++ src/taskbar.js | 62 +- src/themes/win10-dark.css | 162 ++++ src/themes/win10-light.css | 162 ++++ src/themes/win11-dark.css | 165 ++++ src/themes/win11-light.css | 165 ++++ 10 files changed, 1643 insertions(+), 41 deletions(-) create mode 100644 src/startMenu.js create mode 100644 src/themes/win10-dark.css create mode 100644 src/themes/win10-light.css create mode 100644 src/themes/win11-dark.css create mode 100644 src/themes/win11-light.css diff --git a/debian/changelog b/debian/changelog index 1bd4e12..9f88181 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,52 @@ +vesperos-taskbar (26.3) vesperos; urgency=medium + + * Add Windows 10-style Start Menu replacing the GNOME app grid popup. + - Clicking the Show Apps button now opens a floating Win10-style menu + instead of the GNOME overview app grid. + - Includes a search bar that filters installed applications in real time. + - "Pinned" view shows favourites + running apps in a 4-column icon grid. + - "All apps" view shows an alphabetically sorted, scrollable app list + with letter dividers. + - Footer row shows the current user name and a power button with a + submenu for Lock, Sign out, Sleep, Restart, and Shut down. + - Menu is positioned above/beside the Show Apps button depending on + panel side; clamped to monitor bounds on multi-monitor setups. + - Closes on Escape key, outside click, or when the GNOME overview opens. + - Right-click context menu on the Show Apps button is unchanged. + + -- VesperOS Desktop Team Sat, 04 Apr 2026 12:00:00 +0000 + +vesperos-taskbar (26.2) vesperos; urgency=medium + + * Re-based on upstream version 73.1 (additional bug fixes). + * Fix crash in activateFirstWindow when window list is empty. + * Fix memory leaks: properly destroy menus in TaskbarAppIcon and + ShowAppsIconWrapper on extension disable. + * Fix extension enable stalling when Zorin Dash is already disabled. + * Fix intellihide panel visibility logic to avoid unintended hide. + * Fix overview disable to end hotkey preview cycle before teardown. + * Fix null-dereference on intellihide reference in panel toggle handler. + * Fix show-desktop button removal to clean up pending timeout and + restore hidden workspace state before destroying the button. + * Fix panelManager cleanup of boxPointer signal ID on disable. + * Fix Workspace prototype not restored when extension is disabled + while the overview spread is active. + * Fix panel.outerSize reference to panel.geom.outerSize. + * Guard _newLookingGlassResize against missing primary monitor panel. + * Fix taskbar destroy to clean up _onStageKeyPress override when the + overview is open at disable time. + * Fix workspace signal disconnect to tolerate removed dynamic workspaces. + * Fix menu-state-changed signal connected before item container exists. + * Fix animateWindowOpacity reading initialOpacity before window + actor reassignment. + * Fix ColorUtils RGB-to-HSV conversion variable shadowing bug. + * Fix windowPreview workspace.activate to always restore _shouldAnimate + via try/finally. + * Fix windowPreview set_child_at_index with null parent guard and + index clamping. + + -- VesperOS Desktop Team Fri, 03 Apr 2026 15:30:00 +0000 + vesperos-taskbar (26.1) vesperos; urgency=medium * VesperOS fork of Zorin OS's gnome-shell-extension-zorin-taskbar package. diff --git a/debian/rules b/debian/rules index 5acc1e5..110f031 100755 --- a/debian/rules +++ b/debian/rules @@ -59,6 +59,8 @@ override_dh_install: install -m 644 $$lang_dir/LC_MESSAGES/gtk4-ding.mo \ $(PKG_DIR)/usr/share/locale/$$lang/LC_MESSAGES/; \ done + # --- Start Menu themes --- + cp -r $(CURDIR)/src/themes $(EXT_DIR)/themes override_dh_fixperms: dh_fixperms diff --git a/schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml b/schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml index 0b4bfc9..6b071ce 100644 --- a/schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml +++ b/schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml @@ -854,5 +854,10 @@ '' The preferences page name to display + + '' + Custom stylesheet for the Start Menu + Path to a CSS file to load on top of the built-in Start Menu styles. Leave empty to use defaults. + diff --git a/src/startMenu.js b/src/startMenu.js new file mode 100644 index 0000000..3f3d83e --- /dev/null +++ b/src/startMenu.js @@ -0,0 +1,742 @@ +/* + * This file is part of the VesperOS system-taskbar extension. + * + * This program 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 2 of the License, or + * (at your option) any later version. + */ + +import Clutter from 'gi://Clutter' +import Gio from 'gi://Gio' +import GLib from 'gi://GLib' +import GObject from 'gi://GObject' +import Pango from 'gi://Pango' +import Shell from 'gi://Shell' +import St from 'gi://St' + +import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js' +import * as Main from 'resource:///org/gnome/shell/ui/main.js' +import * as Util from 'resource:///org/gnome/shell/misc/util.js' +import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js' + +import * as Utils from './utils.js' +import { SETTINGS } from './extension.js' + +const MENU_WIDTH = 460 +const MENU_HEIGHT = 560 +const APP_ICON_SIZE = 48 +const GRID_COLUMNS = 4 +const CELL_SIZE = Math.floor(MENU_WIDTH / GRID_COLUMNS) // 115 + +export const Win10StartMenu = GObject.registerClass( + class Win10StartMenu extends St.Widget { + _init(dtpPanel) { + super._init({ + style_class: 'win10-start-menu', + reactive: true, + visible: false, + width: MENU_WIDTH, + height: MENU_HEIGHT, + }) + + this._dtpPanel = dtpPanel + this._appSystem = Shell.AppSystem.get_default() + this.isOpen = false + this._captureId = 0 + this._focusId = 0 + this._sourceActor = null + this._customStylesheet = null + this._powerPopup = null + this._timeoutsHandler = new Utils.TimeoutsHandler() + + this._buildUI() + Main.uiGroup.add_child(this) + + this._signalsHandler = new Utils.GlobalSignalsHandler() + this._signalsHandler.add( + [Main.overview, 'showing', () => { if (this.isOpen) this.close() }], + [this._appSystem, 'installed-changed', () => { + this._allAppsList.remove_all_children() + }], + [SETTINGS, 'changed::start-menu-stylesheet', () => this._reloadStylesheet()], + ) + + this._reloadStylesheet() + } + + _reloadStylesheet() { + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme() + + if (this._customStylesheet) { + theme.unload_stylesheet(this._customStylesheet) + this._customStylesheet = null + } + + let path = SETTINGS.get_string('start-menu-stylesheet').trim() + if (path) { + let file = Gio.File.new_for_path(path) + if (file.query_exists(null)) { + this._customStylesheet = file + theme.load_stylesheet(this._customStylesheet) + } + } + } + + _buildUI() { + let outer = new St.BoxLayout({ + style_class: 'win10-start-menu-inner', + vertical: true, + reactive: true, + x_expand: true, + y_expand: true, + }) + + // ── Search bar ────────────────────────────────────────────────────────── + this._searchEntry = new St.Entry({ + style_class: 'win10-search-entry', + hint_text: _('Search apps'), + can_focus: true, + x_expand: true, + }) + this._searchEntry.clutter_text.connect( + 'text-changed', + () => this._onSearchTextChanged(), + ) + outer.add_child(this._searchEntry) + + // ── Content stack (pinned / all-apps / search results) ────────────────── + this._stack = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + y_expand: true, + x_expand: true, + }) + + this._pinnedView = this._buildPinnedView() + this._allAppsView = this._buildAllAppsView() + this._searchView = this._buildSearchView() + this._allAppsView.visible = false + this._searchView.visible = false + + this._stack.add_child(this._pinnedView) + this._stack.add_child(this._allAppsView) + this._stack.add_child(this._searchView) + outer.add_child(this._stack) + + // ── Footer ────────────────────────────────────────────────────────────── + outer.add_child(this._buildFooter()) + this._outer = outer + this.add_child(outer) + } + + _buildPinnedView() { + let view = new St.BoxLayout({ + vertical: true, + x_expand: true, + y_expand: true, + }) + + let header = new St.BoxLayout({ style_class: 'win10-section-header' }) + header.add_child( + new St.Label({ + text: _('Pinned'), + style_class: 'win10-section-label', + x_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }), + ) + let allAppsBtn = new St.Button({ + label: _('All apps ›'), + style_class: 'win10-link-button', + y_align: Clutter.ActorAlign.CENTER, + can_focus: true, + }) + allAppsBtn.connect('clicked', () => this._showView('all')) + header.add_child(allAppsBtn) + view.add_child(header) + + let scroll = new St.ScrollView({ + style_class: 'win10-apps-scroll', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC, + y_expand: true, + x_expand: true, + }) + this._pinnedGrid = new St.BoxLayout({ + style_class: 'win10-app-grid', + vertical: true, + x_expand: true, + }) + scroll.add_child(this._pinnedGrid) + view.add_child(scroll) + return view + } + + _buildAllAppsView() { + let view = new St.BoxLayout({ + vertical: true, + x_expand: true, + y_expand: true, + }) + + let header = new St.BoxLayout({ style_class: 'win10-section-header' }) + + let backBtn = new St.Button({ + style_class: 'win10-link-button', + y_align: Clutter.ActorAlign.CENTER, + can_focus: true, + }) + let backBox = new St.BoxLayout({ y_align: Clutter.ActorAlign.CENTER }) + backBox.add_child( + new St.Icon({ + icon_name: 'go-previous-symbolic', + icon_size: 14, + style_class: 'win10-back-icon', + }), + ) + backBox.add_child( + new St.Label({ text: _('Back'), y_align: Clutter.ActorAlign.CENTER }), + ) + backBtn.set_child(backBox) + backBtn.connect('clicked', () => this._showView('pinned')) + header.add_child(backBtn) + header.add_child( + new St.Label({ + text: _('All apps'), + style_class: 'win10-section-label', + x_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }), + ) + view.add_child(header) + + let scroll = new St.ScrollView({ + style_class: 'win10-apps-scroll', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC, + y_expand: true, + x_expand: true, + }) + this._allAppsList = new St.BoxLayout({ vertical: true, x_expand: true }) + scroll.add_child(this._allAppsList) + view.add_child(scroll) + return view + } + + _buildSearchView() { + let view = new St.BoxLayout({ + vertical: true, + x_expand: true, + y_expand: true, + }) + view.add_child( + new St.Label({ + text: _('Search results'), + style_class: 'win10-section-label', + style: 'margin: 8px 12px 4px;', + }), + ) + let scroll = new St.ScrollView({ + style_class: 'win10-apps-scroll', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC, + y_expand: true, + x_expand: true, + }) + this._searchList = new St.BoxLayout({ vertical: true, x_expand: true }) + scroll.add_child(this._searchList) + view.add_child(scroll) + return view + } + + _buildFooter() { + let footer = new St.BoxLayout({ + style_class: 'win10-footer', + x_expand: true, + }) + + // User button (left) + let userBtn = new St.Button({ + style_class: 'win10-footer-button', + x_expand: true, + x_align: Clutter.ActorAlign.START, + can_focus: true, + }) + let userBox = new St.BoxLayout() + userBox.add_child( + new St.Icon({ + icon_name: 'avatar-default-symbolic', + icon_size: 20, + style_class: 'win10-footer-icon', + }), + ) + let realName = GLib.get_real_name() || '' + let displayName = + realName && realName !== 'Unknown' ? realName : GLib.get_user_name() + userBox.add_child( + new St.Label({ + text: displayName, + style_class: 'win10-footer-label', + y_align: Clutter.ActorAlign.CENTER, + }), + ) + userBtn.set_child(userBox) + footer.add_child(userBtn) + + // Power button (right) + let powerBtn = new St.Button({ + style_class: 'win10-footer-button win10-power-button', + can_focus: true, + }) + powerBtn.set_child( + new St.Icon({ icon_name: 'system-shutdown-symbolic', icon_size: 20 }), + ) + powerBtn.connect('clicked', () => this._openPowerMenu(powerBtn)) + this._powerBtn = powerBtn + footer.add_child(powerBtn) + + return footer + } + + _openPowerMenu(powerBtn) { + // Toggle: clicking again while open closes it + if (this._powerPopup) { + this._powerPopup.destroy() + return + } + + let popup = new St.BoxLayout({ + style_class: 'win10-power-popup', + vertical: true, + reactive: true, + }) + + for (let { label, icon, fn } of [ + { + label: _('Lock'), + icon: 'changes-prevent-symbolic', + fn: () => { + this.close() + Util.spawn(['loginctl', 'lock-session']) + }, + }, + { + label: _('Sign out'), + icon: 'system-log-out-symbolic', + fn: () => Util.spawn(['gnome-session-quit', '--logout']), + }, + { + label: _('Sleep'), + icon: 'weather-clear-night-symbolic', + fn: () => { + this.close() + Util.spawn(['systemctl', 'suspend']) + }, + }, + { + label: _('Restart'), + icon: 'system-reboot-symbolic', + fn: () => Util.spawn(['gnome-session-quit', '--reboot']), + }, + { + label: _('Shut down'), + icon: 'system-shutdown-symbolic', + fn: () => Util.spawn(['gnome-session-quit', '--power-off']), + }, + ]) { + let btn = new St.Button({ + style_class: 'win10-power-item', + x_align: Clutter.ActorAlign.FILL, + x_expand: true, + can_focus: true, + }) + let box = new St.BoxLayout() + box.add_child( + new St.Icon({ + icon_name: icon, + icon_size: 16, + style_class: 'win10-power-icon', + }), + ) + box.add_child( + new St.Label({ + text: label, + style_class: 'win10-power-label', + y_align: Clutter.ActorAlign.CENTER, + }), + ) + btn.set_child(box) + btn.connect('clicked', () => { + popup.destroy() + fn() + }) + popup.add_child(btn) + } + + Main.uiGroup.add_child(popup) + this._powerPopup = popup + + // Position above power button after allocation + let idleId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + idleId = 0 + if (!popup.get_parent()) return GLib.SOURCE_REMOVE + let [bx, by] = powerBtn.get_transformed_position() + popup.set_position( + Math.round(bx + powerBtn.width - popup.width), + Math.round(by - popup.height), + ) + return GLib.SOURCE_REMOVE + }) + + let grabDisconnected = false + let grabId = global.stage.connect('captured-event', (s, e) => { + if (e.type() === Clutter.EventType.BUTTON_PRESS) { + let src = e.get_source() + if (!popup.contains(src) && src !== powerBtn) { + grabDisconnected = true + global.stage.disconnect(grabId) + if (popup.get_parent()) popup.destroy() + } + } + return Clutter.EVENT_PROPAGATE + }) + + popup.connect('destroy', () => { + if (idleId) { + GLib.source_remove(idleId) + idleId = 0 + } + if (!grabDisconnected) { + grabDisconnected = true + global.stage.disconnect(grabId) + } + this._powerPopup = null + }) + } + + _populatePinnedGrid() { + this._pinnedGrid.remove_all_children() + + let favorites = AppFavorites.getAppFavorites().getFavorites() + let favIds = new Set(favorites.map(a => a.get_id())) + let others = Gio.AppInfo.get_all() + .filter(info => info.should_show() && !favIds.has(info.get_id())) + .map(info => this._appSystem.lookup_app(info.get_id())) + .filter(app => app !== null) + .sort((a, b) => a.get_name().localeCompare(b.get_name())) + let apps = [...favorites, ...others] + + let row = null + apps.forEach((app, i) => { + if (i % GRID_COLUMNS === 0) { + row = new St.BoxLayout({ x_expand: true }) + this._pinnedGrid.add_child(row) + } + row.add_child(this._makeGridButton(app)) + }) + } + + _populateAllAppsList() { + this._allAppsList.remove_all_children() + + let apps = Gio.AppInfo.get_all() + .filter(info => info.should_show()) + .map(info => this._appSystem.lookup_app(info.get_id())) + .filter(app => app !== null) + .sort((a, b) => a.get_name().localeCompare(b.get_name())) + + let currentLetter = null + for (let app of apps) { + let firstChar = (app.get_name()[0] || '#').toUpperCase() + if (firstChar !== currentLetter) { + currentLetter = firstChar + this._allAppsList.add_child( + new St.Label({ text: firstChar, style_class: 'win10-alpha-sep' }), + ) + } + this._allAppsList.add_child(this._makeListButton(app)) + } + } + + _makeGridButton(app) { + let btn = new St.Button({ + style_class: 'win10-grid-app-btn', + width: CELL_SIZE, + height: CELL_SIZE, + reactive: true, + track_hover: true, + can_focus: true, + }) + let box = new St.BoxLayout({ + vertical: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + }) + box.add_child(app.create_icon_texture(APP_ICON_SIZE)) + let label = new St.Label({ + text: app.get_name(), + style_class: 'win10-grid-app-label', + x_align: Clutter.ActorAlign.CENTER, + }) + label.clutter_text.ellipsize = Pango.EllipsizeMode.END + label.clutter_text.line_wrap = false + box.add_child(label) + btn.set_child(box) + btn.connect('clicked', () => { + this.close() + app.activate() + }) + return btn + } + + _makeListButton(app) { + let btn = new St.Button({ + style_class: 'win10-list-app-btn', + x_expand: true, + x_align: Clutter.ActorAlign.FILL, + reactive: true, + track_hover: true, + can_focus: true, + }) + let box = new St.BoxLayout({ + x_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }) + let icon = app.create_icon_texture(24) + box.add_child(icon) + box.add_child( + new St.Label({ + text: app.get_name(), + style_class: 'win10-list-app-label', + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }), + ) + btn.set_child(box) + btn.connect('clicked', () => { + this.close() + app.activate() + }) + return btn + } + + _showView(which) { + this._pinnedView.visible = which === 'pinned' + this._allAppsView.visible = which === 'all' + this._searchView.visible = which === 'search' + + if (which === 'all' && this._allAppsList.get_n_children() === 0) { + this._populateAllAppsList() + } + } + + _onSearchTextChanged() { + this._timeoutsHandler.add(['searchDebounce', 150, () => { + let query = this._searchEntry.get_text().trim().toLowerCase() + if (query.length === 0) this._showView('pinned') + else this._doSearch(query) + }]) + } + + _doSearch(query) { + this._searchList.remove_all_children() + + let apps = Gio.AppInfo.get_all() + .filter(info => { + if (!info.should_show()) return false + let name = info.get_name().toLowerCase() + let desc = (info.get_description() || '').toLowerCase() + return name.includes(query) || desc.includes(query) + }) + .map(info => this._appSystem.lookup_app(info.get_id())) + .filter(app => app !== null) + .sort((a, b) => a.get_name().localeCompare(b.get_name())) + + if (apps.length === 0) { + this._searchList.add_child( + new St.Label({ + text: _('No results found'), + style_class: 'win10-no-results', + x_align: Clutter.ActorAlign.CENTER, + }), + ) + } else { + for (let app of apps) { + this._searchList.add_child(this._makeListButton(app)) + } + } + + this._showView('search') + } + + open(sourceActor) { + if (this.isOpen) return + this.isOpen = true + this._sourceActor = sourceActor || null + this.remove_all_transitions() + + this._searchEntry.set_text('') + this._populatePinnedGrid() + this._showView('pinned') + this._updatePosition(sourceActor) + + this.visible = true + this.opacity = 0 + this.translation_y = 10 + this.ease({ + opacity: 255, + translation_y: 0, + duration: 180, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }) + + // Defer captured-event + focus until after the triggering button-press + // finishes propagating, so we don't catch our own opening click. + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + if (!this.isOpen) return GLib.SOURCE_REMOVE + this._captureId = global.stage.connect( + 'captured-event', this._onCapturedEvent.bind(this), + ) + // Close when an app window gains focus (Wayland: window clicks bypass stage) + this._focusId = global.display.connect( + 'notify::focus-window', () => { if (this.isOpen) this.close() }, + ) + this._searchEntry.grab_key_focus() + return GLib.SOURCE_REMOVE + }) + } + + close() { + if (!this.isOpen) return + this.isOpen = false + if (this._captureId) { + global.stage.disconnect(this._captureId) + this._captureId = 0 + } + if (this._focusId) { + global.display.disconnect(this._focusId) + this._focusId = 0 + } + this._sourceActor = null + this.remove_all_transitions() + + if (this._powerPopup) { + this._powerPopup.destroy() + this._powerPopup = null + } + + this.ease({ + opacity: 0, + translation_y: 10, + duration: 120, + mode: Clutter.AnimationMode.EASE_IN_QUAD, + onComplete: () => { + if (!this.isOpen) this.visible = false + }, + }) + } + + toggle(sourceActor) { + if (this.isOpen) this.close() + else this.open(sourceActor) + } + + _updatePosition(sourceActor) { + let monitor = this._dtpPanel.monitor + let panelPos = this._dtpPanel.geom.position + + let menuX, menuY + + if (sourceActor) { + let [ax, ay] = sourceActor.get_transformed_position() + let aw = sourceActor.width + let ah = sourceActor.height + + switch (panelPos) { + case St.Side.BOTTOM: + menuX = ax + menuY = ay - MENU_HEIGHT + break + case St.Side.TOP: + menuX = ax + menuY = ay + ah + break + case St.Side.LEFT: + menuX = ax + aw + menuY = ay + break + case St.Side.RIGHT: + menuX = ax - MENU_WIDTH + menuY = ay + break + default: + menuX = ax + menuY = ay - MENU_HEIGHT + } + } else { + menuX = monitor.x + 4 + menuY = monitor.y + monitor.height - MENU_HEIGHT + } + + // Clamp to monitor bounds + menuX = Math.max( + monitor.x + 4, + Math.min(menuX, monitor.x + monitor.width - MENU_WIDTH - 4), + ) + menuY = Math.max( + monitor.y + 4, + Math.min(menuY, monitor.y + monitor.height - MENU_HEIGHT - 4), + ) + + this.set_size(MENU_WIDTH, MENU_HEIGHT) + this._outer.set_size(MENU_WIDTH, MENU_HEIGHT) + this.set_position(Math.round(menuX), Math.round(menuY)) + } + + _onCapturedEvent(_actor, event) { + if (!this.isOpen) return Clutter.EVENT_PROPAGATE + + if (event.type() === Clutter.EventType.BUTTON_PRESS) { + let [ex, ey] = event.get_coords() + let [mx, my] = this.get_transformed_position() + if (ex < mx || ex > mx + this.width || ey < my || ey > my + this.height) { + // Let the source actor (show-apps button) handle its own toggle + let src = event.get_source() + if (this._sourceActor && this._sourceActor.contains(src)) + return Clutter.EVENT_PROPAGATE + this.close() + } + } else if (event.type() === Clutter.EventType.KEY_PRESS) { + if (event.get_key_symbol() === Clutter.KEY_Escape) { + this.close() + return Clutter.EVENT_STOP + } + } + + return Clutter.EVENT_PROPAGATE + } + + destroy() { + if (this._captureId) { + global.stage.disconnect(this._captureId) + this._captureId = 0 + } + if (this._focusId) { + global.display.disconnect(this._focusId) + this._focusId = 0 + } + if (this._powerPopup) { + this._powerPopup.destroy() + this._powerPopup = null + } + if (this._customStylesheet) { + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme() + theme.unload_stylesheet(this._customStylesheet) + this._customStylesheet = null + } + this._timeoutsHandler.destroy() + this._signalsHandler.destroy() + super.destroy() + } + }, +) diff --git a/src/stylesheet.css b/src/stylesheet.css index bf8bc87..2e19b88 100644 --- a/src/stylesheet.css +++ b/src/stylesheet.css @@ -201,3 +201,173 @@ #uiGroup.br25 .vesperostaskbarMainPanel .panel-button.clock-display .clock { border-radius: 25px; } + +/* ── Windows 10-style Start Menu ────────────────────────────────────────── */ + +.win10-start-menu { + background-color: #202020; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +/* Search bar */ +.win10-search-entry { + background-color: #2d2d2d; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 4px; + color: #ffffff; + font-size: 13px; + margin: 12px 12px 4px; + padding: 8px 12px; + caret-color: #0078d4; +} + +.win10-search-entry:focus { + border-color: #0078d4; +} + +/* Section headers */ +.win10-section-header { + padding: 8px 12px 4px; +} + +.win10-section-label { + color: #b0b0b0; + font-size: 12px; + font-weight: bold; +} + +.win10-link-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #b0b0b0; + font-size: 12px; + padding: 4px 8px; +} + +.win10-link-button:hover { + background-color: rgba(255, 255, 255, 0.08); + color: #ffffff; +} + +.win10-back-icon { + padding-right: 4px; +} + +/* App grid (pinned) */ +.win10-app-grid { + padding: 4px 0; +} + +.win10-grid-app-btn { + background-color: transparent; + border: none; + border-radius: 6px; + padding: 8px 4px; +} + +.win10-grid-app-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-grid-app-label { + color: #e0e0e0; + font-size: 11px; + margin-top: 4px; + text-align: center; + /* max-width matches CELL_SIZE so long names are clipped */ + max-width: 107px; +} + +/* App list (all apps / search results) */ +.win10-list-app-btn { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 6px 12px; +} + +.win10-list-app-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-list-app-label { + color: #e0e0e0; + font-size: 13px; + margin-left: 10px; +} + +.win10-alpha-sep { + color: #0078d4; + font-size: 13px; + font-weight: bold; + padding: 8px 12px 2px; +} + +.win10-no-results { + color: #888888; + font-size: 13px; + padding: 24px 12px; +} + +/* Footer */ +.win10-footer { + background-color: #141414; + border-top: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 0 0 8px 8px; + padding: 4px; +} + +.win10-footer-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #c0c0c0; + padding: 8px 12px; +} + +.win10-footer-button:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #ffffff; +} + +.win10-footer-icon { + padding-right: 4px; +} + +.win10-footer-label { + color: #c0c0c0; + font-size: 13px; + margin-left: 6px; +} + +/* Power popup */ +.win10-power-popup { + background-color: #1e1e1e; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 4px; + min-width: 168px; +} + +.win10-power-item { + background-color: transparent; + border: none; + border-radius: 4px; + color: #e0e0e0; + padding: 8px 12px; +} + +.win10-power-item:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-power-icon { + padding-right: 2px; +} + +.win10-power-label { + font-size: 13px; + margin-left: 8px; +} diff --git a/src/taskbar.js b/src/taskbar.js index c02bdc5..1e38ecc 100644 --- a/src/taskbar.js +++ b/src/taskbar.js @@ -39,6 +39,7 @@ import * as AppIcons from './appIcons.js' import * as PanelManager from './panelManager.js' import * as PanelSettings from './panelSettings.js' import * as Pos from './panelPositions.js' +import * as StartMenu from './startMenu.js' import * as Utils from './utils.js' import * as WindowPreview from './windowPreview.js' import { SETTINGS, tracker } from './extension.js' @@ -248,6 +249,9 @@ export const Taskbar = class extends EventEmitter { this._showAppsIcon.icon.setIconSize(this.iconSize) this._hookUpLabel(this._showAppsIcon, this._showAppsIconWrapper) + this._startMenu = new StartMenu.Win10StartMenu(panel) + this._ignoreShowAppsToggle = false + this._container.add_child(new St.Widget({ width: 0, reactive: false })) this._container.add_child(this._scrollView) @@ -396,6 +400,11 @@ export const Taskbar = class extends EventEmitter { this._waitIdleId = 0 } + if (this._startMenu) { + this._startMenu.destroy() + this._startMenu = null + } + this._timeoutsHandler.destroy() this.iconAnimator.destroy() @@ -1300,55 +1309,26 @@ export const Taskbar = class extends EventEmitter { } _onShowAppsButtonToggled() { - // Sync the status of the default appButtons. Only if the two statuses are - // different, that means the user interacted with the extension provided - // application button, cutomize the behaviour. Otherwise the shell has changed the - // status (due to the _syncShowAppsButtonToggled function below) and it - // has already performed the desired action. + // Guard against re-entry when we programmatically uncheck the button. + if (this._ignoreShowAppsToggle) return + + // Only act when the user interacted with our button (statuses differ). + // When the shell itself changes the status _syncShowAppsButtonToggled + // keeps them in sync, so we don't need to do anything here. let selector = SearchController if ( selector._showAppsButton && selector._showAppsButton.checked !== this.showAppsButton.checked ) { - // find visible view - if (this.showAppsButton.checked) { - //override escape key to return to the desktop when entering the overview using the showapps button - SearchController._onStageKeyPress = function (actor, event) { - if ( - Main.modalCount == 1 && - event.get_key_symbol() === Clutter.KEY_Escape - ) { - this._searchActive ? this.reset() : Main.overview.hide() + // Uncheck the toggle immediately – the start menu manages its own + // open/closed state independently of the button's checked property. + this._ignoreShowAppsToggle = true + this.showAppsButton.checked = false + this._ignoreShowAppsToggle = false - return Clutter.EVENT_STOP - } - - return Object.getPrototypeOf(this)._onStageKeyPress.call( - this, - actor, - event, - ) - } - - let overviewHiddenId = Main.overview.connect('hidden', () => { - Main.overview.disconnect(overviewHiddenId) - delete SearchController._onStageKeyPress - }) - - // force exiting overview if needed - if (!Main.overview._shown) { - this.forcedOverview = true - } - - //temporarily use as primary the monitor on which the showapps btn was clicked, this is - //restored by the panel when exiting the overview - this.dtpPanel.panelManager.setFocusedMonitor(this.dtpPanel.monitor) - - // Finally show the overview - selector._showAppsButton.checked = true - Main.overview.show(2 /*APP_GRID*/) + this._startMenu.toggle(this.showAppsButton) } else { if (this.forcedOverview) { // force exiting overview if needed diff --git a/src/themes/win10-dark.css b/src/themes/win10-dark.css new file mode 100644 index 0000000..ddc1956 --- /dev/null +++ b/src/themes/win10-dark.css @@ -0,0 +1,162 @@ +/* Windows 10 — Dark */ + +.win10-start-menu { + background-color: #202020; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.win10-search-entry { + background-color: #2d2d2d; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 4px; + color: #ffffff; + font-size: 13px; + margin: 12px 12px 4px; + padding: 8px 12px; + caret-color: #0078d4; +} + +.win10-search-entry:focus { + border-color: #0078d4; +} + +.win10-section-header { + padding: 8px 12px 4px; +} + +.win10-section-label { + color: #b0b0b0; + font-size: 12px; + font-weight: bold; +} + +.win10-link-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #b0b0b0; + font-size: 12px; + padding: 4px 8px; +} + +.win10-link-button:hover { + background-color: rgba(255, 255, 255, 0.08); + color: #ffffff; +} + +.win10-back-icon { + padding-right: 4px; +} + +.win10-app-grid { + padding: 4px 0; +} + +.win10-grid-app-btn { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 8px 4px; +} + +.win10-grid-app-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-grid-app-label { + color: #e0e0e0; + font-size: 11px; + margin-top: 4px; + text-align: center; + max-width: 107px; +} + +.win10-list-app-btn { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 6px 12px; +} + +.win10-list-app-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-list-app-label { + color: #e0e0e0; + font-size: 13px; + margin-left: 10px; +} + +.win10-alpha-sep { + color: #0078d4; + font-size: 13px; + font-weight: bold; + padding: 8px 12px 2px; +} + +.win10-no-results { + color: #888888; + font-size: 13px; + padding: 24px 12px; +} + +.win10-footer { + background-color: #141414; + border-top: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 0 0 4px 4px; + padding: 4px; +} + +.win10-footer-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #c0c0c0; + padding: 8px 12px; +} + +.win10-footer-button:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #ffffff; +} + +.win10-footer-icon { + padding-right: 4px; +} + +.win10-footer-label { + color: #c0c0c0; + font-size: 13px; + margin-left: 6px; +} + +.win10-power-popup { + background-color: #1e1e1e; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 4px; + min-width: 168px; +} + +.win10-power-item { + background-color: transparent; + border: none; + border-radius: 4px; + color: #e0e0e0; + padding: 8px 12px; +} + +.win10-power-item:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.win10-power-icon { + padding-right: 2px; +} + +.win10-power-label { + font-size: 13px; + margin-left: 8px; +} diff --git a/src/themes/win10-light.css b/src/themes/win10-light.css new file mode 100644 index 0000000..de6b9e3 --- /dev/null +++ b/src/themes/win10-light.css @@ -0,0 +1,162 @@ +/* Windows 10 — Light */ + +.win10-start-menu { + background-color: #f0f0f0; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +.win10-search-entry { + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.18); + border-radius: 4px; + color: #1a1a1a; + font-size: 13px; + margin: 12px 12px 4px; + padding: 8px 12px; + caret-color: #0078d4; +} + +.win10-search-entry:focus { + border-color: #0078d4; +} + +.win10-section-header { + padding: 8px 12px 4px; +} + +.win10-section-label { + color: #444444; + font-size: 12px; + font-weight: bold; +} + +.win10-link-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #444444; + font-size: 12px; + padding: 4px 8px; +} + +.win10-link-button:hover { + background-color: rgba(0, 0, 0, 0.07); + color: #1a1a1a; +} + +.win10-back-icon { + padding-right: 4px; +} + +.win10-app-grid { + padding: 4px 0; +} + +.win10-grid-app-btn { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 8px 4px; +} + +.win10-grid-app-btn:hover { + background-color: rgba(0, 0, 0, 0.07); +} + +.win10-grid-app-label { + color: #1a1a1a; + font-size: 11px; + margin-top: 4px; + text-align: center; + max-width: 107px; +} + +.win10-list-app-btn { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 6px 12px; +} + +.win10-list-app-btn:hover { + background-color: rgba(0, 0, 0, 0.07); +} + +.win10-list-app-label { + color: #1a1a1a; + font-size: 13px; + margin-left: 10px; +} + +.win10-alpha-sep { + color: #0078d4; + font-size: 13px; + font-weight: bold; + padding: 8px 12px 2px; +} + +.win10-no-results { + color: #666666; + font-size: 13px; + padding: 24px 12px; +} + +.win10-footer { + background-color: #dcdcdc; + border-top: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 0 0 4px 4px; + padding: 4px; +} + +.win10-footer-button { + background-color: transparent; + border: none; + border-radius: 4px; + color: #2a2a2a; + padding: 8px 12px; +} + +.win10-footer-button:hover { + background-color: rgba(0, 0, 0, 0.08); + color: #000000; +} + +.win10-footer-icon { + padding-right: 4px; +} + +.win10-footer-label { + color: #2a2a2a; + font-size: 13px; + margin-left: 6px; +} + +.win10-power-popup { + background-color: #e8e8e8; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + padding: 4px; + min-width: 168px; +} + +.win10-power-item { + background-color: transparent; + border: none; + border-radius: 4px; + color: #1a1a1a; + padding: 8px 12px; +} + +.win10-power-item:hover { + background-color: rgba(0, 0, 0, 0.07); +} + +.win10-power-icon { + padding-right: 2px; +} + +.win10-power-label { + font-size: 13px; + margin-left: 8px; +} diff --git a/src/themes/win11-dark.css b/src/themes/win11-dark.css new file mode 100644 index 0000000..ff511dd --- /dev/null +++ b/src/themes/win11-dark.css @@ -0,0 +1,165 @@ +/* Windows 11 — Dark + * Approximates the Win11 Mica/acrylic aesthetic: rounded corners, + * layered semi-transparent surfaces, and the lighter blue accent. */ + +.win10-start-menu { + background-color: #272727; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.win10-search-entry { + background-color: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + color: #ffffff; + font-size: 13px; + margin: 14px 14px 6px; + padding: 9px 16px; + caret-color: #60cdff; +} + +.win10-search-entry:focus { + border-color: #60cdff; + background-color: rgba(255, 255, 255, 0.09); +} + +.win10-section-header { + padding: 10px 14px 4px; +} + +.win10-section-label { + color: #a0a0a0; + font-size: 12px; + font-weight: bold; +} + +.win10-link-button { + background-color: transparent; + border: none; + border-radius: 6px; + color: #a0a0a0; + font-size: 12px; + padding: 5px 10px; +} + +.win10-link-button:hover { + background-color: rgba(255, 255, 255, 0.07); + color: #ffffff; +} + +.win10-back-icon { + padding-right: 4px; +} + +.win10-app-grid { + padding: 4px 2px; +} + +.win10-grid-app-btn { + background-color: transparent; + border: none; + border-radius: 8px; + padding: 10px 4px; +} + +.win10-grid-app-btn:hover { + background-color: rgba(255, 255, 255, 0.07); +} + +.win10-grid-app-label { + color: #e8e8e8; + font-size: 11px; + margin-top: 5px; + text-align: center; + max-width: 107px; +} + +.win10-list-app-btn { + background-color: transparent; + border: none; + border-radius: 6px; + padding: 7px 14px; +} + +.win10-list-app-btn:hover { + background-color: rgba(255, 255, 255, 0.07); +} + +.win10-list-app-label { + color: #e8e8e8; + font-size: 13px; + margin-left: 12px; +} + +.win10-alpha-sep { + color: #60cdff; + font-size: 13px; + font-weight: bold; + padding: 10px 14px 2px; +} + +.win10-no-results { + color: #777777; + font-size: 13px; + padding: 28px 14px; +} + +.win10-footer { + background-color: rgba(0, 0, 0, 0.25); + border-top: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 0 0 12px 12px; + padding: 6px; +} + +.win10-footer-button { + background-color: transparent; + border: none; + border-radius: 6px; + color: #c8c8c8; + padding: 9px 14px; +} + +.win10-footer-button:hover { + background-color: rgba(255, 255, 255, 0.09); + color: #ffffff; +} + +.win10-footer-icon { + padding-right: 4px; +} + +.win10-footer-label { + color: #c8c8c8; + font-size: 13px; + margin-left: 6px; +} + +.win10-power-popup { + background-color: #2f2f2f; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 6px; + min-width: 176px; +} + +.win10-power-item { + background-color: transparent; + border: none; + border-radius: 6px; + color: #e8e8e8; + padding: 9px 14px; +} + +.win10-power-item:hover { + background-color: rgba(255, 255, 255, 0.07); +} + +.win10-power-icon { + padding-right: 2px; +} + +.win10-power-label { + font-size: 13px; + margin-left: 8px; +} diff --git a/src/themes/win11-light.css b/src/themes/win11-light.css new file mode 100644 index 0000000..c84a008 --- /dev/null +++ b/src/themes/win11-light.css @@ -0,0 +1,165 @@ +/* Windows 11 — Light + * Approximates the Win11 Mica/acrylic aesthetic: rounded corners, + * layered semi-transparent surfaces, and the standard blue accent. */ + +.win10-start-menu { + background-color: #f3f3f3; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.win10-search-entry { + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.14); + border-radius: 20px; + color: #1a1a1a; + font-size: 13px; + margin: 14px 14px 6px; + padding: 9px 16px; + caret-color: #0067c0; +} + +.win10-search-entry:focus { + border-color: #0067c0; + background-color: #ffffff; +} + +.win10-section-header { + padding: 10px 14px 4px; +} + +.win10-section-label { + color: #555555; + font-size: 12px; + font-weight: bold; +} + +.win10-link-button { + background-color: transparent; + border: none; + border-radius: 6px; + color: #555555; + font-size: 12px; + padding: 5px 10px; +} + +.win10-link-button:hover { + background-color: rgba(0, 0, 0, 0.06); + color: #1a1a1a; +} + +.win10-back-icon { + padding-right: 4px; +} + +.win10-app-grid { + padding: 4px 2px; +} + +.win10-grid-app-btn { + background-color: transparent; + border: none; + border-radius: 8px; + padding: 10px 4px; +} + +.win10-grid-app-btn:hover { + background-color: rgba(0, 0, 0, 0.06); +} + +.win10-grid-app-label { + color: #1a1a1a; + font-size: 11px; + margin-top: 5px; + text-align: center; + max-width: 107px; +} + +.win10-list-app-btn { + background-color: transparent; + border: none; + border-radius: 6px; + padding: 7px 14px; +} + +.win10-list-app-btn:hover { + background-color: rgba(0, 0, 0, 0.06); +} + +.win10-list-app-label { + color: #1a1a1a; + font-size: 13px; + margin-left: 12px; +} + +.win10-alpha-sep { + color: #0067c0; + font-size: 13px; + font-weight: bold; + padding: 10px 14px 2px; +} + +.win10-no-results { + color: #888888; + font-size: 13px; + padding: 28px 14px; +} + +.win10-footer { + background-color: rgba(0, 0, 0, 0.05); + border-top: 1px solid rgba(0, 0, 0, 0.07); + border-radius: 0 0 12px 12px; + padding: 6px; +} + +.win10-footer-button { + background-color: transparent; + border: none; + border-radius: 6px; + color: #2a2a2a; + padding: 9px 14px; +} + +.win10-footer-button:hover { + background-color: rgba(0, 0, 0, 0.07); + color: #000000; +} + +.win10-footer-icon { + padding-right: 4px; +} + +.win10-footer-label { + color: #2a2a2a; + font-size: 13px; + margin-left: 6px; +} + +.win10-power-popup { + background-color: #ebebeb; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 10px; + padding: 6px; + min-width: 176px; +} + +.win10-power-item { + background-color: transparent; + border: none; + border-radius: 6px; + color: #1a1a1a; + padding: 9px 14px; +} + +.win10-power-item:hover { + background-color: rgba(0, 0, 0, 0.06); +} + +.win10-power-icon { + padding-right: 2px; +} + +.win10-power-label { + font-size: 13px; + margin-left: 8px; +}