1 /*
2  * application.c: Manage the data common to all workbooks
3  *
4  * Author:
5  *     Jody Goldberg <jody@gnome.org>
6  */
7 #include <gnumeric-config.h>
8 #include <string.h>
9 #include <gnumeric.h>
10 #include <application.h>
11 
12 #include <clipboard.h>
13 #include <selection.h>
14 #include <workbook-control.h>
15 #include <workbook-view.h>
16 #include <workbook.h>
17 #include <sheet.h>
18 #include <sheet-view.h>
19 #include <sheet-private.h>
20 #include <sheet-object.h>
21 #include <gutils.h>
22 #include <ranges.h>
23 #include <sheet-object.h>
24 #include <commands.h>
25 #include <gui-clipboard.h>
26 #include <expr-name.h>
27 #include <workbook-priv.h>
28 
29 #include <gnumeric-conf.h>
30 #include <goffice/goffice.h>
31 #include <gsf/gsf-impl-utils.h>
32 #include <gnm-i18n.h>
33 
34 #define GNM_APP(o)		(G_TYPE_CHECK_INSTANCE_CAST((o), GNM_APP_TYPE, GnmApp))
35 #define GNM_IS_APP(o)		(G_TYPE_CHECK_INSTANCE_TYPE((o), GNM_APP_TYPE))
36 
37 enum {
38 	PROP_0,
39 	PROP_HISTORY_LIST,
40 	PROP_SHUTTING_DOWN,
41 	PROP_INITIAL_OPEN_COMPLETE
42 };
43 /* Signals */
44 enum {
45 	WORKBOOK_ADDED,
46 	WORKBOOK_REMOVED,
47 	WINDOW_LIST_CHANGED,
48 	CUSTOM_UI_ADDED,
49 	CUSTOM_UI_REMOVED,
50 	CLIPBOARD_MODIFIED,
51 	RECALC_FINISHED,
52 	RECALC_CLEAR_CACHES,
53 	LAST_SIGNAL
54 };
55 
56 static guint signals[LAST_SIGNAL] = { 0 };
57 
58 struct _GnmApp {
59 	GObject  base;
60 
61 	/* Clipboard */
62 	SheetView	*clipboard_sheet_view;
63 	GnmCellRegion	*clipboard_copied_contents;
64 	GnmRange	*clipboard_cut_range;
65 
66 	GList		*workbook_list;
67 
68 	/* Recalculation manager.  */
69 	int             recalc_count;
70 
71 	GtkRecentManager *recent;
72 	gulong           recent_sig;
73 
74 	gboolean         shutting_down;
75 	gboolean         initial_open_complete;
76 };
77 
78 typedef struct {
79 	GObjectClass     parent;
80 
81 	void (*workbook_added)      (GnmApp *gnm_app, Workbook *wb);
82 	void (*workbook_removed)    (GnmApp *gnm_app, Workbook *wb);
83 	void (*window_list_changed) (GnmApp *gnm_app);
84 	void (*custom_ui_added)	    (GnmApp *gnm_app, GnmAppExtraUI *ui);
85 	void (*custom_ui_removed)   (GnmApp *gnm_app, GnmAppExtraUI *ui);
86 	void (*clipboard_modified)  (GnmApp *gnm_app);
87 	void (*recalc_finished)     (GnmApp *gnm_app);
88 	void (*recalc_clear_caches) (GnmApp *gnm_app);
89 } GnmAppClass;
90 
91 static GObjectClass *parent_klass;
92 static GnmApp *app;
93 
94 static Workbook *gnm_app_workbook_get_by_uri (char const *uri);
95 
96 /**
97  * gnm_app_get_app:
98  *
99  * Returns: (transfer none): the #GnmApp instance.
100  **/
101 GObject *
gnm_app_get_app(void)102 gnm_app_get_app (void)
103 {
104 	return G_OBJECT (app);
105 }
106 
107 /**
108  * gnm_app_workbook_list_add:
109  * @wb: A #Workbook
110  *
111  * Add @wb to the application's list of workbooks.
112  **/
113 void
gnm_app_workbook_list_add(Workbook * wb)114 gnm_app_workbook_list_add (Workbook *wb)
115 {
116 	g_return_if_fail (GNM_IS_WORKBOOK (wb));
117 	g_return_if_fail (app != NULL);
118 
119 	app->workbook_list = g_list_prepend (app->workbook_list, wb);
120 	g_signal_connect (G_OBJECT (wb),
121 		"notify::uri",
122 		G_CALLBACK (gnm_app_flag_windows_changed_), NULL);
123 	gnm_app_flag_windows_changed_ ();
124 	g_signal_emit (G_OBJECT (app), signals[WORKBOOK_ADDED], 0, wb);
125 }
126 
127 /**
128  * gnm_app_workbook_list_remove:
129  * @wb: A #Workbook
130  *
131  * Remove @wb from the application's list of workbooks.
132  **/
133 void
gnm_app_workbook_list_remove(Workbook * wb)134 gnm_app_workbook_list_remove (Workbook *wb)
135 {
136 	g_return_if_fail (wb != NULL);
137 	g_return_if_fail (app != NULL);
138 
139 	app->workbook_list = g_list_remove (app->workbook_list, wb);
140 	g_signal_handlers_disconnect_by_func (G_OBJECT (wb),
141 		G_CALLBACK (gnm_app_flag_windows_changed_), NULL);
142 	gnm_app_flag_windows_changed_ ();
143 	g_signal_emit (G_OBJECT (app), signals[WORKBOOK_REMOVED], 0, wb);
144 }
145 
146 /**
147  * gnm_app_workbook_list:
148  *
149  * Returns: (element-type Workbook) (transfer none): the workbook list.
150  **/
151 GList *
gnm_app_workbook_list(void)152 gnm_app_workbook_list (void)
153 {
154 	g_return_val_if_fail (app != NULL, NULL);
155 
156 	return app->workbook_list;
157 }
158 
159 void
gnm_app_sanity_check(void)160 gnm_app_sanity_check (void)
161 {
162 	GList *l;
163 	gboolean err = FALSE;
164 	for (l = gnm_app_workbook_list (); l; l = l->next) {
165 		Workbook *wb = l->data;
166 		if (gnm_named_expr_collection_sanity_check (wb->names, "workbook"))
167 			err = TRUE;
168 	}
169 	if (err)
170 		g_error ("Sanity check failed\n");
171 }
172 
173 
174 
175 /**
176  * gnm_app_clipboard_clear:
177  *
178  * Clear and free the contents of the clipboard if it is
179  * not empty.
180  */
181 void
gnm_app_clipboard_clear(gboolean drop_selection)182 gnm_app_clipboard_clear (gboolean drop_selection)
183 {
184 	g_return_if_fail (app != NULL);
185 
186 	if (app->clipboard_copied_contents) {
187 		cellregion_unref (app->clipboard_copied_contents);
188 		app->clipboard_copied_contents = NULL;
189 	}
190 	if (app->clipboard_sheet_view != NULL) {
191 		gnm_sheet_view_unant (app->clipboard_sheet_view);
192 
193 		g_signal_emit (G_OBJECT (app), signals[CLIPBOARD_MODIFIED], 0);
194 
195 		gnm_sheet_view_weak_unref (&(app->clipboard_sheet_view));
196 
197 		/* Release the selection */
198 		if (drop_selection)
199 			gnm_x_disown_clipboard ();
200 	}
201 }
202 
203 void
gnm_app_clipboard_invalidate_sheet(Sheet * sheet)204 gnm_app_clipboard_invalidate_sheet (Sheet *sheet)
205 {
206 	/* Clear the cliboard to avoid dangling references to the deleted sheet */
207 	if (sheet == gnm_app_clipboard_sheet_get ())
208 		gnm_app_clipboard_clear (TRUE);
209 	else if (app->clipboard_copied_contents)
210 		cellregion_invalidate_sheet (app->clipboard_copied_contents, sheet);
211 }
212 
213 void
gnm_app_clipboard_unant(void)214 gnm_app_clipboard_unant (void)
215 {
216 	g_return_if_fail (app != NULL);
217 
218 	if (app->clipboard_sheet_view != NULL)
219 		gnm_sheet_view_unant (app->clipboard_sheet_view);
220 }
221 
222 /**
223  * gnm_app_clipboard_cut_copy:
224  * @wbc: the workbook control that requested the operation.
225  * @is_cut: is this a cut or a copy.
226  * @sv: The source sheet for the copy.
227  * @area: A single rectangular range to be copied.
228  * @animate_range: Do we want to add an animated cursor around things.
229  *
230  * When Cutting we
231  *   Clear and free the contents of the clipboard and save the sheet and area
232  *   to be cut.  DO NOT ACTUALLY CUT!  Paste will move the region if this was a
233  *   cut operation.
234  *
235  * When Copying we
236  *   Clear and free the contents of the clipboard and COPY the designated region
237  *   into the clipboard.
238  *
239  * we need to pass @wbc as a control rather than a simple command-context so
240  * that the control can claim the selection.
241  **/
242 void
gnm_app_clipboard_cut_copy(WorkbookControl * wbc,gboolean is_cut,SheetView * sv,GnmRange const * area,gboolean animate_cursor)243 gnm_app_clipboard_cut_copy (WorkbookControl *wbc, gboolean is_cut,
244 			    SheetView *sv, GnmRange const *area,
245 			    gboolean animate_cursor)
246 {
247 	Sheet *sheet;
248 
249 	g_return_if_fail (GNM_IS_SHEET_VIEW (sv));
250 	g_return_if_fail (area != NULL);
251 	g_return_if_fail (app != NULL);
252 
253 	gnm_app_clipboard_clear (FALSE);
254 	sheet = sv_sheet (sv);
255 	g_free (app->clipboard_cut_range);
256 	app->clipboard_cut_range = gnm_range_dup (area);
257 	gnm_sheet_view_weak_ref (sv, &(app->clipboard_sheet_view));
258 
259 	if (!is_cut)
260 		app->clipboard_copied_contents =
261 			clipboard_copy_range (sheet, area);
262 	if (animate_cursor) {
263 		GList *l = g_list_append (NULL, (gpointer)area);
264 		gnm_sheet_view_ant (sv, l);
265 		g_list_free (l);
266 	}
267 
268 	if (wbc == NULL) {
269 		// Testing
270 	} else if (wb_control_claim_selection (wbc)) {
271 		g_signal_emit (G_OBJECT (app), signals[CLIPBOARD_MODIFIED], 0);
272 	} else {
273 		gnm_app_clipboard_clear (FALSE);
274 		g_warning ("Unable to set selection?");
275 	}
276 }
277 
278 /**
279  * gnm_app_clipboard_cut_copy_obj:
280  * @wbc: #WorkbookControl
281  * @is_cut: %TRUE for cut, %FALSE for copy
282  * @sv: #SheetView
283  * @objects: (element-type SheetObject) (transfer container): a list
284  * of #SheetObject
285  *
286  * Different than copying/cutting a region, this can actually cut an object
287  **/
288 void
gnm_app_clipboard_cut_copy_obj(WorkbookControl * wbc,gboolean is_cut,SheetView * sv,GSList * objects)289 gnm_app_clipboard_cut_copy_obj (WorkbookControl *wbc, gboolean is_cut,
290 				SheetView *sv, GSList *objects)
291 {
292 	g_return_if_fail (GNM_IS_SHEET_VIEW (sv));
293 	g_return_if_fail (objects != NULL);
294 	g_return_if_fail (app != NULL);
295 
296 	gnm_app_clipboard_clear (FALSE);
297 	g_free (app->clipboard_cut_range);
298 	app->clipboard_cut_range = NULL;
299 	gnm_sheet_view_weak_ref (sv, &(app->clipboard_sheet_view));
300 	app->clipboard_copied_contents
301 		= clipboard_copy_obj (sv_sheet (sv), objects);
302 	if (is_cut) {
303 		cmd_objects_delete (wbc, objects, _("Cut Object"));
304 		objects = NULL;
305 	}
306 	if (wb_control_claim_selection (wbc)) {
307 		g_signal_emit (G_OBJECT (app), signals[CLIPBOARD_MODIFIED], 0);
308 	} else {
309 		gnm_app_clipboard_clear (FALSE);
310 		g_warning ("Unable to set selection?");
311 	}
312 	g_slist_free (objects);
313 }
314 
315 gboolean
gnm_app_clipboard_is_empty(void)316 gnm_app_clipboard_is_empty (void)
317 {
318 	g_return_val_if_fail (app != NULL, TRUE);
319 
320 	return app->clipboard_sheet_view == NULL;
321 }
322 
323 gboolean
gnm_app_clipboard_is_cut(void)324 gnm_app_clipboard_is_cut (void)
325 {
326 	g_return_val_if_fail (app != NULL, FALSE);
327 
328 	if (app->clipboard_sheet_view != NULL)
329 		return app->clipboard_copied_contents ? FALSE : TRUE;
330 	return FALSE;
331 }
332 
333 /**
334  * gnm_app_clipboard_sheet_get:
335  *
336  * Returns: (transfer none) (nullable): the current clipboard #Sheet.
337  **/
338 Sheet *
gnm_app_clipboard_sheet_get(void)339 gnm_app_clipboard_sheet_get (void)
340 {
341 	g_return_val_if_fail (app != NULL, NULL);
342 
343 	if (app->clipboard_sheet_view == NULL)
344 		return NULL;
345 	return sv_sheet (app->clipboard_sheet_view);
346 }
347 
348 /**
349  * gnm_app_clipboard_sheet_view_get:
350  *
351  * Returns: (transfer none) (nullable): the current clipboard #SheetView.
352  **/
353 SheetView *
gnm_app_clipboard_sheet_view_get(void)354 gnm_app_clipboard_sheet_view_get (void)
355 {
356 	g_return_val_if_fail (app != NULL, NULL);
357 	return app->clipboard_sheet_view;
358 }
359 
360 /**
361  * gnm_app_clipboard_contents_get:
362  *
363  * Returns: (nullable) (transfer none): the current contents of the clipboard.
364  */
365 GnmCellRegion *
gnm_app_clipboard_contents_get(void)366 gnm_app_clipboard_contents_get (void)
367 {
368 	g_return_val_if_fail (app != NULL, NULL);
369 	return app->clipboard_copied_contents;
370 }
371 
372 /**
373  * gnm_app_clipboard_area_get:
374  *
375  * Returns: (nullable) (transfer none): the current range in the clipboard.
376  */
377 GnmRange const *
gnm_app_clipboard_area_get(void)378 gnm_app_clipboard_area_get (void)
379 {
380 	g_return_val_if_fail (app != NULL, NULL);
381 	/*
382 	 * Only return the range if the sheet has been set.
383 	 * The range will still contain data even after
384 	 * the clipboard has been cleared so we need to be extra
385 	 * safe and only return a range if there is a valid selection
386 	 */
387 	if (app->clipboard_sheet_view != NULL)
388 		return app->clipboard_cut_range;
389 	return NULL;
390 }
391 
392 /**
393  * gnm_app_workbook_get_by_name:
394  * @name: the workbook name.
395  * @ref_uri:
396  *
397  * Returns: (transfer none): the #Workbook or %NULL.
398  **/
399 Workbook *
gnm_app_workbook_get_by_name(char const * name,char const * ref_uri)400 gnm_app_workbook_get_by_name (char const *name,
401 			      char const *ref_uri)
402 {
403 	Workbook *wb;
404 	char *filename = NULL;
405 
406 	if (name == NULL || *name == 0)
407 		return NULL;
408 
409 	/* Try as URI.  */
410 	wb = gnm_app_workbook_get_by_uri (name);
411 	if (wb)
412 		goto out;
413 
414 	filename = g_filename_from_utf8 (name, -1, NULL, NULL, NULL);
415 
416 	/* Try as absolute filename.  */
417 	if (filename && g_path_is_absolute (filename)) {
418 		char *uri = go_filename_to_uri (filename);
419 		if (uri) {
420 			wb = gnm_app_workbook_get_by_uri (uri);
421 			g_free (uri);
422 		}
423 		if (wb)
424 			goto out;
425 	}
426 
427 	if (filename && ref_uri) {
428 		char *rel_uri = go_url_encode (filename, 1);
429 		char *uri = go_url_resolve_relative (ref_uri, rel_uri);
430 		g_free (rel_uri);
431 		if (uri) {
432 			wb = gnm_app_workbook_get_by_uri (uri);
433 			g_free (uri);
434 		}
435 		if (wb)
436 			goto out;
437 	}
438 
439  out:
440 	g_free (filename);
441 	return wb;
442 }
443 
444 struct wb_uri_closure {
445 	Workbook *wb;
446 	char const *uri;
447 };
448 
449 static gboolean
cb_workbook_uri(Workbook * wb,gpointer closure)450 cb_workbook_uri (Workbook * wb, gpointer closure)
451 {
452 	struct wb_uri_closure *dat = closure;
453 	char const *wb_uri = go_doc_get_uri (GO_DOC (wb));
454 
455 	if (wb_uri && strcmp (wb_uri, dat->uri) == 0) {
456 		dat->wb = wb;
457 		return FALSE;
458 	}
459 	return TRUE;
460 }
461 
462 typedef gboolean (*GnmWbIterFunc) (Workbook *wb, gpointer data);
463 static gboolean
464 gnm_app_workbook_foreach (GnmWbIterFunc cback, gpointer data);
465 
466 static Workbook *
gnm_app_workbook_get_by_uri(char const * uri)467 gnm_app_workbook_get_by_uri (char const *uri)
468 {
469 	struct wb_uri_closure closure;
470 
471 	g_return_val_if_fail (uri != NULL, NULL);
472 
473 	closure.wb = NULL;
474 	closure.uri = uri;
475 	gnm_app_workbook_foreach (&cb_workbook_uri, &closure);
476 
477 	return closure.wb;
478 }
479 
480 static gboolean
gnm_app_workbook_foreach(GnmWbIterFunc cback,gpointer data)481 gnm_app_workbook_foreach (GnmWbIterFunc cback, gpointer data)
482 {
483 	GList *l;
484 
485 	g_return_val_if_fail (app != NULL, FALSE);
486 
487 	for (l = app->workbook_list; l; l = l->next){
488 		Workbook *wb = l->data;
489 
490 		if (!(*cback)(wb, data))
491 			return FALSE;
492 	}
493 	return TRUE;
494 }
495 
496 /**
497  * gnm_app_workbook_get_by_index:
498  * @i: index
499  *
500  * Get nth workbook.  Index is zero-based.
501  * Return value: (nullable) (transfer none): the nth workbook, if any.
502  */
503 Workbook *
gnm_app_workbook_get_by_index(int i)504 gnm_app_workbook_get_by_index (int i)
505 {
506 	return g_list_nth_data (app->workbook_list, i);
507 }
508 
509 double
gnm_app_display_dpi_get(gboolean horizontal)510 gnm_app_display_dpi_get (gboolean horizontal)
511 {
512 	return horizontal
513 		? gnm_conf_get_core_gui_screen_horizontaldpi ()
514 		: gnm_conf_get_core_gui_screen_verticaldpi ();
515 }
516 
517 double
gnm_app_dpi_to_pixels(void)518 gnm_app_dpi_to_pixels (void)
519 {
520 	return MIN (gnm_app_display_dpi_get (TRUE),
521 		    gnm_app_display_dpi_get (FALSE)) / 72.;
522 }
523 
524 /* GtkFileFilter */
525 /**
526  * gnm_app_create_opener_filter:
527  * @openers: (element-type GOFileOpener): a list of file openers.
528  *
529  * Creates a #GtkFileFilter from the list of file types supported by the
530  * openers in the list.
531  * Returns: (transfer full): the newly allocated #GtkFileFilter.
532  **/
533 void *
gnm_app_create_opener_filter(GList * openers)534 gnm_app_create_opener_filter (GList *openers)
535 {
536 	/* See below.  */
537 	static const char *const bad_suffixes[] = {
538 		"txt",
539 		"html", "htm",
540 		"xml",
541 		NULL
542 	};
543 
544 	GtkFileFilter *filter = gtk_file_filter_new ();
545 	gboolean for_history = (openers == NULL);
546 
547 	if (openers == NULL)
548 		openers = go_get_file_openers ();
549 
550 	for (; openers; openers = openers->next) {
551 		GOFileOpener *opener = openers->data;
552 		if (opener != NULL) {
553 			const GSList *mimes = go_file_opener_get_mimes (opener);
554 			const GSList *suffixes = go_file_opener_get_suffixes (opener);
555 
556 			if (!for_history)
557 				while (mimes) {
558 					const char *mime = mimes->data;
559 					/*
560 					 * See 438918.  Too many things
561 					 * like *.xml and *.txt get added
562 					 * to be useful for the file history
563 					 */
564 					gtk_file_filter_add_mime_type (filter, mime);
565 					if (0)
566 						g_print ("%s: Adding mime %s\n", go_file_opener_get_description (opener), mime);
567 					mimes = mimes->next;
568 				}
569 
570 			while (suffixes) {
571 				const char *suffix = suffixes->data;
572 				GString *pattern;
573 				int i;
574 
575 				if (for_history)
576 					for (i = 0; bad_suffixes[i]; i++)
577 						if (strcmp (suffix, bad_suffixes[i]) == 0)
578 							goto bad_suffix;
579 
580 				/* Create "*.[xX][lL][sS]" */
581 				pattern = g_string_new ("*.");
582 				while (*suffix) {
583 					gunichar uc = g_utf8_get_char (suffix);
584 					suffix = g_utf8_next_char (suffix);
585 					if (g_unichar_islower (uc)) {
586 						g_string_append_c (pattern, '[');
587 						g_string_append_unichar (pattern, uc);
588 						uc = g_unichar_toupper (uc);
589 						g_string_append_unichar (pattern, uc);
590 						g_string_append_c (pattern, ']');
591 					} else
592 						g_string_append_unichar (pattern, uc);
593 				}
594 
595 				gtk_file_filter_add_pattern (filter, pattern->str);
596 				if (0)
597 					g_print ("%s: Adding %s\n", go_file_opener_get_description (opener), pattern->str);
598 				g_string_free (pattern, TRUE);
599 
600 			bad_suffix:
601 				suffixes = suffixes->next;
602 			}
603 		}
604 	}
605 	return filter;
606 }
607 
608 static gint
compare_mru(GtkRecentInfo * a,GtkRecentInfo * b)609 compare_mru (GtkRecentInfo *a, GtkRecentInfo *b)
610 {
611 	time_t ta = MAX (gtk_recent_info_get_visited (a), gtk_recent_info_get_modified (a));
612 	time_t tb = MAX (gtk_recent_info_get_visited (b), gtk_recent_info_get_modified (b));
613 
614 	return ta < tb;
615 }
616 
617 /**
618  * gnm_app_history_get_list:
619  *
620  * creating it if necessary.
621  *
622  * Return value: (element-type utf8) (transfer full): the list, which must be
623  * freed along with the strings in it.
624  **/
625 GSList *
gnm_app_history_get_list(int max_elements)626 gnm_app_history_get_list (int max_elements)
627 {
628 	GSList *res = NULL;
629 	GList *items, *l;
630 	GtkFileFilter *filter;
631 	int n_elements = 0;
632 
633 	if (app->recent == NULL)
634 		return NULL;
635 
636 	items = gtk_recent_manager_get_items (app->recent);
637 	items = g_list_sort (items, (GCompareFunc)compare_mru);
638 
639 	filter = gnm_app_create_opener_filter (NULL);
640 
641 	for (l = items; l && n_elements < max_elements; l = l->next) {
642 		GtkRecentInfo *ri = l->data;
643 		const char *uri = gtk_recent_info_get_uri (ri);
644 		gboolean want_it;
645 
646 		if (gtk_recent_info_has_application (ri, g_get_application_name ())) {
647 			want_it = TRUE;
648 		} else {
649 			GtkFileFilterInfo fi;
650 			char *display_name = g_filename_display_basename (uri);
651 
652 			memset (&fi, 0, sizeof (fi));
653 			fi.contains = (GTK_FILE_FILTER_MIME_TYPE |
654 				       GTK_FILE_FILTER_URI |
655 				       GTK_FILE_FILTER_DISPLAY_NAME);
656 			fi.uri = uri;
657 			fi.mime_type = gtk_recent_info_get_mime_type (ri);
658 			fi.display_name = display_name;
659 			want_it = gtk_file_filter_filter (filter, &fi);
660 			g_free (display_name);
661 		}
662 
663 		if (want_it) {
664 			char *filename = go_filename_from_uri (uri);
665 			if (filename && !g_file_test (filename, G_FILE_TEST_EXISTS))
666 				want_it = FALSE;
667 			g_free (filename);
668 		}
669 
670 		if (want_it) {
671 			res = g_slist_prepend (res, g_strdup (uri));
672 			n_elements++;
673 		}
674 	}
675 
676 	g_list_free_full (items, (GDestroyNotify)gtk_recent_info_unref);
677 	g_object_ref_sink (filter);
678 	g_object_unref (filter);
679 
680 	return g_slist_reverse (res);
681 }
682 
683 /**
684  * application_history_update_list:
685  * @uri:
686  * @mimetype: (nullable): the mime type for @uri
687  *
688  * Adds @uri to the application's history of files.
689  **/
690 void
gnm_app_history_add(char const * uri,const char * mimetype)691 gnm_app_history_add (char const *uri, const char *mimetype)
692 {
693 	GtkRecentData rd;
694 
695 	if (app->recent == NULL)
696 		return;
697 
698 	memset (&rd, 0, sizeof (rd));
699 
700 #if 0
701 	g_print ("uri: %s\nmime: %s\n\n", uri, mimetype ? mimetype : "-");
702 #endif
703 
704         rd.mime_type =
705 		g_strdup (mimetype ? mimetype : "application/octet-stream");
706 
707 	rd.app_name = g_strdup (g_get_application_name ());
708 	rd.app_exec = g_strjoin (" ", g_get_prgname (), "%u", NULL);
709 	rd.groups = NULL;
710 	rd.is_private = FALSE;
711 
712 	if (!gtk_recent_manager_add_full (app->recent, uri, &rd)) {
713 		/* Now what?  */
714 		g_printerr ("Warning: failed to update recent document.\n");
715 	}
716 
717 	g_free (rd.mime_type);
718 	g_free (rd.app_name);
719 	g_free (rd.app_exec);
720 
721 	g_object_notify (G_OBJECT (app), "file-history-list");
722 }
723 
724 static void
cb_recent_changed(G_GNUC_UNUSED GtkRecentManager * recent,GnmApp * app)725 cb_recent_changed (G_GNUC_UNUSED GtkRecentManager *recent, GnmApp *app)
726 {
727 	g_object_notify (G_OBJECT (app), "file-history-list");
728 }
729 
730 static void
gnm_app_finalize(GObject * obj)731 gnm_app_finalize (GObject *obj)
732 {
733 	GnmApp *application = GNM_APP (obj);
734 
735 	g_free (application->clipboard_cut_range);
736 	application->clipboard_cut_range = NULL;
737 
738 	application->recent = NULL;
739 
740 	if (app == application)
741 		app = NULL;
742 
743 	G_OBJECT_CLASS (parent_klass)->finalize (obj);
744 }
745 
746 static void
gnm_app_get_property(GObject * obj,guint param_id,GValue * value,GParamSpec * pspec)747 gnm_app_get_property (GObject *obj, guint param_id,
748 				   GValue *value, GParamSpec *pspec)
749 {
750 #if 0
751 	GnmApp *application = GNM_APP (obj);
752 #endif
753 
754 	switch (param_id) {
755 	case PROP_HISTORY_LIST:
756 		g_value_set_pointer (value, gnm_app_history_get_list (G_MAXINT));
757 		break;
758 	case PROP_SHUTTING_DOWN:
759 		g_value_set_boolean (value, gnm_app_shutting_down ());
760 		break;
761 	case PROP_INITIAL_OPEN_COMPLETE:
762 		g_value_set_boolean (value, gnm_app_initial_open_complete ());
763 		break;
764 	default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, param_id, pspec);
765 		 break;
766 	}
767 }
768 
769 static void
gnm_app_set_property(GObject * object,guint property_id,GValue const * value,GParamSpec * pspec)770 gnm_app_set_property (GObject *object, guint property_id,
771 		      GValue const *value, GParamSpec *pspec)
772 {
773 	GnmApp *app = (GnmApp *)object;
774 
775 	switch (property_id) {
776 	case PROP_SHUTTING_DOWN:
777 		app->shutting_down = g_value_get_boolean (value);
778 		break;
779 	case PROP_INITIAL_OPEN_COMPLETE:
780 		app->initial_open_complete = g_value_get_boolean (value);
781 		break;
782 	default:
783 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
784 		break;
785 	}
786 }
787 
788 static void
gnm_app_class_init(GObjectClass * gobject_klass)789 gnm_app_class_init (GObjectClass *gobject_klass)
790 {
791 	parent_klass = g_type_class_peek_parent (gobject_klass);
792 
793 	/* Object class method overrides */
794 	gobject_klass->finalize = gnm_app_finalize;
795 	gobject_klass->get_property = gnm_app_get_property;
796 	gobject_klass->set_property = gnm_app_set_property;
797 
798 	g_object_class_install_property (gobject_klass, PROP_HISTORY_LIST,
799 		g_param_spec_pointer ("file-history-list",
800 				      P_("File History List"),
801 				      P_("A list of filenames that have been read recently"),
802 				      GSF_PARAM_STATIC | G_PARAM_READABLE));
803 	g_object_class_install_property (gobject_klass, PROP_SHUTTING_DOWN,
804 		g_param_spec_boolean ("shutting-down",
805 				      P_("Shutting Down"),
806 				      P_("In the process of shutting down?"),
807 				      FALSE,
808 				      GSF_PARAM_STATIC | G_PARAM_READWRITE));
809 	g_object_class_install_property (gobject_klass, PROP_INITIAL_OPEN_COMPLETE,
810 		g_param_spec_boolean ("initial-open-complete",
811 				      P_("Initial Open Complete"),
812 				      P_("All command-line files open?"),
813 				      FALSE,
814 				      GSF_PARAM_STATIC | G_PARAM_READWRITE));
815 
816 
817 	signals[WORKBOOK_ADDED] = g_signal_new ("workbook_added",
818 		GNM_APP_TYPE,
819 		G_SIGNAL_RUN_LAST,
820 		G_STRUCT_OFFSET (GnmAppClass, workbook_added),
821 		NULL, NULL,
822 		g_cclosure_marshal_VOID__OBJECT,
823 		G_TYPE_NONE, 1, GNM_WORKBOOK_TYPE);
824 	signals[WORKBOOK_REMOVED] = g_signal_new ("workbook_removed",
825 		GNM_APP_TYPE,
826 		G_SIGNAL_RUN_LAST,
827 		G_STRUCT_OFFSET (GnmAppClass, workbook_removed),
828 		NULL, NULL,
829 		g_cclosure_marshal_VOID__POINTER,
830 		G_TYPE_NONE, 1, G_TYPE_POINTER);
831 	signals[WINDOW_LIST_CHANGED] = g_signal_new ("window-list-changed",
832 		GNM_APP_TYPE,
833 		G_SIGNAL_RUN_LAST,
834 		G_STRUCT_OFFSET (GnmAppClass, window_list_changed),
835 		NULL, NULL,
836 		g_cclosure_marshal_VOID__VOID,
837 		G_TYPE_NONE, 0);
838 	signals[CUSTOM_UI_ADDED] = g_signal_new ("custom-ui-added",
839 		GNM_APP_TYPE,
840 		G_SIGNAL_RUN_LAST,
841 		G_STRUCT_OFFSET (GnmAppClass, custom_ui_added),
842 		NULL, NULL,
843 		g_cclosure_marshal_VOID__POINTER,
844 		G_TYPE_NONE, 1, G_TYPE_POINTER);
845 	signals[CUSTOM_UI_REMOVED] = g_signal_new ("custom-ui-removed",
846 		GNM_APP_TYPE,
847 		G_SIGNAL_RUN_LAST,
848 		G_STRUCT_OFFSET (GnmAppClass, custom_ui_removed),
849 		NULL, NULL,
850 		g_cclosure_marshal_VOID__POINTER,
851 		G_TYPE_NONE, 1, G_TYPE_POINTER);
852 	signals[CLIPBOARD_MODIFIED] = g_signal_new ("clipboard_modified",
853 		GNM_APP_TYPE,
854 		G_SIGNAL_RUN_LAST,
855 		G_STRUCT_OFFSET (GnmAppClass, clipboard_modified),
856 		NULL, NULL,
857 		g_cclosure_marshal_VOID__VOID,
858 		G_TYPE_NONE, 0);
859 	signals[RECALC_FINISHED] = g_signal_new ("recalc_finished",
860 		GNM_APP_TYPE,
861 		G_SIGNAL_RUN_LAST,
862 		G_STRUCT_OFFSET (GnmAppClass, recalc_finished),
863 		NULL, NULL,
864 		g_cclosure_marshal_VOID__VOID,
865 		G_TYPE_NONE, 0);
866 	signals[RECALC_FINISHED] = g_signal_new ("recalc_clear_caches",
867 		GNM_APP_TYPE,
868 		G_SIGNAL_RUN_LAST,
869 		G_STRUCT_OFFSET (GnmAppClass, recalc_clear_caches),
870 		NULL, NULL,
871 		g_cclosure_marshal_VOID__VOID,
872 		G_TYPE_NONE, 0);
873 }
874 
875 static void
gnm_app_init(GObject * obj)876 gnm_app_init (GObject *obj)
877 {
878 	GnmApp *gnm_app = GNM_APP (obj);
879 
880 	gnm_app->clipboard_copied_contents = NULL;
881 	gnm_app->clipboard_sheet_view = NULL;
882 
883 	gnm_app->workbook_list = NULL;
884 
885 	if (gdk_display_get_default ()) {
886 		/*
887 		 * Only allocate a GtkRecentManager if we have a gui.
888 		 * This is, in part, because it currently throws an error.
889 		 * deep inside gtk+.
890 		 */
891 		gnm_app->recent = gtk_recent_manager_get_default ();
892 		g_signal_connect_object (G_OBJECT (gnm_app->recent),
893 					 "changed",
894 					 G_CALLBACK (cb_recent_changed),
895 					 gnm_app, 0);
896 	}
897 
898 	app = gnm_app;
899 }
900 
901 GSF_CLASS (GnmApp, gnm_app,
902 	   gnm_app_class_init, gnm_app_init,
903 	   G_TYPE_OBJECT)
904 
905 /**********************************************************************/
906 static GSList *extra_uis = NULL;
907 
908 /**
909  * gnm_action_new:
910  * @name: action ID.
911  * @label: label.
912  * @icon: icon name.
913  * @always_available: whether the action should always be available.
914  * @handler: (scope notified): the handler.
915  * @data: user data for @handler
916  * @notify: destroy notification for @data
917  *
918  * Returns: (transfer full): the newly allocated #GnmAction.
919  **/
920 GnmAction *
gnm_action_new(char const * id,char const * label,char const * icon_name,gboolean always_available,GnmActionHandler handler,gpointer data,GDestroyNotify notify)921 gnm_action_new (char const *id, char const *label,
922 		char const *icon_name, gboolean always_available,
923 		GnmActionHandler handler,
924 		gpointer data, GDestroyNotify notify)
925 {
926 	GnmAction *res = g_new0 (GnmAction, 1);
927 	res->ref_count  = 1;
928 	res->id		= g_strdup (id);
929 	res->label	= g_strdup (label);
930 	res->icon_name	= g_strdup (icon_name);
931 	res->always_available = always_available;
932 	res->handler	= handler;
933 	res->data       = data;
934 	res->notify     = notify;
935 	return res;
936 }
937 
938 /**
939  * gnm_action_unref:
940  * @action: (transfer full) (nullable): #GnmAction
941  */
942 void
gnm_action_unref(GnmAction * action)943 gnm_action_unref (GnmAction *action)
944 {
945 	if (!action || action->ref_count-- > 1)
946 		return;
947 
948 	if (action->notify)
949 		action->notify (action->data);
950 
951 	g_free (action->id);
952 	g_free (action->label);
953 	g_free (action->icon_name);
954 	g_free (action);
955 }
956 
957 /**
958  * gnm_action_ref:
959  * @action: (transfer none) (nullable): #GnmAction
960  *
961  * Returns: (transfer full) (nullable): a new reference to @action.
962  */
963 GnmAction *
gnm_action_ref(GnmAction * action)964 gnm_action_ref (GnmAction *action)
965 {
966 	if (action)
967 		action->ref_count++;
968 	return action;
969 }
970 
971 GType
gnm_action_get_type(void)972 gnm_action_get_type (void)
973 {
974 	static GType t = 0;
975 
976 	if (t == 0) {
977 		t = g_boxed_type_register_static ("GnmAction",
978 			 (GBoxedCopyFunc)gnm_action_ref,
979 			 (GBoxedFreeFunc)gnm_action_unref);
980 	}
981 	return t;
982 }
983 
984 /***************
985  * making GnmAppExtraUI a boxed type for introspection. copy and free don't do
986  anything which might be critical, crossing fingers.*/
987 
988 static GnmAppExtraUI *
gnm_app_extra_ui_ref(GnmAppExtraUI * ui)989 gnm_app_extra_ui_ref (GnmAppExtraUI *ui)
990 {
991 	// Nothing
992 	return ui;
993 }
994 
995 static GnmAppExtraUI *
gnm_app_extra_ui_unref(GnmAppExtraUI * ui)996 gnm_app_extra_ui_unref (GnmAppExtraUI *ui)
997 {
998 	// Nothing
999 	return ui;
1000 }
1001 
1002 GType
gnm_app_extra_ui_get_type(void)1003 gnm_app_extra_ui_get_type (void)
1004 {
1005 	static GType t = 0;
1006 
1007 	if (t == 0) {
1008 		t = g_boxed_type_register_static ("GnmAppExtraUI",
1009 			 (GBoxedCopyFunc)gnm_app_extra_ui_ref,
1010 			 (GBoxedFreeFunc)gnm_app_extra_ui_unref);
1011 	}
1012 	return t;
1013 }
1014 
1015 /**
1016  * gnm_app_add_extra_ui:
1017  * @group_name: action group name.
1018  * @actions: (element-type GnmAction): list of actions.
1019  * @layout: the xml string describing the menus and toolbars.
1020  * @domain: localization domain.
1021  *
1022  * Returns: (transfer full): the newly allocated #GnmAppExtraUI.
1023  **/
1024 GnmAppExtraUI *
gnm_app_add_extra_ui(char const * group_name,GSList * actions,const char * layout,char const * domain)1025 gnm_app_add_extra_ui (char const *group_name,
1026 		      GSList *actions,
1027 		      const char *layout,
1028 		      char const *domain)
1029 {
1030 	GnmAppExtraUI *extra_ui = g_new0 (GnmAppExtraUI, 1);
1031 	extra_uis = g_slist_prepend (extra_uis, extra_ui);
1032 	extra_ui->group_name = g_strdup (group_name);
1033 	extra_ui->actions = actions;
1034 	extra_ui->layout = g_strdup (layout);
1035 	g_signal_emit (G_OBJECT (app), signals[CUSTOM_UI_ADDED], 0, extra_ui);
1036 	if (gnm_debug_flag ("extra-ui"))
1037 		g_printerr ("Adding extra ui [%s] %p\n", group_name, extra_ui);
1038 	return extra_ui;
1039 }
1040 
1041 void
gnm_app_remove_extra_ui(GnmAppExtraUI * extra_ui)1042 gnm_app_remove_extra_ui (GnmAppExtraUI *extra_ui)
1043 {
1044 	if (gnm_debug_flag ("extra-ui"))
1045 		g_printerr ("Removing extra ui %p\n", extra_ui);
1046 	extra_uis = g_slist_remove (extra_uis, extra_ui);
1047 	g_signal_emit (G_OBJECT (app), signals[CUSTOM_UI_REMOVED], 0, extra_ui);
1048 	g_free (extra_ui->group_name);
1049 	g_free (extra_ui->layout);
1050 	g_free (extra_ui);
1051 }
1052 
1053 /**
1054  * gnm_app_foreach_extra_ui:
1055  * @func: (scope call): #GFunc
1056  * @data: user data.
1057  *
1058  * Applies @func to each #GnmAppExtraUI.
1059  **/
1060 void
gnm_app_foreach_extra_ui(GFunc func,gpointer data)1061 gnm_app_foreach_extra_ui (GFunc func, gpointer data)
1062 {
1063 	g_slist_foreach (extra_uis, func, data);
1064 }
1065 
1066 /**********************************************************************/
1067 
1068 static gint windows_update_timer = 0;
1069 static gboolean
cb_flag_windows_changed(void)1070 cb_flag_windows_changed (void)
1071 {
1072 	if (app)
1073 		g_signal_emit (G_OBJECT (app), signals[WINDOW_LIST_CHANGED], 0);
1074 	windows_update_timer = 0;
1075 	return FALSE;
1076 }
1077 
1078 /**
1079  * gnm_app_flag_windows_changed_:
1080  *
1081  * An internal utility routine to flag a regeneration of the window lists
1082  **/
1083 void
gnm_app_flag_windows_changed_(void)1084 gnm_app_flag_windows_changed_ (void)
1085 {
1086 	if (windows_update_timer == 0)
1087 		windows_update_timer = g_timeout_add (100,
1088 			(GSourceFunc)cb_flag_windows_changed, NULL);
1089 }
1090 
1091 /**********************************************************************/
1092 
1093 /**
1094  * gnm_app_recalc:
1095  *
1096  * Recalculate everything dirty in all workbooks that have automatic
1097  * recalc turned on.
1098  **/
1099 void
gnm_app_recalc(void)1100 gnm_app_recalc (void)
1101 {
1102 	GList *l;
1103 
1104 	g_return_if_fail (app != NULL);
1105 
1106 	gnm_app_recalc_start ();
1107 
1108 	for (l = app->workbook_list; l; l = l->next) {
1109 		Workbook *wb = l->data;
1110 
1111 		if (workbook_get_recalcmode (wb))
1112 			workbook_recalc (wb);
1113 	}
1114 
1115 	gnm_app_recalc_finish ();
1116 }
1117 
1118 void
gnm_app_recalc_start(void)1119 gnm_app_recalc_start (void)
1120 {
1121 	g_return_if_fail (app->recalc_count >= 0);
1122 	app->recalc_count++;
1123 }
1124 
1125 void
gnm_app_recalc_finish(void)1126 gnm_app_recalc_finish (void)
1127 {
1128 	g_return_if_fail (app->recalc_count > 0);
1129 	app->recalc_count--;
1130 	if (app->recalc_count == 0) {
1131 		gnm_app_recalc_clear_caches ();
1132 		g_signal_emit_by_name (gnm_app_get_app (), "recalc-finished");
1133 	}
1134 }
1135 
1136 void
gnm_app_recalc_clear_caches(void)1137 gnm_app_recalc_clear_caches (void)
1138 {
1139 	g_signal_emit_by_name (gnm_app_get_app (), "recalc-clear-caches");
1140 }
1141 
1142 gboolean
gnm_app_shutting_down(void)1143 gnm_app_shutting_down (void)
1144 {
1145 	return app->shutting_down;
1146 }
1147 
1148 gboolean
gnm_app_initial_open_complete(void)1149 gnm_app_initial_open_complete (void)
1150 {
1151 	return app->initial_open_complete;
1152 }
1153