1208 lines
45 KiB
JavaScript
1208 lines
45 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const GdkPixbuf = imports.gi.GdkPixbuf
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Signals = imports.signals;
|
|
const Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Mainloop = imports.mainloop;
|
|
|
|
// Use __ () and N__() for the extension gettext domain, and reuse
|
|
// the shell domain with the default _() and N_()
|
|
const Gettext = imports.gettext.domain('dashtodock');
|
|
const __ = Gettext.gettext;
|
|
const N__ = function(e) { return e };
|
|
|
|
const AppDisplay = imports.ui.appDisplay;
|
|
const AppFavorites = imports.ui.appFavorites;
|
|
const Dash = imports.ui.dash;
|
|
const DND = imports.ui.dnd;
|
|
const IconGrid = imports.ui.iconGrid;
|
|
const Main = imports.ui.main;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
const Util = imports.misc.util;
|
|
const Workspace = imports.ui.workspace;
|
|
|
|
const Me = imports.misc.extensionUtils.getCurrentExtension();
|
|
const Docking = Me.imports.docking;
|
|
const Utils = Me.imports.utils;
|
|
const WindowPreview = Me.imports.windowPreview;
|
|
const AppIconIndicators = Me.imports.appIconIndicators;
|
|
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
|
|
const clickAction = {
|
|
SKIP: 0,
|
|
MINIMIZE: 1,
|
|
LAUNCH: 2,
|
|
CYCLE_WINDOWS: 3,
|
|
MINIMIZE_OR_OVERVIEW: 4,
|
|
PREVIEWS: 5,
|
|
MINIMIZE_OR_PREVIEWS: 6,
|
|
FOCUS_OR_PREVIEWS: 7,
|
|
QUIT: 8,
|
|
};
|
|
|
|
const scrollAction = {
|
|
DO_NOTHING: 0,
|
|
CYCLE_WINDOWS: 1,
|
|
SWITCH_WORKSPACE: 2
|
|
};
|
|
|
|
let recentlyClickedAppLoopId = 0;
|
|
let recentlyClickedApp = null;
|
|
let recentlyClickedAppWindows = null;
|
|
let recentlyClickedAppIndex = 0;
|
|
let recentlyClickedAppMonitor = -1;
|
|
|
|
/**
|
|
* Extend AppIcon
|
|
*
|
|
* - Apply a css class based on the number of windows of each application (#N);
|
|
* - Customized indicators for running applications in place of 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
|
|
* - Update menu if open on windows change
|
|
*/
|
|
var MyAppIcon = class DashToDock_AppIcon extends AppDisplay.AppIcon {
|
|
|
|
// settings are required inside.
|
|
constructor(remoteModel, app, monitorIndex, iconParams) {
|
|
super(app, iconParams);
|
|
|
|
// a prefix is required to avoid conflicting with the parent class variable
|
|
this.monitorIndex = monitorIndex;
|
|
this._signalsHandler = new Utils.GlobalSignalsHandler();
|
|
this.remoteModel = remoteModel;
|
|
this._indicator = null;
|
|
|
|
let appInfo = app.get_app_info();
|
|
this._location = appInfo ? appInfo.get_string('XdtdUri') : null;
|
|
|
|
this._updateIndicatorStyle();
|
|
this._optionalScrollCycleWindows();
|
|
|
|
// 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._windowsChangedId = this.app.connect('windows-changed',
|
|
this.onWindowsChanged.bind(this));
|
|
this._focusAppChangeId = tracker.connect('notify::focus-app',
|
|
this._onFocusAppChanged.bind(this));
|
|
|
|
// In Wayland sessions, this signal is needed to track the state of windows dragged
|
|
// from one monitor to another. As this is triggered quite often (whenever a new winow
|
|
// of any application opened or moved to a different desktop),
|
|
// we restrict this signal to the case when 'isolate-monitors' is true,
|
|
// and if there are at least 2 monitors.
|
|
if (Docking.DockManager.settings.get_boolean('isolate-monitors') &&
|
|
Main.layoutManager.monitors.length > 1) {
|
|
this._signalsHandler.removeWithLabel('isolate-monitors');
|
|
this._signalsHandler.addWithLabel('isolate-monitors', [
|
|
global.display,
|
|
'window-entered-monitor',
|
|
this._onWindowEntered.bind(this)
|
|
]);
|
|
}
|
|
|
|
this._progressOverlayArea = null;
|
|
this._progress = 0;
|
|
|
|
let keys = ['apply-custom-theme',
|
|
'running-indicator-style',
|
|
];
|
|
|
|
keys.forEach(function(key) {
|
|
this._signalsHandler.add([
|
|
Docking.DockManager.settings,
|
|
'changed::' + key,
|
|
this._updateIndicatorStyle.bind(this)
|
|
]);
|
|
}, this);
|
|
|
|
if (this._location) {
|
|
this._signalsHandler.add([
|
|
Docking.DockManager.getDefault().fm1Client,
|
|
'windows-changed',
|
|
this.onWindowsChanged.bind(this)
|
|
]);
|
|
}
|
|
|
|
this._signalsHandler.add([
|
|
Docking.DockManager.settings,
|
|
'changed::scroll-action',
|
|
this._optionalScrollCycleWindows.bind(this)
|
|
]);
|
|
|
|
this._numberOverlay();
|
|
|
|
this._previewMenuManager = null;
|
|
this._previewMenu = null;
|
|
}
|
|
|
|
_onDestroy() {
|
|
super._onDestroy();
|
|
|
|
// This is necessary due to an upstream bug
|
|
// https://bugzilla.gnome.org/show_bug.cgi?id=757556
|
|
// It can be safely removed once it get solved upstrea.
|
|
if (this._menu)
|
|
this._menu.close(false);
|
|
|
|
// Disconect global signals
|
|
|
|
if (this._windowsChangedId > 0)
|
|
this.app.disconnect(this._windowsChangedId);
|
|
this._windowsChangedId = 0;
|
|
|
|
if (this._focusAppChangeId > 0) {
|
|
tracker.disconnect(this._focusAppChangeId);
|
|
this._focusAppChangeId = 0;
|
|
}
|
|
|
|
this._signalsHandler.destroy();
|
|
|
|
if (this._scrollEventHandler)
|
|
this.actor.disconnect(this._scrollEventHandler);
|
|
}
|
|
|
|
// TOOD Rename this function
|
|
_updateIndicatorStyle() {
|
|
|
|
if (this._indicator !== null) {
|
|
this._indicator.destroy();
|
|
this._indicator = null;
|
|
}
|
|
this._indicator = new AppIconIndicators.AppIconIndicator(this);
|
|
this._indicator.update();
|
|
}
|
|
|
|
_onWindowEntered(metaScreen, monitorIndex, metaWin) {
|
|
let app = Shell.WindowTracker.get_default().get_window_app(metaWin);
|
|
if (app && app.get_id() == this.app.get_id())
|
|
this.onWindowsChanged();
|
|
}
|
|
|
|
_optionalScrollCycleWindows() {
|
|
if (this._scrollEventHandler) {
|
|
this.actor.disconnect(this._scrollEventHandler);
|
|
this._scrollEventHandler = 0;
|
|
}
|
|
|
|
let settings = Docking.DockManager.settings;
|
|
let isEnabled = settings.get_enum('scroll-action') === scrollAction.CYCLE_WINDOWS;
|
|
if (!isEnabled) return;
|
|
this._scrollEventHandler = this.actor.connect('scroll-event',
|
|
this.onScrollEvent.bind(this));
|
|
}
|
|
|
|
onScrollEvent(actor, event) {
|
|
|
|
// We only activate windows of running applications, i.e. we never open new windows
|
|
// 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
|
|
&& this.getInterestingWindows().length > 0;
|
|
|
|
if (!appIsRunning)
|
|
return false
|
|
|
|
if (this._optionalScrollCycleWindowsDeadTimeId)
|
|
return false;
|
|
else
|
|
this._optionalScrollCycleWindowsDeadTimeId = Mainloop.timeout_add(250, () => {
|
|
this._optionalScrollCycleWindowsDeadTimeId = 0;
|
|
});
|
|
|
|
let direction = null;
|
|
|
|
switch (event.get_scroll_direction()) {
|
|
case Clutter.ScrollDirection.UP:
|
|
direction = Meta.MotionDirection.UP;
|
|
break;
|
|
case Clutter.ScrollDirection.DOWN:
|
|
direction = Meta.MotionDirection.DOWN;
|
|
break;
|
|
case Clutter.ScrollDirection.SMOOTH:
|
|
let [dx, dy] = event.get_scroll_delta();
|
|
if (dy < 0)
|
|
direction = Meta.MotionDirection.UP;
|
|
else if (dy > 0)
|
|
direction = Meta.MotionDirection.DOWN;
|
|
break;
|
|
}
|
|
|
|
let focusedApp = tracker.focus_app;
|
|
if (!Main.overview._shown) {
|
|
let reversed = direction === Meta.MotionDirection.UP;
|
|
if (this.app == focusedApp)
|
|
this._cycleThroughWindows(reversed);
|
|
else {
|
|
// Activate the first window
|
|
let windows = this.getInterestingWindows();
|
|
if (windows.length > 0) {
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
this.app.activate();
|
|
return true;
|
|
}
|
|
|
|
onWindowsChanged() {
|
|
|
|
if (this._menu && this._menu.isOpen)
|
|
this._menu.update();
|
|
|
|
this._indicator.update();
|
|
this.updateIconGeometry();
|
|
}
|
|
|
|
/**
|
|
* Update taraget for minimization animation
|
|
*/
|
|
updateIconGeometry() {
|
|
// 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.actor.get_stage() == null)
|
|
return;
|
|
|
|
let rect = new Meta.Rectangle();
|
|
|
|
[rect.x, rect.y] = this.actor.get_transformed_position();
|
|
[rect.width, rect.height] = this.actor.get_transformed_size();
|
|
|
|
let windows = this.getWindows();
|
|
if (Docking.DockManager.settings.get_boolean('multi-monitor')) {
|
|
let monitorIndex = this.monitorIndex;
|
|
windows = windows.filter(function(w) {
|
|
return w.get_monitor() == monitorIndex;
|
|
});
|
|
}
|
|
windows.forEach(function(w) {
|
|
w.set_icon_geometry(rect);
|
|
});
|
|
}
|
|
|
|
_updateRunningStyle() {
|
|
// The logic originally in this function has been moved to
|
|
// AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as
|
|
// it called by the parent constructor.
|
|
}
|
|
|
|
popupMenu() {
|
|
this._removeMenuTimeout();
|
|
this.actor.fake_release();
|
|
this._draggable.fakeRelease();
|
|
|
|
if (!this._menu) {
|
|
this._menu = new MyAppIconMenu(this);
|
|
this._menu.connect('activate-window', (menu, window) => {
|
|
this.activateWindow(window);
|
|
});
|
|
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
|
|
if (!isPoppedUp)
|
|
this._onMenuPoppedDown();
|
|
else {
|
|
// Setting the max-height is s useful if part of the menu is
|
|
// scrollable so the minimum height is smaller than the natural height.
|
|
let monitor_index = Main.layoutManager.findIndexForActor(this.actor);
|
|
let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor_index);
|
|
let position = Utils.getPosition();
|
|
this._isHorizontal = ( position == St.Side.TOP ||
|
|
position == St.Side.BOTTOM);
|
|
// If horizontal also remove the height of the dash
|
|
let fixedDock = Docking.DockManager.settings.get_boolean('dock-fixed');
|
|
let additional_margin = this._isHorizontal && !fixedDock ? Main.overview._dash.actor.height : 0;
|
|
let verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom;
|
|
// Also set a max width to the menu, so long labels (long windows title) get truncated
|
|
this._menu.actor.style = ('max-height: ' + Math.round(workArea.height - additional_margin - verticalMargins) + 'px;' +
|
|
'max-width: 400px');
|
|
}
|
|
});
|
|
let id = Main.overview.connect('hiding', () => {
|
|
this._menu.close();
|
|
});
|
|
this._menu.actor.connect('destroy', function() {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
|
|
this._menuManager.addMenu(this._menu);
|
|
}
|
|
|
|
this.emit('menu-state-changed', true);
|
|
|
|
this.actor.set_hover(true);
|
|
this._menu.popup();
|
|
this._menuManager.ignoreRelease();
|
|
this.emit('sync-tooltip');
|
|
|
|
return false;
|
|
}
|
|
|
|
_onFocusAppChanged() {
|
|
this._indicator.update();
|
|
}
|
|
|
|
activate(button) {
|
|
let event = Clutter.get_current_event();
|
|
let modifiers = event ? event.get_state() : 0;
|
|
let focusedApp = tracker.focus_app;
|
|
|
|
// Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.)
|
|
modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK);
|
|
|
|
// We don't change the CTRL-click behaviour: in such case we just chain
|
|
// up the parent method and return.
|
|
if (modifiers & Clutter.ModifierType.CONTROL_MASK) {
|
|
// Keep default behaviour: launch new window
|
|
// By calling the parent method I make it compatible
|
|
// with other extensions tweaking ctrl + click
|
|
super.activate(button);
|
|
return;
|
|
}
|
|
|
|
// 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 settings = Docking.DockManager.settings;
|
|
if (button && button == 2 ) {
|
|
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
|
|
buttonAction = settings.get_enum('shift-middle-click-action');
|
|
else
|
|
buttonAction = settings.get_enum('middle-click-action');
|
|
}
|
|
else if (button && button == 1) {
|
|
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
|
|
buttonAction = settings.get_enum('shift-click-action');
|
|
else
|
|
buttonAction = settings.get_enum('click-action');
|
|
}
|
|
|
|
// We check if the app is running, and that the # of windows is > 0 in
|
|
// case we use workspace isolation.
|
|
let windows = this.getInterestingWindows();
|
|
let appIsRunning = (this.app.state == Shell.AppState.RUNNING || this.isLocation())
|
|
&& windows.length > 0;
|
|
|
|
// Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open
|
|
// This variable keeps track of this
|
|
let shouldHideOverview = true;
|
|
|
|
// We customize the action only when the application is already running
|
|
if (appIsRunning) {
|
|
switch (buttonAction) {
|
|
case clickAction.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 (this.app == focusedApp || 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 click_count = 0;
|
|
if (Clutter.EventType.CLUTTER_BUTTON_PRESS)
|
|
click_count = event.get_click_count();
|
|
let all_windows = (button == 1 && ! modifiers) || click_count > 1;
|
|
this._minimizeWindow(all_windows);
|
|
}
|
|
else
|
|
this._activateAllWindows();
|
|
}
|
|
else {
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
}
|
|
break;
|
|
|
|
case clickAction.MINIMIZE_OR_OVERVIEW:
|
|
// When a single window is present, toggle minimization
|
|
// If only one windows is present toggle minimization, but only when trigggered with the
|
|
// simple click action (no modifiers, no middle click).
|
|
if (windows.length == 1 && !modifiers && button == 1) {
|
|
let w = windows[0];
|
|
if (this.app == focusedApp) {
|
|
// Window is raised, minimize it
|
|
this._minimizeWindow(w);
|
|
} else {
|
|
// Window is minimized, raise it
|
|
Main.activateWindow(w);
|
|
}
|
|
// Launch overview when multiple windows are present
|
|
// TODO: only show current app windows when gnome shell API will allow it
|
|
} else {
|
|
shouldHideOverview = false;
|
|
Main.overview.toggle();
|
|
}
|
|
break;
|
|
|
|
case clickAction.CYCLE_WINDOWS:
|
|
if (!Main.overview._shown){
|
|
if (this.app == focusedApp)
|
|
this._cycleThroughWindows();
|
|
else {
|
|
// Activate the first window
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
}
|
|
}
|
|
else
|
|
this.app.activate();
|
|
break;
|
|
|
|
case clickAction.FOCUS_OR_PREVIEWS:
|
|
if (this.app == focusedApp &&
|
|
(windows.length > 1 || modifiers || button != 1)) {
|
|
this._windowPreviews();
|
|
} else {
|
|
// Activate the first window
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
}
|
|
break;
|
|
|
|
case clickAction.LAUNCH:
|
|
this.launchNewWindow();
|
|
break;
|
|
|
|
case clickAction.PREVIEWS:
|
|
if (!Main.overview._shown) {
|
|
// If only one windows is present just switch to it, but only when trigggered with the
|
|
// simple click action (no modifiers, no middle click).
|
|
if (windows.length == 1 && !modifiers && button == 1) {
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
} else
|
|
this._windowPreviews();
|
|
}
|
|
else {
|
|
this.app.activate();
|
|
}
|
|
break;
|
|
|
|
case clickAction.MINIMIZE_OR_PREVIEWS:
|
|
// When a single window is present, toggle minimization
|
|
// If only one windows is present toggle minimization, but only when trigggered with the
|
|
// simple click action (no modifiers, no middle click).
|
|
if (!Main.overview._shown){
|
|
if (windows.length == 1 && !modifiers && button == 1) {
|
|
let w = windows[0];
|
|
if (this.app == focusedApp) {
|
|
// Window is raised, minimize it
|
|
this._minimizeWindow(w);
|
|
} else {
|
|
// Window is minimized, raise it
|
|
Main.activateWindow(w);
|
|
}
|
|
} else {
|
|
// Launch previews when multiple windows are present
|
|
this._windowPreviews();
|
|
}
|
|
} else {
|
|
this.app.activate();
|
|
}
|
|
break;
|
|
|
|
case clickAction.QUIT:
|
|
this.closeAllWindows();
|
|
break;
|
|
|
|
case clickAction.SKIP:
|
|
let w = windows[0];
|
|
Main.activateWindow(w);
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
this.launchNewWindow();
|
|
}
|
|
|
|
// Hide overview except when action mode requires it
|
|
if(shouldHideOverview) {
|
|
Main.overview.hide();
|
|
}
|
|
}
|
|
|
|
shouldShowTooltip() {
|
|
return this.actor.hover && (!this._menu || !this._menu.isOpen) &&
|
|
(!this._previewMenu || !this._previewMenu.isOpen);
|
|
}
|
|
|
|
_windowPreviews() {
|
|
if (!this._previewMenu) {
|
|
this._previewMenuManager = new PopupMenu.PopupMenuManager(this.actor);
|
|
|
|
this._previewMenu = new WindowPreview.WindowPreviewMenu(this);
|
|
|
|
this._previewMenuManager.addMenu(this._previewMenu);
|
|
|
|
this._previewMenu.connect('open-state-changed', (menu, isPoppedUp) => {
|
|
if (!isPoppedUp)
|
|
this._onMenuPoppedDown();
|
|
});
|
|
let id = Main.overview.connect('hiding', () => {
|
|
this._previewMenu.close();
|
|
});
|
|
this._previewMenu.actor.connect('destroy', function() {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
|
|
}
|
|
|
|
if (this._previewMenu.isOpen)
|
|
this._previewMenu.close();
|
|
else
|
|
this._previewMenu.popup();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Try to do the right thing when attempting to launch a new window of an app. In
|
|
// particular, if the application doens't allow to launch a new window, activate
|
|
// the existing window instead.
|
|
launchNewWindow(p) {
|
|
let appInfo = this.app.get_app_info();
|
|
let actions = appInfo.list_actions();
|
|
if (this.app.can_open_new_window()) {
|
|
this.animateLaunch();
|
|
// This is used as a workaround for a bug resulting in no new windows being opened
|
|
// for certain running applications when calling open_new_window().
|
|
//
|
|
// https://bugzilla.gnome.org/show_bug.cgi?id=756844
|
|
//
|
|
// Similar to what done when generating the popupMenu entries, if the application provides
|
|
// a "New Window" action, use it instead of directly requesting a new window with
|
|
// open_new_window(), which fails for certain application, notably Nautilus.
|
|
if (actions.indexOf('new-window') == -1) {
|
|
this.app.open_new_window(-1);
|
|
}
|
|
else {
|
|
let i = actions.indexOf('new-window');
|
|
if (i !== -1)
|
|
this.app.launch_action(actions[i], global.get_current_time(), -1);
|
|
}
|
|
}
|
|
else {
|
|
// Try to manually activate the first window. Otherwise, when the app is activated by
|
|
// switching to a different workspace, a launch spinning icon is shown and disappers only
|
|
// after a timeout.
|
|
let windows = this.getWindows();
|
|
if (windows.length > 0)
|
|
Main.activateWindow(windows[0])
|
|
else
|
|
this.app.activate();
|
|
}
|
|
}
|
|
|
|
_numberOverlay() {
|
|
// Add label for a Hot-Key visual aid
|
|
this._numberOverlayLabel = new St.Label();
|
|
this._numberOverlayBin = new St.Bin({
|
|
child: this._numberOverlayLabel,
|
|
x_align: St.Align.START, y_align: St.Align.START,
|
|
x_expand: true, y_expand: true
|
|
});
|
|
this._numberOverlayLabel.add_style_class_name('number-overlay');
|
|
this._numberOverlayOrder = -1;
|
|
this._numberOverlayBin.hide();
|
|
|
|
this._iconContainer.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.
|
|
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
|
// 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 [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1);
|
|
let font_size = Math.round(Math.max(12, 0.3*natWidth) / scaleFactor);
|
|
let size = Math.round(font_size*1.2);
|
|
this._numberOverlayLabel.set_style(
|
|
'font-size: ' + font_size + 'px;' +
|
|
'border-radius: ' + this.icon.iconSize + 'px;' +
|
|
'width: ' + size + 'px; height: ' + size +'px;'
|
|
);
|
|
}
|
|
|
|
setNumberOverlay(number) {
|
|
this._numberOverlayOrder = number;
|
|
this._numberOverlayLabel.set_text(number.toString());
|
|
}
|
|
|
|
toggleNumberOverlay(activate) {
|
|
if (activate && this._numberOverlayOrder > -1) {
|
|
this.updateNumberOverlay();
|
|
this._numberOverlayBin.show();
|
|
}
|
|
else
|
|
this._numberOverlayBin.hide();
|
|
}
|
|
|
|
_minimizeWindow(param) {
|
|
// Param true make all app windows minimize
|
|
let windows = this.getInterestingWindows();
|
|
let current_workspace = global.workspace_manager.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.
|
|
_activateAllWindows() {
|
|
// First activate first window so workspace is switched if needed.
|
|
// We don't do this if isolation is on!
|
|
if (!Docking.DockManager.settings.get_boolean('isolate-workspaces') &&
|
|
!Docking.DockManager.settings.get_boolean('isolate-monitors'))
|
|
this.app.activate();
|
|
|
|
// then activate all other app windows in the current workspace
|
|
let windows = this.getInterestingWindows();
|
|
let activeWorkspace = global.workspace_manager.get_active_workspace_index();
|
|
|
|
if (windows.length <= 0)
|
|
return;
|
|
|
|
let activatedWindows = 0;
|
|
|
|
for (let i = windows.length - 1; i >= 0; i--) {
|
|
if (windows[i].get_workspace().index() == activeWorkspace) {
|
|
Main.activateWindow(windows[i]);
|
|
activatedWindows++;
|
|
}
|
|
}
|
|
}
|
|
|
|
//This closes all windows of the app.
|
|
closeAllWindows() {
|
|
let windows = this.getInterestingWindows();
|
|
for (let i = 0; i < windows.length; i++)
|
|
windows[i].delete(global.get_current_time());
|
|
}
|
|
|
|
_cycleThroughWindows(reversed) {
|
|
// 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 = this.getInterestingWindows();
|
|
|
|
if (app_windows.length <1)
|
|
return
|
|
|
|
if (recentlyClickedAppLoopId > 0)
|
|
Mainloop.source_remove(recentlyClickedAppLoopId);
|
|
recentlyClickedAppLoopId = Mainloop.timeout_add(MEMORY_TIME, this._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.
|
|
let monitorIsolation = Docking.DockManager.settings.get_boolean('isolate-monitors');
|
|
if (!recentlyClickedApp ||
|
|
recentlyClickedApp.get_id() != this.app.get_id() ||
|
|
recentlyClickedAppWindows.length != app_windows.length ||
|
|
(recentlyClickedAppMonitor != this.monitorIndex && monitorIsolation)) {
|
|
recentlyClickedApp = this.app;
|
|
recentlyClickedAppWindows = app_windows;
|
|
recentlyClickedAppMonitor = this.monitorIndex;
|
|
recentlyClickedAppIndex = 0;
|
|
}
|
|
|
|
if (reversed) {
|
|
recentlyClickedAppIndex--;
|
|
if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1;
|
|
} else {
|
|
recentlyClickedAppIndex++;
|
|
}
|
|
let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length;
|
|
let window = recentlyClickedAppWindows[index];
|
|
|
|
Main.activateWindow(window);
|
|
}
|
|
|
|
_resetRecentlyClickedApp() {
|
|
if (recentlyClickedAppLoopId > 0)
|
|
Mainloop.source_remove(recentlyClickedAppLoopId);
|
|
recentlyClickedAppLoopId=0;
|
|
recentlyClickedApp =null;
|
|
recentlyClickedAppWindows = null;
|
|
recentlyClickedAppIndex = 0;
|
|
recentlyClickedAppMonitor = -1;
|
|
|
|
return false;
|
|
}
|
|
|
|
getWindows() {
|
|
return getWindows(this.app, this._location);
|
|
}
|
|
|
|
// Filter out unnecessary windows, for instance
|
|
// nautilus desktop window.
|
|
getInterestingWindows() {
|
|
return getInterestingWindows(this.app, this.monitorIndex, this._location);
|
|
}
|
|
|
|
// Does the Icon represent a location rather than an App
|
|
isLocation() {
|
|
return this._location != null;
|
|
}
|
|
};
|
|
/**
|
|
* Extend AppIconMenu
|
|
*
|
|
* - set popup arrow side based on dash orientation
|
|
* - Add close windows option based on quitfromdash extension
|
|
* (https://github.com/deuill/shell-extension-quitfromdash)
|
|
* - Add open windows thumbnails instead of list
|
|
* - update menu when application windows change
|
|
*/
|
|
const MyAppIconMenu = class DashToDock_MyAppIconMenu extends AppDisplay.AppIconMenu {
|
|
|
|
constructor(source) {
|
|
let side = Utils.getPosition();
|
|
|
|
// Damm it, there has to be a proper way of doing this...
|
|
// As I can't call the parent parent constructor (?) passing the side
|
|
// parameter, I overwite what I need later
|
|
super(source);
|
|
|
|
// Change the initialized side where required.
|
|
this._arrowSide = side;
|
|
this._boxPointer._arrowSide = side;
|
|
this._boxPointer._userArrowSide = side;
|
|
}
|
|
|
|
_redisplay() {
|
|
this.removeAll();
|
|
|
|
if (Docking.DockManager.settings.get_boolean('show-windows-preview')) {
|
|
// Display the app windows menu items and the separator between windows
|
|
// of the current desktop and other windows.
|
|
|
|
this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false);
|
|
this._allWindowsMenuItem.actor.hide();
|
|
this.addMenuItem(this._allWindowsMenuItem);
|
|
|
|
if (!this._source.app.is_window_backed()) {
|
|
this._appendSeparator();
|
|
|
|
let appInfo = this._source.app.get_app_info();
|
|
let actions = appInfo.list_actions();
|
|
if (this._source.app.can_open_new_window() &&
|
|
actions.indexOf('new-window') == -1) {
|
|
this._newWindowMenuItem = this._appendMenuItem(_("New Window"));
|
|
this._newWindowMenuItem.connect('activate', () => {
|
|
if (this._source.app.state == Shell.AppState.STOPPED)
|
|
this._source.animateLaunch();
|
|
|
|
this._source.app.open_new_window(-1);
|
|
this.emit('activate-window', null);
|
|
});
|
|
this._appendSeparator();
|
|
}
|
|
|
|
|
|
if (AppDisplay.discreteGpuAvailable &&
|
|
this._source.app.state == Shell.AppState.STOPPED &&
|
|
actions.indexOf('activate-discrete-gpu') == -1) {
|
|
this._onDiscreteGpuMenuItem = this._appendMenuItem(_("Launch using Dedicated Graphics Card"));
|
|
this._onDiscreteGpuMenuItem.connect('activate', () => {
|
|
if (this._source.app.state == Shell.AppState.STOPPED)
|
|
this._source.animateLaunch();
|
|
|
|
this._source.app.launch(0, -1, true);
|
|
this.emit('activate-window', null);
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < actions.length; i++) {
|
|
let action = actions[i];
|
|
let item = this._appendMenuItem(appInfo.get_action_name(action));
|
|
item.connect('activate', (emitter, event) => {
|
|
this._source.app.launch_action(action, event.get_time(), -1);
|
|
this.emit('activate-window', null);
|
|
});
|
|
}
|
|
|
|
let canFavorite = global.settings.is_writable('favorite-apps') &&
|
|
!this._source.isLocation();
|
|
|
|
if (canFavorite) {
|
|
this._appendSeparator();
|
|
|
|
let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id());
|
|
|
|
if (isFavorite) {
|
|
let item = this._appendMenuItem(_("Remove from Favorites"));
|
|
item.connect('activate', () => {
|
|
let favs = AppFavorites.getAppFavorites();
|
|
favs.removeFavorite(this._source.app.get_id());
|
|
});
|
|
} else {
|
|
let item = this._appendMenuItem(_("Add to Favorites"));
|
|
item.connect('activate', () => {
|
|
let favs = AppFavorites.getAppFavorites();
|
|
favs.addFavorite(this._source.app.get_id());
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop') &&
|
|
!this._source.isLocation()) {
|
|
this._appendSeparator();
|
|
let item = this._appendMenuItem(_("Show Details"));
|
|
item.connect('activate', () => {
|
|
let id = this._source.app.get_id();
|
|
let args = GLib.Variant.new('(ss)', [id, '']);
|
|
Gio.DBus.get(Gio.BusType.SESSION, null,
|
|
function(o, res) {
|
|
let bus = Gio.DBus.get_finish(res);
|
|
bus.call('org.gnome.Software',
|
|
'/org/gnome/Software',
|
|
'org.gtk.Actions', 'Activate',
|
|
GLib.Variant.new('(sava{sv})',
|
|
['details', [args], null]),
|
|
null, 0, -1, null, null);
|
|
Main.overview.hide();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
} else {
|
|
super._redisplay();
|
|
}
|
|
|
|
// quit menu
|
|
this._appendSeparator();
|
|
this._quitfromDashMenuItem = this._appendMenuItem(_("Quit"));
|
|
this._quitfromDashMenuItem.connect('activate', () => {
|
|
this._source.closeAllWindows();
|
|
});
|
|
|
|
this.update();
|
|
}
|
|
|
|
// update menu content when application windows change. This is desirable as actions
|
|
// acting on windows (closing) are performed while the menu is shown.
|
|
update() {
|
|
|
|
if(Docking.DockManager.settings.get_boolean('show-windows-preview')){
|
|
|
|
let windows = this._source.getInterestingWindows();
|
|
|
|
// update, show or hide the quit menu
|
|
if ( windows.length > 0) {
|
|
let quitFromDashMenuText = "";
|
|
if (windows.length == 1)
|
|
this._quitfromDashMenuItem.label.set_text(_("Quit"));
|
|
else
|
|
this._quitfromDashMenuItem.label.set_text(_("Quit %d Windows").format(windows.length));
|
|
|
|
this._quitfromDashMenuItem.actor.show();
|
|
|
|
} else {
|
|
this._quitfromDashMenuItem.actor.hide();
|
|
}
|
|
|
|
// update, show, or hide the allWindows menu
|
|
// Check if there are new windows not already displayed. In such case, repopulate the allWindows
|
|
// menu. Windows removal is already handled by each preview being connected to the destroy signal
|
|
let old_windows = this._allWindowsMenuItem.menu._getMenuItems().map(function(item){
|
|
return item._window;
|
|
});
|
|
|
|
let new_windows = windows.filter(function(w) {return old_windows.indexOf(w) < 0;});
|
|
if (new_windows.length > 0) {
|
|
this._populateAllWindowMenu(windows);
|
|
|
|
// Try to set the width to that of the submenu.
|
|
// TODO: can't get the actual size, getting a bit less.
|
|
// Temporary workaround: add 15px to compensate
|
|
this._allWindowsMenuItem.actor.width = this._allWindowsMenuItem.menu.actor.width + 15;
|
|
|
|
}
|
|
|
|
// The menu is created hidden and never hidded after being shown. Instead, a singlal
|
|
// connected to its items destroy will set is insensitive if no more windows preview are shown.
|
|
if (windows.length > 0){
|
|
this._allWindowsMenuItem.actor.show();
|
|
this._allWindowsMenuItem.setSensitive(true);
|
|
}
|
|
|
|
// Update separators
|
|
this._getMenuItems().forEach(this._updateSeparatorVisibility.bind(this));
|
|
}
|
|
|
|
|
|
}
|
|
|
|
_populateAllWindowMenu(windows) {
|
|
|
|
this._allWindowsMenuItem.menu.removeAll();
|
|
|
|
if (windows.length > 0) {
|
|
|
|
let activeWorkspace = global.workspace_manager.get_active_workspace();
|
|
let separatorShown = windows[0].get_workspace() != activeWorkspace;
|
|
|
|
for (let i = 0; i < windows.length; i++) {
|
|
let window = windows[i];
|
|
if (!separatorShown && window.get_workspace() != activeWorkspace) {
|
|
this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
separatorShown = true;
|
|
}
|
|
|
|
let item = new WindowPreview.WindowPreviewMenuItem(window);
|
|
this._allWindowsMenuItem.menu.addMenuItem(item);
|
|
item.connect('activate', () => {
|
|
this.emit('activate-window', window);
|
|
});
|
|
|
|
// This is to achieve a more gracefull transition when the last windows is closed.
|
|
item.connect('destroy', () => {
|
|
if(this._allWindowsMenuItem.menu._getMenuItems().length == 1) // It's still counting the item just going to be destroyed
|
|
this._allWindowsMenuItem.setSensitive(false);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Signals.addSignalMethods(MyAppIconMenu.prototype);
|
|
|
|
function getWindows(app, location) {
|
|
if (location != null && Docking.DockManager.getDefault().fm1Client) {
|
|
return Docking.DockManager.getDefault().fm1Client.getWindows(location);
|
|
} else {
|
|
return app.get_windows();
|
|
}
|
|
}
|
|
|
|
// Filter out unnecessary windows, for instance
|
|
// nautilus desktop window.
|
|
function getInterestingWindows(app, monitorIndex, location) {
|
|
let windows = getWindows(app, location).filter(function(w) {
|
|
return !w.skip_taskbar;
|
|
});
|
|
|
|
let settings = Docking.DockManager.settings;
|
|
|
|
// When using workspace isolation, we filter out windows
|
|
// that are not in the current workspace
|
|
if (settings.get_boolean('isolate-workspaces'))
|
|
windows = windows.filter(function(w) {
|
|
return w.get_workspace().index() == global.workspace_manager.get_active_workspace_index();
|
|
});
|
|
|
|
if (settings.get_boolean('isolate-monitors'))
|
|
windows = windows.filter(function(w) {
|
|
return w.get_monitor() == monitorIndex;
|
|
});
|
|
|
|
return windows;
|
|
}
|
|
|
|
/**
|
|
* A ShowAppsIcon improved class.
|
|
*
|
|
* - 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)
|
|
*
|
|
*/
|
|
|
|
var MyShowAppsIcon = GObject.registerClass({
|
|
Signals: {
|
|
'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
|
|
'sync-tooltip': {}
|
|
}
|
|
}
|
|
, class DashToDock_MyShowAppsIcon extends Dash.ShowAppsIcon {
|
|
_init() {
|
|
super._init();
|
|
|
|
// Re-use appIcon methods
|
|
let appIconPrototype = AppDisplay.AppIcon.prototype;
|
|
this.actor.connect('leave-event', appIconPrototype._onLeaveEvent.bind(this));
|
|
this.actor.connect('button-press-event', appIconPrototype._onButtonPress.bind(this));
|
|
this.actor.connect('touch-event', appIconPrototype._onTouchEvent.bind(this));
|
|
this.actor.connect('popup-menu', appIconPrototype._onKeyboardPopupMenu.bind(this));
|
|
this.actor.connect('clicked', this._removeMenuTimeout.bind(this));
|
|
|
|
this._menu = null;
|
|
this._menuManager = new PopupMenu.PopupMenuManager(this.actor);
|
|
this._menuTimeoutId = 0;
|
|
}
|
|
|
|
get actor() {
|
|
/* Until GNOME Shell AppIcon is an actor we need to provide this
|
|
* compatibility layer or the shell won't be able to access to the
|
|
* actual actor */
|
|
return this.toggleButton;
|
|
}
|
|
|
|
showLabel() {
|
|
itemShowLabel.call(this);
|
|
}
|
|
|
|
_onMenuPoppedDown() {
|
|
AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments);
|
|
}
|
|
|
|
_setPopupTimeout() {
|
|
AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments);
|
|
}
|
|
|
|
_removeMenuTimeout() {
|
|
AppDisplay.AppIcon.prototype._removeMenuTimeout.apply(this, arguments);
|
|
}
|
|
|
|
popupMenu() {
|
|
this._removeMenuTimeout();
|
|
this.actor.fake_release();
|
|
|
|
if (!this._menu) {
|
|
this._menu = new MyShowAppsIconMenu(this);
|
|
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', function() {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
this._menuManager.addMenu(this._menu);
|
|
}
|
|
|
|
this.emit('menu-state-changed', true);
|
|
|
|
this.actor.set_hover(true);
|
|
this._menu.popup();
|
|
this._menuManager.ignoreRelease();
|
|
this.emit('sync-tooltip');
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* A menu for the showAppsIcon
|
|
*/
|
|
var MyShowAppsIconMenu = class DashToDock_MyShowAppsIconMenu extends MyAppIconMenu {
|
|
_redisplay() {
|
|
this.removeAll();
|
|
|
|
/* Translators: %s is "Settings", which is automatically translated. You
|
|
can also translate the full message if this fits better your language. */
|
|
let name = __('Dash to Dock %s').format(_('Settings'))
|
|
let item = this._appendMenuItem(name);
|
|
|
|
item.connect('activate', function () {
|
|
Util.spawn(["gnome-shell-extension-prefs", Me.metadata.uuid]);
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This function is used for both extendShowAppsIcon and extendDashItemContainer
|
|
*/
|
|
function itemShowLabel() {
|
|
// Check if the label is still present at all. When switching workpaces, the
|
|
// item might have been destroyed in between.
|
|
if (!this._labelText || this.label.get_stage() == null)
|
|
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 x, y, xOffset, yOffset;
|
|
|
|
let position = Utils.getPosition();
|
|
this._isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM));
|
|
let labelOffset = node.get_length('-x-offset');
|
|
|
|
switch (position) {
|
|
case St.Side.LEFT:
|
|
yOffset = Math.floor((itemHeight - labelHeight) / 2);
|
|
y = stageY + yOffset;
|
|
xOffset = labelOffset;
|
|
x = stageX + this.get_width() + xOffset;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
yOffset = Math.floor((itemHeight - labelHeight) / 2);
|
|
y = stageY + yOffset;
|
|
xOffset = labelOffset;
|
|
x = Math.round(stageX) - labelWidth - xOffset;
|
|
break;
|
|
case St.Side.TOP:
|
|
y = stageY + labelOffset + itemHeight;
|
|
xOffset = Math.floor((itemWidth - labelWidth) / 2);
|
|
x = stageX + xOffset;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
yOffset = labelOffset;
|
|
y = stageY - labelHeight - yOffset;
|
|
xOffset = Math.floor((itemWidth - labelWidth) / 2);
|
|
x = stageX + xOffset;
|
|
break;
|
|
}
|
|
|
|
// keep the label inside the screen border
|
|
// Only needed fot the x coordinate.
|
|
|
|
// Leave a few pixel gap
|
|
let gap = 5;
|
|
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.remove_all_transitions();
|
|
this.label.set_position(x, y);
|
|
this.label.ease({
|
|
opacity: 255,
|
|
duration: Dash.DASH_ITEM_LABEL_SHOW_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD
|
|
});
|
|
}
|