diff --git a/src/Dialogs/NewBranchDialog.vala b/src/Dialogs/NewBranchDialog.vala index 19471a4369..a93f55a95e 100644 --- a/src/Dialogs/NewBranchDialog.vala +++ b/src/Dialogs/NewBranchDialog.vala @@ -20,8 +20,7 @@ */ public class Scratch.Dialogs.NewBranchDialog : Granite.MessageDialog { - public FolderManager.ProjectFolderItem? active_project { get; construct; } - public unowned List project_list { get; construct; } + public FolderManager.ProjectFolderItem active_project { get; construct; } private Granite.ValidatedEntry new_branch_name_entry; public string new_branch_name { @@ -30,66 +29,53 @@ public class Scratch.Dialogs.NewBranchDialog : Granite.MessageDialog { } } - public NewBranchDialog (FolderManager.ProjectFolderItem? project, List project_list) { + public NewBranchDialog (FolderManager.ProjectFolderItem project) { Object ( transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), active_project: project, - project_list: project_list, image_icon: new ThemedIcon ("git") ); } construct { - if (active_project != null) { - assert (active_project.is_git_repo); - primary_text = _("Create a new branch of ā€œ%s/%sā€").printf ( - active_project.file.file.get_basename (), - active_project.get_current_branch_name () - ); - ///TRANSLATORS "Git" is a proper name and must not be translated - secondary_text = _("The branch name must be unique and follow Git naming rules."); - badge_icon = new ThemedIcon ("list-add"); - - new_branch_name_entry = new Granite.ValidatedEntry () { - activates_default = true - }; - - custom_bin.add (new_branch_name_entry); + assert (active_project.is_git_repo); + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + primary_text = _("Create a new branch of ā€œ%s/%sā€").printf ( + active_project.file.file.get_basename (), + active_project.get_current_branch_name () + ); + ///TRANSLATORS "Git" is a proper name and must not be translated + secondary_text = _("The branch name must be unique and follow Git naming rules."); + badge_icon = new ThemedIcon ("list-add"); + new_branch_name_entry = new Granite.ValidatedEntry () { + activates_default = true + }; - var create_button = (Gtk.Button) add_button (_("Create Branch"), Gtk.ResponseType.APPLY); - create_button.can_default = true; - create_button.has_default = true; - create_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + custom_bin.add (new_branch_name_entry); - new_branch_name_entry.bind_property ( - "is-valid", create_button, "sensitive", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE - ); + var create_button = (Gtk.Button) add_button (_("Create Branch"), Gtk.ResponseType.APPLY); + create_button.can_default = true; + create_button.has_default = true; + create_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - new_branch_name_entry.changed.connect (() => { - unowned var new_name = new_branch_name_entry.text; - if (!active_project.is_valid_new_branch_name (new_name)) { - new_branch_name_entry.is_valid = false; - return; - } + new_branch_name_entry.bind_property ( + "is-valid", create_button, "sensitive", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE + ); - if (active_project.has_local_branch_name (new_name)) { - new_branch_name_entry.is_valid = false; - return; - } + new_branch_name_entry.changed.connect (() => { + unowned var new_name = new_branch_name_entry.text; + if (!active_project.is_valid_new_branch_name (new_name)) { + new_branch_name_entry.is_valid = false; + return; + } - //Do we need to check remote branches as well? - new_branch_name_entry.is_valid = true; - }); - } else { - primary_text = _("You must have an active Git project before creating a new branch."); - badge_icon = new ThemedIcon ("dialog-warning"); - if (project_list.length () == 0) { - secondary_text = _("Open a Git project folder in the sidebar."); - } else { - secondary_text = _("Open a document in a Git project folder in the sidebar or use a project context menu."); + if (active_project.has_local_branch_name (new_name)) { + new_branch_name_entry.is_valid = false; + return; } - } - add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + //Do we need to check remote branches as well? + new_branch_name_entry.is_valid = true; + }); } } diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index e81011a212..fccdd65b2f 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -163,50 +163,26 @@ namespace Scratch.FolderManager { } } - public void new_branch (GLib.File? current_doc_file) { - GLib.List project_list; + public void new_branch (string active_project_path) { + unowned var active_project = (ProjectFolderItem)(find_path (root, active_project_path)); + if (active_project == null || !active_project.is_git_repo) { + Gdk.beep (); + return; + } + string? branch_name = null; - unowned var active_project = get_active_project (current_doc_file, out project_list); - var dialog = new Dialogs.NewBranchDialog (active_project, project_list); + var dialog = new Dialogs.NewBranchDialog (active_project); dialog.show_all (); if (dialog.run () == Gtk.ResponseType.APPLY) { branch_name = dialog.new_branch_name; } dialog.destroy (); - if (active_project != null && branch_name != null) { + if (branch_name != null) { active_project.new_branch (branch_name); } } - public unowned ProjectFolderItem? get_active_project (GLib.File? active_file, out List project_list) { - unowned ProjectFolderItem? project = null; - project_list = null; - foreach (var child in root.children) { - project = (ProjectFolderItem)child; - if (!project.is_git_repo) { - // Ignore sidebar folders that are not git repos - continue; - } - - if (active_file != null) - if (project.file.file.equal (active_file) || - project.file.file.get_relative_path (active_file) != null) { - - return project; - } - - project_list.prepend (project); - } - - if (project_list.length () == 1) { - //There was only one project so use that - return project_list.data; - } - - return null; - } - private void add_folder (File folder, bool expand) { if (is_open (folder)) { warning ("Folder '%s' is already open.", folder.path); @@ -216,20 +192,23 @@ namespace Scratch.FolderManager { return; } - var folder_root = new ProjectFolderItem (folder, this); + var folder_root = new ProjectFolderItem (folder, this); // Constructor adds project to GitManager this.root.add (folder_root); folder_root.expanded = expand; folder_root.closed.connect (() => { close_all_docs_from_path (folder_root.file.path); root.remove (folder_root); + Scratch.Services.GitManager.get_instance ().remove_project (folder_root.file.file); write_settings (); }); folder_root.close_all_except.connect (() => { foreach (var child in root.children) { - if (child != folder_root) { - root.remove (child); + var project_folder_item = (ProjectFolderItem)child; + if (project_folder_item != folder_root) { + root.remove (project_folder_item); + Scratch.Services.GitManager.get_instance ().remove_project (project_folder_item.file.file); } } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index bc172c85fe..a2a08d326b 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -97,7 +97,9 @@ namespace Scratch.FolderManager { public override Gtk.Menu? get_context_menu () { var close_item = new Gtk.MenuItem.with_label (_("Close Folder")); - close_item.activate.connect (() => { closed (); }); + close_item.activate.connect (() => { + closed (); + }); var close_all_except_item = new Gtk.MenuItem.with_label (_("Close Other Folders")); close_all_except_item.activate.connect (() => { close_all_except (); }); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index a254adccda..cf6f8fedd6 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -876,19 +876,7 @@ namespace Scratch { } private void action_find_global (SimpleAction action, Variant? param) { - string path = ""; - if (param != null) { - path = param.get_string (); - } - - if (path == "") { - var current_doc = get_current_document (); - if (current_doc != null) { - path = current_doc.file.get_path (); - } - } - - folder_manager_view.search_global (path); + folder_manager_view.search_global (get_target_path_for_git_actions (param)); } private void set_search_text () { @@ -1006,22 +994,28 @@ namespace Scratch { } private void action_new_branch (SimpleAction action, Variant? param) { - string path = ""; - File? file = null; - if (param != null) { - path = param.get_string (); - } - - if (path == "") { - var current_doc = get_current_document (); - if (current_doc != null) { - file = current_doc.file; - } - } else { - file = File.new_for_path (path); - } - - folder_manager_view.new_branch (file); - } + folder_manager_view.new_branch (get_target_path_for_git_actions (param)); + } + + private string? get_target_path_for_git_actions (Variant? path_variant) { + string? path = ""; + if (path_variant != null) { + path = path_variant.get_string (); + } + + if (path == "") { // Happens when keyboard accelerator is used + path = Services.GitManager.get_instance ().active_project_path; + if (path == null) { + var current_doc = get_current_document (); + if (current_doc != null) { + path = current_doc.file.get_path (); + } else { + return null; // Cannot determine target project + } + } + } + + return path; + } } } diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 162765f246..0318c6420f 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -20,6 +20,9 @@ namespace Scratch.Services { public class GitManager : Object { + public ListStore project_liststore { get; private set; } + public string active_project_path { get; set; default = "";} + static Gee.HashMap project_gitrepo_map; static GitManager? instance; @@ -37,9 +40,13 @@ namespace Scratch.Services { return instance; } - private GitManager () {} + private GitManager () { + project_liststore = new ListStore (typeof (File)); + } public MonitoredRepository? add_project (GLib.File root_folder) { + project_liststore.insert_sorted (root_folder, (CompareDataFunc) project_sort_func); + var root_path = root_folder.get_path (); try { var git_repo = Ggit.Repository.open (root_folder); @@ -57,8 +64,21 @@ namespace Scratch.Services { } } + [CCode (instance_pos = -1)] + private int project_sort_func (File a, File b) { + return Path.get_basename (a.get_path ()).collate (Path.get_basename (b.get_path ())); + } + public void remove_project (GLib.File root_folder) { var root_path = root_folder.get_path (); + + uint position; + if (project_liststore.find (root_folder, out position)) { + project_liststore.remove (position); + } else { + critical ("Can't remove: %s", root_path); + } + if (project_gitrepo_map.has_key (root_path)) { project_gitrepo_map.unset (root_path); } diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala new file mode 100644 index 0000000000..bbe128171d --- /dev/null +++ b/src/Widgets/ChooseProjectButton.vala @@ -0,0 +1,171 @@ +/*- + * Copyright (c) 2021 elementary Inc. (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License 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 warranty of + * MERCHANTABILITY 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 . + * + */ + +public class Code.ChooseProjectButton : Gtk.MenuButton { + private const string NO_PROJECT_SELECTED = N_("No Project Selected"); + private Gtk.Label label_widget; + private Gtk.ListBox project_listbox; + private ProjectRow? last_entry = null; + + construct { + var img = new Gtk.Image () { + gicon = new ThemedIcon ("git-symbolic"), + icon_size = Gtk.IconSize.SMALL_TOOLBAR + }; + + label_widget = new Gtk.Label (_(NO_PROJECT_SELECTED)) { + width_chars = 24, + ellipsize = Pango.EllipsizeMode.END, + max_width_chars = 24, + xalign = 0.0f + }; + + tooltip_text = _("Active Git project: %s").printf (_(NO_PROJECT_SELECTED)); + + var grid = new Gtk.Grid () { + halign = Gtk.Align.START + }; + grid.add (img); + grid.add (label_widget); + add (grid); + + project_listbox = new Gtk.ListBox () { + selection_mode = Gtk.SelectionMode.SINGLE + }; + var project_filter = new Gtk.SearchEntry () { + margin = 12, + margin_bottom = 6, + placeholder_text = _("Filter projects") + }; + + project_listbox.set_filter_func ((row) => { + //Both are lowercased so that the case doesn't matter when comparing. + return (((ProjectRow) row).project_name.down ().contains (project_filter.text.down ().strip ())); + }); + + project_filter.changed.connect (() => { + project_listbox.invalidate_filter (); + }); + + var project_scrolled = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = Gtk.PolicyType.NEVER, + expand = true, + margin_top = 3, + margin_bottom = 3, + max_content_height = 350, + propagate_natural_height = true + }; + + project_scrolled.add (project_listbox); + + var popover_content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + popover_content.add (project_filter); + popover_content.add (project_scrolled); + + popover_content.show_all (); + + var project_popover = new Gtk.Popover (this) { + position = Gtk.PositionType.BOTTOM + }; + + project_popover.add (popover_content); + + popover = project_popover; + + var hsizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); + hsizegroup.add_widget (this); + hsizegroup.add_widget (project_listbox); + + project_listbox.bind_model ( + Scratch.Services.GitManager.get_instance ().project_liststore, + create_project_row + ); + + project_listbox.row_activated.connect ((row) => { + var project_entry = ((ProjectRow) row); + select_project (project_entry); + }); + } + + private Gtk.Widget create_project_row (GLib.Object object) { + unowned var project_folder = (File) object; + var project_row = new ProjectRow (project_folder.get_path ()); + if (last_entry != null) { + project_row.project_radio.join_group (last_entry.project_radio); + } + last_entry = project_row; + + return project_row; + } + + private void select_project (ProjectRow project_entry) { + project_listbox.select_row (project_entry); + label_widget.label = project_entry.project_name; + label_widget.tooltip_text = _("Active Git project: %s").printf (project_entry.project_path); + project_entry.active = true; + Scratch.Services.GitManager.get_instance ().active_project_path = project_entry.project_path; + } + + public void set_document (Scratch.Services.Document doc) { + set_active_path (doc.file.get_path ()); + } + + public void set_active_path (string active_path) { + project_listbox.get_children ().foreach ((child) => { + var project_entry = ((ProjectRow) child); + if (active_path.has_prefix (project_entry.project_path)) { + select_project (project_entry); + } + }); + } + + public class ProjectRow : Gtk.ListBoxRow { + public bool active { get; set; } + public string project_path { get; construct; } + public string project_name { + owned get { + return Path.get_basename (project_path); + } + } + + public Gtk.RadioButton project_radio { get; construct; } + + public ProjectRow (string project_path) { + Object ( + project_path: project_path + ); + } + + class construct { + set_css_name (Gtk.STYLE_CLASS_MENUITEM); + } + + construct { + project_radio = new Gtk.RadioButton.with_label (null, project_name); + add (project_radio); + show_all (); + + bind_property ("active", project_radio, "active", BindingFlags.BIDIRECTIONAL); + + project_radio.button_release_event.connect (() => { + activate (); + return Gdk.EVENT_PROPAGATE; + }); + } + } +} diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index a3ef598ae9..de045cefcd 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -27,6 +27,7 @@ namespace Scratch.Widgets { public Gtk.ToggleButton find_button; public Gtk.Button templates_button; public Code.FormatBar format_bar; + public Code.ChooseProjectButton choose_project_button; private const string STYLE_SCHEME_HIGH_CONTRAST = "classic"; private const string STYLE_SCHEME_LIGHT = "solarized-light"; @@ -42,6 +43,10 @@ namespace Scratch.Widgets { construct { var app_instance = (Scratch.Application) GLib.Application.get_default (); + choose_project_button = new Code.ChooseProjectButton () { + valign = Gtk.Align.CENTER + }; + var open_button = new Gtk.Button.from_icon_name ("document-open", Gtk.IconSize.LARGE_TOOLBAR); open_button.action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_OPEN; open_button.tooltip_markup = Granite.markup_accel_tooltip ( @@ -190,6 +195,7 @@ namespace Scratch.Widgets { }; set_custom_title (format_bar); + pack_start (choose_project_button); pack_start (open_button); pack_start (templates_button); pack_start (save_button); @@ -277,6 +283,7 @@ namespace Scratch.Widgets { public void set_document_focus (Scratch.Services.Document doc) { format_bar.set_document (doc); + choose_project_button.set_document (doc); } } } diff --git a/src/meson.build b/src/meson.build index c7c5c0e5d0..d55fd05e3b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -34,6 +34,7 @@ code_files = files( 'Services/PluginManager.vala', 'Services/Settings.vala', 'Services/TemplateManager.vala', + 'Widgets/ChooseProjectButton.vala', 'Widgets/DocumentView.vala', 'Widgets/FormatBar.vala', 'Widgets/HeaderBar.vala',