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:
Corentin Noël 2022-10-11 02:47:37 +02:00 committed by GitHub
parent 80db0620dc
commit 7c540a2a06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 742 additions and 11 deletions

179
lib/App.vala Normal file
View 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;
}
}

View File

@ -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
View 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 ();
}
}

View File

@ -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'

View File

@ -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

View File

@ -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
View 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;
}
}

View File

@ -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
View 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 ();
}
}

View File

@ -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',