diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js new file mode 100644 index 0000000..c6c694c --- /dev/null +++ b/js/ui/windowManager.js @@ -0,0 +1,1501 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Cinnamon = imports.gi.Cinnamon; +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const St = imports.gi.St; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Main = imports.ui.main; +const WindowMenu = imports.ui.windowMenu; +const GObject = imports.gi.GObject; +const AppSwitcher = imports.ui.appSwitcher.appSwitcher; +const Dialog = imports.ui.dialog; +const ModalDialog = imports.ui.modalDialog; +const CloseDialog = imports.ui.closeDialog; +const WorkspaceOsd = imports.ui.workspaceOsd; + +const {CoverflowSwitcher} = imports.ui.appSwitcher.coverflowSwitcher; +const {TimelineSwitcher} = imports.ui.appSwitcher.timelineSwitcher; +const {ClassicSwitcher} = imports.ui.appSwitcher.classicSwitcher; + +// maps org.cinnamon window-effect-speed +const WINDOW_ANIMATION_TIME_MULTIPLIERS = [ + 1.4, // 0 SLOW + 1.0, // 1 DEFAULT + 0.6 // 2 FAST +] + +const DIM_TIME = 500; +const UNDIM_TIME = 250; +const DIM_BRIGHTNESS = -0.2; + +/* edge zones for tiling/snapping identification + copied from muffin/src/core/window-private.h + + ___________________________ + | 4 0 5 | + | | + | | + | | + | | + | 2 3 | + | | + | | + | | + | | + | 7 1 6 | + |_________________________| + +*/ + +const ZONE_TOP = 0; +const ZONE_BOTTOM = 1; +const ZONE_LEFT = 2; +const ZONE_RIGHT = 3; +const ZONE_TL = 4; +const ZONE_TR = 5; +const ZONE_BR = 6; +const ZONE_BL = 7; + +var DisplayChangeDialog = GObject.registerClass( +class DisplayChangeDialog extends ModalDialog.ModalDialog { + _init(wm) { + super._init(); + + this._wm = wm; + + this._countDown = Meta.MonitorManager.get_display_configuration_timeout(); + + // Translators: This string should be shorter than 30 characters + let title = _("Keep these display settings?"); + let description = this._formatCountDown(); + + this._content = new Dialog.MessageDialogContent({ title, description }); + this.contentLayout.add_child(this._content); + + /* Translators: this and the following message should be limited in length, + to avoid ellipsizing the labels. + */ + this._cancelButton = this.addButton({ label: _("Revert"), + action: this._onFailure.bind(this), + key: Clutter.KEY_Escape }); + this._cancelButton.grab_key_focus(); + this._okButton = this.addButton({ label: _("Keep changes"), + action: this._onSuccess.bind(this) }); + + this._timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, this._tick.bind(this)); + GLib.Source.set_name_by_id(this._timeoutId, '[cinnamon] this._tick'); + } + + close(timestamp) { + if (this._timeoutId > 0) { + GLib.source_remove(this._timeoutId); + this._timeoutId = 0; + } + + super.close(timestamp); + } + + _formatCountDown() { + return ngettext("Reverting to previous display settings in %d second.", + "Reverting to previous display settings in %d seconds.", + this._countDown).format(this._countDown); + } + + _tick() { + this._countDown--; + if (this._countDown == 0) { + /* muffin already takes care of failing at timeout */ + this._timeoutId = 0; + this.close(); + return GLib.SOURCE_REMOVE; + } + + this._content.description = this._formatCountDown(); + return GLib.SOURCE_CONTINUE; + } + + _onFailure() { + this._wm.complete_display_change(false); + this.close(); + } + + _onSuccess() { + this._wm.complete_display_change(true); + this.close(); + } +}); + +class WindowDimmer { + constructor(actor) { + this._brightnessEffect = new Clutter.BrightnessContrastEffect({ + name: 'dim', + enabled: false + }); + actor.add_effect(this._brightnessEffect); + this.actor = actor; + this._enabled = true; + } + + _syncEnabled() { + let animating = this.actor.get_transition('@effects.dim.brightness') != null; + let dimmed = this._brightnessEffect.brightness.red != 127; + this._brightnessEffect.enabled = this._enabled && (animating || dimmed); + } + + setEnabled(enabled) { + this._enabled = enabled; + this._syncEnabled(); + } + + setDimmed(dimmed, animate) { + let val = 127 * (1 + (dimmed ? 1 : 0) * DIM_BRIGHTNESS); + let color = Clutter.Color.new(val, val, val, 255); + + this.actor.ease_property('@effects.dim.brightness', color, { + mode: Clutter.AnimationMode.LINEAR, + duration: (dimmed ? DIM_TIME : UNDIM_TIME) * (animate ? 1 : 0), + onComplete: () => this._syncEnabled() + }); + + this._syncEnabled(); + } +}; + +function getWindowDimmer(actor) { + let enabled = Meta.prefs_get_attach_modal_dialogs(); + + if (enabled) { + if (!actor._windowDimmer) + actor._windowDimmer = new WindowDimmer(actor); + return actor._windowDimmer; + } else { + return null; + } +} + +class TilePreview { + constructor() { + this.actor = new St.Bin({ style_class: 'tile-preview', important: true }); + global.window_group.add_actor(this.actor); + + this._reset(); + this._showing = false; + this.anim_time = null + } + + show(window, tileRect, monitorIndex, animate, anim_time) { + this.anim_time = anim_time; + + let windowActor = window.get_compositor_private(); + if (!windowActor) + return; + + if (this._rect && this._rect.equal(tileRect)) + return; + + let changeMonitor = (this._monitorIndex === -1 || + this._monitorIndex != monitorIndex); + + this._monitorIndex = monitorIndex; + this._rect = tileRect; + let monitor = Main.layoutManager.monitors[monitorIndex]; + let {x, y, width, height} = tileRect; + + if (!this._showing || changeMonitor) { + let monitorRect = new Meta.Rectangle({ x: monitor.x, + y: monitor.y, + width: monitor.width, + height: monitor.height }); + let [, rect] = window.get_buffer_rect().intersect(monitorRect); + this.actor.set_size(rect.width, rect.height); + this.actor.set_position(rect.x, rect.y); + this.actor.opacity = 0; + } + + this._showing = true; + this.actor.show(); + + let props = { + x, + y, + width, + height, + opacity: 255, + }; + + if (animate) { + this.actor.remove_all_transitions(); + + Object.assign(props, { + duration: this.anim_time, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); + this.actor.ease(props); + return; + } + + Object.assign(this.actor, props); + } + + hide() { + if (!this._showing) + return; + + this._showing = false; + + this.actor.remove_all_transitions(); + this.actor.ease({ + opacity: 0, + duration: this.anim_time, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this._reset() + }); + } + + _reset() { + this.actor.hide(); + this._rect = null; + this._monitorIndex = -1; + } + + destroy() { + this.actor.destroy(); + } +}; + +var ResizePopup = GObject.registerClass( +class ResizePopup extends St.Widget { + _init() { + super._init({ layout_manager: new Clutter.BinLayout() }); + this._label = new St.Label({ + style_class: 'resize-popup', + important: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + y_expand: true + }); + this.add_child(this._label); + Main.uiGroup.add_actor(this); + } + + set(rect, displayW, displayH) { + /* Translators: This represents the size of a window. The first number is + * the width of the window and the second is the height. */ + let text = "%d × %d".format(displayW, displayH); + this._label.set_text(text); + + this.set_position(rect.x, rect.y); + this.set_size(rect.width, rect.height); + } +}); + +var WindowManager = class WindowManager { + MENU_ANIMATION_TIME = 150; + WORKSPACE_ANIMATION_TIME = 150; + TILE_PREVIEW_ANIMATION_TIME = 150; + SIZE_CHANGE_ANIMATION_TIME = 120; + MAP_ANIMATION_TIME = 120; + DESTROY_ANIMATION_TIME = 120; + MINIMIZE_ANIMATION_TIME = 120; + + constructor() { + this._cinnamonwm = global.window_manager; + + this._minimizing = new Set(); + this._unminimizing = new Set(); + this._mapping = new Set(); + this._resizing = new Set(); + this._resizePending = new Set(); + this._destroying = new Set(); + this._movingWindow = null; + this._seenWindows = new Set(); + + this.wm_settings = new Gio.Settings({schema_id: 'org.cinnamon.muffin'}); + + global.settings.connect('changed::desktop-effects', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-workspace', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-on-menus', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-on-dialogs', this.onSettingsChanged.bind(this)); + + global.settings.connect('changed::desktop-effects-change-size', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-close', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-map', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::desktop-effects-minimize', this.onSettingsChanged.bind(this)); + global.settings.connect('changed::window-effect-speed', this.onSettingsChanged.bind(this)); + + this.onSettingsChanged(global.settings, "desktop-effects-workspace"); + + this._workspace_osd_array = []; + this._tilePreview = null; + this._dimmedWindows = []; + this._animationBlockCount = 0; + this._switchData = null; + this._workspaceOsds = {}; + + this._cinnamonwm.connect('kill-window-effects', (cinnamonwm, actor) => { + this._unminimizeWindowDone(cinnamonwm, actor); + this._minimizeWindowDone(cinnamonwm, actor); + this._mapWindowDone(cinnamonwm, actor); + this._destroyWindowDone(cinnamonwm, actor); + this._sizeChangeWindowDone(cinnamonwm, actor); + }); + + this._cinnamonwm.connect('show-tile-preview', this._showTilePreview.bind(this)); + this._cinnamonwm.connect('hide-tile-preview', this._hideTilePreview.bind(this)); + this._cinnamonwm.connect('show-window-menu', this._showWindowMenu.bind(this)); + this._cinnamonwm.connect('minimize', this._minimizeWindow.bind(this)); + this._cinnamonwm.connect('unminimize', this._unminimizeWindow.bind(this)); + this._cinnamonwm.connect('size-change', this._sizeChangeWindow.bind(this)); + this._cinnamonwm.connect('size-changed', this._sizeChangedWindow.bind(this)); + this._cinnamonwm.connect('map', this._mapWindow.bind(this)); + this._cinnamonwm.connect('destroy', this._destroyWindow.bind(this)); + this._cinnamonwm.connect('filter-keybinding', this._filterKeybinding.bind(this)); + global.window_manager.connect('switch-workspace', (c, f, t, d) => this._switchWorkspace(c, f, t, d)); + + let kbm = Main.keybindingManager; + let WORKSPACE_MODES = Cinnamon.ActionMode.NORMAL | + Cinnamon.ActionMode.OVERVIEW | + Cinnamon.ActionMode.EXPO; + + kbm.setBuiltinHandler('move-to-workspace-left', Meta.KeyBindingAction.MOVE_TO_WORKSPACE_LEFT, + (d, w, b) => this._moveWindowToWorkspaceLeft(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('move-to-workspace-right', Meta.KeyBindingAction.MOVE_TO_WORKSPACE_RIGHT, + (d, w, b) => this._moveWindowToWorkspaceRight(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('toggle-window-selection', Meta.KeyBindingAction.TOGGLE_WINDOW_SELECTION, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('toggle-workspace-selection', Meta.KeyBindingAction.TOGGLE_WORKSPACE_SELECTION, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('switch-to-workspace-left', Meta.KeyBindingAction.WORKSPACE_LEFT, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('switch-to-workspace-right', Meta.KeyBindingAction.WORKSPACE_RIGHT, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('switch-to-workspace-up', Meta.KeyBindingAction.WORKSPACE_UP, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('switch-to-workspace-down', Meta.KeyBindingAction.WORKSPACE_DOWN, + (d, w, b) => this._showWorkspaceSwitcher(d, w, b), WORKSPACE_MODES); + kbm.setBuiltinHandler('switch-windows', Meta.KeyBindingAction.SWITCH_WINDOWS, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('switch-group', Meta.KeyBindingAction.SWITCH_GROUP, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('switch-windows-backward', Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('switch-group-backward', Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('switch-panels', Meta.KeyBindingAction.SWITCH_PANELS, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + kbm.setBuiltinHandler('switch-panels-backward', Meta.KeyBindingAction.SWITCH_PANELS_BACKWARD, + (d, w, b) => this._startAppSwitcher(d, w, b), Cinnamon.ActionMode.NORMAL); + + global.display.connect('show-resize-popup', this._showResizePopup.bind(this)); + this._cinnamonwm.connect('create-close-dialog', this._createCloseDialog.bind(this)); + this._cinnamonwm.connect('confirm-display-change', this._confirmDisplayChange.bind(this)); + + /* TODO: Wacom + global.display.connect('show-pad-osd', this._showPadOsd.bind(this)); + global.display.connect('show-osd', (display, monitorIndex, iconName, label) => { + let icon = Gio.Icon.new_for_string(iconName); + Main.osdWindowManager.show(monitorIndex, icon, label, null); + }); + */ + + Main.overview.connect('showing', () => { + let {_dimmedWindows} = this; + for (let i = 0, len = _dimmedWindows.length; i < len; i++) { + this._undimWindow(_dimmedWindows[i], true); + } + }); + Main.overview.connect('hiding', () => { + let {_dimmedWindows} = this; + for (let i = 0, len = _dimmedWindows.length; i < len; i++) { + this._dimWindow(_dimmedWindows[i], true); + } + }); + + this._windowMenuManager = new WindowMenu.WindowMenuManager(); + + // Minimized windows won't be reliable clone sources until they're + // shown once. If they start minimized, monitor them until they've + // been shown for the first time. (See windowUtils.js) + const handleSeen = (metaWindow) => { + if (this.windowSeen(metaWindow) || metaWindow === null || !Main.isInteresting(metaWindow)) { + return; + } + + if (metaWindow.get_workspace().index() !== global.workspace_manager.get_active_workspace_index()) { + return; + } + + if (!metaWindow.minimized) { + this._seenWindows.add(metaWindow); + return; + } + + // If not, add it when it gets unminimized. + let minimize_id = metaWindow.connect("notify::minimized", () => { + if (!metaWindow.minimized) { + this._seenWindows.add(metaWindow); + } + }); + + metaWindow.connect("unmanaging", () => { + metaWindow.disconnect(minimize_id); + this._seenWindows.delete(metaWindow); + }); + } + + global.display.connect("window-created", (display, metaWindow) => { + handleSeen(metaWindow); + }); + + global.workspace_manager.connect("workspace-switched", (from, to, direction) => { + const allWindowActors = Meta.get_window_actors(global.display); + allWindowActors.forEach((actor) => handleSeen(actor.meta_window)); + }); + + const allWindowActors = Meta.get_window_actors(global.display); + allWindowActors.forEach((actor) => handleSeen(actor.meta_window)); + } + + windowSeen(metaWindow) { + return this._seenWindows.has(metaWindow); + } + + + onSettingsChanged(settings, key, data=null) { + if (key === "desktop-effects-workspace") { + Main.updateAnimationsEnabled(); + } + + this.desktop_effects_windows = Main.animations_enabled && global.settings.get_boolean("desktop-effects"); + this.desktop_effects_menus = Main.animations_enabled && global.settings.get_boolean("desktop-effects-on-menus"); + this.desktop_effects_dialogs = Main.animations_enabled && global.settings.get_boolean("desktop-effects-on-dialogs"); + this.desktop_effects_size_change = this.desktop_effects_windows && global.settings.get_boolean("desktop-effects-change-size"); + + this.desktop_effects_close_type = global.settings.get_string("desktop-effects-close"); + this.desktop_effects_map_type = global.settings.get_string("desktop-effects-map"); + this.desktop_effects_minimize_type = global.settings.get_string("desktop-effects-minimize"); + + this.window_effect_multiplier = WINDOW_ANIMATION_TIME_MULTIPLIERS[global.settings.get_int("window-effect-speed")]; + } + + _shouldAnimate(actor, types=null) { + // Check if system is in modal state or in software rendering + if (Main.modalCount || !Main.animations_enabled) { + return false; + } + + let type = actor.meta_window.get_window_type(); + + if (types !== null) { + if (!types.includes(type)) { + return false; + } + } + + switch (type) { + case Meta.WindowType.NORMAL: + return this.desktop_effects_windows; + case Meta.WindowType.DIALOG: + case Meta.WindowType.MODAL_DIALOG: + return this.desktop_effects_dialogs; + case Meta.WindowType.MENU: + case Meta.WindowType.DROPDOWN_MENU: + case Meta.WindowType.POPUP_MENU: + default: + return false; + } + } + + _minimizeWindow(cinnamonwm, actor) { + Main.soundManager.play('minimize'); + + if (!this._shouldAnimate(actor) || this.desktop_effects_minimize_type == "none") { + cinnamonwm.completed_minimize(actor); + return; + } + this._minimizing.add(actor); + + switch (this.desktop_effects_minimize_type) { + case "traditional": + { + let [success, geom] = actor.meta_window.get_icon_geometry(); + + if (success) { + let rect = actor.meta_window.get_buffer_rect(); + + actor.set_position(rect.x, rect.y); + actor.set_scale(1.0, 1.0); + + let xDest, yDest, xScale, yScale; + xDest = geom.x; + yDest = geom.y; + xScale = geom.width / actor.width; + yScale = geom.height / actor.height; + + actor.ease({ + scale_x: xScale, + scale_y: yScale, + x: xDest, + y: yDest, + duration: this.MINIMIZE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_IN_QUAD, + onStopped: () => this._minimizeWindowDone(cinnamonwm, actor), + }); + + return; + } + } + case "fade": + { // this fallback for 'traditional' also + actor.set_scale(1.0, 1.0); + actor.set_pivot_point(0.5, 0.5); + + actor.ease({ + opacity: 0, + scale_x: 0.88, + scale_y: 0.88, + duration: this.MINIMIZE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._minimizeWindowDone(cinnamonwm, actor), + }); + + return; + } + case "fly": + { + let xDest = actor.x; + let workarea = actor.meta_window.get_work_area_current_monitor(); + + let yDest = workarea.y + workarea.height; + + // The transition time set is the time if the animation starts/ends at the middle of the screen. + // Scale it proportional to the actual distance so that the speed of all animations will be constant. + let dist = Math.abs(actor.y - yDest); + let time = this.MINIMIZE_ANIMATION_TIME * (dist / yDest * 2); + + actor.ease({ + x: xDest, + y: yDest, + duration: time * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_IN_SINE, + onStopped: () => this._minimizeWindowDone(cinnamonwm, actor), + }); + + return; + } + default: + { + this._minimizeWindowDone(cinnamonwm, actor); + } + } + } + + _minimizeWindowDone(cinnamonwm, actor) { + if (this._minimizing.delete(actor)) { + actor.remove_all_transitions() + let rect = actor.meta_window.get_buffer_rect(); + actor.set_position(rect.x, rect.y); + actor.set_pivot_point(0, 0); + actor.set_scale(1.0, 1.0); + actor.set_opacity(255); + cinnamonwm.completed_minimize(actor); + } + } + + _unminimizeWindow(cinnamonwm, actor) { + Main.soundManager.play('minimize'); + + if (!this._shouldAnimate(actor) || this.desktop_effects_map_type == "none") { + cinnamonwm.completed_unminimize(actor); + return; + } + + this._unminimizing.add(actor); + + switch (this.desktop_effects_map_type) { + case "move": // this is really fade.. a move effect would essentially make it look like traditional, + // and it looks bad for things like restoring windows from a tray icon with multiple monitors. + + { + actor.orig_opacity = actor.opacity; + actor.set_pivot_point(0.5, 0.5); + actor.scale_x = 0.94; + actor.scale_y = 0.94; + actor.opacity = 0; + actor.show(); + + actor.ease({ + opacity: actor.orig_opacity, + scale_x: 1, + scale_y: 1, + duration: this.MAP_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._unminimizeWindowDone(cinnamonwm, actor), + }); + + return; + } + case "fly": + { + // buffer rect will have the true position of the window. + // if we interrupted a minimize,, the actor's position won't match. If it doesn't, + // we use that as its starting point, otherwise we use the monitor workarea. + + let rect = actor.meta_window.get_buffer_rect(); + let [xDest, yDest] = [rect.x, rect.y]; + + let ySrc; + + if (actor.y === yDest) { + let workarea = actor.meta_window.get_work_area_current_monitor(); + ySrc = workarea.y + workarea.height; + } else { + ySrc = actor.y; + } + + actor.set_position(xDest, ySrc); + + let dist = Math.abs(ySrc - yDest); + let time = this.MAP_ANIMATION_TIME * (dist / ySrc * 2); + + actor.show(); + + actor.ease({ + x: xDest, + y: yDest, + duration: time * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_IN_SINE, + onStopped: () => this._unminimizeWindowDone(cinnamonwm, actor), + }); + + return; + } + case "traditional": + { + let [success, geom] = actor.meta_window.get_icon_geometry(); + if (success) { + let rect = actor.meta_window.get_buffer_rect(); + let [xDest, yDest] = [rect.x, rect.y]; + + actor.set_position(geom.x, geom.y); + actor.set_scale(geom.width / actor.width, + geom.height / actor.height); + actor.show(); + + actor.ease({ + scale_x: 1.0, + scale_y: 1.0, + x: xDest, + y: yDest, + duration: this.MAP_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._unminimizeWindowDone(cinnamonwm, actor), + }); + } else { // fall-back effect. Same as map + actor.set_pivot_point(0.5, 0.5); + actor.scale_x = 0.94; + actor.scale_y = 0.94; + actor.opacity = 0; + actor.show(); + + actor.ease({ + opacity: 255, + scale_x: 1.0, + scale_y: 1.0, + duration: this.MAP_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._unminimizeWindowDone(cinnamonwm, actor), + }); + } + + return; + } + default: + { + this._unminimizeWindowDone(cinnamonwm, actor); + } + } + } + + _unminimizeWindowDone(cinnamonwm, actor) { + if (this._unminimizing.delete(actor)) { + actor.remove_all_transitions() + actor.set_scale(1.0, 1.0); + actor.set_opacity(255); + actor.set_pivot_point(0, 0); + + cinnamonwm.completed_unminimize(actor); + } + } + + _sizeChangeWindow(cinnamonwm, actor, whichChange, oldFrameRect, _oldBufferRect) { + switch (whichChange) { + case Meta.SizeChange.MAXIMIZE: + Main.soundManager.play('maximize'); + break; + case Meta.SizeChange.UNMAXIMIZE: + Main.soundManager.play('unmaximize'); + break; + case Meta.SizeChange.TILE: + Main.soundManager.play('tile'); + break; + } + + if (!this._shouldAnimate(actor, [Meta.WindowType.NORMAL]) || !this.desktop_effects_size_change) { + cinnamonwm.completed_size_change(actor); + return; + } + + if (oldFrameRect.width > 0 && oldFrameRect.height > 0) + this._prepareAnimationInfo(cinnamonwm, actor, oldFrameRect, whichChange); + else + cinnamonwm.completed_size_change(actor); + } + + _prepareAnimationInfo(cinnamonwm, actor, oldFrameRect, _change) { + // Position a clone of the window on top of the old position, + // while actor updates are frozen. + let actorContent = Cinnamon.util_get_content_for_window_actor(actor, oldFrameRect); + let actorClone = new St.Widget({ content: actorContent }); + actorClone.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); + actorClone.set_position(oldFrameRect.x, oldFrameRect.y); + actorClone.set_size(oldFrameRect.width, oldFrameRect.height); + + if (this._clearSizeAnimationInfo(actor)) + this._cinnamonwm.completed_size_change(actor); + + let destroyId = actor.connect('destroy', () => { + this._clearSizeAnimationInfo(actor); + }); + + this._resizePending.add(actor); + actor.__animationInfo = { clone: actorClone, + oldRect: oldFrameRect, + destroyId }; + } + + _sizeChangedWindow(cinnamonwm, actor) { + if (!actor.__animationInfo) + return; + if (this._resizing.has(actor)) + return; + + let actorClone = actor.__animationInfo.clone; + let targetRect = actor.meta_window.get_frame_rect(); + let sourceRect = actor.__animationInfo.oldRect; + + let scaleX = targetRect.width / sourceRect.width; + let scaleY = targetRect.height / sourceRect.height; + + this._resizePending.delete(actor); + this._resizing.add(actor); + + Main.uiGroup.add_child(actorClone); + + // Now scale and fade out the clone + actorClone.ease({ + x: targetRect.x, + y: targetRect.y, + scale_x: scaleX, + scale_y: scaleY, + opacity: 0, + duration: this.SIZE_CHANGE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + actor.translation_x = -targetRect.x + sourceRect.x; + actor.translation_y = -targetRect.y + sourceRect.y; + + // Now set scale the actor to size it as the clone. + actor.scale_x = 1 / scaleX; + actor.scale_y = 1 / scaleY; + + // Scale it to its actual new size + actor.ease({ + scale_x: 1, + scale_y: 1, + translation_x: 0, + translation_y: 0, + duration: this.SIZE_CHANGE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._sizeChangeWindowDone(cinnamonwm, actor), + }); + + // Now unfreeze actor updates, to get it to the new size. + // It's important that we don't wait until the animation is completed to + // do this, otherwise our scale will be applied to the old texture size. + cinnamonwm.completed_size_change(actor); + } + + _clearSizeAnimationInfo(actor) { + if (actor.__animationInfo) { + actor.__animationInfo.clone.destroy(); + actor.disconnect(actor.__animationInfo.destroyId); + delete actor.__animationInfo; + return true; + } + return false; + } + + _sizeChangeWindowDone(cinnamonwm, actor) { + if (this._resizing.delete(actor)) { + actor.remove_all_transitions(); + actor.scale_x = 1.0; + actor.scale_y = 1.0; + actor.translation_x = 0; + actor.translation_y = 0; + this._clearSizeAnimationInfo(actor); + } + + if (this._resizePending.delete(actor)) + this._cinnamonwm.completed_size_change(actor); + } + + _filterKeybinding(shellwm, binding) { + let bindingName = binding.get_name(); + let [, entry] = Main.keybindingManager._lookupEntry(bindingName); + + return Main._shouldFilterKeybinding(entry); + } + + _hasAttachedDialogs(window, ignoreWindow) { + let count = 0; + window.foreach_transient(function(win) { + if (win != ignoreWindow && win.is_attached_dialog()) + count++; + return false; + }); + return count != 0; + } + + _checkDimming(window, ignoreWindow) { + let shouldDim = this._hasAttachedDialogs(window, ignoreWindow); + + if (shouldDim && !window._dimmed) { + window._dimmed = true; + this._dimmedWindows.push(window); + if (!Main.overview.visible) + this._dimWindow(window, true); + } else if (!shouldDim && window._dimmed) { + window._dimmed = false; + this._dimmedWindows = this._dimmedWindows.filter(function(win) { + return win !== window; + }); + if (!Main.overview.visible) + this._undimWindow(window, true); + } + } + + _dimWindow(window, animate) { + let actor = window.get_compositor_private(); + if (!actor) + return; + + let dimmer = getWindowDimmer(actor); + if (!dimmer) + return; + + dimmer.setDimmed(true, animate); + } + + _undimWindow(window, animate) { + let actor = window.get_compositor_private(); + if (!actor) + return; + + let dimmer = getWindowDimmer(actor); + if (!dimmer) + return; + + dimmer.setDimmed(false, animate); + } + + _mapWindow(cinnamonwm, actor) { + actor._windowType = actor.meta_window.get_window_type(); + actor._notifyWindowTypeSignalId = + actor.meta_window.connect('notify::window-type', () => { + let type = actor.meta_window.get_window_type(); + if (type === actor._windowType) + return; + if (type === Meta.WindowType.MODAL_DIALOG || + actor._windowType === Meta.WindowType.MODAL_DIALOG) { + let parent = actor.get_meta_window().get_transient_for(); + if (parent) + this._checkDimming(parent); + } + + actor._windowType = type; + }); + actor.meta_window.connect('unmanaged', window => { + let parent = window.get_transient_for(); + if (parent) + this._checkDimming(parent); + }); + + if (actor._windowType === Meta.WindowType.NORMAL) { + Main.soundManager.play('map'); + } + + if (actor.meta_window.is_attached_dialog()) { + this._checkDimming(actor.get_meta_window().get_transient_for()); + } + + if (!this._shouldAnimate(actor) || this.desktop_effects_map_type == "none") { + cinnamonwm.completed_map(actor); + return; + } + + this._mapping.add(actor); + + switch (this.desktop_effects_map_type) { + case "traditional": + { + actor.orig_opacity = actor.opacity; + actor.set_pivot_point(0.5, 0.5); + actor.x -= 1; + actor.scale_x = 0.94; + actor.scale_y = 0.94; + actor.opacity = 0; + actor.show(); + + actor.ease({ + opacity: actor.orig_opacity, + scale_x: 1, + scale_y: 1, + x: actor.x + 1, + duration: this.MAP_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._mapWindowDone(cinnamonwm, actor), + }); + + return; + } + case "move": + { + let [width, height] = actor.get_size(); + let [xDest, yDest] = actor.get_position(); + let [xSrc, ySrc] = global.get_pointer(); + + actor.set_position(xSrc, ySrc); + actor.set_scale(0, 0); + actor.show(); + + actor.ease({ + scale_x: 1.0, + scale_y: 1.0, + x: xDest, + y: yDest, + duration: this.MAP_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._mapWindowDone(cinnamonwm, actor), + }); + + return; + } + case "fly": + { + let ySrc = global.stage.get_height(); + let yDest = actor.y; + + actor.set_position(actor.x, ySrc); + + // The transition time set is the time if the animation starts/ends at the middle of the screen. + // Scale it proportional to the actual distance so that the speed of all animations will be constant. + let dist = Math.abs(ySrc - yDest); + let time = this.MAP_ANIMATION_TIME * (dist / ySrc * 2); + + actor.show(); + + actor.ease({ + y: yDest, + duration: time * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_IN_SINE, + onStopped: () => this._mapWindowDone(cinnamonwm, actor), + }); + + return; + } + default: + { + this._mapWindowDone(cinnamonwm, actor); + } + } + } + + _mapWindowDone(cinnamonwm, actor) { + if (this._mapping.delete(actor)) { + actor.remove_all_transitions() + actor.opacity = 255; + actor.set_pivot_point(0, 0); + actor.scale_y = 1; + actor.scale_x = 1; + cinnamonwm.completed_map(actor); + } + } + + _destroyWindow(cinnamonwm, actor) { + let window = actor.meta_window; + if (actor._notifyWindowTypeSignalId > 0) { + window.disconnect(actor._notifyWindowTypeSignalId); + actor._notifyWindowTypeSignalId = 0; + } + if (window._dimmed) { + this._dimmedWindows = + this._dimmedWindows.filter(win => win != window); + } + + if (actor.meta_window.window_type === Meta.WindowType.NORMAL) { + Main.soundManager.play('close'); + } + + if (window.is_attached_dialog()) + this._checkDimming(window.get_transient_for(), window); + + if (window.minimized) { + cinnamonwm.completed_destroy(actor); + return; + } + + let types = [Meta.WindowType.NORMAL, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG]; + + if (!this._shouldAnimate(actor, types) || this.desktop_effects_close_type === "none") { + cinnamonwm.completed_destroy(actor); + return; + } + + this._destroying.add(actor); + + switch (this.desktop_effects_close_type) { + case "fly": + { + let [xSrc, ySrc] = actor.get_position(); + + let workarea = actor.meta_window.get_work_area_current_monitor(); + let yDest = workarea.y + workarea.height; + // The transition time set is the time if the animation starts/ends at the middle of the screen. + // Scale it proportional to the actual distance so that the speed of all animations will be constant. + let dist = Math.abs(ySrc - yDest); + let time = this.DESTROY_ANIMATION_TIME * (dist / yDest * 2); + + actor.ease({ + y: yDest, + duration: time * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_IN_SINE, + onStopped: () => this._destroyWindowDone(cinnamonwm, actor), + }); + + return; + } + case "traditional": + { + switch (actor.meta_window.window_type) { + case Meta.WindowType.NORMAL: + case Meta.WindowType.MODAL_DIALOG: + case Meta.WindowType.DIALOG: + { + actor.set_pivot_point(0.5, 0.5); + + if (window.is_attached_dialog()) { + let parent = window.get_transient_for(); + actor._parentDestroyId = parent.connect('unmanaged', () => { + actor.remove_all_transitions(); + this._destroyWindowDone(cinnamonwm, actor); + }); + } + + actor.ease({ + opacity: 0, + scale_x: 0.88, + scale_y: 0.88, + duration: this.DESTROY_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => this._destroyWindowDone(cinnamonwm, actor), + }); + + return; + } + default: + { + this._destroyWindowDone(cinnamonwm, actor); + } + } + } + default: + { + this._destroyWindowDone(cinnamonwm, actor); + } + } + } + + _destroyWindowDone(cinnamonwm, actor) { + if (this._destroying.delete(actor)) { + const parent = actor.get_meta_window()?.get_transient_for(); + if (parent && actor._parentDestroyId) { + parent.disconnect(actor._parentDestroyId); + actor._parentDestroyId = 0; + } + cinnamonwm.completed_destroy(actor); + } + } + + _switchWorkspace(cinnamonwm, from, to, direction) { + if (!Main.animations_enabled || Main.modalCount) { + this.showWorkspaceOSD(); + cinnamonwm.completed_switch_workspace(); + return; + } + + Main.soundManager.play('switch'); + this.showWorkspaceOSD(); + + let windows = global.get_window_actors(); + + /* @direction is the direction that the "camera" moves, so the + * screen contents have to move one screen's worth in the + * opposite direction. + */ + let xDest = 0, yDest = 0; + let {display, screen_width, screen_height} = global; + let {focus_window} = display; + let grabOp = display.get_grab_op(); + + + if (direction === Meta.MotionDirection.UP || + direction === Meta.MotionDirection.UP_LEFT || + direction === Meta.MotionDirection.UP_RIGHT) + yDest = screen_height; + else if (direction === Meta.MotionDirection.DOWN || + direction === Meta.MotionDirection.DOWN_LEFT || + direction === Meta.MotionDirection.DOWN_RIGHT) + yDest = -screen_height; + + if (direction === Meta.MotionDirection.LEFT || + direction === Meta.MotionDirection.UP_LEFT || + direction === Meta.MotionDirection.DOWN_LEFT) + xDest = screen_width; + else if (direction === Meta.MotionDirection.RIGHT || + direction === Meta.MotionDirection.UP_RIGHT || + direction === Meta.MotionDirection.DOWN_RIGHT) + xDest = -screen_width; + + let from_windows = new Set(); + let to_windows = new Set(); + let kill_id = 0; + + let cleanup_window_effect = (window, hide=false) => { + window.remove_all_transitions(); + window.set_position(window.origX, window.origY); + window.origX = undefined; + window.origY = undefined; + + if (hide) { + window.hide(); + } + } + + let finish_switch_workspace = (actor) => + { + if (to_windows.delete(actor)) { + cleanup_window_effect(actor); + } + else + if (from_windows.delete(actor)) { + cleanup_window_effect(actor, true); + }; + + if (to_windows.size === 0 && from_windows.size === 0) { + if (kill_id > 0) { + this._cinnamonwm.disconnect(kill_id); + kill_id = 0; + + cinnamonwm.completed_switch_workspace(); + } + } + }; + + for (let i = 0; i < windows.length; i++) { + let window = windows[i]; + let {meta_window} = window; + + if (!meta_window.showing_on_its_workspace()) + continue; + + // Muffin 5.2 window.showing_on_its_workspace() no longer + // ends up filtering the desktop window (If I re-add it, it + // breaks things elsewhere that rely on the new behavior). + if (meta_window.get_window_type() === Meta.WindowType.DESKTOP || + meta_window.get_window_type() === Meta.WindowType.OVERRIDE_OTHER) { + continue; + } + + if (meta_window.is_on_all_workspaces()) { + continue; + } + + if ((meta_window === this._movingWindow) || + ((grabOp === Meta.GrabOp.MOVING || + grabOp === Meta.GrabOp.KEYBOARD_MOVING) + && meta_window === focus_window)) { + /* We are moving this window to the other workspace. In fact, + * it is already on the other workspace, so it is hidden. We + * force it to show and then don't animate it, so it stays + * there while other windows move. */ + window.show_all(); + this._movingWindow = undefined; + } else if (window.get_workspace() === from) { + if (window.origX == undefined) { + window.origX = window.x; + window.origY = window.y; + } + from_windows.add(window); + window.ease({ + x: window.origX + xDest, + y: window.origY + yDest, + duration: this.WORKSPACE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => finish_switch_workspace(window) + }); + } else if (window.get_workspace() === to) { + if (window.origX == undefined) { + window.origX = window.x; + window.origY = window.y; + window.set_position(window.origX - xDest, window.origY - yDest); + } + to_windows.add(window); + window.show_all(); + window.ease({ + x: window.origX, + y: window.origY, + duration: this.WORKSPACE_ANIMATION_TIME * this.window_effect_multiplier, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => finish_switch_workspace(window) + }); + } + } + + if (to_windows.size === 0 && from_windows.size === 0) { + this._cinnamonwm.completed_switch_workspace(); + return; + } + + kill_id = this._cinnamonwm.connect('kill-switch-workspace', cinnamonwm => { + let iter = to_windows.forEach((actor) => { + cleanup_window_effect(actor); + }); + iter = from_windows.forEach((actor) => { + cleanup_window_effect(actor, true); + }); + + to_windows.clear(); + from_windows.clear(); + + if (kill_id > 0) { + this._cinnamonwm.disconnect(kill_id); + kill_id = 0; + } + + cinnamonwm.completed_switch_workspace(); + }); + } + + _showTilePreview(cinnamonwm, window, tileRect, monitorIndex) { + if (!this._tilePreview) + this._tilePreview = new TilePreview(); + this._tilePreview.show(window, tileRect, monitorIndex, Main.animations_enabled, this.TILE_PREVIEW_ANIMATION_TIME * this.window_effect_multiplier); + } + + _hideTilePreview(cinnamonwm) { + if (!this._tilePreview) + return; + this._tilePreview.hide(); + } + + showWorkspaceOSD() { + if (global.settings.get_boolean('workspace-osd-visible')) { + let currentWorkspaceIndex = global.workspace_manager.get_active_workspace_index(); + if (this.wm_settings.get_boolean('workspaces-only-on-primary')) { + this._showWorkspaceOSDForMonitor(Main.layoutManager.primaryMonitor.index, currentWorkspaceIndex); + } else { + for (let i = 0; i < Main.layoutManager.monitors.length; i++) { + this._showWorkspaceOSDForMonitor(i, currentWorkspaceIndex); + } + } + } + } + + _showWorkspaceOSDForMonitor(index, currentWorkspaceIndex) { + if (this._workspaceOsds[index] === undefined) { + let osd = new WorkspaceOsd.WorkspaceOsd(index); + this._workspaceOsds[index] = osd; + osd.connect('destroy', () => { + this._workspaceOsds[index] = undefined; + }); + } + + let text = Main.getWorkspaceName(currentWorkspaceIndex); + this._workspaceOsds[index].display(currentWorkspaceIndex, text); + } + + _showWindowMenu(cinnamonwm, window, menu, rect) { + this._windowMenuManager.showWindowMenuForWindow(window, menu, rect); + } + + _createAppSwitcher(binding) { + if (AppSwitcher.getWindowsForBinding(binding).length === 0) return; + + switch (global.settings.get_string('alttab-switcher-style')) { + case 'coverflow': + new CoverflowSwitcher(binding); + break; + case 'timeline': + new TimelineSwitcher(binding); + break; + default: + new ClassicSwitcher(binding); + } + } + + _startAppSwitcher(display, window, binding) { + this._createAppSwitcher(binding); + } + + _shiftWindowToWorkspace(window, direction) { + if (!window || window.window_type === Meta.WindowType.DESKTOP) { + return; + } + this._movingWindow = window; + let workspace = global.workspace_manager.get_active_workspace().get_neighbor(direction); + if (workspace != global.workspace_manager.get_active_workspace()) { + window.change_workspace(workspace); + workspace.activate_with_focus(window, global.get_current_time()); + } + } + + _moveWindowToWorkspaceLeft(display, window, binding) { + this._shiftWindowToWorkspace(window, Meta.MotionDirection.LEFT); + } + + _moveWindowToWorkspaceRight(display, window, binding) { + this._shiftWindowToWorkspace(window, Meta.MotionDirection.RIGHT); + } + + moveToWorkspace(workspace, direction_hint) { + let active = global.workspace_manager.get_active_workspace(); + // if (workspace != active) { + // if (direction_hint) + // workspace.activate_with_direction_hint(direction_hint, global.get_current_time()); + // else + workspace.activate(global.get_current_time()); + // } + } + + _showWorkspaceSwitcher(display, window, binding) { + let bindingName = binding.get_name(); + + if (bindingName === 'toggle-workspace-selection') { + if (Main.overview.visible || Main.expo.visible) { + Main.overview.hide(); + Main.expo.hide(); + } else { + Main.expo.toggle(); + } + return; + } + if (bindingName === 'toggle-window-selection') { + if (Main.overview.visible || Main.expo.visible) { + Main.overview.hide(); + Main.expo.hide(); + } else { + Main.overview.toggle(); + } + return; + } + + if (global.workspace_manager.n_workspaces === 1) + return; + + if (bindingName === 'switch-to-workspace-left') { + this.actionMoveWorkspaceLeft(); + } else if (bindingName === 'switch-to-workspace-right') { + this.actionMoveWorkspaceRight(); + } else if (bindingName === 'switch-to-workspace-up') { + this.actionMoveWorkspaceUp(); + } else if (bindingName === 'switch-to-workspace-down') { + this.actionMoveWorkspaceDown(); + } + } + + actionMoveWorkspaceLeft() { + let active = global.workspace_manager.get_active_workspace(); + let neighbor = active.get_neighbor(Meta.MotionDirection.LEFT) + if (active != neighbor) { + this.moveToWorkspace(neighbor, Meta.MotionDirection.LEFT); + } + } + + actionMoveWorkspaceRight() { + let active = global.workspace_manager.get_active_workspace(); + let neighbor = active.get_neighbor(Meta.MotionDirection.RIGHT) + if (active != neighbor) { + this.moveToWorkspace(neighbor, Meta.MotionDirection.RIGHT); + } + } + + actionMoveWorkspaceUp() { + global.workspace_manager.get_active_workspace().get_neighbor(Meta.MotionDirection.UP).activate(global.get_current_time()); + } + + actionMoveWorkspaceDown() { + global.workspace_manager.get_active_workspace().get_neighbor(Meta.MotionDirection.DOWN).activate(global.get_current_time()); + } + + actionFlipWorkspaceLeft() { + let active = global.workspace_manager.get_active_workspace(); + let neighbor = active.get_neighbor(Meta.MotionDirection.LEFT); + if (active != neighbor) { + neighbor.activate(global.get_current_time()); + let [x, y, mods] = global.get_pointer(); + global.set_pointer(global.screen_width - 10, y); + } + } + + actionFlipWorkspaceRight() { + let active = global.workspace_manager.get_active_workspace(); + let neighbor = active.get_neighbor(Meta.MotionDirection.RIGHT); + if (active != neighbor) { + neighbor.activate(global.get_current_time()); + let [x, y, mods] = global.get_pointer(); + global.set_pointer(10, y); + } + } + + _showResizePopup(display, show, rect, displayW, displayH) { + if (show) { + if (!this._resizePopup) + this._resizePopup = new ResizePopup(); + + this._resizePopup.set(rect, displayW, displayH); + } else { + if (!this._resizePopup) + return; + + this._resizePopup.destroy(); + this._resizePopup = null; + } + } + + _createCloseDialog(cinnamonwm, window) { + return new CloseDialog.CloseDialog(window); + } + + _confirmDisplayChange() { + let dialog = new DisplayChangeDialog(this._cinnamonwm); + dialog.open(); + } +};