Files
dash-to-panel/src/appIcons.js
2025-02-12 21:48:18 -05:00

2396 lines
73 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 PanelSettings from './panelSettings.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.w)
}
// 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}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.monitor.index)
const margin = SETTINGS.get_int('appicon-margin')
const margin_todesktop = SETTINGS.get_int('appicon-margin-todesktop')
const margin_toscreenborder = SETTINGS.get_int(
'appicon-margin-toscreenborder',
)
let margin_style = ''
const panelPosition = this.dtpPanel.getPosition()
if (panelPosition == St.Side.TOP) {
margin_style = `${margin_toscreenborder}px ${margin}px ${margin_todesktop}px ${margin}px`
} else if (panelPosition == St.Side.RIGHT) {
margin_style = `${margin}px ${margin_toscreenborder}px ${margin}px ${margin_todesktop}px`
} else if (panelPosition == St.Side.LEFT) {
margin_style = `${margin}px ${margin_todesktop}px ${margin}px ${margin_toscreenborder}px`
} else {
margin_style = `${margin_todesktop}px ${margin}px ${margin_toscreenborder}px ${margin}px`
}
this.set_style(`padding: ${margin_style};`)
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 false
}
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(monitorIndex) {
let panelSize = PanelSettings.getPanelSize(SETTINGS, monitorIndex)
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 'Show 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
// Remove "Show Details" menu item
if (!SETTINGS.get_boolean('secondarymenu-contains-showdetails')) {
let existingMenuItems = this._getMenuItems()
for (let i = 0; i < existingMenuItems.length; i++) {
let item = existingMenuItems[i]
if (item !== undefined && item.label !== undefined) {
if (item.label.text == 'Show Details') {
this.box.remove_child(item.actor)
}
}
}
}
// 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)
this._detailsItem.visible = !app.hideDetails
}
}
/**
* 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.monitor.index)
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', 'info-overview'],
})
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()
}
this._appendItem({
title: _('Terminal'),
cmd: [TERMINALSETTINGS.get_string('exec')],
})
this._appendItem({
title: _('System monitor'),
cmd: ['gnome-system-monitor'],
})
this._appendItem({
title: _('Files'),
cmd: ['nautilus'],
})
this._appendItem({
title: _('Extensions'),
cmd: ['gnome-extensions-app'],
})
this._appendItem({
title: _('Settings'),
cmd: ['gnome-control-center'],
})
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'),
)
})
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
}