1 /* SPDX-FileCopyrightText: 2016-2020 - Sébastien Wilmet <swilmet@gnome.org>
2  * SPDX-License-Identifier: LGPL-3.0-or-later
3  */
4 
5 #include "config.h"
6 #include "tepl-tab.h"
7 #include <glib/gi18n-lib.h>
8 #include "tepl-close-confirm-dialog-single.h"
9 #include "tepl-info-bar.h"
10 #include "tepl-tab-group.h"
11 
12 /**
13  * SECTION:tab
14  * @Short_description: Contains a TeplView and GtkInfoBars
15  * @Title: TeplTab
16  * @See_also: #TeplInfoBar
17  *
18  * #TeplTab is meant to be the content of one tab in the text editor (if the
19  * text editor has a Tabbed Document Interface). It is a #GtkGrid container that
20  * contains the #TeplView and can contain one or several #GtkInfoBar's. Since it
21  * is a #GtkGrid, an application can of course add any other widget to it.
22  *
23  * To create a new #GtkInfoBar, it is recommended to use #TeplInfoBar (but
24  * #TeplTab doesn't enforce it).
25  *
26  * By default:
27  * - #TeplTab has a vertical #GtkOrientation.
28  * - The main child widget of #TeplTab is a #GtkScrolledWindow which contains
29  *   the #TeplView.
30  * - #GtkInfoBar's are added on top of the #GtkScrolledWindow.
31  *
32  * The way that the #TeplView is packed into the #TeplTab is customizable with
33  * the ::pack_view virtual function. Similarly, the way that #GtkInfoBar's are
34  * added can be customized with ::pack_info_bar.
35  *
36  * # TeplTabGroup implementation
37  *
38  * #TeplTab implements the #TeplTabGroup interface, for a group of only one tab.
39  * It is useful for text editors that open each file in a separate window, or
40  * for applications that don't require to open more than one file. But the
41  * tepl_tab_group_append_tab() operation is not supported, so some higher-level
42  * features of Tepl don't work with #TeplTab as the #TeplTabGroup of the window.
43  * This will maybe be improved in the future by creating automatically a new
44  * window.
45  */
46 
47 struct _TeplTabPrivate
48 {
49 	GtkScrolledWindow *scrolled_window;
50 	TeplView *view;
51 	TeplGotoLineBar *goto_line_bar;
52 };
53 
54 enum
55 {
56 	PROP_0,
57 	PROP_VIEW,
58 	PROP_ACTIVE_TAB,
59 	PROP_ACTIVE_VIEW,
60 	PROP_ACTIVE_BUFFER,
61 };
62 
63 enum
64 {
65 	SIGNAL_CLOSE_REQUEST,
66 	N_SIGNALS
67 };
68 
69 static guint signals[N_SIGNALS];
70 
71 static void tepl_tab_group_interface_init (gpointer g_iface,
72 					   gpointer iface_data);
73 
G_DEFINE_TYPE_WITH_CODE(TeplTab,tepl_tab,GTK_TYPE_GRID,G_ADD_PRIVATE (TeplTab)G_IMPLEMENT_INTERFACE (TEPL_TYPE_TAB_GROUP,tepl_tab_group_interface_init))74 G_DEFINE_TYPE_WITH_CODE (TeplTab,
75 			 tepl_tab,
76 			 GTK_TYPE_GRID,
77 			 G_ADD_PRIVATE (TeplTab)
78 			 G_IMPLEMENT_INTERFACE (TEPL_TYPE_TAB_GROUP,
79 						tepl_tab_group_interface_init))
80 
81 static GtkScrolledWindow *
82 create_scrolled_window (void)
83 {
84 	GtkScrolledWindow *scrolled_window;
85 
86 	scrolled_window = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL));
87 
88 	/* Disable overlay scrolling, it doesn't work well with GtkTextView. For
89 	 * example to place the cursor with the mouse on the last character of a
90 	 * line.
91 	 */
92 	gtk_scrolled_window_set_overlay_scrolling (scrolled_window, FALSE);
93 
94 	g_object_set (scrolled_window,
95 		      "expand", TRUE,
96 		      NULL);
97 
98 	gtk_widget_show (GTK_WIDGET (scrolled_window));
99 
100 	return scrolled_window;
101 }
102 
103 static void
tepl_tab_pack_view_default(TeplTab * tab,TeplView * view)104 tepl_tab_pack_view_default (TeplTab  *tab,
105 			    TeplView *view)
106 {
107 	if (tab->priv->scrolled_window != NULL)
108 	{
109 		g_warning ("The TeplTab::pack_view virtual function can be called only once.");
110 		return;
111 	}
112 
113 	tab->priv->scrolled_window = create_scrolled_window ();
114 	g_object_ref_sink (tab->priv->scrolled_window);
115 
116 	gtk_container_add (GTK_CONTAINER (tab->priv->scrolled_window),
117 			   GTK_WIDGET (view));
118 
119 	gtk_container_add (GTK_CONTAINER (tab),
120 			   GTK_WIDGET (tab->priv->scrolled_window));
121 }
122 
123 static void
tepl_tab_pack_info_bar_default(TeplTab * tab,GtkInfoBar * info_bar)124 tepl_tab_pack_info_bar_default (TeplTab    *tab,
125 				GtkInfoBar *info_bar)
126 {
127 	GtkWidget *sibling = NULL;
128 
129 	if (tab->priv->scrolled_window != NULL)
130 	{
131 		sibling = GTK_WIDGET (tab->priv->scrolled_window);
132 	}
133 
134 	if (sibling != NULL)
135 	{
136 		gtk_grid_insert_next_to (GTK_GRID (tab), sibling, GTK_POS_TOP);
137 
138 		gtk_grid_attach_next_to (GTK_GRID (tab),
139 					 GTK_WIDGET (info_bar),
140 					 sibling,
141 					 GTK_POS_TOP,
142 					 1, 1);
143 	}
144 	else
145 	{
146 		gtk_container_add (GTK_CONTAINER (tab), GTK_WIDGET (info_bar));
147 	}
148 }
149 
150 static void
tepl_tab_pack_goto_line_bar_default(TeplTab * tab,TeplGotoLineBar * goto_line_bar)151 tepl_tab_pack_goto_line_bar_default (TeplTab         *tab,
152 				     TeplGotoLineBar *goto_line_bar)
153 {
154 	gtk_container_add (GTK_CONTAINER (tab), GTK_WIDGET (goto_line_bar));
155 }
156 
157 static void
close_confirm_dialog_single_cb(GObject * source_object,GAsyncResult * result,gpointer user_data)158 close_confirm_dialog_single_cb (GObject      *source_object,
159 				GAsyncResult *result,
160 				gpointer      user_data)
161 {
162 	TeplTab *tab = TEPL_TAB (source_object);
163 
164 	if (_tepl_close_confirm_dialog_single_finish (tab, result))
165 	{
166 		gtk_widget_destroy (GTK_WIDGET (tab));
167 	}
168 }
169 
170 static void
tepl_tab_close_request_default(TeplTab * tab)171 tepl_tab_close_request_default (TeplTab *tab)
172 {
173 	_tepl_close_confirm_dialog_single_async (tab, close_confirm_dialog_single_cb, NULL);
174 }
175 
176 static void
buffer_notify_cb(GtkTextView * view,GParamSpec * pspec,TeplTab * tab)177 buffer_notify_cb (GtkTextView *view,
178 		  GParamSpec  *pspec,
179 		  TeplTab     *tab)
180 {
181 	g_object_notify (G_OBJECT (tab), "active-buffer");
182 }
183 
184 static void
set_view(TeplTab * tab,TeplView * view)185 set_view (TeplTab  *tab,
186 	  TeplView *view)
187 {
188 	if (view == NULL)
189 	{
190 		/* For tepl_tab_new(). */
191 		view = TEPL_VIEW (tepl_view_new ());
192 		gtk_widget_show (GTK_WIDGET (view));
193 	}
194 
195 	g_return_if_fail (TEPL_IS_VIEW (view));
196 
197 	g_assert (tab->priv->view == NULL);
198 	tab->priv->view = g_object_ref_sink (view);
199 
200 	TEPL_TAB_GET_CLASS (tab)->pack_view (tab, view);
201 
202 	g_signal_connect_object (view,
203 				 "notify::buffer",
204 				 G_CALLBACK (buffer_notify_cb),
205 				 tab,
206 				 0);
207 
208 	g_object_notify (G_OBJECT (tab), "view");
209 }
210 
211 static void
tepl_tab_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)212 tepl_tab_get_property (GObject    *object,
213                        guint       prop_id,
214                        GValue     *value,
215                        GParamSpec *pspec)
216 {
217 	TeplTab *tab = TEPL_TAB (object);
218 	TeplTabGroup *tab_group = TEPL_TAB_GROUP (object);
219 
220 	switch (prop_id)
221 	{
222 		case PROP_VIEW:
223 			g_value_set_object (value, tepl_tab_get_view (tab));
224 			break;
225 
226 		case PROP_ACTIVE_TAB:
227 			g_value_set_object (value, tepl_tab_group_get_active_tab (tab_group));
228 			break;
229 
230 		case PROP_ACTIVE_VIEW:
231 			g_value_set_object (value, tepl_tab_group_get_active_view (tab_group));
232 			break;
233 
234 		case PROP_ACTIVE_BUFFER:
235 			g_value_set_object (value, tepl_tab_group_get_active_buffer (tab_group));
236 			break;
237 
238 		default:
239 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
240 			break;
241 	}
242 }
243 
244 static void
tepl_tab_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)245 tepl_tab_set_property (GObject      *object,
246                        guint         prop_id,
247                        const GValue *value,
248                        GParamSpec   *pspec)
249 {
250 	TeplTab *tab = TEPL_TAB (object);
251 	TeplTabGroup *tab_group = TEPL_TAB_GROUP (object);
252 
253 	switch (prop_id)
254 	{
255 		case PROP_VIEW:
256 			set_view (tab, g_value_get_object (value));
257 			break;
258 
259 		case PROP_ACTIVE_TAB:
260 			tepl_tab_group_set_active_tab (tab_group, g_value_get_object (value));
261 			break;
262 
263 		default:
264 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
265 			break;
266 	}
267 }
268 
269 static void
tepl_tab_dispose(GObject * object)270 tepl_tab_dispose (GObject *object)
271 {
272 	TeplTab *tab = TEPL_TAB (object);
273 
274 	g_clear_object (&tab->priv->scrolled_window);
275 	g_clear_object (&tab->priv->view);
276 	g_clear_object (&tab->priv->goto_line_bar);
277 
278 	G_OBJECT_CLASS (tepl_tab_parent_class)->dispose (object);
279 }
280 
281 static void
tepl_tab_class_init(TeplTabClass * klass)282 tepl_tab_class_init (TeplTabClass *klass)
283 {
284 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
285 
286 	object_class->get_property = tepl_tab_get_property;
287 	object_class->set_property = tepl_tab_set_property;
288 	object_class->dispose = tepl_tab_dispose;
289 
290 	klass->pack_view = tepl_tab_pack_view_default;
291 	klass->pack_info_bar = tepl_tab_pack_info_bar_default;
292 	klass->pack_goto_line_bar = tepl_tab_pack_goto_line_bar_default;
293 	klass->close_request = tepl_tab_close_request_default;
294 
295 	/**
296 	 * TeplTab:view:
297 	 *
298 	 * The #TeplView contained in the tab. When this property is set, the
299 	 * ::pack_view virtual function is called.
300 	 *
301 	 * Since: 3.0
302 	 */
303 	g_object_class_install_property (object_class,
304 					 PROP_VIEW,
305 					 g_param_spec_object ("view",
306 							      "View",
307 							      "",
308 							      TEPL_TYPE_VIEW,
309 							      G_PARAM_READWRITE |
310 							      G_PARAM_CONSTRUCT_ONLY |
311 							      G_PARAM_STATIC_STRINGS));
312 
313 	g_object_class_override_property (object_class, PROP_ACTIVE_TAB, "active-tab");
314 	g_object_class_override_property (object_class, PROP_ACTIVE_VIEW, "active-view");
315 	g_object_class_override_property (object_class, PROP_ACTIVE_BUFFER, "active-buffer");
316 
317 	/**
318 	 * TeplTab::close-request:
319 	 * @tab: the #TeplTab emitting the signal.
320 	 *
321 	 * The ::close-request signal is emitted when there is a request to
322 	 * close the #TeplTab, for example if the user clicks on a close button.
323 	 *
324 	 * The default object method handler does the following:
325 	 * - If the buffer is not modified (according to
326 	 *   gtk_text_buffer_get_modified()), close the tab.
327 	 * - Else, show a message dialog to propose to save the file before
328 	 *   closing.
329 	 *
330 	 * To override the default object method handler, either override the
331 	 * virtual function in a #TeplTab subclass or connect to the signal and
332 	 * call g_signal_stop_emission_by_name().
333 	 *
334 	 * Since: 3.0
335 	 */
336 	signals[SIGNAL_CLOSE_REQUEST] =
337 		g_signal_new ("close-request",
338 			      G_TYPE_FROM_CLASS (klass),
339 			      G_SIGNAL_RUN_LAST,
340 			      G_STRUCT_OFFSET (TeplTabClass, close_request),
341 			      NULL, NULL, NULL,
342 			      G_TYPE_NONE, 0);
343 }
344 
345 static GList *
tepl_tab_get_tabs(TeplTabGroup * tab_group)346 tepl_tab_get_tabs (TeplTabGroup *tab_group)
347 {
348 	return g_list_append (NULL, TEPL_TAB (tab_group));
349 }
350 
351 static TeplTab *
tepl_tab_get_active_tab(TeplTabGroup * tab_group)352 tepl_tab_get_active_tab (TeplTabGroup *tab_group)
353 {
354 	return TEPL_TAB (tab_group);
355 }
356 
357 static void
tepl_tab_group_interface_init(gpointer g_iface,gpointer iface_data)358 tepl_tab_group_interface_init (gpointer g_iface,
359 			       gpointer iface_data)
360 {
361 	TeplTabGroupInterface *interface = g_iface;
362 
363 	interface->get_tabs = tepl_tab_get_tabs;
364 	interface->get_active_tab = tepl_tab_get_active_tab;
365 }
366 
367 static void
tepl_tab_init(TeplTab * tab)368 tepl_tab_init (TeplTab *tab)
369 {
370 	tab->priv = tepl_tab_get_instance_private (tab);
371 
372 	gtk_orientable_set_orientation (GTK_ORIENTABLE (tab), GTK_ORIENTATION_VERTICAL);
373 }
374 
375 /**
376  * tepl_tab_new:
377  *
378  * Creates a new #TeplTab with a new #TeplView. The new #TeplView can be
379  * retrieved afterwards with tepl_tab_get_view().
380  *
381  * Returns: a new #TeplTab.
382  * Since: 3.0
383  */
384 TeplTab *
tepl_tab_new(void)385 tepl_tab_new (void)
386 {
387 	return g_object_new (TEPL_TYPE_TAB, NULL);
388 }
389 
390 /**
391  * tepl_tab_new_with_view:
392  * @view: the #TeplView that will be contained in the tab.
393  *
394  * Returns: a new #TeplTab.
395  * Since: 3.0
396  */
397 TeplTab *
tepl_tab_new_with_view(TeplView * view)398 tepl_tab_new_with_view (TeplView *view)
399 {
400 	g_return_val_if_fail (TEPL_IS_VIEW (view), NULL);
401 
402 	return g_object_new (TEPL_TYPE_TAB,
403 			     "view", view,
404 			     NULL);
405 }
406 
407 /**
408  * tepl_tab_get_view:
409  * @tab: a #TeplTab.
410  *
411  * Returns: (transfer none): the #TeplView contained in @tab.
412  * Since: 3.0
413  */
414 TeplView *
tepl_tab_get_view(TeplTab * tab)415 tepl_tab_get_view (TeplTab *tab)
416 {
417 	g_return_val_if_fail (TEPL_IS_TAB (tab), NULL);
418 
419 	return tab->priv->view;
420 }
421 
422 /**
423  * tepl_tab_get_buffer:
424  * @tab: a #TeplTab.
425  *
426  * A convenience function that calls gtk_text_view_get_buffer() on the
427  * #TeplTab:view associated with the @tab.
428  *
429  * Returns: (transfer none): the #TeplBuffer of the #TeplTab:view.
430  * Since: 3.0
431  */
432 TeplBuffer *
tepl_tab_get_buffer(TeplTab * tab)433 tepl_tab_get_buffer (TeplTab *tab)
434 {
435 	g_return_val_if_fail (TEPL_IS_TAB (tab), NULL);
436 
437 	if (tab->priv->view == NULL)
438 	{
439 		return NULL;
440 	}
441 
442 	return TEPL_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (tab->priv->view)));
443 }
444 
445 /**
446  * tepl_tab_get_goto_line_bar:
447  * @tab: a #TeplTab.
448  *
449  * Gets the #TeplGotoLineBar widget belonging to @tab. The #TeplGotoLineBar must
450  * not be destroyed by the application, the purpose of this function is to
451  * show/hide the widget.
452  *
453  * Returns: (transfer none): the #TeplGotoLineBar widget belonging to @tab.
454  * Since: 5.0
455  */
456 TeplGotoLineBar *
tepl_tab_get_goto_line_bar(TeplTab * tab)457 tepl_tab_get_goto_line_bar (TeplTab *tab)
458 {
459 	g_return_val_if_fail (TEPL_IS_TAB (tab), NULL);
460 
461 	if (tab->priv->goto_line_bar == NULL)
462 	{
463 		tab->priv->goto_line_bar = tepl_goto_line_bar_new ();
464 		g_object_ref_sink (tab->priv->goto_line_bar);
465 
466 		/* The TeplGotoLineBar needs to be explicitly shown/hidden. */
467 		gtk_widget_set_no_show_all (GTK_WIDGET (tab->priv->goto_line_bar), TRUE);
468 
469 		tepl_goto_line_bar_set_view (tab->priv->goto_line_bar,
470 					     tab->priv->view);
471 
472 		TEPL_TAB_GET_CLASS (tab)->pack_goto_line_bar (tab, tab->priv->goto_line_bar);
473 	}
474 
475 	return tab->priv->goto_line_bar;
476 }
477 
478 /**
479  * tepl_tab_add_info_bar:
480  * @tab: a #TeplTab.
481  * @info_bar: a #GtkInfoBar.
482  *
483  * Attaches @info_bar to @tab.
484  *
485  * This function calls the ::pack_info_bar virtual function.
486  *
487  * Since: 1.0
488  */
489 void
tepl_tab_add_info_bar(TeplTab * tab,GtkInfoBar * info_bar)490 tepl_tab_add_info_bar (TeplTab    *tab,
491 		       GtkInfoBar *info_bar)
492 {
493 	g_return_if_fail (TEPL_IS_TAB (tab));
494 	g_return_if_fail (GTK_IS_INFO_BAR (info_bar));
495 
496 	_tepl_info_bar_set_size_request (info_bar);
497 
498 	TEPL_TAB_GET_CLASS (tab)->pack_info_bar (tab, info_bar);
499 }
500