Skip to content

Commit f189a79

Browse files
jeremypwzeebok
andauthored
BranchActions dialog (#1607)
* Start BranchActionDialog * Cleanup, refactor, bind apply button sensitivity * Do not bind model; private Row class * Include headers * Use Stack and StackSidebar for more actions * Move stuff in to private classes * Use common interface for stack pages * Reimplement checkout branch; refactor * Start to implement recent branches * Coding improvements * Get name of Branch not Ref * Split out private classes * Implement create new branch * Small Cleanup * Focus desired widget on startup * Code improvement * Activate on double-click * Refocus after select, activate on Enter * Make listbox more general purpose * Simplify header func * Suppress terminal warnings * Remove unimplemented functions * Remove commented out code * Fix faulty merge * Cleanup, add whitespace * Apply suggestions from code review Cleanup as suggested Co-authored-by: Ryan Kornheisl <ryan@skarva.tech> --------- Co-authored-by: Ryan Kornheisl <ryan@skarva.tech>
1 parent 09ed764 commit f189a79

File tree

10 files changed

+679
-215
lines changed

10 files changed

+679
-215
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
6+
*/
7+
public enum Scratch.BranchAction {
8+
CHECKOUT,
9+
COMMIT,
10+
PUSH,
11+
PULL,
12+
MERGE,
13+
DELETE,
14+
CREATE
15+
}
16+
17+
public interface Scratch.BranchActionPage : Gtk.Widget {
18+
public abstract BranchAction action { get; }
19+
public abstract Ggit.Ref? branch_ref { get; }
20+
public abstract string new_branch_name { get; }
21+
public virtual void focus_start_widget () {}
22+
}
23+
24+
public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog {
25+
public signal void page_activated ();
26+
27+
public BranchAction action {
28+
get {
29+
return ((BranchActionPage)stack.get_visible_child ()).action;
30+
}
31+
}
32+
33+
public Ggit.Ref branch_ref {
34+
get {
35+
return ((BranchActionPage)stack.get_visible_child ()).branch_ref;
36+
}
37+
}
38+
39+
public string new_branch_name {
40+
get {
41+
return ((BranchActionPage)stack.get_visible_child ()).new_branch_name;
42+
}
43+
}
44+
45+
public bool can_apply { get; set; default = false; }
46+
public FolderManager.ProjectFolderItem project { get; construct; }
47+
48+
private Gtk.Stack stack;
49+
50+
public BranchActionDialog (FolderManager.ProjectFolderItem project) {
51+
Object (
52+
project: project
53+
);
54+
}
55+
56+
construct {
57+
transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window ();
58+
add_button (_("Cancel"), Gtk.ResponseType.CANCEL);
59+
if (project.is_git_repo) {
60+
primary_text = _("Perform branch action on project '%s'").printf (
61+
project.file.file.get_basename ()
62+
);
63+
primary_label.can_focus = false;
64+
65+
image_icon = new ThemedIcon ("git");
66+
67+
var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY);
68+
bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE);
69+
70+
var checkout_page = new BranchCheckoutPage (this);
71+
var create_page = new BranchCreatePage (this);
72+
73+
stack = new Gtk.Stack ();
74+
stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout"));
75+
stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("New"));
76+
77+
var sidebar = new Gtk.StackSidebar () {
78+
stack = stack
79+
};
80+
81+
var content_box = new Gtk.Box (HORIZONTAL, 12);
82+
content_box.add (sidebar);
83+
content_box.add (stack);
84+
85+
custom_bin.add (content_box);
86+
custom_bin.show_all ();
87+
} else {
88+
primary_text = _("'%s' is not a git repository").printf (
89+
project.file.file.get_basename ()
90+
);
91+
secondary_text = _("Unable to perform branch actions");
92+
image_icon = new ThemedIcon ("dialog-error");
93+
}
94+
95+
realize.connect (() => {
96+
((BranchActionPage)stack.get_visible_child ()).focus_start_widget ();
97+
});
98+
99+
page_activated.connect (() => {
100+
if (can_apply) {
101+
response (Gtk.ResponseType.APPLY);
102+
}
103+
});
104+
}
105+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
6+
*/
7+
8+
public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage {
9+
public BranchAction action {
10+
get {
11+
return BranchAction.CHECKOUT;
12+
}
13+
}
14+
15+
public Ggit.Ref? branch_ref {
16+
get {
17+
return list_box.get_selected_row ().bref;
18+
}
19+
}
20+
21+
public string new_branch_name {
22+
get {
23+
return "";
24+
}
25+
}
26+
27+
public BranchActionDialog dialog { get; construct; }
28+
29+
private BranchListBox list_box;
30+
31+
public BranchCheckoutPage (BranchActionDialog dialog) {
32+
Object (
33+
dialog: dialog
34+
);
35+
}
36+
37+
construct {
38+
list_box = new BranchListBox (dialog, true);
39+
add (list_box);
40+
list_box.branch_changed.connect ((text) => {
41+
dialog.can_apply = dialog.project.has_branch_name (text, null);
42+
});
43+
}
44+
45+
public override void focus_start_widget () {
46+
list_box.grab_focus ();
47+
}
48+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
6+
*/
7+
8+
public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage {
9+
public BranchAction action {
10+
get {
11+
return BranchAction.CREATE;
12+
}
13+
}
14+
15+
public Ggit.Ref? branch_ref {
16+
get {
17+
return null;
18+
}
19+
}
20+
21+
public string new_branch_name {
22+
get {
23+
return new_branch_name_entry.text;
24+
}
25+
}
26+
27+
public BranchActionDialog dialog { get; construct; }
28+
29+
private Granite.ValidatedEntry new_branch_name_entry;
30+
31+
public BranchCreatePage (BranchActionDialog dialog) {
32+
Object (
33+
dialog: dialog
34+
);
35+
}
36+
37+
construct {
38+
orientation = VERTICAL;
39+
vexpand = false;
40+
hexpand = true;
41+
margin_start = 24;
42+
spacing = 12;
43+
valign = CENTER;
44+
var label = new Granite.HeaderLabel (_("Name of branch to create"));
45+
new_branch_name_entry = new Granite.ValidatedEntry () {
46+
activates_default = true,
47+
placeholder_text = _("Enter new branch name")
48+
};
49+
50+
add (label);
51+
add (new_branch_name_entry);
52+
53+
new_branch_name_entry.bind_property ("is-valid", dialog, "can-apply");
54+
55+
new_branch_name_entry.changed.connect (() => {
56+
unowned var new_name = new_branch_name_entry.text;
57+
if (!dialog.project.is_valid_new_branch_name (new_name)) {
58+
new_branch_name_entry.is_valid = false;
59+
return;
60+
}
61+
62+
if (dialog.project.has_local_branch_name (new_name)) {
63+
new_branch_name_entry.is_valid = false;
64+
return;
65+
}
66+
67+
//Do we need to check remote branches as well?
68+
new_branch_name_entry.is_valid = true;
69+
});
70+
}
71+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
6+
*/
7+
private class Scratch.Dialogs.BranchListBox : Gtk.Bin {
8+
9+
public signal void branch_changed (string branch_name);
10+
11+
public string text {
12+
get {
13+
return search_entry.text;
14+
}
15+
}
16+
17+
public bool show_remotes { get; construct;}
18+
public BranchActionDialog dialog { get; construct;}
19+
20+
private Gtk.ListBox list_box;
21+
private Gtk.SearchEntry search_entry;
22+
private Gtk.Label local_header;
23+
private Gtk.Label remote_header;
24+
private Gtk.Label recent_header;
25+
26+
public BranchListBox (BranchActionDialog dialog, bool show_remotes) {
27+
Object (
28+
dialog: dialog,
29+
show_remotes: show_remotes
30+
);
31+
}
32+
33+
construct {
34+
list_box = new Gtk.ListBox () {
35+
activate_on_single_click = false
36+
};
37+
38+
var scrolled_window = new Gtk.ScrolledWindow (null, null) {
39+
hscrollbar_policy = NEVER,
40+
vscrollbar_policy = AUTOMATIC,
41+
min_content_height = 200,
42+
vexpand = true
43+
};
44+
scrolled_window.child = list_box;
45+
46+
search_entry = new Gtk.SearchEntry () {
47+
placeholder_text = _("Enter search term")
48+
};
49+
50+
var box = new Gtk.Box (VERTICAL, 6);
51+
box.add (search_entry);
52+
box.add (scrolled_window);
53+
54+
child = box;
55+
56+
recent_header = new Granite.HeaderLabel (_("Recent Branches"));
57+
local_header = new Granite.HeaderLabel (_("Local Branches"));
58+
remote_header = new Granite.HeaderLabel (_("Remote Branches"));
59+
var branch_refs = dialog.project.get_all_branch_refs ();
60+
61+
foreach (var branch_ref in branch_refs) {
62+
if (branch_ref.is_branch () || show_remotes) {
63+
var row = new BranchNameRow (branch_ref);
64+
if (dialog.project.is_recent_ref (branch_ref)) {
65+
row.is_recent = true;
66+
}
67+
68+
list_box.add (row);
69+
}
70+
}
71+
72+
list_box.set_sort_func (listbox_sort_func);
73+
list_box.set_header_func (listbox_header_func);
74+
list_box.row_selected.connect ((listboxrow) => {
75+
//We want cursor to end up after the inserted text
76+
search_entry.text = ((BranchNameRow)(listboxrow)).branch_name;
77+
search_entry.grab_focus_without_selecting ();
78+
search_entry.move_cursor (DISPLAY_LINE_ENDS, 1, false);
79+
});
80+
list_box.row_activated.connect ((listboxrow) => {
81+
dialog.page_activated ();
82+
});
83+
list_box.set_filter_func ((listboxrow) => {
84+
return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text));
85+
});
86+
87+
search_entry.changed.connect (() => {
88+
list_box.invalidate_filter ();
89+
branch_changed (text);
90+
});
91+
search_entry.activate.connect (() => {
92+
dialog.page_activated ();
93+
});
94+
}
95+
96+
public BranchNameRow? get_selected_row () {
97+
int index = 0;
98+
var row = list_box.get_row_at_index (index);
99+
while (row != null &&
100+
((BranchNameRow)row).branch_name != search_entry.text) {
101+
102+
row = list_box.get_row_at_index (++index);
103+
}
104+
105+
return (BranchNameRow)row;
106+
}
107+
108+
109+
private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) {
110+
var a = (BranchNameRow)(rowa);
111+
var b = (BranchNameRow)(rowb);
112+
113+
if (a.is_recent && !b.is_recent) {
114+
return -1;
115+
} else if (b.is_recent && !a.is_recent) {
116+
return 1;
117+
}
118+
119+
if (a.is_remote && !b.is_remote) {
120+
return 1;
121+
} else if (b.is_remote && !a.is_remote) {
122+
return -1;
123+
}
124+
125+
return (a.branch_name.collate (b.branch_name));
126+
}
127+
128+
private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) {
129+
var a = (BranchNameRow)row;
130+
var b = (BranchNameRow?)row_before;
131+
a.set_header (null);
132+
if (b == null) {
133+
if (a.is_recent) {
134+
a.set_header (recent_header);
135+
} else if (!a.is_remote) {
136+
a.set_header (local_header);
137+
} else {
138+
a.set_header (remote_header);
139+
}
140+
} else if (b.is_recent) {
141+
if (!a.is_remote) {
142+
a.set_header (local_header);
143+
} else if (a.is_remote) {
144+
a.set_header (remote_header);
145+
}
146+
} else if (!b.is_remote) {
147+
if (a.is_remote) {
148+
a.set_header (remote_header);
149+
}
150+
}
151+
}
152+
153+
public new void grab_focus () {
154+
search_entry.grab_focus ();
155+
}
156+
}

0 commit comments

Comments
 (0)