1 /*
2 ** Copyright (C) 2010-2020 Dirk-Jan C. Binnema  <djcb@djcbsoftware.nl>
3 **
4 ** This program is free software; you can redistribute it and/or modify it
5 ** under the terms of the GNU General Public License as published by the
6 ** Free Software Foundation; either version 3, or (at your option) any
7 ** later version.
8 **
9 ** This program is distributed in the hope that it will be useful,
10 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 ** GNU General Public License for more details.
13 **
14 ** You should have received a copy of the GNU General Public License
15 ** along with this program; if not, write to the Free Software Foundation,
16 ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 **
18 */
19 
20 #include "config.h"
21 
22 #include <gtk/gtk.h>
23 #include <gdk/gdkkeysyms.h>
24 #include <string.h>		/* for memset */
25 
26 #include <utils/mu-util.h>
27 #include <mu-store.hh>
28 #include <mu-runtime.hh>
29 
30 #include "mug-msg-list-view.h"
31 #include "mug-query-bar.h"
32 #include "mug-msg-view.h"
33 #include "mug-shortcuts.h"
34 
35 struct _MugData {
36 	GtkWidget *win;
37 	GtkWidget *statusbar;
38 	GtkWidget *mlist;
39 	GtkWidget *toolbar;
40 	GtkWidget *msgview;
41 	GtkWidget *querybar;
42 	GtkWidget *shortcuts;
43 	gchar *muhome;
44 };
45 typedef struct _MugData MugData;
46 
47 
48 static void
about_mug(MugData * mugdata)49 about_mug (MugData * mugdata)
50 {
51 	GtkWidget *about;
52 	about = gtk_message_dialog_new
53 	    (GTK_WINDOW (mugdata->win), GTK_DIALOG_MODAL,
54 	     GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
55 	     "Mug version %s\n"
56 	     "A graphical frontend to the 'mu' e-mail search engine\n\n"
57 	     "(c) 2010-2013 Dirk-Jan C. Binnema\n"
58 	     "Released under the terms of the GPLv3+", VERSION);
59 
60 	gtk_dialog_run (GTK_DIALOG (about));
61 	gtk_widget_destroy (about);
62 }
63 
64 enum _ToolAction {
65 	ACTION_PREV_MSG = 1,
66 	ACTION_NEXT_MSG,
67 	ACTION_REINDEX,
68 	ACTION_DO_QUIT,
69 	ACTION_ABOUT,
70 	ACTION_SEPARATOR	/* pseudo action */
71 };
72 typedef enum _ToolAction ToolAction;
73 
74 static void
on_tool_button_clicked(GtkToolButton * btn,MugData * mugdata)75 on_tool_button_clicked (GtkToolButton * btn, MugData * mugdata)
76 {
77 	ToolAction action;
78 	action = (ToolAction)
79 	    GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (btn), "action"));
80 	switch (action) {
81 
82 	case ACTION_DO_QUIT:
83 		gtk_main_quit ();
84 		break;
85 	case ACTION_NEXT_MSG:
86 		mug_msg_list_view_move_next (MUG_MSG_LIST_VIEW
87 					     (mugdata->mlist));
88 		break;
89 	case ACTION_PREV_MSG:
90 		mug_msg_list_view_move_prev (MUG_MSG_LIST_VIEW
91 					     (mugdata->mlist));
92 		break;
93 	case ACTION_ABOUT:
94 		about_mug (mugdata);
95 		break;
96 	default:
97 		g_print ("%u\n", action);
98 	}
99 }
100 
101 
102 
103 static GtkToolItem*
tool_button(const char * name)104 tool_button (const char *name)
105 {
106 	GtkWidget *icon;
107 
108 	icon = gtk_image_new_from_icon_name
109 		(name, GTK_ICON_SIZE_SMALL_TOOLBAR);
110 
111 	return gtk_menu_tool_button_new (icon, NULL);
112 }
113 
114 
115 static GtkToolItem*
get_connected_tool_button(const char * stock_id,ToolAction action,MugData * mugdata)116 get_connected_tool_button (const char* stock_id, ToolAction action,
117 			   MugData *mugdata)
118 {
119 	GtkToolItem *btn;
120 
121 	btn = tool_button (stock_id);
122 	g_object_set_data (G_OBJECT (btn), "action",
123 			   GUINT_TO_POINTER (action));
124 	g_signal_connect (G_OBJECT (btn), "clicked",
125 			  G_CALLBACK (on_tool_button_clicked),
126 			  mugdata);
127 	return btn;
128 }
129 
130 static GtkWidget *
mug_toolbar(MugData * mugdata)131 mug_toolbar (MugData * mugdata)
132 {
133 	GtkWidget *toolbar;
134 	int i;
135 	struct {
136 		const char *stock_id;
137 		ToolAction action;
138 	} tools[] = {
139 		{"go-up", ACTION_PREV_MSG},
140 		{"go-down", ACTION_NEXT_MSG},
141 		{NULL, ACTION_SEPARATOR},
142 		{"view-refresh", ACTION_REINDEX},
143 		{NULL, ACTION_SEPARATOR},
144 		{"help-about", ACTION_ABOUT},
145 		{NULL, ACTION_SEPARATOR},
146 		{"application-exit", ACTION_DO_QUIT}};
147 
148 	toolbar = gtk_toolbar_new ();
149 	for (i = 0; i != G_N_ELEMENTS (tools); ++i) {
150 		if (tools[i].action == ACTION_SEPARATOR) { /* separator? */
151 			gtk_toolbar_insert (GTK_TOOLBAR (toolbar),
152 					    gtk_separator_tool_item_new (), i);
153 			continue;
154 		} else /* nope: a real item */
155 			gtk_toolbar_insert (GTK_TOOLBAR (toolbar),
156 					    get_connected_tool_button
157 					    (tools[i].stock_id, tools[i].action,
158 					     mugdata), i);
159 	}
160 
161 	return toolbar;
162 }
163 
164 static void
on_shortcut_clicked(GtkWidget * w,const gchar * query,MugData * mdata)165 on_shortcut_clicked (GtkWidget * w, const gchar * query, MugData * mdata)
166 {
167 	mug_query_bar_set_query (MUG_QUERY_BAR (mdata->querybar), query, TRUE);
168 }
169 
170 static GtkWidget *
mug_shortcuts_bar(MugData * data)171 mug_shortcuts_bar (MugData * data)
172 {
173 	data->shortcuts = mug_shortcuts_new
174 		(mu_runtime_path(MU_RUNTIME_PATH_BOOKMARKS));
175 
176 	g_signal_connect (G_OBJECT (data->shortcuts), "clicked",
177 			  G_CALLBACK (on_shortcut_clicked), data);
178 
179 	return data->shortcuts;
180 }
181 
182 static GtkWidget *
mug_statusbar(void)183 mug_statusbar (void)
184 {
185 	GtkWidget *statusbar;
186 
187 	statusbar = gtk_statusbar_new ();
188 
189 	return statusbar;
190 }
191 
192 static void
on_query_changed(MugQueryBar * bar,const char * query,MugData * mugdata)193 on_query_changed (MugQueryBar * bar, const char *query, MugData * mugdata)
194 {
195 	int count;
196 
197 	/* clear the old message */
198 	mug_msg_view_set_msg (MUG_MSG_VIEW (mugdata->msgview), NULL);
199 
200 	count = mug_msg_list_view_query (MUG_MSG_LIST_VIEW (mugdata->mlist),
201 					 query);
202 	if (count >= 0) {
203 		gchar *msg =
204 		    g_strdup_printf ("%d message%s found matching '%s'",
205 				     count,
206 				     count > 1 ? "s" : "",
207 				     mug_msg_list_view_get_query
208 				     (MUG_MSG_LIST_VIEW (mugdata->mlist)));
209 		gtk_statusbar_push (GTK_STATUSBAR (mugdata->statusbar), 0, msg);
210 		g_free (msg);
211 
212 		mug_msg_list_view_move_first (MUG_MSG_LIST_VIEW
213 					      (mugdata->mlist));
214 		gtk_widget_grab_focus (GTK_WIDGET (mugdata->mlist));
215 	}
216 
217 	if (count == 0)		/* nothing found */
218 		mug_query_bar_grab_focus (MUG_QUERY_BAR (bar));
219 }
220 
221 static void
on_msg_selected(MugMsgListView * mlist,const char * mpath,MugData * mugdata)222 on_msg_selected (MugMsgListView * mlist, const char *mpath, MugData * mugdata)
223 {
224 	mug_msg_view_set_msg (MUG_MSG_VIEW (mugdata->msgview), mpath);
225 }
226 
227 static void
on_list_view_error(MugMsgListView * mlist,MugError err,MugData * mugdata)228 on_list_view_error (MugMsgListView * mlist, MugError err, MugData * mugdata)
229 {
230 	GtkWidget *errdialog;
231 	const char *msg;
232 
233 	switch (err) {
234 	case MUG_ERROR_XAPIAN_NOT_UPTODATE:
235 		msg = "The Xapian Database has the wrong version\n"
236 		    "Please run 'mu index --rebuild'";
237 		break;
238 	case MUG_ERROR_XAPIAN_DIR:
239 		msg = "Cannot find the Xapian database dir\n"
240 		    "Please restart mug with --muhome=... pointing\n"
241 		    "to your mu home directory";
242 		break;
243 	case MUG_ERROR_QUERY:
244 		msg = "Error in query";
245 		break;
246 	default:
247 		msg = "Some error occurred";
248 		break;
249 	}
250 
251 	errdialog = gtk_message_dialog_new
252 	    (GTK_WINDOW (mugdata->win), GTK_DIALOG_MODAL,
253 	     GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", msg);
254 
255 	gtk_dialog_run (GTK_DIALOG (errdialog));
256 	gtk_widget_destroy (errdialog);
257 
258 	if (err == MUG_ERROR_QUERY)
259 		mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata->querybar));
260 }
261 
262 static GtkWidget *
mug_querybar(void)263 mug_querybar (void)
264 {
265 	GtkWidget *querybar;
266 
267 	querybar = mug_query_bar_new ();
268 
269 	return querybar;
270 }
271 
272 static GtkWidget *
mug_query_area(MugData * mugdata)273 mug_query_area (MugData * mugdata)
274 {
275 	GtkWidget *queryarea, *paned, *scrolled;
276 
277 	queryarea = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
278 	paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
279 
280 	mugdata->mlist = mug_msg_list_view_new
281 		(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB));
282 	scrolled = gtk_scrolled_window_new (NULL, NULL);
283 	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
284 					GTK_POLICY_AUTOMATIC,
285 					GTK_POLICY_AUTOMATIC);
286 
287 	gtk_container_add (GTK_CONTAINER (scrolled), mugdata->mlist);
288 	gtk_paned_add1 (GTK_PANED (paned), scrolled);
289 
290 	mugdata->msgview = mug_msg_view_new ();
291 	mug_msg_view_set_note (MUG_MSG_VIEW(mugdata->msgview),
292 			       "<h1>Welcome to <i>mug</i>!</h1><hr>"
293 			       "<tt>mug</tt> is an experimental UI for <tt>mu</tt>, which will "
294 			       "slowly evolve into something useful.<br><br>Enjoy the ride.");
295 	g_signal_connect (G_OBJECT (mugdata->mlist), "msg-selected",
296 			  G_CALLBACK (on_msg_selected), mugdata);
297 	g_signal_connect (G_OBJECT (mugdata->mlist), "error-occured",
298 			  G_CALLBACK (on_list_view_error), mugdata);
299 	gtk_paned_add2 (GTK_PANED (paned), mugdata->msgview);
300 
301 	mugdata->querybar = mug_querybar ();
302 	g_signal_connect (G_OBJECT (mugdata->querybar), "query-changed",
303 			  G_CALLBACK (on_query_changed), mugdata);
304 
305 	gtk_box_pack_start (GTK_BOX (queryarea),
306 			    mugdata->querybar, FALSE, FALSE, 2);
307 	gtk_box_pack_start (GTK_BOX (queryarea), paned, TRUE, TRUE, 2);
308 
309 	gtk_widget_show_all (queryarea);
310 	return queryarea;
311 }
312 
313 static GtkWidget *
mug_main_area(MugData * mugdata)314 mug_main_area (MugData * mugdata)
315 {
316 	GtkWidget *mainarea, *w;
317 
318 	mainarea = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
319 
320 	w = mug_shortcuts_bar (mugdata);
321 	gtk_box_pack_start (GTK_BOX (mainarea), w, FALSE, FALSE, 0);
322 	gtk_widget_show (w);
323 
324 	w = mug_query_area (mugdata);
325 	gtk_box_pack_start (GTK_BOX (mainarea), w, TRUE, TRUE, 0);
326 	gtk_widget_show (w);
327 
328 	return mainarea;
329 }
330 
331 static GtkWidget*
mug_shell(MugData * mugdata)332 mug_shell (MugData *mugdata)
333 {
334 	GtkWidget *vbox;
335 
336 	mugdata->win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
337 	gtk_window_set_title (GTK_WINDOW (mugdata->win), "Mug Mail Search");
338 
339 	vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
340 
341 	mugdata->toolbar = mug_toolbar (mugdata);
342 	gtk_box_pack_start (GTK_BOX (vbox), mugdata->toolbar, FALSE, FALSE, 2);
343 	gtk_box_pack_start (GTK_BOX (vbox), mug_main_area (mugdata), TRUE,
344 			    TRUE, 2);
345 
346 	mugdata->statusbar = mug_statusbar ();
347 	gtk_box_pack_start (GTK_BOX (vbox), mugdata->statusbar, FALSE, FALSE,
348 			    2);
349 
350 	gtk_container_add (GTK_CONTAINER (mugdata->win), vbox);
351 	gtk_widget_show_all (vbox);
352 
353 	gtk_window_set_default_size (GTK_WINDOW (mugdata->win), 700, 500);
354 	gtk_window_set_resizable (GTK_WINDOW (mugdata->win), TRUE);
355 
356 	{
357 		gchar *icon;
358 		icon = g_strdup_printf ("%s%cmug.svg",
359 					MUGDIR, G_DIR_SEPARATOR);
360 		gtk_window_set_icon_from_file (GTK_WINDOW (mugdata->win), icon, NULL);
361 		g_free (icon);
362 	}
363 
364 	return mugdata->win;
365 }
366 
367 static gint
on_focus_query_bar(GtkWidget * ignored,GdkEventKey * event,MugData * mugdata)368 on_focus_query_bar (GtkWidget* ignored, GdkEventKey *event, MugData* mugdata)
369 {
370 	if (event->type==GDK_KEY_RELEASE && event->keyval==GDK_KEY_Escape) {
371 		mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata->querybar));
372 		return 1;
373 	}
374 	return 0;
375 }
376 
377 int
main(int argc,char * argv[])378 main (int argc, char *argv[])
379 {
380 	MugData mugdata;
381 	GtkWidget *mugshell;
382 	GOptionContext *octx;
383 	GOptionEntry entries[] = {
384 		{"muhome", 0, 0, G_OPTION_ARG_FILENAME, &mugdata.muhome,
385 		 "specify an alternative mu directory", NULL},
386 		{NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}	/* sentinel */
387 	};
388 
389 	gtk_init (&argc, &argv);
390 
391 	octx = g_option_context_new ("- mug options");
392 	g_option_context_add_main_entries (octx, entries, "Mug");
393 
394 	memset (&mugdata, 0, sizeof (MugData));
395 	if (!g_option_context_parse (octx, &argc, &argv, NULL)) {
396 		g_option_context_free (octx);
397 		g_printerr ("mug: error in options\n");
398 		return 1;
399 	}
400 
401 	g_option_context_free (octx);
402 	mu_runtime_init (mugdata.muhome, "mug", FALSE);
403 
404 	mugshell = mug_shell (&mugdata);
405 	g_signal_connect (G_OBJECT (mugshell), "destroy",
406 			  G_CALLBACK (gtk_main_quit), NULL);
407 	g_signal_connect (G_OBJECT (mugshell), "key_release_event",
408 			  G_CALLBACK ( on_focus_query_bar ), (gpointer)&mugdata );
409 
410 	gtk_widget_show (mugshell);
411 	mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata.querybar));
412 
413 	gtk_main ();
414 	g_free (mugdata.muhome);
415 
416 	mu_runtime_uninit ();
417 
418 	return 0;
419 }
420