[gnome-shell/eos3.8: 14/255] appActivation: Added Endless-specific AppActivation class



commit b3a8c3c07faf0f89c68d72eaefe2740d484f03a3
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Tue Apr 25 15:41:01 2017 +0000

    appActivation: Added Endless-specific AppActivation class
    
    This implements Endless-specific quirks for apps (e.g. launching them
    maximized) and provide integration with eos-speedwagon, to handle
    splash screens.
    
     - 2019-09-19: Move D-Bus interface to com.endlessm.Speedwagon.xml
     - 2020-03-13: Code style updates
    
    https://phabricator.endlessm.com/T20114
    https://phabricator.endlessm.com/T20699
    https://phabricator.endlessm.com/T20725

 data/dbus-interfaces/com.endlessm.Speedwagon.xml |  10 +
 data/gnome-shell-dbus-interfaces.gresource.xml   |   1 +
 data/org.gnome.shell.gschema.xml.in              |  16 +
 js/js-resources.gresource.xml                    |   1 +
 js/ui/appActivation.js                           | 378 +++++++++++++++++++++++
 js/ui/main.js                                    |   5 +
 6 files changed, 411 insertions(+)
---
diff --git a/data/dbus-interfaces/com.endlessm.Speedwagon.xml 
b/data/dbus-interfaces/com.endlessm.Speedwagon.xml
new file mode 100644
index 0000000000..fea3fdeb3d
--- /dev/null
+++ b/data/dbus-interfaces/com.endlessm.Speedwagon.xml
@@ -0,0 +1,10 @@
+<node>
+  <interface name="com.endlessm.Speedwagon">
+    <method name="ShowSplash">
+      <arg type="s" direction="in" name="desktopFile" />
+    </method>
+    <method name="HideSplash">
+      <arg type="s" direction="in" name="desktopFile" />
+    </method>
+  </interface>
+</node>
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index db3ef4ac22..22140c3da3 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/shell/dbus-interfaces">
+    <file preprocess="xml-stripblanks">com.endlessm.Speedwagon.xml</file>
     <file preprocess="xml-stripblanks">net.hadess.SensorProxy.xml</file>
     <file preprocess="xml-stripblanks">net.hadess.SwitcherooControl.xml</file>
     <file preprocess="xml-stripblanks">org.freedesktop.Application.xml</file>
diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index 3d2d0cb200..82e94f9e56 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -109,6 +109,22 @@
         the shell.
       </description>
     </key>
+
+    <!-- Endless-specific keys beyond this point -->
+
+    <key name="no-default-maximize" type="b">
+      <default>false</default>
+      <summary>
+        Prevent apps from being automatically maximized on launch
+      </summary>
+      <description>
+        Makes window management more like standard Gnome.
+        Hides application splash screens, prevents applications from being forced
+        to open maximized, and does not automatically switch to the app selector
+        when a window is minimized.
+      </description>
+    </key>
+
     <child name="keybindings" schema="org.gnome.shell.keybindings"/>
   </schema>
 
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index b353ec8e3b..3137c774cc 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -141,6 +141,7 @@
 
     <!-- Endless-specific files beyond this point -->
 
+    <file>ui/appActivation.js</file>
     <file>ui/forceAppExitDialog.js</file>
   </gresource>
 </gresources>
diff --git a/js/ui/appActivation.js b/js/ui/appActivation.js
new file mode 100644
index 0000000000..76888a31e4
--- /dev/null
+++ b/js/ui/appActivation.js
@@ -0,0 +1,378 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+/* exported DesktopAppClient */
+
+const { Clutter, Gio, GLib, Meta, Shell, St } = imports.gi;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const Main = imports.ui.main;
+const Util = imports.misc.util;
+
+const SPLASH_SCREEN_TIMEOUT = 700; // ms
+
+// By default, maximized windows are 75% of the workarea
+// of the monitor they're on when unmaximized.
+const DEFAULT_MAXIMIZED_WINDOW_SIZE = 0.75;
+const LAUNCH_MAXIMIZED_DESKTOP_KEY = 'X-Endless-LaunchMaximized';
+
+// GSettings that controls whether apps start maximized (default).
+const NO_DEFAULT_MAXIMIZE_KEY = 'no-default-maximize';
+
+// Determine if a splash screen should be shown for the provided
+// ShellApp and other global settings
+function _shouldShowSplash(app) {
+    let info = app.get_app_info();
+
+    // Don't show the splash screen if the app is already running.
+    if (app.state === Shell.AppState.RUNNING)
+        return false;
+
+    if (!(info && info.has_key(LAUNCH_MAXIMIZED_DESKTOP_KEY) &&
+          info.get_boolean(LAUNCH_MAXIMIZED_DESKTOP_KEY)))
+        return false;
+
+    // Don't show splash screen if default maximize is disabled
+    if (global.settings.get_boolean(NO_DEFAULT_MAXIMIZE_KEY))
+        return false;
+
+    // Don't show splash screen if this is a link and the browser is
+    // running. We can't rely on any signal being emitted in that
+    // case, as links open in browser tabs.
+    if (app.get_id().indexOf('eos-link-') !== -1 &&
+        Util.getBrowserApp().state !== Shell.AppState.STOPPED)
+        return false;
+
+    return true;
+}
+
+var AppActivationContext = class {
+    constructor(app) {
+        this._app = app;
+
+        this._splash = null;
+
+        this._appStateId = 0;
+        this._timeoutId = 0;
+
+        this._appActivationTime = 0;
+
+        this._hiddenWindows = [];
+
+        this._appSystem = Shell.AppSystem.get_default();
+
+        this._tracker = Shell.WindowTracker.get_default();
+        this._tracker.connect('notify::focus-app', this._onFocusAppChanged.bind(this));
+    }
+
+    _doActivate(showSplash, timestamp) {
+        if (!timestamp)
+            timestamp = global.get_current_time();
+
+        try {
+            this._app.activate_full(-1, timestamp);
+        } catch (e) {
+            logError(e, `error while activating: ${this._app.get_id()}`);
+            return;
+        }
+
+        if (showSplash)
+            this.showSplash();
+    }
+
+    activate(event, timestamp) {
+        let button = event ? event.get_button() : 0;
+        let modifiers = event ? event.get_state() : 0;
+        let isMiddleButton = button && button === Clutter.BUTTON_MIDDLE;
+        let isCtrlPressed = (modifiers & Clutter.ModifierType.CONTROL_MASK) !== 0;
+        let openNewWindow = this._app.can_open_new_window() &&
+                            this._app.state === Shell.AppState.RUNNING &&
+                            (isCtrlPressed || isMiddleButton);
+
+        if (this._app.state === Shell.AppState.RUNNING) {
+            if (openNewWindow)
+                this._app.open_new_window(-1);
+            else
+                this._doActivate(false, timestamp);
+        } else {
+            this._doActivate(true, timestamp);
+        }
+
+        Main.overview.hide();
+    }
+
+    showSplash() {
+        if (!_shouldShowSplash(this._app))
+            return;
+
+        // Prevent windows from being shown when the overview is hidden so it does
+        // not affect the speedwagon's animation
+        if (Main.overview.visible)
+            this._hideWindows();
+
+        this._splash = new SpeedwagonSplash(this._app);
+        this._splash.show();
+
+        // Scale the timeout by the slow down factor, because otherwise
+        // we could be trying to destroy the splash screen window before
+        // the map animation has finished.
+        // This buffer time ensures that the user can never destroy the
+        // splash before the animation is completed.
+        this._timeoutId = GLib.timeout_add(
+            GLib.PRIORITY_DEFAULT,
+            SPLASH_SCREEN_TIMEOUT * St.Settings.get().slow_down_factor,
+            this._splashTimeout.bind(this));
+
+        // We can't fully trust windows-changed to be emitted with the
+        // same ShellApp we called activate() on, as WMClass matching might
+        // fail. For this reason, just pick to the first application that
+        // will flip its state to running
+        this._appStateId =
+            this._appSystem.connect('app-state-changed', this._onAppStateChanged.bind(this));
+        this._appActivationTime = GLib.get_monotonic_time();
+    }
+
+    _clearSplash() {
+        this._resetWindowsVisibility();
+
+        if (this._splash) {
+            this._splash.rampOut();
+            this._splash = null;
+        }
+    }
+
+    _maybeClearSplash() {
+        // Clear the splash only when we've waited at least 700ms,
+        // and when the app has transitioned to the running state...
+        if (this._appStateId === 0 && this._timeoutId === 0)
+            this._clearSplash();
+    }
+
+    _splashTimeout() {
+        this._timeoutId = 0;
+        this._maybeClearSplash();
+
+        return false;
+    }
+
+    _resetWindowsVisibility() {
+        for (let actor of this._hiddenWindows)
+            actor.visible = true;
+
+        this._hiddenWindows = [];
+    }
+
+    _hideWindows() {
+        let windows = global.get_window_actors();
+
+        for (let actor of windows) {
+            if (!actor.visible)
+                continue;
+
+            this._hiddenWindows.push(actor);
+            actor.visible = false;
+        }
+    }
+
+    _recordLaunchTime() {
+        let activationTime = this._appActivationTime;
+        this._appActivationTime = 0;
+
+        if (activationTime === 0)
+            return;
+
+        if (!GLib.getenv('SHELL_DEBUG_LAUNCH_TIME'))
+            return;
+
+        let currentTime = GLib.get_monotonic_time();
+        let elapsedTime = currentTime - activationTime;
+
+        log(`Application ${this._app.get_name()} took ${elapsedTime / 1000000} seconds to launch`);
+    }
+
+    _isBogusWindow(app) {
+        let launchedAppId = this._app.get_id();
+        let appId = app.get_id();
+
+        // When the application IDs match, the window is not bogus
+        if (appId === launchedAppId)
+            return false;
+
+        // Special case for Libreoffice splash screen; we will get a non-matching
+        // app with 'Soffice' as its name when the recovery screen comes up,
+        // so special case that too
+        if (launchedAppId.indexOf('libreoffice') !== -1 &&
+            app.get_name() !== 'Soffice')
+            return true;
+
+        return false;
+    }
+
+    _onAppStateChanged(appSystem, app) {
+        if (!(app.state === Shell.AppState.RUNNING ||
+              app.state === Shell.AppState.STOPPED))
+            return;
+
+        if (this._isBogusWindow(app))
+            return;
+
+        appSystem.disconnect(this._appStateId);
+        this._appStateId = 0;
+
+        if (app.state === Shell.AppState.STOPPED) {
+            this._clearSplash();
+        } else {
+            this._recordLaunchTime();
+            this._maybeClearSplash();
+        }
+    }
+
+    _onFocusAppChanged(tracker) {
+        if (this._splash === null)
+            return;
+
+        let app = tracker.focus_app;
+        if (!app || app.get_id() === this._app.get_id())
+            return;
+
+        // The focused application changed and it is not the one that we are showing
+        // the splash for, so clear the splash after it times out (because we don't
+        // want to risk hiding too early)
+        this._appSystem.disconnect(this._appStateId);
+        this._appStateId = 0;
+        this._maybeClearSplash();
+    }
+};
+
+const SpeedwagonIface = loadInterfaceXML('com.endlessm.Speedwagon');
+const SpeedwagonProxy = Gio.DBusProxy.makeProxyWrapper(SpeedwagonIface);
+
+var SpeedwagonSplash =  class {
+    constructor(app) {
+        this._app = app;
+
+        this._proxy = new SpeedwagonProxy(
+            Gio.DBus.session,
+            'com.endlessm.Speedwagon',
+            '/com/endlessm/Speedwagon');
+    }
+
+    show() {
+        this._proxy.ShowSplashRemote(this._app.get_id());
+    }
+
+    rampOut() {
+        this._proxy.HideSplashRemote(this._app.get_id());
+    }
+};
+
+var DesktopAppClient = class DesktopAppClient {
+    constructor() {
+        this._lastDesktopApp = null;
+        this._subscription =
+            Gio.DBus.session.signal_subscribe(
+                null,
+                'org.gtk.gio.DesktopAppInfo',
+                'Launched',
+                '/org/gtk/gio/DesktopAppInfo',
+                null, 0,
+                this._onLaunched.bind(this));
+
+        global.display.connect('window-created', this._windowCreated.bind(this));
+    }
+
+    _onLaunched(connection, senderName, objectPath, interfaceName, signalName, parameters) {
+        let [desktopIdByteString] = parameters.deep_unpack();
+
+        let desktopIdPath = imports.byteArray.toString(desktopIdByteString);
+        let desktopIdFile = Gio.File.new_for_path(desktopIdPath);
+        let desktopDirs = GLib.get_system_data_dirs();
+        desktopDirs.push(GLib.get_user_data_dir());
+
+        let desktopId = GLib.path_get_basename(desktopIdPath);
+
+        // Convert subdirectories to app ID prefixes like GIO does
+        desktopDirs.some(desktopDir => {
+            let path = GLib.build_filenamev([desktopDir, 'applications']);
+            let file = Gio.File.new_for_path(path);
+
+            if (desktopIdFile.has_prefix(file)) {
+                let relPath = file.get_relative_path(desktopIdFile);
+                desktopId = relPath.replace(/\//g, '-');
+                return true;
+            }
+
+            return false;
+        });
+
+        this._lastDesktopApp = Shell.AppSystem.get_default().lookup_app(desktopId);
+
+        // Show the splash page if we didn't launch this ourselves, since in that case
+        // we already explicitly control when the splash screen should be used
+        let launchedByShell = senderName === Gio.DBus.session.get_unique_name();
+        let showSplash =
+            (this._lastDesktopApp !== null) &&
+            (this._lastDesktopApp.state !== Shell.AppState.RUNNING) &&
+            this._lastDesktopApp.get_app_info().should_show() &&
+            !launchedByShell;
+
+        if (showSplash) {
+            let context = new AppActivationContext(this._lastDesktopApp);
+            context.showSplash();
+        }
+    }
+
+    _windowCreated(metaDisplay, metaWindow) {
+        // Ignore splash screens, which will already be maximized.
+        if (Shell.WindowTracker.is_speedwagon_window(metaWindow))
+            return;
+
+        // Don't maximize if key to disable default maximize is set
+        if (global.settings.get_boolean(NO_DEFAULT_MAXIMIZE_KEY))
+            return;
+
+        // Don't maximize windows in non-overview sessions (e.g. initial setup)
+        if (!Main.sessionMode.hasOverview)
+            return;
+
+        // Skip unknown applications
+        let tracker = Shell.WindowTracker.get_default();
+        let app = tracker.get_window_app(metaWindow);
+        if (!app)
+            return;
+
+        // Skip applications we are not aware of
+        if (!this._lastDesktopApp)
+            return;
+
+        // Don't maximize if the launch maximized key is false
+        let info = app.get_app_info();
+        if (info && info.has_key(LAUNCH_MAXIMIZED_DESKTOP_KEY) &&
+            !info.get_boolean(LAUNCH_MAXIMIZED_DESKTOP_KEY))
+            return;
+
+        // Skip if the window does not belong to the launched app, but
+        // special case eos-link launchers if we detect a browser window
+        if (app !== this._lastDesktopApp &&
+            !(this._lastDesktopApp.get_id().indexOf('eos-link-') !== -1 &&
+              app === Util.getBrowserApp()))
+            return;
+
+        this._lastDesktopApp = null;
+
+        if (metaWindow.is_skip_taskbar() || !metaWindow.resizeable)
+            return;
+
+        // Position the window so it's where we want it to be if the user
+        // unmaximizes the window.
+        let workArea = Main.layoutManager.getWorkAreaForMonitor(metaWindow.get_monitor());
+        let width = workArea.width * DEFAULT_MAXIMIZED_WINDOW_SIZE;
+        let height = workArea.height * DEFAULT_MAXIMIZED_WINDOW_SIZE;
+        let x = workArea.x + (workArea.width - width) / 2;
+        let y = workArea.y + (workArea.height - height) / 2;
+        metaWindow.move_resize_frame(false, x, y, width, height);
+
+        metaWindow.maximize(Meta.MaximizeFlags.HORIZONTAL |
+                            Meta.MaximizeFlags.VERTICAL);
+    }
+};
diff --git a/js/ui/main.js b/js/ui/main.js
index ebf9b333a8..fbfc8a058d 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -11,6 +11,7 @@
 const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
 
 const AccessDialog = imports.ui.accessDialog;
+const AppActivation = imports.ui.appActivation;
 const AudioDeviceSelection = imports.ui.audioDeviceSelection;
 const Components = imports.ui.components;
 const CtrlAltTab = imports.ui.ctrlAltTab;
@@ -92,6 +93,7 @@ let _cssStylesheet = null;
 let _a11ySettings = null;
 let _themeResource = null;
 let _oskResource = null;
+let _desktopAppClient = null;
 
 Gio._promisify(Gio._LocalFilePrototype, 'delete_async', 'delete_finish');
 Gio._promisify(Gio._LocalFilePrototype, 'touch_async', 'touch_finish');
@@ -206,6 +208,9 @@ function _initializeUI() {
 
     introspectService = new Introspect.IntrospectService();
 
+    // The DesktopAppClient needs to be initialized before the layout manager.
+    _desktopAppClient = new AppActivation.DesktopAppClient();
+
     layoutManager.init();
     overview.init();
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]