Skip to content

Commit 87ddd97

Browse files
committed
Initial formatting support
1 parent 516dcc2 commit 87ddd97

File tree

5 files changed

+329
-3
lines changed

5 files changed

+329
-3
lines changed

data/Application.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ editablelabel {
8888
}
8989

9090
editablelabel.editing {
91-
font-weight: 600;
91+
font-weight: 500;
9292
font-size: 1em;
9393
text-shadow: none;
9494
letter-spacing: .0em;

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ executable(
4747
'src/Services/Stash.vala',
4848
'src/Services/Themer.vala',
4949
'src/Services/Utils.vala',
50+
'src/Services/FormatRequest.vala',
5051
'src/Widgets/StickyView.vala',
5152
'src/Widgets/ColorPill.vala',
5253
'src/Widgets/SettingsPopover.vala',

src/Services/FormatRequest.vala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* Courtesy of Colin Kiama
2+
https://github.com/colinkiama/vala-gtk4-text-formatting-demo/tree/main
3+
4+
5+
*/
6+
7+
8+
9+
public struct FormattingRequest {
10+
public Gee.ArrayList<FormattingType?> formatting_types;
11+
public int insert_offset;
12+
public int insert_length;
13+
}
14+
15+
public enum FormattingType {
16+
BOLD,
17+
ITALIC,
18+
UNDERLINE;
19+
}

src/Widgets/SettingsPopover.vala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ public class jorts.SettingsPopover : Gtk.Popover {
151151
homogeneous = true,
152152
hexpand = true,
153153
margin_top = 12,
154-
//margin_start = 6,
155-
//margin_end = 6,
154+
margin_start = 6,
155+
margin_end = 6,
156156
margin_bottom = 6
157157
};
158158
font_size_box.append (this.zoom_out_button);

src/Widgets/StickyView.vala

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ Notably:
2626
-recognize local links to open in Files
2727
-Allow a zoom unzoom
2828
29+
Hypertextview is a Granite widget derived from TextView
30+
31+
Formatting code courtesy of Colin Kiama
32+
https://github.com/colinkiama/vala-gtk4-text-formatting-demo/tree/main
33+
2934
*/
3035

3136
public class jorts.StickyView : Granite.HyperTextView {
@@ -34,6 +39,28 @@ public class jorts.StickyView : Granite.HyperTextView {
3439
public int max_zoom;
3540
public int min_zoom;
3641

42+
private SimpleActionGroup actions;
43+
private Gee.ArrayQueue<FormattingRequest?> formatting_queue = new Gee.ArrayQueue<FormattingRequest?> ();
44+
45+
public const string FORMAT_ACTION_GROUP_PREFIX = "format";
46+
public const string FORMAT_ACTION_PREFIX = FORMAT_ACTION_GROUP_PREFIX + ".";
47+
public const string FORMAT_ACTION_BOLD = "bold";
48+
public const string FORMAT_ACTION_ITALIC = "italic";
49+
public const string FORMAT_ACTION_UNDERLINE = "underline";
50+
51+
public const string ICON_NAME_BOLD = "format-text-bold-symbolic";
52+
public const string ICON_NAME_ITALIC = "format-text-italic-symbolic";
53+
public const string ICON_NAME_UNDERLINE = "format-text-underline-symbolic";
54+
55+
private static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string> ();
56+
57+
static construct {
58+
action_accelerators[FORMAT_ACTION_BOLD] = "<Control>B";
59+
action_accelerators[FORMAT_ACTION_ITALIC] = "<Control>I";
60+
action_accelerators[FORMAT_ACTION_UNDERLINE] = "<Control>U";
61+
}
62+
63+
3764
public StickyView (int zoom, string? content) {
3865

3966
this.buffer = new Gtk.TextBuffer (null);
@@ -45,6 +72,19 @@ public class jorts.StickyView : Granite.HyperTextView {
4572
this.top_margin = 10;
4673
this.set_hexpand (true);
4774
this.set_vexpand (true);
75+
76+
this.buffer.create_tag (FORMAT_ACTION_BOLD, "weight", 700);
77+
this.buffer.create_tag (FORMAT_ACTION_ITALIC, "style", 2);
78+
this.buffer.create_tag (FORMAT_ACTION_UNDERLINE, "underline", Pango.Underline.SINGLE);
79+
this.buffer.changed.connect (this.handle_text_buffer_change);
80+
this.buffer.insert_text.connect (this.handle_text_buffer_inserted_text);
81+
this.buffer.mark_set.connect (this.handle_text_buffer_mark_set);
82+
83+
84+
this.actions = this.create_formatting_actions ();
85+
this.register_action_accelerators ();
86+
this.add_formatting_options_to_text_view_context_menu (this);
87+
4888
}
4989

5090

@@ -53,4 +93,270 @@ public class jorts.StickyView : Granite.HyperTextView {
5393
this.buffer.get_bounds (out start, out end);
5494
return this.buffer.get_text (start, end, true);
5595
}
96+
97+
98+
99+
100+
private void register_action_accelerators () {
101+
this.insert_action_group (FORMAT_ACTION_GROUP_PREFIX, actions);
102+
103+
foreach (var action_name in action_accelerators.get_keys ()) {
104+
((Gtk.Application) GLib.Application.get_default ()).set_accels_for_action (
105+
FORMAT_ACTION_PREFIX + action_name,
106+
action_accelerators[action_name].to_array ()
107+
);
108+
}
109+
}
110+
111+
private SimpleActionGroup create_formatting_actions () {
112+
var actions_to_return = new SimpleActionGroup ();
113+
114+
ActionEntry[] entries = {
115+
{ FORMAT_ACTION_BOLD, null, null, "false", this.toggle_format},
116+
{ FORMAT_ACTION_ITALIC, null, null, "false", this.toggle_format },
117+
{ FORMAT_ACTION_UNDERLINE, null, null, "false", this.toggle_format },
118+
};
119+
120+
SimpleAction action;
121+
122+
actions_to_return.add_action_entries (entries, this);
123+
124+
action = (SimpleAction)actions_to_return.lookup_action (FORMAT_ACTION_BOLD);
125+
action.set_enabled (true);
126+
127+
action = (SimpleAction)actions_to_return.lookup_action (FORMAT_ACTION_ITALIC);
128+
action.set_enabled (true);
129+
130+
action = (SimpleAction)actions_to_return.lookup_action (FORMAT_ACTION_UNDERLINE);
131+
action.set_enabled (true);
132+
133+
return actions_to_return;
134+
}
135+
136+
137+
// Adapted from GTK 4 Widget Factory Demo: https://gitlab.gnome.org/GNOME/gtk/-/tree/main/demos/widget-factory
138+
private void add_formatting_options_to_text_view_context_menu (Gtk.TextView text_view) {
139+
Menu menu = this.create_formatting_menu ();
140+
this.set_extra_menu (menu);
141+
}
142+
143+
private void handle_text_buffer_inserted_text (ref Gtk.TextIter iter, string new_text, int new_text_length) {
144+
Gtk.TextTagTable text_buffer_tags = this.buffer.get_tag_table ();
145+
Gtk.TextTag bold_tag = text_buffer_tags.lookup (FORMAT_ACTION_BOLD);
146+
Gtk.TextTag italic_tag = text_buffer_tags.lookup (FORMAT_ACTION_ITALIC);
147+
Gtk.TextTag underline_tag = text_buffer_tags.lookup (FORMAT_ACTION_UNDERLINE);
148+
149+
Gee.ArrayList<FormattingType?> formatting_types_to_add = new Gee.ArrayList<FormattingType?> ();
150+
151+
152+
if (!iter.has_tag (bold_tag) && (bool)this.get_formatting_action (FORMAT_ACTION_BOLD).get_state ()) {
153+
formatting_types_to_add.add (FormattingType.BOLD);
154+
}
155+
156+
if (!iter.has_tag (italic_tag) && (bool)this.get_formatting_action (FORMAT_ACTION_ITALIC).get_state ()) {
157+
formatting_types_to_add.add (FormattingType.ITALIC);
158+
}
159+
160+
if (!iter.has_tag (underline_tag) && (bool)this.get_formatting_action (FORMAT_ACTION_UNDERLINE).get_state ()) {
161+
formatting_types_to_add.add (FormattingType.UNDERLINE);
162+
}
163+
164+
if (formatting_types_to_add.size == 0) {
165+
return;
166+
}
167+
168+
this.formatting_queue.offer (FormattingRequest () {
169+
formatting_types = formatting_types_to_add,
170+
insert_offset = iter.get_offset (),
171+
insert_length = new_text.length,
172+
});
173+
}
174+
175+
176+
177+
178+
179+
180+
181+
182+
183+
184+
185+
private Menu create_formatting_menu () {
186+
Menu menu = new Menu ();
187+
MenuItem item;
188+
189+
this.insert_action_group (FORMAT_ACTION_GROUP_PREFIX, this.actions);
190+
191+
item = new MenuItem ("Bold", FORMAT_ACTION_PREFIX + FORMAT_ACTION_BOLD);
192+
item.set_attribute ("touch-icon", "s", ICON_NAME_BOLD);
193+
menu.append_item (item);
194+
195+
item = new MenuItem ("Italic", FORMAT_ACTION_PREFIX + FORMAT_ACTION_ITALIC);
196+
item.set_attribute ("touch-icon", "s", ICON_NAME_ITALIC);
197+
menu.append_item (item);
198+
199+
item = new MenuItem ("Underline", FORMAT_ACTION_PREFIX + FORMAT_ACTION_UNDERLINE);
200+
item.set_attribute ("touch-icon", "s", ICON_NAME_UNDERLINE);
201+
menu.append_item (item);
202+
203+
return menu;
204+
}
205+
206+
207+
208+
private void process_formatting_queue (Gtk.TextBuffer buffer) {
209+
while (this.formatting_queue.size > 0) {
210+
FormattingRequest formatting_request = this.formatting_queue.poll ();
211+
212+
foreach (FormattingType? formatting_type in formatting_request.formatting_types) {
213+
Gtk.TextIter start_iterator;
214+
Gtk.TextIter end_iterator;
215+
buffer.get_iter_at_offset (out start_iterator, formatting_request.insert_offset);
216+
buffer.get_iter_at_offset (
217+
out end_iterator,
218+
formatting_request.insert_offset + formatting_request.insert_length);
219+
buffer.apply_tag_by_name (enum_to_nick (
220+
(int)formatting_type,
221+
typeof (FormattingType)),
222+
start_iterator,
223+
end_iterator
224+
);
225+
}
226+
}
227+
}
228+
229+
private void handle_text_cursor_movement_with_no_selection (
230+
Gtk.TextIter cursor_iter,
231+
Gee.HashMap<string, Gtk.TextTag> formatting_tags,
232+
Gee.HashMap<string, SimpleAction> formatting_actions) {
233+
// Get cursor position and set action state
234+
// based on if tag is applied in cursor position
235+
bool has_bold_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_BOLD]);
236+
bool has_italic_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_ITALIC]);
237+
bool has_underline_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_UNDERLINE]);
238+
bool has_formatting = has_bold_tag || has_italic_tag || has_underline_tag;
239+
240+
if (!has_formatting) {
241+
bool could_move_back = cursor_iter.backward_char ();
242+
if (could_move_back) {
243+
has_bold_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_BOLD]);
244+
has_italic_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_ITALIC]);
245+
has_underline_tag = cursor_iter.has_tag (formatting_tags[FORMAT_ACTION_UNDERLINE]);
246+
247+
formatting_actions[FORMAT_ACTION_BOLD].set_state (has_bold_tag);
248+
formatting_actions[FORMAT_ACTION_ITALIC].set_state (has_italic_tag);
249+
formatting_actions[FORMAT_ACTION_UNDERLINE].set_state (has_underline_tag);
250+
return;
251+
}
252+
}
253+
254+
formatting_actions[FORMAT_ACTION_BOLD].set_state (has_bold_tag);
255+
formatting_actions[FORMAT_ACTION_ITALIC].set_state (has_italic_tag);
256+
formatting_actions[FORMAT_ACTION_UNDERLINE].set_state (has_underline_tag);
257+
return;
258+
}
259+
260+
private void handle_text_buffer_change (Gtk.TextBuffer buffer) {
261+
if (this.formatting_queue.size > 0) {
262+
this.process_formatting_queue (buffer);
263+
return;
264+
}
265+
266+
SimpleAction bold_action = this.get_formatting_action (FORMAT_ACTION_BOLD);
267+
SimpleAction italic_action = this.get_formatting_action (FORMAT_ACTION_ITALIC);
268+
SimpleAction underline_action = this.get_formatting_action (FORMAT_ACTION_UNDERLINE);
269+
Gtk.TextTagTable text_buffer_tags = this.buffer.get_tag_table ();
270+
Gtk.TextTag bold_tag = text_buffer_tags.lookup (FORMAT_ACTION_BOLD);
271+
Gtk.TextTag italic_tag = text_buffer_tags.lookup (FORMAT_ACTION_ITALIC);
272+
Gtk.TextTag underline_tag = text_buffer_tags.lookup (FORMAT_ACTION_UNDERLINE);
273+
Gtk.TextIter iterator = Gtk.TextIter ();
274+
Gtk.TextIter start_iterator, end_iterator;
275+
bool is_all_bold = true;
276+
bool is_all_italic = true;
277+
bool is_all_underline = true;
278+
bool has_selection = this.buffer.get_selection_bounds (out start_iterator, out end_iterator);
279+
280+
if (!has_selection) {
281+
int cursor_position = this.buffer.cursor_position;
282+
Gtk.TextIter cursor_iter;
283+
284+
this.buffer.get_iter_at_offset (out cursor_iter, cursor_position);
285+
var formatting_tags = new Gee.HashMap<string, Gtk.TextTag> ();
286+
formatting_tags[FORMAT_ACTION_BOLD] = bold_tag;
287+
formatting_tags[FORMAT_ACTION_ITALIC] = italic_tag;
288+
formatting_tags[FORMAT_ACTION_UNDERLINE] = underline_tag;
289+
290+
var formatting_actions = new Gee.HashMap<string, SimpleAction> ();
291+
formatting_actions[FORMAT_ACTION_BOLD] = bold_action;
292+
formatting_actions[FORMAT_ACTION_ITALIC] = italic_action;
293+
formatting_actions[FORMAT_ACTION_UNDERLINE] = underline_action;
294+
295+
this.handle_text_cursor_movement_with_no_selection (cursor_iter, formatting_tags, formatting_actions);
296+
return;
297+
}
298+
299+
iterator.assign (start_iterator);
300+
301+
// For each formatting option, check if selected text is all within
302+
// the respective formatting tags
303+
while (!iterator.equal (end_iterator)) {
304+
is_all_bold &= iterator.has_tag (bold_tag);
305+
is_all_italic &= iterator.has_tag (italic_tag);
306+
is_all_underline &= iterator.has_tag (underline_tag);
307+
iterator.forward_char ();
308+
}
309+
310+
bold_action.set_state (is_all_bold);
311+
italic_action.set_state (is_all_italic);
312+
underline_action.set_state (is_all_underline);
313+
}
314+
315+
// Is called whenever a text selection is made and in each time the caret moves
316+
private void handle_text_buffer_mark_set (Gtk.TextBuffer buffer, Gtk.TextIter iterator, Gtk.TextMark mark) {
317+
if (mark.name != "insert") {
318+
return;
319+
}
320+
321+
this.handle_text_buffer_change (buffer);
322+
}
323+
324+
private void toggle_format (SimpleAction action, Variant value) {
325+
Gtk.TextIter start_iterator, end_itrator;
326+
string name = action.get_name ();
327+
328+
action.set_state (value);
329+
330+
this.buffer.get_selection_bounds (out start_iterator, out end_itrator);
331+
332+
if (value.get_boolean ()) {
333+
this.buffer.apply_tag_by_name (name, start_iterator, end_itrator);
334+
} else {
335+
this.buffer.remove_tag_by_name (name, start_iterator, end_itrator);
336+
}
337+
}
338+
339+
private SimpleAction get_formatting_action (string name) {
340+
return this.actions.lookup_action (name) as SimpleAction;
341+
}
342+
343+
string enum_to_nick (int @value, Type enum_type) {
344+
var enum_class = (EnumClass) enum_type.class_ref ();
345+
346+
if (enum_class == null) {
347+
return "%i".printf (@value);
348+
}
349+
350+
unowned var enum_value = enum_class.get_value (@value);
351+
352+
if (enum_value == null) {
353+
return "%i".printf (@value);
354+
}
355+
356+
return enum_value.value_nick;
357+
}
358+
359+
360+
361+
56362
}

0 commit comments

Comments
 (0)