@@ -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
3136public 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