Added a skeleton implementation of the ChimaraIF widget, which is a subclass of Chima...
[rodin/chimara.git] / libchimara / fileref.c
1 #include <errno.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <gtk/gtk.h>
5 #include <glib/gstdio.h>
6 #include "fileref.h"
7 #include "magic.h"
8 #include "chimara-glk-private.h"
9 #include "gi_dispa.h"
10
11 extern GPrivate *glk_data_key;
12
13 /* Internal function: create a fileref using the given parameters. */
14 frefid_t
15 fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode)
16 {
17         g_return_val_if_fail(filename != NULL, NULL);
18
19         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
20         
21         frefid_t f = g_new0(struct glk_fileref_struct, 1);
22         f->magic = MAGIC_FILEREF;
23         f->rock = rock;
24         if(glk_data->register_obj)
25                 f->disprock = (*glk_data->register_obj)(f, gidisp_Class_Fileref);
26         
27         f->filename = g_strdup(filename);
28         f->usage = usage;
29         f->orig_filemode = orig_filemode;
30         
31         /* Add it to the global fileref list */
32         glk_data->fileref_list = g_list_prepend(glk_data->fileref_list, f);
33         f->fileref_list = glk_data->fileref_list;
34         
35         return f;
36 }
37
38 static void
39 fileref_close_common(frefid_t fref)
40 {
41         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
42         
43         glk_data->fileref_list = g_list_delete_link(glk_data->fileref_list, fref->fileref_list);
44
45         if(glk_data->unregister_obj)
46         {
47                 (*glk_data->unregister_obj)(fref, gidisp_Class_Fileref, fref->disprock);
48                 fref->disprock.ptr = NULL;
49         }
50         
51         if(fref->filename)
52                 g_free(fref->filename);
53         
54         fref->magic = MAGIC_FREE;
55         g_free(fref);
56 }
57
58 /**
59  * glk_fileref_iterate:
60  * @fref: A file reference, or %NULL.
61  * @rockptr: Return location for the next fileref's rock, or %NULL.
62  *
63  * Iterates through all the existing filerefs. See <link
64  * linkend="chimara-Iterating-Through-Opaque-Objects">Iterating Through Opaque
65  * Objects</link>.
66  *
67  * Returns: the next file reference, or %NULL if there are no more.
68  */
69 frefid_t
70 glk_fileref_iterate(frefid_t fref, glui32 *rockptr)
71 {
72         VALID_FILEREF_OR_NULL(fref, return NULL);
73
74         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
75         GList *retnode;
76         
77         if(fref == NULL)
78                 retnode = glk_data->fileref_list;
79         else
80                 retnode = fref->fileref_list->next;
81         frefid_t retval = retnode? (frefid_t)retnode->data : NULL;
82                 
83         /* Store the fileref's rock in rockptr */
84         if(retval && rockptr)
85                 *rockptr = glk_fileref_get_rock(retval);
86                 
87         return retval;
88 }
89
90 /**
91  * glk_fileref_get_rock:
92  * @fref: A file reference.
93  * 
94  * Retrieves the file reference @fref's rock value. See <link 
95  * linkend="chimara-Rocks">Rocks</link>.
96  *
97  * Returns: A rock value.
98  */
99 glui32
100 glk_fileref_get_rock(frefid_t fref)
101 {
102         VALID_FILEREF(fref, return 0);
103         return fref->rock;
104 }
105
106 /**
107  * glk_fileref_create_temp:
108  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
109  * @rock: The new fileref's rock value.
110  *
111  * Creates a reference to a temporary file. It is always a new file (one which
112  * does not yet exist). The file (once created) will be somewhere out of the
113  * player's way.
114  *
115  * <note><para>
116  *   This is why no name is specified; the player will never need to know it.
117  * </para></note>
118  *
119  * A temporary file should never be used for long-term storage. It may be
120  * deleted automatically when the program exits, or at some later time, say
121  * when the machine is turned off or rebooted. You do not have to worry about
122  * deleting it yourself.
123  *
124  * Returns: A new fileref, or #NULL if the fileref creation failed.
125  */ 
126 frefid_t
127 glk_fileref_create_temp(glui32 usage, glui32 rock)
128 {
129         /* Get a temp file */
130         GError *error = NULL;
131         gchar *filename = NULL;
132         gint handle = g_file_open_tmp("glkXXXXXX", &filename, &error);
133         if(handle == -1)
134         {
135                 WARNING_S("Error creating temporary file", error->message);
136                 if(filename)
137                         g_free(filename);
138                 return NULL;
139         }
140         if(close(handle) == -1) /* There is no g_close() */
141         {
142                 IO_WARNING( "Error closing temporary file", filename, g_strerror(errno) );
143                 if(filename)
144                         g_free(filename);
145                 return NULL;
146         }
147         
148         frefid_t f = fileref_new(filename, rock, usage, filemode_Write);
149         g_free(filename);
150         return f;
151 }
152
153 /**
154  * glk_fileref_create_by_prompt:
155  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
156  * @fmode: File mode, contolling the dialog's behavior.
157  * @rock: The new fileref's rock value.
158  *
159  * Creates a reference to a file by asking the player to locate it. The library
160  * may simply prompt the player to type a name, or may use a platform-native
161  * file navigation tool. (The prompt, if any, is inferred from the usage
162  * argument.)
163  *
164  * <note><title>Chimara</title>
165  * <para>
166  * Chimara uses a <link 
167  * linkend="gtk-GtkFileChooserDialog">GtkFileChooserDialog</link>. The default
168  * starting location for the dialog may be set with glkunix_set_base_file().
169  * </para></note>
170  *
171  * @fmode must be one of these values:
172  * <variablelist>
173  * <varlistentry>
174  *   <term>%filemode_Read</term>
175  *   <listitem><para>The file must already exist; and the player will be asked
176  *   to select from existing files which match the usage.</para></listitem>
177  * </varlistentry>
178  * <varlistentry>
179  *   <term>%filemode_Write</term>
180  *   <listitem><para>The file should not exist; if the player selects an
181  *   existing file, he will be warned that it will be replaced.
182  *   </para></listitem>
183  * </varlistentry>
184  * <varlistentry>
185  *   <term>%filemode_ReadWrite</term>
186  *   <listitem><para>The file may or may not exist; if it already exists, the
187  *   player will be warned that it will be modified.</para></listitem>
188  * </varlistentry>
189  * <varlistentry>
190  *   <term>%filemode_WriteAppend</term>
191  *   <listitem><para>Same behavior as %filemode_ReadWrite.</para></listitem>
192  * </varlistentry>
193  * </variablelist>
194  *
195  * The @fmode argument should generally match the @fmode which will be used to
196  * open the file.
197  *
198  * <note><para>
199  *   It is possible that the prompt or file tool will have a 
200  *   <quote>cancel</quote> option. If the player chooses this,
201  *   glk_fileref_create_by_prompt() will return %NULL. This is a major reason
202  *   why you should make sure the return value is valid before you use it.
203  * </para></note>
204  *
205  * Returns: A new fileref, or #NULL if the fileref creation failed or the
206  * dialog was canceled.
207  */
208 frefid_t
209 glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock)
210 {
211         /* TODO: Remember current working directory and last used filename
212         for each usage */
213         GtkWidget *chooser;
214
215         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
216         
217         gdk_threads_enter();
218
219         switch(fmode)
220         {
221                 case filemode_Read:
222                         chooser = gtk_file_chooser_dialog_new("Select a file to open", NULL,
223                                 GTK_FILE_CHOOSER_ACTION_OPEN,
224                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
225                                 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
226                                 NULL);
227                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_OPEN);
228                         break;
229                 case filemode_Write:
230                         chooser = gtk_file_chooser_dialog_new("Select a file to save to", NULL,
231                                 GTK_FILE_CHOOSER_ACTION_SAVE,
232                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
233                                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
234                                 NULL);
235                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE);
236                         gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE);
237                         break;
238                 case filemode_ReadWrite:
239                 case filemode_WriteAppend:
240                         chooser = gtk_file_chooser_dialog_new("Select a file to save to", NULL,
241                                 GTK_FILE_CHOOSER_ACTION_SAVE,
242                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
243                                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
244                                 NULL);
245                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE);
246                         break;
247                 default:
248                         ILLEGAL_PARAM("Unknown file mode: %u", fmode);
249                         gdk_threads_leave();
250                         return NULL;
251         }
252         
253         if(glk_data->current_dir)
254                 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(chooser), glk_data->current_dir);
255         
256         if(gtk_dialog_run( GTK_DIALOG(chooser) ) != GTK_RESPONSE_ACCEPT)
257         {
258                 gtk_widget_destroy(chooser);
259                 gdk_threads_leave();
260                 return NULL;
261         }
262         gchar *filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) );
263         frefid_t f = fileref_new(filename, rock, usage, fmode);
264         g_free(filename);
265         gtk_widget_destroy(chooser);
266
267         gdk_threads_leave();
268         return f;
269 }
270
271 /**
272  * glk_fileref_create_by_name:
273  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
274  * @name: A filename.
275  * @rock: The new fileref's rock value.
276  *
277  * This creates a reference to a file with a specific name. The file will be
278  * in a fixed location relevant to your program, and visible to the player.
279  *
280  * <note><para>
281  *   This usually means <quote>in the same directory as your program.</quote>
282  * </para></note>
283  * <note><title>Chimara</title>
284  * <para>
285  * In Chimara, the file is created in the directory last set by 
286  * glkunix_set_base_file(), and otherwise in the current working directory.
287  * </para></note>
288  *
289  * Since filenames are highly platform-specific, you should use
290  * glk_fileref_create_by_name() with care. It is legal to pass any string in the
291  * name argument. However, the library may have to mangle, transform, or
292  * truncate the string to make it a legal native filename. 
293  *
294  * <note><para>
295  *   For example, if you create two filerefs with the names <quote>File</quote>
296  *   and <quote>FILE</quote>, they may wind up pointing to the same file; the
297  *   platform may not support case distinctions in file names. Another example:
298  *   on a platform where file type is specified by filename suffix, the library
299  *   will add an appropriate suffix based on the usage; any suffix in the string
300  *   will be overwritten or added to. For that matter, remember that the period
301  *   is not a legal character in Acorn filenames...
302  * </para></note>
303  *
304  * The most conservative approach is to pass a string of no more than 8
305  * characters, consisting entirely of upper-case letters and numbers, starting
306  * with a letter. You can then be reasonably sure that the resulting filename
307  * will display all the characters you specify &mdash; in some form. 
308  *
309  * Returns: A new fileref, or %NULL if the fileref creation failed. 
310  */
311 frefid_t
312 glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock)
313 {
314         g_return_val_if_fail(name != NULL && strlen(name) > 0, NULL);
315
316         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
317         
318         /* Do any string-munging here to remove illegal Latin-1 characters from 
319         filename. On ext3, the only illegal characters are '/' and '\0'. */
320         g_strdelimit(name, "/", '_');
321         
322         /* Find out what encoding filenames are in */
323         const gchar **charsets; /* Do not free */
324         g_get_filename_charsets(&charsets);
325
326         /* Convert name to that encoding */
327         GError *error = NULL;
328         gchar *osname = g_convert(name, -1, charsets[0], "ISO-8859-1", NULL, NULL,
329                 &error);
330         if(osname == NULL)
331         {
332                 WARNING_S("Error during latin1->filename conversion", error->message);
333                 return NULL;
334         }
335         
336         gchar *path;
337         if(glk_data->current_dir)
338                 path = g_build_filename(glk_data->current_dir, osname, NULL);
339         else
340                 path = g_strdup(osname);
341         g_free(osname);
342         
343         frefid_t f = fileref_new(path, rock, usage, filemode_ReadWrite);
344         g_free(path);
345         return f;
346 }
347
348 /**
349  * glk_fileref_create_from_fileref:
350  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
351  * @fref: Fileref to copy.
352  * @rock: The new fileref's rock value.
353  *
354  * This copies an existing file reference @fref, but changes the usage. (The
355  * original fileref is not modified.)
356  *
357  * The use of this function can be tricky. If you change the type of the fileref
358  * (%fileusage_Data, %fileusage_SavedGame, etc), the new reference may or may
359  * not point to the same actual disk file.
360  *
361  * <note><para>
362  *   This generally depends on whether the platform uses suffixes to indicate
363  *   file type.
364  * </para></note>
365  *
366  * If you do this, and open both file references for writing, the results are
367  * unpredictable. It is safest to change the type of a fileref only if it refers
368  * to a nonexistent file.
369  *
370  * If you change the mode of a fileref (%fileusage_TextMode,
371  * %fileusage_BinaryMode), but leave the rest of the type unchanged, the new
372  * fileref will definitely point to the same disk file as the old one.
373  * 
374  * Obviously, if you write to a file in text mode and then read from it in
375  * binary mode, the results are platform-dependent. 
376  *
377  * Returns: A new fileref, or %NULL if the fileref creation failed. 
378  */
379 frefid_t
380 glk_fileref_create_from_fileref(glui32 usage, frefid_t fref, glui32 rock)
381 {
382         VALID_FILEREF(fref, return NULL);
383         return fileref_new(fref->filename, rock, usage, fref->orig_filemode);
384 }
385
386 /**
387  * glk_fileref_destroy:
388  * @fref: Fileref to destroy.
389  * 
390  * Destroys a fileref which you have created. This does <emphasis>not</emphasis>
391  * affect the disk file; it just reclaims the resources allocated by the
392  * <code>glk_fileref_create...</code> function.
393  *
394  * It is legal to destroy a fileref after opening a file with it (while the
395  * file is still open.) The fileref is only used for the opening operation,
396  * not for accessing the file stream.
397  */
398 void
399 glk_fileref_destroy(frefid_t fref)
400 {
401         VALID_FILEREF(fref, return);
402         fileref_close_common(fref);
403 }
404
405 /**
406  * glk_fileref_delete_file:
407  * @fref: A refrence to the file to delete.
408  *
409  * Deletes the file referred to by @fref. It does not destroy @fref itself.
410  */
411 void
412 glk_fileref_delete_file(frefid_t fref)
413 {
414         VALID_FILEREF(fref, return);
415         if( glk_fileref_does_file_exist(fref) )
416                 if(g_unlink(fref->filename) == -1)
417                         IO_WARNING( "Error deleting file", fref->filename, g_strerror(errno) );
418 }
419
420 /**
421  * glk_fileref_does_file_exist:
422  * @fref: A fileref to check.
423  *
424  * Checks whether the file referred to by @fref exists.
425  *
426  * Returns: %TRUE (1) if @fref refers to an existing file, and %FALSE (0) if 
427  * not.
428  */
429 glui32
430 glk_fileref_does_file_exist(frefid_t fref)
431 {
432         VALID_FILEREF(fref, return 0);
433         if( g_file_test(fref->filename, G_FILE_TEST_EXISTS) )
434                 return 1;
435         return 0;
436 }
437