From e1d2e5cf2b06aae6853bc3d648b8611d7b1c691a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 26 Mar 2025 17:23:40 +0000 Subject: [PATCH 01/56] Add clone button to sidebar --- src/MainWindow.vala | 1 + src/Widgets/Sidebar.vala | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 79cb340450..8bff138aac 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -71,6 +71,7 @@ namespace Scratch { public const string ACTION_GROUP = "win"; public const string ACTION_PREFIX = ACTION_GROUP + "."; public const string ACTION_FIND = "action-find"; + public const string ACTION_CLONE_REPO = "action-clone-repo"; public const string ACTION_FIND_NEXT = "action-find-next"; public const string ACTION_FIND_PREVIOUS = "action-find-previous"; public const string ACTION_FIND_GLOBAL = "action-find-global"; diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 209548d2c5..1c7d3db8e8 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -63,6 +63,13 @@ public class Code.Sidebar : Gtk.Grid { label = _("Open Folder…") }; + var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { + action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, + action_target = new Variant.string (""), + always_show_image = true, + label = _("Clone Repository…") + }; + var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_COLLAPSE_ALL_FOLDERS); @@ -81,6 +88,7 @@ public class Code.Sidebar : Gtk.Grid { project_more_button.tooltip_text = _("Manage project folders"); actionbar.add (add_folder_button); + actionbar.add (clone_button); actionbar.pack_end (project_more_button); add (headerbar); From ebb8ed1ddca70f8f49ada9bdafbb9d4b2c90aa98 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 26 Mar 2025 19:40:26 +0000 Subject: [PATCH 02/56] Sketch out functionality --- src/Dialogs/CloneRepositoryDialog.vala | 134 +++++++++++++++++++++++++ src/MainWindow.vala | 33 ++++++ src/Services/GitManager.vala | 6 ++ src/meson.build | 1 + 4 files changed, 174 insertions(+) create mode 100644 src/Dialogs/CloneRepositoryDialog.vala diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala new file mode 100644 index 0000000000..cc3269998f --- /dev/null +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -0,0 +1,134 @@ +/* +* Copyright 2021 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 2 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA. +* +* Authored by: Jeremy Wootten +*/ + +public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { + public FolderManager.ProjectFolderItem? active_project { get; construct; } + public bool can_clone { get; private set; default = false; } + + + //Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala" + private const string NAME_REGEX = """[/w.-]+"""; + private const string LOCAL_FOLDER_REGEX ="^/[0-9a-zA-Z_-]+$"; + private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)"; + private Regex name_regex; + private Regex local_folder_regex; + private Regex url_regex; + private Granite.ValidatedEntry repository_host_uri_entry; + private Granite.ValidatedEntry repository_user_entry; + private Granite.ValidatedEntry repository_name_entry; + private Granite.ValidatedEntry repository_local_folder_entry; + public string initial_folder = ""; + + public CloneRepositoryDialog (string local_folder) { + Object ( + transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), + image_icon: new ThemedIcon ("git") + ); + + repository_local_folder_entry.text = local_folder; + repository_host_uri_entry.text = "https://github.com"; + repository_user_entry.text = "elementary"; + repository_name_entry.text = ""; + } + + construct { + try { + name_regex = new Regex (NAME_REGEX, RegexCompileFlags.OPTIMIZE); + local_folder_regex = new Regex (LOCAL_FOLDER_REGEX, RegexCompileFlags.OPTIMIZE); + url_regex = new Regex (URL_REGEX, RegexCompileFlags.OPTIMIZE); + } catch (RegexError e) { + warning ("%s\n", e.message); + } + + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + primary_text = _("Create a local clone of a git repository"); + ///TRANSLATORS "Git" is a proper name and must not be translated + secondary_text = _("The source repository and local folder must exist and be accessible"); + badge_icon = new ThemedIcon ("download"); + var repository_name_label = new Gtk.Label (_("Source Repository Name")); + repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + activates_default = false + }; + var repository_user_label = new Gtk.Label (_("Source Repository User")); + repository_user_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + activates_default = false + }; + var repository_host_uri_label = new Gtk.Label (_("Source Repository Host URI")); + repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { + activates_default = false + }; + var repository_local_folder_label = new Gtk.Label (_("Target Folder")); + repository_local_folder_entry = new Granite.ValidatedEntry.from_regex (local_folder_regex) { + activates_default = false + }; + var content_grid = new Gtk.Grid (); + content_grid.attach (repository_host_uri_label, 0, 0); + content_grid.attach (repository_host_uri_entry, 1, 0); + content_grid.attach (repository_user_label, 0, 1); + content_grid.attach (repository_user_entry, 1, 1); + content_grid.attach (repository_name_label, 0, 2); + content_grid.attach (repository_name_entry, 1, 2); + content_grid.attach (repository_local_folder_label, 0, 3); + content_grid.attach (repository_local_folder_entry, 1, 3); + custom_bin.add (content_grid); + + var clone_button = (Gtk.Button) add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); + clone_button.can_default = true; + clone_button.has_default = true; + clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + repository_host_uri_entry.notify["is-valid"].connect (on_is_valid_changed); + repository_user_entry.notify["is-valid"].connect (on_is_valid_changed); + repository_name_entry.notify["is-valid"].connect (on_is_valid_changed); + repository_local_folder_entry.notify["is-valid"].connect (on_is_valid_changed); + + bind_property ("can-clone", clone_button, "sensitive"); + } + + public string get_source_repository_uri () { + if (!can_clone) { + return ""; + } + + //TODO Further validation here? + return Path.build_path ( + Path.DIR_SEPARATOR_S, + repository_host_uri_entry.text, + repository_user_entry.text, + repository_name_entry.text + ); + } + + public string get_local_folder () { + if (!can_clone) { + return ""; + } + + //TODO Further validation here? + return repository_local_folder_entry.text; + } + private void on_is_valid_changed () { + can_clone = repository_host_uri_entry.is_valid && + repository_user_entry.is_valid && + repository_name_entry.is_valid && + repository_local_folder_entry.is_valid; + } +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 8bff138aac..ae0d344230 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -129,6 +129,7 @@ namespace Scratch { private const ActionEntry[] ACTION_ENTRIES = { { ACTION_FIND, action_fetch, "s" }, + { ACTION_FIND, action_clone_repo }, { ACTION_FIND_NEXT, action_find_next }, { ACTION_FIND_PREVIOUS, action_find_previous }, { ACTION_FIND_GLOBAL, action_find_global, "s" }, @@ -1029,6 +1030,38 @@ namespace Scratch { } } + private void action_clone_repo (SimpleAction action, Variant? param) { + var uri = ""; + var local_folder = Path.get_dirname (git_manager.active_project_path); + var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); + clone_dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.ACCEPT) { + uri = clone_dialog.get_source_repository_uri (); + local_folder = clone_dialog.get_local_folder (); + } + + clone_dialog.destroy (); + }); + + clone_dialog.run (); + + if (uri == "") { + return; + } + + git_manager.clone_repository.begin (uri, (obj, res) => { + try { + string folder; + if (git_manager.clone_repository.end (res, out folder)) { + //TODO Open repo and make active + } + } catch (Error e) { + ///TRANSLATORS the first %s is a URI; the second %s is a system error message + warning ("Unable to clone '%s'. %s", uri, e.message); + } + }); + } + private void action_collapse_all_folders () { folder_manager_view.collapse_all (); } diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 3b43bb88aa..8867e1c2e9 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -112,5 +112,11 @@ namespace Scratch.Services { return build_path; } + + public async bool clone_repository (string uri, out string path_to_repo) throws Error { + path_to_repo = ""; + //TODO Actually clone repo at uri; + return false; + } } } diff --git a/src/meson.build b/src/meson.build index 53acbb5ca9..80be058231 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,6 +21,7 @@ code_files = files( 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', 'Dialogs/CloseProjectsConfirmationDialog.vala', + 'Dialogs/CloneRepositoryDialog.vala', 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', From 5200d9d3adbda3a1288e83d23ac4374802fbd11a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 26 Mar 2025 20:00:08 +0000 Subject: [PATCH 03/56] Fix action, Tweak regex --- src/Dialogs/CloneRepositoryDialog.vala | 12 +++++++++--- src/MainWindow.vala | 2 +- src/Widgets/Sidebar.vala | 1 - 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index cc3269998f..41bbab222c 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -25,8 +25,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { //Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala" - private const string NAME_REGEX = """[/w.-]+"""; - private const string LOCAL_FOLDER_REGEX ="^/[0-9a-zA-Z_-]+$"; + private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$"; + private const string LOCAL_FOLDER_REGEX ="""^(/[^/ ]*)+/?$"""; private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)"; private Regex name_regex; private Regex local_folder_regex; @@ -47,6 +47,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { repository_host_uri_entry.text = "https://github.com"; repository_user_entry.text = "elementary"; repository_name_entry.text = ""; + + on_is_valid_changed (); } construct { @@ -77,7 +79,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }; var repository_local_folder_label = new Gtk.Label (_("Target Folder")); repository_local_folder_entry = new Granite.ValidatedEntry.from_regex (local_folder_regex) { - activates_default = false + activates_default = false, + width_chars = 50 }; var content_grid = new Gtk.Grid (); content_grid.attach (repository_host_uri_label, 0, 0); @@ -100,7 +103,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { repository_name_entry.notify["is-valid"].connect (on_is_valid_changed); repository_local_folder_entry.notify["is-valid"].connect (on_is_valid_changed); + clone_button.sensitive = can_clone; bind_property ("can-clone", clone_button, "sensitive"); + + show_all (); } public string get_source_repository_uri () { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index ae0d344230..60e0c970f7 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -129,7 +129,7 @@ namespace Scratch { private const ActionEntry[] ACTION_ENTRIES = { { ACTION_FIND, action_fetch, "s" }, - { ACTION_FIND, action_clone_repo }, + { ACTION_CLONE_REPO, action_clone_repo }, { ACTION_FIND_NEXT, action_find_next }, { ACTION_FIND_PREVIOUS, action_find_previous }, { ACTION_FIND_GLOBAL, action_find_global, "s" }, diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 1c7d3db8e8..fe3fae5ad6 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -65,7 +65,6 @@ public class Code.Sidebar : Gtk.Grid { var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, - action_target = new Variant.string (""), always_show_image = true, label = _("Clone Repository…") }; From 01d124febaf0b1013ee89599afd13d0fffa317a2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 26 Mar 2025 20:10:31 +0000 Subject: [PATCH 04/56] Various corrections --- src/Dialogs/CloneRepositoryDialog.vala | 8 +++++++- src/MainWindow.vala | 6 ++++-- src/Services/GitManager.vala | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 41bbab222c..a3e0f1b7bc 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -115,12 +115,18 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } //TODO Further validation here? - return Path.build_path ( + var repo_uri = Path.build_path ( Path.DIR_SEPARATOR_S, repository_host_uri_entry.text, repository_user_entry.text, repository_name_entry.text ); + + if (!repo_uri.has_suffix (".git")) { + repo_uri += ".git"; + } + + return repo_uri; } public string get_local_folder () { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 60e0c970f7..97fd25378f 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1035,7 +1035,8 @@ namespace Scratch { var local_folder = Path.get_dirname (git_manager.active_project_path); var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); clone_dialog.response.connect ((res) => { - if (res == Gtk.ResponseType.ACCEPT) { + warning ("response %s", res.to_string ()); + if (res == Gtk.ResponseType.APPLY) { uri = clone_dialog.get_source_repository_uri (); local_folder = clone_dialog.get_local_folder (); } @@ -1046,10 +1047,11 @@ namespace Scratch { clone_dialog.run (); if (uri == "") { + warning ("returned no uri"); return; } - git_manager.clone_repository.begin (uri, (obj, res) => { + git_manager.clone_repository.begin (uri, local_folder, (obj, res) => { try { string folder; if (git_manager.clone_repository.end (res, out folder)) { diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 8867e1c2e9..69d099cf17 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -113,8 +113,9 @@ namespace Scratch.Services { return build_path; } - public async bool clone_repository (string uri, out string path_to_repo) throws Error { + public async bool clone_repository (string uri, string local_folder, out string path_to_repo) throws Error { path_to_repo = ""; + warning ("Uri to clone is %s, into %s", uri, local_folder); //TODO Actually clone repo at uri; return false; } From e39eac8e529f21605645bc906159d8168dc1ed3a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 26 Mar 2025 20:40:25 +0000 Subject: [PATCH 05/56] Clone repository --- src/Dialogs/CloneRepositoryDialog.vala | 2 +- src/MainWindow.vala | 4 +-- src/Services/GitManager.vala | 34 ++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index a3e0f1b7bc..fa4b1a174d 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -115,7 +115,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } //TODO Further validation here? - var repo_uri = Path.build_path ( + var repo_uri = Path.build_path ( Path.DIR_SEPARATOR_S, repository_host_uri_entry.text, repository_user_entry.text, diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 97fd25378f..5d9abecfd0 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1053,8 +1053,8 @@ namespace Scratch { git_manager.clone_repository.begin (uri, local_folder, (obj, res) => { try { - string folder; - if (git_manager.clone_repository.end (res, out folder)) { + File? workdir = null; + if (git_manager.clone_repository.end (res, out workdir)) { //TODO Open repo and make active } } catch (Error e) { diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 69d099cf17..20097fbbd1 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -113,11 +113,35 @@ namespace Scratch.Services { return build_path; } - public async bool clone_repository (string uri, string local_folder, out string path_to_repo) throws Error { - path_to_repo = ""; - warning ("Uri to clone is %s, into %s", uri, local_folder); - //TODO Actually clone repo at uri; - return false; + public async bool clone_repository ( + string uri, + string local_folder, + out File? repo_workdir + ) throws Error { + + repo_workdir = null; + var folder_file = File.new_for_path (local_folder); + + var fetch_options = new Ggit.FetchOptions (); + fetch_options.set_download_tags (Ggit.RemoteDownloadTagsType.UNSPECIFIED); + fetch_options.set_remote_callbacks (null); + + var clone_options = new Ggit.CloneOptions (); + clone_options.set_local (Ggit.CloneLocal.AUTO); + clone_options.set_is_bare (false); + clone_options.set_fetch_options (fetch_options); + + var new_repo = Ggit.Repository.clone ( + uri, + folder_file, + clone_options + ); + + if (new_repo != null) { + repo_workdir = new_repo.get_workdir (); + } + + return new_repo != null; } } } From 7e3da0455d1d8ee94a2aeb4e6c313b7c4bfc78df Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 3 Apr 2025 16:55:46 +0000 Subject: [PATCH 06/56] Reformat, local repo name, make active option --- src/Dialogs/CloneRepositoryDialog.vala | 90 +++++++++++++++++--------- src/MainWindow.vala | 39 ++++++----- 2 files changed, 83 insertions(+), 46 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index fa4b1a174d..5ad52cbebc 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -1,5 +1,5 @@ /* -* Copyright 2021 elementary, Inc. (https://elementary.io) +* Copyright 2025 elementary, Inc. (https://elementary.io) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public @@ -16,7 +16,7 @@ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA. * -* Authored by: Jeremy Wootten +* Authored by: Jeremy Wootten */ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { @@ -35,7 +35,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Granite.ValidatedEntry repository_user_entry; private Granite.ValidatedEntry repository_name_entry; private Granite.ValidatedEntry repository_local_folder_entry; - public string initial_folder = ""; + private Granite.ValidatedEntry repository_local_name_entry; + private Gtk.CheckButton set_as_active_check; public CloneRepositoryDialog (string local_folder) { Object ( @@ -47,6 +48,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { repository_host_uri_entry.text = "https://github.com"; repository_user_entry.text = "elementary"; repository_name_entry.text = ""; + repository_local_name_entry.text = ""; on_is_valid_changed (); } @@ -61,37 +63,44 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - primary_text = _("Create a local clone of a git repository"); + primary_text = _("Create a local clone of a Git repository"); ///TRANSLATORS "Git" is a proper name and must not be translated secondary_text = _("The source repository and local folder must exist and be accessible"); badge_icon = new ThemedIcon ("download"); - var repository_name_label = new Gtk.Label (_("Source Repository Name")); - repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + + repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { activates_default = false }; - var repository_user_label = new Gtk.Label (_("Source Repository User")); repository_user_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false }; - var repository_host_uri_label = new Gtk.Label (_("Source Repository Host URI")); - repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { + repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false }; - var repository_local_folder_label = new Gtk.Label (_("Target Folder")); repository_local_folder_entry = new Granite.ValidatedEntry.from_regex (local_folder_regex) { activates_default = false, width_chars = 50 }; - var content_grid = new Gtk.Grid (); - content_grid.attach (repository_host_uri_label, 0, 0); - content_grid.attach (repository_host_uri_entry, 1, 0); - content_grid.attach (repository_user_label, 0, 1); - content_grid.attach (repository_user_entry, 1, 1); - content_grid.attach (repository_name_label, 0, 2); - content_grid.attach (repository_name_entry, 1, 2); - content_grid.attach (repository_local_folder_label, 0, 3); - content_grid.attach (repository_local_folder_entry, 1, 3); - custom_bin.add (content_grid); + repository_local_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + activates_default = false, + }; + + set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) { + margin_top = 12, + active = true + }; + + var content_box = new Gtk.Box (VERTICAL, 12); + content_box.add (new Granite.HeaderLabel (_("Source Repository"))); + content_box.add (new CloneEntry (_("Host URI"), repository_host_uri_entry)); + content_box.add (new CloneEntry (_("User"), repository_user_entry)); + content_box.add (new CloneEntry (_("Name"), repository_name_entry)); + content_box.add (new Granite.HeaderLabel (_("Clone"))); + content_box.add (new CloneEntry (_("Target Folder"), repository_local_folder_entry)); + content_box.add (new CloneEntry (_("Target Name"), repository_local_name_entry)); + content_box.add (set_as_active_check); + + custom_bin.add (content_box); var clone_button = (Gtk.Button) add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); clone_button.can_default = true; @@ -109,11 +118,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { show_all (); } - public string get_source_repository_uri () { - if (!can_clone) { - return ""; - } - + public string get_source_repository_uri () requires (can_clone) { //TODO Further validation here? var repo_uri = Path.build_path ( Path.DIR_SEPARATOR_S, @@ -129,18 +134,43 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { return repo_uri; } - public string get_local_folder () { - if (!can_clone) { - return ""; - } - + public string get_local_folder () requires (can_clone) { //TODO Further validation here? return repository_local_folder_entry.text; } + + public string get_local_name () requires (can_clone) { + var local_name = repository_local_name_entry.text; + if (local_name == "") { + local_name = repository_name_entry.text; + } + //TODO Further validation here? + return local_name; + } + private void on_is_valid_changed () { can_clone = repository_host_uri_entry.is_valid && repository_user_entry.is_valid && repository_name_entry.is_valid && repository_local_folder_entry.is_valid; } + + private class CloneEntry : Gtk.Box { + public CloneEntry (string label_text, Gtk.Widget entry) { + var label = new Gtk.Label (label_text) { + halign = START + }; + add (label); + add (entry); + } + + construct { + orientation = VERTICAL; + spacing = 6; + hexpand = false; + margin_start = 12; + margin_end = 12; + margin_bottom = 12; + } + } } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 5d9abecfd0..bd0727129f 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1032,13 +1032,15 @@ namespace Scratch { private void action_clone_repo (SimpleAction action, Variant? param) { var uri = ""; + //By default, create clone in parent of the current project var local_folder = Path.get_dirname (git_manager.active_project_path); + var local_name = ""; var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); clone_dialog.response.connect ((res) => { - warning ("response %s", res.to_string ()); if (res == Gtk.ResponseType.APPLY) { uri = clone_dialog.get_source_repository_uri (); local_folder = clone_dialog.get_local_folder (); + local_name = clone_dialog.get_local_name (); } clone_dialog.destroy (); @@ -1046,22 +1048,27 @@ namespace Scratch { clone_dialog.run (); - if (uri == "") { - warning ("returned no uri"); - return; - } - - git_manager.clone_repository.begin (uri, local_folder, (obj, res) => { - try { - File? workdir = null; - if (git_manager.clone_repository.end (res, out workdir)) { - //TODO Open repo and make active + if (clone_dialog.can_clone) { + //TODO Show progress while cloning + git_manager.clone_repository.begin ( + uri, + Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name), + (obj, res) => { + try { + File? workdir = null; + if (git_manager.clone_repository.end (res, out workdir)) { + debug ("Repository cloned into %s", workdir.get_uri ()); + open_folder (workdir); + //TODO Make active according to dialog checkbox + } + } catch (Error e) { + warning ("Unable to clone '%s'. %s", uri, e.message); + } } - } catch (Error e) { - ///TRANSLATORS the first %s is a URI; the second %s is a system error message - warning ("Unable to clone '%s'. %s", uri, e.message); - } - }); + ); + } else { + //TODO Give feedback + } } private void action_collapse_all_folders () { From a267a98320e9e1e418dacc9345e5f75911c66f44 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 22 Apr 2025 10:29:42 +0100 Subject: [PATCH 07/56] Fix lint --- src/Dialogs/CloneRepositoryDialog.vala | 2 +- src/MainWindow.vala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 5ad52cbebc..d68b2f4efc 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -139,7 +139,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { return repository_local_folder_entry.text; } - public string get_local_name () requires (can_clone) { + public string get_local_name () requires (can_clone) { var local_name = repository_local_name_entry.text; if (local_name == "") { local_name = repository_name_entry.text; diff --git a/src/MainWindow.vala b/src/MainWindow.vala index ed6d08bd28..1c80c99ca7 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1046,7 +1046,7 @@ namespace Scratch { if (clone_dialog.can_clone) { //TODO Show progress while cloning git_manager.clone_repository.begin ( - uri, + uri, Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name), (obj, res) => { try { From 22518500d6788fa63bdea7f1e4f59e0548daf09a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 27 May 2025 16:35:50 +0100 Subject: [PATCH 08/56] Allow narrower sidebar --- src/Widgets/Sidebar.vala | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index fe3fae5ad6..b24f37f16f 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -60,13 +60,16 @@ public class Code.Sidebar : Gtk.Grid { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER, action_target = new Variant.string (""), always_show_image = true, - label = _("Open Folder…") + label = _("Open …"), + xalign = 0.0f }; var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, always_show_image = true, - label = _("Clone Repository…") + label = _("Clone …"), + xalign = 0.0f + }; var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX @@ -86,8 +89,16 @@ public class Code.Sidebar : Gtk.Grid { project_more_button.menu_model = project_menu_model; project_more_button.tooltip_text = _("Manage project folders"); - actionbar.add (add_folder_button); - actionbar.add (clone_button); + var tool_flowbox = new Gtk.FlowBox () { + orientation = HORIZONTAL, + hexpand = true, + selection_mode = NONE, + column_spacing = 0, + max_children_per_line = 2 + }; + tool_flowbox.add (add_folder_button); + tool_flowbox.add (clone_button); + actionbar.add (tool_flowbox); actionbar.pack_end (project_more_button); add (headerbar); From ea1a35615b8173f1f370324ac12e51073b6d4004 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 15:36:17 +0100 Subject: [PATCH 09/56] Update src/Dialogs/CloneRepositoryDialog.vala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set input purpose Co-authored-by: Danielle Foré --- src/Dialogs/CloneRepositoryDialog.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index d68b2f4efc..6a04d7d3cb 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -69,6 +69,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { badge_icon = new ThemedIcon ("download"); repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { + input_purpose = URL, activates_default = false }; repository_user_entry = new Granite.ValidatedEntry.from_regex (name_regex) { From 576ca4352d04c2b652feb51de4149509a499698f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 15:37:11 +0100 Subject: [PATCH 10/56] Update src/Dialogs/CloneRepositoryDialog.vala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPDX header Co-authored-by: Danielle Foré --- src/Dialogs/CloneRepositoryDialog.vala | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 6a04d7d3cb..d9d9baa697 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -1,23 +1,9 @@ /* -* Copyright 2025 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 2 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA. -* -* Authored by: Jeremy Wootten -*/ + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Jeremy Wootten + */ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public FolderManager.ProjectFolderItem? active_project { get; construct; } From 9c38d0080f30c33f04cb6637c1b241f148ef4420 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 16:58:27 +0100 Subject: [PATCH 11/56] Replace local folder path entry with button linked to filechooser --- src/Dialogs/CloneRepositoryDialog.vala | 52 +++++++++++++++++++++----- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index d9d9baa697..7fc32982b7 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -12,7 +12,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { //Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala" private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$"; - private const string LOCAL_FOLDER_REGEX ="""^(/[^/ ]*)+/?$"""; private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)"; private Regex name_regex; private Regex local_folder_regex; @@ -24,13 +23,15 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Granite.ValidatedEntry repository_local_name_entry; private Gtk.CheckButton set_as_active_check; - public CloneRepositoryDialog (string local_folder) { + public string suggested_local_folder { get; construct; } + + public CloneRepositoryDialog (string _suggested_local_folder) { Object ( transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), - image_icon: new ThemedIcon ("git") + image_icon: new ThemedIcon ("git"), + suggested_local_folder: _suggested_local_folder ); - repository_local_folder_entry.text = local_folder; repository_host_uri_entry.text = "https://github.com"; repository_user_entry.text = "elementary"; repository_name_entry.text = ""; @@ -42,7 +43,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { construct { try { name_regex = new Regex (NAME_REGEX, RegexCompileFlags.OPTIMIZE); - local_folder_regex = new Regex (LOCAL_FOLDER_REGEX, RegexCompileFlags.OPTIMIZE); url_regex = new Regex (URL_REGEX, RegexCompileFlags.OPTIMIZE); } catch (RegexError e) { warning ("%s\n", e.message); @@ -64,10 +64,44 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false }; - repository_local_folder_entry = new Granite.ValidatedEntry.from_regex (local_folder_regex) { - activates_default = false, - width_chars = 50 + + var folder_image = new Gtk.Image.from_icon_name ("folder-download", BUTTON) { + margin_end = 6 }; + // The suggested folder is assumed to be valid as it is generated internally + var folder_label = new Gtk.Label (suggested_local_folder) { + hexpand = true, + halign = START + }; + var view_more_image = new Gtk.Image.from_icon_name ("view-more-horizontal-symbolic", BUTTON); + var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 0); + folder_chooser_button_child.add (folder_image); + folder_chooser_button_child.add (folder_label); + folder_chooser_button_child.add (view_more_image); + + var folder_chooser_button = new Gtk.Button () { + child = folder_chooser_button_child + }; + folder_chooser_button.clicked.connect (() => { + var chooser = new Gtk.FileChooserNative ( + _("Select folder where the cloned repository will be created"), + this.transient_for, + SELECT_FOLDER, + _("Select"), + _("Cancel") + ); + chooser.set_current_folder (folder_label.label); + chooser.response.connect ((res) => { + if (res == Gtk.ResponseType.ACCEPT) { + folder_label.label = chooser.get_filename (); + } + + chooser.destroy (); + }); + chooser.show (); + + }); + repository_local_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false, }; @@ -83,7 +117,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { content_box.add (new CloneEntry (_("User"), repository_user_entry)); content_box.add (new CloneEntry (_("Name"), repository_name_entry)); content_box.add (new Granite.HeaderLabel (_("Clone"))); - content_box.add (new CloneEntry (_("Target Folder"), repository_local_folder_entry)); + content_box.add (new CloneEntry (_("Target Folder"), folder_chooser_button)); content_box.add (new CloneEntry (_("Target Name"), repository_local_name_entry)); content_box.add (set_as_active_check); From 5287f593492b35be34a635d6c7f250b986c2bb59 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:05:20 +0100 Subject: [PATCH 12/56] Set mnemonic widget for CloneEntry label --- src/Dialogs/CloneRepositoryDialog.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 7fc32982b7..aee28cf7af 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -179,7 +179,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private class CloneEntry : Gtk.Box { public CloneEntry (string label_text, Gtk.Widget entry) { var label = new Gtk.Label (label_text) { - halign = START + halign = START, + mnemonic_widget = entry }; add (label); add (entry); From 9f8bf46dccdc13c73e2843877c60669fc12d4a83 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:15:15 +0100 Subject: [PATCH 13/56] Fix construction as advised, add comments --- src/Dialogs/CloneRepositoryDialog.vala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index aee28cf7af..bf5b7972db 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -32,12 +32,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { suggested_local_folder: _suggested_local_folder ); - repository_host_uri_entry.text = "https://github.com"; - repository_user_entry.text = "elementary"; - repository_name_entry.text = ""; - repository_local_name_entry.text = ""; - - on_is_valid_changed (); } construct { @@ -136,6 +130,15 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { clone_button.sensitive = can_clone; bind_property ("can-clone", clone_button, "sensitive"); + // Set default values. + //TODO Persist user choices for these + //TODO Use a dropdown of common/recent hosts? + //TODO Use a dropdown of recent user names? + repository_host_uri_entry.text = "https://github.com"; + repository_user_entry.text = "elementary"; + + on_is_valid_changed (); + show_all (); } From 497bac79a62925e30edc99ab197a6cf3d6e402b2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:31:19 +0100 Subject: [PATCH 14/56] Better variable names --- src/Dialogs/CloneRepositoryDialog.vala | 64 +++++++++++++------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index bf5b7972db..dba24e8233 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -16,11 +16,11 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Regex name_regex; private Regex local_folder_regex; private Regex url_regex; - private Granite.ValidatedEntry repository_host_uri_entry; - private Granite.ValidatedEntry repository_user_entry; - private Granite.ValidatedEntry repository_name_entry; - private Granite.ValidatedEntry repository_local_folder_entry; - private Granite.ValidatedEntry repository_local_name_entry; + private Granite.ValidatedEntry remote_host_uri_entry; + private Granite.ValidatedEntry remote_user_name_entry; + private Granite.ValidatedEntry remote_project_name_entry; + private Gtk.Label clone_parent_folder_label; + private Granite.ValidatedEntry local_project_name_entry; private Gtk.CheckButton set_as_active_check; public string suggested_local_folder { get; construct; } @@ -48,14 +48,14 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { secondary_text = _("The source repository and local folder must exist and be accessible"); badge_icon = new ThemedIcon ("download"); - repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { + remote_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { input_purpose = URL, activates_default = false }; - repository_user_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + remote_user_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false }; - repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + remote_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false }; @@ -63,14 +63,14 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { margin_end = 6 }; // The suggested folder is assumed to be valid as it is generated internally - var folder_label = new Gtk.Label (suggested_local_folder) { + clone_parent_folder_label = new Gtk.Label (suggested_local_folder) { hexpand = true, halign = START }; var view_more_image = new Gtk.Image.from_icon_name ("view-more-horizontal-symbolic", BUTTON); var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 0); folder_chooser_button_child.add (folder_image); - folder_chooser_button_child.add (folder_label); + folder_chooser_button_child.add (clone_parent_folder_label); folder_chooser_button_child.add (view_more_image); var folder_chooser_button = new Gtk.Button () { @@ -84,10 +84,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { _("Select"), _("Cancel") ); - chooser.set_current_folder (folder_label.label); + chooser.set_current_folder (clone_parent_folder_label.label); chooser.response.connect ((res) => { if (res == Gtk.ResponseType.ACCEPT) { - folder_label.label = chooser.get_filename (); + clone_parent_folder_label.label = chooser.get_filename (); } chooser.destroy (); @@ -96,7 +96,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }); - repository_local_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { + local_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { activates_default = false, }; @@ -107,12 +107,12 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { var content_box = new Gtk.Box (VERTICAL, 12); content_box.add (new Granite.HeaderLabel (_("Source Repository"))); - content_box.add (new CloneEntry (_("Host URI"), repository_host_uri_entry)); - content_box.add (new CloneEntry (_("User"), repository_user_entry)); - content_box.add (new CloneEntry (_("Name"), repository_name_entry)); + content_box.add (new CloneEntry (_("Host URI"), remote_host_uri_entry)); + content_box.add (new CloneEntry (_("User"), remote_user_name_entry)); + content_box.add (new CloneEntry (_("Name"), remote_project_name_entry)); content_box.add (new Granite.HeaderLabel (_("Clone"))); content_box.add (new CloneEntry (_("Target Folder"), folder_chooser_button)); - content_box.add (new CloneEntry (_("Target Name"), repository_local_name_entry)); + content_box.add (new CloneEntry (_("Target Name"), local_project_name_entry)); content_box.add (set_as_active_check); custom_bin.add (content_box); @@ -122,10 +122,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { clone_button.has_default = true; clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - repository_host_uri_entry.notify["is-valid"].connect (on_is_valid_changed); - repository_user_entry.notify["is-valid"].connect (on_is_valid_changed); - repository_name_entry.notify["is-valid"].connect (on_is_valid_changed); - repository_local_folder_entry.notify["is-valid"].connect (on_is_valid_changed); + remote_host_uri_entry.notify["is-valid"].connect (on_is_valid_changed); + remote_user_name_entry.notify["is-valid"].connect (on_is_valid_changed); + remote_project_name_entry.notify["is-valid"].connect (on_is_valid_changed); clone_button.sensitive = can_clone; bind_property ("can-clone", clone_button, "sensitive"); @@ -134,8 +133,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { //TODO Persist user choices for these //TODO Use a dropdown of common/recent hosts? //TODO Use a dropdown of recent user names? - repository_host_uri_entry.text = "https://github.com"; - repository_user_entry.text = "elementary"; + remote_host_uri_entry.text = "https://github.com"; + remote_user_name_entry.text = "elementary"; on_is_valid_changed (); @@ -146,9 +145,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { //TODO Further validation here? var repo_uri = Path.build_path ( Path.DIR_SEPARATOR_S, - repository_host_uri_entry.text, - repository_user_entry.text, - repository_name_entry.text + remote_host_uri_entry.text, + remote_user_name_entry.text, + remote_project_name_entry.text ); if (!repo_uri.has_suffix (".git")) { @@ -160,23 +159,22 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public string get_local_folder () requires (can_clone) { //TODO Further validation here? - return repository_local_folder_entry.text; + return clone_parent_folder_label.label; } public string get_local_name () requires (can_clone) { - var local_name = repository_local_name_entry.text; + var local_name = local_project_name_entry.text; if (local_name == "") { - local_name = repository_name_entry.text; + local_name = remote_project_name_entry.text; } //TODO Further validation here? return local_name; } private void on_is_valid_changed () { - can_clone = repository_host_uri_entry.is_valid && - repository_user_entry.is_valid && - repository_name_entry.is_valid && - repository_local_folder_entry.is_valid; + can_clone = remote_host_uri_entry.is_valid && + remote_user_name_entry.is_valid && + remote_project_name_entry.is_valid; } private class CloneEntry : Gtk.Box { From b4c79ed04315ed6385336051be35c7a2266e0622 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:40:48 +0100 Subject: [PATCH 15/56] Remove activates_default = false; --- src/Dialogs/CloneRepositoryDialog.vala | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index dba24e8233..411eb883b7 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -49,15 +49,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { badge_icon = new ThemedIcon ("download"); remote_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { - input_purpose = URL, - activates_default = false - }; - remote_user_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { - activates_default = false - }; - remote_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { - activates_default = false + input_purpose = URL }; + remote_user_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); + remote_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); var folder_image = new Gtk.Image.from_icon_name ("folder-download", BUTTON) { margin_end = 6 @@ -96,9 +91,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }); - local_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) { - activates_default = false, - }; + local_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) { margin_top = 12, From 55490d281d4c2b875c47a5c4bcf4e2a740771f48 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:46:34 +0100 Subject: [PATCH 16/56] Rewording --- src/Dialogs/CloneRepositoryDialog.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 411eb883b7..e75f2abdbe 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -43,9 +43,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - primary_text = _("Create a local clone of a Git repository"); ///TRANSLATORS "Git" is a proper name and must not be translated - secondary_text = _("The source repository and local folder must exist and be accessible"); + primary_text = _("Create a local clone of a Git repository"); + secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); badge_icon = new ThemedIcon ("download"); remote_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { From 7a4bf5d52ceaa532cbc28979b966f7866d55f551 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:48:41 +0100 Subject: [PATCH 17/56] Use "emblem-downloads" icon --- src/Dialogs/CloneRepositoryDialog.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index e75f2abdbe..a3a8536c23 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -46,7 +46,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { ///TRANSLATORS "Git" is a proper name and must not be translated primary_text = _("Create a local clone of a Git repository"); secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); - badge_icon = new ThemedIcon ("download"); + badge_icon = new ThemedIcon ("emblem-downloads"); remote_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { input_purpose = URL From a59c64be322b16c2c35b5cfd4919289829c39be1 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 17:52:35 +0100 Subject: [PATCH 18/56] No space before ellipsis --- src/Widgets/Sidebar.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index b24f37f16f..42374b5d7e 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -60,14 +60,14 @@ public class Code.Sidebar : Gtk.Grid { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER, action_target = new Variant.string (""), always_show_image = true, - label = _("Open …"), + label = _("Open…"), xalign = 0.0f }; var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, always_show_image = true, - label = _("Clone …"), + label = _("Clone…"), xalign = 0.0f }; From 6138cfac65103e5e803af16a1a55874fd14b7efd Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 18:17:44 +0100 Subject: [PATCH 19/56] Do not use Gtk.Button.from_icon_name --- src/Utils.vala | 13 +++++++++++++ src/Widgets/Sidebar.vala | 19 +++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Utils.vala b/src/Utils.vala index 54f9c20a40..2ea4e3879d 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -337,4 +337,17 @@ namespace Scratch.Utils { warning ("Accelerators were not found for the action: %s", detailed_action_name); return ""; } + + public Gtk.Button make_button_with_icon_and_label (string icon_name, string text) { + var box = new Gtk.Box (HORIZONTAL, 0); + var image = new Gtk.Image.from_icon_name (icon_name, BUTTON) { + margin_end = 3 + }; + var label = new Gtk.Label (text); + box.add (image); + box.add (label); + return new Gtk.Button () { + child = box + }; + } } diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 42374b5d7e..9409094601 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -56,21 +56,12 @@ public class Code.Sidebar : Gtk.Grid { var actionbar = new Gtk.ActionBar (); actionbar.get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR); - var add_folder_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { - action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER, - action_target = new Variant.string (""), - always_show_image = true, - label = _("Open…"), - xalign = 0.0f - }; - - var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { - action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, - always_show_image = true, - label = _("Clone…"), - xalign = 0.0f + var add_folder_button = Scratch.Utils.make_button_with_icon_and_label ("folder-open-symbolic", _("Open…")); + add_folder_button.action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER; + add_folder_button.action_target = new Variant.string (""); - }; + var clone_button = Scratch.Utils.make_button_with_icon_and_label ("folder-download-symbolic", _("Clone…")); + clone_button.action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO; var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_COLLAPSE_ALL_FOLDERS); From 0582c77b77bcb4740c72249d0e4a414c164329e4 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 18:47:58 +0100 Subject: [PATCH 20/56] More explicit labels --- src/Dialogs/CloneRepositoryDialog.vala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index a3a8536c23..b64f17d8ca 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -100,12 +100,12 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { var content_box = new Gtk.Box (VERTICAL, 12); content_box.add (new Granite.HeaderLabel (_("Source Repository"))); - content_box.add (new CloneEntry (_("Host URI"), remote_host_uri_entry)); - content_box.add (new CloneEntry (_("User"), remote_user_name_entry)); - content_box.add (new CloneEntry (_("Name"), remote_project_name_entry)); + content_box.add (new CloneEntry (_("Remote Host URI"), remote_host_uri_entry)); + content_box.add (new CloneEntry (_("Username on Remote"), remote_user_name_entry)); + content_box.add (new CloneEntry (_("Project Name on Remote"), remote_project_name_entry)); content_box.add (new Granite.HeaderLabel (_("Clone"))); - content_box.add (new CloneEntry (_("Target Folder"), folder_chooser_button)); - content_box.add (new CloneEntry (_("Target Name"), local_project_name_entry)); + content_box.add (new CloneEntry (_("Parent Folder of Clone"), folder_chooser_button)); + content_box.add (new CloneEntry (_("Name of Clone "), local_project_name_entry)); content_box.add (set_as_active_check); custom_bin.add (content_box); From ec4b2d0b5e5fdd147527d446b881b4bc0ec7c03d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 20:50:09 +0100 Subject: [PATCH 21/56] Simplify --- src/Dialogs/CloneRepositoryDialog.vala | 78 +++++++++++--------------- src/MainWindow.vala | 2 +- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index b64f17d8ca..9e56b0cc99 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -14,12 +14,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$"; private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)"; private Regex name_regex; - private Regex local_folder_regex; private Regex url_regex; - private Granite.ValidatedEntry remote_host_uri_entry; - private Granite.ValidatedEntry remote_user_name_entry; - private Granite.ValidatedEntry remote_project_name_entry; private Gtk.Label clone_parent_folder_label; + private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; private Gtk.CheckButton set_as_active_check; @@ -42,28 +39,25 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { warning ("%s\n", e.message); } - add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); ///TRANSLATORS "Git" is a proper name and must not be translated primary_text = _("Create a local clone of a Git repository"); secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); badge_icon = new ThemedIcon ("emblem-downloads"); - remote_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { - input_purpose = URL + remote_repository_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { + input_purpose = URL, + placeholder_text = _("https:////.git") }; - remote_user_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); - remote_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); - var folder_image = new Gtk.Image.from_icon_name ("folder-download", BUTTON) { - margin_end = 6 - }; + var folder_image = new Gtk.Image.from_icon_name ("folder-download", BUTTON); // The suggested folder is assumed to be valid as it is generated internally clone_parent_folder_label = new Gtk.Label (suggested_local_folder) { hexpand = true, halign = START }; var view_more_image = new Gtk.Image.from_icon_name ("view-more-horizontal-symbolic", BUTTON); - var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 0); + var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 6); folder_chooser_button_child.add (folder_image); folder_chooser_button_child.add (clone_parent_folder_label); folder_chooser_button_child.add (view_more_image); @@ -100,9 +94,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { var content_box = new Gtk.Box (VERTICAL, 12); content_box.add (new Granite.HeaderLabel (_("Source Repository"))); - content_box.add (new CloneEntry (_("Remote Host URI"), remote_host_uri_entry)); - content_box.add (new CloneEntry (_("Username on Remote"), remote_user_name_entry)); - content_box.add (new CloneEntry (_("Project Name on Remote"), remote_project_name_entry)); + content_box.add (new CloneEntry (_("Remote Repository URI"), remote_repository_uri_entry)); content_box.add (new Granite.HeaderLabel (_("Clone"))); content_box.add (new CloneEntry (_("Parent Folder of Clone"), folder_chooser_button)); content_box.add (new CloneEntry (_("Name of Clone "), local_project_name_entry)); @@ -115,59 +107,57 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { clone_button.has_default = true; clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - remote_host_uri_entry.notify["is-valid"].connect (on_is_valid_changed); - remote_user_name_entry.notify["is-valid"].connect (on_is_valid_changed); - remote_project_name_entry.notify["is-valid"].connect (on_is_valid_changed); + remote_repository_uri_entry.notify["is-valid"].connect (on_is_valid_changed); clone_button.sensitive = can_clone; bind_property ("can-clone", clone_button, "sensitive"); - // Set default values. - //TODO Persist user choices for these - //TODO Use a dropdown of common/recent hosts? - //TODO Use a dropdown of recent user names? - remote_host_uri_entry.text = "https://github.com"; - remote_user_name_entry.text = "elementary"; - on_is_valid_changed (); show_all (); + + // Focus cancel button so that entry placeholder text shows + cancel_button.grab_focus (); } public string get_source_repository_uri () requires (can_clone) { //TODO Further validation here? - var repo_uri = Path.build_path ( - Path.DIR_SEPARATOR_S, - remote_host_uri_entry.text, - remote_user_name_entry.text, - remote_project_name_entry.text - ); - - if (!repo_uri.has_suffix (".git")) { - repo_uri += ".git"; - } - - return repo_uri; + return remote_repository_uri_entry.text; } public string get_local_folder () requires (can_clone) { - //TODO Further validation here? return clone_parent_folder_label.label; } public string get_local_name () requires (can_clone) { var local_name = local_project_name_entry.text; if (local_name == "") { - local_name = remote_project_name_entry.text; + var uri_string = remote_repository_uri_entry.text; + string? scheme, userinfo, host, path, query,fragment; + int port; + try { + Uri.split ( + uri_string, + UriFlags.PARSE_RELAXED, + out scheme, out userinfo, out host, out port, out path, out query, out fragment + ); + + if (path.has_suffix (".git")) { + path = path.slice (0, -4); + } + + local_name = Path.get_basename (path); + } catch (UriError e) { + warning ("Could not parse remote uri"); + can_clone = false; + } } - //TODO Further validation here? + return local_name; } private void on_is_valid_changed () { - can_clone = remote_host_uri_entry.is_valid && - remote_user_name_entry.is_valid && - remote_project_name_entry.is_valid; + can_clone = remote_repository_uri_entry.is_valid; } private class CloneEntry : Gtk.Box { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 2b465914e5..47789e0625 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1058,7 +1058,7 @@ namespace Scratch { clone_dialog.destroy (); }); - clone_dialog.run (); + clone_dialog.present (); if (clone_dialog.can_clone) { //TODO Show progress while cloning From ffbb88345d279b59141b0abef7b530bedad4ba69 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 20:53:46 +0100 Subject: [PATCH 22/56] Show all only on content_box --- src/Dialogs/CloneRepositoryDialog.vala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 9e56b0cc99..f484f5d64c 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -99,6 +99,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { content_box.add (new CloneEntry (_("Parent Folder of Clone"), folder_chooser_button)); content_box.add (new CloneEntry (_("Name of Clone "), local_project_name_entry)); content_box.add (set_as_active_check); + content_box.show_all (); custom_bin.add (content_box); @@ -114,8 +115,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { on_is_valid_changed (); - show_all (); - // Focus cancel button so that entry placeholder text shows cancel_button.grab_focus (); } From 636d871eaaea097a312d5b54ea2bf453be41abba Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 28 May 2025 20:56:18 +0100 Subject: [PATCH 23/56] Use box padding not margin --- src/Utils.vala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Utils.vala b/src/Utils.vala index 2ea4e3879d..46b83a56b0 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -339,10 +339,8 @@ namespace Scratch.Utils { } public Gtk.Button make_button_with_icon_and_label (string icon_name, string text) { - var box = new Gtk.Box (HORIZONTAL, 0); - var image = new Gtk.Image.from_icon_name (icon_name, BUTTON) { - margin_end = 3 - }; + var box = new Gtk.Box (HORIZONTAL, 3); + var image = new Gtk.Image.from_icon_name (icon_name, BUTTON); var label = new Gtk.Label (text); box.add (image); box.add (label); From 7cadd28184d3e0f127760dff53d9b72ae1270ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Fri, 30 May 2025 01:35:17 -0700 Subject: [PATCH 24/56] CloneRepositoryDialog: design tweaks (#1584) --- src/Dialogs/CloneRepositoryDialog.vala | 30 +++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index f484f5d64c..ed7e818c3a 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -26,9 +26,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { Object ( transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), image_icon: new ThemedIcon ("git"), + modal: true, suggested_local_folder: _suggested_local_folder ); - } construct { @@ -40,6 +40,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + ///TRANSLATORS "Git" is a proper name and must not be translated primary_text = _("Create a local clone of a Git repository"); secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); @@ -47,20 +48,20 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { remote_repository_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { input_purpose = URL, - placeholder_text = _("https:////.git") + placeholder_text = _("https://example.com/username/projectname.git") }; - var folder_image = new Gtk.Image.from_icon_name ("folder-download", BUTTON); // The suggested folder is assumed to be valid as it is generated internally clone_parent_folder_label = new Gtk.Label (suggested_local_folder) { hexpand = true, halign = START }; - var view_more_image = new Gtk.Image.from_icon_name ("view-more-horizontal-symbolic", BUTTON); + var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 6); - folder_chooser_button_child.add (folder_image); folder_chooser_button_child.add (clone_parent_folder_label); - folder_chooser_button_child.add (view_more_image); + folder_chooser_button_child.add ( + new Gtk.Image.from_icon_name ("folder-open-symbolic", BUTTON) + ); var folder_chooser_button = new Gtk.Button () { child = folder_chooser_button_child @@ -93,11 +94,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }; var content_box = new Gtk.Box (VERTICAL, 12); - content_box.add (new Granite.HeaderLabel (_("Source Repository"))); - content_box.add (new CloneEntry (_("Remote Repository URI"), remote_repository_uri_entry)); - content_box.add (new Granite.HeaderLabel (_("Clone"))); - content_box.add (new CloneEntry (_("Parent Folder of Clone"), folder_chooser_button)); - content_box.add (new CloneEntry (_("Name of Clone "), local_project_name_entry)); + content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); + content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); + content_box.add (new CloneEntry (_("Name of Clone"), local_project_name_entry)); content_box.add (set_as_active_check); content_box.show_all (); @@ -161,21 +160,16 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private class CloneEntry : Gtk.Box { public CloneEntry (string label_text, Gtk.Widget entry) { - var label = new Gtk.Label (label_text) { - halign = START, + var label = new Granite.HeaderLabel (label_text) { mnemonic_widget = entry }; + add (label); add (entry); } construct { orientation = VERTICAL; - spacing = 6; - hexpand = false; - margin_start = 12; - margin_end = 12; - margin_bottom = 12; } } } From 2c315a2bc66be447ec2f260e218fae8cf537b07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Fri, 30 May 2025 01:39:08 -0700 Subject: [PATCH 25/56] Move buttons from actionbar to projectchooser (#1583) * Move buttons from actionbar to projectchooser * Fix lint and merge error * Create PopoverMenuItem, add symbolic git icon --------- Co-authored-by: Jeremy Wootten --- data/icons/git-symbolic.svg | 4 +++ data/io.elementary.code.gresource.xml | 3 +- src/Utils.vala | 11 ------ src/Widgets/ChooseProjectButton.vala | 14 ++++++++ src/Widgets/PopoverMenuItem.vala | 48 +++++++++++++++++++++++++++ src/Widgets/Sidebar.vala | 29 ++++------------ src/meson.build | 1 + 7 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 data/icons/git-symbolic.svg create mode 100644 src/Widgets/PopoverMenuItem.vala diff --git a/data/icons/git-symbolic.svg b/data/icons/git-symbolic.svg new file mode 100644 index 0000000000..8d3a0dbd30 --- /dev/null +++ b/data/icons/git-symbolic.svg @@ -0,0 +1,4 @@ + +image/svg+xml + + diff --git a/data/io.elementary.code.gresource.xml b/data/io.elementary.code.gresource.xml index 2149e3b022..4f1e7c8c50 100644 --- a/data/io.elementary.code.gresource.xml +++ b/data/io.elementary.code.gresource.xml @@ -2,7 +2,6 @@ Application.css - icons/48/git.svg icons/SymbolOutline/abstractclass.svg icons/SymbolOutline/abstractmethod.svg icons/SymbolOutline/abstractproperty.svg @@ -30,8 +29,10 @@ icons/panel-right-symbolic.svg + icons/48/git.svg icons/48/open-project.svg icons/filter-symbolic.svg + icons/git-symbolic.svg icons/emblem-git-modified-symbolic.svg icons/emblem-git-new-symbolic.svg diff --git a/src/Utils.vala b/src/Utils.vala index 46b83a56b0..54f9c20a40 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -337,15 +337,4 @@ namespace Scratch.Utils { warning ("Accelerators were not found for the action: %s", detailed_action_name); return ""; } - - public Gtk.Button make_button_with_icon_and_label (string icon_name, string text) { - var box = new Gtk.Box (HORIZONTAL, 3); - var image = new Gtk.Image.from_icon_name (icon_name, BUTTON); - var label = new Gtk.Label (text); - box.add (image); - box.add (label); - return new Gtk.Button () { - child = box - }; - } } diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 90fba92596..877f54925b 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -71,9 +71,23 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { project_scrolled.add (project_listbox); + var add_folder_button = new PopoverMenuItem (_("Open Folder…")) { + action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER, + action_target = new Variant.string (""), + icon_name = "folder-open-symbolic" + }; + + var clone_button = new PopoverMenuItem (_("Clone Git Repository…")) { + action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO, + icon_name = "git-symbolic" + }; + var popover_content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); popover_content.add (project_filter); popover_content.add (project_scrolled); + popover_content.add (new Gtk.Separator (HORIZONTAL)); + popover_content.add (add_folder_button); + popover_content.add (clone_button); popover_content.show_all (); diff --git a/src/Widgets/PopoverMenuItem.vala b/src/Widgets/PopoverMenuItem.vala new file mode 100644 index 0000000000..72461986fc --- /dev/null +++ b/src/Widgets/PopoverMenuItem.vala @@ -0,0 +1,48 @@ +/* +* SPDX-License-Identifier: GPL-2.0-or-later +* SPDX-FileCopyrightText: 2017-2023 elementary, Inc. (https://elementary.io) +*/ + +public class Code.PopoverMenuItem : Gtk.Button { + /** + * The label for the button + */ + public string text { get; construct; } + + /** + * The icon name for the button + */ + public string icon_name { get; set; } + + public PopoverMenuItem (string text) { + Object (text: text); + } + + class construct { + set_css_name ("modelbutton"); + } + + construct { + var image = new Gtk.Image (); + + var label = new Granite.AccelLabel (text); + + var box = new Gtk.Box (HORIZONTAL, 6); + box.add (image); + box.add (label); + + child = box; + + get_accessible ().accessible_role = MENU_ITEM; + + clicked.connect (() => { + var popover = (Gtk.Popover) get_ancestor (typeof (Gtk.Popover)); + if (popover != null) { + popover.popdown (); + } + }); + + bind_property ("action-name", label, "action-name"); + bind_property ("icon-name", image, "icon-name"); + } +} diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 9409094601..0656c33d0c 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -56,13 +56,6 @@ public class Code.Sidebar : Gtk.Grid { var actionbar = new Gtk.ActionBar (); actionbar.get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR); - var add_folder_button = Scratch.Utils.make_button_with_icon_and_label ("folder-open-symbolic", _("Open…")); - add_folder_button.action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER; - add_folder_button.action_target = new Variant.string (""); - - var clone_button = Scratch.Utils.make_button_with_icon_and_label ("folder-download-symbolic", _("Clone…")); - clone_button.action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO; - var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_COLLAPSE_ALL_FOLDERS); @@ -74,23 +67,15 @@ public class Code.Sidebar : Gtk.Grid { project_menu.append_item (order_projects_menu_item); project_menu_model = project_menu; - var project_more_button = new Gtk.MenuButton (); - project_more_button.image = new Gtk.Image.from_icon_name ("view-more-symbolic", Gtk.IconSize.SMALL_TOOLBAR); - project_more_button.use_popover = false; - project_more_button.menu_model = project_menu_model; - project_more_button.tooltip_text = _("Manage project folders"); - - var tool_flowbox = new Gtk.FlowBox () { - orientation = HORIZONTAL, + var project_more_button = new Gtk.MenuButton () { hexpand = true, - selection_mode = NONE, - column_spacing = 0, - max_children_per_line = 2 + use_popover = false, + menu_model = project_menu_model, + label = _("Manage project folders…"), + xalign = 0.0f }; - tool_flowbox.add (add_folder_button); - tool_flowbox.add (clone_button); - actionbar.add (tool_flowbox); - actionbar.pack_end (project_more_button); + + actionbar.pack_start (project_more_button); add (headerbar); add (stack_switcher); diff --git a/src/meson.build b/src/meson.build index ec53d58bfc..b5a5aca596 100644 --- a/src/meson.build +++ b/src/meson.build @@ -50,6 +50,7 @@ code_files = files( 'Widgets/HeaderBar.vala', 'Widgets/Sidebar.vala', 'Widgets/PaneSwitcher.vala', + 'Widgets/PopoverMenuItem.vala', 'Widgets/SearchBar.vala', 'Widgets/SourceList/CellRendererBadge.vala', 'Widgets/SourceList/CellRendererExpander.vala', From 1bfa1aaa45acd059932a8ea26b4e7365d01aad86 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 1 Jun 2025 18:41:48 +0100 Subject: [PATCH 26/56] Check URL is in correct form using function not regex --- src/Dialogs/CloneRepositoryDialog.vala | 66 ++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index ed7e818c3a..1ff65088de 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -12,9 +12,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { //Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala" private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$"; - private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)"; private Regex name_regex; - private Regex url_regex; private Gtk.Label clone_parent_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; @@ -34,7 +32,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { construct { try { name_regex = new Regex (NAME_REGEX, RegexCompileFlags.OPTIMIZE); - url_regex = new Regex (URL_REGEX, RegexCompileFlags.OPTIMIZE); } catch (RegexError e) { warning ("%s\n", e.message); } @@ -46,10 +43,11 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); badge_icon = new ThemedIcon ("emblem-downloads"); - remote_repository_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) { - input_purpose = URL, - placeholder_text = _("https://example.com/username/projectname.git") + remote_repository_uri_entry = new Granite.ValidatedEntry () { + placeholder_text = _("https://example.com/username/projectname.git"), + input_purpose = URL }; + remote_repository_uri_entry.changed.connect (on_remote_uri_changed); // The suggested folder is assumed to be valid as it is generated internally clone_parent_folder_label = new Gtk.Label (suggested_local_folder) { @@ -107,12 +105,12 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { clone_button.has_default = true; clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - remote_repository_uri_entry.notify["is-valid"].connect (on_is_valid_changed); - clone_button.sensitive = can_clone; bind_property ("can-clone", clone_button, "sensitive"); - on_is_valid_changed (); + //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry + //text changed. So call explicitly after we validate the text. + can_clone = false; // Focus cancel button so that entry placeholder text shows cancel_button.grab_focus (); @@ -154,10 +152,58 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { return local_name; } - private void on_is_valid_changed () { + private void update_can_clone () { can_clone = remote_repository_uri_entry.is_valid; } + private void on_remote_uri_changed (Gtk.Editable source) { + var entry = (Granite.ValidatedEntry)source; + if (entry.is_valid) { //entry is a URL + //Only accept HTTPS url atm but may also accept ssh address in future + entry.is_valid = validate_https_address (entry.text); + } + + update_can_clone (); + } + + private bool validate_https_address (string address) { + var valid = false; + string? scheme, userinfo, host, path, query, fragment; + int port; + try { + Uri.split ( + address, + UriFlags.NONE, + out scheme, + out userinfo, + out host, + out port, + out path, + out query, + out fragment + ); + + if (query == null && + fragment == null && + scheme == "https" && + host != null && //e.g. github.com + userinfo == null && //User is first part of pat + (port < 0 || port == 443)) { //TODO Allow non-standard port to be selected + + if (path.has_prefix (Path.DIR_SEPARATOR_S)) { + path = path.substring (1, -1); + } + + var parts = path.split (Path.DIR_SEPARATOR_S); + valid = parts.length == 2 && parts[1].has_suffix (".git"); + } + } catch (UriError e) { + warning ("Uri split error %s", e.message); + } + + return valid; + } + private class CloneEntry : Gtk.Box { public CloneEntry (string label_text, Gtk.Widget entry) { var label = new Granite.HeaderLabel (label_text) { From 3273fe5bb5c1256ae507e793076a9bb5987ad361 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 12:03:58 +0100 Subject: [PATCH 27/56] Prefill local name; improve NAME_REGEX --- src/Dialogs/CloneRepositoryDialog.vala | 44 ++++++++++---------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 1ff65088de..93e3b1f65e 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -10,8 +10,13 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public bool can_clone { get; private set; default = false; } - //Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala" - private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$"; + //Git project name rules: https://github.com/jeremypw/code-dogfood-7.1.git + // - Must start and end with a letter ( a-zA-Z ) or digit ( 0-9 ). + // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). + // - Must not contain consecutive special characters. + // - Cannot end in . git or . atom . + + private const string NAME_REGEX = "^[0-9a-zA-Z].[-0-9a-zA-Z_.]+$"; //TODO additional validation required private Regex name_regex; private Gtk.Label clone_parent_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; @@ -85,6 +90,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }); local_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); + local_project_name_entry.changed.connect (update_can_clone); set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) { margin_top = 12, @@ -126,34 +132,13 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } public string get_local_name () requires (can_clone) { - var local_name = local_project_name_entry.text; - if (local_name == "") { - var uri_string = remote_repository_uri_entry.text; - string? scheme, userinfo, host, path, query,fragment; - int port; - try { - Uri.split ( - uri_string, - UriFlags.PARSE_RELAXED, - out scheme, out userinfo, out host, out port, out path, out query, out fragment - ); - - if (path.has_suffix (".git")) { - path = path.slice (0, -4); - } - - local_name = Path.get_basename (path); - } catch (UriError e) { - warning ("Could not parse remote uri"); - can_clone = false; - } - } - - return local_name; + return local_project_name_entry.text; } private void update_can_clone () { - can_clone = remote_repository_uri_entry.is_valid; + can_clone = remote_repository_uri_entry.is_valid && local_project_name_entry.is_valid; + // We can assume the folder entry is valid as it defaults to a valid folder and + // can only be changed with the filechooser. } private void on_remote_uri_changed (Gtk.Editable source) { @@ -196,12 +181,15 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { var parts = path.split (Path.DIR_SEPARATOR_S); valid = parts.length == 2 && parts[1].has_suffix (".git"); + if (valid) { + local_project_name_entry.text = parts[1].slice (0, -4); + } } } catch (UriError e) { warning ("Uri split error %s", e.message); } - return valid; + return valid; } private class CloneEntry : Gtk.Box { From 67be8d9306d725af7e3f35fc5e09f1e7ea60a3ee Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:00:52 +0100 Subject: [PATCH 28/56] Restrict project folder names --- src/Dialogs/CloneRepositoryDialog.vala | 28 +++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 93e3b1f65e..24f2a3bfa1 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -16,7 +16,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { // - Must not contain consecutive special characters. // - Cannot end in . git or . atom . - private const string NAME_REGEX = "^[0-9a-zA-Z].[-0-9a-zA-Z_.]+$"; //TODO additional validation required + private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required private Regex name_regex; private Gtk.Label clone_parent_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; @@ -36,7 +36,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { construct { try { - name_regex = new Regex (NAME_REGEX, RegexCompileFlags.OPTIMIZE); + name_regex = new Regex (NAME_REGEX, OPTIMIZE, ANCHORED | NOTEMPTY); } catch (RegexError e) { warning ("%s\n", e.message); } @@ -89,8 +89,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }); - local_project_name_entry = new Granite.ValidatedEntry.from_regex (name_regex); - local_project_name_entry.changed.connect (update_can_clone); + local_project_name_entry = new Granite.ValidatedEntry (); + local_project_name_entry.changed.connect (validate_local_name); set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) { margin_top = 12, @@ -137,7 +137,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private void update_can_clone () { can_clone = remote_repository_uri_entry.is_valid && local_project_name_entry.is_valid; - // We can assume the folder entry is valid as it defaults to a valid folder and + // We can assume the folder entry is valid as it defaults to a valid folder and // can only be changed with the filechooser. } @@ -192,6 +192,24 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { return valid; } + private void validate_local_name () { + unowned var name = local_project_name_entry.text; + MatchInfo? match_info; + bool valid = false; + try { + name_regex.match (name, ANCHORED | NOTEMPTY, out match_info); + if (match_info.matches ()) { + valid = !name.has_suffix (".git") && !name.has_suffix (".atom"); + } + } catch (Error e) { + warning ("Error match regex"); + } finally { + local_project_name_entry.is_valid = valid; + } + + update_can_clone (); + } + private class CloneEntry : Gtk.Box { public CloneEntry (string label_text, Gtk.Widget entry) { var label = new Granite.HeaderLabel (label_text) { From 731a05a5b12e6636d4c1b0d8a01a5932de9d6313 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:19:00 +0100 Subject: [PATCH 29/56] Fix extraneous comment --- src/Dialogs/CloneRepositoryDialog.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 24f2a3bfa1..116276028c 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -10,7 +10,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public bool can_clone { get; private set; default = false; } - //Git project name rules: https://github.com/jeremypw/code-dogfood-7.1.git + // Git project name rules according to GitLab // - Must start and end with a letter ( a-zA-Z ) or digit ( 0-9 ). // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). // - Must not contain consecutive special characters. From e3a22b95ed8391b920c15b1af608b7a3d6b032c5 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:19:44 +0100 Subject: [PATCH 30/56] Remove set as active checkbox for now --- src/Dialogs/CloneRepositoryDialog.vala | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 116276028c..ad5ee2685c 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -6,7 +6,6 @@ */ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { - public FolderManager.ProjectFolderItem? active_project { get; construct; } public bool can_clone { get; private set; default = false; } @@ -21,7 +20,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Gtk.Label clone_parent_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; - private Gtk.CheckButton set_as_active_check; public string suggested_local_folder { get; construct; } @@ -92,16 +90,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry = new Granite.ValidatedEntry (); local_project_name_entry.changed.connect (validate_local_name); - set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) { - margin_top = 12, - active = true - }; - var content_box = new Gtk.Box (VERTICAL, 12); content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); content_box.add (new CloneEntry (_("Name of Clone"), local_project_name_entry)); - content_box.add (set_as_active_check); content_box.show_all (); custom_bin.add (content_box); From 25bbd0e155121ce138de0894157b4ba7ba484bb6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:20:03 +0100 Subject: [PATCH 31/56] Use SYNC_CREATE --- src/Dialogs/CloneRepositoryDialog.vala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index ad5ee2685c..6f3ef655a2 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -103,8 +103,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { clone_button.has_default = true; clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - clone_button.sensitive = can_clone; - bind_property ("can-clone", clone_button, "sensitive"); + bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry //text changed. So call explicitly after we validate the text. From 4d66c36dcd2c5bbe577173e5cf946cb814de1400 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:20:23 +0100 Subject: [PATCH 32/56] Fix unneeded try-catch --- src/Dialogs/CloneRepositoryDialog.vala | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 6f3ef655a2..bdaeee14f3 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -187,17 +187,11 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { unowned var name = local_project_name_entry.text; MatchInfo? match_info; bool valid = false; - try { - name_regex.match (name, ANCHORED | NOTEMPTY, out match_info); - if (match_info.matches ()) { - valid = !name.has_suffix (".git") && !name.has_suffix (".atom"); - } - } catch (Error e) { - warning ("Error match regex"); - } finally { - local_project_name_entry.is_valid = valid; + if (name_regex.match (name, ANCHORED | NOTEMPTY, out match_info) && match_info.matches ()) { + valid = !name.has_suffix (".git") && !name.has_suffix (".atom"); } + local_project_name_entry.is_valid = valid; update_can_clone (); } From 379ef30ab957777e123dc1bb657f1587d8a67982 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:30:46 +0100 Subject: [PATCH 33/56] Move cloning into response callback --- src/MainWindow.vala | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 5b2a1ace58..070ab0ed01 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1049,38 +1049,34 @@ namespace Scratch { var local_name = ""; var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); clone_dialog.response.connect ((res) => { - if (res == Gtk.ResponseType.APPLY) { + if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? uri = clone_dialog.get_source_repository_uri (); local_folder = clone_dialog.get_local_folder (); local_name = clone_dialog.get_local_name (); + + //TODO Show progress while cloning + git_manager.clone_repository.begin ( + uri, + Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name), + (obj, res) => { + try { + File? workdir = null; + if (git_manager.clone_repository.end (res, out workdir)) { + debug ("Repository cloned into %s", workdir.get_uri ()); + open_folder (workdir); + //TODO Make active according to dialog checkbox + } + } catch (Error e) { + warning ("Unable to clone '%s'. %s", uri, e.message); + } + } + ); } clone_dialog.destroy (); }); clone_dialog.present (); - - if (clone_dialog.can_clone) { - //TODO Show progress while cloning - git_manager.clone_repository.begin ( - uri, - Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name), - (obj, res) => { - try { - File? workdir = null; - if (git_manager.clone_repository.end (res, out workdir)) { - debug ("Repository cloned into %s", workdir.get_uri ()); - open_folder (workdir); - //TODO Make active according to dialog checkbox - } - } catch (Error e) { - warning ("Unable to clone '%s'. %s", uri, e.message); - } - } - ); - } else { - //TODO Give feedback - } } private void action_collapse_all_folders () { From c2fe76f3a21a09bed37b9231a355fbc329139c55 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 16:39:30 +0100 Subject: [PATCH 34/56] Close dialog before cloning starts --- src/MainWindow.vala | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 070ab0ed01..700d775cc0 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1043,17 +1043,15 @@ namespace Scratch { } private void action_clone_repo (SimpleAction action, Variant? param) { - var uri = ""; - //By default, create clone in parent of the current project - var local_folder = Path.get_dirname (git_manager.active_project_path); - var local_name = ""; var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); clone_dialog.response.connect ((res) => { + var uri = clone_dialog.get_source_repository_uri (); + var local_folder = clone_dialog.get_local_folder (); + var local_name = clone_dialog.get_local_name (); + // MainWindow should provide feedback on cloning progress + // Close modal dialog now + clone_dialog.destroy (); if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? - uri = clone_dialog.get_source_repository_uri (); - local_folder = clone_dialog.get_local_folder (); - local_name = clone_dialog.get_local_name (); - //TODO Show progress while cloning git_manager.clone_repository.begin ( uri, @@ -1064,7 +1062,6 @@ namespace Scratch { if (git_manager.clone_repository.end (res, out workdir)) { debug ("Repository cloned into %s", workdir.get_uri ()); open_folder (workdir); - //TODO Make active according to dialog checkbox } } catch (Error e) { warning ("Unable to clone '%s'. %s", uri, e.message); @@ -1072,8 +1069,6 @@ namespace Scratch { } ); } - - clone_dialog.destroy (); }); clone_dialog.present (); From 22e57b741e5cfcb23abd8930528136e6c6bcfcfc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Jun 2025 19:58:38 +0100 Subject: [PATCH 35/56] Add TODO comment --- src/Services/GitManager.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 20097fbbd1..2a735f4767 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -113,6 +113,7 @@ namespace Scratch.Services { return build_path; } + //TODO Make this a real async function that does not block the main loop. public async bool clone_repository ( string uri, string local_folder, From 7d6020858ca67ab2406ea5b3e4f68c4b9dc4ef2f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Jun 2025 10:37:08 +0100 Subject: [PATCH 36/56] Fix undefined variable --- src/MainWindow.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 700d775cc0..09da4ad897 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1043,7 +1043,9 @@ namespace Scratch { } private void action_clone_repo (SimpleAction action, Variant? param) { - var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder); + var default_folder = git_manager.active_project_path != "" ? + Path.get_dirname (git_manager.active_project_path) : ""; + var clone_dialog = new Dialogs.CloneRepositoryDialog (default_folder); clone_dialog.response.connect ((res) => { var uri = clone_dialog.get_source_repository_uri (); var local_folder = clone_dialog.get_local_folder (); From 2595107e6e6fdd03f68ae4a787511de976914ba8 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Jun 2025 10:59:30 +0100 Subject: [PATCH 37/56] Handle clone failure with dialog - option to retry --- src/MainWindow.vala | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 09da4ad897..02ed5de634 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1051,8 +1051,8 @@ namespace Scratch { var local_folder = clone_dialog.get_local_folder (); var local_name = clone_dialog.get_local_name (); // MainWindow should provide feedback on cloning progress - // Close modal dialog now - clone_dialog.destroy (); + // Hide clone dialog in case needed to retry + clone_dialog.hide (); if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? //TODO Show progress while cloning git_manager.clone_repository.begin ( @@ -1064,9 +1064,25 @@ namespace Scratch { if (git_manager.clone_repository.end (res, out workdir)) { debug ("Repository cloned into %s", workdir.get_uri ()); open_folder (workdir); + clone_dialog.destroy (); } } catch (Error e) { - warning ("Unable to clone '%s'. %s", uri, e.message); + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + "Uanble to clone %s".printf (uri), + e.message, + "dialog-error", + Gtk.ButtonsType.CLOSE + ); + message_dialog.add_button (_("Retry"), 1); + message_dialog.response.connect ((res) => { + if (res == 1) { + clone_dialog.show (); + } else { + clone_dialog.destroy (); + } + message_dialog.destroy (); + }); + message_dialog.present (); } } ); From 1454a5b9874af10cc4f4b790670af18676980f9f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Jun 2025 11:17:00 +0100 Subject: [PATCH 38/56] Handle no current active project --- src/Dialogs/CloneRepositoryDialog.vala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index bdaeee14f3..e4357c8b4a 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -79,6 +79,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { chooser.response.connect ((res) => { if (res == Gtk.ResponseType.ACCEPT) { clone_parent_folder_label.label = chooser.get_filename (); + update_can_clone (); } chooser.destroy (); @@ -127,9 +128,11 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } private void update_can_clone () { - can_clone = remote_repository_uri_entry.is_valid && local_project_name_entry.is_valid; - // We can assume the folder entry is valid as it defaults to a valid folder and - // can only be changed with the filechooser. + can_clone = remote_repository_uri_entry.is_valid && + local_project_name_entry.is_valid && + clone_parent_folder_label.label != ""; + + //TODO Check whether the target folder already exists and is not empty? } private void on_remote_uri_changed (Gtk.Editable source) { From 1b3232d70e714b666b010622eefb75f283252c97 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Jun 2025 12:15:55 +0100 Subject: [PATCH 39/56] Persist projects folder and remote entries --- data/io.elementary.code.gschema.xml | 10 ++++++ src/Dialogs/CloneRepositoryDialog.vala | 43 +++++++++++++++++--------- src/MainWindow.vala | 20 +++++++----- src/Services/GitManager.vala | 1 + 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/data/io.elementary.code.gschema.xml b/data/io.elementary.code.gschema.xml index 397b293ed1..0f78a64f73 100644 --- a/data/io.elementary.code.gschema.xml +++ b/data/io.elementary.code.gschema.xml @@ -162,6 +162,16 @@ The default build directory's relative path. The directory, relative to the project root, at which to open the terminal pane and where to run build commands by default. + + '' + The default Projects folder + The path to the folder below which projects are saved or cloned + + + '' + The default git remote + The URL of the remote from where repositories can be cloned, for example https://github.com/elementary/ + false Request dark Gtk stylesheet variant diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index e4357c8b4a..a4a4aa405e 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -17,18 +17,20 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required private Regex name_regex; - private Gtk.Label clone_parent_folder_label; + private Gtk.Label projects_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; public string suggested_local_folder { get; construct; } + public string suggested_remote { get; construct; } - public CloneRepositoryDialog (string _suggested_local_folder) { + public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { Object ( transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), image_icon: new ThemedIcon ("git"), modal: true, - suggested_local_folder: _suggested_local_folder + suggested_local_folder: _suggested_local_folder, + suggested_remote: _suggested_remote ); } @@ -51,15 +53,16 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { input_purpose = URL }; remote_repository_uri_entry.changed.connect (on_remote_uri_changed); + remote_repository_uri_entry.text = suggested_remote; // The suggested folder is assumed to be valid as it is generated internally - clone_parent_folder_label = new Gtk.Label (suggested_local_folder) { + projects_folder_label = new Gtk.Label (suggested_local_folder) { hexpand = true, halign = START }; var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 6); - folder_chooser_button_child.add (clone_parent_folder_label); + folder_chooser_button_child.add (projects_folder_label); folder_chooser_button_child.add ( new Gtk.Image.from_icon_name ("folder-open-symbolic", BUTTON) ); @@ -75,10 +78,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { _("Select"), _("Cancel") ); - chooser.set_current_folder (clone_parent_folder_label.label); + chooser.set_current_folder (projects_folder_label.label); chooser.response.connect ((res) => { if (res == Gtk.ResponseType.ACCEPT) { - clone_parent_folder_label.label = chooser.get_filename (); + projects_folder_label.label = chooser.get_filename (); update_can_clone (); } @@ -114,23 +117,33 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { cancel_button.grab_focus (); } - public string get_source_repository_uri () requires (can_clone) { - //TODO Further validation here? - return remote_repository_uri_entry.text; + public string get_projects_folder () { + return projects_folder_label.label; } - public string get_local_folder () requires (can_clone) { - return clone_parent_folder_label.label; + public string get_remote () { + if (remote_repository_uri_entry.is_valid) { + var uri = remote_repository_uri_entry.text; + var last_separator = uri.last_index_of (Path.DIR_SEPARATOR_S); + return uri.slice (0, last_separator + 1); + } else { + return suggested_remote; + } + } + + public string get_valid_source_repository_uri () requires (can_clone) { + //TODO Further validation here? + return remote_repository_uri_entry.text; } - public string get_local_name () requires (can_clone) { - return local_project_name_entry.text; + public string get_valid_target () requires (can_clone) { + return Path.build_filename (Path.DIR_SEPARATOR_S, projects_folder_label.label, local_project_name_entry.text); } private void update_can_clone () { can_clone = remote_repository_uri_entry.is_valid && local_project_name_entry.is_valid && - clone_parent_folder_label.label != ""; + projects_folder_label.label != ""; //TODO Check whether the target folder already exists and is not empty? } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 02ed5de634..b6fb2a582e 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1043,21 +1043,27 @@ namespace Scratch { } private void action_clone_repo (SimpleAction action, Variant? param) { - var default_folder = git_manager.active_project_path != "" ? - Path.get_dirname (git_manager.active_project_path) : ""; - var clone_dialog = new Dialogs.CloneRepositoryDialog (default_folder); + var default_projects_folder = Scratch.settings.get_string ("default-projects-folder"); + if (default_projects_folder == "" && git_manager.active_project_path != "") { + default_projects_folder = Path.get_dirname (git_manager.active_project_path); + } + + var default_remote = Scratch.settings.get_string ("default-remote"); + var clone_dialog = new Dialogs.CloneRepositoryDialog (default_projects_folder, default_remote); clone_dialog.response.connect ((res) => { - var uri = clone_dialog.get_source_repository_uri (); - var local_folder = clone_dialog.get_local_folder (); - var local_name = clone_dialog.get_local_name (); + // Persist last entries (not necessarily valid) + Scratch.settings.set_string ("default-remote", clone_dialog.get_remote ()); + Scratch.settings.set_string ("default-projects-folder", clone_dialog.get_projects_folder ()); // MainWindow should provide feedback on cloning progress // Hide clone dialog in case needed to retry clone_dialog.hide (); if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? + var uri = clone_dialog.get_valid_source_repository_uri (); + var target = clone_dialog.get_valid_target (); //TODO Show progress while cloning git_manager.clone_repository.begin ( uri, - Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name), + target, (obj, res) => { try { File? workdir = null; diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 2a735f4767..421a5297b7 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -125,6 +125,7 @@ namespace Scratch.Services { var fetch_options = new Ggit.FetchOptions (); fetch_options.set_download_tags (Ggit.RemoteDownloadTagsType.UNSPECIFIED); + //TODO Set callbacks for authentification and progress fetch_options.set_remote_callbacks (null); var clone_options = new Ggit.CloneOptions (); From 0ae3fb9f2c0bc1e97074329abc6e878ac5706e05 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Jun 2025 12:17:34 +0100 Subject: [PATCH 40/56] Fix lint --- src/MainWindow.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index b6fb2a582e..ceaff962fe 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1045,7 +1045,7 @@ namespace Scratch { private void action_clone_repo (SimpleAction action, Variant? param) { var default_projects_folder = Scratch.settings.get_string ("default-projects-folder"); if (default_projects_folder == "" && git_manager.active_project_path != "") { - default_projects_folder = Path.get_dirname (git_manager.active_project_path); + default_projects_folder = Path.get_dirname (git_manager.active_project_path); } var default_remote = Scratch.settings.get_string ("default-remote"); From 01346c937f9408ca3b43f359cf702b3b4dc5b002 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 20 Jun 2025 18:25:53 +0100 Subject: [PATCH 41/56] Add on accelerator to open a project --- src/MainWindow.vala | 1 + src/Widgets/ChooseProjectButton.vala | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 9611d2c7bc..c91eb9566c 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -207,6 +207,7 @@ namespace Scratch { action_accelerators.set (ACTION_FIND_PREVIOUS, "g"); action_accelerators.set (ACTION_FIND_GLOBAL + "::", "f"); action_accelerators.set (ACTION_OPEN, "o"); + action_accelerators.set (ACTION_OPEN_FOLDER, "o"); action_accelerators.set (ACTION_REVERT, "o"); action_accelerators.set (ACTION_SAVE, "s"); action_accelerators.set (ACTION_SAVE_AS, "s"); diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 877f54925b..3b035c5b2d 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -74,7 +74,7 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { var add_folder_button = new PopoverMenuItem (_("Open Folder…")) { action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_OPEN_FOLDER, action_target = new Variant.string (""), - icon_name = "folder-open-symbolic" + icon_name = "folder-open-symbolic", }; var clone_button = new PopoverMenuItem (_("Clone Git Repository…")) { From af36e424a6b547632fc066d15f899a5187bfab5c Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 22 Jun 2025 20:01:47 +0100 Subject: [PATCH 42/56] Set parent to error dialog, fix spelling --- src/MainWindow.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index c91eb9566c..e386290862 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1075,11 +1075,13 @@ namespace Scratch { } } catch (Error e) { var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - "Uanble to clone %s".printf (uri), + "Unable to clone %s".printf (uri), e.message, "dialog-error", Gtk.ButtonsType.CLOSE - ); + ) { + transient_for = this + }; message_dialog.add_button (_("Retry"), 1); message_dialog.response.connect ((res) => { if (res == 1) { From e463c9f72310251d8dbaded984ed1b8a73c54742 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 08:35:03 +0100 Subject: [PATCH 43/56] Make clone function async --- src/MainWindow.vala | 17 ++++++++--------- src/Services/GitManager.vala | 33 ++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index e386290862..e7de02c3ee 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1066,17 +1066,16 @@ namespace Scratch { uri, target, (obj, res) => { - try { - File? workdir = null; - if (git_manager.clone_repository.end (res, out workdir)) { - debug ("Repository cloned into %s", workdir.get_uri ()); - open_folder (workdir); - clone_dialog.destroy (); - } - } catch (Error e) { + File? workdir = null; + string? error = null; + if (git_manager.clone_repository.end (res, out workdir, out error)) { + debug ("Repository cloned into %s", workdir.get_uri ()); + open_folder (workdir); + clone_dialog.destroy (); + } else { var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( "Unable to clone %s".printf (uri), - e.message, + error, "dialog-error", Gtk.ButtonsType.CLOSE ) { diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 421a5297b7..440c3efb65 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -113,15 +113,14 @@ namespace Scratch.Services { return build_path; } - //TODO Make this a real async function that does not block the main loop. public async bool clone_repository ( string uri, string local_folder, - out File? repo_workdir + out File? repo_workdir, + out string? error ) throws Error { - repo_workdir = null; - var folder_file = File.new_for_path (local_folder); + error = null; var fetch_options = new Ggit.FetchOptions (); fetch_options.set_download_tags (Ggit.RemoteDownloadTagsType.UNSPECIFIED); @@ -133,14 +132,30 @@ namespace Scratch.Services { clone_options.set_is_bare (false); clone_options.set_fetch_options (fetch_options); - var new_repo = Ggit.Repository.clone ( - uri, - folder_file, - clone_options - ); + var e_message = ""; // Cannot capture out parameter so make local proxy + var folder_file = File.new_for_path (local_folder); + Ggit.Repository? new_repo = null; + Idle.add (() => { + try { + new_repo = Ggit.Repository.clone ( + uri, + folder_file, + clone_options + ); + } catch (Error e) { + e_message = e.message; + new_repo = null; + } + + clone_repository.callback (); + return Source.REMOVE; + }); + yield; if (new_repo != null) { repo_workdir = new_repo.get_workdir (); + } else { + error = e_message; } return new_repo != null; From 9d951508ea6c2d83cddfec30e4ed87fb12c50818 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 08:51:28 +0100 Subject: [PATCH 44/56] Include success messagedialog --- src/MainWindow.vala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index e7de02c3ee..94b97d2f74 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1069,9 +1069,18 @@ namespace Scratch { File? workdir = null; string? error = null; if (git_manager.clone_repository.end (res, out workdir, out error)) { - debug ("Repository cloned into %s", workdir.get_uri ()); open_folder (workdir); clone_dialog.destroy (); + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + "Repository %s successfully cloned".printf (uri), + "Local repository working directory is %s".printf (workdir.get_uri ()), + "dialog-information", + Gtk.ButtonsType.CLOSE + ) { + transient_for = this + }; + message_dialog.response.connect (message_dialog.destroy); + message_dialog.present (); } else { var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( "Unable to clone %s".printf (uri), From d049990200cbcbd1cbf0be7770c9d342c8ee59c2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 08:55:19 +0100 Subject: [PATCH 45/56] Only construct parameters in Object () --- src/Dialogs/CloneRepositoryDialog.vala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index a4a4aa405e..5026fef32f 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -26,15 +26,16 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { Object ( - transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), - image_icon: new ThemedIcon ("git"), - modal: true, suggested_local_folder: _suggested_local_folder, suggested_remote: _suggested_remote ); } construct { + transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + image_icon = new ThemedIcon ("git"); + modal = true; + try { name_regex = new Regex (NAME_REGEX, OPTIMIZE, ANCHORED | NOTEMPTY); } catch (RegexError e) { From 1a09d9184c0ef393110bc14f4896372c7d786221 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 12:11:01 +0100 Subject: [PATCH 46/56] Use a thread for cloning, show spinner --- src/Dialogs/CloneRepositoryDialog.vala | 48 +++++++++++++++++++++----- src/MainWindow.vala | 6 +++- src/Services/GitManager.vala | 7 ++-- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 5026fef32f..0c42120780 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -14,16 +14,40 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). // - Must not contain consecutive special characters. // - Cannot end in . git or . atom . + private const string CLONE_REPOSITORY = N_("Clone Button"); + private const string CLONING = N_("Cloning…"); private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required private Regex name_regex; private Gtk.Label projects_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; + private Gtk.Button clone_button; + private Gtk.Spinner spinner; + private Gtk.Revealer revealer; public string suggested_local_folder { get; construct; } public string suggested_remote { get; construct; } + public bool cloning_in_progress { + set { + if (value) { + clone_button.label = _(CLONING); + spinner.start (); + + } else { + clone_button.label = _(CLONE_REPOSITORY); + spinner.stop (); + } + + revealer.reveal_child = value; + } + + get { + return revealer.reveal_child; + } + } + public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { Object ( suggested_local_folder: _suggested_local_folder, @@ -95,19 +119,25 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry = new Granite.ValidatedEntry (); local_project_name_entry.changed.connect (validate_local_name); - var content_box = new Gtk.Box (VERTICAL, 12); - content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); - content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); - content_box.add (new CloneEntry (_("Name of Clone"), local_project_name_entry)); + revealer = new Gtk.Revealer () { + valign = END + }; + spinner = new Gtk.Spinner (); + revealer.add (spinner); + + var content_box = new Gtk.Grid (); + content_box.attach (new CloneEntry (_("Repository URL"), remote_repository_uri_entry), 0, 0); + content_box.attach (new CloneEntry (_("Location"), folder_chooser_button), 0, 1); + content_box.attach (new CloneEntry (_("Name of Clone"), local_project_name_entry), 0, 2); + content_box.attach (revealer, 1, 2); content_box.show_all (); custom_bin.add (content_box); + custom_bin.show_all (); - var clone_button = (Gtk.Button) add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); - clone_button.can_default = true; - clone_button.has_default = true; - clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - + clone_button = new Gtk.Button.with_label (_(CLONE_REPOSITORY)); + clone_button.show (); + add_action_widget (clone_button, Gtk.ResponseType.APPLY); bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 94b97d2f74..4be8b78baf 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1057,8 +1057,8 @@ namespace Scratch { Scratch.settings.set_string ("default-projects-folder", clone_dialog.get_projects_folder ()); // MainWindow should provide feedback on cloning progress // Hide clone dialog in case needed to retry - clone_dialog.hide (); if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? + clone_dialog.cloning_in_progress = true; var uri = clone_dialog.get_valid_source_repository_uri (); var target = clone_dialog.get_valid_target (); //TODO Show progress while cloning @@ -1066,6 +1066,7 @@ namespace Scratch { uri, target, (obj, res) => { + clone_dialog.cloning_in_progress = false; File? workdir = null; string? error = null; if (git_manager.clone_repository.end (res, out workdir, out error)) { @@ -1082,6 +1083,7 @@ namespace Scratch { message_dialog.response.connect (message_dialog.destroy); message_dialog.present (); } else { + clone_dialog.hide (); var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( "Unable to clone %s".printf (uri), error, @@ -1103,6 +1105,8 @@ namespace Scratch { } } ); + } else { + clone_dialog.destroy (); } }); diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 440c3efb65..db2dc6fc68 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -135,7 +135,9 @@ namespace Scratch.Services { var e_message = ""; // Cannot capture out parameter so make local proxy var folder_file = File.new_for_path (local_folder); Ggit.Repository? new_repo = null; - Idle.add (() => { + + SourceFunc callback = clone_repository.callback; + new Thread ("cloning", () => { try { new_repo = Ggit.Repository.clone ( uri, @@ -147,8 +149,7 @@ namespace Scratch.Services { new_repo = null; } - clone_repository.callback (); - return Source.REMOVE; + Idle.add ((owned)callback); }); yield; From fc536e01709e507ea2f47f496bd3dbdbd7a09cce Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 23 Jun 2025 15:36:52 +0100 Subject: [PATCH 47/56] Remove unneeded throws Error --- src/Services/GitManager.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index db2dc6fc68..5b80d3084e 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -118,7 +118,7 @@ namespace Scratch.Services { string local_folder, out File? repo_workdir, out string? error - ) throws Error { + ) { repo_workdir = null; error = null; From 49e95a9485e44b43fbcfa4a2a43976bae2338502 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 09:14:40 +0100 Subject: [PATCH 48/56] Fix some coding issues --- src/Dialogs/CloneRepositoryDialog.vala | 13 +++++-------- src/Widgets/Sidebar.vala | 6 ++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 0c42120780..0dd36dc7e7 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -14,7 +14,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). // - Must not contain consecutive special characters. // - Cannot end in . git or . atom . - private const string CLONE_REPOSITORY = N_("Clone Button"); + private const string CLONE_REPOSITORY = N_("Clone Repository"); private const string CLONING = N_("Cloning…"); private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required @@ -119,11 +119,11 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry = new Granite.ValidatedEntry (); local_project_name_entry.changed.connect (validate_local_name); + spinner = new Gtk.Spinner (); revealer = new Gtk.Revealer () { - valign = END + valign = END, + child = spinner }; - spinner = new Gtk.Spinner (); - revealer.add (spinner); var content_box = new Gtk.Grid (); content_box.attach (new CloneEntry (_("Repository URL"), remote_repository_uri_entry), 0, 0); @@ -133,11 +133,8 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { content_box.show_all (); custom_bin.add (content_box); - custom_bin.show_all (); - clone_button = new Gtk.Button.with_label (_(CLONE_REPOSITORY)); - clone_button.show (); - add_action_widget (clone_button, Gtk.ResponseType.APPLY); + clone_button = (Gtk.Button)add_button (_(CLONE_REPOSITORY), Gtk.ResponseType.APPLY); bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 0656c33d0c..04177f9f57 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -67,12 +67,14 @@ public class Code.Sidebar : Gtk.Grid { project_menu.append_item (order_projects_menu_item); project_menu_model = project_menu; + var label = new Gtk.Label ( _("Manage project folders…")) { + halign = START + }; var project_more_button = new Gtk.MenuButton () { hexpand = true, use_popover = false, menu_model = project_menu_model, - label = _("Manage project folders…"), - xalign = 0.0f + child = label }; actionbar.pack_start (project_more_button); From e4cf12bea6ce3d377d8debf198260efcfcf517dc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 09:16:07 +0100 Subject: [PATCH 49/56] Fix typo --- src/Widgets/Sidebar.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 04177f9f57..ff3060cf42 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -70,14 +70,14 @@ public class Code.Sidebar : Gtk.Grid { var label = new Gtk.Label ( _("Manage project folders…")) { halign = START }; - var project_more_button = new Gtk.MenuButton () { + var project_menu_button = new Gtk.MenuButton () { hexpand = true, use_popover = false, menu_model = project_menu_model, child = label }; - actionbar.pack_start (project_more_button); + actionbar.pack_start (project_menu_button); add (headerbar); add (stack_switcher); From 4469d6c84c00997aaa6fa0f332ae87a68170e2de Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 09:46:01 +0100 Subject: [PATCH 50/56] Show spinner on separate page; hide buttons --- src/Dialogs/CloneRepositoryDialog.vala | 42 ++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 0dd36dc7e7..2aaed3c3f8 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -14,8 +14,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). // - Must not contain consecutive special characters. // - Cannot end in . git or . atom . - private const string CLONE_REPOSITORY = N_("Clone Repository"); - private const string CLONING = N_("Cloning…"); private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required private Regex name_regex; @@ -23,6 +21,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Granite.ValidatedEntry remote_repository_uri_entry; private Granite.ValidatedEntry local_project_name_entry; private Gtk.Button clone_button; + private Gtk.Stack stack; private Gtk.Spinner spinner; private Gtk.Revealer revealer; @@ -32,19 +31,13 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { public bool cloning_in_progress { set { if (value) { - clone_button.label = _(CLONING); + stack.visible_child_name = "cloning"; spinner.start (); } else { - clone_button.label = _(CLONE_REPOSITORY); + stack.visible_child_name = "entries"; spinner.stop (); } - - revealer.reveal_child = value; - } - - get { - return revealer.reveal_child; } } @@ -119,24 +112,35 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry = new Granite.ValidatedEntry (); local_project_name_entry.changed.connect (validate_local_name); - spinner = new Gtk.Spinner (); - revealer = new Gtk.Revealer () { - valign = END, - child = spinner - }; + var content_box = new Gtk.Grid (); content_box.attach (new CloneEntry (_("Repository URL"), remote_repository_uri_entry), 0, 0); content_box.attach (new CloneEntry (_("Location"), folder_chooser_button), 0, 1); content_box.attach (new CloneEntry (_("Name of Clone"), local_project_name_entry), 0, 2); content_box.attach (revealer, 1, 2); - content_box.show_all (); - custom_bin.add (content_box); + var cloning_box = new Gtk.Box (HORIZONTAL, 12) { + valign = CENTER, + halign = CENTER + }; + var cloning_label = new Granite.HeaderLabel (_("Cloning in progress")); + spinner = new Gtk.Spinner (); + cloning_box.add (cloning_label); + cloning_box.add (spinner); - clone_button = (Gtk.Button)add_button (_(CLONE_REPOSITORY), Gtk.ResponseType.APPLY); - bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); + stack = new Gtk.Stack (); + stack.add_named (content_box, "entries"); + stack.add_named (cloning_box, "cloning"); + stack.visible_child_name = "entries"; + custom_bin.add (stack); + custom_bin.show_all (); + + clone_button = (Gtk.Button)add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); + bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); + spinner.bind_property ("active", clone_button, "visible", INVERT_BOOLEAN); + spinner.bind_property ("active", cancel_button, "visible", INVERT_BOOLEAN); //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry //text changed. So call explicitly after we validate the text. can_clone = false; From 1afe9834d9f323096291f7045675de369d205e27 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 09:54:05 +0100 Subject: [PATCH 51/56] Cleanup comments --- src/Dialogs/CloneRepositoryDialog.vala | 4 +--- src/MainWindow.vala | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 2aaed3c3f8..faf1a68795 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -112,8 +112,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry = new Granite.ValidatedEntry (); local_project_name_entry.changed.connect (validate_local_name); - - var content_box = new Gtk.Grid (); content_box.attach (new CloneEntry (_("Repository URL"), remote_repository_uri_entry), 0, 0); content_box.attach (new CloneEntry (_("Location"), folder_chooser_button), 0, 1); @@ -177,7 +175,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { local_project_name_entry.is_valid && projects_folder_label.label != ""; - //TODO Check whether the target folder already exists and is not empty? + // Checking whether the target folder already exists and is not empty occurs after pressing apply } private void on_remote_uri_changed (Gtk.Editable source) { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 4be8b78baf..8399e64286 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1055,13 +1055,12 @@ namespace Scratch { // Persist last entries (not necessarily valid) Scratch.settings.set_string ("default-remote", clone_dialog.get_remote ()); Scratch.settings.set_string ("default-projects-folder", clone_dialog.get_projects_folder ()); - // MainWindow should provide feedback on cloning progress - // Hide clone dialog in case needed to retry - if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { // Should not need second test? + // Clone dialog show spinner during cloning so keep visible + //TODO Show more information re progress using Ggit callbacks + if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { clone_dialog.cloning_in_progress = true; var uri = clone_dialog.get_valid_source_repository_uri (); var target = clone_dialog.get_valid_target (); - //TODO Show progress while cloning git_manager.clone_repository.begin ( uri, target, @@ -1099,6 +1098,7 @@ namespace Scratch { } else { clone_dialog.destroy (); } + message_dialog.destroy (); }); message_dialog.present (); From 6779e662e8bb80791569e85a39895c554bca701b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 10:14:12 +0100 Subject: [PATCH 52/56] Reorganise, reinstate default action --- src/Dialogs/CloneRepositoryDialog.vala | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index faf1a68795..2ff3687360 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -6,16 +6,13 @@ */ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { - public bool can_clone { get; private set; default = false; } - - // Git project name rules according to GitLab // - Must start and end with a letter ( a-zA-Z ) or digit ( 0-9 ). // - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). // - Must not contain consecutive special characters. // - Cannot end in . git or . atom . - private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required + private Regex name_regex; private Gtk.Label projects_folder_label; private Granite.ValidatedEntry remote_repository_uri_entry; @@ -25,6 +22,7 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Gtk.Spinner spinner; private Gtk.Revealer revealer; + public bool can_clone { get; private set; default = false; } public string suggested_local_folder { get; construct; } public string suggested_remote { get; construct; } @@ -51,24 +49,27 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { construct { transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); image_icon = new ThemedIcon ("git"); + badge_icon = new ThemedIcon ("emblem-downloads"); modal = true; + ///TRANSLATORS "Git" is a proper name and must not be translated + primary_text = _("Create a local clone of a Git repository"); + secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); + + var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + clone_button = (Gtk.Button)add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); + set_default (clone_button); + try { name_regex = new Regex (NAME_REGEX, OPTIMIZE, ANCHORED | NOTEMPTY); } catch (RegexError e) { warning ("%s\n", e.message); } - var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - - ///TRANSLATORS "Git" is a proper name and must not be translated - primary_text = _("Create a local clone of a Git repository"); - secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); - badge_icon = new ThemedIcon ("emblem-downloads"); - remote_repository_uri_entry = new Granite.ValidatedEntry () { placeholder_text = _("https://example.com/username/projectname.git"), - input_purpose = URL + input_purpose = URL, + activates_default = true }; remote_repository_uri_entry.changed.connect (on_remote_uri_changed); remote_repository_uri_entry.text = suggested_remote; @@ -109,7 +110,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }); - local_project_name_entry = new Granite.ValidatedEntry (); + local_project_name_entry = new Granite.ValidatedEntry () { + activates_default = true + }; local_project_name_entry.changed.connect (validate_local_name); var content_box = new Gtk.Grid (); @@ -135,12 +138,9 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { custom_bin.add (stack); custom_bin.show_all (); - clone_button = (Gtk.Button)add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); spinner.bind_property ("active", clone_button, "visible", INVERT_BOOLEAN); spinner.bind_property ("active", cancel_button, "visible", INVERT_BOOLEAN); - //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry - //text changed. So call explicitly after we validate the text. can_clone = false; // Focus cancel button so that entry placeholder text shows From ebcfd416df2df11beb6129b1fe430b5fadda0324 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 24 Jun 2025 13:59:49 +0100 Subject: [PATCH 53/56] Implement some remote callbacks --- src/Dialogs/CloneRepositoryDialog.vala | 120 ++++++++++++++++++++++++- src/MainWindow.vala | 2 + src/Services/GitManager.vala | 4 +- 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 2ff3687360..2a64912d7f 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -21,6 +21,16 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Gtk.Stack stack; private Gtk.Spinner spinner; private Gtk.Revealer revealer; + private Gtk.Label message_label; + private Gtk.Label indexing_label; + private Gtk.Label progress_label; + private Gtk.ProgressBar transfer_progress_bar; + private uint total_objects = 0; + private uint received_objects = 0; + private uint indexed_objects = 0; + private size_t received_bytes = 0; + private string remote_message = ""; + private uint update_transfer_info_timeout_id = 0; public bool can_clone { get; private set; default = false; } public string suggested_local_folder { get; construct; } @@ -37,6 +47,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { spinner.stop (); } } + + get { + return spinner.active; + } } public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { @@ -46,6 +60,12 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { ); } + ~CloneRepositoryDialog () { + if (update_transfer_info_timeout_id > 0) { + Source.remove (update_transfer_info_timeout_id); + } + } + construct { transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); image_icon = new ThemedIcon ("git"); @@ -121,14 +141,31 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { content_box.attach (new CloneEntry (_("Name of Clone"), local_project_name_entry), 0, 2); content_box.attach (revealer, 1, 2); - var cloning_box = new Gtk.Box (HORIZONTAL, 12) { + var cloning_box = new Gtk.Box (VERTICAL, 24) { valign = CENTER, halign = CENTER }; + var spinner_box = new Gtk.Box (HORIZONTAL, 12) { + valign = CENTER, + halign = CENTER + }; + message_label = new Gtk.Label (""); + progress_label = new Gtk.Label (""); + indexing_label = new Gtk.Label (""); + transfer_progress_bar = new Gtk.ProgressBar () { + fraction = 0.0 + }; + var cloning_label = new Granite.HeaderLabel (_("Cloning in progress")); spinner = new Gtk.Spinner (); - cloning_box.add (cloning_label); - cloning_box.add (spinner); + spinner_box.add (cloning_label); + spinner_box.add (spinner); + + cloning_box.add (spinner_box); + cloning_box.add (message_label); + cloning_box.add (transfer_progress_bar); + cloning_box.add (progress_label); + cloning_box.add (indexing_label); stack = new Gtk.Stack (); stack.add_named (content_box, "entries"); @@ -161,6 +198,58 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { } } + public Ggit.RemoteCallbacks? get_remote_callbacks () { + update_transfer_info_timeout_id = Timeout.add (200, () => { + if (total_objects > 0) { + transfer_progress_bar.fraction = received_objects / total_objects; + } + + indexing_label.label = "%u indexed objects".printf (indexed_objects); + progress_label.label = "%u bytes received".printf ((uint)received_bytes); + message_label.label = remote_message; + + if (cloning_in_progress) { + return Source.CONTINUE; + } else { + update_transfer_info_timeout_id = 0; + return Source.REMOVE; + } + }); + + var cbs = new RemoteCallbacks (); + cbs.progress.connect ((message) => { + remote_message = message; + }); + cbs.transfer_progress.connect ((progress) => { + if (progress != null) { + received_objects = progress.get_received_objects (); + received_bytes = progress.get_received_bytes (); + + indexed_objects = progress.get_indexed_objects (); + total_objects = progress.get_total_objects (); + } + }); + //TODO Decide what to do with completion info. Notification? + cbs.completion.connect ((type) => { + warning ("received completion signal"); + switch (type) { + case DOWNLOAD: + warning ("Download completed"); + break; + case INDEXING: + warning ("Indexing completed"); + break; + case ERROR: + warning ("Transfer ended with error"); + break; + default: + assert_not_reached (); + } + }); + + return cbs; + } + public string get_valid_source_repository_uri () requires (can_clone) { //TODO Further validation here? return remote_repository_uri_entry.text; @@ -255,4 +344,29 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { orientation = VERTICAL; } } + + //Provide for plaintext user password for now + private class RemoteCallbacks : Ggit.RemoteCallbacks { + public string? user { get; construct; } + public string? password { get; construct; } + + public RemoteCallbacks (string? user = null, string? password = null) { + Object ( + user: user, + password: password + ); + } + + public override Ggit.Cred? credentials ( + string url, + string? username_from_url, + Ggit.Credtype allowed_types + ) throws Error { + if (user != null && password != null && Ggit.Credtype.USERPASS_PLAINTEXT in allowed_types) { + return new Ggit.CredPlaintext (user, password); + } else { + return null; + } + } + } } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 8399e64286..52f4b28dca 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1061,9 +1061,11 @@ namespace Scratch { clone_dialog.cloning_in_progress = true; var uri = clone_dialog.get_valid_source_repository_uri (); var target = clone_dialog.get_valid_target (); + var callbacks = clone_dialog.get_remote_callbacks (); git_manager.clone_repository.begin ( uri, target, + callbacks, (obj, res) => { clone_dialog.cloning_in_progress = false; File? workdir = null; diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 5b80d3084e..f83b0404a2 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -116,6 +116,7 @@ namespace Scratch.Services { public async bool clone_repository ( string uri, string local_folder, + Ggit.RemoteCallbacks? rcallbacks, out File? repo_workdir, out string? error ) { @@ -125,7 +126,8 @@ namespace Scratch.Services { var fetch_options = new Ggit.FetchOptions (); fetch_options.set_download_tags (Ggit.RemoteDownloadTagsType.UNSPECIFIED); //TODO Set callbacks for authentification and progress - fetch_options.set_remote_callbacks (null); + + fetch_options.set_remote_callbacks (rcallbacks); var clone_options = new Ggit.CloneOptions (); clone_options.set_local (Ggit.CloneLocal.AUTO); From 4e35200dfc4d8358f3ff9eb2c8aa7da13298d5a7 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 25 Jun 2025 10:10:17 +0100 Subject: [PATCH 54/56] Remove extraneous --- src/Dialogs/CloneRepositoryDialog.vala | 25 ------------------------- src/MainWindow.vala | 6 ------ 2 files changed, 31 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index c41852ec9a..2204be8919 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -32,8 +32,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private size_t received_bytes = 0; private string remote_message = ""; private uint update_transfer_info_timeout_id = 0; -// ======= -// >>>>>>> master public bool can_clone { get; private set; default = false; } public string suggested_local_folder { get; construct; } @@ -50,13 +48,10 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { spinner.stop (); } } -// <<<<<<< HEAD get { return spinner.active; } -// ======= -// >>>>>>> master } public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { @@ -66,15 +61,12 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { ); } -// <<<<<<< HEAD ~CloneRepositoryDialog () { if (update_transfer_info_timeout_id > 0) { Source.remove (update_transfer_info_timeout_id); } } -// ======= -// >>>>>>> master construct { transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); image_icon = new ThemedIcon ("git"); @@ -144,7 +136,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { }; local_project_name_entry.changed.connect (validate_local_name); -// <<<<<<< HEAD var content_box = new Gtk.Box (VERTICAL, 0); content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); @@ -175,22 +166,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { cloning_box.add (transfer_progress_bar); cloning_box.add (progress_label); cloning_box.add (indexing_label); -// ======= -// var content_box = new Gtk.Box (VERTICAL, 0); -// content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); -// content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); -// content_box.add (new CloneEntry (_("Name of Clone"), local_project_name_entry)); - -// var cloning_label = new Granite.HeaderLabel (_("Cloning in progress")); -// spinner = new Gtk.Spinner (); - -// var cloning_box = new Gtk.Box (HORIZONTAL, 12) { -// valign = CENTER, -// halign = CENTER -// }; -// cloning_box.add (cloning_label); -// cloning_box.add (spinner); -// >>>>>>> master stack = new Gtk.Stack (); stack.add_named (content_box, "entries"); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 6bce4f8e4b..df3023c402 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1061,17 +1061,11 @@ namespace Scratch { clone_dialog.cloning_in_progress = true; var uri = clone_dialog.get_valid_source_repository_uri (); var target = clone_dialog.get_valid_target (); -// <<<<<<< HEAD var callbacks = clone_dialog.get_remote_callbacks (); git_manager.clone_repository.begin ( uri, target, callbacks, -// ======= -// git_manager.clone_repository.begin ( -// uri, -// target, -// >>>>>>> master (obj, res) => { clone_dialog.cloning_in_progress = false; File? workdir = null; From d5ce7491d6ba73a9ea130ba30d8b778c504e2184 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 18 Jul 2025 13:18:56 +0100 Subject: [PATCH 55/56] Show cloning spinner in ChooseProjectButton --- src/Dialogs/CloneRepositoryDialog.vala | 1 - src/MainWindow.vala | 8 ++++---- src/Widgets/ChooseProjectButton.vala | 17 ++++++++++++----- src/Widgets/Sidebar.vala | 11 +++++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/Dialogs/CloneRepositoryDialog.vala b/src/Dialogs/CloneRepositoryDialog.vala index 2204be8919..149eed2ac7 100644 --- a/src/Dialogs/CloneRepositoryDialog.vala +++ b/src/Dialogs/CloneRepositoryDialog.vala @@ -20,7 +20,6 @@ public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { private Gtk.Button clone_button; private Gtk.Stack stack; private Gtk.Spinner spinner; -// <<<<<<< HEAD private Gtk.Revealer revealer; private Gtk.Label message_label; private Gtk.Label indexing_label; diff --git a/src/MainWindow.vala b/src/MainWindow.vala index df3023c402..ca495aa082 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1055,24 +1055,25 @@ namespace Scratch { // Persist last entries (not necessarily valid) Scratch.settings.set_string ("default-remote", clone_dialog.get_remote ()); Scratch.settings.set_string ("default-projects-folder", clone_dialog.get_projects_folder ()); - // Clone dialog show spinner during cloning so keep visible //TODO Show more information re progress using Ggit callbacks if (res == Gtk.ResponseType.APPLY && clone_dialog.can_clone) { - clone_dialog.cloning_in_progress = true; + sidebar.cloning_in_progress = true; var uri = clone_dialog.get_valid_source_repository_uri (); var target = clone_dialog.get_valid_target (); var callbacks = clone_dialog.get_remote_callbacks (); + clone_dialog.hide (); git_manager.clone_repository.begin ( uri, target, callbacks, (obj, res) => { - clone_dialog.cloning_in_progress = false; + sidebar.cloning_in_progress = false; File? workdir = null; string? error = null; if (git_manager.clone_repository.end (res, out workdir, out error)) { open_folder (workdir); clone_dialog.destroy (); + //TODO Show toast instead var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("Repository %s successfully cloned").printf (uri), _("Local repository working directory is %s").printf (workdir.get_uri ()), @@ -1084,7 +1085,6 @@ namespace Scratch { message_dialog.response.connect (message_dialog.destroy); message_dialog.present (); } else { - clone_dialog.hide (); var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("Unable to clone %s").printf (uri), error, diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 3b035c5b2d..1dc1aa44c8 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -17,6 +17,7 @@ */ public class Code.ChooseProjectButton : Gtk.MenuButton { + public bool cloning_in_progress { get; set; } private const string NO_PROJECT_SELECTED = N_("No Project Selected"); private const string PROJECT_TOOLTIP = N_("Active Git Project: %s"); private Gtk.Label label_widget; @@ -35,12 +36,18 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { xalign = 0.0f }; - var grid = new Gtk.Grid () { - halign = Gtk.Align.START + var cloning_spinner = new Gtk.Spinner (); + bind_property ("cloning-in-progress", cloning_spinner, "active"); + + var box = new Gtk.Box (HORIZONTAL, 3) { + hexpand = true, + vexpand = false }; - grid.add (img); - grid.add (label_widget); - add (grid); + + box.add (img); + box.add (label_widget); + box.add (cloning_spinner); + add (box); project_listbox = new Gtk.ListBox () { selection_mode = Gtk.SelectionMode.SINGLE diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index ff3060cf42..94e0455497 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -27,6 +27,17 @@ public class Code.Sidebar : Gtk.Grid { public Hdy.HeaderBar headerbar { get; private set; } public GLib.MenuModel project_menu_model { get; construct; } + // May show progress in different way in future + public bool cloning_in_progress { + get { + return choose_project_button.cloning_in_progress; + } + + set { + choose_project_button.cloning_in_progress = value; + } + } + private Gtk.StackSwitcher stack_switcher; construct { From 1e6872f7ae24abf3a0ae2f195044ac2809c4c3bf Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 18 Jul 2025 15:46:25 +0100 Subject: [PATCH 56/56] Add cloning success toast to sidebar --- src/MainWindow.vala | 12 +----------- src/Widgets/Sidebar.vala | 8 ++++++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index ca495aa082..e13cb080ea 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -1073,17 +1073,7 @@ namespace Scratch { if (git_manager.clone_repository.end (res, out workdir, out error)) { open_folder (workdir); clone_dialog.destroy (); - //TODO Show toast instead - var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - _("Repository %s successfully cloned").printf (uri), - _("Local repository working directory is %s").printf (workdir.get_uri ()), - "dialog-information", - Gtk.ButtonsType.CLOSE - ) { - transient_for = this - }; - message_dialog.response.connect (message_dialog.destroy); - message_dialog.present (); + sidebar.notify_cloning_success (); } else { var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("Unable to clone %s").printf (uri), diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 94e0455497..1998cb6c9a 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -39,6 +39,7 @@ public class Code.Sidebar : Gtk.Grid { } private Gtk.StackSwitcher stack_switcher; + private Granite.Widgets.Toast cloning_success_toast; construct { orientation = Gtk.Orientation.VERTICAL; @@ -49,6 +50,8 @@ public class Code.Sidebar : Gtk.Grid { valign = Gtk.Align.CENTER }; + cloning_success_toast = new Granite.Widgets.Toast (_("Cloning complete")); + headerbar = new Hdy.HeaderBar () { custom_title = choose_project_button, show_close_button = true @@ -91,6 +94,7 @@ public class Code.Sidebar : Gtk.Grid { actionbar.pack_start (project_menu_button); add (headerbar); + add (cloning_success_toast); add (stack_switcher); add (stack); add (actionbar); @@ -172,4 +176,8 @@ public class Code.Sidebar : Gtk.Grid { public void remove_tab (Code.PaneSwitcher tab) { stack.remove (tab); } + + public void notify_cloning_success () { + cloning_success_toast.send_notification (); + } }