Files
dash-to-panel/src/appIcons.js
Yevhen Popok 644de41a79 Fix "App Details" context menu hiding
Upstream changed label in GNOME 44.

[appmenu: Change "Show Details" entry label to "App Details"](https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2585)

---

Let's use the appmenu property directly instead of finding it using the label, so that would work for every language.

We keep the same schema setting name, so users don't loose their configuration.
2025-02-23 20:17:47 +02:00

2364 lines
71 KiB
JavaScript

/*
* This file is part of the Dash-To-Panel extension for Gnome 3
*
* 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.
*
* 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 <http://www.gnu.org/licenses/>.
*
*
* Credits:
* This file is based on code from the Dash to Dock extension by micheleg
* and code from the Taskbar extension by Zorin OS
* Some code was also adapted from the upstream Gnome Shell source code.
*/
import Clutter from 'gi://Clutter'
import GLib from 'gi://GLib'
import Gio from 'gi://Gio'
import Graphene from 'gi://Graphene'
import GObject from 'gi://GObject'
import Mtk from 'gi://Mtk'
import Shell from 'gi://Shell'
import St from 'gi://St'
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'
import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js'
import * as Dash from 'resource:///org/gnome/shell/ui/dash.js'
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'
import * as Main from 'resource:///org/gnome/shell/ui/main.js'
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'
import * as Util from 'resource:///org/gnome/shell/misc/util.js'
import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js'
import { EventEmitter } from 'resource:///org/gnome/shell/misc/signals.js'
import * as Utils from './utils.js'
import * as Taskbar from './taskbar.js'
import {
DTP_EXTENSION,
SETTINGS,
DESKTOPSETTINGS,
TERMINALSETTINGS,
EXTENSION_PATH,
} from './extension.js'
import {
gettext as _,
ngettext,
} from 'resource:///org/gnome/shell/extensions/extension.js'
//timeout names
const T2 = 'mouseScrollTimeout'
const T3 = 'showDotsTimeout'
const T4 = 'overviewWindowDragEndTimeout'
const T5 = 'switchWorkspaceTimeout'
const T6 = 'displayProperIndicatorTimeout'
//right padding defined for .overview-label in stylesheet.css
const TITLE_RIGHT_PADDING = 8
const DOUBLE_CLICK_DELAY_MS = 450
let LABEL_GAP = 5
let MAX_INDICATORS = 4
export const DEFAULT_PADDING_SIZE = 4
let APPICON_STYLE = {
NORMAL: 'NORMAL',
SYMBOLIC: 'SYMBOLIC',
GRAYSCALE: 'GRAYSCALE',
}
let DOT_STYLE = {
DOTS: 'DOTS',
SQUARES: 'SQUARES',
DASHES: 'DASHES',
SEGMENTED: 'SEGMENTED',
CILIORA: 'CILIORA',
METRO: 'METRO',
SOLID: 'SOLID',
}
let DOT_POSITION = {
TOP: 'TOP',
BOTTOM: 'BOTTOM',
LEFT: 'LEFT',
RIGHT: 'RIGHT',
}
let recentlyClickedAppLoopId = 0
let recentlyClickedApp = null
let recentlyClickedAppWindows = null
let recentlyClickedAppIndex = 0
let recentlyClickedAppMonitorIndex
let tracker = Shell.WindowTracker.get_default()
/**
* Extend AppIcon
*
* - Apply a css class based on the number of windows of each application (#N);
* - Draw a dot for each window of the application based on the default "dot" style which is hidden (#N);
* a class of the form "running#N" is applied to the AppWellIcon actor.
* like the original .running one.
* - add a .focused style to the focused app
* - Customize click actions.
* - Update minimization animation target
*
*/
export const TaskbarAppIcon = GObject.registerClass(
{},
class TaskbarAppIcon extends AppDisplay.AppIcon {
_init(appInfo, panel, iconParams, previewMenu, iconAnimator) {
this.dtpPanel = panel
this._nWindows = 0
this.window = appInfo.window
this.isLauncher = appInfo.isLauncher
this._previewMenu = previewMenu
this.iconAnimator = iconAnimator
this.lastClick = 0
this._appicon_normalstyle = ''
this._appicon_hoverstyle = ''
this._appicon_pressedstyle = ''
super._init(appInfo.app, iconParams)
this._signalsHandler = new Utils.GlobalSignalsHandler()
this._timeoutsHandler = new Utils.TimeoutsHandler()
// Fix touchscreen issues before the listener is added by the parent constructor.
this._onTouchEvent = function (actor, event) {
if (event.type() == Clutter.EventType.TOUCH_BEGIN) {
// Open the popup menu on long press.
this._setPopupTimeout()
} else if (
this._menuTimeoutId != 0 &&
(event.type() == Clutter.EventType.TOUCH_END ||
event.type() == Clutter.EventType.TOUCH_CANCEL)
) {
// Activate/launch the application.
this.activate(1)
this._removeMenuTimeout()
}
// Disable dragging via touch screen as it's buggy as hell. Not perfect for tablet users, but the alternative is way worse.
// Also, EVENT_PROPAGATE launches applications twice with this solution, so this.activate(1) above must only be called if there's already a window.
return Clutter.EVENT_STOP
}
// Hack for missing TOUCH_END event.
this._onLeaveEvent = function () {
this.fake_release()
if (this._menuTimeoutId != 0) this.activate(1) // Activate/launch the application if TOUCH_END didn't fire.
this._removeMenuTimeout()
}
this._dot.set_width(0)
this._isGroupApps = SETTINGS.get_boolean('group-apps')
this._container = new St.Widget({
style_class: 'dtp-container',
layout_manager: new Clutter.BinLayout(),
})
this._dotsContainer = new St.Widget({
style_class: 'dtp-dots-container',
layout_manager: new Clutter.BinLayout(),
})
this._dtpIconContainer = new St.Widget({
layout_manager: new Clutter.BinLayout(),
style: getIconContainerStyle(panel.checkIfVertical()),
})
this.remove_child(this._iconContainer)
this.icon._iconBin.set_pivot_point(0.5, 0.5)
this._dtpIconContainer.add_child(this._iconContainer)
if (appInfo.window) {
let box = Utils.createBoxLayout()
this._windowTitle = new St.Label({
y_align: Clutter.ActorAlign.CENTER,
x_align: Clutter.ActorAlign.START,
style_class: 'overview-label',
})
this._updateWindowTitle()
this._updateWindowTitleStyle()
box.add_child(this._dtpIconContainer)
box.add_child(this._windowTitle)
this._dotsContainer.add_child(box)
} else {
this._dotsContainer.add_child(this._dtpIconContainer)
}
this._container.add_child(this._dotsContainer)
this.set_child(this._container)
if (panel.checkIfVertical()) {
this.set_width(panel.geom.innerSize)
}
// Monitor windows-changes instead of app state.
// Keep using the same Id and function callback (that is extended)
if (this._stateChangedId > 0) {
this.app.disconnect(this._stateChangedId)
this._stateChangedId = 0
}
this._onAnimateAppiconHoverChanged()
this._onAppIconHoverHighlightChanged()
this._setAppIconPadding()
this._setAppIconStyle()
this._showDots()
this._numberOverlay()
this._signalsHandler.add(
[
this,
'notify::mapped',
() => (this.mapped ? this._handleNotifications() : null),
],
[
Utils.getStageTheme(),
'changed',
this._updateWindowTitleStyle.bind(this),
],
[
global.display,
'notify::focus-window',
this._onFocusAppChanged.bind(this),
],
[this.app, 'windows-changed', this.onWindowsChanged.bind(this)],
)
if (!this.window) {
if (SETTINGS.get_boolean('isolate-monitors')) {
this._signalsHandler.add([
Utils.DisplayWrapper.getScreen(),
['window-entered-monitor', 'window-left-monitor'],
this.onWindowEnteredOrLeft.bind(this),
])
}
this._signalsHandler.add([
Utils.DisplayWrapper.getScreen(),
'in-fullscreen-changed',
() => {
if (
global.display.focus_window?.get_monitor() ==
this.dtpPanel.monitor.index &&
!this.dtpPanel.monitor.inFullscreen
) {
this._resetDots(true)
this._displayProperIndicator()
}
},
])
} else {
this._signalsHandler.add(
[this.window, 'notify::title', this._updateWindowTitle.bind(this)],
[
this.window,
'notify::minimized',
this._updateWindowTitleStyle.bind(this),
],
)
}
this._signalsHandler.add(
[this, 'scroll-event', this._onMouseScroll.bind(this)],
[
Main.overview,
'window-drag-end',
this._onOverviewWindowDragEnd.bind(this),
],
[
global.window_manager,
'switch-workspace',
this._onSwitchWorkspace.bind(this),
],
[
this,
'notify::hover',
() => {
this._onAppIconHoverChanged()
this._onAppIconHoverChanged_GtkWorkaround()
},
],
[
this,
'notify::pressed',
this._onAppIconPressedChanged_GtkWorkaround.bind(this),
],
[
this.dtpPanel.panelManager.notificationsMonitor,
`update-${this.app.id}`,
this._handleNotifications.bind(this),
],
[
SETTINGS,
'changed::progress-show-count',
this._handleNotifications.bind(this),
],
[
SETTINGS,
'changed::animate-appicon-hover',
() => {
this._onAnimateAppiconHoverChanged()
this._onAppIconHoverHighlightChanged()
},
],
[
SETTINGS,
[
'changed::highlight-appicon-hover',
'changed::highlight-appicon-hover-background-color',
'changed::highlight-appicon-pressed-background-color',
'changed::highlight-appicon-hover-border-radius',
],
this._onAppIconHoverHighlightChanged.bind(this),
],
[
SETTINGS,
[
'changed::dot-position',
'changed::dot-size',
'changed::dot-style-focused',
'changed::dot-style-unfocused',
'changed::dot-color-dominant',
'changed::dot-color-override',
'changed::dot-color-1',
'changed::dot-color-2',
'changed::dot-color-3',
'changed::dot-color-4',
'changed::dot-color-unfocused-different',
'changed::dot-color-unfocused-1',
'changed::dot-color-unfocused-2',
'changed::dot-color-unfocused-3',
'changed::dot-color-unfocused-4',
'changed::focus-highlight',
'changed::focus-highlight-dominant',
'changed::focus-highlight-color',
'changed::focus-highlight-opacity',
'changed::group-apps-underline-unfocused',
],
this._settingsChangeRefresh.bind(this),
],
[
SETTINGS,
[
'changed::group-apps-label-font-size',
'changed::group-apps-label-font-weight',
'changed::group-apps-label-font-color',
'changed::group-apps-label-font-color-minimized',
'changed::group-apps-label-max-width',
'changed::group-apps-use-fixed-width',
],
this._updateWindowTitleStyle.bind(this),
],
[
SETTINGS,
'changed::highlight-appicon-hover-border-radius',
() => this._setIconStyle(this._isFocusedWindow()),
],
)
}
getDragActor() {
return this.app.create_icon_texture(this.dtpPanel.taskbar.iconSize)
}
// Used by TaskbarItemContainer to animate appIcons on hover
getCloneButton() {
// The source of the clone is this._dtpIconContainer,
// which contains the icon but no highlighting elements
// using this.actor directly would break DnD style.
let cloneSource = this._dtpIconContainer
let clone = new Clutter.Clone({
source: cloneSource,
x: this.child.x,
y: this.child.y,
width: cloneSource.width,
height: cloneSource.height,
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
opacity: 255,
reactive: false,
x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.CENTER,
})
clone._delegate = this._delegate
// "clone" of this.actor
return new St.Button({
child: clone,
x: this.x,
y: this.y,
width: this.width,
height: this.height,
reactive: false,
})
}
shouldShowTooltip() {
if (
!SETTINGS.get_boolean('show-tooltip') ||
(!this.isLauncher &&
SETTINGS.get_boolean('show-window-previews') &&
this.getAppIconInterestingWindows().length > 0)
) {
return false
} else {
return (
this.hover &&
!this.window &&
(!this._menu || !this._menu.isOpen) &&
this._previewMenu.getCurrentAppIcon() !== this
)
}
}
_onAppIconHoverChanged() {
if (
!SETTINGS.get_boolean('show-window-previews') ||
(!this.window && !this._nWindows)
) {
return
}
if (this.hover) {
this._previewMenu.requestOpen(this)
} else {
this._previewMenu.requestClose()
}
}
_onDestroy() {
super._onDestroy()
this._timeoutsHandler.destroy()
this._signalsHandler.destroy()
this._previewMenu.close(true)
}
onWindowsChanged() {
this._updateWindows()
this.updateIcon()
if (this._isGroupApps) this._setIconStyle()
}
onWindowEnteredOrLeft(display, number, metaWindow) {
if (number > 0 && tracker.get_window_app(metaWindow) == this.app) {
this._updateWindows()
this._displayProperIndicator()
}
}
updateTitleStyle() {
this._updateWindowTitleStyle()
}
// Update indicator and target for minimization animation
updateIcon() {
// If (for unknown reason) the actor is not on the stage the reported size
// and position are random values, which might exceeds the integer range
// resulting in an error when assigned to the a rect. This is a more like
// a workaround to prevent flooding the system with errors.
if (this.get_stage() == null) return
let rect = new Mtk.Rectangle()
;[rect.x, rect.y] = this.get_transformed_position()
;[rect.width, rect.height] = this.get_transformed_size()
let windows = this.window
? [this.window]
: this.getAppIconInterestingWindows(true)
windows.forEach(function (w) {
w.set_icon_geometry(rect)
})
}
_onAnimateAppiconHoverChanged() {
if (SETTINGS.get_boolean('animate-appicon-hover')) {
this._container.add_style_class_name('animate-appicon-hover')
// Workaround to prevent scaled icon from being ugly when it is animated on hover.
// It increases the "resolution" of the icon without changing the icon size.
this.icon.createIcon = (iconSize) =>
this.app.create_icon_texture(2 * iconSize)
this._iconIconBinActorAddedId = this.icon._iconBin.connect(
'child-added',
() => {
let size = this.icon.iconSize * Utils.getScaleFactor()
if (this.icon._iconBin.child.mapped) {
this.icon._iconBin.child.set_size(size, size)
} else {
let iconMappedId = this.icon._iconBin.child.connect(
'notify::mapped',
() => {
this.icon._iconBin.child.set_size(size, size)
this.icon._iconBin.child.disconnect(iconMappedId)
},
)
}
},
)
if (this.icon._iconBin.child)
this.icon._createIconTexture(this.icon.iconSize)
} else {
this._container.remove_style_class_name('animate-appicon-hover')
if (this._iconIconBinActorAddedId) {
this.icon._iconBin.disconnect(this._iconIconBinActorAddedId)
this._iconIconBinActorAddedId = 0
this.icon.createIcon = this._createIcon.bind(this)
}
}
}
_onAppIconHoverHighlightChanged() {
const background_color = SETTINGS.get_string(
'highlight-appicon-hover-background-color',
)
const pressed_color = SETTINGS.get_string(
'highlight-appicon-pressed-background-color',
)
const border_radius = SETTINGS.get_int(
'highlight-appicon-hover-border-radius',
)
// Some trickery needed to get the effect
const br = border_radius ? `border-radius: ${border_radius}px;` : ''
this._appicon_normalstyle = br
this._container.set_style(this._appicon_normalstyle)
this._appicon_hoverstyle = `background-color: ${background_color}; ${br}`
this._appicon_pressedstyle = `background-color: ${pressed_color}; ${br}`
if (SETTINGS.get_boolean('highlight-appicon-hover')) {
this._container.remove_style_class_name('no-highlight')
} else {
this._container.add_style_class_name('no-highlight')
this._appicon_normalstyle = ''
this._appicon_hoverstyle = ''
this._appicon_pressedstyle = ''
}
}
_onAppIconHoverChanged_GtkWorkaround() {
if (this.hover && this._appicon_hoverstyle) {
this._container.set_style(this._appicon_hoverstyle)
} else if (this._appicon_normalstyle) {
this._container.set_style(this._appicon_normalstyle)
} else {
this._container.set_style('')
}
}
_onAppIconPressedChanged_GtkWorkaround() {
if (this.pressed && this._appicon_pressedstyle) {
this._container.set_style(this._appicon_pressedstyle)
} else if (this.hover && this._appicon_hoverstyle) {
this._container.set_style(this._appicon_hoverstyle)
} else if (this._appicon_normalstyle) {
this._container.set_style(this._appicon_normalstyle)
} else {
this._container.set_style('')
}
}
_onMouseScroll(actor, event) {
let scrollAction = SETTINGS.get_string('scroll-icon-action')
if (scrollAction === 'PASS_THROUGH') {
return this.dtpPanel._onPanelMouseScroll(actor, event)
} else if (
scrollAction === 'NOTHING' ||
(!this.window && !this._nWindows)
) {
return
}
let direction = Utils.getMouseScrollDirection(event)
if (direction && !this._timeoutsHandler.getId(T2)) {
this._timeoutsHandler.add([
T2,
SETTINGS.get_int('scroll-icon-delay'),
() => {},
])
let windows = this.getAppIconInterestingWindows()
windows.sort(Taskbar.sortWindowsCompareFunction)
Utils.activateSiblingWindow(windows, direction, this.window)
}
}
_showDots() {
// Just update style if dots already exist
if (this._focusedDots && this._unfocusedDots) {
this._updateWindows()
return
}
if (!this._isGroupApps) {
this._focusedDots = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true,
y_expand: true,
visible: false,
})
let mappedId = this.connect('notify::mapped', () => {
this._displayProperIndicator()
this.disconnect(mappedId)
})
} else {
;(this._focusedDots = new St.DrawingArea()),
(this._unfocusedDots = new St.DrawingArea())
this._focusedDots.connect('repaint', () => {
if (!this._dashItemContainer.animatingOut)
// don't draw and trigger more animations if the icon is in the middle of
// being removed from the panel
this._drawRunningIndicator(
this._focusedDots,
SETTINGS.get_string('dot-style-focused'),
true,
)
})
this._unfocusedDots.connect('repaint', () => {
if (!this._dashItemContainer.animatingOut)
this._drawRunningIndicator(
this._unfocusedDots,
SETTINGS.get_string('dot-style-unfocused'),
false,
)
})
this._dotsContainer.add_child(this._unfocusedDots)
this._updateWindows()
this._timeoutsHandler.add([
T3,
0,
() => {
this._resetDots()
this._displayProperIndicator()
},
])
}
this._dotsContainer.add_child(this._focusedDots)
}
_resetDots(ignoreSizeReset) {
let position = SETTINGS.get_string('dot-position')
let isHorizontalDots =
position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM
let sizeProp = isHorizontalDots ? 'width' : 'height'
let focusedDotStyle = SETTINGS.get_string('dot-style-focused')
let unfocusedDotStyle = SETTINGS.get_string('dot-style-unfocused')
this._focusedIsWide = this._isWideDotStyle(focusedDotStyle)
this._unfocusedIsWide = this._isWideDotStyle(unfocusedDotStyle)
;[, this._containerSize] =
this._container[`get_preferred_${sizeProp}`](-1)
if (!ignoreSizeReset) {
;[this._focusedDots, this._unfocusedDots].forEach((d) => {
d.set_size(-1, -1)
d.x_expand = d.y_expand = false
d[sizeProp] = 1
d[(isHorizontalDots ? 'y' : 'x') + '_expand'] = true
})
}
}
_settingsChangeRefresh() {
if (this._isGroupApps) {
this._updateWindows()
this._resetDots()
this._focusedDots.queue_repaint()
this._unfocusedDots.queue_repaint()
}
this._displayProperIndicator()
}
_updateWindowTitleStyle() {
if (this._windowTitle) {
let useFixedWidth = SETTINGS.get_boolean('group-apps-use-fixed-width')
let fontWeight = SETTINGS.get_string('group-apps-label-font-weight')
let fontScale = DESKTOPSETTINGS.get_double('text-scaling-factor')
let fontColor = this.window.minimized
? SETTINGS.get_string('group-apps-label-font-color-minimized')
: SETTINGS.get_string('group-apps-label-font-color')
let scaleFactor = Utils.getScaleFactor()
let maxLabelWidth =
SETTINGS.get_int('group-apps-label-max-width') * scaleFactor
let variableWidth =
!useFixedWidth ||
this.dtpPanel.checkIfVertical() ||
this.dtpPanel.taskbar.fullScrollView
this._windowTitle[maxLabelWidth > 0 ? 'show' : 'hide']()
this._windowTitle.set_width(
variableWidth
? -1
: maxLabelWidth + TITLE_RIGHT_PADDING * scaleFactor,
)
this._windowTitle.clutter_text.natural_width = useFixedWidth
? maxLabelWidth
: 0
this._windowTitle.clutter_text.natural_width_set = useFixedWidth
this._windowTitle.set_style(
'font-size: ' +
SETTINGS.get_int('group-apps-label-font-size') * fontScale +
'px;' +
'font-weight: ' +
fontWeight +
';' +
(useFixedWidth ? '' : 'max-width: ' + maxLabelWidth + 'px;') +
'color: ' +
fontColor,
)
}
}
_updateWindowTitle() {
if (this._windowTitle.text != this.window.title) {
this._windowTitle.text = (
this.window.title ? this.window.title : this.app.get_name()
)
.replace(/\r?\n|\r/g, '')
.trim()
if (this._focusedDots) {
this._displayProperIndicator()
}
}
}
_setIconStyle(isFocused) {
let inlineStyle = 'margin: 0;'
if (
SETTINGS.get_boolean('focus-highlight') &&
this._checkIfFocusedApp() &&
!this.isLauncher &&
(!this.window || isFocused) &&
!this._isThemeProvidingIndicator() &&
this._checkIfMonitorHasFocus()
) {
let focusedDotStyle = SETTINGS.get_string('dot-style-focused')
let pos = SETTINGS.get_string('dot-position')
let highlightMargin = this._focusedIsWide
? SETTINGS.get_int('dot-size')
: 0
if (!this.window) {
let containerWidth =
this._dtpIconContainer.get_width() / Utils.getScaleFactor()
let backgroundSize =
containerWidth +
'px ' +
(containerWidth -
(pos == DOT_POSITION.BOTTOM ? highlightMargin : 0)) +
'px;'
if (
focusedDotStyle == DOT_STYLE.CILIORA ||
focusedDotStyle == DOT_STYLE.SEGMENTED
)
highlightMargin += 1
if (this._nWindows > 1 && focusedDotStyle == DOT_STYLE.METRO) {
let bgSvg = '/img/highlight_stacked_bg'
if (pos == DOT_POSITION.LEFT || pos == DOT_POSITION.RIGHT) {
bgSvg += this.dtpPanel.checkIfVertical() ? '_2' : '_3'
}
inlineStyle +=
"background-image: url('" +
EXTENSION_PATH +
bgSvg +
".svg');" +
'background-position: 0 ' +
(pos == DOT_POSITION.TOP ? highlightMargin : 0) +
'px;' +
'background-size: ' +
backgroundSize
}
}
let highlightColor = this._getFocusHighlightColor()
inlineStyle +=
'background-color: ' +
cssHexTocssRgba(
highlightColor,
SETTINGS.get_int('focus-highlight-opacity') * 0.01,
) +
';'
inlineStyle += this._appicon_normalstyle
}
if (this._dotsContainer.get_style() != inlineStyle) {
this._dotsContainer.set_style(inlineStyle)
}
}
_checkIfFocusedApp() {
return tracker.focus_app == this.app
}
_checkIfMonitorHasFocus() {
return (
global.display.focus_window &&
(!SETTINGS.get_boolean('multi-monitors') || // only check same monitor index if multi window is enabled.
!SETTINGS.get_boolean('isolate-monitors') ||
global.display.focus_window.get_monitor() ===
this.dtpPanel.monitor.index)
)
}
_setAppIconPadding() {
const padding = getIconPadding(this.dtpPanel)
const margin = SETTINGS.get_int('appicon-margin')
let vertical = this.dtpPanel.checkIfVertical()
this.set_style(
`padding: ${vertical ? margin : 0}px ${vertical ? 0 : margin}px;`,
)
this._iconContainer.set_style('padding: ' + padding + 'px;')
}
_setAppIconStyle() {
let appIconStyle = SETTINGS.get_string('appicon-style')
if (appIconStyle === APPICON_STYLE.SYMBOLIC) {
this.add_style_class_name('symbolic-icon-style')
} else if (appIconStyle === APPICON_STYLE.GRAYSCALE) {
this._iconContainer.add_effect_with_name(
'desaturate',
new Clutter.DesaturateEffect({ factor: 1 }),
)
}
}
popupMenu() {
this._removeMenuTimeout()
this.fake_release()
if (!this._menu) {
this._menu = new TaskbarSecondaryMenu(this, this.dtpPanel.geom.position)
this._menu.setApp(this.app)
this._signalsHandler.add(
[
this._menu,
'open-state-changed',
(menu, isPoppedUp) => {
if (!isPoppedUp) this._onMenuPoppedDown()
else this._previewMenu.close(true)
},
],
[Main.overview, 'hiding', () => this._menu.close()],
)
// We want to keep the item hovered while the menu is up
this._menu.blockSourceEvents = true
Main.uiGroup.add_child(this._menu.actor)
this._menuManager.addMenu(this._menu)
}
this._menu.updateQuitText()
this.emit('menu-state-changed', true)
this.set_hover(true)
this._menu.open(BoxPointer.PopupAnimation.FULL)
this._menuManager.ignoreRelease()
this.emit('sync-tooltip')
return false
}
_onFocusAppChanged() {
this._displayProperIndicator()
}
_onOverviewWindowDragEnd() {
this._timeoutsHandler.add([
T4,
0,
() => {
if (SETTINGS.get_boolean('isolate-workspaces')) this._updateWindows()
this._displayProperIndicator()
},
])
}
_onSwitchWorkspace() {
if (this._isGroupApps) {
this._timeoutsHandler.add([T5, 0, () => this._displayProperIndicator()])
} else {
this._displayProperIndicator()
}
}
_displayProperIndicator() {
let isFocused = this._isFocusedWindow()
let position = SETTINGS.get_string('dot-position')
let isHorizontalDots =
position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM
this._setIconStyle(isFocused)
if (!this._isGroupApps) {
if (
this.window &&
(SETTINGS.get_boolean('group-apps-underline-unfocused') || isFocused)
) {
let align =
Clutter.ActorAlign[
position == DOT_POSITION.TOP || position == DOT_POSITION.LEFT
? 'START'
: 'END'
]
this._focusedDots.set_size(0, 0)
this._focusedDots[isHorizontalDots ? 'height' : 'width'] =
this._getRunningIndicatorSize()
this._focusedDots.y_align = this._focusedDots.x_align =
Clutter.ActorAlign.FILL
this._focusedDots[(isHorizontalDots ? 'y' : 'x') + '_align'] = align
this._focusedDots.background_color =
this._getRunningIndicatorColor(isFocused)
this._focusedDots.show()
} else if (this._focusedDots.visible) {
this._focusedDots.hide()
}
} else {
let sizeProp = isHorizontalDots ? 'width' : 'height'
let newFocusedDotsSize = 0
let newFocusedDotsOpacity = 0
let newUnfocusedDotsSize = 0
let newUnfocusedDotsOpacity = 0
isFocused = this._checkIfFocusedApp() && this._checkIfMonitorHasFocus()
this._timeoutsHandler.add([
T6,
0,
() => {
if (isFocused) this.add_style_class_name('focused')
else this.remove_style_class_name('focused')
},
])
if (this._focusedIsWide) {
newFocusedDotsSize =
isFocused && this._nWindows > 0 ? this._containerSize : 0
newFocusedDotsOpacity = 255
} else {
newFocusedDotsSize = this._containerSize
newFocusedDotsOpacity = isFocused && this._nWindows > 0 ? 255 : 0
}
if (this._unfocusedIsWide) {
newUnfocusedDotsSize =
!isFocused && this._nWindows > 0 ? this._containerSize : 0
newUnfocusedDotsOpacity = 255
} else {
newUnfocusedDotsSize = this._containerSize
newUnfocusedDotsOpacity = !isFocused && this._nWindows > 0 ? 255 : 0
}
// Only animate if...
// animation is enabled in settings
// AND (going from a wide style to a narrow style indicator or vice-versa
// OR going from an open app to a closed app or vice versa)
let animate =
SETTINGS.get_boolean('animate-app-switch') &&
(this._focusedIsWide != this._unfocusedIsWide ||
this._focusedDots[sizeProp] != newUnfocusedDotsSize ||
this._unfocusedDots[sizeProp] != newFocusedDotsSize)
let duration = animate ? Taskbar.DASH_ANIMATION_TIME : 0.001
this._animateDotDisplay(
this._focusedDots,
newFocusedDotsSize,
this._unfocusedDots,
newUnfocusedDotsOpacity,
sizeProp,
duration,
)
this._animateDotDisplay(
this._unfocusedDots,
newUnfocusedDotsSize,
this._focusedDots,
newFocusedDotsOpacity,
sizeProp,
duration,
)
}
}
_animateDotDisplay(
dots,
newSize,
otherDots,
newOtherOpacity,
sizeProp,
duration,
) {
Utils.stopAnimations(dots)
let tweenOpts = {
time: duration,
transition: 'easeInOutCubic',
onComplete: () => {
if (newOtherOpacity > 0) otherDots.opacity = newOtherOpacity
},
}
if (newOtherOpacity == 0) otherDots.opacity = newOtherOpacity
tweenOpts[sizeProp] = newSize
Utils.animate(dots, tweenOpts)
}
_isFocusedWindow() {
let focusedWindow = global.display.focus_window
while (focusedWindow) {
if (focusedWindow == this.window) {
return true
}
focusedWindow = focusedWindow.get_transient_for()
}
return false
}
_isWideDotStyle(dotStyle) {
return (
dotStyle == DOT_STYLE.SEGMENTED ||
dotStyle == DOT_STYLE.CILIORA ||
dotStyle == DOT_STYLE.METRO ||
dotStyle == DOT_STYLE.SOLID
)
}
_isThemeProvidingIndicator() {
// This is an attempt to determine if the theme is providing their own
// running indicator by way of a border image on the icon, for example in
// the theme Ciliora
return (
this.icon.get_stage() && this.icon.get_theme_node().get_border_image()
)
}
activate(button, modifiers, handleAsGrouped) {
let event = Clutter.get_current_event()
modifiers = event ? event.get_state() : modifiers || 0
// Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.)
modifiers =
modifiers &
(Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK)
let ctrlPressed = modifiers & Clutter.ModifierType.CONTROL_MASK
if (ctrlPressed) {
// CTRL-click or hotkey with ctrl
return this._launchNewInstance(true)
}
// We check what type of click we have and if the modifier SHIFT is
// being used. We then define what buttonAction should be for this
// event.
let buttonAction = 0
let doubleClick
if (button && button == 2) {
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
buttonAction = SETTINGS.get_string('shift-middle-click-action')
else buttonAction = SETTINGS.get_string('middle-click-action')
}
// fixed issue #1676 by checking for button 0 or 1 to also handle touchscreen
// input, probably not the proper fix as i'm not aware button 0 should exist
// but from using this fix for months it seems to not create any issues
else if (button === 0 || button === 1) {
let now = global.get_current_time()
doubleClick = now - this.lastClick < DOUBLE_CLICK_DELAY_MS
this.lastClick = now
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
buttonAction = SETTINGS.get_string('shift-click-action')
else buttonAction = SETTINGS.get_string('click-action')
}
let closePreview = () =>
this._previewMenu.close(
SETTINGS.get_boolean('window-preview-hide-immediate-click'),
)
let appCount = this.getAppIconInterestingWindows().length
let previewedAppIcon = this._previewMenu.getCurrentAppIcon()
if (this.window || buttonAction != 'TOGGLE-SHOWPREVIEW') closePreview()
// We check if the app is running, and that the # of windows is > 0 in
// case we use workspace isolation,
let appIsRunning =
this.app.state == Shell.AppState.RUNNING && appCount > 0
// We customize the action only when the application is already running
if (appIsRunning && !this.isLauncher) {
if (this.window && !handleAsGrouped) {
//ungrouped applications behaviors
switch (buttonAction) {
case 'LAUNCH':
this._launchNewInstance()
break
case 'QUIT':
this.window.delete(global.get_current_time())
break
default:
if (
!Main.overview._shown &&
(buttonAction == 'MINIMIZE' ||
buttonAction == 'TOGGLE-SHOWPREVIEW' ||
buttonAction == 'TOGGLE-CYCLE' ||
buttonAction == 'TOGGLE-SPREAD' ||
buttonAction == 'CYCLE-MIN') &&
(this._isFocusedWindow() ||
(buttonAction == 'MINIMIZE' &&
(button == 2 ||
modifiers & Clutter.ModifierType.SHIFT_MASK)))
) {
this.window.minimize()
} else {
Main.activateWindow(this.window)
}
}
} else {
//grouped application behaviors
let monitor = this.dtpPanel.monitor
let appHasFocus =
this._checkIfFocusedApp() && this._checkIfMonitorHasFocus()
switch (buttonAction) {
case 'RAISE':
activateAllWindows(this.app, monitor)
break
case 'LAUNCH':
this._launchNewInstance()
break
case 'MINIMIZE':
// In overview just activate the app, unless the acion is explicitely
// requested with a keyboard modifier
if (!Main.overview._shown || modifiers) {
// If we have button=2 or a modifier, allow minimization even if
// the app is not focused
if (
appHasFocus ||
button == 2 ||
modifiers & Clutter.ModifierType.SHIFT_MASK
) {
// minimize all windows on double click and always in the case of primary click without
// additional modifiers
let all_windows = (button == 1 && !modifiers) || doubleClick
minimizeWindow(this.app, all_windows, monitor)
} else activateAllWindows(this.app, monitor)
} else this.app.activate()
break
case 'CYCLE':
if (!Main.overview._shown) {
if (appHasFocus)
cycleThroughWindows(this.app, false, false, monitor)
else {
activateFirstWindow(this.app, monitor)
}
} else this.app.activate()
break
case 'CYCLE-MIN':
if (!Main.overview._shown) {
if (
appHasFocus ||
(recentlyClickedApp == this.app &&
recentlyClickedAppWindows[
recentlyClickedAppIndex % recentlyClickedAppWindows.length
] == 'MINIMIZE')
)
cycleThroughWindows(this.app, false, true, monitor)
else {
activateFirstWindow(this.app, monitor)
}
} else this.app.activate()
break
case 'TOGGLE-SHOWPREVIEW':
if (!Main.overview._shown) {
if (appCount == 1) {
closePreview()
if (appHasFocus) minimizeWindow(this.app, false, monitor)
else activateFirstWindow(this.app, monitor)
} else {
if (doubleClick) {
// minimize all windows if double clicked
closePreview()
minimizeWindow(this.app, true, monitor)
} else if (previewedAppIcon != this) {
this._previewMenu.open(this)
}
this.emit('sync-tooltip')
}
} else this.app.activate()
break
case 'TOGGLE-CYCLE':
if (!Main.overview._shown) {
if (appCount == 1) {
if (appHasFocus) minimizeWindow(this.app, false, monitor)
else activateFirstWindow(this.app, monitor)
} else {
cycleThroughWindows(this.app, false, false, monitor)
}
} else this.app.activate()
break
case 'QUIT':
closeAllWindows(this.app, monitor)
break
case 'TOGGLE-SPREAD':
if (appCount == 1) {
if (appHasFocus && !Main.overview._shown)
minimizeWindow(this.app, false, monitor)
else activateFirstWindow(this.app, monitor)
} else
// return so the overview stays open if it already is
return this.dtpPanel.panelManager.showFocusedAppInOverview(
this.app,
)
}
}
} else {
this._launchNewInstance()
}
global.display.emit('grab-op-begin', null, null)
Main.overview.hide()
}
_launchNewInstance(ctrlPressed) {
let maybeAnimate = () =>
SETTINGS.get_boolean('animate-window-launch') && this.animateLaunch()
if (
(ctrlPressed || this.app.state == Shell.AppState.RUNNING) &&
this.app.can_open_new_window()
) {
maybeAnimate()
this.app.open_new_window(-1)
} else {
let windows = this.window ? [this.window] : this.app.get_windows()
if (windows.length) {
Main.activateWindow(windows[0])
} else {
maybeAnimate()
this.app.activate()
}
}
}
_updateWindows() {
let windows = [this.window]
if (!this.window) {
windows = this.getAppIconInterestingWindows()
this._nWindows = windows.length
for (let i = 1; i <= MAX_INDICATORS; i++) {
let className = 'running' + i
if (i != this._nWindows) this.remove_style_class_name(className)
else this.add_style_class_name(className)
}
}
this._previewMenu.update(this, windows)
}
_getRunningIndicatorCount() {
return Math.min(this._nWindows, MAX_INDICATORS)
}
_getRunningIndicatorSize() {
return SETTINGS.get_int('dot-size') * Utils.getScaleFactor()
}
_getRunningIndicatorColor(isFocused) {
let color
const fallbackColor = new Utils.ColorUtils.Color({
red: 82,
green: 148,
blue: 226,
alpha: 255,
})
if (SETTINGS.get_boolean('dot-color-dominant')) {
let dce = new Utils.DominantColorExtractor(this.app)
let palette = dce._getColorPalette()
if (palette) {
color = Utils.ColorUtils.color_from_string(palette.original)[1]
} else {
// unable to determine color, fall back to theme
let themeNode = this._dot.get_theme_node()
color = themeNode.get_background_color()
// theme didn't provide one, use a default
if (color.alpha == 0) color = fallbackColor
}
} else if (SETTINGS.get_boolean('dot-color-override')) {
let dotColorSettingPrefix = 'dot-color-'
if (!isFocused && SETTINGS.get_boolean('dot-color-unfocused-different'))
dotColorSettingPrefix = 'dot-color-unfocused-'
color = Utils.ColorUtils.color_from_string(
SETTINGS.get_string(
dotColorSettingPrefix + (this._getRunningIndicatorCount() || 1),
),
)[1]
} else {
// Re-use the style - background color, and border width and color -
// of the default dot
let themeNode = this._dot.get_theme_node()
color = themeNode.get_background_color()
// theme didn't provide one, use a default
if (color.alpha == 0) color = fallbackColor
}
return color
}
_getFocusHighlightColor() {
if (SETTINGS.get_boolean('focus-highlight-dominant')) {
let dce = new Utils.DominantColorExtractor(this.app)
let palette = dce._getColorPalette()
if (palette) return palette.original
}
return SETTINGS.get_string('focus-highlight-color')
}
_drawRunningIndicator(area, type, isFocused) {
let n = this._getRunningIndicatorCount()
if (!n) {
return
}
let position = SETTINGS.get_string('dot-position')
let isHorizontalDots =
position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM
let bodyColor = this._getRunningIndicatorColor(isFocused)
let [areaWidth, areaHeight] = area.get_surface_size()
let cr = area.get_context()
let size = this._getRunningIndicatorSize()
let areaSize = areaWidth
let startX = 0
let startY = 0
if (isHorizontalDots) {
if (position == DOT_POSITION.BOTTOM) {
startY = areaHeight - size
}
} else {
areaSize = areaHeight
if (position == DOT_POSITION.RIGHT) {
startX = areaWidth - size
}
}
if (type == DOT_STYLE.SOLID || type == DOT_STYLE.METRO) {
if (type == DOT_STYLE.SOLID || n <= 1) {
cr.translate(startX, startY)
cr.setSourceColor(bodyColor)
cr.newSubPath()
cr.rectangle.apply(
cr,
[0, 0].concat(
isHorizontalDots ? [areaSize, size] : [size, areaSize],
),
)
cr.fill()
} else {
let blackenedLength = (1 / 48) * areaSize // need to scale with the SVG for the stacked highlight
let darkenedLength = isFocused
? (2 / 48) * areaSize
: (10 / 48) * areaSize
let blackenedColor = new Utils.ColorUtils.Color({
red: bodyColor.red * 0.3,
green: bodyColor.green * 0.3,
blue: bodyColor.blue * 0.3,
alpha: bodyColor.alpha,
})
let darkenedColor = new Utils.ColorUtils.Color({
red: bodyColor.red * 0.7,
green: bodyColor.green * 0.7,
blue: bodyColor.blue * 0.7,
alpha: bodyColor.alpha,
})
let solidDarkLength = areaSize - darkenedLength
let solidLength = solidDarkLength - blackenedLength
cr.translate(startX, startY)
cr.setSourceColor(bodyColor)
cr.newSubPath()
cr.rectangle.apply(
cr,
[0, 0].concat(
isHorizontalDots ? [solidLength, size] : [size, solidLength],
),
)
cr.fill()
cr.setSourceColor(blackenedColor)
cr.newSubPath()
cr.rectangle.apply(
cr,
isHorizontalDots
? [solidLength, 0, 1, size]
: [0, solidLength, size, 1],
)
cr.fill()
cr.setSourceColor(darkenedColor)
cr.newSubPath()
cr.rectangle.apply(
cr,
isHorizontalDots
? [solidDarkLength, 0, darkenedLength, size]
: [0, solidDarkLength, size, darkenedLength],
)
cr.fill()
}
} else {
let spacing = Math.ceil(areaSize / 18) // separation between the indicators
let length
let dist
let indicatorSize
let translate
let preDraw = () => {}
let draw
let drawDash = (i, dashLength) => {
dist = i * dashLength + i * spacing
cr.rectangle.apply(
cr,
isHorizontalDots
? [dist, 0, dashLength, size]
: [0, dist, size, dashLength],
)
}
switch (type) {
case DOT_STYLE.CILIORA:
spacing = size
length = areaSize - size * (n - 1) - spacing * (n - 1)
translate = () => cr.translate(startX, startY)
preDraw = () => {
cr.newSubPath()
cr.rectangle.apply(
cr,
[0, 0].concat(
isHorizontalDots ? [length, size] : [size, length],
),
)
}
draw = (i) => {
dist = length + i * spacing + (i - 1) * size
cr.rectangle.apply(
cr,
(isHorizontalDots ? [dist, 0] : [0, dist]).concat([size, size]),
)
}
break
case DOT_STYLE.DOTS: {
let radius = size / 2
translate = () => {
indicatorSize = Math.floor(
(areaSize - n * size - (n - 1) * spacing) / 2,
)
cr.translate.apply(
cr,
isHorizontalDots
? [indicatorSize, startY]
: [startX, indicatorSize],
)
}
draw = (i) => {
dist = (2 * i + 1) * radius + i * spacing
cr.arc.apply(
cr,
(isHorizontalDots ? [dist, radius] : [radius, dist]).concat([
radius,
0,
2 * Math.PI,
]),
)
}
break
}
case DOT_STYLE.SQUARES:
translate = () => {
indicatorSize = Math.floor(
(areaSize - n * size - (n - 1) * spacing) / 2,
)
cr.translate.apply(
cr,
isHorizontalDots
? [indicatorSize, startY]
: [startX, indicatorSize],
)
}
draw = (i) => {
dist = i * size + i * spacing
cr.rectangle.apply(
cr,
(isHorizontalDots ? [dist, 0] : [0, dist]).concat([size, size]),
)
}
break
case DOT_STYLE.DASHES:
length = Math.floor(areaSize / 4) - spacing
translate = () => {
indicatorSize = Math.floor(
(areaSize - n * length - (n - 1) * spacing) / 2,
)
cr.translate.apply(
cr,
isHorizontalDots
? [indicatorSize, startY]
: [startX, indicatorSize],
)
}
draw = (i) => drawDash(i, length)
break
case DOT_STYLE.SEGMENTED:
length = Math.ceil((areaSize - (n - 1) * spacing) / n)
translate = () => cr.translate(startX, startY)
draw = (i) => drawDash(i, length)
break
}
translate()
cr.setSourceColor(bodyColor)
preDraw()
for (let i = 0; i < n; i++) {
cr.newSubPath()
draw(i)
}
cr.fill()
}
cr.$dispose()
}
_handleNotifications() {
if (!this._nWindows && !this.window) return
let monitor = this.dtpPanel.panelManager.notificationsMonitor
let state = monitor.getState(this.app)
let count = 0
if (!state) return
if (SETTINGS.get_boolean('progress-show-count')) {
this.iconAnimator[`${state.urgent ? 'add' : 'remove'}Animation`](
this.icon._iconBin,
'dance',
)
if (state.total) count = state.total > 9 ? '9+' : state.total
}
this._notificationsCount = count
this._maybeUpdateNumberOverlay()
}
_maybeUpdateNumberOverlay() {
let visible = this._numberOverlayBin.visible
let shouldBeVisible =
(this._hotkeysOverlayActiveMode &&
this._numberHotkeysOverlayLabel > -1) ||
this._notificationsCount
let showNotifications =
this._notificationsCount &&
this._hotkeysOverlayActiveMode !== 'TEMPORARILY'
let label = showNotifications
? this._notificationsCount
: this._numberHotkeysOverlayLabel
this._numberOverlayLabel[
`${showNotifications ? 'add' : 'remove'}_style_class_name`
]('notification-badge')
if (shouldBeVisible && label !== this._numberOverlayLabel.get_text()) {
this._numberOverlayLabel.set_text(label.toString())
this._updateNumberOverlay()
}
if (visible && !shouldBeVisible) this._numberOverlayBin.hide()
else if (!visible && shouldBeVisible) this._numberOverlayBin.show()
}
_numberOverlay() {
// Add label for a numeric visual aid (hotkeys or notification)
this._numberOverlayLabel = new St.Label({ style_class: 'badge' })
this._numberOverlayBin = new St.Bin({
child: this._numberOverlayLabel,
y: 2,
})
this._numberOverlayLabel.add_style_class_name('number-overlay')
this._numberHotkeysOverlayLabel = -1
this._numberOverlayBin.hide()
this._dtpIconContainer.add_child(this._numberOverlayBin)
}
_updateNumberOverlay() {
// We apply an overall scale factor that might come from a HiDPI monitor.
// Clutter dimensions are in physical pixels, but CSS measures are in logical
// pixels, so make sure to consider the scale.
// Set the font size to something smaller than the whole icon so it is
// still visible. The border radius is large to make the shape circular
let panelSize =
this.dtpPanel.geom[this.dtpPanel.checkIfVertical() ? 'w' : 'h']
let minFontSize = panelSize >= 32 ? 12 : 10
let fontSize = Math.round(
Math.max(minFontSize, 0.3 * panelSize) / Utils.getScaleFactor(),
)
let size = Math.round(fontSize * 1.3)
let style = `
font-size: ${fontSize}px;
height: ${size}px;
`
this._numberOverlayLabel.set_style(style)
}
setHotkeysNumberOverlayLabel(number) {
this._numberHotkeysOverlayLabel = number
}
toggleHotkeysNumberOverlay(activateMode) {
this._hotkeysOverlayActiveMode =
this._numberHotkeysOverlayLabel > -1 && activateMode
this._maybeUpdateNumberOverlay()
}
handleDragOver(source) {
if (source == Main.xdndHandler) {
this._previewMenu.close(true)
if (!this._nWindows && !this.window)
return DND.DragMotionResult.MOVE_DROP
if (this._nWindows == 1 || this.window) {
this.window
? Main.activateWindow(this.window)
: activateFirstWindow(this.app, this.monitor)
} else
this.dtpPanel.panelManager.showFocusedAppInOverview(this.app, true)
return DND.DragMotionResult.MOVE_DROP
}
return DND.DragMotionResult.CONTINUE
}
getAppIconInterestingWindows(isolateMonitors) {
return getInterestingWindows(
this.app,
this.dtpPanel.monitor,
isolateMonitors,
)
}
},
)
TaskbarAppIcon.prototype.scaleAndFade =
TaskbarAppIcon.prototype.undoScaleAndFade = () => {}
export function minimizeWindow(app, param, monitor) {
// Param true make all app windows minimize
let windows = getInterestingWindows(app, monitor)
let current_workspace =
Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace()
for (let i = 0; i < windows.length; i++) {
let w = windows[i]
if (
w.get_workspace() == current_workspace &&
w.showing_on_its_workspace()
) {
w.minimize()
// Just minimize one window. By specification it should be the
// focused window on the current workspace.
if (!param) break
}
}
}
/*
* By default only non minimized windows are activated.
* This activates all windows in the current workspace.
*/
export function activateAllWindows(app, monitor) {
// First activate first window so workspace is switched if needed,
// then activate all other app windows in the current workspace.
let windows = getInterestingWindows(app, monitor)
let w = windows[0]
Main.activateWindow(w)
let activeWorkspace =
Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace_index()
if (windows.length <= 0) return
for (let i = windows.length - 1; i >= 0; i--) {
if (windows[i].get_workspace().index() == activeWorkspace) {
Main.activateWindow(windows[i])
}
}
}
export function activateFirstWindow(app, monitor) {
let windows = getInterestingWindows(app, monitor)
Main.activateWindow(windows[0])
}
export function cycleThroughWindows(app, reversed, shouldMinimize, monitor) {
// Store for a little amount of time last clicked app and its windows
// since the order changes upon window interaction
let MEMORY_TIME = 3000
let app_windows = getInterestingWindows(app, monitor)
if (shouldMinimize) app_windows.push('MINIMIZE')
if (recentlyClickedAppLoopId > 0) GLib.Source.remove(recentlyClickedAppLoopId)
recentlyClickedAppLoopId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
MEMORY_TIME,
resetRecentlyClickedApp,
)
// If there isn't already a list of windows for the current app,
// or the stored list is outdated, use the current windows list.
if (
!recentlyClickedApp ||
recentlyClickedApp.get_id() != app.get_id() ||
recentlyClickedAppWindows.length != app_windows.length ||
recentlyClickedAppMonitorIndex != monitor.index
) {
recentlyClickedApp = app
recentlyClickedAppWindows = app_windows
recentlyClickedAppIndex = 0
recentlyClickedAppMonitorIndex = monitor.index
}
if (reversed) {
recentlyClickedAppIndex--
if (recentlyClickedAppIndex < 0)
recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1
} else {
recentlyClickedAppIndex++
}
let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length
if (recentlyClickedAppWindows[index] === 'MINIMIZE')
minimizeWindow(app, true, monitor)
else Main.activateWindow(recentlyClickedAppWindows[index])
}
export function resetRecentlyClickedApp() {
if (recentlyClickedAppLoopId > 0) GLib.Source.remove(recentlyClickedAppLoopId)
recentlyClickedAppLoopId = 0
recentlyClickedApp = null
recentlyClickedAppWindows = null
recentlyClickedAppIndex = 0
recentlyClickedAppMonitorIndex = null
return GLib.SOURCE_REMOVE
}
export function closeAllWindows(app, monitor) {
let windows = getInterestingWindows(app, monitor)
for (let i = 0; i < windows.length; i++)
windows[i].delete(global.get_current_time())
}
// Filter out unnecessary windows, for instance
// nautilus desktop window.
export function getInterestingWindows(app, monitor, isolateMonitors) {
let windows = (app ? app.get_windows() : Utils.getAllMetaWindows()).filter(
(w) => !w.skip_taskbar,
)
// When using workspace or monitor isolation, we filter out windows
// that are not in the current workspace or on the same monitor as the appicon
if (SETTINGS.get_boolean('isolate-workspaces'))
windows = windows.filter(function (w) {
return (
w.get_workspace() && w.get_workspace() == Utils.getCurrentWorkspace()
)
})
if (
monitor &&
SETTINGS.get_boolean('multi-monitors') &&
(isolateMonitors || SETTINGS.get_boolean('isolate-monitors'))
) {
windows = windows.filter(function (w) {
return w.get_monitor() == monitor.index
})
}
return windows
}
export function cssHexTocssRgba(cssHex, opacity) {
let bigint = parseInt(cssHex.slice(1), 16)
let r = (bigint >> 16) & 255
let g = (bigint >> 8) & 255
let b = bigint & 255
return 'rgba(' + [r, g, b].join(',') + ',' + opacity + ')'
}
export function getIconPadding(dtpPanel) {
let panelSize = dtpPanel.geom.innerSize
let padding = SETTINGS.get_int('appicon-padding')
let availSize = panelSize - Taskbar.MIN_ICON_SIZE - (panelSize % 2)
if (padding * 2 > availSize) {
padding = availSize * 0.5
}
return padding
}
/**
* Extend AppMenu (AppIconMenu for pre gnome 41)
*
* - hide 'App Details' according to setting
* - show windows header only if show-window-previews is disabled
* - Add close windows option based on quitfromdash extension
* (https://github.com/deuill/shell-extension-quitfromdash)
*/
export class TaskbarSecondaryMenu extends AppMenu.AppMenu {
constructor(source, side) {
super(source, side)
// constructor parameter does nos work for some reason
this._enableFavorites = true
this._showSingleWindows = true
// replace quit item
delete this._quitItem
this._quitItem = this.addAction(_('Quit'), () => this._quitFromTaskbar())
}
updateQuitText() {
let count = this.sourceActor.window
? 1
: getInterestingWindows(this._app, this.sourceActor.dtpPanel.monitor)
.length
if (count > 0) {
let quitFromTaskbarMenuText = ''
if (count == 1) quitFromTaskbarMenuText = _('Quit')
else
quitFromTaskbarMenuText = ngettext(
'Quit %d Window',
'Quit %d Windows',
count,
).format(count)
this._quitItem.label.set_text(quitFromTaskbarMenuText)
}
}
_quitFromTaskbar() {
let time = global.get_current_time()
let windows = this.sourceActor.window // ungrouped applications
? [this.sourceActor.window]
: getInterestingWindows(this._app, this.sourceActor.dtpPanel.monitor)
if (windows.length == this._app.get_windows().length)
this._app.request_quit()
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
windows.forEach((w) => !!w.get_compositor_private() && w.delete(time++))
return GLib.SOURCE_REMOVE
})
}
setApp(app) {
super.setApp(app)
// set "App Details" menu item visibility
this._detailsItem.visible =
!app.hideDetails &&
SETTINGS.get_boolean('secondarymenu-contains-showdetails')
}
}
/**
* This function is used for extendDashItemContainer
*/
export function ItemShowLabel() {
if (!this._labelText) return
this.label.set_text(this._labelText)
this.label.opacity = 0
this.label.show()
let [stageX, stageY] = this.get_transformed_position()
let node = this.label.get_theme_node()
let itemWidth = this.allocation.x2 - this.allocation.x1
let itemHeight = this.allocation.y2 - this.allocation.y1
let labelWidth = this.label.get_width()
let labelHeight = this.label.get_height()
let position = this._dtpPanel.getPosition()
let labelOffset = node.get_length('-x-offset')
// From TaskbarItemContainer
if (this._getIconAnimationOffset)
labelOffset += this._getIconAnimationOffset()
let xOffset = Math.floor((itemWidth - labelWidth) / 2)
let x = stageX + xOffset
let y = stageY + (itemHeight - labelHeight) * 0.5
switch (position) {
case St.Side.TOP:
y = stageY + labelOffset + itemHeight
break
case St.Side.BOTTOM:
y = stageY - labelHeight - labelOffset
break
case St.Side.LEFT:
x = stageX + labelOffset + itemWidth
break
case St.Side.RIGHT:
x = stageX - labelWidth - labelOffset
break
}
// keep the label inside the screen border
// Only needed for the x coordinate.
// Leave a few pixel gap
let gap = LABEL_GAP
let monitor = Main.layoutManager.findMonitorForActor(this)
if (x - monitor.x < gap) x += monitor.x - x + labelOffset
else if (x + labelWidth > monitor.x + monitor.width - gap)
x -= x + labelWidth - (monitor.x + monitor.width) + gap
this.label.set_position(Math.round(x), Math.round(y))
let duration = Dash.DASH_ITEM_LABEL_SHOW_TIME
if (duration > 1) {
duration /= 1000
}
Utils.animate(this.label, {
opacity: 255,
time: duration,
transition: 'easeOutQuad',
})
}
/**
* A wrapper class around the ShowAppsIcon class.
*
* - Pass settings to the constructor
* - set label position based on dash orientation (Note, I am reusing most machinery of the appIcon class)
* - implement a popupMenu based on the AppIcon code (Note, I am reusing most machinery of the appIcon class)
*
* I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973.
* thus use this pattern where the real showAppsIcon object is encaptulated, and a reference to it will be properly wired upon
* use of this class in place of the original showAppsButton.
*
*/
export const ShowAppsIconWrapper = class extends EventEmitter {
constructor(dtpPanel) {
super()
this.realShowAppsIcon = new Dash.ShowAppsIcon()
/* the variable equivalent to toggleButton has a different name in the appIcon class
(actor): duplicate reference to easily reuse appIcon methods */
this.actor = this.realShowAppsIcon.toggleButton
this.realShowAppsIcon.show(false)
// Re-use appIcon methods
this._removeMenuTimeout = AppDisplay.AppIcon.prototype._removeMenuTimeout
this._setPopupTimeout = AppDisplay.AppIcon.prototype._setPopupTimeout
this._onKeyboardPopupMenu =
AppDisplay.AppIcon.prototype._onKeyboardPopupMenu
// No action on clicked (showing of the appsview is controlled elsewhere)
this._onClicked = () => this._removeMenuTimeout()
this.actor.connect('leave-event', this._onLeaveEvent.bind(this))
this.actor.connect('button-press-event', this._onButtonPress.bind(this))
this.actor.connect('touch-event', this._onTouchEvent.bind(this))
this.actor.connect('clicked', this._onClicked.bind(this))
this.actor.connect('popup-menu', this._onKeyboardPopupMenu.bind(this))
this._menu = null
this._menuManager = new PopupMenu.PopupMenuManager(this.actor)
this._menuTimeoutId = 0
this.realShowAppsIcon._dtpPanel = dtpPanel
Taskbar.extendDashItemContainer(this.realShowAppsIcon)
let customIconPath = SETTINGS.get_string('show-apps-icon-file')
this.realShowAppsIcon.icon.createIcon = function (size) {
this._iconActor = new St.Icon({
icon_name: 'view-app-grid-symbolic',
icon_size: size,
style_class: 'show-apps-icon',
track_hover: true,
})
if (customIconPath) {
this._iconActor.gicon = new Gio.FileIcon({
file: Gio.File.new_for_path(customIconPath),
})
}
return this._iconActor
}
this._changedShowAppsIconId = SETTINGS.connect(
'changed::show-apps-icon-file',
() => {
customIconPath = SETTINGS.get_string('show-apps-icon-file')
this.realShowAppsIcon.icon._createIconTexture(
this.realShowAppsIcon.icon.iconSize,
)
},
)
this._changedAppIconPaddingId = SETTINGS.connect(
'changed::appicon-padding',
() => this.setShowAppsPadding(),
)
this._changedAppIconSidePaddingId = SETTINGS.connect(
'changed::show-apps-icon-side-padding',
() => this.setShowAppsPadding(),
)
this.setShowAppsPadding()
}
_onButtonPress(_actor, event) {
let button = event.get_button()
if (button == 1) {
this._setPopupTimeout()
} else if (button == 3) {
this.popupMenu()
return Clutter.EVENT_STOP
}
return Clutter.EVENT_PROPAGATE
}
_onLeaveEvent() {
this.actor.fake_release()
this._removeMenuTimeout()
}
_onTouchEvent(actor, event) {
if (event.type() == Clutter.EventType.TOUCH_BEGIN) this._setPopupTimeout()
return Clutter.EVENT_PROPAGATE
}
_onMenuPoppedDown() {
this._menu.sourceActor = this.actor
this.actor.sync_hover()
this.emit('menu-state-changed', false)
}
setShowAppsPadding() {
let padding = getIconPadding(this.realShowAppsIcon._dtpPanel)
let sidePadding = SETTINGS.get_int('show-apps-icon-side-padding')
let isVertical = this.realShowAppsIcon._dtpPanel.checkIfVertical()
this.actor.set_style(
'padding:' +
(padding + (isVertical ? sidePadding : 0)) +
'px ' +
(padding + (isVertical ? 0 : sidePadding)) +
'px;',
)
}
createMenu() {
if (!this._menu) {
this._menu = new MyShowAppsIconMenu(
this.realShowAppsIcon,
this.realShowAppsIcon._dtpPanel,
)
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
if (!isPoppedUp) this._onMenuPoppedDown()
})
let id = Main.overview.connect('hiding', () => {
this._menu.close()
})
this._menu.actor.connect('destroy', () => {
Main.overview.disconnect(id)
})
// We want to keep the item hovered while the menu is up
this._menu.blockSourceEvents = true
Main.uiGroup.add_child(this._menu.actor)
this._menuManager.addMenu(this._menu)
}
}
popupMenu(sourceActor = null) {
this._removeMenuTimeout()
this.actor.fake_release()
this.createMenu()
this._menu.updateItems(
sourceActor == null ? this.realShowAppsIcon : sourceActor,
)
this.actor.set_hover(true)
this._menu.open(BoxPointer.PopupAnimation.FULL)
this._menuManager.ignoreRelease()
this.emit('sync-tooltip')
return false
}
shouldShowTooltip() {
return (
SETTINGS.get_boolean('show-tooltip') &&
this.actor.hover &&
(!this._menu || !this._menu.isOpen)
)
}
destroy() {
SETTINGS.disconnect(this._changedShowAppsIconId)
SETTINGS.disconnect(this._changedAppIconSidePaddingId)
SETTINGS.disconnect(this._changedAppIconPaddingId)
this.realShowAppsIcon.destroy()
}
}
/**
* A menu for the showAppsIcon
*/
export const MyShowAppsIconMenu = class extends PopupMenu.PopupMenu {
constructor(actor, dtpPanel) {
super(actor, 0, dtpPanel.getPosition())
this._dtpPanel = dtpPanel
this.updateItems(actor)
}
updateItems(sourceActor) {
this.sourceActor = sourceActor
this.removeAll()
if (this.sourceActor != Main.layoutManager.dummyCursor) {
this._appendItem({
title: _('Power options'),
cmd: ['gnome-control-center', 'power'],
})
this._appendItem({
title: _('Event logs'),
cmd: ['gnome-logs'],
})
this._appendItem({
title: _('System'),
cmd: ['gnome-control-center', 'system'],
})
this._appendItem({
title: _('Device Management'),
cmd: ['gnome-control-center', 'display'],
})
this._appendItem({
title: _('Disk Management'),
cmd: ['gnome-disks'],
})
this._appendList(
SETTINGS.get_strv('show-apps-button-context-menu-commands'),
SETTINGS.get_strv('show-apps-button-context-menu-titles'),
)
this._appendSeparator()
}
JSON.parse(SETTINGS.get_string('context-menu-entries')).forEach((e) => {
if (e.cmd == 'TERMINALSETTINGS')
e.cmd = TERMINALSETTINGS.get_string('exec')
this._appendItem({
title: e.title,
cmd: e.cmd.split(' '),
})
})
this._appendList(
SETTINGS.get_strv('panel-context-menu-commands'),
SETTINGS.get_strv('panel-context-menu-titles'),
)
this._appendSeparator()
let lockTaskbarMenuItem = this._appendMenuItem(
SETTINGS.get_boolean('taskbar-locked')
? _('Unlock taskbar')
: _('Lock taskbar'),
)
lockTaskbarMenuItem.connect('activate', () => {
SETTINGS.set_boolean(
'taskbar-locked',
!SETTINGS.get_boolean('taskbar-locked'),
)
})
this._appendItem({
title: _('Gnome Settings'),
cmd: ['gnome-control-center'],
})
let settingsMenuItem = this._appendMenuItem(_('Dash to Panel Settings'))
settingsMenuItem.connect('activate', () => DTP_EXTENSION.openPreferences())
if (this.sourceActor == Main.layoutManager.dummyCursor) {
this._appendSeparator()
let item = this._appendMenuItem(
this._dtpPanel._restoreWindowList
? _('Restore Windows')
: _('Show Desktop'),
)
item.connect(
'activate',
this._dtpPanel._onShowDesktopButtonPress.bind(this._dtpPanel),
)
}
}
// Only add menu entries for commands that exist in path
_appendItem(info) {
if (GLib.find_program_in_path(info.cmd[0])) {
let item = this._appendMenuItem(_(info.title))
item.connect('activate', function () {
Util.spawn(info.cmd)
})
return item
}
return null
}
_appendList(commandList, titleList) {
if (commandList.length != titleList.length) {
return
}
for (let entry = 0; entry < commandList.length; entry++) {
this._appendItem({
title: titleList[entry],
cmd: commandList[entry].split(' '),
})
}
}
_appendSeparator() {
let separator = new PopupMenu.PopupSeparatorMenuItem()
this.addMenuItem(separator)
}
_appendMenuItem(labelText) {
// FIXME: app-well-menu-item style
let item = new PopupMenu.PopupMenuItem(labelText)
this.addMenuItem(item)
return item
}
}
export const getIconContainerStyle = function (isVertical) {
let style = 'padding: '
if (SETTINGS.get_boolean('group-apps')) {
style += isVertical ? '0;' : '0 ' + DEFAULT_PADDING_SIZE + 'px;'
} else {
style += (isVertical ? '' : '0 ') + DEFAULT_PADDING_SIZE + 'px;'
}
return style
}