From 4e512ab4a406ba1f9f09031291f6135488d55f9e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 20 Jun 2025 11:56:16 +0100 Subject: [PATCH 01/23] Start BranchActionDialog --- src/Dialogs/BranchActionDialog.vala | 112 +++++++++++ src/FolderManager/FileView.vala | 74 ++++++-- src/FolderManager/ProjectFolderItem.vala | 226 ++++++++++++----------- src/MainWindow.vala | 10 +- src/Services/MonitoredRepository.vala | 22 ++- src/meson.build | 3 +- 6 files changed, 312 insertions(+), 135 deletions(-) create mode 100644 src/Dialogs/BranchActionDialog.vala diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala new file mode 100644 index 0000000000..90fdfc8796 --- /dev/null +++ b/src/Dialogs/BranchActionDialog.vala @@ -0,0 +1,112 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +public enum Scratch.BranchAction { + CHECKOUT, + CREATE, + DELETE +} + +public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { + public FolderManager.ProjectFolderItem project { get; construct; } + public BranchAction action { get; set; } + public string branch { get; set; } + + public BranchActionDialog (FolderManager.ProjectFolderItem project) { + Object ( + transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), + project: project, + primary_text: _("Perform branch action on project %s").printf ( + project.file.file.get_basename ()), + image_icon: new ThemedIcon ("git"), + buttons: Gtk.ButtonsType.CANCEL + ); + } + + construct { + assert (project.is_git_repo); + var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); + set_default_response ( Gtk.ResponseType.CANCEL); + + unowned var local_branches = project.get_branches (); + + var list_store = new ListStore (typeof (Ggit.Branch)); + foreach (Ggit.Branch branch in local_branches) { + list_store.insert_sorted (branch, (a, b) => { + return (((Ggit.Branch)a).get_name ()).collate (((Ggit.Branch)b).get_name ()); + }); + } + + var list_box = new Gtk.ListBox (); + list_box.bind_model (list_store, (obj) => { + var row = new Gtk.ListBoxRow (); + var label = new Gtk.Label (((Ggit.Branch)obj).get_name ()) { + halign = START + }; + row.add (label); + return row; + }); + + var search_entry = new Gtk.SearchEntry (); + list_box.set_filter_func ((row) => { + return (((Gtk.Label)(row.get_child ())).label.contains (search_entry.text)); + }); + + list_box.row_activated.connect ((row) => { + search_entry.text = ((Gtk.Label)(row.get_child ())).label; + }); + + var scrolled_window = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = NEVER, + vscrollbar_policy = AUTOMATIC, + min_content_height = 100 + }; + scrolled_window.child = list_box; + + search_entry.changed.connect (() => { + list_box.invalidate_filter (); + }); + var search_box = new Gtk.Box (VERTICAL, 6); + search_box.add (search_entry); + search_box.add (scrolled_window); + + custom_bin.add (search_box); + // 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); + + // var apply_button = (Gtk.Button) add_button (_("Apply"), Gtk.ResponseType.APPLY); + // apply_button.can_default = true; + // apply_button.has_default = true; + // apply_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + // new_branch_name_entry.bind_property ( + // "is-valid", apply_button, "sensitive", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE + // ); + + // 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; + // } + + // if (active_project.has_local_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; + // }); + + show_all (); + } +} diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 2bb5035b36..86cecdf27a 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -32,8 +32,8 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane public const string ACTION_DELETE = "delete"; public const string ACTION_NEW_FILE = "new-file"; public const string ACTION_NEW_FOLDER = "new-folder"; - public const string ACTION_CHECKOUT_LOCAL_BRANCH = "checkout-local-branch"; - public const string ACTION_CHECKOUT_REMOTE_BRANCH = "checkout-remote-branch"; + // public const string ACTION_CHECKOUT_LOCAL_BRANCH = "checkout-local-branch"; + // public const string ACTION_CHECKOUT_REMOTE_BRANCH = "checkout-remote-branch"; public const string ACTION_CLOSE_FOLDER = "close-folder"; public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; public const string ACTION_SET_ACTIVE_PROJECT = "set-active-project"; @@ -133,14 +133,20 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane return; } + set_active_project (path); + } + + private ProjectFolderItem? set_active_project (string path) { var folder_root = find_path (root, path) as ProjectFolderItem; if (folder_root == null) { - return; + return null; } git_manager.active_project_path = path; write_settings (); + + return folder_root; } public async void restore_saved_state () { @@ -209,6 +215,34 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } } + public void branch_actions (string path) { + warning ("branch actions"); + // Must only carry out branch actions on active project so switch if necessary. + //TODO Warn before switching active project? + var active_project = set_active_project (path); + if (active_project == null || !active_project.is_git_repo) { + Gdk.beep (); + return; + } + + var dialog = new Dialogs.BranchActionDialog (active_project); + dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.APPLY) { + var action = dialog.action; + var branch = dialog.branch; + perform_branch_action.begin (active_project, action, branch); + } + + dialog.destroy (); + }); + + dialog.present (); + } + + private async void perform_branch_action (ProjectFolderItem project, BranchAction action, string branch) { + warning ("perform action"); + } + private unowned Code.Widgets.SourceList.Item? find_path ( Code.Widgets.SourceList.ExpandableItem list, string path, @@ -288,25 +322,25 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } } - 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; - } + // 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; - var dialog = new Dialogs.NewBranchDialog (active_project); - dialog.show_all (); - if (dialog.run () == Gtk.ResponseType.APPLY) { - branch_name = dialog.new_branch_name; - } + // string? branch_name = null; + // 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 (branch_name != null) { - active_project.new_branch (branch_name); - } - } + // dialog.destroy (); + // if (branch_name != null) { + // active_project.new_branch (branch_name); + // } + // } public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { plugins.hook_folder_item_change (source, dest, event); diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index c53be4d553..fb4d727c83 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -26,8 +26,8 @@ namespace Scratch.FolderManager { private static Icon added_icon; private static Icon modified_icon; - private SimpleAction checkout_local_branch_action; - private SimpleAction checkout_remote_branch_action; + // private SimpleAction checkout_local_branch_action; + // private SimpleAction checkout_remote_branch_action; public signal void closed (); @@ -69,7 +69,7 @@ namespace Scratch.FolderManager { ); } - checkout_local_branch_action.set_state (monitored_repo.branch_name); + // checkout_local_branch_action.set_state (monitored_repo.branch_name); } } @@ -77,23 +77,23 @@ namespace Scratch.FolderManager { monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); notify["name"].connect (branch_or_name_changed); if (monitored_repo != null) { - checkout_local_branch_action = new SimpleAction.stateful ( - FileView.ACTION_CHECKOUT_LOCAL_BRANCH, - GLib.VariantType.STRING, - "" - ); - checkout_remote_branch_action = new SimpleAction.stateful ( - FileView.ACTION_CHECKOUT_REMOTE_BRANCH, - GLib.VariantType.STRING, - "" - ); + // checkout_local_branch_action = new SimpleAction.stateful ( + // FileView.ACTION_CHECKOUT_LOCAL_BRANCH, + // GLib.VariantType.STRING, + // "" + // ); + // checkout_remote_branch_action = new SimpleAction.stateful ( + // FileView.ACTION_CHECKOUT_REMOTE_BRANCH, + // GLib.VariantType.STRING, + // "" + // ); monitored_repo.branch_changed.connect (branch_or_name_changed); monitored_repo.ignored_changed.connect ((deprioritize_git_ignored)); monitored_repo.file_status_change.connect (() => update_item_status (null)); monitored_repo.update_status_map (); monitored_repo.branch_changed (); - checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action); - checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); + // checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action); + // checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); } } @@ -179,7 +179,15 @@ namespace Scratch.FolderManager { var folder_actions_section = new GLib.Menu (); folder_actions_section.append_item (create_submenu_for_new ()); if (monitored_repo != null) { - folder_actions_section.append_item (create_submenu_for_branch ()); + var branch_action_item = new MenuItem ( + _("Branch Actions…"), + GLib.Action.print_detailed_name ( + MainWindow.ACTION_PREFIX + MainWindow.ACTION_BRANCH_ACTIONS, + new Variant.string (file.path) + ) + ); + folder_actions_section.append_item (branch_action_item); + // folder_actions_section.append_item (create_submenu_for_branch ()); } var close_folder_item = new GLib.MenuItem ( @@ -320,97 +328,97 @@ namespace Scratch.FolderManager { return menu; } - protected GLib.MenuItem create_submenu_for_branch () { - // Ensures that action for relevant project is being used - view.actions.add_action (checkout_local_branch_action); - view.actions.add_action (checkout_remote_branch_action); - - unowned var local_branches = monitored_repo.get_local_branches (); - var local_branch_submenu = new Menu (); - var local_branch_menu = new Menu (); - if (local_branches.length () > 0) { - local_branch_submenu.append_submenu (_("Local"), local_branch_menu); - foreach (unowned var branch_name in local_branches) { - local_branch_menu.append ( - branch_name, - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, - branch_name - ) - ); - } - } - - - unowned var remote_branches = monitored_repo.get_remote_branches (); - var remote_branch_submenu = new Menu (); - var remote_branch_menu = new Menu (); - if (remote_branches.length () > 0) { - remote_branch_submenu.append_submenu (_("Remote"), remote_branch_menu); - foreach (unowned var branch_name in remote_branches) { - remote_branch_menu.append ( - branch_name, - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, - branch_name - ) - ); - } - - - } - - var new_branch_item = new GLib.MenuItem ( - _("New Branch…"), - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - file.path - ) - ); - - new_branch_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - "" - ) - ) - ); - - GLib.Menu bottom_section = new GLib.Menu (); - bottom_section.append_item (new_branch_item); - - var menu = new GLib.Menu (); - menu.append_section (null, local_branch_submenu); - menu.append_section (null, remote_branch_submenu); - menu.append_section (null, bottom_section); - - var menu_item = new GLib.MenuItem.submenu (_("Branch"), menu); - return menu_item; - } - - private void handle_checkout_local_branch_action (GLib.Variant? param) { - var branch_name = param != null ? param.get_string () : ""; - try { - monitored_repo.change_local_branch (branch_name); - } catch (GLib.Error e) { - warning ("Failed to change branch to %s. %s", branch_name, e.message); - } - } - - private void handle_checkout_remote_branch_action (GLib.Variant? param) { - var branch_name = param != null ? param.get_string () : ""; - if (branch_name == "") { - return; - } - - try { - monitored_repo.checkout_remote_branch (branch_name); - } catch (GLib.Error e) { - warning ("Failed to change branch to %s. %s", branch_name, e.message); - } - } + // protected GLib.MenuItem create_submenu_for_branch () { + // // Ensures that action for relevant project is being used + // view.actions.add_action (checkout_local_branch_action); + // view.actions.add_action (checkout_remote_branch_action); + + // unowned var local_branches = monitored_repo.get_local_branches (); + // var local_branch_submenu = new Menu (); + // var local_branch_menu = new Menu (); + // if (local_branches.length () > 0) { + // local_branch_submenu.append_submenu (_("Local"), local_branch_menu); + // foreach (unowned var branch_name in local_branches) { + // local_branch_menu.append ( + // branch_name, + // GLib.Action.print_detailed_name ( + // FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, + // branch_name + // ) + // ); + // } + // } + + + // unowned var remote_branches = monitored_repo.get_remote_branches (); + // var remote_branch_submenu = new Menu (); + // var remote_branch_menu = new Menu (); + // if (remote_branches.length () > 0) { + // remote_branch_submenu.append_submenu (_("Remote"), remote_branch_menu); + // foreach (unowned var branch_name in remote_branches) { + // remote_branch_menu.append ( + // branch_name, + // GLib.Action.print_detailed_name ( + // FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, + // branch_name + // ) + // ); + // } + + + // } + + // var new_branch_item = new GLib.MenuItem ( + // _("New Branch…"), + // GLib.Action.print_detailed_name ( + // MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, + // file.path + // ) + // ); + + // new_branch_item.set_attribute_value ( + // "accel", + // Utils.get_accel_for_action ( + // GLib.Action.print_detailed_name ( + // MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, + // "" + // ) + // ) + // ); + + // GLib.Menu bottom_section = new GLib.Menu (); + // bottom_section.append_item (new_branch_item); + + // var menu = new GLib.Menu (); + // menu.append_section (null, local_branch_submenu); + // menu.append_section (null, remote_branch_submenu); + // menu.append_section (null, bottom_section); + + // var menu_item = new GLib.MenuItem.submenu (_("Branch"), menu); + // return menu_item; + // } + + // private void handle_checkout_local_branch_action (GLib.Variant? param) { + // var branch_name = param != null ? param.get_string () : ""; + // try { + // monitored_repo.change_local_branch (branch_name); + // } catch (GLib.Error e) { + // warning ("Failed to change branch to %s. %s", branch_name, e.message); + // } + // } + + // private void handle_checkout_remote_branch_action (GLib.Variant? param) { + // var branch_name = param != null ? param.get_string () : ""; + // if (branch_name == "") { + // return; + // } + + // try { + // monitored_repo.checkout_remote_branch (branch_name); + // } catch (GLib.Error e) { + // warning ("Failed to change branch to %s. %s", branch_name, e.message); + // } + // } public void update_item_status (FolderItem? start_folder) { if (monitored_repo == null) { @@ -496,6 +504,10 @@ namespace Scratch.FolderManager { } public unowned List get_branch_names () { + return is_git_repo ? monitored_repo.get_local_branch_names () : null; + } + + public unowned List get_branches () { return is_git_repo ? monitored_repo.get_local_branches () : null; } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 440e025c3b..659d137c5f 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -111,7 +111,7 @@ namespace Scratch { public const string ACTION_NEXT_TAB = "action-next-tab"; 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_BRANCH_ACTIONS = "action-branch-actions"; public const string ACTION_CLOSE_TAB = "action-close-tab"; public const string ACTION_CLOSE_TABS_TO_RIGHT = "action-close-tabs-to-right"; public const string ACTION_CLOSE_OTHER_TABS = "action-close-other-tabs"; @@ -169,7 +169,7 @@ 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_BRANCH_ACTIONS, action_branch_actions, "s" }, { ACTION_ADD_MARK, action_add_mark}, { ACTION_PREVIOUS_MARK, action_previous_mark}, { ACTION_NEXT_MARK, action_next_mark}, @@ -240,7 +240,7 @@ namespace Scratch { action_accelerators.set (ACTION_PREVIOUS_TAB, "Tab"); action_accelerators.set (ACTION_PREVIOUS_TAB, "Page_Up"); action_accelerators.set (ACTION_CLEAR_LINES, "K"); //Geany - action_accelerators.set (ACTION_NEW_BRANCH + "::", "B"); + action_accelerators.set (ACTION_BRANCH_ACTIONS + "::", "B"); action_accelerators.set (ACTION_ADD_MARK, "equal"); action_accelerators.set (ACTION_PREVIOUS_MARK, "Left"); action_accelerators.set (ACTION_NEXT_MARK, "Right"); @@ -1403,8 +1403,8 @@ namespace Scratch { doc.source_view.clear_selected_lines (); } - private void action_new_branch (SimpleAction action, Variant? param) { - folder_manager_view.new_branch (get_target_path_for_actions (param)); + private void action_branch_actions (SimpleAction action, Variant? param) { + folder_manager_view.branch_actions (get_target_path_for_actions (param)); } private void action_previous_mark () { diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index e48b874c01..2ac11a8ebe 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -158,7 +158,7 @@ namespace Scratch.Services { } //Returns an alphabetically sorted list of local branch names - public unowned List get_local_branches () { + public unowned List get_local_branch_names () { unowned List branches = null; try { var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.LOCAL); @@ -177,8 +177,26 @@ namespace Scratch.Services { return branches; } + public unowned List get_local_branches () { + unowned List branches = null; + try { + var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.LOCAL); + foreach (Ggit.Ref branch_ref in branch_enumerator) { + if (branch_ref is Ggit.Branch) { + branches.insert ( + (Ggit.Branch)branch_ref, + 0 + ); + } + } + } catch (Error e) { + warning ("Could not enumerate branches %s", e.message); + } + + return branches; + } //Returns an alphabetically sorted list of remote branch names - public unowned List get_remote_branches () { + public unowned List get_remote_branch_names () { unowned List branch_names = null; try { var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.REMOTE); diff --git a/src/meson.build b/src/meson.build index 27af837449..531ddd1e0d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,7 +23,8 @@ code_files = files( 'Dialogs/CloseProjectsConfirmationDialog.vala', 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', - 'Dialogs/NewBranchDialog.vala', + # 'Dialogs/NewBranchDialog.vala', + 'Dialogs/BranchActionDialog.vala', 'FolderManager/File.vala', 'FolderManager/FileItem.vala', 'FolderManager/FileView.vala', From da9904f4d29b3dd8257bec03e39fa7a692f054c6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 14:31:28 +0100 Subject: [PATCH 02/23] Cleanup, refactor, bind apply button sensitivity --- src/Dialogs/BranchActionDialog.vala | 165 +++++++++++------------ src/FolderManager/ProjectFolderItem.vala | 132 +++--------------- src/Services/MonitoredRepository.vala | 78 ++++------- 3 files changed, 127 insertions(+), 248 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index 90fdfc8796..c41b220697 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -4,7 +4,7 @@ * * Authored by: Jeremy Wootten */ -public enum Scratch.BranchAction { +public enum Scratch.BranchAction { CHECKOUT, CREATE, DELETE @@ -15,98 +15,97 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { public BranchAction action { get; set; } public string branch { get; set; } + public bool can_apply { get; private set; default = false; } + public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( - transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), - project: project, - primary_text: _("Perform branch action on project %s").printf ( - project.file.file.get_basename ()), - image_icon: new ThemedIcon ("git"), - buttons: Gtk.ButtonsType.CANCEL + project: project ); } construct { - assert (project.is_git_repo); - var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); - set_default_response ( Gtk.ResponseType.CANCEL); + transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + if (project.is_git_repo) { + primary_text = _("Perform branch action on project '%s'").printf ( + project.file.file.get_basename () + ); + image_icon = new ThemedIcon ("git"); + var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); + bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); + set_default_response ( Gtk.ResponseType.CANCEL); + + var branch_refs = project.get_all_branch_refs (); + var list_store = new ListStore (typeof (Ggit.Ref)); + foreach (var branch_ref in branch_refs) { + list_store.insert_sorted (branch_ref, branch_sort_func); + } + + var list_box = new Gtk.ListBox (); + list_box.bind_model (list_store, (obj) => { + var row = new Gtk.ListBoxRow (); + var label = new Gtk.Label (((Ggit.Ref)obj).get_shorthand ()) { + halign = START + }; + row.add (label); + return row; + }); - unowned var local_branches = project.get_branches (); + var search_entry = new Gtk.SearchEntry (); + list_box.set_filter_func ((row) => { + return (((Gtk.Label)(row.get_child ())).label.contains (search_entry.text)); + }); - var list_store = new ListStore (typeof (Ggit.Branch)); - foreach (Ggit.Branch branch in local_branches) { - list_store.insert_sorted (branch, (a, b) => { - return (((Ggit.Branch)a).get_name ()).collate (((Ggit.Branch)b).get_name ()); + list_box.row_activated.connect ((row) => { + search_entry.text = ((Gtk.Label)(row.get_child ())).label; }); - } - var list_box = new Gtk.ListBox (); - list_box.bind_model (list_store, (obj) => { - var row = new Gtk.ListBoxRow (); - var label = new Gtk.Label (((Ggit.Branch)obj).get_name ()) { - halign = START + search_entry.changed.connect (() => { + + // Checkout action + can_apply = project.has_branch_name (search_entry.text, null); + warning ("can apply %s", can_apply.to_string ()); + }); + + var scrolled_window = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = NEVER, + vscrollbar_policy = AUTOMATIC, + min_content_height = 200, + vexpand = true }; - row.add (label); - return row; - }); - - var search_entry = new Gtk.SearchEntry (); - list_box.set_filter_func ((row) => { - return (((Gtk.Label)(row.get_child ())).label.contains (search_entry.text)); - }); - - list_box.row_activated.connect ((row) => { - search_entry.text = ((Gtk.Label)(row.get_child ())).label; - }); - - var scrolled_window = new Gtk.ScrolledWindow (null, null) { - hscrollbar_policy = NEVER, - vscrollbar_policy = AUTOMATIC, - min_content_height = 100 - }; - scrolled_window.child = list_box; - - search_entry.changed.connect (() => { - list_box.invalidate_filter (); - }); - var search_box = new Gtk.Box (VERTICAL, 6); - search_box.add (search_entry); - search_box.add (scrolled_window); - - custom_bin.add (search_box); - // 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); - - // var apply_button = (Gtk.Button) add_button (_("Apply"), Gtk.ResponseType.APPLY); - // apply_button.can_default = true; - // apply_button.has_default = true; - // apply_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - - // new_branch_name_entry.bind_property ( - // "is-valid", apply_button, "sensitive", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE - // ); - - // 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; - // } - - // if (active_project.has_local_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; - // }); - - show_all (); + scrolled_window.child = list_box; + + search_entry.changed.connect (() => { + list_box.invalidate_filter (); + }); + var search_box = new Gtk.Box (VERTICAL, 6); + search_box.add (search_entry); + search_box.add (scrolled_window); + + custom_bin.add (search_box); + show_all (); + } else { + primary_text = _("'%s' is not a git repository").printf ( + project.file.file.get_basename () + ); + secondary_text = _("Unable to perform branch actions"); + image_icon = new ThemedIcon ("dialog-error"); + } + } + + private int branch_sort_func (Object oa, Object ob) { + + var a = (Ggit.Ref)oa; + var b = (Ggit.Ref)ob; + + if (a.is_branch () && !b.is_branch ()) { + return -1; + } + + if (b.is_branch () && !a.is_branch ()) { + return 1; + } + + return (a.get_shorthand ()).collate (b.get_shorthand ()); } } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index fb4d727c83..39af68b0a5 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -77,23 +77,11 @@ namespace Scratch.FolderManager { monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); notify["name"].connect (branch_or_name_changed); if (monitored_repo != null) { - // checkout_local_branch_action = new SimpleAction.stateful ( - // FileView.ACTION_CHECKOUT_LOCAL_BRANCH, - // GLib.VariantType.STRING, - // "" - // ); - // checkout_remote_branch_action = new SimpleAction.stateful ( - // FileView.ACTION_CHECKOUT_REMOTE_BRANCH, - // GLib.VariantType.STRING, - // "" - // ); monitored_repo.branch_changed.connect (branch_or_name_changed); monitored_repo.ignored_changed.connect ((deprioritize_git_ignored)); monitored_repo.file_status_change.connect (() => update_item_status (null)); monitored_repo.update_status_map (); monitored_repo.branch_changed (); - // checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action); - // checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); } } @@ -187,7 +175,6 @@ namespace Scratch.FolderManager { ) ); folder_actions_section.append_item (branch_action_item); - // folder_actions_section.append_item (create_submenu_for_branch ()); } var close_folder_item = new GLib.MenuItem ( @@ -328,98 +315,6 @@ namespace Scratch.FolderManager { return menu; } - // protected GLib.MenuItem create_submenu_for_branch () { - // // Ensures that action for relevant project is being used - // view.actions.add_action (checkout_local_branch_action); - // view.actions.add_action (checkout_remote_branch_action); - - // unowned var local_branches = monitored_repo.get_local_branches (); - // var local_branch_submenu = new Menu (); - // var local_branch_menu = new Menu (); - // if (local_branches.length () > 0) { - // local_branch_submenu.append_submenu (_("Local"), local_branch_menu); - // foreach (unowned var branch_name in local_branches) { - // local_branch_menu.append ( - // branch_name, - // GLib.Action.print_detailed_name ( - // FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, - // branch_name - // ) - // ); - // } - // } - - - // unowned var remote_branches = monitored_repo.get_remote_branches (); - // var remote_branch_submenu = new Menu (); - // var remote_branch_menu = new Menu (); - // if (remote_branches.length () > 0) { - // remote_branch_submenu.append_submenu (_("Remote"), remote_branch_menu); - // foreach (unowned var branch_name in remote_branches) { - // remote_branch_menu.append ( - // branch_name, - // GLib.Action.print_detailed_name ( - // FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, - // branch_name - // ) - // ); - // } - - - // } - - // var new_branch_item = new GLib.MenuItem ( - // _("New Branch…"), - // GLib.Action.print_detailed_name ( - // MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - // file.path - // ) - // ); - - // new_branch_item.set_attribute_value ( - // "accel", - // Utils.get_accel_for_action ( - // GLib.Action.print_detailed_name ( - // MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - // "" - // ) - // ) - // ); - - // GLib.Menu bottom_section = new GLib.Menu (); - // bottom_section.append_item (new_branch_item); - - // var menu = new GLib.Menu (); - // menu.append_section (null, local_branch_submenu); - // menu.append_section (null, remote_branch_submenu); - // menu.append_section (null, bottom_section); - - // var menu_item = new GLib.MenuItem.submenu (_("Branch"), menu); - // return menu_item; - // } - - // private void handle_checkout_local_branch_action (GLib.Variant? param) { - // var branch_name = param != null ? param.get_string () : ""; - // try { - // monitored_repo.change_local_branch (branch_name); - // } catch (GLib.Error e) { - // warning ("Failed to change branch to %s. %s", branch_name, e.message); - // } - // } - - // private void handle_checkout_remote_branch_action (GLib.Variant? param) { - // var branch_name = param != null ? param.get_string () : ""; - // if (branch_name == "") { - // return; - // } - - // try { - // monitored_repo.checkout_remote_branch (branch_name); - // } catch (GLib.Error e) { - // warning ("Failed to change branch to %s. %s", branch_name, e.message); - // } - // } - public void update_item_status (FolderItem? start_folder) { if (monitored_repo == null) { debug ("Ignore non-git folders"); @@ -503,18 +398,35 @@ namespace Scratch.FolderManager { } } - public unowned List get_branch_names () { - return is_git_repo ? monitored_repo.get_local_branch_names () : null; - } - public unowned List get_branches () { - return is_git_repo ? monitored_repo.get_local_branches () : null; + public Gee.ArrayList get_all_branch_refs () requires (is_git_repo) { + return monitored_repo.get_all_branch_refs (); } public bool has_local_branch_name (string name) { return is_git_repo ? monitored_repo.has_local_branch_name (name) : false; } + public bool has_remote_branch_name (string name) { + return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; + } + + public bool has_branch_name (string name, out bool? found_is_remote) { + var is_remote = false; + var found = false; + if (!has_local_branch_name (name)) { + found = has_remote_branch_name (name); + is_remote = found; + } else { + found = true; + } + + if (found_is_remote != null) { + found_is_remote = is_remote; + } + + return found; + } public string get_current_branch_name () { return is_git_repo ? monitored_repo.branch_name : ""; } diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 2ac11a8ebe..7511a1196d 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -68,7 +68,7 @@ namespace Scratch.Services { // Need to use nullable status in order to pass Flatpak CI. private Gee.HashMap file_status_map; - private List remote_branch_ref_list; + private Gee.ArrayList all_branch_refs; public Gee.Set> non_current_entries { owned get { @@ -101,7 +101,7 @@ namespace Scratch.Services { null ); - remote_branch_ref_list = new List (); + all_branch_refs = new Gee.ArrayList (); } public MonitoredRepository (Ggit.Repository _git_repo) { @@ -157,68 +157,28 @@ namespace Scratch.Services { return ""; } - //Returns an alphabetically sorted list of local branch names - public unowned List get_local_branch_names () { - unowned List branches = null; + public Gee.ArrayList get_all_branch_refs () { + all_branch_refs = new Gee.ArrayList (); try { var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.LOCAL); foreach (Ggit.Ref branch_ref in branch_enumerator) { - if (branch_ref is Ggit.Branch) { - branches.insert_sorted ( - ((Ggit.Branch)branch_ref).get_name (), - string.collate - ); - } + all_branch_refs.add (branch_ref); } - } catch (Error e) { - warning ("Could not enumerate branches %s", e.message); - } - - return branches; - } - - public unowned List get_local_branches () { - unowned List branches = null; - try { - var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.LOCAL); - foreach (Ggit.Ref branch_ref in branch_enumerator) { - if (branch_ref is Ggit.Branch) { - branches.insert ( - (Ggit.Branch)branch_ref, - 0 - ); - } - } - } catch (Error e) { - warning ("Could not enumerate branches %s", e.message); - } - - return branches; - } - //Returns an alphabetically sorted list of remote branch names - public unowned List get_remote_branch_names () { - unowned List branch_names = null; - try { - var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.REMOTE); + branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.REMOTE); foreach (Ggit.Ref branch_ref in branch_enumerator) { var remote_name = branch_ref.get_shorthand (); if (!remote_name.has_suffix ("HEAD") && !has_local_branch_name (remote_name.substring (ORIGIN_PREFIX.length))) { - branch_names.insert_sorted ( - branch_ref.get_shorthand (), - string.collate - ); + all_branch_refs.add (branch_ref); } - - remote_branch_ref_list.append (branch_ref); } } catch (Error e) { warning ("Could not enumerate local branches %s", e.message); } - return branch_names; + return all_branch_refs; } public bool has_local_branch_name (string name) { @@ -230,6 +190,15 @@ namespace Scratch.Services { return false; } + public bool has_remote_branch_name (string name) { + try { + git_repo.lookup_branch (name, Ggit.BranchType.REMOTE); + return true; + } catch (Error e) {} + + return false; + } + public bool is_valid_new_local_branch_name (string new_name) { if (!Ggit.Ref.is_valid_name ("refs/heads/" + new_name) || has_local_branch_name (new_name) ) { @@ -249,16 +218,15 @@ namespace Scratch.Services { public void checkout_remote_branch (string target_shorthand) throws Error requires (target_shorthand.has_prefix (ORIGIN_PREFIX)) { - Ggit.Ref? branch_ref; + Ggit.Ref? branch_ref = null; //Assume list is up to date as this is called from context menu - unowned var list_pointer = remote_branch_ref_list.first (); - while (list_pointer.data != null && - list_pointer.data.get_shorthand () != target_shorthand) { - - list_pointer = list_pointer.next; + foreach (var bref in all_branch_refs) { + if (bref.get_shorthand () == target_shorthand) { + branch_ref = bref; + break; + } } - branch_ref = list_pointer.data; if (branch_ref == null) { var dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("Remote Branch '%s' not found").printf (target_shorthand), From c39c7bf8c12fe10e1697e5e8a991c611d7cd836b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 17:20:42 +0100 Subject: [PATCH 03/23] Do not bind model; private Row class --- src/Dialogs/BranchActionDialog.vala | 67 ++++++++++++++++++----------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index c41b220697..daf7392d50 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -36,35 +36,28 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { set_default_response ( Gtk.ResponseType.CANCEL); var branch_refs = project.get_all_branch_refs (); - var list_store = new ListStore (typeof (Ggit.Ref)); + var list_box = new Gtk.ListBox (); foreach (var branch_ref in branch_refs) { - list_store.insert_sorted (branch_ref, branch_sort_func); + var row = new BranchNameRow (branch_ref); + list_box.add (row); } - var list_box = new Gtk.ListBox (); - list_box.bind_model (list_store, (obj) => { - var row = new Gtk.ListBoxRow (); - var label = new Gtk.Label (((Ggit.Ref)obj).get_shorthand ()) { - halign = START - }; - row.add (label); - return row; - }); - var search_entry = new Gtk.SearchEntry (); + list_box.set_filter_func ((row) => { - return (((Gtk.Label)(row.get_child ())).label.contains (search_entry.text)); + return (((BranchNameRow)row).name.contains (search_entry.text)); }); + list_box.set_sort_func (listbox_sort_func); + list_box.row_activated.connect ((row) => { - search_entry.text = ((Gtk.Label)(row.get_child ())).label; + search_entry.text = ((BranchNameRow)row).name; }); search_entry.changed.connect (() => { - + list_box.invalidate_filter (); // Checkout action can_apply = project.has_branch_name (search_entry.text, null); - warning ("can apply %s", can_apply.to_string ()); }); var scrolled_window = new Gtk.ScrolledWindow (null, null) { @@ -83,7 +76,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { search_box.add (scrolled_window); custom_bin.add (search_box); - show_all (); + custom_bin.show_all (); } else { primary_text = _("'%s' is not a git repository").printf ( project.file.file.get_basename () @@ -93,19 +86,45 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - private int branch_sort_func (Object oa, Object ob) { + private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { + var a = (BranchNameRow)rowa; + var b = (BranchNameRow)rowb; - var a = (Ggit.Ref)oa; - var b = (Ggit.Ref)ob; + if (a.is_remote && !b.is_remote) { + return 1; + } - if (a.is_branch () && !b.is_branch ()) { + if (b.is_remote && !a.is_remote) { return -1; } - if (b.is_branch () && !a.is_branch ()) { - return 1; + return (a.name.collate (b.name)); + } + + private class BranchNameRow : Gtk.ListBoxRow { + public Ggit.Ref bref { get; construct; } + public bool is_remote { get; private set; } + public string name { + get { + return label.label; + } + } + + private Gtk.Label label; + + public BranchNameRow (Ggit.Ref bref) { + Object ( + bref: bref + ); } - return (a.get_shorthand ()).collate (b.get_shorthand ()); + construct { + is_remote = !bref.is_branch (); + label = new Gtk.Label (bref.get_shorthand ()) { + halign = START + }; + + this.child = label; + } } } From 4a90587ca64fdfc3aa8d43ec327bd8b23adc829c Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 17:59:10 +0100 Subject: [PATCH 04/23] Include headers --- src/Dialogs/BranchActionDialog.vala | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index daf7392d50..700bd95459 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -17,6 +17,9 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { public bool can_apply { get; private set; default = false; } + private Gtk.Label local_header; + private Gtk.Label remote_header; + public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( project: project @@ -33,8 +36,9 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { image_icon = new ThemedIcon ("git"); var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); - set_default_response ( Gtk.ResponseType.CANCEL); + local_header = new Granite.HeaderLabel (_("Local Branches")); + remote_header = new Granite.HeaderLabel (_("Remote Branches")); var branch_refs = project.get_all_branch_refs (); var list_box = new Gtk.ListBox (); foreach (var branch_ref in branch_refs) { @@ -49,6 +53,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { }); list_box.set_sort_func (listbox_sort_func); + list_box.set_header_func (listbox_header_func); list_box.row_activated.connect ((row) => { search_entry.text = ((BranchNameRow)row).name; @@ -101,6 +106,18 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { return (a.name.collate (b.name)); } + private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { + var a = (BranchNameRow)row; + var b = (BranchNameRow)row_before; + if (b == null && a.get_header () != local_header) { + a.set_header (local_header); + } else if (a.is_remote && !b.is_remote && a.get_header () != remote_header) { + a.set_header (remote_header); + } else { + a.set_header (null); + } + } + private class BranchNameRow : Gtk.ListBoxRow { public Ggit.Ref bref { get; construct; } public bool is_remote { get; private set; } @@ -121,7 +138,8 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { construct { is_remote = !bref.is_branch (); label = new Gtk.Label (bref.get_shorthand ()) { - halign = START + halign = START, + margin_start = 24 }; this.child = label; From a5849533c11b385be1a5f4ed319e61c755d527b6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 19:23:36 +0100 Subject: [PATCH 05/23] Use Stack and StackSidebar for more actions --- src/Dialogs/BranchActionDialog.vala | 55 +++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index 700bd95459..6d9edb6134 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -6,8 +6,12 @@ */ public enum Scratch.BranchAction { CHECKOUT, - CREATE, - DELETE + COMMIT, + PUSH, + PULL, + MERGE, + DELETE, + CREATE } public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { @@ -19,6 +23,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { private Gtk.Label local_header; private Gtk.Label remote_header; + private Gtk.Stack stack; public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( @@ -80,7 +85,25 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { search_box.add (search_entry); search_box.add (scrolled_window); - custom_bin.add (search_box); + stack = new Gtk.Stack (); + stack.add_titled (search_box, BranchAction.CHECKOUT.to_string (), _("Checkout")); + stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); + stack.add_titled (new Gtk.Label (_("Push not implemented yet")), BranchAction.PUSH.to_string (), _("Push")); + stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); + stack.add_titled (new Gtk.Label (_("Merge not implemented yet")), BranchAction.MERGE.to_string (), _("Merge")); + stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); + stack.add_titled (new Gtk.Label (_("Create not implemented yet")), BranchAction.CREATE.to_string (), _("Create")); + + + var sidebar = new Gtk.StackSidebar () { + stack = stack + }; + + var content_box = new Gtk.Box (HORIZONTAL, 12); + content_box.add (sidebar); + content_box.add (stack); + + custom_bin.add (content_box); custom_bin.show_all (); } else { primary_text = _("'%s' is not a git repository").printf ( @@ -91,6 +114,13 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } + private void on_toggle_button_toggled (Gtk.Widget src) { + var action_button = (ActionRadioButton)src; + if (action_button.active) { + stack.set_visible_child_name (action_button.branch_action.to_string ()); + } + } + private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { var a = (BranchNameRow)rowa; var b = (BranchNameRow)rowb; @@ -145,4 +175,23 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { this.child = label; } } + + private class ActionRadioButton : Gtk.RadioButton { + public BranchAction branch_action { get; construct; } + + public ActionRadioButton (BranchAction action, Gtk.RadioButton? sibling, string text) { + Object ( + branch_action: action + ); + + join_group (sibling); + label = text; + } + + construct { + halign = Gtk.Align.START; + valign = Gtk.Align.START; + vexpand = true; + } + } } From d9392bac7eed38d82c7d3ef8a4727a0381f8e446 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 20:52:56 +0100 Subject: [PATCH 06/23] Move stuff in to private classes --- src/Dialogs/BranchActionDialog.vala | 203 +++++++++++++++------------- 1 file changed, 109 insertions(+), 94 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index 6d9edb6134..59d9a0e226 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -19,11 +19,8 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { public BranchAction action { get; set; } public string branch { get; set; } - public bool can_apply { get; private set; default = false; } - - private Gtk.Label local_header; - private Gtk.Label remote_header; private Gtk.Stack stack; + protected bool can_apply { get; set; default = false; } public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( @@ -41,52 +38,10 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { image_icon = new ThemedIcon ("git"); var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); - - local_header = new Granite.HeaderLabel (_("Local Branches")); - remote_header = new Granite.HeaderLabel (_("Remote Branches")); - var branch_refs = project.get_all_branch_refs (); - var list_box = new Gtk.ListBox (); - foreach (var branch_ref in branch_refs) { - var row = new BranchNameRow (branch_ref); - list_box.add (row); - } - - var search_entry = new Gtk.SearchEntry (); - - list_box.set_filter_func ((row) => { - return (((BranchNameRow)row).name.contains (search_entry.text)); - }); - - list_box.set_sort_func (listbox_sort_func); - list_box.set_header_func (listbox_header_func); - - list_box.row_activated.connect ((row) => { - search_entry.text = ((BranchNameRow)row).name; - }); - - search_entry.changed.connect (() => { - list_box.invalidate_filter (); - // Checkout action - can_apply = project.has_branch_name (search_entry.text, null); - }); - - var scrolled_window = new Gtk.ScrolledWindow (null, null) { - hscrollbar_policy = NEVER, - vscrollbar_policy = AUTOMATIC, - min_content_height = 200, - vexpand = true - }; - scrolled_window.child = list_box; - - search_entry.changed.connect (() => { - list_box.invalidate_filter (); - }); - var search_box = new Gtk.Box (VERTICAL, 6); - search_box.add (search_entry); - search_box.add (scrolled_window); - stack = new Gtk.Stack (); - stack.add_titled (search_box, BranchAction.CHECKOUT.to_string (), _("Checkout")); + var checkout_page = new BranchCheckoutPage (this); + stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); + stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); stack.add_titled (new Gtk.Label (_("Push not implemented yet")), BranchAction.PUSH.to_string (), _("Push")); stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); @@ -94,7 +49,6 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); stack.add_titled (new Gtk.Label (_("Create not implemented yet")), BranchAction.CREATE.to_string (), _("Create")); - var sidebar = new Gtk.StackSidebar () { stack = stack }; @@ -114,44 +68,124 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - private void on_toggle_button_toggled (Gtk.Widget src) { - var action_button = (ActionRadioButton)src; - if (action_button.active) { - stack.set_visible_child_name (action_button.branch_action.to_string ()); + private class BranchCheckoutPage : Gtk.Box { + public BranchActionDialog dialog { get; construct; } + public BranchCheckoutPage (BranchActionDialog dialog) { + Object ( + dialog: dialog + ); + } + + construct { + var list_box = new BranchListBox (dialog, true); + add (list_box); } } - private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { - var a = (BranchNameRow)rowa; - var b = (BranchNameRow)rowb; + private class BranchListBox : Gtk.Bin { + public string text { + get { + return search_entry.text; + } + } + + public bool show_remotes { get; construct;} + public BranchActionDialog dialog { get; construct;} + + private Gtk.ListBox list_box; + private Gtk.SearchEntry search_entry; + private Gtk.Label local_header; + private Gtk.Label remote_header; + + public BranchListBox (BranchActionDialog dialog, bool show_remotes) { + Object ( + dialog: dialog, + show_remotes: show_remotes + ); + } + + construct { + list_box = new Gtk.ListBox (); + var scrolled_window = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = NEVER, + vscrollbar_policy = AUTOMATIC, + min_content_height = 200, + vexpand = true + }; + scrolled_window.child = list_box; + search_entry = new Gtk.SearchEntry (); + var box = new Gtk.Box (VERTICAL, 6); + box.add (search_entry); + box.add (scrolled_window); + child = box; + + local_header = new Granite.HeaderLabel (_("Local Branches")); + remote_header = new Granite.HeaderLabel (_("Remote Branches")); + var branch_refs = dialog.project.get_all_branch_refs (); + foreach (var branch_ref in branch_refs) { + if (branch_ref.is_branch () || show_remotes) { + var row = new BranchNameRow (branch_ref); + list_box.add (row); + } + } + list_box.set_sort_func (listbox_sort_func); + list_box.set_header_func (listbox_header_func); + list_box.row_activated.connect ((listboxrow) => { + search_entry.text = ((BranchNameRow)(listboxrow.get_child ())).branch_name; + }); + list_box.set_filter_func ((listboxrow) => { + return (((BranchNameRow)(listboxrow.get_child ())).branch_name.contains (search_entry.text)); + }); + search_entry.changed.connect (() => { + list_box.invalidate_filter (); + // Checkout action + dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); + }); + } - if (a.is_remote && !b.is_remote) { - return 1; + public void set_filter_func (Gtk.ListBoxFilterFunc f) { + list_box.set_filter_func (f); } - if (b.is_remote && !a.is_remote) { - return -1; + private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { + var a = (BranchNameRow)(rowa.get_child ()); + var b = (BranchNameRow)(rowb.get_child ()); + + if (a.is_remote && !b.is_remote) { + return 1; + } + + if (b.is_remote && !a.is_remote) { + return -1; + } + + return (a.branch_name.collate (b.branch_name)); } - return (a.name.collate (b.name)); - } + private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { + if (row_before == null) { + if (row.get_header () != local_header) { + row.set_header (local_header); + } - private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { - var a = (BranchNameRow)row; - var b = (BranchNameRow)row_before; - if (b == null && a.get_header () != local_header) { - a.set_header (local_header); - } else if (a.is_remote && !b.is_remote && a.get_header () != remote_header) { - a.set_header (remote_header); - } else { - a.set_header (null); + return; + } + + var a = (BranchNameRow)(row.get_child ()); + var b = (BranchNameRow)(row_before.get_child ()); + + if (a.is_remote && !b.is_remote && row.get_header () != remote_header) { + row.set_header (remote_header); + } else { + row.set_header (null); + } } } - private class BranchNameRow : Gtk.ListBoxRow { + private class BranchNameRow : Gtk.Box { public Ggit.Ref bref { get; construct; } public bool is_remote { get; private set; } - public string name { + public string branch_name { get { return label.label; } @@ -172,26 +206,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { margin_start = 24 }; - this.child = label; - } - } - - private class ActionRadioButton : Gtk.RadioButton { - public BranchAction branch_action { get; construct; } - - public ActionRadioButton (BranchAction action, Gtk.RadioButton? sibling, string text) { - Object ( - branch_action: action - ); - - join_group (sibling); - label = text; - } - - construct { - halign = Gtk.Align.START; - valign = Gtk.Align.START; - vexpand = true; + add (label); } } } From 9e86c5f956a016febc37d9489e826af01b517d53 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 21 Jun 2025 21:15:21 +0100 Subject: [PATCH 07/23] Use common interface for stack pages --- src/Dialogs/BranchActionDialog.vala | 41 +++++++++++++++++++++++++---- src/FolderManager/FileView.vala | 1 + 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index 59d9a0e226..e7c844644a 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -14,14 +14,30 @@ public enum Scratch.BranchAction { CREATE } +public interface Scratch.BranchActionPage : Gtk.Widget { + public abstract BranchAction action { get; } + public abstract string branch { get; } +} + public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { - public FolderManager.ProjectFolderItem project { get; construct; } - public BranchAction action { get; set; } - public string branch { get; set; } + public BranchAction action { + get { + return ((BranchActionPage)stack.get_visible_child ()).action; + } + } + + public string branch { + get { + return ((BranchActionPage)stack.get_visible_child ()).branch; + } + } + + private Gtk.Stack stack; protected bool can_apply { get; set; default = false; } + public FolderManager.ProjectFolderItem project { get; construct; } public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( project: project @@ -68,8 +84,23 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - private class BranchCheckoutPage : Gtk.Box { + private class BranchCheckoutPage : Gtk.Box, BranchActionPage { + public BranchAction action { + get { + return BranchAction.CHECKOUT; + } + } + + public string branch { + get { + return list_box.text; + } + } + public BranchActionDialog dialog { get; construct; } + + private BranchListBox list_box; + public BranchCheckoutPage (BranchActionDialog dialog) { Object ( dialog: dialog @@ -77,7 +108,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } construct { - var list_box = new BranchListBox (dialog, true); + list_box = new BranchListBox (dialog, true); add (list_box); } } diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 86cecdf27a..2c9b158455 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -228,6 +228,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane var dialog = new Dialogs.BranchActionDialog (active_project); dialog.response.connect ((res) => { if (res == Gtk.ResponseType.APPLY) { +warning ("Applying"); var action = dialog.action; var branch = dialog.branch; perform_branch_action.begin (active_project, action, branch); From f5db6261006ade954615e476e7d25d52549fc419 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 22 Jun 2025 10:55:38 +0100 Subject: [PATCH 08/23] Reimplement checkout branch; refactor --- src/Dialogs/BranchActionDialog.vala | 40 +++++++++++++++--------- src/FolderManager/FileView.vala | 32 +++++++++++++++---- src/FolderManager/ProjectFolderItem.vala | 10 ++++++ 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index e7c844644a..2ff393ba99 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -16,7 +16,7 @@ public enum Scratch.BranchAction { public interface Scratch.BranchActionPage : Gtk.Widget { public abstract BranchAction action { get; } - public abstract string branch { get; } + public abstract Ggit.Ref branch_ref { get; } } public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { @@ -26,14 +26,12 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - public string branch { + public Ggit.Ref branch_ref { get { - return ((BranchActionPage)stack.get_visible_child ()).branch; + return ((BranchActionPage)stack.get_visible_child ()).branch_ref; } } - - private Gtk.Stack stack; protected bool can_apply { get; set; default = false; } @@ -91,9 +89,9 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - public string branch { + public Ggit.Ref branch_ref { get { - return list_box.text; + return list_box.get_selected_row ().bref; } } @@ -162,10 +160,10 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { list_box.set_sort_func (listbox_sort_func); list_box.set_header_func (listbox_header_func); list_box.row_activated.connect ((listboxrow) => { - search_entry.text = ((BranchNameRow)(listboxrow.get_child ())).branch_name; + search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; }); list_box.set_filter_func ((listboxrow) => { - return (((BranchNameRow)(listboxrow.get_child ())).branch_name.contains (search_entry.text)); + return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text)); }); search_entry.changed.connect (() => { list_box.invalidate_filter (); @@ -174,13 +172,25 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { }); } + public BranchNameRow? get_selected_row () { + int index = 0; + var row = list_box.get_row_at_index (index); + while (row != null && + ((BranchNameRow)row).branch_name != search_entry.text) { + + row = list_box.get_row_at_index (++index); + } + + return (BranchNameRow)row; + } + public void set_filter_func (Gtk.ListBoxFilterFunc f) { list_box.set_filter_func (f); } private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { - var a = (BranchNameRow)(rowa.get_child ()); - var b = (BranchNameRow)(rowb.get_child ()); + var a = (BranchNameRow)(rowa); + var b = (BranchNameRow)(rowb); if (a.is_remote && !b.is_remote) { return 1; @@ -202,8 +212,8 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { return; } - var a = (BranchNameRow)(row.get_child ()); - var b = (BranchNameRow)(row_before.get_child ()); + var a = (BranchNameRow)row; + var b = (BranchNameRow)row_before; if (a.is_remote && !b.is_remote && row.get_header () != remote_header) { row.set_header (remote_header); @@ -213,7 +223,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - private class BranchNameRow : Gtk.Box { + private class BranchNameRow : Gtk.ListBoxRow { public Ggit.Ref bref { get; construct; } public bool is_remote { get; private set; } public string branch_name { @@ -237,7 +247,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { margin_start = 24 }; - add (label); + child = label; } } } diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 2c9b158455..aebeee14a4 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -216,7 +216,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } public void branch_actions (string path) { - warning ("branch actions"); // Must only carry out branch actions on active project so switch if necessary. //TODO Warn before switching active project? var active_project = set_active_project (path); @@ -228,10 +227,9 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane var dialog = new Dialogs.BranchActionDialog (active_project); dialog.response.connect ((res) => { if (res == Gtk.ResponseType.APPLY) { -warning ("Applying"); var action = dialog.action; - var branch = dialog.branch; - perform_branch_action.begin (active_project, action, branch); + var branch_ref = dialog.branch_ref; + perform_branch_action.begin (active_project, action, branch_ref); } dialog.destroy (); @@ -240,8 +238,30 @@ warning ("Applying"); dialog.present (); } - private async void perform_branch_action (ProjectFolderItem project, BranchAction action, string branch) { - warning ("perform action"); + private async void perform_branch_action ( + ProjectFolderItem project, + BranchAction action, + Ggit.Ref branch_ref + ) { + switch (action) { + case CHECKOUT: + project.checkout_branch_ref (branch_ref); + break; + case COMMIT: + break; + case PUSH: + break; + case PULL: + break; + case MERGE: + break; + case DELETE: + break; + case CREATE: + break; + default: + assert_not_reached (); + } } private unowned Code.Widgets.SourceList.Item? find_path ( diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 39af68b0a5..35ac8f6e1e 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -374,6 +374,16 @@ namespace Scratch.FolderManager { }); } + public void checkout_branch_ref (Ggit.Ref branch_ref) { + if (branch_ref.is_branch ()) { + var branch_name = ((Ggit.Branch)branch_ref).get_name (); + monitored_repo.change_local_branch (branch_name); + } else { + var target_shorthand = branch_ref.get_shorthand (); + monitored_repo.checkout_remote_branch (target_shorthand); + } + } + public void new_branch (string branch_name) { try { if (monitored_repo.head_is_branch) { From ee18b67c3c6be3130424795b9b32c76cde371796 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 22 Jun 2025 19:25:18 +0100 Subject: [PATCH 09/23] Start to implement recent branches --- src/Dialogs/BranchActionDialog.vala | 44 +++++++++++++++++------- src/FolderManager/ProjectFolderItem.vala | 5 ++- src/Services/MonitoredRepository.vala | 16 +++++++++ 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala index 2ff393ba99..595e1a3852 100644 --- a/src/Dialogs/BranchActionDialog.vala +++ b/src/Dialogs/BranchActionDialog.vala @@ -125,6 +125,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { private Gtk.SearchEntry search_entry; private Gtk.Label local_header; private Gtk.Label remote_header; + private Gtk.Label recent_header; public BranchListBox (BranchActionDialog dialog, bool show_remotes) { Object ( @@ -148,12 +149,17 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { box.add (scrolled_window); child = box; + recent_header = new Granite.HeaderLabel (_("Recent Branches")); local_header = new Granite.HeaderLabel (_("Local Branches")); remote_header = new Granite.HeaderLabel (_("Remote Branches")); var branch_refs = dialog.project.get_all_branch_refs (); + foreach (var branch_ref in branch_refs) { if (branch_ref.is_branch () || show_remotes) { var row = new BranchNameRow (branch_ref); + if (dialog.project.is_recent_ref (branch_ref)) { + row.is_recent = true; + } list_box.add (row); } } @@ -167,6 +173,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { }); search_entry.changed.connect (() => { list_box.invalidate_filter (); + list_box.invalidate_headers (); // Checkout action dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); }); @@ -184,19 +191,20 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { return (BranchNameRow)row; } - public void set_filter_func (Gtk.ListBoxFilterFunc f) { - list_box.set_filter_func (f); - } private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { var a = (BranchNameRow)(rowa); var b = (BranchNameRow)(rowb); - if (a.is_remote && !b.is_remote) { + if (a.is_recent && !b.is_recent) { + return -1; + } else if (b.is_recent && !a.is_recent) { return 1; } - if (b.is_remote && !a.is_remote) { + if (a.is_remote && !b.is_remote) { + return 1; + } else if (b.is_remote && !a.is_remote) { return -1; } @@ -204,21 +212,31 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { + var a = (BranchNameRow)row; + a.set_header (null); if (row_before == null) { - if (row.get_header () != local_header) { - row.set_header (local_header); + if (a.is_recent && a.get_header () != recent_header) { + a.set_header (recent_header); + } else if (!a.is_remote && a.get_header () != local_header) { + a.set_header (local_header); + } else { + a.set_header (remote_header); } return; } - var a = (BranchNameRow)row; + var b = (BranchNameRow)row_before; - if (a.is_remote && !b.is_remote && row.get_header () != remote_header) { - row.set_header (remote_header); - } else { - row.set_header (null); + if (b.is_recent && !a.is_recent) { + if (!a.is_remote && a.get_header () != local_header) { + a.set_header (local_header); + } else if (a.is_remote && a.get_header () != remote_header) { + a.set_header (remote_header); + } + } else if (!b.is_remote && a.is_remote && a.get_header () != remote_header) { + a.set_header (remote_header); } } } @@ -226,6 +244,8 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { private class BranchNameRow : Gtk.ListBoxRow { public Ggit.Ref bref { get; construct; } public bool is_remote { get; private set; } + public bool is_recent { get; set; default = false; } + public string branch_name { get { return label.label; diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 35ac8f6e1e..dbb9cd75a8 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -408,6 +408,9 @@ namespace Scratch.FolderManager { } } + public bool is_recent_ref (Ggit.Ref bref) { + return monitored_repo.is_recent_ref (bref); + } public Gee.ArrayList get_all_branch_refs () requires (is_git_repo) { return monitored_repo.get_all_branch_refs (); @@ -421,7 +424,7 @@ namespace Scratch.FolderManager { return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; } - public bool has_branch_name (string name, out bool? found_is_remote) { + public bool has_branch_name (string name, out bool? found_is_remote = null) { var is_remote = false; var found = false; if (!has_local_branch_name (name)) { diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 7511a1196d..ca7ba0969f 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -69,6 +69,7 @@ namespace Scratch.Services { private Gee.HashMap file_status_map; private Gee.ArrayList all_branch_refs; + private Gee.HashSet recently_used_branches; public Gee.Set> non_current_entries { owned get { @@ -102,6 +103,15 @@ namespace Scratch.Services { ); all_branch_refs = new Gee.ArrayList (); + recently_used_branches = new Gee.HashSet ( + (v) => { + return v.get_name ().hash (); + }, + + (va, vb) => { + return va.get_name () == vb.get_name (); + } + ); } public MonitoredRepository (Ggit.Repository _git_repo) { @@ -181,6 +191,10 @@ namespace Scratch.Services { return all_branch_refs; } + public bool is_recent_ref (Ggit.Ref bref) { + return bref.is_branch () && recently_used_branches.contains ((Ggit.Branch)bref); + } + public bool has_local_branch_name (string name) { try { git_repo.lookup_branch (name, Ggit.BranchType.LOCAL); @@ -265,6 +279,8 @@ namespace Scratch.Services { git_repo.checkout_head (options); branch_name = new_branch_name; + //TODO limit recent to ? branches and persist + recently_used_branches.add (new_head_branch); } catch (Error e) { var dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("An error occurred while checking out the requested branch"), From b32a7c899014509747ddb07b17185bb3b94ea37c Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 15:02:06 +0100 Subject: [PATCH 10/23] Coding improvements --- src/FolderManager/ProjectFolderItem.vala | 29 +++-- src/Services/MonitoredRepository.vala | 135 +++++++++++++++-------- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index dbb9cd75a8..649040c146 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -374,13 +374,22 @@ namespace Scratch.FolderManager { }); } - public void checkout_branch_ref (Ggit.Ref branch_ref) { + public bool checkout_branch_ref (Ggit.Ref branch_ref) { if (branch_ref.is_branch ()) { - var branch_name = ((Ggit.Branch)branch_ref).get_name (); - monitored_repo.change_local_branch (branch_name); + var branch_name = branch_ref.get_name (); + if (branch_name != null) { + return monitored_repo.change_local_branch (branch_name); + } else { + return false; + } + } else { var target_shorthand = branch_ref.get_shorthand (); - monitored_repo.checkout_remote_branch (target_shorthand); + if (target_shorthand != null) { + return monitored_repo.checkout_remote_branch (target_shorthand); + } else { + return false; + } } } @@ -424,22 +433,20 @@ namespace Scratch.FolderManager { return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; } - public bool has_branch_name (string name, out bool? found_is_remote = null) { - var is_remote = false; + public bool has_branch_name (string name, out bool found_is_remote) { + found_is_remote = false; + // var is_remote = false; var found = false; if (!has_local_branch_name (name)) { found = has_remote_branch_name (name); - is_remote = found; + found_is_remote = found; } else { found = true; } - if (found_is_remote != null) { - found_is_remote = is_remote; - } - return found; } + public string get_current_branch_name () { return is_git_repo ? monitored_repo.branch_name : ""; } diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index ca7ba0969f..1b5ed620cb 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -69,7 +69,7 @@ namespace Scratch.Services { private Gee.HashMap file_status_map; private Gee.ArrayList all_branch_refs; - private Gee.HashSet recently_used_branches; + private Gee.HashSet recently_used_branches; public Gee.Set> non_current_entries { owned get { @@ -103,7 +103,7 @@ namespace Scratch.Services { ); all_branch_refs = new Gee.ArrayList (); - recently_used_branches = new Gee.HashSet ( + recently_used_branches = new Gee.HashSet ( (v) => { return v.get_name ().hash (); }, @@ -192,7 +192,7 @@ namespace Scratch.Services { } public bool is_recent_ref (Ggit.Ref bref) { - return bref.is_branch () && recently_used_branches.contains ((Ggit.Branch)bref); + return bref.is_branch () && recently_used_branches.contains (bref); } public bool has_local_branch_name (string name) { @@ -222,19 +222,27 @@ namespace Scratch.Services { return true; } - public void change_local_branch (string new_branch_name) throws Error + public bool change_local_branch (string new_branch_name) requires (!new_branch_name.has_prefix (ORIGIN_PREFIX)) { - var branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); - checkout_branch (branch); + Ggit.Branch branch; + try { + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + } catch (Error e) { + warning ("failed to lookup branch %s", new_branch_name); + return false; + } + + return checkout_branch (branch); } - public void checkout_remote_branch (string target_shorthand) throws Error + public bool checkout_remote_branch (string target_shorthand) requires (target_shorthand.has_prefix (ORIGIN_PREFIX)) { Ggit.Ref? branch_ref = null; //Assume list is up to date as this is called from context menu foreach (var bref in all_branch_refs) { + // Can assume that all brefs in this list have a non-null shorthand if (bref.get_shorthand () == target_shorthand) { branch_ref = bref; break; @@ -252,26 +260,74 @@ namespace Scratch.Services { dialog.response.connect (() => {dialog.destroy ();}); dialog.present (); - return; + return false; + } + + Ggit.Object commit; + try { + commit = branch_ref.lookup (); + } catch (Error e) { + warning ("Failed to lookup commit. %s", e.message); + return false; } - var commit = branch_ref.lookup (); var local_name = target_shorthand.substring (ORIGIN_PREFIX.length); - var local_branch = git_repo.create_branch (local_name, commit, NONE) as Ggit.Branch; - checkout_branch (local_branch); - local_branch.set_upstream (target_shorthand); + Ggit.Branch local_branch; + try { + local_branch = git_repo.create_branch (local_name, commit, NONE); + } catch (Error e) { + warning ("Failed to create branch. %s", e.message); + return false; + } + + if (checkout_branch (local_branch)) { + try { + local_branch.set_upstream (target_shorthand); + return true; + } catch (Error e) { + warning ("Failed to set upstream. %s", e.message); + } + } + + return false; } - private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { - var new_branch_name = ""; - try { - new_branch_name = new_head_branch.get_name (); - if (confirm && has_uncommitted) { - confirm_checkout_branch (new_head_branch); - return; + private bool checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { + var new_branch_name = ((Ggit.Ref) new_head_branch).get_name (); + if (new_branch_name == null) { + return false; + } + + if (confirm && has_uncommitted) { + var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + // var new_branch_name = new_head_branch.get_name (); + string project_diff; + try { + project_diff = get_project_diff (); + } catch (Error e) { + warning ("Failed to get project diff. %s", e.message); + return false; } - git_repo.set_head (((Ggit.Ref) new_head_branch).get_name ()); + var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog ( + parent, + new_branch_name, + project_diff + ); + dialog.response.connect ((res) => { + dialog.destroy (); + if (res == Gtk.ResponseType.ACCEPT) { + checkout_branch (new_head_branch, false); + } + }); + + dialog.present (); + // confirm_checkout_branch (new_head_branch); + return false; + } + + try { + git_repo.set_head (new_branch_name); var options = new Ggit.CheckoutOptions () { //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) strategy = Ggit.CheckoutStrategy.FORCE @@ -280,7 +336,7 @@ namespace Scratch.Services { git_repo.checkout_head (options); branch_name = new_branch_name; //TODO limit recent to ? branches and persist - recently_used_branches.add (new_head_branch); + recently_used_branches.add ((Ggit.Ref)new_head_branch); } catch (Error e) { var dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("An error occurred while checking out the requested branch"), @@ -288,33 +344,24 @@ namespace Scratch.Services { "dialog-warning" ); - dialog.run (); - dialog.destroy (); + dialog.response.connect (dialog.destroy); + dialog.present (); + return false; } - } - - private void confirm_checkout_branch (Ggit.Branch new_head_branch) throws Error { - var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); - var new_branch_name = new_head_branch.get_name (); - var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog ( - parent, - new_branch_name, - get_project_diff () - ); - dialog.response.connect ((res) => { - dialog.destroy (); - if (res == Gtk.ResponseType.ACCEPT) { - checkout_branch (new_head_branch, false); - } - }); - dialog.present (); + return true; } - public void create_new_branch (string name) throws Error { - Ggit.Object git_object = git_repo.get_head ().lookup (); - var new_branch = git_repo.create_branch (name, git_object, Ggit.CreateFlags.NONE); - git_repo.set_head (((Ggit.Ref)new_branch).get_name ()); + public bool create_new_branch (string name) { + try { + Ggit.Object git_object = git_repo.get_head ().lookup (); + var new_branch = git_repo.create_branch (name, git_object, Ggit.CreateFlags.NONE); + git_repo.set_head (((Ggit.Ref)new_branch).get_name ()); + } catch (Error e) { + warning ("Failed to create new branch. %s", e.message); + } + + return true; } private bool do_update = false; From f957a063530d649bf59e5f6eda22a8b5b7da8c76 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 15:12:25 +0100 Subject: [PATCH 11/23] Get name of Branch not Ref --- src/FolderManager/ProjectFolderItem.vala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 649040c146..bc1e4ce1e3 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -376,13 +376,17 @@ namespace Scratch.FolderManager { public bool checkout_branch_ref (Ggit.Ref branch_ref) { if (branch_ref.is_branch ()) { - var branch_name = branch_ref.get_name (); - if (branch_name != null) { - return monitored_repo.change_local_branch (branch_name); - } else { + try { + var branch_name = ((Ggit.Branch)branch_ref).get_name (); + if (branch_name != null) { + return monitored_repo.change_local_branch (branch_name); + } else { + return false; + } + } catch (Error e) { + warning ("Failed to get branch name from ref. %s", e.message); return false; } - } else { var target_shorthand = branch_ref.get_shorthand (); if (target_shorthand != null) { From 8378d854f04fcda4e564ae17db3313b4d8ec6b8e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 15:56:44 +0100 Subject: [PATCH 12/23] Split out private classes --- src/Dialogs/BranchActionDialog.vala | 273 ------------------ .../BranchActions/BranchActionDialog.vala | 84 ++++++ .../BranchActions/BranchCheckoutPage.vala | 35 +++ src/Dialogs/BranchActions/BranchListBox.vala | 135 +++++++++ src/Dialogs/BranchActions/BranchNameRow.vala | 35 +++ src/meson.build | 5 +- 6 files changed, 293 insertions(+), 274 deletions(-) delete mode 100644 src/Dialogs/BranchActionDialog.vala create mode 100644 src/Dialogs/BranchActions/BranchActionDialog.vala create mode 100644 src/Dialogs/BranchActions/BranchCheckoutPage.vala create mode 100644 src/Dialogs/BranchActions/BranchListBox.vala create mode 100644 src/Dialogs/BranchActions/BranchNameRow.vala diff --git a/src/Dialogs/BranchActionDialog.vala b/src/Dialogs/BranchActionDialog.vala deleted file mode 100644 index 595e1a3852..0000000000 --- a/src/Dialogs/BranchActionDialog.vala +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright 2025 elementary, Inc. - * SPDX-License-Identifier: GPL-3.0-or-later -* -* Authored by: Jeremy Wootten -*/ -public enum Scratch.BranchAction { - CHECKOUT, - COMMIT, - PUSH, - PULL, - MERGE, - DELETE, - CREATE -} - -public interface Scratch.BranchActionPage : Gtk.Widget { - public abstract BranchAction action { get; } - public abstract Ggit.Ref branch_ref { get; } -} - -public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { - public BranchAction action { - get { - return ((BranchActionPage)stack.get_visible_child ()).action; - } - } - - public Ggit.Ref branch_ref { - get { - return ((BranchActionPage)stack.get_visible_child ()).branch_ref; - } - } - - private Gtk.Stack stack; - protected bool can_apply { get; set; default = false; } - - public FolderManager.ProjectFolderItem project { get; construct; } - public BranchActionDialog (FolderManager.ProjectFolderItem project) { - Object ( - project: project - ); - } - - construct { - transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); - add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - if (project.is_git_repo) { - primary_text = _("Perform branch action on project '%s'").printf ( - project.file.file.get_basename () - ); - image_icon = new ThemedIcon ("git"); - var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); - bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); - stack = new Gtk.Stack (); - var checkout_page = new BranchCheckoutPage (this); - stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); - - stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); - stack.add_titled (new Gtk.Label (_("Push not implemented yet")), BranchAction.PUSH.to_string (), _("Push")); - stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); - stack.add_titled (new Gtk.Label (_("Merge not implemented yet")), BranchAction.MERGE.to_string (), _("Merge")); - stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); - stack.add_titled (new Gtk.Label (_("Create not implemented yet")), BranchAction.CREATE.to_string (), _("Create")); - - var sidebar = new Gtk.StackSidebar () { - stack = stack - }; - - var content_box = new Gtk.Box (HORIZONTAL, 12); - content_box.add (sidebar); - content_box.add (stack); - - custom_bin.add (content_box); - custom_bin.show_all (); - } else { - primary_text = _("'%s' is not a git repository").printf ( - project.file.file.get_basename () - ); - secondary_text = _("Unable to perform branch actions"); - image_icon = new ThemedIcon ("dialog-error"); - } - } - - private class BranchCheckoutPage : Gtk.Box, BranchActionPage { - public BranchAction action { - get { - return BranchAction.CHECKOUT; - } - } - - public Ggit.Ref branch_ref { - get { - return list_box.get_selected_row ().bref; - } - } - - public BranchActionDialog dialog { get; construct; } - - private BranchListBox list_box; - - public BranchCheckoutPage (BranchActionDialog dialog) { - Object ( - dialog: dialog - ); - } - - construct { - list_box = new BranchListBox (dialog, true); - add (list_box); - } - } - - private class BranchListBox : Gtk.Bin { - public string text { - get { - return search_entry.text; - } - } - - public bool show_remotes { get; construct;} - public BranchActionDialog dialog { get; construct;} - - private Gtk.ListBox list_box; - private Gtk.SearchEntry search_entry; - private Gtk.Label local_header; - private Gtk.Label remote_header; - private Gtk.Label recent_header; - - public BranchListBox (BranchActionDialog dialog, bool show_remotes) { - Object ( - dialog: dialog, - show_remotes: show_remotes - ); - } - - construct { - list_box = new Gtk.ListBox (); - var scrolled_window = new Gtk.ScrolledWindow (null, null) { - hscrollbar_policy = NEVER, - vscrollbar_policy = AUTOMATIC, - min_content_height = 200, - vexpand = true - }; - scrolled_window.child = list_box; - search_entry = new Gtk.SearchEntry (); - var box = new Gtk.Box (VERTICAL, 6); - box.add (search_entry); - box.add (scrolled_window); - child = box; - - recent_header = new Granite.HeaderLabel (_("Recent Branches")); - local_header = new Granite.HeaderLabel (_("Local Branches")); - remote_header = new Granite.HeaderLabel (_("Remote Branches")); - var branch_refs = dialog.project.get_all_branch_refs (); - - foreach (var branch_ref in branch_refs) { - if (branch_ref.is_branch () || show_remotes) { - var row = new BranchNameRow (branch_ref); - if (dialog.project.is_recent_ref (branch_ref)) { - row.is_recent = true; - } - list_box.add (row); - } - } - list_box.set_sort_func (listbox_sort_func); - list_box.set_header_func (listbox_header_func); - list_box.row_activated.connect ((listboxrow) => { - search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; - }); - list_box.set_filter_func ((listboxrow) => { - return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text)); - }); - search_entry.changed.connect (() => { - list_box.invalidate_filter (); - list_box.invalidate_headers (); - // Checkout action - dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); - }); - } - - public BranchNameRow? get_selected_row () { - int index = 0; - var row = list_box.get_row_at_index (index); - while (row != null && - ((BranchNameRow)row).branch_name != search_entry.text) { - - row = list_box.get_row_at_index (++index); - } - - return (BranchNameRow)row; - } - - - private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { - var a = (BranchNameRow)(rowa); - var b = (BranchNameRow)(rowb); - - if (a.is_recent && !b.is_recent) { - return -1; - } else if (b.is_recent && !a.is_recent) { - return 1; - } - - if (a.is_remote && !b.is_remote) { - return 1; - } else if (b.is_remote && !a.is_remote) { - return -1; - } - - return (a.branch_name.collate (b.branch_name)); - } - - private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { - var a = (BranchNameRow)row; - a.set_header (null); - if (row_before == null) { - if (a.is_recent && a.get_header () != recent_header) { - a.set_header (recent_header); - } else if (!a.is_remote && a.get_header () != local_header) { - a.set_header (local_header); - } else { - a.set_header (remote_header); - } - - return; - } - - - var b = (BranchNameRow)row_before; - - if (b.is_recent && !a.is_recent) { - if (!a.is_remote && a.get_header () != local_header) { - a.set_header (local_header); - } else if (a.is_remote && a.get_header () != remote_header) { - a.set_header (remote_header); - } - } else if (!b.is_remote && a.is_remote && a.get_header () != remote_header) { - a.set_header (remote_header); - } - } - } - - private class BranchNameRow : Gtk.ListBoxRow { - public Ggit.Ref bref { get; construct; } - public bool is_remote { get; private set; } - public bool is_recent { get; set; default = false; } - - public string branch_name { - get { - return label.label; - } - } - - private Gtk.Label label; - - public BranchNameRow (Ggit.Ref bref) { - Object ( - bref: bref - ); - } - - construct { - is_remote = !bref.is_branch (); - label = new Gtk.Label (bref.get_shorthand ()) { - halign = START, - margin_start = 24 - }; - - child = label; - } - } -} diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala new file mode 100644 index 0000000000..22b4a3506d --- /dev/null +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -0,0 +1,84 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +public enum Scratch.BranchAction { + CHECKOUT, + COMMIT, + PUSH, + PULL, + MERGE, + DELETE, + CREATE +} + +public interface Scratch.BranchActionPage : Gtk.Widget { + public abstract BranchAction action { get; } + public abstract Ggit.Ref branch_ref { get; } +} + +public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { + public BranchAction action { + get { + return ((BranchActionPage)stack.get_visible_child ()).action; + } + } + + public Ggit.Ref branch_ref { + get { + return ((BranchActionPage)stack.get_visible_child ()).branch_ref; + } + } + + private Gtk.Stack stack; + public bool can_apply { get; set; default = false; } + + public FolderManager.ProjectFolderItem project { get; construct; } + public BranchActionDialog (FolderManager.ProjectFolderItem project) { + Object ( + project: project + ); + } + + construct { + transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + if (project.is_git_repo) { + primary_text = _("Perform branch action on project '%s'").printf ( + project.file.file.get_basename () + ); + image_icon = new ThemedIcon ("git"); + var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); + bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); + stack = new Gtk.Stack (); + var checkout_page = new BranchCheckoutPage (this); + stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); + + stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); + stack.add_titled (new Gtk.Label (_("Push not implemented yet")), BranchAction.PUSH.to_string (), _("Push")); + stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); + stack.add_titled (new Gtk.Label (_("Merge not implemented yet")), BranchAction.MERGE.to_string (), _("Merge")); + stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); + stack.add_titled (new Gtk.Label (_("Create not implemented yet")), BranchAction.CREATE.to_string (), _("Create")); + + var sidebar = new Gtk.StackSidebar () { + stack = stack + }; + + var content_box = new Gtk.Box (HORIZONTAL, 12); + content_box.add (sidebar); + content_box.add (stack); + + custom_bin.add (content_box); + custom_bin.show_all (); + } else { + primary_text = _("'%s' is not a git repository").printf ( + project.file.file.get_basename () + ); + secondary_text = _("Unable to perform branch actions"); + image_icon = new ThemedIcon ("dialog-error"); + } + } +} diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala new file mode 100644 index 0000000000..e394cd5f6a --- /dev/null +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -0,0 +1,35 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ + +public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { + public BranchAction action { + get { + return BranchAction.CHECKOUT; + } + } + + public Ggit.Ref branch_ref { + get { + return list_box.get_selected_row ().bref; + } + } + + public BranchActionDialog dialog { get; construct; } + + private BranchListBox list_box; + + public BranchCheckoutPage (BranchActionDialog dialog) { + Object ( + dialog: dialog + ); + } + + construct { + list_box = new BranchListBox (dialog, true); + add (list_box); + } +} diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala new file mode 100644 index 0000000000..39be0b98e6 --- /dev/null +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -0,0 +1,135 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +private class Scratch.Dialogs.BranchListBox : Gtk.Bin { + public string text { + get { + return search_entry.text; + } + } + + public bool show_remotes { get; construct;} + public BranchActionDialog dialog { get; construct;} + + private Gtk.ListBox list_box; + private Gtk.SearchEntry search_entry; + private Gtk.Label local_header; + private Gtk.Label remote_header; + private Gtk.Label recent_header; + + public BranchListBox (BranchActionDialog dialog, bool show_remotes) { + Object ( + dialog: dialog, + show_remotes: show_remotes + ); + } + + construct { + list_box = new Gtk.ListBox (); + var scrolled_window = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = NEVER, + vscrollbar_policy = AUTOMATIC, + min_content_height = 200, + vexpand = true + }; + scrolled_window.child = list_box; + search_entry = new Gtk.SearchEntry (); + var box = new Gtk.Box (VERTICAL, 6); + box.add (search_entry); + box.add (scrolled_window); + child = box; + + recent_header = new Granite.HeaderLabel (_("Recent Branches")); + local_header = new Granite.HeaderLabel (_("Local Branches")); + remote_header = new Granite.HeaderLabel (_("Remote Branches")); + var branch_refs = dialog.project.get_all_branch_refs (); + + foreach (var branch_ref in branch_refs) { + if (branch_ref.is_branch () || show_remotes) { + var row = new BranchNameRow (branch_ref); + if (dialog.project.is_recent_ref (branch_ref)) { + row.is_recent = true; + } + list_box.add (row); + } + } + list_box.set_sort_func (listbox_sort_func); + list_box.set_header_func (listbox_header_func); + list_box.row_activated.connect ((listboxrow) => { + search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; + }); + list_box.set_filter_func ((listboxrow) => { + return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text)); + }); + search_entry.changed.connect (() => { + list_box.invalidate_filter (); + list_box.invalidate_headers (); + // Checkout action + dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); + }); + } + + public BranchNameRow? get_selected_row () { + int index = 0; + var row = list_box.get_row_at_index (index); + while (row != null && + ((BranchNameRow)row).branch_name != search_entry.text) { + + row = list_box.get_row_at_index (++index); + } + + return (BranchNameRow)row; + } + + + private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { + var a = (BranchNameRow)(rowa); + var b = (BranchNameRow)(rowb); + + if (a.is_recent && !b.is_recent) { + return -1; + } else if (b.is_recent && !a.is_recent) { + return 1; + } + + if (a.is_remote && !b.is_remote) { + return 1; + } else if (b.is_remote && !a.is_remote) { + return -1; + } + + return (a.branch_name.collate (b.branch_name)); + } + + private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { + var a = (BranchNameRow)row; + a.set_header (null); + if (row_before == null) { + if (a.is_recent && a.get_header () != recent_header) { + a.set_header (recent_header); + } else if (!a.is_remote && a.get_header () != local_header) { + a.set_header (local_header); + } else { + a.set_header (remote_header); + } + + return; + } + + + var b = (BranchNameRow)row_before; + + if (b.is_recent && !a.is_recent) { + if (!a.is_remote && a.get_header () != local_header) { + a.set_header (local_header); + } else if (a.is_remote && a.get_header () != remote_header) { + a.set_header (remote_header); + } + } else if (!b.is_remote && a.is_remote && a.get_header () != remote_header) { + a.set_header (remote_header); + } + } +} diff --git a/src/Dialogs/BranchActions/BranchNameRow.vala b/src/Dialogs/BranchActions/BranchNameRow.vala new file mode 100644 index 0000000000..8e6f66f4eb --- /dev/null +++ b/src/Dialogs/BranchActions/BranchNameRow.vala @@ -0,0 +1,35 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +private class Scratch.Dialogs.BranchNameRow : Gtk.ListBoxRow { + public Ggit.Ref bref { get; construct; } + public bool is_remote { get; private set; } + public bool is_recent { get; set; default = false; } + + public string branch_name { + get { + return label.label; + } + } + + private Gtk.Label label; + + public BranchNameRow (Ggit.Ref bref) { + Object ( + bref: bref + ); + } + + construct { + is_remote = !bref.is_branch (); + label = new Gtk.Label (bref.get_shorthand ()) { + halign = START, + margin_start = 24 + }; + + child = label; + } +} diff --git a/src/meson.build b/src/meson.build index 531ddd1e0d..bbde4e58e7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -24,7 +24,10 @@ code_files = files( 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', # 'Dialogs/NewBranchDialog.vala', - 'Dialogs/BranchActionDialog.vala', + 'Dialogs/BranchActions/BranchActionDialog.vala', + 'Dialogs/BranchActions/BranchCheckoutPage.vala', + 'Dialogs/BranchActions/BranchListBox.vala', + 'Dialogs/BranchActions/BranchNameRow.vala', 'FolderManager/File.vala', 'FolderManager/FileItem.vala', 'FolderManager/FileView.vala', From 66ec62d9a0fb877e39d65de91968f8051d4892a2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 16:47:11 +0100 Subject: [PATCH 13/23] Implement create new branch --- .../BranchActions/BranchActionDialog.vala | 12 ++- .../BranchActions/BranchCheckoutPage.vala | 8 +- .../BranchActions/BranchCreatePage.vala | 78 +++++++++++++++++++ src/FolderManager/FileView.vala | 13 ++-- src/meson.build | 1 + 5 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 src/Dialogs/BranchActions/BranchCreatePage.vala diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index 22b4a3506d..22844700fa 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -16,7 +16,8 @@ public enum Scratch.BranchAction { public interface Scratch.BranchActionPage : Gtk.Widget { public abstract BranchAction action { get; } - public abstract Ggit.Ref branch_ref { get; } + public abstract Ggit.Ref? branch_ref { get; } + public abstract string new_branch_name { get; } } public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { @@ -32,6 +33,12 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } + public string new_branch_name { + get { + return ((BranchActionPage)stack.get_visible_child ()).new_branch_name; + } + } + private Gtk.Stack stack; public bool can_apply { get; set; default = false; } @@ -54,6 +61,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); stack = new Gtk.Stack (); var checkout_page = new BranchCheckoutPage (this); + var create_page = new BranchCreatePage (this); stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); @@ -61,7 +69,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); stack.add_titled (new Gtk.Label (_("Merge not implemented yet")), BranchAction.MERGE.to_string (), _("Merge")); stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); - stack.add_titled (new Gtk.Label (_("Create not implemented yet")), BranchAction.CREATE.to_string (), _("Create")); + stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("Create")); var sidebar = new Gtk.StackSidebar () { stack = stack diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala index e394cd5f6a..c50a4b0f8e 100644 --- a/src/Dialogs/BranchActions/BranchCheckoutPage.vala +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -12,12 +12,18 @@ public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { } } - public Ggit.Ref branch_ref { + public Ggit.Ref? branch_ref { get { return list_box.get_selected_row ().bref; } } + public string new_branch_name { + get { + return ""; + } + } + public BranchActionDialog dialog { get; construct; } private BranchListBox list_box; diff --git a/src/Dialogs/BranchActions/BranchCreatePage.vala b/src/Dialogs/BranchActions/BranchCreatePage.vala new file mode 100644 index 0000000000..67b8867cb1 --- /dev/null +++ b/src/Dialogs/BranchActions/BranchCreatePage.vala @@ -0,0 +1,78 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ + +public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage { + public BranchAction action { + get { + return BranchAction.CREATE; + } + } + + + public Ggit.Ref? branch_ref { + get { + return null; + } + } + + public string new_branch_name { + get { + return new_branch_name_entry.text; + } + } + + public BranchActionDialog dialog { get; construct; } + // public bool can_apply { get; set; default = false; } + + private Granite.ValidatedEntry new_branch_name_entry; + // public string new_branch_name { + // get { + // return new_branch_name_entry.text; + // } + // } + + public BranchCreatePage (BranchActionDialog dialog) { + Object ( + dialog: dialog + ); + } + + construct { + orientation = VERTICAL; + vexpand = false; + hexpand = true; + margin_start = 24; + spacing = 12; + valign = CENTER; + var label = new Granite.HeaderLabel (_("Name of branch to create")); + new_branch_name_entry = new Granite.ValidatedEntry () { + activates_default = true, + placeholder_text = _("Enter new branch name") + }; + + add (label); + add (new_branch_name_entry); + + new_branch_name_entry.bind_property ("is-valid", dialog, "can-apply"); + + new_branch_name_entry.changed.connect (() => { + unowned var new_name = new_branch_name_entry.text; + if (!dialog.project.is_valid_new_branch_name (new_name)) { + new_branch_name_entry.is_valid = false; + return; + } + + if (dialog.project.has_local_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; + }); + } +} diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index aebeee14a4..68df17d6a6 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -229,7 +229,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane if (res == Gtk.ResponseType.APPLY) { var action = dialog.action; var branch_ref = dialog.branch_ref; - perform_branch_action.begin (active_project, action, branch_ref); + perform_branch_action (dialog); } dialog.destroy (); @@ -238,14 +238,12 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane dialog.present (); } - private async void perform_branch_action ( - ProjectFolderItem project, - BranchAction action, - Ggit.Ref branch_ref + private void perform_branch_action ( + Scratch.Dialogs.BranchActionDialog dialog ) { - switch (action) { + switch (dialog.action) { case CHECKOUT: - project.checkout_branch_ref (branch_ref); + dialog.project.checkout_branch_ref (dialog.branch_ref); break; case COMMIT: break; @@ -258,6 +256,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane case DELETE: break; case CREATE: + dialog.project.new_branch (dialog.new_branch_name); break; default: assert_not_reached (); diff --git a/src/meson.build b/src/meson.build index bbde4e58e7..33f4c6730e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -26,6 +26,7 @@ code_files = files( # 'Dialogs/NewBranchDialog.vala', 'Dialogs/BranchActions/BranchActionDialog.vala', 'Dialogs/BranchActions/BranchCheckoutPage.vala', + 'Dialogs/BranchActions/BranchCreatePage.vala', 'Dialogs/BranchActions/BranchListBox.vala', 'Dialogs/BranchActions/BranchNameRow.vala', 'FolderManager/File.vala', From 830b05b65066f570b2568e19ba6684fe8a068ae0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 17:31:31 +0100 Subject: [PATCH 14/23] Small Cleanup --- src/Dialogs/BranchActions/BranchActionDialog.vala | 1 + src/Dialogs/BranchActions/BranchCreatePage.vala | 7 ------- src/Dialogs/BranchActions/BranchListBox.vala | 4 +++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index 22844700fa..d02d763f78 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -56,6 +56,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { primary_text = _("Perform branch action on project '%s'").printf ( project.file.file.get_basename () ); + primary_label.can_focus = false; image_icon = new ThemedIcon ("git"); var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); diff --git a/src/Dialogs/BranchActions/BranchCreatePage.vala b/src/Dialogs/BranchActions/BranchCreatePage.vala index 67b8867cb1..675b7d802f 100644 --- a/src/Dialogs/BranchActions/BranchCreatePage.vala +++ b/src/Dialogs/BranchActions/BranchCreatePage.vala @@ -12,7 +12,6 @@ public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage { } } - public Ggit.Ref? branch_ref { get { return null; @@ -26,14 +25,8 @@ public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage { } public BranchActionDialog dialog { get; construct; } - // public bool can_apply { get; set; default = false; } private Granite.ValidatedEntry new_branch_name_entry; - // public string new_branch_name { - // get { - // return new_branch_name_entry.text; - // } - // } public BranchCreatePage (BranchActionDialog dialog) { Object ( diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 39be0b98e6..8fa8e49851 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -36,7 +36,9 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { vexpand = true }; scrolled_window.child = list_box; - search_entry = new Gtk.SearchEntry (); + search_entry = new Gtk.SearchEntry () { + placeholder_text = _("Enter search term") + }; var box = new Gtk.Box (VERTICAL, 6); box.add (search_entry); box.add (scrolled_window); From af0d77804e89d770b73ffb9d74b427bb240eb776 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 10:44:28 +0100 Subject: [PATCH 15/23] Focus desired widget on startup --- src/Dialogs/BranchActions/BranchActionDialog.vala | 10 ++++++++-- src/Dialogs/BranchActions/BranchCheckoutPage.vala | 5 +++++ src/Dialogs/BranchActions/BranchListBox.vala | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index d02d763f78..63cd8063f1 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -18,6 +18,7 @@ public interface Scratch.BranchActionPage : Gtk.Widget { public abstract BranchAction action { get; } public abstract Ggit.Ref? branch_ref { get; } public abstract string new_branch_name { get; } + public virtual void focus_start_widget () {} } public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { @@ -39,10 +40,11 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { } } - private Gtk.Stack stack; public bool can_apply { get; set; default = false; } - public FolderManager.ProjectFolderItem project { get; construct; } + + private Gtk.Stack stack; + public BranchActionDialog (FolderManager.ProjectFolderItem project) { Object ( project: project @@ -89,5 +91,9 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { secondary_text = _("Unable to perform branch actions"); image_icon = new ThemedIcon ("dialog-error"); } + + realize.connect (() => { + ((BranchActionPage)stack.get_visible_child ()).focus_start_widget (); + }); } } diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala index c50a4b0f8e..8f49ac90db 100644 --- a/src/Dialogs/BranchActions/BranchCheckoutPage.vala +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -38,4 +38,9 @@ public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { list_box = new BranchListBox (dialog, true); add (list_box); } + + public override void focus_start_widget () { + warning ("checkout page focus start"); + list_box.grab_focus (); + } } diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 8fa8e49851..19c6c468ca 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -134,4 +134,8 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { a.set_header (remote_header); } } + + public new void grab_focus () { + search_entry.grab_focus (); + } } From 61d0101524d1abda452174b4f7e63c8998f5dd75 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 10:53:42 +0100 Subject: [PATCH 16/23] Code improvement --- src/Dialogs/BranchActions/BranchListBox.vala | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 19c6c468ca..8f5707d038 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -108,23 +108,16 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { var a = (BranchNameRow)row; - a.set_header (null); - if (row_before == null) { - if (a.is_recent && a.get_header () != recent_header) { + var b = (BranchNameRow?)row_before; + if (b == null) { + if (a.is_recent) { a.set_header (recent_header); - } else if (!a.is_remote && a.get_header () != local_header) { + } else if (!a.is_remote) { a.set_header (local_header); } else { a.set_header (remote_header); } - - return; - } - - - var b = (BranchNameRow)row_before; - - if (b.is_recent && !a.is_recent) { + } else if (b.is_recent && !a.is_recent) { if (!a.is_remote && a.get_header () != local_header) { a.set_header (local_header); } else if (a.is_remote && a.get_header () != remote_header) { From b22bdaccdf07816c1882c2405e1dc45323672a77 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 11:05:07 +0100 Subject: [PATCH 17/23] Activate on double-click --- src/Dialogs/BranchActions/BranchActionDialog.vala | 8 ++++++++ src/Dialogs/BranchActions/BranchCheckoutPage.vala | 1 - src/Dialogs/BranchActions/BranchListBox.vala | 9 +++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index 63cd8063f1..3cb8cc83ee 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -22,6 +22,8 @@ public interface Scratch.BranchActionPage : Gtk.Widget { } public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { + public signal void page_activated (); + public BranchAction action { get { return ((BranchActionPage)stack.get_visible_child ()).action; @@ -95,5 +97,11 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { realize.connect (() => { ((BranchActionPage)stack.get_visible_child ()).focus_start_widget (); }); + + page_activated.connect (() => { + if (can_apply) { + response (Gtk.ResponseType.APPLY); + } + }); } } diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala index 8f49ac90db..8b8878aa5f 100644 --- a/src/Dialogs/BranchActions/BranchCheckoutPage.vala +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -40,7 +40,6 @@ public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { } public override void focus_start_widget () { - warning ("checkout page focus start"); list_box.grab_focus (); } } diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 8f5707d038..5b72f47365 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -28,7 +28,9 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { } construct { - list_box = new Gtk.ListBox (); + list_box = new Gtk.ListBox () { + activate_on_single_click = false + }; var scrolled_window = new Gtk.ScrolledWindow (null, null) { hscrollbar_policy = NEVER, vscrollbar_policy = AUTOMATIC, @@ -60,9 +62,12 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { } list_box.set_sort_func (listbox_sort_func); list_box.set_header_func (listbox_header_func); - list_box.row_activated.connect ((listboxrow) => { + list_box.row_selected.connect ((listboxrow) => { search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; }); + list_box.row_activated.connect ((listboxrow) => { + dialog.page_activated (); + }); list_box.set_filter_func ((listboxrow) => { return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text)); }); From 8ae2378694dd7351087f506383485fa386019181 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 11:24:23 +0100 Subject: [PATCH 18/23] Refocus after select, activate on Enter --- src/Dialogs/BranchActions/BranchListBox.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 5b72f47365..4511513596 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -63,7 +63,10 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { list_box.set_sort_func (listbox_sort_func); list_box.set_header_func (listbox_header_func); list_box.row_selected.connect ((listboxrow) => { + //We want cursor to end up after the inserted text search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; + search_entry.grab_focus_without_selecting (); + search_entry.move_cursor (DISPLAY_LINE_ENDS, 1, false); }); list_box.row_activated.connect ((listboxrow) => { dialog.page_activated (); @@ -77,6 +80,9 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { // Checkout action dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); }); + search_entry.activate.connect (() => { + dialog.page_activated (); + }); } public BranchNameRow? get_selected_row () { From e28ceceed38173cdbbc3045f9fe7b52cd0063cfb Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 11:56:24 +0100 Subject: [PATCH 19/23] Make listbox more general purpose --- src/Dialogs/BranchActions/BranchCheckoutPage.vala | 3 +++ src/Dialogs/BranchActions/BranchListBox.vala | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala index 8b8878aa5f..b68e1be183 100644 --- a/src/Dialogs/BranchActions/BranchCheckoutPage.vala +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -37,6 +37,9 @@ public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { construct { list_box = new BranchListBox (dialog, true); add (list_box); + list_box.branch_changed.connect ((text) => { + dialog.can_apply = dialog.project.has_branch_name (text, null); + }); } public override void focus_start_widget () { diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index 4511513596..ba7f23a4da 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -5,6 +5,7 @@ * Authored by: Jeremy Wootten */ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { + public signal void branch_changed (string branch_name); public string text { get { return search_entry.text; @@ -77,8 +78,7 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { search_entry.changed.connect (() => { list_box.invalidate_filter (); list_box.invalidate_headers (); - // Checkout action - dialog.can_apply = dialog.project.has_branch_name (search_entry.text, null); + branch_changed (text); }); search_entry.activate.connect (() => { dialog.page_activated (); From 567338c71f37d875cd0aab1b0d6a9c84f2833482 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 12:26:26 +0100 Subject: [PATCH 20/23] Simplify header func --- src/Dialogs/BranchActions/BranchListBox.vala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala index ba7f23a4da..dc36072cee 100644 --- a/src/Dialogs/BranchActions/BranchListBox.vala +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -77,7 +77,10 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { }); search_entry.changed.connect (() => { list_box.invalidate_filter (); - list_box.invalidate_headers (); + // recent_header.unparent (); + // local_header.unparent (); + // remote_header.unparent (); + // list_box.invalidate_headers (); branch_changed (text); }); search_entry.activate.connect (() => { @@ -120,6 +123,7 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { var a = (BranchNameRow)row; var b = (BranchNameRow?)row_before; + a.set_header (null); if (b == null) { if (a.is_recent) { a.set_header (recent_header); @@ -128,14 +132,16 @@ private class Scratch.Dialogs.BranchListBox : Gtk.Bin { } else { a.set_header (remote_header); } - } else if (b.is_recent && !a.is_recent) { - if (!a.is_remote && a.get_header () != local_header) { + } else if (b.is_recent) { + if (!a.is_remote) { a.set_header (local_header); - } else if (a.is_remote && a.get_header () != remote_header) { + } else if (a.is_remote) { + a.set_header (remote_header); + } + } else if (!b.is_remote) { + if (a.is_remote) { a.set_header (remote_header); } - } else if (!b.is_remote && a.is_remote && a.get_header () != remote_header) { - a.set_header (remote_header); } } From 8432a9bb83266560826ff650cba4ba7a83da1d46 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 3 Jul 2025 01:51:24 +0100 Subject: [PATCH 21/23] Suppress terminal warnings --- src/FolderManager/FileView.vala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 68df17d6a6..62f532bded 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -227,8 +227,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane var dialog = new Dialogs.BranchActionDialog (active_project); dialog.response.connect ((res) => { if (res == Gtk.ResponseType.APPLY) { - var action = dialog.action; - var branch_ref = dialog.branch_ref; perform_branch_action (dialog); } From aae9ab305bf39ed99c8286141f1f66f1d1fdd1bc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 18 Oct 2025 17:26:37 +0000 Subject: [PATCH 22/23] Remove unimplemented functions --- src/Dialogs/BranchActions/BranchActionDialog.vala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala index 3cb8cc83ee..b264484b71 100644 --- a/src/Dialogs/BranchActions/BranchActionDialog.vala +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -68,13 +68,7 @@ public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { var checkout_page = new BranchCheckoutPage (this); var create_page = new BranchCreatePage (this); stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); - - stack.add_titled (new Gtk.Label (_("Commit not implemented yet")), BranchAction.COMMIT.to_string (), _("Commit")); - stack.add_titled (new Gtk.Label (_("Push not implemented yet")), BranchAction.PUSH.to_string (), _("Push")); - stack.add_titled (new Gtk.Label (_("Pull not implemented yet")), BranchAction.PULL.to_string (), _("Pull")); - stack.add_titled (new Gtk.Label (_("Merge not implemented yet")), BranchAction.MERGE.to_string (), _("Merge")); - stack.add_titled (new Gtk.Label (_("Delete not implemented yet")), BranchAction.DELETE.to_string (), _("Delete")); - stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("Create")); + stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("New")); var sidebar = new Gtk.StackSidebar () { stack = stack From 62bd40b565cc748d2cc310b6a6beba30049db050 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 5 Nov 2025 15:41:34 +0000 Subject: [PATCH 23/23] Remove commented out code --- src/FolderManager/FileView.vala | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 62f532bded..4921bbfe4b 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -32,8 +32,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane public const string ACTION_DELETE = "delete"; public const string ACTION_NEW_FILE = "new-file"; public const string ACTION_NEW_FOLDER = "new-folder"; - // public const string ACTION_CHECKOUT_LOCAL_BRANCH = "checkout-local-branch"; - // public const string ACTION_CHECKOUT_REMOTE_BRANCH = "checkout-remote-branch"; public const string ACTION_CLOSE_FOLDER = "close-folder"; public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; public const string ACTION_SET_ACTIVE_PROJECT = "set-active-project"; @@ -340,26 +338,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } } - // 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; - // 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 (branch_name != null) { - // active_project.new_branch (branch_name); - // } - // } - public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { plugins.hook_folder_item_change (source, dest, event); }