1 /** \file   uismartattach.c
2  * \brief   GTK3 smart-attach dialog
3  *
4  * \author  Bas Wassink <b.wassink@ziggo.nl>
5  */
6 
7 /*
8  * This file is part of VICE, the Versatile Commodore Emulator.
9  * See README for copyright notice.
10  *
11  *  This program is free software; you can redistribute it and/or modify
12  *  it under the terms of the GNU General Public License as published by
13  *  the Free Software Foundation; either version 2 of the License, or
14  *  (at your option) any later version.
15  *
16  *  This program is distributed in the hope that it will be useful,
17  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  *  GNU General Public License for more details.
20  *
21  *  You should have received a copy of the GNU General Public License
22  *  along with this program; if not, write to the Free Software
23  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
24  *  02111-1307  USA.
25  */
26 
27 #include "vice.h"
28 
29 #include <gtk/gtk.h>
30 
31 #include "attach.h"
32 #include "autostart.h"
33 #include "tape.h"
34 #include "debug_gtk3.h"
35 #include "basedialogs.h"
36 #include "contentpreviewwidget.h"
37 #include "diskcontents.h"
38 #include "tapecontents.h"
39 #include "filechooserhelpers.h"
40 #include "ui.h"
41 #include "uimachinewindow.h"
42 #include "lastdir.h"
43 
44 #include "uismartattach.h"
45 
46 
47 /** \brief  File type filters for the dialog
48  */
49 static ui_file_filter_t filters[] = {
50     { "All files", file_chooser_pattern_all },
51     { "Disk images", file_chooser_pattern_disk },
52     { "Tape images", file_chooser_pattern_tape },
53     { "Program files", file_chooser_pattern_program },
54     { "Archives files", file_chooser_pattern_archive },
55     { "Compressed files", file_chooser_pattern_compressed },
56     { NULL, NULL }
57 };
58 
59 
60 /** \brief  Preview widget reference
61  */
62 static GtkWidget *preview_widget = NULL;
63 
64 
65 /** \brief  Last directory used
66  *
67  * When an image is attached, this is set to the directory of that file. Since
68  * it's heap-allocated by Gtk3, it must be freed with a call to
69  * ui_smart_attach_shutdown() on emulator shutdown.
70  */
71 static gchar *last_dir = NULL;
72 
73 
74 #if 0
75 /** \brief  Update the last directory reference
76  *
77  * \param[in]   widget  dialog
78  */
79 static void update_last_dir(GtkWidget *widget)
80 {
81     gchar *new_dir;
82 
83     new_dir = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(widget));
84     debug_gtk3("new dir = '%s'.", new_dir);
85     if (new_dir != NULL) {
86         /* clean up previous value */
87         if (last_dir != NULL) {
88             g_free(last_dir);
89         }
90         last_dir = new_dir;
91     }
92 }
93 #endif
94 
95 
96 /** \brief  Tigger autostart
97  *
98  * \param[in]   widget  dialog
99  */
do_autostart(GtkWidget * widget,gpointer data)100 static void do_autostart(GtkWidget *widget, gpointer data)
101 {
102     gchar *filename;
103     int index = GPOINTER_TO_INT(data);
104 
105     lastdir_update(widget, &last_dir);
106     filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
107     debug_gtk3("Autostarting file '%s'.", filename);
108     /* if this function exists, why is there no attach_autodetect()
109      * or something similar? -- compyx */
110     if (autostart_autodetect(
111                 filename,
112                 NULL,   /* program name */
113                 index,  /* Program number? Probably used when clicking
114                            in the preview widget to load the proper
115                            file in an image */
116                 AUTOSTART_MODE_RUN) < 0) {
117         /* oeps */
118         debug_gtk3("autostart-smart-attach failed.");
119     }
120     g_free(filename);
121     gtk_widget_destroy(widget);
122 }
123 
124 
125 
on_file_activated(GtkWidget * chooser,gpointer data)126 static void on_file_activated(GtkWidget *chooser, gpointer data)
127 {
128     debug_gtk3("I haz called.");
129     do_autostart(chooser, data);
130 }
131 
132 
133 
134 /** \brief  Handler for the "update-preview" event
135  *
136  * \param[in]   chooser file chooser dialog
137  * \param[in]   data    extra event data (unused)
138  */
on_update_preview(GtkFileChooser * chooser,gpointer data)139 static void on_update_preview(GtkFileChooser *chooser, gpointer data)
140 {
141     GFile *file;
142     gchar *path;
143 
144     file = gtk_file_chooser_get_preview_file(chooser);
145     if (file != NULL) {
146         path = g_file_get_path(file);
147         if (path != NULL) {
148             debug_gtk3("called with '%s'.", path);
149 
150             content_preview_widget_set_image(preview_widget, path);
151            g_free(path);
152         }
153         g_object_unref(file);
154     }
155 }
156 
157 /** \brief  Handler for the 'toggled' event of the 'show hidden files' checkbox
158  *
159  * \param[in]   widget      checkbox triggering the event
160  * \param[in]   user_data   data for the event (the dialog)
161  */
on_hidden_toggled(GtkWidget * widget,gpointer user_data)162 static void on_hidden_toggled(GtkWidget *widget, gpointer user_data)
163 {
164     int state;
165 
166     state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
167     debug_gtk3("show hidden files: %s.", state ? "enabled" : "disabled");
168 
169     gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(user_data), state);
170 }
171 
172 
173 
174 /** \brief  Handler for the 'toggled' event of the 'show preview' checkbox
175  *
176  * \param[in]   widget      checkbox triggering the event
177  * \param[in]   user_data   data for the event (unused)
178  */
on_preview_toggled(GtkWidget * widget,gpointer user_data)179 static void on_preview_toggled(GtkWidget *widget, gpointer user_data)
180 {
181 #ifdef HAVE_DEBUG_GTK3UI
182     int state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
183 #endif
184     debug_gtk3("preview %s.", state ? "enabled" : "disabled");
185     /* TODO: actually disable the preview widget and resize the dialog */
186 }
187 
188 
189 /** \brief  Handler for 'response' event of the dialog
190  *
191  * This handler is called when the user clicks a button in the dialog.
192  *
193  * \param[in]   widget      the dialog
194  * \param[in]   response_id response ID
195  * \param[in]   user_data   extra data (unused)
196  *
197  * TODO:    proper (error) messages, which requires implementing ui_error() and
198  *          ui_message() and moving them into gtk3/widgets to avoid circular
199  *          references
200  */
on_response(GtkWidget * widget,gint response_id,gpointer user_data)201 static void on_response(GtkWidget *widget, gint response_id, gpointer user_data)
202 {
203     gchar *filename;
204     int index;
205 
206     index = GPOINTER_TO_INT(user_data);
207 
208     debug_gtk3("got response ID %d, index %d.", response_id, index);
209 
210     switch (response_id) {
211 
212         /* 'Open' button, double-click on file */
213         case GTK_RESPONSE_ACCEPT:
214             lastdir_update(widget, &last_dir);
215             filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
216             /* ui_message("Opening file '%s' ...", filename); */
217             debug_gtk3("Opening file '%s'.", filename);
218 
219             /* copied from Gtk2: I fail to see how brute-forcing your way
220              * through file types is 'smart', but hell, it works */
221             if (file_system_attach_disk(8, filename) < 0
222                     && tape_image_attach(1, filename) < 0
223                     && autostart_snapshot(filename, NULL) < 0
224                     && autostart_prg(filename, AUTOSTART_MODE_LOAD) < 0) {
225                 /* failed */
226                 debug_gtk3("smart attach failed.");
227             }
228 
229             g_free(filename);
230             gtk_widget_destroy(widget);
231             break;
232 
233         /* 'Autostart' button clicked */
234         case VICE_RESPONSE_AUTOSTART:
235             do_autostart(widget, user_data);
236 #if 0
237             lastdir_update(widget, &last_dir);
238             filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
239             debug_gtk3("Autostarting file '%s'.", filename);
240             /* if this function exists, why is there no attach_autodetect()
241              * or something similar? -- compyx */
242             if (autostart_autodetect(
243                         filename,
244                         NULL,   /* program name */
245                         index,  /* Program number? Probably used when clicking
246                                    in the preview widget to load the proper
247                                    file in an image */
248                         AUTOSTART_MODE_RUN) < 0) {
249                 /* oeps */
250                 debug_gtk3("autostart-smart-attach failed.");
251             }
252             g_free(filename);
253             gtk_widget_destroy(widget);
254 #endif
255             break;
256 
257         /* 'Close'/'X' button */
258         case GTK_RESPONSE_REJECT:
259             gtk_widget_destroy(widget);
260             break;
261         default:
262             break;
263     }
264 
265     ui_set_ignore_mouse_hide(FALSE);
266 }
267 
268 
269 /** \brief  Create the 'extra' widget
270  *
271  * \return  GtkGrid
272  */
create_extra_widget(GtkWidget * parent)273 static GtkWidget *create_extra_widget(GtkWidget *parent)
274 {
275     GtkWidget *grid;
276     GtkWidget *hidden_check;
277     GtkWidget *readonly_check;
278     GtkWidget *preview_check;
279 
280     grid = gtk_grid_new();
281     gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
282 
283     hidden_check = gtk_check_button_new_with_label("Show hidden files");
284     g_signal_connect(hidden_check, "toggled", G_CALLBACK(on_hidden_toggled),
285             (gpointer)(parent));
286     gtk_grid_attach(GTK_GRID(grid), hidden_check, 0, 0, 1, 1);
287 
288     readonly_check = gtk_check_button_new_with_label("Attach read-only");
289     gtk_grid_attach(GTK_GRID(grid), readonly_check, 1, 0, 1, 1);
290 
291     preview_check = gtk_check_button_new_with_label("Show image contents");
292     g_signal_connect(preview_check, "toggled", G_CALLBACK(on_preview_toggled),
293             NULL);
294     gtk_grid_attach(GTK_GRID(grid), preview_check, 2, 0, 1, 1);
295 
296     gtk_widget_show_all(grid);
297     return grid;
298 }
299 
300 
301 /** \brief  Wrapper around disk/tape contents readers
302  *
303  * First treats \a path as disk image file and when that fails it falls back
304  * to treating \a path as a tape image, when that fails as well, it gives up.
305  *
306  * \param[in]   path    path to image file
307  *
308  * \return  image contents or `NULL` on failure
309  */
read_contents_wrapper(const char * path)310 static image_contents_t *read_contents_wrapper(const char *path)
311 {
312     image_contents_t *content;
313 
314     /* try disk contents first */
315     content = diskcontents_filesystem_read(path);
316     if (content == NULL) {
317         /* fall back to tape */
318         content = tapecontents_read(path);
319     }
320     return content;
321 }
322 
323 
324 /** \brief  Create the smart-attach dialog
325  *
326  * \param[in]   parent  parent widget, used to get the top level window
327  *
328  * \return  GtkFileChooserDialog
329  */
create_smart_attach_dialog(GtkWidget * parent)330 static GtkWidget *create_smart_attach_dialog(GtkWidget *parent)
331 {
332     GtkWidget *dialog;
333     size_t i;
334 
335     ui_set_ignore_mouse_hide(TRUE);
336 
337     /* create new dialog */
338     dialog = gtk_file_chooser_dialog_new(
339             "Smart-attach a file",
340             ui_get_active_window(),
341             GTK_FILE_CHOOSER_ACTION_OPEN,
342             /* buttons */
343             "Open", GTK_RESPONSE_ACCEPT,
344             "Autostart", VICE_RESPONSE_AUTOSTART,
345             "Close", GTK_RESPONSE_REJECT,
346             NULL, NULL);
347 
348     /* set modal so mouse-grab doesn't get triggered */
349     gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
350 
351     /* set last used directory */
352     lastdir_set(dialog, &last_dir);
353 
354     /* add 'extra' widget: 'readony' and 'show preview' checkboxes */
355     gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(dialog),
356             create_extra_widget(dialog));
357 
358     preview_widget = content_preview_widget_create(dialog,
359             read_contents_wrapper, on_response);
360     gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),
361             preview_widget);
362 
363     /* add filters */
364     for (i = 0; filters[i].name != NULL; i++) {
365         gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog),
366                 create_file_chooser_filter(filters[i], FALSE));
367     }
368 
369     /* connect "reponse" handler: the `user_data` argument gets filled in when
370      * the "response" signal is emitted: a response ID */
371     g_signal_connect(dialog, "response", G_CALLBACK(on_response), NULL);
372     g_signal_connect(dialog, "update-preview",
373             G_CALLBACK(on_update_preview), NULL);
374     g_signal_connect(dialog, "file-activated",
375             G_CALLBACK(on_file_activated), NULL);
376 
377     return dialog;
378 
379 }
380 
381 
382 /** \brief  Callback for the File menu's "smart-attach" item
383  *
384  * Creates the smart-dialog and runs it.
385  *
386  * \param[in]   widget      menu item triggering the callback
387  * \param[in]   user_data   data for the event (unused)
388  */
ui_smart_attach_callback(GtkWidget * widget,gpointer user_data)389 void ui_smart_attach_callback(GtkWidget *widget, gpointer user_data)
390 {
391     GtkWidget *dialog;
392 
393     debug_gtk3("called.");
394 
395     dialog = create_smart_attach_dialog(widget);
396 
397     gtk_widget_show(dialog);
398 
399 }
400 
401 
402 /** \brief  Clean up last used directory string
403  */
ui_smart_attach_shutdown(void)404 void ui_smart_attach_shutdown(void)
405 {
406     lastdir_shutdown(&last_dir);
407 }
408