Add chimara_app_foreach_game_window()
[projects/chimara/chimara.git] / player / app.c
1 /*
2  * Copyright (C) 2008, 2009, 2010, 2011 Philip Chimento and Marijn van Vliet.
3  * All rights reserved.
4  *
5  * Chimara is free software copyrighted by Philip Chimento and Marijn van Vliet.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are met:
9  *
10  * 1. Redistributions of source code must retain the above copyright notice,
11  *    this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  * 3. Neither of the names Philip Chimento or Marijn van Vliet, nor the name of
16  *    any other contributor may be used to endorse or promote products derived
17  *    from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29  * POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <glib-object.h>
35 #include <glib/gi18n.h>
36 #include <glib/gstdio.h>
37
38 /* Use a custom GSettings backend for our preferences file */
39 #define G_SETTINGS_ENABLE_BACKEND
40 #include <gio/gsettingsbackend.h>
41
42 #include <config.h>
43 #include <libchimara/chimara-if.h>
44 #include "app.h"
45 #include "browser.h"
46 #include "error.h"
47 #include "player.h"
48 #include "preferences.h"
49 #include "util.h"
50
51 typedef struct _ChimaraAppPrivate {
52         /* Action group containing "application actions" */
53         GtkActionGroup *action_group;
54         /* List of currently opened player windows */
55         GSList *window_list;
56 } ChimaraAppPrivate;
57
58 #define CHIMARA_APP_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), CHIMARA_TYPE_APP, ChimaraAppPrivate))
59 #define CHIMARA_APP_USE_PRIVATE ChimaraAppPrivate *priv = CHIMARA_APP_PRIVATE(self)
60
61 G_DEFINE_TYPE(ChimaraApp, chimara_app, G_TYPE_OBJECT);
62
63 static void
64 chimara_app_finalize(GObject *self)
65 {
66         CHIMARA_APP_USE_PRIVATE;
67         g_object_unref(priv->action_group);
68         g_slist_free(priv->window_list);
69         
70         /* Chain up */
71         G_OBJECT_CLASS(chimara_app_parent_class)->finalize(self);
72 }
73
74 static void
75 chimara_app_class_init(ChimaraAppClass *klass)
76 {
77         /* Override methods of parent classes */
78         GObjectClass *object_class = G_OBJECT_CLASS(klass);
79         object_class->finalize = chimara_app_finalize;
80
81         /* Private data */
82         g_type_class_add_private(klass, sizeof(ChimaraAppPrivate));
83 }
84
85 static void
86 chimara_app_init(ChimaraApp *self)
87 {
88         CHIMARA_APP_USE_PRIVATE;
89
90         /* Create configuration dir ~/.chimara */
91         gchar *configdir = g_build_filename(g_get_home_dir(), ".chimara", NULL);
92         if(!g_file_test(configdir, G_FILE_TEST_IS_DIR)
93                 && g_mkdir(configdir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0)
94                 g_error(_("Cannot create configuration directory ~/.chimara"));
95         g_free(configdir);
96
97         /* Initialize settings file; it can be overridden by a "chimara-config" file
98          in the current directory */
99         gchar *keyfile;
100         if(g_file_test("chimara-config", G_FILE_TEST_IS_REGULAR))
101                 keyfile = g_strdup("chimara-config");
102         else
103                 keyfile = g_build_filename(g_get_home_dir(), ".chimara", "config", NULL);
104         GSettingsBackend *backend = g_keyfile_settings_backend_new(keyfile, "/org/chimara-if/player/", NULL);
105         self->prefs_settings = g_settings_new_with_backend("org.chimara-if.player.preferences", backend);
106         self->state_settings = g_settings_new_with_backend("org.chimara-if.player.state", backend);
107         g_free(keyfile);
108
109         /* Build user interface */
110         char *object_ids[] = {
111                 "app_group",
112                 "aboutwindow",
113                 NULL
114         };
115         GtkBuilder *builder = new_builder_with_objects(object_ids);
116
117         self->aboutwindow = GTK_WIDGET(load_object(builder, "aboutwindow"));
118         priv->action_group = GTK_ACTION_GROUP(load_object(builder, "app_group"));
119         g_object_ref(priv->action_group);
120
121         const gchar **ptr;
122         GtkRecentFilter *filter = gtk_recent_filter_new();
123         /* TODO: Use mimetypes and construct the filter dynamically depending on 
124         what plugins are installed */
125         const gchar *patterns[] = {
126                 "*.z[1-8]", "*.[zg]lb", "*.[zg]blorb", "*.ulx", "*.blb", "*.blorb", NULL
127         };
128
129         for(ptr = patterns; *ptr; ptr++)
130                 gtk_recent_filter_add_pattern(filter, *ptr);
131         GtkRecentChooser *recent = GTK_RECENT_CHOOSER(load_object(builder, "recent"));
132         gtk_recent_chooser_add_filter(recent, filter);
133
134         gtk_builder_connect_signals(builder, self);
135
136         g_object_unref(builder);
137 }
138
139 /* PUBLIC FUNCTIONS */
140
141 ChimaraApp *
142 chimara_app_get(void)
143 {
144     static ChimaraApp *theapp = NULL;
145
146     if(G_UNLIKELY(theapp == NULL)) {
147                 theapp = CHIMARA_APP(g_object_new(CHIMARA_TYPE_APP, NULL));
148
149                 /* Create one-per-application windows */
150                 theapp->prefswindow = chimara_prefs_new();
151                 theapp->browser_window = chimara_browser_new();
152         }
153
154         return theapp;
155 }
156
157 GtkActionGroup *
158 chimara_app_get_action_group(ChimaraApp *self)
159 {
160         CHIMARA_APP_USE_PRIVATE;
161         return priv->action_group;
162 }
163
164 /* Internal function: See if there is a corresponding graphics file. If so,
165 return its path. If not, return NULL. */
166 static char *
167 search_for_graphics_file(const char *path)
168 {
169         ChimaraApp *theapp = chimara_app_get();
170
171         /* First get the name of the story file */
172         char *scratch = g_path_get_basename(path);
173         *(strrchr(scratch, '.')) = '\0';
174
175         /* Check in the stored resource path, if set */
176         char *resource_path;
177         g_settings_get(theapp->prefs_settings, "resource-path", "ms", &resource_path);
178
179         /* Otherwise check in the current directory */
180         if(!resource_path)
181                 resource_path = g_path_get_dirname(path);
182
183         char *blorbfile = g_strconcat(resource_path, "/", scratch, ".blb", NULL);
184         g_free(scratch);
185         g_free(resource_path);
186
187         if(g_file_test(blorbfile, G_FILE_TEST_EXISTS))
188                 return blorbfile;
189
190         g_free(blorbfile);
191         return NULL;
192 }
193
194 /* Remove a deleted player window from the list of currently opened windows */
195 static gboolean
196 on_player_delete_event(GtkWidget *player, GdkEvent *event, ChimaraApp *self)
197 {
198         CHIMARA_APP_USE_PRIVATE;
199         priv->window_list = g_slist_remove(priv->window_list, player);
200         return FALSE; /* don't block event */
201 }
202
203 ChimaraPlayer *
204 chimara_app_open_game(ChimaraApp *self, const char *path)
205 {
206         CHIMARA_APP_USE_PRIVATE;
207         GError *error = NULL;
208
209         /* Open a new player window */
210         ChimaraPlayer *player = CHIMARA_PLAYER(chimara_player_new());
211         gtk_widget_show_all(GTK_WIDGET(player));
212         gtk_window_present(GTK_WINDOW(player));
213
214         gchar *blorbfile = search_for_graphics_file(path);
215         if(blorbfile) {
216                 g_object_set(player->glk, "graphics-file", blorbfile, NULL);
217                 g_free(blorbfile);
218         }
219         if(!chimara_if_run_game(CHIMARA_IF(player->glk), path, &error)) {
220                 error_dialog(GTK_WINDOW(player), error, _("Could not open game file '%s': "), path);
221                 gtk_widget_destroy(GTK_WIDGET(player));
222                 return NULL;
223         }
224
225         /* Add the opened game to the list of currently opened windows */
226         priv->window_list = g_slist_prepend(priv->window_list, player);
227         g_signal_connect_after(player, "delete-event", G_CALLBACK(on_player_delete_event), self);
228
229         return player;
230 }
231
232 void
233 chimara_app_foreach_game_window(ChimaraApp *self, GFunc func, gpointer data)
234 {
235         CHIMARA_APP_USE_PRIVATE;
236         g_slist_foreach(priv->window_list, func, data);
237 }
238
239 /* GLADE CALLBACKS */
240
241 void
242 on_open_activate(GtkAction *action, ChimaraApp *theapp)
243 {
244         //if(!confirm_open_new_game(CHIMARA_GLK(player->glk)))
245         //      return;
246
247         GtkWidget *dialog = gtk_file_chooser_dialog_new(_("Open Game"),
248             GTK_WINDOW(theapp->browser_window),
249             GTK_FILE_CHOOSER_ACTION_OPEN,
250             GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
251             GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
252             NULL);
253
254         /* Get last opened path */
255         gchar *path;
256         g_settings_get(theapp->state_settings, "last-open-path", "ms", &path);
257         if(path) {
258                 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), path);
259                 g_free(path);
260         }
261
262         if(gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_ACCEPT)
263                 goto finally;
264
265         GError *error = NULL;
266         char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
267
268         if(!chimara_app_open_game(theapp, filename))
269                 goto finally2;
270
271         path = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(dialog));
272         if(path) {
273                 g_settings_set(theapp->state_settings, "last-open-path", "ms", path);
274                 g_free(path);
275         }
276
277         /* Add file to recent files list */
278         GtkRecentManager *manager = gtk_recent_manager_get_default();
279         gchar *uri;
280
281         if(!(uri = g_filename_to_uri(filename, NULL, &error)))
282                 g_warning(_("Could not convert filename '%s' to URI: %s"), filename, error->message);
283         else {
284                 if(!gtk_recent_manager_add_item(manager, uri))
285                         g_warning(_("Could not add URI '%s' to recent files list."), uri);
286                 g_free(uri);
287         }
288
289 finally2:
290         g_free(filename);
291 finally:
292         gtk_widget_destroy(dialog);
293 }
294
295 void
296 on_recent_item_activated(GtkRecentChooser *chooser, ChimaraApp *theapp)
297 {
298         GError *error = NULL;
299         gchar *uri = gtk_recent_chooser_get_current_uri(chooser);
300         gchar *filename;
301         if(!(filename = g_filename_from_uri(uri, NULL, &error))) {
302                 error_dialog(GTK_WINDOW(theapp->browser_window), error, _("Could not open game file '%s': "), uri);
303                 goto finally;
304         }
305         
306         //if(!confirm_open_new_game(CHIMARA_GLK(player->glk)))
307         //      goto finally2;
308
309         if(!chimara_app_open_game(theapp, filename))
310                 goto finally2;
311         
312         /* Add file to recent files list again, this updates it to most recently used */
313         GtkRecentManager *manager = gtk_recent_manager_get_default();
314         if(!gtk_recent_manager_add_item(manager, uri))
315                 g_warning(_("Could not add URI '%s' to recent files list."), uri);
316
317 finally2:
318         g_free(filename);
319 finally:
320         g_free(uri);
321 }
322
323 void 
324 on_quit_chimara_activate(GtkAction *action, ChimaraApp *theapp)
325 {
326         gtk_main_quit();
327 }
328
329 void
330 on_preferences_activate(GtkAction *action, ChimaraApp *theapp)
331 {
332         gtk_window_present(GTK_WINDOW(theapp->prefswindow));
333 }
334
335 void
336 on_about_activate(GtkAction *action, ChimaraApp *theapp)
337 {
338         gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(theapp->aboutwindow), PACKAGE_VERSION);
339         gtk_window_present(GTK_WINDOW(theapp->aboutwindow));
340 }
341