diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index ac5b9d096c..3330ad6810 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -1,5 +1,5 @@ /*- - * Copyright (c) 2017 elementary LLC. (https://elementary.io), + * Copyright (c) 2017 - 2022 elementary LLC. (https://elementary.io), * 2013 Julien Spautz * * This program is free software: you can redistribute it and/or modify @@ -23,15 +23,19 @@ */ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.PaneSwitcher { private GLib.Settings settings; + private Scratch.Services.GitManager git_manager; + private ActionGroup? toplevel_action_group = null; public signal void select (string file); - public signal void close_all_docs_from_path (string path); - // This is a workaround for SourceList silliness: you cannot remove an item - // without it automatically selecting another one. public bool ignore_next_select { get; set; default = false; } public string icon_name { get; set; } public string title { get; set; } + public string active_project_path { + get { + return git_manager.active_project_path; + } + } construct { width_request = 180; @@ -41,6 +45,13 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P item_selected.connect (on_item_selected); settings = new GLib.Settings ("io.elementary.code.folder-manager"); + + git_manager = Scratch.Services.GitManager.get_instance (); + + realize.connect (() => { + toplevel_action_group = get_action_group (MainWindow.ACTION_GROUP); + assert_nonnull (toplevel_action_group); + }); } private void on_item_selected (Granite.Widgets.SourceList.Item? item) { @@ -104,6 +115,27 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P item_selected.connect (on_item_selected); } + public void collapse_other_projects (string? keep_open_path = null) { + unowned string path; + if (keep_open_path == null) { + path = git_manager.active_project_path; + } else { + path = keep_open_path; + git_manager.active_project_path = path; + } + + foreach (var child in root.children) { + var project_folder = ((ProjectFolderItem) child); + if (project_folder.path != path) { + project_folder.expanded = false; + toplevel_action_group.activate_action (MainWindow.ACTION_HIDE_PROJECT_DOCS, new Variant.string (project_folder.path)); + } else if (project_folder.path == path) { + project_folder.expanded = true; + toplevel_action_group.activate_action (MainWindow.ACTION_RESTORE_PROJECT_DOCS, new Variant.string (project_folder.path)); + } + } + } + private unowned Granite.Widgets.SourceList.Item? find_path (Granite.Widgets.SourceList.ExpandableItem list, string path, bool expand = false) { @@ -231,7 +263,7 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P folder_root.expanded = expand; folder_root.closed.connect (() => { - close_all_docs_from_path (folder_root.file.path); + toplevel_action_group.activate_action (MainWindow.ACTION_CLOSE_PROJECT_DOCS, new Variant.string (folder_root.path)); root.remove (folder_root); foreach (var child in root.children) { var child_folder = (ProjectFolderItem) child; @@ -247,6 +279,7 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P 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); Scratch.Services.GitManager.get_instance ().remove_project (project_folder_item); } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index f357ffa183..c8b750a3f3 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -108,8 +108,8 @@ namespace Scratch.FolderManager { } public override Gtk.Menu? get_context_menu () { - var close_item = new Gtk.MenuItem.with_label (_("Close Folder")); - close_item.activate.connect (() => { + var close_folder_item = new Gtk.MenuItem.with_label (_("Close Folder")); + close_folder_item.activate.connect (() => { closed (); }); @@ -117,6 +117,49 @@ namespace Scratch.FolderManager { close_all_except_item.activate.connect (() => { close_all_except (); }); close_all_except_item.sensitive = view.root.children.size > 1; + var n_open = Scratch.Services.DocumentManager.get_instance ().open_for_project (path); + var open_text = ngettext (_("Close %u Open Document"), + _("Close %u Open Documents"), + n_open).printf (n_open); + + var close_accellabel = new Granite.AccelLabel.from_action_name ( + open_text, + MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS + "::" + ); + var close_item = new Gtk.MenuItem () { + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_CLOSE_PROJECT_DOCS, + action_target = new Variant.string (file.file.get_path ()) + }; + close_item.add (close_accellabel); + + var hide_text = ngettext (_("Hide %u Open Document"), + _("Hide %u Open Documents"), + n_open).printf (n_open); + + var hide_accellabel = new Granite.AccelLabel.from_action_name ( + hide_text, + MainWindow.ACTION_PREFIX + MainWindow.ACTION_HIDE_PROJECT_DOCS + "::" + ); + var hide_item = new Gtk.MenuItem () { + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_HIDE_PROJECT_DOCS, + action_target = new Variant.string (file.file.get_path ()) + }; + hide_item.add (hide_accellabel); + + var n_restorable = Scratch.Services.DocumentManager.get_instance ().restorable_for_project (path); + var restore_text = ngettext (_("Restore %u Hidden Document"), + _("Restore %u Hidden Documents"), + n_restorable).printf (n_restorable); + var restore_accellabel = new Granite.AccelLabel.from_action_name ( + restore_text, + MainWindow.ACTION_PREFIX + MainWindow.ACTION_RESTORE_PROJECT_DOCS + "::" + ); + var restore_item = new Gtk.MenuItem () { + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_RESTORE_PROJECT_DOCS, + action_target = new Variant.string (file.file.get_path ()) + }; + restore_item.add (restore_accellabel); + var delete_item = new Gtk.MenuItem.with_label (_("Move to Trash")); delete_item.activate.connect (() => { closed (); @@ -129,7 +172,7 @@ namespace Scratch.FolderManager { ); var search_item = new Gtk.MenuItem () { - action_name = "win.action_find_global", + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_FIND_GLOBAL, action_target = new Variant.string (file.file.get_path ()) }; search_item.add (search_accellabel); @@ -157,8 +200,22 @@ namespace Scratch.FolderManager { } menu.append (new Gtk.SeparatorMenuItem ()); - menu.append (close_item); + menu.append (close_folder_item); menu.append (close_all_except_item); + menu.append (new Gtk.SeparatorMenuItem ()); + if (n_restorable > 0) { + menu.append (restore_item); + } + + if (n_open > 0) { + menu.append (hide_item); + menu.append (close_item); + } + + if (n_restorable + n_open > 1) { + menu.append (new Gtk.SeparatorMenuItem ()); + } + menu.append (delete_item); menu.append (new Gtk.SeparatorMenuItem ()); menu.append (search_item); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 798a955c29..70268b9c39 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -34,6 +34,7 @@ namespace Scratch { public Scratch.Widgets.SearchBar search_bar; private Code.WelcomeView welcome_view; private FolderManager.FileView folder_manager_view; + private Scratch.Services.DocumentManager document_manager; // Plugins private Scratch.Services.PluginsManager plugins; @@ -54,7 +55,8 @@ namespace Scratch { public SimpleActionGroup actions { get; construct; } - public const string ACTION_PREFIX = "win."; + public const string ACTION_GROUP = "win"; + public const string ACTION_PREFIX = ACTION_GROUP + "."; public const string ACTION_FIND = "action_find"; public const string ACTION_FIND_NEXT = "action_find_next"; public const string ACTION_FIND_PREVIOUS = "action_find_previous"; @@ -90,6 +92,9 @@ namespace Scratch { public const string ACTION_PREVIOUS_TAB = "action_previous_tab"; public const string ACTION_CLEAR_LINES = "action_clear_lines"; public const string ACTION_NEW_BRANCH = "action_new_branch"; + public const string ACTION_CLOSE_PROJECT_DOCS = "action_close_project_docs"; + public const string ACTION_HIDE_PROJECT_DOCS = "action_hide_project_docs"; + public const string ACTION_RESTORE_PROJECT_DOCS = "action_restore_project_docs"; public static Gee.MultiMap action_accelerators = new Gee.HashMultiMap (); @@ -129,7 +134,10 @@ namespace Scratch { { ACTION_NEXT_TAB, action_next_tab }, { ACTION_PREVIOUS_TAB, action_previous_tab }, { ACTION_CLEAR_LINES, action_clear_lines }, - { ACTION_NEW_BRANCH, action_new_branch, "s" } + { ACTION_NEW_BRANCH, action_new_branch, "s" }, + { ACTION_HIDE_PROJECT_DOCS, action_hide_project_docs, "s"}, + { ACTION_CLOSE_PROJECT_DOCS, action_close_project_docs, "s"}, + { ACTION_RESTORE_PROJECT_DOCS, action_restore_project_docs, "s"} }; public MainWindow (Scratch.Application scratch_app) { @@ -176,6 +184,8 @@ namespace Scratch { action_accelerators.set (ACTION_PREVIOUS_TAB, "Tab"); action_accelerators.set (ACTION_CLEAR_LINES, "K"); //Geany action_accelerators.set (ACTION_NEW_BRANCH + "::", "B"); + action_accelerators.set (ACTION_HIDE_PROJECT_DOCS + "::", "h"); + action_accelerators.set (ACTION_RESTORE_PROJECT_DOCS + "::", "r"); var provider = new Gtk.CssProvider (); provider.load_from_resource ("io/elementary/code/Application.css"); @@ -190,9 +200,11 @@ namespace Scratch { weak Gtk.IconTheme default_theme = Gtk.IconTheme.get_default (); default_theme.add_resource_path ("/io/elementary/code"); + document_manager = Scratch.Services.DocumentManager.get_instance (); + actions = new SimpleActionGroup (); actions.add_action_entries (ACTION_ENTRIES, this); - insert_action_group ("win", actions); + insert_action_group (ACTION_GROUP, actions); actions.action_state_changed.connect ((name, new_state) => { if (name == ACTION_SHOW_FIND) { @@ -279,6 +291,10 @@ namespace Scratch { toolbar = new Scratch.Widgets.HeaderBar (); toolbar.title = title; + toolbar.choose_project_button.project_chosen.connect (() => { + folder_manager_view.collapse_other_projects (); + }); + // SearchBar search_bar = new Scratch.Widgets.SearchBar (this); search_revealer = new Gtk.Revealer (); @@ -339,14 +355,6 @@ namespace Scratch { } }); - folder_manager_view.close_all_docs_from_path.connect ((a) => { - var docs = document_view.docs.copy (); - docs.foreach ((doc) => { - if (doc.file.get_path ().has_prefix (a)) { - document_view.close_document (doc); - } - }); - }); folder_manager_view.restore_saved_state (); @@ -564,7 +572,10 @@ namespace Scratch { folder_manager_view.open_folder (foldermanager_file); } - public void open_document (Scratch.Services.Document doc, bool focus = true, int cursor_position = 0) { + public void open_document (Scratch.Services.Document doc, + bool focus = true, + int cursor_position = 0) { + FolderManager.ProjectFolderItem? project = folder_manager_view.get_project_for_file (doc.file); doc.source_view.project = project; document_view.open_document (doc, focus, cursor_position); @@ -862,6 +873,42 @@ namespace Scratch { } } + private void action_hide_project_docs (SimpleAction action, Variant? param) { + close_project_docs (get_target_path_for_actions (param), true); + } + + private void action_close_project_docs (SimpleAction action, Variant? param) { + close_project_docs (get_target_path_for_actions (param), false); + } + + private void action_restore_project_docs (SimpleAction action, Variant? param) { + restore_project_docs (get_target_path_for_actions (param)); + } + + private void close_project_docs (string project_path, bool make_restorable) { + unowned var docs = document_view.docs; + docs.foreach ((doc) => { + if (doc.file.get_path ().has_prefix (project_path)) { + document_view.close_document (doc); + if (make_restorable) { + document_manager.make_restorable (doc); + } + } + }); + + if (!make_restorable) { + document_manager.remove_project (project_path); + } + } + + private void restore_project_docs (string project_path) { + document_manager.take_restorable_paths (project_path).@foreach ((doc_path) => { + var doc = new Scratch.Services.Document (actions, File.new_for_path (doc_path)); + open_document (doc); // Use this to reassociate project and document. + return true; + }); + } + /** Not a toggle action - linked to keyboard short cut (Ctrl-f). **/ private string current_search_term = ""; private void action_fetch (SimpleAction action, Variant? param) { @@ -905,7 +952,7 @@ namespace Scratch { term = search_bar.search_entry.text; } - folder_manager_view.search_global (get_target_path_for_git_actions (param), term); + folder_manager_view.search_global (get_target_path_for_actions (param), term); } private void set_search_text () { @@ -1023,10 +1070,10 @@ namespace Scratch { } private void action_new_branch (SimpleAction action, Variant? param) { - folder_manager_view.new_branch (get_target_path_for_git_actions (param)); + folder_manager_view.new_branch (get_target_path_for_actions (param)); } - private string? get_target_path_for_git_actions (Variant? path_variant) { + private string? get_target_path_for_actions (Variant? path_variant) { string? path = ""; if (path_variant != null) { path = path_variant.get_string (); diff --git a/src/Services/DocumentManager.vala b/src/Services/DocumentManager.vala new file mode 100644 index 0000000000..24edde5a4b --- /dev/null +++ b/src/Services/DocumentManager.vala @@ -0,0 +1,76 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2022 elementary LLC. (https://elementary.io), + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Jeremy Wootten + */ + + public class Scratch.Services.DocumentManager : Object { + static Gee.HashMultiMap project_restorable_docs_map; + static Gee.HashMultiMap project_open_docs_map; + + static DocumentManager? instance; + public static DocumentManager get_instance () { + if (instance == null) { + instance = new DocumentManager (); + } + + return instance; + } + + static construct { + project_restorable_docs_map = new Gee.HashMultiMap (); + project_open_docs_map = new Gee.HashMultiMap (); + } + + public void make_restorable (Document doc) { + project_restorable_docs_map.@set (doc.source_view.project.path, doc.file.get_path ()); + } + + public void add_open_document (Document doc) { + if (doc.source_view.project == null) { + return; + } + + project_open_docs_map.@set (doc.source_view.project.path, doc.file.get_path ()); + } + + public void remove_open_document (Document doc) { + if (doc.source_view.project == null) { + return; + } + + project_open_docs_map.remove (doc.source_view.project.path, doc.file.get_path ()); + } + + public void remove_project (string project_path) { + project_restorable_docs_map.remove_all (project_path); + } + + public Gee.Collection take_restorable_paths (string project_path) { + var docs = project_restorable_docs_map.@get (project_path); + project_restorable_docs_map.remove_all (project_path); + return docs; + } + + public uint restorable_for_project (string project_path) { + return project_restorable_docs_map.@get (project_path).size; + } + + public uint open_for_project (string project_path) { + return project_open_docs_map.@get (project_path).size; + } + } diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 378aaa0a0f..62dfe3ee90 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -22,6 +22,8 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { private Gtk.ListBox project_listbox; private ProjectRow? last_entry = null; + public signal void project_chosen (); + construct { var img = new Gtk.Image () { gicon = new ThemedIcon ("git-symbolic"), @@ -105,6 +107,7 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { project_listbox.row_activated.connect ((row) => { var project_entry = ((ProjectRow) row); select_project (project_entry); + project_chosen (); }); } diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 9a3969b12c..0b6995ae27 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -335,6 +335,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { doc.actions = window.actions; docs.append (doc); + Scratch.Services.DocumentManager.get_instance ().add_open_document (doc); + if (!doc.is_file_temporary) { rename_tabs_with_same_title (doc); } @@ -346,6 +348,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { var doc = tab as Services.Document; docs.remove (doc); + Scratch.Services.DocumentManager.get_instance ().remove_open_document (doc); + doc.source_view.focus_in_event.disconnect (on_focus_in_event); request_placeholder_if_empty (); diff --git a/src/meson.build b/src/meson.build index 8404a86ebb..a3075d043d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -29,6 +29,7 @@ code_files = files( 'FolderManager/ProjectFolderItem.vala', 'Services/CommentToggler.vala', 'Services/Document.vala', + 'Services/DocumentManager.vala', 'Services/FileHandler.vala', 'Services/GitManager.vala', 'Services/MonitoredRepository.vala',