mirror of
https://github.com/elementary/gala.git
synced 2024-11-26 00:04:27 +03:00
App: Implement Applications Management (#1241)
This allows us to completely replace libbamf in some specific cases (like system shell elements) Co-authored-by: Danielle Foré <danielle@elementary.io>
This commit is contained in:
parent
80db0620dc
commit
7c540a2a06
179
lib/App.vala
Normal file
179
lib/App.vala
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2021 elementary, Inc. <https://elementary.io>
|
||||
* Copyright 2021 Corentin Noël <tintou@noel.tf>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
public enum Gala.AppState {
|
||||
STOPPED,
|
||||
STARTING,
|
||||
RUNNING
|
||||
}
|
||||
|
||||
public class Gala.App : GLib.Object {
|
||||
public string id {
|
||||
get {
|
||||
if (app_info != null) {
|
||||
return app_info.get_id ();
|
||||
} else {
|
||||
return window_id_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GLib.DesktopAppInfo? app_info { get; construct; }
|
||||
|
||||
public GLib.Icon icon {
|
||||
get {
|
||||
if (app_info != null) {
|
||||
return app_info.get_icon ();
|
||||
}
|
||||
|
||||
if (fallback_icon == null) {
|
||||
fallback_icon = new GLib.ThemedIcon ("application-x-executable");
|
||||
}
|
||||
|
||||
return fallback_icon;
|
||||
}
|
||||
}
|
||||
|
||||
public string name {
|
||||
get {
|
||||
if (app_info != null) {
|
||||
return app_info.get_name ();
|
||||
} else {
|
||||
unowned string? name = null;
|
||||
var window = get_backing_window ();
|
||||
if (window != null) {
|
||||
name = window.get_wm_class ();
|
||||
}
|
||||
|
||||
return name ?? C_("program", "Unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? description {
|
||||
get {
|
||||
if (app_info != null) {
|
||||
return app_info.get_description ();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Gala.AppState state { get; private set; default = AppState.STOPPED; }
|
||||
|
||||
private GLib.SList<Meta.Window> windows = new GLib.SList<Meta.Window> ();
|
||||
private uint interesting_windows = 0;
|
||||
private string? window_id_string = null;
|
||||
private GLib.Icon? fallback_icon = null;
|
||||
private int started_on_workspace;
|
||||
|
||||
public static unowned App? new_from_startup_sequence (Meta.StartupSequence sequence) {
|
||||
unowned string? app_id = sequence.get_application_id ();
|
||||
if (app_id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var basename = GLib.Path.get_basename (app_id);
|
||||
unowned var appsys = Gala.AppSystem.get_default ();
|
||||
return appsys.lookup_app (basename);
|
||||
}
|
||||
|
||||
public App (GLib.DesktopAppInfo info) {
|
||||
Object (app_info: info);
|
||||
}
|
||||
|
||||
public App.for_window (Meta.Window window) {
|
||||
window_id_string = "window:%u".printf (window.get_stable_sequence ());
|
||||
add_window (window);
|
||||
}
|
||||
|
||||
public unowned GLib.SList<Meta.Window> get_windows () {
|
||||
return windows;
|
||||
}
|
||||
|
||||
public void add_window (Meta.Window window) {
|
||||
if (windows.find (window) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
windows.prepend (window);
|
||||
if (!window.is_skip_taskbar ()) {
|
||||
interesting_windows++;
|
||||
}
|
||||
|
||||
sync_running_state ();
|
||||
}
|
||||
|
||||
|
||||
public void remove_window (Meta.Window window) {
|
||||
if (windows.find (window) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.is_skip_taskbar ()) {
|
||||
interesting_windows--;
|
||||
}
|
||||
|
||||
windows.remove (window);
|
||||
sync_running_state ();
|
||||
}
|
||||
|
||||
private void sync_running_state () {
|
||||
if (state != Gala.AppState.STARTING) {
|
||||
unowned var app_sys = Gala.AppSystem.get_default ();
|
||||
if (interesting_windows == 0) {
|
||||
state = Gala.AppState.STOPPED;
|
||||
app_sys.notify_app_state_changed (this);
|
||||
} else {
|
||||
state = Gala.AppState.RUNNING;
|
||||
app_sys.notify_app_state_changed (this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handle_startup_sequence (Meta.StartupSequence sequence) {
|
||||
bool starting = !sequence.get_completed ();
|
||||
|
||||
if (starting && state == AppState.STOPPED) {
|
||||
state = AppState.STARTING;
|
||||
}
|
||||
|
||||
if (starting) {
|
||||
started_on_workspace = sequence.workspace;
|
||||
} else if (interesting_windows > 0) {
|
||||
state = AppState.RUNNING;
|
||||
} else {
|
||||
state = AppState.STOPPED;
|
||||
}
|
||||
|
||||
unowned var app_sys = Gala.AppSystem.get_default ();
|
||||
app_sys.notify_app_state_changed (this);
|
||||
}
|
||||
|
||||
private Meta.Window? get_backing_window () requires (app_info == null) {
|
||||
return windows.data;
|
||||
}
|
||||
|
||||
public GLib.SList<Posix.pid_t?> get_pids () {
|
||||
var results = new GLib.SList<Posix.pid_t?> ();
|
||||
foreach (unowned var window in windows) {
|
||||
var pid = window.get_pid ();
|
||||
if (pid < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Note in the (by far) common case, app will only have one pid, so
|
||||
* we'll hit the first element, so don't worry about O(N^2) here.
|
||||
*/
|
||||
if (results.find (pid) == null) {
|
||||
results.prepend (pid);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
@ -20,16 +20,16 @@ public class Gala.AppCache : GLib.Object {
|
||||
|
||||
private const int DEFAULT_TIMEOUT_SECONDS = 3;
|
||||
|
||||
private Gee.HashMap<string, string> startup_wm_class_to_id;
|
||||
private Gee.HashMap<string, GLib.DesktopAppInfo> id_to_app;
|
||||
private GLib.HashTable<unowned string, unowned string> startup_wm_class_to_id;
|
||||
private GLib.HashTable<unowned string, GLib.DesktopAppInfo> id_to_app;
|
||||
|
||||
private GLib.AppInfoMonitor app_info_monitor;
|
||||
|
||||
private uint queued_update_id = 0;
|
||||
|
||||
construct {
|
||||
startup_wm_class_to_id = new Gee.HashMap<string, string> ();
|
||||
id_to_app = new Gee.HashMap<string, GLib.DesktopAppInfo> ();
|
||||
startup_wm_class_to_id = new GLib.HashTable<unowned string, unowned string> (str_hash, str_equal);
|
||||
id_to_app = new GLib.HashTable<unowned string, GLib.DesktopAppInfo> (str_hash, str_equal);
|
||||
|
||||
app_info_monitor = GLib.AppInfoMonitor.@get ();
|
||||
app_info_monitor.changed.connect (queue_cache_update);
|
||||
@ -59,8 +59,8 @@ public class Gala.AppCache : GLib.Object {
|
||||
|
||||
new Thread<void> ("rebuild_cache", () => {
|
||||
lock (startup_wm_class_to_id) {
|
||||
startup_wm_class_to_id.clear ();
|
||||
id_to_app.clear ();
|
||||
startup_wm_class_to_id.remove_all ();
|
||||
id_to_app.remove_all ();
|
||||
|
||||
var app_infos = GLib.AppInfo.get_all ();
|
||||
|
||||
@ -74,7 +74,7 @@ public class Gala.AppCache : GLib.Object {
|
||||
continue;
|
||||
}
|
||||
|
||||
var old_id = startup_wm_class_to_id[startup_wm_class];
|
||||
unowned var old_id = startup_wm_class_to_id[startup_wm_class];
|
||||
if (old_id == null || id == startup_wm_class) {
|
||||
startup_wm_class_to_id[startup_wm_class] = id;
|
||||
}
|
||||
@ -87,7 +87,7 @@ public class Gala.AppCache : GLib.Object {
|
||||
yield;
|
||||
}
|
||||
|
||||
public GLib.DesktopAppInfo? lookup_id (string? id) {
|
||||
public unowned GLib.DesktopAppInfo? lookup_id (string? id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
@ -100,7 +100,7 @@ public class Gala.AppCache : GLib.Object {
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = startup_wm_class_to_id[wm_class];
|
||||
unowned var id = startup_wm_class_to_id[wm_class];
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
111
lib/AppSystem.vala
Normal file
111
lib/AppSystem.vala
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2021 elementary, Inc. <https://elementary.io>
|
||||
* Copyright 2021 Corentin Noël <tintou@noel.tf>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
public class Gala.AppSystem : GLib.Object {
|
||||
private static GLib.Once<AppSystem> instance;
|
||||
public static unowned AppSystem get_default () {
|
||||
return instance.once (() => new AppSystem ());
|
||||
}
|
||||
|
||||
private GLib.HashTable<Gala.App, unowned Gala.App> running_apps;
|
||||
private GLib.HashTable<unowned string, Gala.App> id_to_app;
|
||||
private GLib.HashTable<string, string> startup_wm_class_to_id;
|
||||
private Gala.AppCache app_cache;
|
||||
|
||||
construct {
|
||||
id_to_app = new GLib.HashTable<unowned string, Gala.App> (str_hash, str_equal);
|
||||
startup_wm_class_to_id = new GLib.HashTable<string, string> (str_hash, str_equal);
|
||||
running_apps = new GLib.HashTable<Gala.App, unowned Gala.App> (null, null);
|
||||
app_cache = new AppCache ();
|
||||
}
|
||||
|
||||
public unowned Gala.App? lookup_app (string id) {
|
||||
unowned Gala.App? app = id_to_app.lookup (id);
|
||||
if (app != null) {
|
||||
return app;
|
||||
}
|
||||
|
||||
GLib.DesktopAppInfo? info = app_cache.lookup_id (id);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var owned_app = new Gala.App (info);
|
||||
app = owned_app;
|
||||
id_to_app.insert (owned_app.id, (owned) owned_app);
|
||||
return app;
|
||||
}
|
||||
|
||||
public unowned Gala.App? lookup_startup_wmclass (string wmclass) {
|
||||
GLib.DesktopAppInfo? info = app_cache.lookup_startup_wmclass (wmclass);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return lookup_app (info.get_id ());
|
||||
}
|
||||
|
||||
private unowned Gala.App? lookup_heuristic_basename (string name) {
|
||||
/* Vendor prefixes are something that can be preprended to a .desktop
|
||||
* file name.
|
||||
*/
|
||||
const string[] VENDOR_PREFIXES = {
|
||||
"gnome-",
|
||||
"fedora-",
|
||||
"mozilla-",
|
||||
"debian-",
|
||||
};
|
||||
|
||||
unowned Gala.App? result = lookup_app (name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (unowned string prefix in VENDOR_PREFIXES) {
|
||||
result = lookup_app (prefix.concat (name));
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public unowned Gala.App? lookup_desktop_wmclass (string wmclass) {
|
||||
/* First try without changing the case (this handles
|
||||
org.example.Foo.Bar.desktop applications)
|
||||
|
||||
Note that is slightly wrong in that Gtk+ would set
|
||||
the WM_CLASS to Org.example.Foo.Bar, but it also
|
||||
sets the instance part to org.example.Foo.Bar, so we're ok
|
||||
*/
|
||||
var desktop_file = wmclass.concat (".desktop");
|
||||
unowned Gala.App? app = lookup_heuristic_basename (desktop_file);
|
||||
if (app != null) {
|
||||
return app;
|
||||
}
|
||||
|
||||
/* This handles "Fedora Eclipse", probably others.
|
||||
* Note _strdelimit is modify-in-place. */
|
||||
desktop_file._delimit (" ", '-');
|
||||
|
||||
desktop_file = desktop_file.ascii_down ().concat (".desktop");
|
||||
|
||||
return lookup_heuristic_basename (desktop_file);
|
||||
}
|
||||
|
||||
public void notify_app_state_changed (Gala.App app) {
|
||||
if (app.state == Gala.AppState.RUNNING) {
|
||||
running_apps.insert (app, app);
|
||||
} else if (app.state == Gala.AppState.STOPPED) {
|
||||
running_apps.remove (app);
|
||||
}
|
||||
}
|
||||
|
||||
public GLib.List<unowned Gala.App> get_running_apps () {
|
||||
return running_apps.get_keys ();
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
gala_lib_sources = files(
|
||||
'ActivatableComponent.vala',
|
||||
'App.vala',
|
||||
'AppCache.vala',
|
||||
'AppSystem.vala',
|
||||
'Constants.vala',
|
||||
'DragDropAction.vala',
|
||||
'Drawing/BufferSurface.vala',
|
||||
@ -48,7 +50,7 @@ pkg.generate(
|
||||
name: 'Gala',
|
||||
description: 'Library to develop plugins for Gala',
|
||||
subdirs: 'gala',
|
||||
requires: [glib_dep, gobject_dep, libmutter_dep],
|
||||
requires: [glib_dep, gobject_dep, gio_dep, gio_unix_dep, libmutter_dep],
|
||||
variables: [
|
||||
'datarootdir=${prefix}/@0@'.format(get_option('datadir')),
|
||||
'pkgdatadir=${datarootdir}/gala'
|
||||
|
@ -87,6 +87,7 @@ canberra_dep = dependency('libcanberra')
|
||||
glib_dep = dependency('glib-2.0', version: '>= @0@'.format(glib_version_required))
|
||||
gobject_dep = dependency('gobject-2.0', version: '>= @0@'.format(glib_version_required))
|
||||
gio_dep = dependency('gio-2.0', version: '>= @0@'.format(glib_version_required))
|
||||
gio_unix_dep = dependency('gio-unix-2.0', version: '>= @0@'.format(glib_version_required))
|
||||
gmodule_dep = dependency('gmodule-2.0')
|
||||
gtk_dep = [dependency('gtk+-3.0', version: '>= @0@'.format(gtk_version_required)), dependency('gdk-x11-3.0')]
|
||||
gee_dep = dependency('gee-0.8')
|
||||
@ -190,7 +191,7 @@ endif
|
||||
add_project_arguments(vala_flags, language: 'vala')
|
||||
add_project_link_arguments(['-Wl,-rpath,@0@'.format(mutter_typelib_dir)], language: 'c')
|
||||
|
||||
gala_base_dep = [canberra_dep, glib_dep, gobject_dep, gio_dep, gmodule_dep, gee_dep, gtk_dep, mutter_dep, granite_dep, gnome_desktop_dep, m_dep, posix_dep, gexiv2_dep, config_dep]
|
||||
gala_base_dep = [canberra_dep, glib_dep, gobject_dep, gio_dep, gio_unix_dep, gmodule_dep, gee_dep, gtk_dep, mutter_dep, granite_dep, gnome_desktop_dep, m_dep, posix_dep, gexiv2_dep, config_dep]
|
||||
|
||||
if get_option('systemd')
|
||||
gala_base_dep += systemd_dep
|
||||
|
@ -33,6 +33,10 @@ namespace Gala {
|
||||
try {
|
||||
connection.register_object ("/org/pantheon/gala", instance);
|
||||
} catch (Error e) { warning (e.message); }
|
||||
|
||||
try {
|
||||
connection.register_object ("/org/pantheon/gala/DesktopInterface", new DesktopIntegration (wm));
|
||||
} catch (Error e) { warning (e.message); }
|
||||
},
|
||||
() => {},
|
||||
() => warning ("Could not acquire name\n") );
|
||||
|
105
src/DesktopIntegration.vala
Normal file
105
src/DesktopIntegration.vala
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2022 elementary, Inc. <https://elementary.io>
|
||||
* Copyright 2022 Corentin Noël <tintou@noel.tf>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
[DBus (name="org.pantheon.gala.DesktopIntegration")]
|
||||
public class Gala.DesktopIntegration : GLib.Object {
|
||||
public struct RunningApplication {
|
||||
string app_id;
|
||||
GLib.HashTable<unowned string, Variant> details;
|
||||
}
|
||||
|
||||
public struct Window {
|
||||
uint64 uid;
|
||||
GLib.HashTable<unowned string, Variant> properties;
|
||||
}
|
||||
|
||||
private unowned WindowManager wm;
|
||||
public uint version { get; default = 1; }
|
||||
public signal void running_applications_changed ();
|
||||
public signal void windows_changed ();
|
||||
|
||||
public DesktopIntegration (WindowManager wm) {
|
||||
this.wm = wm;
|
||||
unowned WindowManagerGala? gala_wm = wm as WindowManagerGala;
|
||||
if (gala_wm != null) {
|
||||
gala_wm.window_tracker.windows_changed.connect (() => windows_changed ());
|
||||
}
|
||||
}
|
||||
|
||||
public RunningApplication[] get_running_applications () throws GLib.DBusError, GLib.IOError {
|
||||
RunningApplication[] returned_apps = {};
|
||||
var apps = Gala.AppSystem.get_default ().get_running_apps ();
|
||||
foreach (unowned var app in apps) {
|
||||
returned_apps += RunningApplication () {
|
||||
app_id = app.id,
|
||||
details = new GLib.HashTable<unowned string, Variant> (str_hash, str_equal)
|
||||
};
|
||||
}
|
||||
|
||||
return (owned) returned_apps;
|
||||
}
|
||||
|
||||
private bool is_eligible_window (Meta.Window window) {
|
||||
if (window.is_override_redirect ()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (window.get_window_type ()) {
|
||||
case Meta.WindowType.NORMAL:
|
||||
case Meta.WindowType.DIALOG:
|
||||
case Meta.WindowType.MODAL_DIALOG:
|
||||
case Meta.WindowType.UTILITY:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Window[] get_windows () throws GLib.DBusError, GLib.IOError {
|
||||
Window[] returned_windows = {};
|
||||
var apps = Gala.AppSystem.get_default ().get_running_apps ();
|
||||
foreach (unowned var app in apps) {
|
||||
foreach (weak Meta.Window window in app.get_windows ()) {
|
||||
if (!is_eligible_window (window)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var properties = new GLib.HashTable<unowned string, Variant> (str_hash, str_equal);
|
||||
var frame_rect = window.get_frame_rect ();
|
||||
unowned var title = window.get_title ();
|
||||
unowned var wm_class = window.get_wm_class ();
|
||||
unowned var sandboxed_app_id = window.get_sandboxed_app_id ();
|
||||
|
||||
properties.insert ("app-id", new GLib.Variant.string (app.id));
|
||||
properties.insert ("client-type", new GLib.Variant.uint32 (window.get_client_type ()));
|
||||
properties.insert ("is-hidden", new GLib.Variant.boolean (window.is_hidden ()));
|
||||
properties.insert ("has-focus", new GLib.Variant.boolean (window.has_focus ()));
|
||||
properties.insert ("width", new GLib.Variant.uint32 (frame_rect.width));
|
||||
properties.insert ("height", new GLib.Variant.uint32 (frame_rect.height));
|
||||
|
||||
// These properties may not be available for all windows:
|
||||
if (title != null) {
|
||||
properties.insert ("title", new GLib.Variant.string (title));
|
||||
}
|
||||
|
||||
if (wm_class != null) {
|
||||
properties.insert ("wm-class", new GLib.Variant.string (wm_class));
|
||||
}
|
||||
|
||||
if (sandboxed_app_id != null) {
|
||||
properties.insert ("sandboxed-app-id", new GLib.Variant.string (sandboxed_app_id));
|
||||
}
|
||||
|
||||
returned_windows += Window () {
|
||||
uid = window.get_id (),
|
||||
properties = properties
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (owned) returned_windows;
|
||||
}
|
||||
}
|
@ -76,6 +76,8 @@ namespace Gala {
|
||||
|
||||
HotCornerManager? hot_corner_manager = null;
|
||||
|
||||
public WindowTracker? window_tracker { get; private set; }
|
||||
|
||||
/**
|
||||
* Allow to zoom in/out the entire desktop.
|
||||
*/
|
||||
@ -175,6 +177,8 @@ namespace Gala {
|
||||
|
||||
WindowListener.init (display);
|
||||
KeyboardManager.init (display);
|
||||
window_tracker = new WindowTracker ();
|
||||
window_tracker.init (display);
|
||||
|
||||
notification_stack = new NotificationStack (display);
|
||||
|
||||
|
323
src/WindowTracker.vala
Normal file
323
src/WindowTracker.vala
Normal file
@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright 2021 elementary, Inc. <https://elementary.io>
|
||||
* Copyright 2021 Corentin Noël <tintou@noel.tf>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
public class Gala.WindowTracker : GLib.Object {
|
||||
private Gala.App? focused_app = null;
|
||||
private GLib.HashTable<unowned Meta.Window, Gala.App> window_to_app;
|
||||
|
||||
public signal void windows_changed ();
|
||||
|
||||
construct {
|
||||
window_to_app = new GLib.HashTable<unowned Meta.Window, Gala.App> (direct_hash, direct_equal);
|
||||
}
|
||||
|
||||
public void init (Meta.Display display) {
|
||||
unowned Meta.StartupNotification sn = display.get_startup_notification ();
|
||||
sn.changed.connect (on_startup_sequence_changed);
|
||||
load_initial_windows (display);
|
||||
init_window_tracking (display);
|
||||
}
|
||||
|
||||
private void load_initial_windows (Meta.Display display) {
|
||||
#if HAS_MUTTER42
|
||||
GLib.List<weak Meta.Window> windows = display.list_all_windows ();
|
||||
foreach (weak Meta.Window window in windows) {
|
||||
track_window (window);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void init_window_tracking (Meta.Display display) {
|
||||
display.notify["focus-window"].connect (() => {});
|
||||
display.window_created.connect ((window) => track_window (window));
|
||||
}
|
||||
|
||||
private void on_startup_sequence_changed (Meta.StartupSequence sequence) {
|
||||
unowned Gala.App? app = Gala.App.new_from_startup_sequence (sequence);
|
||||
if (app != null) {
|
||||
app.handle_startup_sequence (sequence);
|
||||
}
|
||||
}
|
||||
|
||||
private static unowned Gala.App? get_app_from_id (string id) {
|
||||
var desktop_file = id.concat (".desktop");
|
||||
return Gala.AppSystem.get_default ().lookup_app (desktop_file);
|
||||
}
|
||||
|
||||
private static unowned Gala.App? get_app_from_gapplication_id (Meta.Window window) {
|
||||
unowned string? id = window.get_gtk_application_id ();
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_app_from_id (id);
|
||||
}
|
||||
|
||||
private static unowned Gala.App? get_app_from_pid (Posix.pid_t pid) {
|
||||
var running_apps = Gala.AppSystem.get_default ().get_running_apps ();
|
||||
foreach (unowned Gala.App app in running_apps) {
|
||||
var app_pids = app.get_pids ();
|
||||
foreach (var app_pid in app_pids) {
|
||||
if (app_pid == pid) {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private unowned Gala.App? get_app_from_window_pid (Meta.Window window) {
|
||||
if (window.is_remote ()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var pid = window.get_pid ();
|
||||
if (pid < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_app_from_pid (pid);
|
||||
}
|
||||
|
||||
private static bool check_app_id_prefix (Gala.App app, string? prefix) {
|
||||
if (prefix == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return app.id.has_prefix (prefix);
|
||||
}
|
||||
|
||||
private unowned Gala.App? get_app_from_window_wmclass (Meta.Window window) {
|
||||
string? app_prefix = null;
|
||||
unowned string? sandbox_id = window.get_sandboxed_app_id ();
|
||||
if (sandbox_id != null) {
|
||||
app_prefix = "%s.".printf (sandbox_id);
|
||||
}
|
||||
|
||||
/* Notes on the heuristics used here:
|
||||
much of the complexity here comes from the desire to support
|
||||
Chrome apps.
|
||||
|
||||
From https://bugzilla.gnome.org/show_bug.cgi?id=673657#c13
|
||||
|
||||
Currently chrome sets WM_CLASS as follows (the first string is the 'instance',
|
||||
the second one is the 'class':
|
||||
|
||||
For the normal browser:
|
||||
WM_CLASS(STRING) = "chromium", "Chromium"
|
||||
|
||||
For a bookmarked page (through 'Tools -> Create application shortcuts')
|
||||
WM_CLASS(STRING) = "wiki.gnome.org__GnomeShell_ApplicationBased", "Chromium"
|
||||
|
||||
For an application from the chrome store (with a .desktop file created through
|
||||
right click, "Create shortcuts" from Chrome's apps overview)
|
||||
WM_CLASS(STRING) = "crx_blpcfgokakmgnkcojhhkbfbldkacnbeo", "Chromium"
|
||||
|
||||
The .desktop file has a matching StartupWMClass, but the name differs, e.g. for
|
||||
the store app (youtube) there is
|
||||
|
||||
.local/share/applications/chrome-blpcfgokakmgnkcojhhkbfbldkacnbeo-Default.desktop
|
||||
|
||||
with
|
||||
|
||||
StartupWMClass=crx_blpcfgokakmgnkcojhhkbfbldkacnbeo
|
||||
|
||||
Note that chromium (but not google-chrome!) includes a StartupWMClass=chromium
|
||||
in their .desktop file, so we must match the instance first.
|
||||
|
||||
Also note that in the good case (regular gtk+ app without hacks), instance and
|
||||
class are the same except for case and there is no StartupWMClass at all.
|
||||
*/
|
||||
|
||||
/* first try a match from WM_CLASS (instance part) to StartupWMClass */
|
||||
unowned string wm_instance = window.get_wm_class_instance ();
|
||||
unowned var appsys = Gala.AppSystem.get_default ();
|
||||
|
||||
unowned Gala.App? app = appsys.lookup_startup_wmclass (wm_instance);
|
||||
if (app != null && check_app_id_prefix (app, app_prefix)) {
|
||||
return app;
|
||||
}
|
||||
|
||||
/* then try a match from WM_CLASS to StartupWMClass */
|
||||
unowned string wm_class = window.get_wm_class ();
|
||||
app = appsys.lookup_startup_wmclass (wm_class);
|
||||
if (app != null && check_app_id_prefix (app, app_prefix)) {
|
||||
return app;
|
||||
}
|
||||
|
||||
/* then try a match from WM_CLASS (instance part) to .desktop */
|
||||
app = appsys.lookup_desktop_wmclass (wm_instance);
|
||||
if (app != null && check_app_id_prefix (app, app_prefix)) {
|
||||
return app;
|
||||
}
|
||||
|
||||
/* finally, try a match from WM_CLASS to .desktop */
|
||||
app = appsys.lookup_desktop_wmclass (wm_class);
|
||||
if (app != null && check_app_id_prefix (app, app_prefix)) {
|
||||
return app;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private unowned Gala.App? get_app_from_sandboxed_app_id (Meta.Window window) {
|
||||
unowned string? id = window.get_sandboxed_app_id ();
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_app_from_id (id);
|
||||
}
|
||||
|
||||
private unowned Gala.App? get_app_from_window_group (Meta.Window window) {
|
||||
unowned Meta.Group? group = window.get_group ();
|
||||
if (group == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GLib.SList<weak Meta.Window> group_windows = group.list_windows ();
|
||||
foreach (weak Meta.Window group_window in group_windows) {
|
||||
if (group_window.window_type != Meta.WindowType.NORMAL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unowned Gala.App? result = window_to_app.lookup (group_window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Gala.App get_app_for_window (Meta.Window window) {
|
||||
unowned Meta.Window? transient_for = window.get_transient_for ();
|
||||
if (transient_for != null) {
|
||||
return get_app_for_window (transient_for);
|
||||
}
|
||||
|
||||
/* First, we check whether we already know about this window,
|
||||
* if so, just return that.
|
||||
*/
|
||||
unowned Gala.App? result;
|
||||
if (window.window_type == Meta.WindowType.NORMAL || window.is_remote ()) {
|
||||
result = window_to_app.lookup (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.is_remote ()) {
|
||||
return new Gala.App.for_window (window);
|
||||
}
|
||||
|
||||
/* Check if the app's WM_CLASS specifies an app; this is
|
||||
* canonical if it does.
|
||||
*/
|
||||
|
||||
result = get_app_from_window_wmclass (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Check if the window was opened from within a sandbox; if this
|
||||
* is the case, a corresponding .desktop file is guaranteed to match;
|
||||
*/
|
||||
result = get_app_from_sandboxed_app_id (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Check if the window has a GApplication ID attached; this is
|
||||
* canonical if it does
|
||||
*/
|
||||
result = get_app_from_gapplication_id (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = get_app_from_window_pid (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Now we check whether we have a match through startup-notification */
|
||||
unowned string? startup_id = window.get_startup_id ();
|
||||
if (startup_id != null) {
|
||||
unowned Meta.StartupNotification sn = window.get_display ().get_startup_notification ();
|
||||
unowned GLib.SList<Meta.StartupSequence> sequences = sn.get_sequences ();
|
||||
foreach (unowned var sequence in sequences) {
|
||||
unowned string id = sequence.get_id ();
|
||||
if (id != startup_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unowned string? appid = sequence.get_application_id ();
|
||||
if (appid != null) {
|
||||
result = AppSystem.get_default ().lookup_app (GLib.Path.get_basename (appid));
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we didn't get a startup-notification match, see if we matched
|
||||
* any other windows in the group.
|
||||
*/
|
||||
result = get_app_from_window_group (window);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Our last resort - we create a fake app from the window */
|
||||
return new Gala.App.for_window (window);
|
||||
|
||||
}
|
||||
|
||||
private void tracked_window_changed (Meta.Window window) {
|
||||
/* It's simplest to just treat this as a remove + add. */
|
||||
disassociate_window (window);
|
||||
track_window (window);
|
||||
}
|
||||
|
||||
private void tracked_window_notified (GLib.Object object, GLib.ParamSpec pspec) {
|
||||
tracked_window_changed ((Meta.Window) object);
|
||||
}
|
||||
|
||||
private void track_window (Meta.Window window) {
|
||||
var app = get_app_for_window (window);
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window_to_app.insert (window, app);
|
||||
|
||||
window.notify["wm-class"].connect (tracked_window_notified);
|
||||
window.notify["gtk-application-id"].connect (tracked_window_notified);
|
||||
window.unmanaged.connect (disassociate_window);
|
||||
|
||||
app.add_window (window);
|
||||
|
||||
windows_changed ();
|
||||
}
|
||||
|
||||
private void disassociate_window (Meta.Window window) {
|
||||
var app = get_app_for_window (window);
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.unmanaged.disconnect (disassociate_window);
|
||||
window.notify["wm-class"].disconnect (tracked_window_notified);
|
||||
window.notify["gtk-application-id"].disconnect (tracked_window_notified);
|
||||
app.remove_window (window);
|
||||
window_to_app.remove (window);
|
||||
|
||||
windows_changed ();
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
gala_bin_sources = files(
|
||||
'DBus.vala',
|
||||
'DBusAccelerator.vala',
|
||||
'DesktopIntegration.vala',
|
||||
'Dialogs.vala',
|
||||
'GalaAccountsServicePlugin.vala',
|
||||
'InternalUtils.vala',
|
||||
@ -15,6 +16,7 @@ gala_bin_sources = files(
|
||||
'ShadowEffect.vala',
|
||||
'WindowListener.vala',
|
||||
'WindowManager.vala',
|
||||
'WindowTracker.vala',
|
||||
'WorkspaceManager.vala',
|
||||
'Zoom.vala',
|
||||
'AccentColor/AccentColorManager.vala',
|
||||
|
Loading…
Reference in New Issue
Block a user