Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4e512ab
Start BranchActionDialog
jeremypw Jun 20, 2025
da9904f
Cleanup, refactor, bind apply button sensitivity
jeremypw Jun 21, 2025
c39c7bf
Do not bind model; private Row class
jeremypw Jun 21, 2025
4a90587
Include headers
jeremypw Jun 21, 2025
a584953
Use Stack and StackSidebar for more actions
jeremypw Jun 21, 2025
d9392ba
Move stuff in to private classes
jeremypw Jun 21, 2025
9e86c5f
Use common interface for stack pages
jeremypw Jun 21, 2025
f5db626
Reimplement checkout branch; refactor
jeremypw Jun 22, 2025
ee18b67
Start to implement recent branches
jeremypw Jun 22, 2025
b32a7c8
Coding improvements
jeremypw Jun 23, 2025
f957a06
Get name of Branch not Ref
jeremypw Jun 23, 2025
8205853
Merge branch 'master' into jeremypw/branch-actions-dialog
jeremypw Jun 23, 2025
8378d85
Split out private classes
jeremypw Jun 23, 2025
66ec62d
Implement create new branch
jeremypw Jun 23, 2025
830b05b
Small Cleanup
jeremypw Jun 23, 2025
af0d778
Focus desired widget on startup
jeremypw Jun 25, 2025
61d0101
Code improvement
jeremypw Jun 25, 2025
b22bdac
Activate on double-click
jeremypw Jun 25, 2025
8ae2378
Refocus after select, activate on Enter
jeremypw Jun 25, 2025
e28cece
Make listbox more general purpose
jeremypw Jun 25, 2025
567338c
Simplify header func
jeremypw Jun 25, 2025
4973ca7
Merge branch 'master' into jeremypw/branch-actions-dialog
jeremypw Jul 3, 2025
8432a9b
Suppress terminal warnings
jeremypw Jul 3, 2025
e86fe23
Merge branch 'master' into jeremypw/branch-actions-dialog
jeremypw Jul 16, 2025
ab6280d
Merge branch 'master' into jeremypw/branch-actions-dialog
jeremypw Oct 18, 2025
aae9ab3
Remove unimplemented functions
jeremypw Oct 18, 2025
23d4c3d
Merge branch 'master' into jeremypw/branch-actions-dialog
jeremypw Nov 4, 2025
62bd40b
Remove commented out code
jeremypw Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/Dialogs/BranchActions/BranchActionDialog.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2025 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/
public enum Scratch.BranchAction {
CHECKOUT,
COMMIT,
PUSH,
PULL,
MERGE,
DELETE,
CREATE
}

public interface Scratch.BranchActionPage : Gtk.Widget {
public abstract BranchAction action { get; }
public abstract Ggit.Ref? branch_ref { get; }
public abstract string new_branch_name { get; }
public virtual void focus_start_widget () {}
}

public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog {
public signal void page_activated ();

public BranchAction action {
get {
return ((BranchActionPage)stack.get_visible_child ()).action;
}
}

public Ggit.Ref branch_ref {
get {
return ((BranchActionPage)stack.get_visible_child ()).branch_ref;
}
}

public string new_branch_name {
get {
return ((BranchActionPage)stack.get_visible_child ()).new_branch_name;
}
}

public bool can_apply { get; set; default = false; }
public FolderManager.ProjectFolderItem project { get; construct; }

private Gtk.Stack stack;

public BranchActionDialog (FolderManager.ProjectFolderItem project) {
Object (
project: project
);
}

construct {
transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window ();
add_button (_("Cancel"), Gtk.ResponseType.CANCEL);
if (project.is_git_repo) {
primary_text = _("Perform branch action on project '%s'").printf (
project.file.file.get_basename ()
);
primary_label.can_focus = false;
image_icon = new ThemedIcon ("git");
var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY);
bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE);
stack = new Gtk.Stack ();
var checkout_page = new BranchCheckoutPage (this);
var create_page = new BranchCreatePage (this);
stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout"));
stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("New"));

var sidebar = new Gtk.StackSidebar () {
stack = stack
};

var content_box = new Gtk.Box (HORIZONTAL, 12);
content_box.add (sidebar);
content_box.add (stack);

custom_bin.add (content_box);
custom_bin.show_all ();
} else {
primary_text = _("'%s' is not a git repository").printf (
project.file.file.get_basename ()
);
secondary_text = _("Unable to perform branch actions");
image_icon = new ThemedIcon ("dialog-error");
}

realize.connect (() => {
((BranchActionPage)stack.get_visible_child ()).focus_start_widget ();
});

page_activated.connect (() => {
if (can_apply) {
response (Gtk.ResponseType.APPLY);
}
});
}
}
48 changes: 48 additions & 0 deletions src/Dialogs/BranchActions/BranchCheckoutPage.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2025 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/

public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage {
public BranchAction action {
get {
return BranchAction.CHECKOUT;
}
}

public Ggit.Ref? branch_ref {
get {
return list_box.get_selected_row ().bref;
}
}

public string new_branch_name {
get {
return "";
}
}

public BranchActionDialog dialog { get; construct; }

private BranchListBox list_box;

public BranchCheckoutPage (BranchActionDialog dialog) {
Object (
dialog: dialog
);
}

construct {
list_box = new BranchListBox (dialog, true);
add (list_box);
list_box.branch_changed.connect ((text) => {
dialog.can_apply = dialog.project.has_branch_name (text, null);
});
}

public override void focus_start_widget () {
list_box.grab_focus ();
}
}
71 changes: 71 additions & 0 deletions src/Dialogs/BranchActions/BranchCreatePage.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2025 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/

public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage {
public BranchAction action {
get {
return BranchAction.CREATE;
}
}

public Ggit.Ref? branch_ref {
get {
return null;
}
}

public string new_branch_name {
get {
return new_branch_name_entry.text;
}
}

public BranchActionDialog dialog { get; construct; }

private Granite.ValidatedEntry new_branch_name_entry;

public BranchCreatePage (BranchActionDialog dialog) {
Object (
dialog: dialog
);
}

construct {
orientation = VERTICAL;
vexpand = false;
hexpand = true;
margin_start = 24;
spacing = 12;
valign = CENTER;
var label = new Granite.HeaderLabel (_("Name of branch to create"));
new_branch_name_entry = new Granite.ValidatedEntry () {
activates_default = true,
placeholder_text = _("Enter new branch name")
};

add (label);
add (new_branch_name_entry);

new_branch_name_entry.bind_property ("is-valid", dialog, "can-apply");

new_branch_name_entry.changed.connect (() => {
unowned var new_name = new_branch_name_entry.text;
if (!dialog.project.is_valid_new_branch_name (new_name)) {
new_branch_name_entry.is_valid = false;
return;
}

if (dialog.project.has_local_branch_name (new_name)) {
new_branch_name_entry.is_valid = false;
return;
}

//Do we need to check remote branches as well?
new_branch_name_entry.is_valid = true;
});
}
}
151 changes: 151 additions & 0 deletions src/Dialogs/BranchActions/BranchListBox.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2025 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/
private class Scratch.Dialogs.BranchListBox : Gtk.Bin {
public signal void branch_changed (string branch_name);
public string text {
get {
return search_entry.text;
}
}

public bool show_remotes { get; construct;}
public BranchActionDialog dialog { get; construct;}

private Gtk.ListBox list_box;
private Gtk.SearchEntry search_entry;
private Gtk.Label local_header;
private Gtk.Label remote_header;
private Gtk.Label recent_header;

public BranchListBox (BranchActionDialog dialog, bool show_remotes) {
Object (
dialog: dialog,
show_remotes: show_remotes
);
}

construct {
list_box = new Gtk.ListBox () {
activate_on_single_click = false
};
var scrolled_window = new Gtk.ScrolledWindow (null, null) {
hscrollbar_policy = NEVER,
vscrollbar_policy = AUTOMATIC,
min_content_height = 200,
vexpand = true
};
scrolled_window.child = list_box;
search_entry = new Gtk.SearchEntry () {
placeholder_text = _("Enter search term")
};
var box = new Gtk.Box (VERTICAL, 6);
box.add (search_entry);
box.add (scrolled_window);
child = box;

recent_header = new Granite.HeaderLabel (_("Recent Branches"));
local_header = new Granite.HeaderLabel (_("Local Branches"));
remote_header = new Granite.HeaderLabel (_("Remote Branches"));
var branch_refs = dialog.project.get_all_branch_refs ();

foreach (var branch_ref in branch_refs) {
if (branch_ref.is_branch () || show_remotes) {
var row = new BranchNameRow (branch_ref);
if (dialog.project.is_recent_ref (branch_ref)) {
row.is_recent = true;
}
list_box.add (row);
}
}
list_box.set_sort_func (listbox_sort_func);
list_box.set_header_func (listbox_header_func);
list_box.row_selected.connect ((listboxrow) => {
//We want cursor to end up after the inserted text
search_entry.text = ((BranchNameRow)(listboxrow)).branch_name;
search_entry.grab_focus_without_selecting ();
search_entry.move_cursor (DISPLAY_LINE_ENDS, 1, false);
});
list_box.row_activated.connect ((listboxrow) => {
dialog.page_activated ();
});
list_box.set_filter_func ((listboxrow) => {
return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text));
});
search_entry.changed.connect (() => {
list_box.invalidate_filter ();
// recent_header.unparent ();
// local_header.unparent ();
// remote_header.unparent ();
// list_box.invalidate_headers ();
branch_changed (text);
});
search_entry.activate.connect (() => {
dialog.page_activated ();
});
}

public BranchNameRow? get_selected_row () {
int index = 0;
var row = list_box.get_row_at_index (index);
while (row != null &&
((BranchNameRow)row).branch_name != search_entry.text) {

row = list_box.get_row_at_index (++index);
}

return (BranchNameRow)row;
}


private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) {
var a = (BranchNameRow)(rowa);
var b = (BranchNameRow)(rowb);

if (a.is_recent && !b.is_recent) {
return -1;
} else if (b.is_recent && !a.is_recent) {
return 1;
}

if (a.is_remote && !b.is_remote) {
return 1;
} else if (b.is_remote && !a.is_remote) {
return -1;
}

return (a.branch_name.collate (b.branch_name));
}

private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) {
var a = (BranchNameRow)row;
var b = (BranchNameRow?)row_before;
a.set_header (null);
if (b == null) {
if (a.is_recent) {
a.set_header (recent_header);
} else if (!a.is_remote) {
a.set_header (local_header);
} else {
a.set_header (remote_header);
}
} else if (b.is_recent) {
if (!a.is_remote) {
a.set_header (local_header);
} else if (a.is_remote) {
a.set_header (remote_header);
}
} else if (!b.is_remote) {
if (a.is_remote) {
a.set_header (remote_header);
}
}
}

public new void grab_focus () {
search_entry.grab_focus ();
}
}
Loading