743 lines
22 KiB
JavaScript
743 lines
22 KiB
JavaScript
/*
|
||
* 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()
|
||
}
|
||
},
|
||
)
|