From 41ba2aa5fb1acdbea6fd2d862ac4908d0b53b88c Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 15:40:37 +0100 Subject: [PATCH 1/6] Add settings key for closed folders --- data/io.elementary.code.gschema.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/io.elementary.code.gschema.xml b/data/io.elementary.code.gschema.xml index 0cfce65a00..480e8c0fea 100644 --- a/data/io.elementary.code.gschema.xml +++ b/data/io.elementary.code.gschema.xml @@ -243,5 +243,10 @@ Opened folders. Opened folders that should be restored in startup. + + [] + Recently closed folders. + Recently closed folders to be shown in project chooser button dropdown. + From a50f5ffbf4a29295c793d348eb05402b2a7a250b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 16:17:02 +0100 Subject: [PATCH 2/6] Add closed folder to settings in closed handler --- src/FolderManager/FileView.vala | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 2841e2715e..9c4051e357 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -663,12 +663,14 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } Scratch.Services.GitManager.get_instance ().remove_project (folder_root); write_settings (); + update_recently_closed_projects (folder_root); }); // We do not want to rewrite settings while restoring from settings // This interferes with fuzzy-finder plugins_manager // See https://github.com/elementary/code/issues/1533 if (!restoring) { + // Add opened folder to settings write_settings (); } @@ -709,4 +711,29 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane settings.set_strv ("opened-folders", to_save); } + + private void update_recently_closed_projects (ProjectFolderItem closed_folder) { + string[] recently_closed = settings.get_strv ("closed-folders"); + var closed_path = closed_folder.path; + bool found = false; + foreach (string path in recently_closed) { + if (path == closed_path) { + found = true; + break; + } + } + + if (found) { + return; + } + + if (recently_closed.length < 10) { + recently_closed += closed_path; + } else { + recently_closed.move (1, 0, 9); + recently_closed[9] = closed_path; + } + + settings.set_strv ("closed-folders", recently_closed); + } } From bbbfec8998639ca1112f2b7c43bfb16fdbe39c40 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 16:21:49 +0100 Subject: [PATCH 3/6] Always close with close signal --- src/FolderManager/FileView.vala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 9c4051e357..da62a6b110 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -117,12 +117,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane foreach (var child in root.children) { var project_folder_item = (ProjectFolderItem) child; if (project_folder_item != folder_root) { - toplevel_action_group.activate_action ( - MainWindow.ACTION_CLOSE_PROJECT_DOCS, - new Variant.string (project_folder_item.path) - ); - root.remove (project_folder_item); - git_manager.remove_project (project_folder_item); + project_folder_item.closed (); } } //Make remaining project the active one From bc190583914a767ac8a78f883ee1c5ade1ff25ce Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 17:04:01 +0100 Subject: [PATCH 4/6] Remove opened folder from recently closed list --- src/FolderManager/FileView.vala | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index da62a6b110..916bdfdd71 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -117,7 +117,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane foreach (var child in root.children) { var project_folder_item = (ProjectFolderItem) child; if (project_folder_item != folder_root) { - project_folder_item.closed (); + project_folder_item.closed (); // Signal not processed 'til loop complete } } //Make remaining project the active one @@ -658,7 +658,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } Scratch.Services.GitManager.get_instance ().remove_project (folder_root); write_settings (); - update_recently_closed_projects (folder_root); + update_recently_closed_projects (folder_root, true); }); // We do not want to rewrite settings while restoring from settings @@ -667,6 +667,9 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane if (!restoring) { // Add opened folder to settings write_settings (); + // Remove folder from recently closed if present + update_recently_closed_projects (folder_root, false); + } add_folder.callback (); @@ -707,26 +710,44 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane settings.set_strv ("opened-folders", to_save); } - private void update_recently_closed_projects (ProjectFolderItem closed_folder) { + private void update_recently_closed_projects ( + ProjectFolderItem closed_folder, + bool to_be_added + ) { string[] recently_closed = settings.get_strv ("closed-folders"); var closed_path = closed_folder.path; bool found = false; + int index = 0; foreach (string path in recently_closed) { if (path == closed_path) { found = true; break; } + + index++; } - if (found) { + if ((to_be_added && found) || (!to_be_added && !found)) { return; } - if (recently_closed.length < 10) { - recently_closed += closed_path; + if (to_be_added) { + if (recently_closed.length < 10) { + recently_closed += closed_path; + } else { + recently_closed.move (1, 0, 9); + recently_closed[9] = closed_path; + } } else { - recently_closed.move (1, 0, 9); - recently_closed[9] = closed_path; + var size = recently_closed.length; + if (index < size - 1) { + recently_closed.move (index + 1, index, size - index); + } else { + recently_closed[index] = null; // Need to use null for resize to work + } + + + recently_closed.resize (size - 1); } settings.set_strv ("closed-folders", recently_closed); From 59c25a2cb9bd472ff3dea4feaf9d15b2ef4bb3b0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 18:46:09 +0100 Subject: [PATCH 5/6] List recently closed projects in projectchooserbutton popover --- src/Widgets/ChooseProjectButton.vala | 105 +++++++++++++++++++-------- 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 132e2c74d4..accfac88e9 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -52,6 +52,14 @@ public class Code.ChooseProjectButton : Gtk.Bin { return (((ProjectRow) row).project_name.down ().contains (project_filter.text.down ().strip ())); }); + project_listbox.set_header_func ((row, before) => { + if (before == null && ((ProjectRow) row).is_open) { + row.set_header (new Granite.HeaderLabel ("Open Projects") { halign = START }); + } else if (!((ProjectRow) row).is_open && (before == null || ((ProjectRow) before).is_open)) { + row.set_header (new Granite.HeaderLabel ("Recent Closed Projects") { halign = START }); + } + }); + project_filter.changed.connect (() => { project_listbox.invalidate_filter (); }); @@ -98,30 +106,13 @@ public class Code.ChooseProjectButton : Gtk.Bin { child = menu_button; // Initialise with any pre-existing projects (needed for second and subsequent window) - var git_manager = Scratch.Services.GitManager.get_instance (); - var src = git_manager.project_liststore; - for (int index = 0; index < src.n_items; index++) { - var item = src.get_object (index); - if (item is Scratch.FolderManager.ProjectFolderItem) { - var row = create_project_row ((Scratch.FolderManager.ProjectFolderItem)item); - project_listbox.insert (row, index); - } - } + insert_project_rows (); - git_manager.project_liststore.items_changed.connect ((src, pos, n_removed, n_added) => { - var rows = project_listbox.get_children (); - for (int index = (int)pos; index < pos + n_removed; index++) { - var row = rows.nth_data (index); - row.destroy (); - } - - for (int index = (int)pos; index < pos + n_added; index++) { - var item = src.get_object (index); - if (item is Scratch.FolderManager.ProjectFolderItem) { - var row = create_project_row ((Scratch.FolderManager.ProjectFolderItem)item); - project_listbox.insert (row, index); - } - } + // Update the list when projects added to or deleted from git manager + var git_manager = Scratch.Services.GitManager.get_instance (); + git_manager.project_liststore.items_changed.connect (() => { + // Inserting is delayed so the settings should have been updated when runs + insert_project_rows (); }); menu_button.activate.connect (() => { @@ -139,6 +130,51 @@ public class Code.ChooseProjectButton : Gtk.Bin { update_button (); } + // Throttle rebuilding the list as gitmanager emits multiple changed signals on startup + // and we need to wait for settings to update + uint insert_timeout_id = 0; + bool insert_wait = false; + private void insert_project_rows () { + if (insert_timeout_id == 0) { + insert_wait = false; + Timeout.add (20, () => { + if (insert_wait) { + insert_wait = false; + return Source.CONTINUE; + } + + var rows = project_listbox.get_children (); + foreach (var row in rows) { + row.destroy (); + } + + var git_manager = Scratch.Services.GitManager.get_instance (); + var src = git_manager.project_liststore; + int index = 0; + for (index = 0; index < src.n_items; index++) { + var item = src.get_object (index); + if (item is Scratch.FolderManager.ProjectFolderItem) { + var row = create_project_row ((Scratch.FolderManager.ProjectFolderItem)item, true); + project_listbox.insert (row, index); + } + } + + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); + var recently_closed = settings.get_strv ("closed-folders"); + var size = recently_closed.length; + // List in reverse order so most recently closed appears first + for (int idx = size - 1; idx >= 0; idx--) { + var row = new ProjectRow (recently_closed[idx], false); + project_listbox.insert (row, index++); + } + + return Source.REMOVE; + }); + } else { + insert_wait = true; + } + } + // Set appearance (only) of project chooser button and list according to active path private void update_button () { unowned var active_path = Scratch.Services.GitManager.get_instance ().active_project_path; @@ -151,9 +187,12 @@ public class Code.ChooseProjectButton : Gtk.Bin { } } - private Gtk.Widget create_project_row (Scratch.FolderManager.ProjectFolderItem project_folder) { + private Gtk.Widget create_project_row ( + Scratch.FolderManager.ProjectFolderItem project_folder, + bool is_open = true + ) { var project_path = project_folder.file.file.get_path (); - var project_row = new ProjectRow (project_path); + var project_row = new ProjectRow (project_path, is_open); // Project folder items cannot be renamed in UI, no need to handle return project_row; @@ -172,15 +211,18 @@ public class Code.ChooseProjectButton : Gtk.Bin { } public string project_path { get; construct; } + public bool is_open { get; construct; } + public string project_name { get { return check_button.label; } } - public ProjectRow (string project_path) { + public ProjectRow (string project_path, bool is_open) { Object ( - project_path: project_path + project_path: project_path, + is_open: is_open ); } @@ -192,8 +234,13 @@ public class Code.ChooseProjectButton : Gtk.Bin { construct { can_focus = true; - action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_SET_ACTIVE_PROJECT; + action_name = Scratch.MainWindow.ACTION_PREFIX; action_target = new Variant.string (project_path); + if (is_open) { + action_name += Scratch.MainWindow.ACTION_SET_ACTIVE_PROJECT; + } else { + action_name += Scratch.MainWindow.ACTION_OPEN_FOLDER; + } check_button = new Gtk.CheckButton.with_label (Path.get_basename (project_path)) { can_focus = false @@ -206,7 +253,7 @@ public class Code.ChooseProjectButton : Gtk.Bin { button = 0 }; button_controller.released.connect (() => { - activate (); + activate (); // Trigger the linked action }); show_all (); From 4f3eb25c53f4fbeb72c338ff7911e2820c463071 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Aug 2026 18:59:48 +0100 Subject: [PATCH 6/6] Headers: Correct grammar, make translatable --- src/Widgets/ChooseProjectButton.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index accfac88e9..be3c0e6bbb 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -54,9 +54,9 @@ public class Code.ChooseProjectButton : Gtk.Bin { project_listbox.set_header_func ((row, before) => { if (before == null && ((ProjectRow) row).is_open) { - row.set_header (new Granite.HeaderLabel ("Open Projects") { halign = START }); + row.set_header (new Granite.HeaderLabel (_("Open Projects")) { halign = START }); } else if (!((ProjectRow) row).is_open && (before == null || ((ProjectRow) before).is_open)) { - row.set_header (new Granite.HeaderLabel ("Recent Closed Projects") { halign = START }); + row.set_header (new Granite.HeaderLabel (_("Recently Closed Projects")) { halign = START }); } });