1 /*
2  * dialog-recent.c:
3  *   Dialog for selecting from recently used files.
4  *
5  * Author:
6  *   Morten Welinder (terra@gnome.org)
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, see <https://www.gnu.org/licenses/>.
20  */
21 
22 #include <gnumeric-config.h>
23 #include <glib/gi18n-lib.h>
24 #include <gnumeric.h>
25 #include <dialogs/dialogs.h>
26 #include <wbc-gtk.h>
27 #include <gui-file.h>
28 #include <gui-util.h>
29 #include <string.h>
30 #include <goffice/goffice.h>
31 
32 #define RECENT_KEY "recent-dialog"
33 
34 enum {
35 	RECENT_COL_INFO
36 };
37 
38 /* ------------------------------------------------------------------------- */
39 
40 static void
cb_selected(GtkTreeModel * model,G_GNUC_UNUSED GtkTreePath * path,GtkTreeIter * iter,WBCGtk * wbcg)41 cb_selected (GtkTreeModel *model,
42              G_GNUC_UNUSED GtkTreePath *path,
43              GtkTreeIter *iter,
44              WBCGtk *wbcg)
45 {
46 	char *uri = NULL;
47 	GtkRecentInfo *info;
48 	gtk_tree_model_get (model, iter, RECENT_COL_INFO, &info, -1);
49 	uri = g_strdup (gtk_recent_info_get_uri (info));
50 	gtk_recent_info_unref (info);
51 	if (uri) {
52 		gui_file_read (wbcg, uri, NULL, NULL);
53 		g_free (uri);
54 	}
55 }
56 
57 static void
cb_response(GtkWidget * dialog,gint response_id,WBCGtk * wbcg)58 cb_response (GtkWidget *dialog,
59 	     gint response_id,
60 	     WBCGtk *wbcg)
61 {
62 	GtkBuilder *gui = g_object_get_data (G_OBJECT (dialog), "gui");
63 	GtkTreeView *tv = GTK_TREE_VIEW (gtk_builder_get_object (gui, "docs_treeview"));
64 	GtkTreeSelection *tsel = gtk_tree_view_get_selection (tv);
65 
66 	switch (response_id) {
67 	case GTK_RESPONSE_OK:
68 		gtk_tree_selection_selected_foreach (tsel, (GtkTreeSelectionForeachFunc) cb_selected, wbcg);
69 		gtk_widget_destroy (dialog);
70 		break;
71 
72 	default:
73 		gtk_widget_destroy (dialog);
74 	}
75 }
76 
77 static void
cb_destroy(GtkDialog * dialog)78 cb_destroy (GtkDialog *dialog)
79 {
80 	/* Trigger tear-down.  */
81 	g_object_set_data (G_OBJECT (dialog), "gui", NULL);
82 }
83 
84 
85 static gboolean
cb_key_press(GtkWidget * widget,GdkEventKey * event)86 cb_key_press (GtkWidget *widget, GdkEventKey *event)
87 {
88   GtkTreeView *tree_view = (GtkTreeView *) widget;
89 
90   switch (event->keyval) {
91   case GDK_KEY_KP_Delete:
92   case GDK_KEY_Delete: {
93 	GtkTreeSelection *tsel = gtk_tree_view_get_selection (tree_view);
94 	GtkTreeModel *model;
95 	GtkTreeIter iter;
96 
97 	if (gtk_tree_selection_get_selected (tsel, &model, &iter)) {
98 		GtkRecentInfo *info;
99 		const char *uri;
100 		GtkRecentManager *manager = gtk_recent_manager_get_default ();
101 
102 		gtk_tree_model_get (model, &iter, RECENT_COL_INFO, &info, -1);
103 		uri = gtk_recent_info_get_uri (info);
104 
105 		gtk_recent_manager_remove_item (manager, uri, NULL);
106 		gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
107 		gtk_recent_info_unref (info);
108 	}
109 	return TRUE;
110   }
111 
112   default:
113 	  break;
114   }
115 
116   return FALSE;
117 }
118 
119 static gboolean
cb_button_press(GtkWidget * w,GdkEventButton * ev,WBCGtk * wbcg)120 cb_button_press (GtkWidget *w, GdkEventButton *ev, WBCGtk *wbcg)
121 {
122 	if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
123 		GtkWidget *dlg = gtk_widget_get_toplevel (w);
124 		if (GTK_IS_DIALOG (dlg)) {
125 			cb_response (dlg, GTK_RESPONSE_OK, wbcg);
126 			return TRUE;
127 		}
128 	}
129 	return FALSE;
130 }
131 
132 static void
url_renderer_func(GtkTreeViewColumn * tree_column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer user_data)133 url_renderer_func (GtkTreeViewColumn *tree_column,
134 		   GtkCellRenderer   *cell,
135 		   GtkTreeModel      *model,
136 		   GtkTreeIter       *iter,
137 		   gpointer           user_data)
138 {
139 	GtkRecentInfo *ri = NULL;
140 	const char *uri;
141 	char *markup, *shortname, *filename, *longname;
142 
143 	gtk_tree_model_get (model, iter, RECENT_COL_INFO, &ri, -1);
144 
145 	uri = gtk_recent_info_get_uri (ri);
146 	filename = go_filename_from_uri (uri);
147 	if (filename) {
148 		shortname = g_filename_display_basename (filename);
149 	} else {
150 		shortname = g_filename_display_basename (uri);
151 	}
152 
153 	if (filename) {
154 		longname = g_strdup (filename);
155 	} else {
156 		char *duri = g_uri_unescape_string (uri, NULL);
157 		longname = duri
158 			? g_filename_display_name (duri)
159 			: g_strdup (uri);
160 		g_free (duri);
161 	}
162 
163 	markup = g_markup_printf_escaped (_("<b>%s</b>\n"
164 					    "<small>Location: %s</small>"),
165 					  shortname,
166 					  longname);
167 	g_object_set (cell, "markup", markup, NULL);
168 	g_free (markup);
169 	g_free (shortname);
170 	g_free (longname);
171 	g_free (filename);
172 	gtk_recent_info_unref (ri);
173 }
174 
175 static void
age_renderer_func(GtkTreeViewColumn * tree_column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer user_data)176 age_renderer_func (GtkTreeViewColumn *tree_column,
177 		   GtkCellRenderer   *cell,
178 		   GtkTreeModel      *model,
179 		   GtkTreeIter       *iter,
180 		   gpointer           user_data)
181 {
182 	GtkRecentInfo *ri = NULL;
183 	GDateTime *now = user_data;
184 	GDateTime *last_used;
185 	GTimeSpan age;
186 	char *text;
187 	const char *date_format;
188 	const char *p;
189 
190 	gtk_tree_model_get (model, iter, RECENT_COL_INFO, &ri, -1);
191 	last_used = g_date_time_new_from_unix_local (gtk_recent_info_get_modified (ri));
192 	gtk_recent_info_unref (ri);
193 
194 	age = g_date_time_difference (now, last_used);
195 	if (age < G_TIME_SPAN_DAY &&
196 	    g_date_time_get_day_of_month (now) == g_date_time_get_day_of_month (last_used)) {
197 		if (go_locale_24h ())
198 			/*
199 			 * xgettext: This is a time format for
200 			 * g_date_time_format used in locales that use a
201 			 * 24 hour clock.  You probably do not need to change
202 			 * this.  The default will show things like "09:50"
203 			 * and "21:50".
204 			 */
205 			date_format = _("%H:%M");
206 		else
207 			/*
208 			 * xgettext: This is a time format for
209 			 * g_date_time_format used in locales that use
210 			 * a 12 hour clock. You probably do not need
211 			 * to change this.  The default will show
212 			 * things like " 9:50 am" and " 9:50 pm".
213 			 */
214 			date_format = _("%l:%M %P");
215 	} else {
216 		date_format = "%x";
217 	}
218 
219 	p = text = g_date_time_format (last_used, date_format);
220 	while (g_ascii_isspace (*p))
221 		p++;
222 	g_object_set (cell, "text", p, "xalign", 0.5, NULL);
223 	g_free (text);
224 
225 	g_date_time_unref (last_used);
226 }
227 
228 static gint
by_age_uri(gconstpointer a_,gconstpointer b_)229 by_age_uri (gconstpointer a_, gconstpointer b_)
230 {
231 	GtkRecentInfo *a = (gpointer)a_;
232 	GtkRecentInfo *b = (gpointer)b_;
233 	int res;
234 
235 	res = gtk_recent_info_get_modified (b) - gtk_recent_info_get_modified (a);
236 	if (res) return res;
237 
238 	res = strcmp (gtk_recent_info_get_uri (a), gtk_recent_info_get_uri (b));
239 	return res;
240 }
241 
242 
243 static void
populate_recent_model(GtkBuilder * gui)244 populate_recent_model (GtkBuilder *gui)
245 {
246 	GtkListStore *list = GTK_LIST_STORE (gtk_builder_get_object (gui, "recent_model"));
247 	gboolean existing_only = gtk_toggle_button_get_active
248 		(GTK_TOGGLE_BUTTON (gtk_builder_get_object (gui, "existing_only_button")));
249 	gboolean gnumeric_only = gtk_toggle_button_get_active
250 		(GTK_TOGGLE_BUTTON (gtk_builder_get_object (gui, "gnumeric_only_button")));
251 	GtkRecentManager *manager = gtk_recent_manager_get_default ();
252 	GList *docs, *l;
253 
254 	gtk_list_store_clear (list);
255 
256 	docs = gtk_recent_manager_get_items (manager);
257 	docs = g_list_sort (docs, by_age_uri);
258 	for (l = docs; l; l = l->next) {
259 		GtkRecentInfo *ri = l->data;
260 		GtkTreeIter iter;
261 
262 		if (existing_only) {
263 			gboolean exists = gtk_recent_info_is_local (ri)
264 				? gtk_recent_info_exists (ri)
265 				: TRUE;  /* Just assume so */
266 			if (!exists)
267 				continue;
268 		}
269 
270 		if (gnumeric_only) {
271 			if (!gtk_recent_info_has_application (ri, g_get_application_name ()))
272 				continue;
273 		}
274 
275 		gtk_list_store_append (list, &iter);
276 		gtk_list_store_set (list, &iter, RECENT_COL_INFO, ri, -1);
277 	}
278 	g_list_free_full (docs, (GDestroyNotify)gtk_recent_info_unref);
279 }
280 
281 
282 void
dialog_recent_used(WBCGtk * wbcg)283 dialog_recent_used (WBCGtk *wbcg)
284 {
285 	GtkBuilder *gui;
286 	GtkDialog *dialog;
287 
288 	/* Only pop up one copy per workbook */
289 	if (gnm_dialog_raise_if_exists (wbcg, RECENT_KEY))
290 		return;
291 
292 	gui = gnm_gtk_builder_load ("res:ui/recent.ui", NULL, GO_CMD_CONTEXT (wbcg));
293         if (gui == NULL)
294                 return;
295 
296 	dialog = GTK_DIALOG (go_gtk_builder_get_widget (gui, "recent_dialog"));
297 
298 	g_signal_connect (G_OBJECT (dialog), "response",
299 			  G_CALLBACK (cb_response), wbcg);
300 
301 
302 	{
303 		GtkWidget *w;
304 		int width, height, vsep;
305 		PangoLayout *layout;
306 		GtkTreeView *tv;
307 		GtkTreeSelection *tsel;
308 
309 		w = GTK_WIDGET (wbcg_toplevel (wbcg));
310 		layout = gtk_widget_create_pango_layout (w, "Mg19");
311 
312 		w = go_gtk_builder_get_widget (gui, "docs_treeview");
313 		gtk_widget_style_get (w, "vertical_separator", &vsep, NULL);
314 		g_signal_connect (w, "key-press-event",
315 				  G_CALLBACK (cb_key_press),
316 				  NULL);
317 		g_signal_connect (w, "button-press-event",
318 				  G_CALLBACK (cb_button_press),
319 				  wbcg);
320 
321 		pango_layout_get_pixel_size (layout, &width, &height);
322 		gtk_widget_set_size_request (go_gtk_builder_get_widget (gui, "docs_scrolledwindow"),
323 					     width * 60 / 4,
324 					     (2 * height + vsep) * (5 + 1));
325 		g_object_unref (layout);
326 		tv = GTK_TREE_VIEW (gtk_builder_get_object (gui, "docs_treeview"));
327 		tsel = gtk_tree_view_get_selection (tv);
328 		gtk_tree_selection_set_mode (tsel, GTK_SELECTION_MULTIPLE);
329 	}
330 
331 	g_signal_connect_swapped (gtk_builder_get_object (gui, "existing_only_button"),
332 				  "toggled", G_CALLBACK (populate_recent_model), gui);
333 	g_signal_connect_swapped (gtk_builder_get_object (gui, "gnumeric_only_button"),
334 				  "toggled", G_CALLBACK (populate_recent_model), gui);
335 
336 	populate_recent_model (gui);
337 
338 	gtk_tree_view_column_set_cell_data_func
339 		(GTK_TREE_VIEW_COLUMN (gtk_builder_get_object (gui, "url_column")),
340 		 GTK_CELL_RENDERER (gtk_builder_get_object (gui, "url_renderer")),
341 		 url_renderer_func,
342 		 NULL,
343 		 NULL);
344 
345 	gtk_tree_view_column_set_cell_data_func
346 		(GTK_TREE_VIEW_COLUMN (gtk_builder_get_object (gui, "age_column")),
347 		 GTK_CELL_RENDERER (gtk_builder_get_object (gui, "age_renderer")),
348 		 age_renderer_func,
349 		 g_date_time_new_now_local (),
350 		 (GDestroyNotify)g_date_time_unref);
351 
352 	/* ---------------------------------------- */
353 
354 	g_object_set_data_full (G_OBJECT (dialog), "gui", gui, g_object_unref);
355 	g_signal_connect (dialog, "destroy", G_CALLBACK (cb_destroy), NULL);
356 	go_gtk_nonmodal_dialog (wbcg_toplevel (wbcg), GTK_WINDOW (dialog));
357 	gtk_widget_show_all (GTK_WIDGET (dialog));
358 }
359 
360 /* ------------------------------------------------------------------------- */
361