diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28a82d04..604950ce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y gettext gnome-settings-daemon-dev gsettings-desktop-schemas-dev libbamf3-dev libcanberra-dev libcanberra-gtk3-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-3-dev libgranite-dev libgtk-3-dev ${{ matrix.mutter_pkg }} libplank-dev libxml2-utils libgexiv2-dev meson valac valadoc + apt install -y gettext gnome-settings-daemon-dev gsettings-desktop-schemas-dev libbamf3-dev libcanberra-dev libcanberra-gtk3-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-3-dev libgranite-dev libgtk-3-dev ${{ matrix.mutter_pkg }} libplank-dev libxml2-utils libgexiv2-dev libsqlite3-dev meson valac valadoc - name: Build env: DESTDIR: out diff --git a/meson.build b/meson.build index 39e0d1f2..1837c307 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,7 @@ gsd_dep = dependency('gnome-settings-daemon', version: '>= @0@'.format(gsd_versi m_dep = cc.find_library('m', required: false) posix_dep = vala.find_library('posix', required: false) gexiv2_dep = dependency('gexiv2') +sqlite3_dep = dependency('sqlite3') if get_option('systemd') systemd_dep = dependency('libsystemd') endif @@ -163,7 +164,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, gio_unix_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, sqlite3_dep, config_dep] if get_option('systemd') gala_base_dep += systemd_dep diff --git a/src/WindowManager.vala b/src/WindowManager.vala index ef04073e..b840864e 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -187,6 +187,7 @@ namespace Gala { WindowListener.init (display); KeyboardManager.init (display); window_tracker = new WindowTracker (); + WindowStateSaver.init (window_tracker); window_tracker.init (display); notification_stack = new NotificationStack (display); @@ -1436,6 +1437,8 @@ namespace Gala { public override void map (Meta.WindowActor actor) { unowned var window = actor.get_meta_window (); + WindowStateSaver.on_map (window); + if ((window.maximized_horizontally && behavior_settings.get_boolean ("move-maximized-workspace")) || (window.fullscreen && window.is_on_primary_monitor () && behavior_settings.get_boolean ("move-fullscreened-workspace"))) { move_window_to_next_ws (window); diff --git a/src/WindowStateSaver.vala b/src/WindowStateSaver.vala new file mode 100644 index 00000000..6744694b --- /dev/null +++ b/src/WindowStateSaver.vala @@ -0,0 +1,164 @@ +/* + * Copyright 2023 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.WindowStateSaver : GLib.Object { + private static unowned WindowTracker window_tracker; + private static GLib.HashTable> app_windows; + private static Sqlite.Database db; + + public static void init (WindowTracker window_tracker) { + WindowStateSaver.window_tracker = window_tracker; + app_windows = new GLib.HashTable> (GLib.str_hash, GLib.str_equal); + + var dir = Path.build_filename (GLib.Environment.get_user_data_dir (), "io.elementary.gala"); + Posix.mkdir (dir, 0775); + var path = Path.build_filename (dir, "windowstate.db"); + var rc = Sqlite.Database.open_v2 (path, out db); + + if (rc != Sqlite.OK) { + critical ("Cannot open database: %d, %s", rc, db.errmsg ()); + return; + } + + Sqlite.Statement stmt; + rc = db.prepare_v2 ( + """ + CREATE TABLE IF NOT EXISTS apps ( + app_id TEXT, + window_index INTEGER, + last_x INTEGER, + last_y INTEGER, + last_width INTEGER, + last_height INTEGER, + PRIMARY KEY (app_id, window_index) + ); + """, + -1, out stmt + ); + + if (rc == Sqlite.OK) { + rc = stmt.step (); + if (rc == Sqlite.DONE) { + // disable synchronized commits for performance reasons + rc = db.exec ("PRAGMA synchronous=OFF"); + if (rc != Sqlite.OK) { + warning ("Unable to disable synchronous mode %d, %s", rc, db.errmsg ()); + } + + return; + } + } + + critical ("Cannot create table 'apps': %d, %s", rc, db.errmsg ()); + } + + public static void on_map (Meta.Window window) { + var app_id = GLib.Markup.escape_text (window_tracker.get_app_for_window (window).id); + + if (app_id.has_prefix ("window:")) { + // if window failed to be identified, don't remember it + return; + } + + if (window.window_type != Meta.WindowType.NORMAL) { + return; + } + + if (!(app_id in app_windows)) { + app_windows[app_id] = new GLib.Array (); + } + + var window_index = find_window_index (window, app_id); + app_windows[app_id].insert_val (window_index, window); + + var tracking_window = false; + db.exec ( + "SELECT last_x, last_y, last_width, last_height FROM apps WHERE app_id = '%s' AND window_index = '%d';".printf (app_id, window_index), + (n_columns, values, column_names) => { + window.move_resize_frame (false, int.parse (values[0]), int.parse (values[1]), int.parse (values[2]), int.parse (values[3])); + track_window (window, app_id); + tracking_window = true; + + return 0; + } + ); + + if (tracking_window) { + // App was added in callback + return; + } + + var frame_rect = window.get_frame_rect (); + + Sqlite.Statement stmt; + var rc = db.prepare_v2 ( + "INSERT INTO apps (app_id, window_index, last_x, last_y, last_width, last_height) VALUES ('%s', '%d', '%d', '%d', '%d', '%d');" + .printf (app_id, window_index, frame_rect.x, frame_rect.y, frame_rect.width, frame_rect.height), + -1, out stmt + ); + + if (rc == Sqlite.OK) { + rc = stmt.step (); + if (rc == Sqlite.DONE) { + track_window (window, app_id); + return; + } + } + + critical ("Cannot insert app information into database: %d, %s", rc, db.errmsg ()); + } + + private static void track_window (Meta.Window window, string app_id) { + window.unmanaging.connect (on_window_unmanaging); + } + + private static void on_window_unmanaging (Meta.Window window) { + var app_id = GLib.Markup.escape_text (window_tracker.get_app_for_window (window).id); + + var window_index = find_window_index (window, app_id); + app_windows[app_id].remove_index (window_index); + var value = null; // insert_val requires lvalue + app_windows[app_id].insert_val (window_index, value); + + var frame_rect = window.get_frame_rect (); + + Sqlite.Statement stmt; + var rc = db.prepare_v2 ( + "UPDATE apps SET last_x = '%d', last_y = '%d', last_width = '%d', last_height = '%d' WHERE app_id = '%s' AND window_index = '%d';" + .printf (frame_rect.x, frame_rect.y, frame_rect.width, frame_rect.height, app_id, window_index), + -1, out stmt + ); + + if (rc == Sqlite.OK) { + rc = stmt.step (); + if (rc == Sqlite.DONE) { + return; + } + } + + critical ("Cannot update app position in database: %d, %s", rc, db.errmsg ()); + } + + private static int find_window_index (Meta.Window window, string app_id) requires (app_id in app_windows) { + unowned var windows_list = app_windows[app_id]; + var first_null = -1; + for (int i = 0; i < windows_list.length; i++) { + var w = windows_list.data[i]; + if (w == window) { + return i; + } + + if (w == null && first_null == -1) { + first_null = i; + } + } + + if (first_null != -1) { + return first_null; + } + + return (int) windows_list.length; + } +} diff --git a/src/meson.build b/src/meson.build index a60a295e..6f01ef30 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,6 +15,7 @@ gala_bin_sources = files( 'SessionManager.vala', 'WindowListener.vala', 'WindowManager.vala', + 'WindowStateSaver.vala', 'WindowTracker.vala', 'WorkspaceManager.vala', 'Zoom.vala',