1 /** \file   uimedia.c
2  * \brief   Media recording dialog
3  *
4  * \author  Bas Wassink <b.wassink@ziggo.nl>
5  */
6 
7 /*
8  * $VICERES SoundRecordDeviceArg        all
9  * $VICERES SoundRecordDeviceName       all
10  * $VICERES DoodleOversizeHandling      -vsid
11  * $VICERES DoodleUndersizeHandling     -vsid
12  * $VICERES DoodleMultiColorHandling    -vsid
13  * $VICERES DoodleTEDLumHandling        -vsid
14  * $VICERES DoodleCRTCTextColor         -vsid
15  * $VICERES KoalaOversizeHandling       -vsid
16  * $VICERES KoalaUndersizeHandling      -vsid
17  * $VICERES KoalaTEDLumHandling         -vsid
18  * $VICERES KoalaCRTCTextColor          -vsid
19  */
20 
21 /*
22  * This file is part of VICE, the Versatile Commodore Emulator.
23  * See README for copyright notice.
24  *
25  *  This program is free software; you can redistribute it and/or modify
26  *  it under the terms of the GNU General Public License as published by
27  *  the Free Software Foundation; either version 2 of the License, or
28  *  (at your option) any later version.
29  *
30  *  This program is distributed in the hope that it will be useful,
31  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
32  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33  *  GNU General Public License for more details.
34  *
35  *  You should have received a copy of the GNU General Public License
36  *  along with this program; if not, write to the Free Software
37  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
38  *  02111-1307  USA.
39  */
40 
41 #include "vice.h"
42 #include <gtk/gtk.h>
43 #include <stdlib.h>
44 #include <string.h>
45 #include <stdint.h>
46 
47 #include "debug_gtk3.h"
48 #include "lib.h"
49 #include "resources.h"
50 #include "machine.h"
51 #include "sound.h"
52 #include "screenshot.h"
53 #include "widgethelpers.h"
54 #include "basewidgets.h"
55 #include "basedialogs.h"
56 #include "openfiledialog.h"
57 #include "savefiledialog.h"
58 #include "selectdirectorydialog.h"
59 #include "ui.h"
60 #include "gfxoutput.h"
61 
62 #ifdef HAVE_FFMPEG
63 #include "ffmpegwidget.h"
64 #endif
65 
66 #include "uimedia.h"
67 
68 
69 /** \brief  Custom response IDs for the dialog
70  */
71 enum {
72     RESPONSE_SAVE = 1   /**< Save button clicked */
73 };
74 
75 
76 /** \brief  Name of the stack child for screenshots
77  */
78 #define CHILD_SCREENSHOT    "Screenshot"
79 
80 /** \brief  Name of the stack child for sound recording
81  */
82 #define CHILD_SOUND         "Sound"
83 
84 /** \brief  Name of the stack child for video recording
85 */
86 #define CHILD_VIDEO         "Video"
87 
88 
89 /** \brief  Struct to hold information on available video recording drivers
90  */
91 typedef struct video_driver_info_s {
92     const char *display;    /**< display string (used in the UI) */
93     const char *name;       /**< driver name */
94     const char *ext;        /**< default file extension */
95 } video_driver_info_t;
96 
97 
98 /** \brief  Struct to hold information on available audio recording drivers
99  */
100 typedef struct audio_driver_info_s {
101     const char *display;    /**< display name (used in the UI) */
102     const char *name;       /**< driver name */
103     const char *ext;        /**< default file extension */
104 } audio_driver_info_t;
105 
106 
107 /** \brief  List of available video drivers, gotten during runtime
108  */
109 static video_driver_info_t *video_driver_list = NULL;
110 
111 
112 /** \brief  Number of available video drivers
113  */
114 static int video_driver_count = 0;
115 
116 
117 /** \brief  Index of currently selected video driver
118  */
119 static int video_driver_index = 0;
120 
121 
122 /** \brief  Index of currently selected audio driver
123  */
124 static int audio_driver_index = 0;
125 
126 
127 /** \brief  List of available audio recording drivers
128  *
129  * This list is dependent on compile-time options
130  */
131 static audio_driver_info_t audio_driver_list[] = {
132     { "WAV", "wav", "wav" },
133     { "AIFF", "aiff", "aif" },
134     { "VOC", "voc", "voc" },
135     { "IFF", "iff", "iff" },
136 #ifdef USE_LAMEMP3
137     { "MP3", "mp3", "mp3" },
138 #endif
139 #ifdef USE_FLAC
140     { "FLAC", "flac", "flac" },
141 #endif
142 #ifdef USE_VORBIS
143     { "Ogg/Vorbis", "ogg", "ogg" },
144 #endif
145     { NULL, NULL, NULL }
146 };
147 
148 
149 /** \brief  List of 'oversize modes' for some screenshot output drivers
150  */
151 static vice_gtk3_combo_entry_int_t oversize_modes[] = {
152     { "scale down", 0 },
153     { "crop left top", 1, },
154     { "crop center top", 2 },
155     { "crop right top", 3 },
156     { "crop left center", 4 },
157     { "crop center", 5 },
158     { "crop right center", 6 },
159     { "crop left bottom (huhu)", 7 },
160     { "crop center bottom", 8 },
161     { "crop right bottom", 9 },
162     { NULL, -1 }
163 };
164 
165 
166 /** \brief  List of 'undersize modes' for some screenshot output drivers
167  */
168 static vice_gtk3_combo_entry_int_t undersize_modes[] = {
169     { "scale up", 0 },
170     { "border size", 1 },
171     { NULL, -1 }
172 };
173 
174 
175 /** \brief  List of multi color modes for some screenshot output drivers
176  */
177 static const vice_gtk3_combo_entry_int_t multicolor_modes[] = {
178     { "B&W", 0 },
179     { "2 colors", 1 },
180     { "4 colors", 2 },
181     { "Grey scale", 3 },
182     { "Best cell colors", 4 },
183     { NULL, -1 }
184 };
185 
186 
187 /** \brief  TED output driver Luma modes
188  */
189 static const vice_gtk3_combo_entry_int_t ted_luma_modes[] = {
190     { "ignore", 0 },
191     { "dither", 1 },
192     { NULL, -1 },
193 };
194 
195 
196 /** \brief  List of available colors to use for CRTC screenshots
197  */
198 static const vice_gtk3_combo_entry_int_t crtc_colors[] = {
199     { "White", 0 },
200     { "Amber", 1 },
201     { "Green", 2 },
202     { NULL, -1 }
203 };
204 
205 
206 /** \brief  Reference to the GtkStack containing the media types
207  *
208  * Used in the dialog response callback to determine recording mode and params
209  */
210 static GtkWidget *stack;
211 
212 /* references to widgets, used from various event handlers */
213 static GtkWidget *screenshot_options_grid = NULL;
214 static GtkWidget *oversize_widget = NULL;
215 static GtkWidget *undersize_widget = NULL;
216 static GtkWidget *multicolor_widget = NULL;
217 static GtkWidget *ted_luma_widget = NULL;
218 static GtkWidget *crtc_textcolor_widget = NULL;
219 static GtkWidget *video_driver_options_grid = NULL;
220 
221 
222 /* forward declarations of helper functions */
223 static GtkWidget *create_screenshot_param_widget(const char *prefix);
224 static void save_screenshot_handler(void);
225 static void save_audio_recording_handler(void);
226 static void save_video_recording_handler(void);
227 
228 
229 /*****************************************************************************
230  *                              Event handlers                               *
231  ****************************************************************************/
232 
233 /** \brief  Handler for the "destroy" event of the dialog
234  *
235  * \param[in]   widget  dialog
236  * \param[in]   data    extra event data (unused)
237  */
on_dialog_destroy(GtkWidget * widget,gpointer data)238 static void on_dialog_destroy(GtkWidget *widget, gpointer data)
239 {
240     if (machine_class != VICE_MACHINE_VSID) {
241         debug_gtk3("called: cleaning up driver list.");
242         lib_free(video_driver_list);
243     }
244     ui_pause_emulation(FALSE);
245 }
246 
247 
248 /** \brief  Set new widget for the screenshot options, replacing the old one
249  *
250  * Destroys any widget present before setting the \a new widget.
251  *
252  * \param[in]   new     new widget
253  */
update_screenshot_options_grid(GtkWidget * new)254 static void update_screenshot_options_grid(GtkWidget *new)
255 {
256     GtkWidget *old = gtk_grid_get_child_at(GTK_GRID(screenshot_options_grid),
257             0, 1);
258     if (old != NULL) {
259         gtk_widget_destroy(old);
260     }
261     gtk_grid_attach(GTK_GRID(screenshot_options_grid), new, 0, 1, 1, 1);
262 }
263 
264 
265 /** \brief  Handler for the "response" event of the \a dialog
266  *
267  * \param[in,out]   dialog      dialog triggering the event
268  * \param[in]       response_id response ID
269  * \param[in]       data        extra event data (unused)
270  */
on_response(GtkDialog * dialog,gint response_id,gpointer data)271 static void on_response(GtkDialog *dialog, gint response_id, gpointer data)
272 {
273     const gchar *child_name;
274 
275     debug_gtk3("got response ID %d.", response_id);
276 
277     switch (response_id) {
278         case GTK_RESPONSE_DELETE_EVENT:
279             debug_gtk3("destroying dialog.");
280             gtk_widget_destroy(GTK_WIDGET(dialog));
281             break;
282 
283         case RESPONSE_SAVE:
284 
285             if (machine_class != VICE_MACHINE_VSID) {
286                 /* stack child name determines what to do next */
287                 child_name = gtk_stack_get_visible_child_name(GTK_STACK(stack));
288                 debug_gtk3("Saving media, tab '%s' selected.", child_name);
289 
290                 if (strcmp(child_name, CHILD_SCREENSHOT) == 0) {
291                     debug_gtk3("Screenshot requested, driver %d.",
292                             video_driver_index);
293                     save_screenshot_handler();
294                 } else if (strcmp(child_name, CHILD_SOUND) == 0) {
295                     debug_gtk3("Audio recording requested, driver %d.",
296                             audio_driver_index);
297                     save_audio_recording_handler();
298                 } else if (strcmp(child_name, CHILD_VIDEO) == 0) {
299                     debug_gtk3("Video recording requested, driver %d.",
300                             video_driver_index);
301                     save_video_recording_handler();
302                 }
303             } else {
304                 debug_gtk3("Audio recording requested, driver %d.",
305                         audio_driver_index);
306                 save_audio_recording_handler();
307             }
308             break;
309 
310 
311         default:
312             break;
313     }
314 }
315 
316 
317 /** \brief  Handler for the "toggled" event of the screenshot driver radios
318  *
319  * \param[in]       widget      radio button riggering the event
320  * \param[in]       data        extra event data (unused)
321  */
on_screenshot_driver_toggled(GtkWidget * widget,gpointer data)322 static void on_screenshot_driver_toggled(GtkWidget *widget, gpointer data)
323 {
324     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
325         int index = GPOINTER_TO_INT(data);
326         debug_gtk3("screenshot driver %d (%s) selected.",
327                 index, video_driver_list[index].name);
328         video_driver_index = index;
329         update_screenshot_options_grid(
330                 create_screenshot_param_widget(video_driver_list[index].name));
331     }
332 }
333 
334 
335 /** \brief  Handler for the "toggled" event of the audio driver radios
336  *
337  * \param[in]       widget      radio button riggering the event
338  * \param[in]       data        extra event data (unused)
339  */
on_audio_driver_toggled(GtkWidget * widget,gpointer data)340 static void on_audio_driver_toggled(GtkWidget *widget, gpointer data)
341 {
342     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
343         int index = GPOINTER_TO_INT(data);
344         debug_gtk3("audio driver %d (%s) selected.",
345                 index, audio_driver_list[index].name);
346         audio_driver_index = index;
347     }
348 }
349 
350 
351 
352 /*****************************************************************************
353  *                              Helpers functions                            *
354  ****************************************************************************/
355 
356 /** \brief  Create a string in the format 'yyyymmddHHMM' of the current time
357  *
358  * \return  string owned by GLib, free with g_free()
359  */
create_datetime_string(void)360 static gchar *create_datetime_string(void)
361 {
362     GDateTime *d;
363     gchar *s;
364 
365     d = g_date_time_new_now_local();
366     s = g_date_time_format(d, "%Y%m%d%H%M");
367     g_date_time_unref(d);
368     return s;
369 }
370 
371 
372 /** \brief  Create a filename based on the current datetime and \a ext
373  *
374  * \param[in]   ext file extension (without the dot)
375  *
376  * \return  heap-allocated string, owned by VICE, free with lib_free()
377  */
create_proposed_screenshot_name(const char * ext)378 static char *create_proposed_screenshot_name(const char *ext)
379 {
380     char *date;
381     char *filename;
382 
383     date = create_datetime_string();
384     filename = lib_msprintf("vice-screen-%s.%s", date, ext);
385     g_free(date);
386     return filename;
387 }
388 
389 
390 
391 /** \brief  Create a filename based on the current datetime and \a ext
392  *
393  * \param[in]   ext file extension (without the dot)
394  *
395  * \return  heap-allocated string, owned by VICE, free with lib_free()
396  */
create_proposed_video_recording_name(const char * ext)397 static char *create_proposed_video_recording_name(const char *ext)
398 {
399     gchar *date;
400     char *filename;
401 
402     date = create_datetime_string();
403     filename = lib_msprintf("vice-video-%s.%s", date, ext);
404     g_free(date);
405     return filename;
406 }
407 
408 
409 
410 /** \brief  Create a filename based on the current datetime and \a ext
411  *
412  * \param[in]   ext file extension (without the dot)
413  *
414  * \return  heap-allocated string, owned by VICE, free with lib_free()
415  */
create_proposed_audio_recording_name(const char * ext)416 static char *create_proposed_audio_recording_name(const char *ext)
417 {
418     gchar *date;
419     char *filename;
420 
421     date = create_datetime_string();
422     filename = lib_msprintf("vice-audio-%s.%s", date, ext);
423     g_free(date);
424     return filename;
425 }
426 
427 
428 
429 /** \brief  Save a screenshot
430  *
431  * Pops up a save-file dialog with a proposed filename (ie
432  * 'screenshot-197411151210.png'.
433  */
save_screenshot_handler(void)434 static void save_screenshot_handler(void)
435 {
436     const char *display;
437     const char *name;
438     const char *ext;
439     gchar *filename;
440     char *title;
441     char *proposed;
442 
443     display = video_driver_list[video_driver_index].display;
444     name = video_driver_list[video_driver_index].name;
445     ext = video_driver_list[video_driver_index].ext;
446 
447     title = lib_msprintf("Save %s file", display);
448     proposed = create_proposed_screenshot_name(ext);
449 
450     filename = vice_gtk3_save_file_dialog(title, proposed, TRUE, NULL);
451     if (filename != NULL) {
452         /* TODO: add extension if not present? */
453         if (screenshot_save(name, filename, ui_get_active_canvas()) < 0) {
454             vice_gtk3_message_error("VICE Error",
455                     "Failed to write screenshot file '%s'", filename);
456         } else {
457             vice_gtk3_message_info("VICE Info",
458                     "Saved screenshot as '%s'", filename);
459         }
460         g_free(filename);
461     }
462     lib_free(proposed);
463     lib_free(title);
464 }
465 
466 
467 /** \brief  Start an audio recording
468  *
469  * Pops up a save-file dialog with a proposed filename (ie
470  * 'audio-recording-197411151210.png'.
471  */
save_audio_recording_handler(void)472 static void save_audio_recording_handler(void)
473 {
474     const char *display;
475     const char *name;
476     const char *ext;
477     gchar *filename;
478     char *title;
479     char *proposed;
480 
481     display = audio_driver_list[audio_driver_index].display;
482     name = audio_driver_list[audio_driver_index].name;
483     ext = audio_driver_list[audio_driver_index].ext;
484 
485     title = lib_msprintf("Save %s file", display);
486     proposed = create_proposed_audio_recording_name(ext);
487 
488     filename = vice_gtk3_save_file_dialog(title, proposed, TRUE, NULL);
489     if (filename != NULL) {
490         /* XXX: setting resources doesn't exactly help with catching errors */
491         resources_set_string("SoundRecordDeviceArg", filename);
492         resources_set_string("SoundRecordDeviceName", name);
493         g_free(filename);
494     }
495 }
496 
497 
498 /** \brief  Start recording a video
499  *
500  * Pops up a save-file dialog with a proposed filename (ie
501  * 'video-recording-197411151210.png'.
502  */
save_video_recording_handler(void)503 static void save_video_recording_handler(void)
504 {
505     /* these may be useful once QuickTime is supported */
506 #if 0
507     const char *display;
508     const char *name;
509 #endif
510     const char *ext;
511     gchar *filename;
512     char *title;
513     char *proposed;
514 
515     debug_gtk3("video driver index = %d.", video_driver_index);
516 
517 #if 0
518     display = video_driver_list[video_driver_index].display;
519     name = video_driver_list[video_driver_index].name;
520 #endif
521     /* we dont' have a format->extension mapping, so the format name itself is
522        better than `video_driver_list[video_driver_index].ext' */
523     resources_get_string("FFMPEGFormat", &ext);
524 
525     title = lib_msprintf("Save %s file", "FFMPEG");
526     proposed = create_proposed_video_recording_name(ext);
527 
528     filename = vice_gtk3_save_file_dialog(title, proposed, TRUE, NULL);
529     if (filename != NULL) {
530 
531         const char *driver;
532         int ac;
533         int vc;
534         int ab;
535         int vb;
536 
537         resources_get_string("FFMPEGFormat", &driver);
538         resources_get_int("FFMPEGVideoCodec", &vc);
539         resources_get_int("FFMPEGVideoBitrate", &vb);
540         resources_get_int("FFMPEGAudioCodec", &ac);
541         resources_get_int("FFMPEGAudioBitrate", &ab);
542 
543         debug_gtk3("Format = '%s'.", driver);
544         debug_gtk3("Video = %d, bitrate %d.", vc, vb);
545         debug_gtk3("Audio = %d, bitrate %d.", ac, ab);
546 
547 
548         ui_pause_emulation(FALSE);
549 
550         /* TODO: add extension if not present? */
551         if (screenshot_save("FFMPEG", filename, ui_get_active_canvas()) < 0) {
552             vice_gtk3_message_error("VICE Error",
553                     "Failed to write video file '%s'", filename);
554         }
555         g_free(filename);
556     }
557     lib_free(proposed);
558     lib_free(title);
559 }
560 
561 
562 /** \brief  Create heap-allocated list of available video drivers
563  *
564  * Queries the gfxoutputdrv subsystem and builds a list of currently available
565  * drivers and stores it in `driver_list`. Must be freed with lib_free() after
566  * use.
567  */
create_video_driver_list(void)568 static void create_video_driver_list(void)
569 {
570     gfxoutputdrv_t *driver;
571     int index;
572 
573     video_driver_count = gfxoutput_num_drivers();
574     debug_gtk3("got %d output drivers.", video_driver_count);
575 
576     video_driver_list = lib_malloc((size_t)(video_driver_count + 1) *
577             sizeof *video_driver_list);
578 
579     index = 0;
580 
581     if (video_driver_count > 0) {
582         driver = gfxoutput_drivers_iter_init();
583         while (driver != NULL) {
584             debug_gtk3(".. adding driver '%s'. ext: %s.",
585 			    driver->name,
586 			    driver->default_extension);
587             video_driver_list[index].display = driver->displayname;
588             video_driver_list[index].name = driver->name;
589             video_driver_list[index].ext = driver->default_extension;
590             index++;
591             driver = gfxoutput_drivers_iter_next();
592         }
593     }
594     video_driver_list[index].display = NULL;
595     video_driver_list[index].name = NULL;
596     video_driver_list[index].ext = NULL;
597 }
598 
599 
600 /** \brief  Determine if driver \a name is a video driver
601  *
602  * \param[in]   name    driver name
603  *
604  * \return  bool
605  *
606  * \todo    There has to be a better, more reliable way than this
607  */
driver_is_video(const char * name)608 static int driver_is_video(const char *name)
609 {
610     return strcmp(name, "FFMPEG") == 0 || strcmp(name, "QuickTime") == 0;
611 }
612 
613 
614 /** \brief  Create widget to control screenshot parameters
615  *
616  * This widget exposes controls to alter screenshot parameters, depending on
617  * machine class and output file format
618  *
619  * \param[in]   prefix  resource prefix (either "Doodle", "Koala", or ""/NULL)
620  */
create_screenshot_param_widget(const char * prefix)621 static GtkWidget *create_screenshot_param_widget(const char *prefix)
622 {
623     GtkWidget *grid;
624     GtkWidget *label;
625     int row;
626     int doodle = 0;
627     int koala = 0;
628 
629     grid = gtk_grid_new();
630     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
631     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
632 
633     /* according to the standard, doing a strcmp() with one or more `NULL`
634      * arguments is implementation-defined, so better safe than sorry */
635     if (prefix == NULL) {
636         debug_gtk3("some idiot passed NULL.");
637         return grid;
638     }
639 
640     if ((strcmp(prefix, "DOODLE") == 0)
641             || (strcmp(prefix, "DOODLE_COMPRESSED") == 0)) {
642         prefix = "Doodle";  /* XXX: not strictly required since resource names
643                                     seem to be case insensitive, but better
644                                     safe than sorry */
645         doodle = 1;
646     } else if ((strcmp(prefix, "KOALA") == 0)
647             || (strcmp(prefix, "KOALA_COMPRESSED") == 0)) {
648         prefix = "Koala";
649         koala = 1;
650     }
651 
652     if (!koala && !doodle) {
653         label = gtk_label_new("No parameters required");
654         g_object_set(label, "margin-left", 16, NULL);
655         gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
656         gtk_widget_show_all(grid);
657         return grid;
658     }
659 
660     /* ${FORMAT}OversizeHandling */
661     label = gtk_label_new("Oversize handling");
662     g_object_set(label, "margin-left", 16, NULL);
663     gtk_widget_set_halign(label, GTK_ALIGN_START);
664     oversize_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
665             "%sOversizeHandling", oversize_modes, prefix);
666     gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
667     gtk_grid_attach(GTK_GRID(grid), oversize_widget, 1, 0, 1, 1);
668 
669     /* ${FORMAT}UndersizeHandling */
670     label = gtk_label_new("Undersize handling");
671     g_object_set(label, "margin-left", 16, NULL);
672     gtk_widget_set_halign(label, GTK_ALIGN_START);
673     undersize_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
674             "%sUndersizeHandling", undersize_modes, prefix);
675     gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
676     gtk_grid_attach(GTK_GRID(grid), undersize_widget, 1, 1, 1, 1);
677 
678     /* ${FORMAT}MultiColorHandling */
679 
680     row = 2;    /* from now on, the widgets depend on machine and image type */
681 
682     /* DoodleMultiColorHandling */
683     if (doodle) {
684         label = gtk_label_new("Multi color handling");
685         g_object_set(label, "margin-left", 16, NULL);
686         gtk_widget_set_halign(label, GTK_ALIGN_START);
687         multicolor_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
688                 "%sMultiColorHandling", multicolor_modes, prefix);
689         gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
690         gtk_grid_attach(GTK_GRID(grid), multicolor_widget, 1, row, 1, 1);
691         row++;
692     }
693 
694     /* ${FORMAT}TEDLumaHandling */
695     if (machine_class == VICE_MACHINE_PLUS4) {
696         label = gtk_label_new("TED luma handling");
697         g_object_set(label, "margin-left", 16, NULL);
698         gtk_widget_set_halign(label, GTK_ALIGN_START);
699         ted_luma_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
700                 "%sTEDLumHandling", ted_luma_modes, prefix);
701         gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
702         gtk_grid_attach(GTK_GRID(grid), ted_luma_widget, 1, row, 1, 1);
703         row++;
704     }
705 
706     /* ${FORMAT}CRTCTextColor */
707     if (machine_class == VICE_MACHINE_PET
708             || machine_class == VICE_MACHINE_CBM6x0) {
709         label = gtk_label_new("CRTC text color");
710         g_object_set(label, "margin-left", 16, NULL);
711         gtk_widget_set_halign(label, GTK_ALIGN_START);
712         crtc_textcolor_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
713                 "%sCRTCTextColor", crtc_colors, prefix);
714         gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
715         gtk_grid_attach(GTK_GRID(grid), crtc_textcolor_widget, 1, row, 1, 1);
716         row++;
717     }
718 
719     gtk_widget_show_all(grid);
720     return grid;
721 }
722 
723 
724 
725 
726 /** \brief  Create the main 'screenshot' widget
727  *
728  * \return  GtkGrid
729  */
create_screenshot_widget(void)730 static GtkWidget *create_screenshot_widget(void)
731 {
732     GtkWidget *grid;
733     GtkWidget *drv_grid;
734     GtkWidget *radio;
735     GtkWidget *last;
736     GSList *group = NULL;
737     int index;
738     int grid_index;
739 
740     grid = gtk_grid_new();
741     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
742     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
743 
744     drv_grid = uihelpers_create_grid_with_label("Driver", 1);
745     grid_index = 1;
746     last = NULL;
747     for (index = 0; video_driver_list[index].name != NULL; index++) {
748         const char *display = video_driver_list[index].display;
749         const char *name = video_driver_list[index].name;
750 
751         if (!driver_is_video(name)) {
752             radio = gtk_radio_button_new_with_label(group, display);
753             g_object_set(radio, "margin-left", 8, NULL);
754             gtk_radio_button_join_group(GTK_RADIO_BUTTON(radio),
755                     GTK_RADIO_BUTTON(last));
756             gtk_grid_attach(GTK_GRID(drv_grid), radio, 0, grid_index, 1, 1);
757 
758             g_signal_connect(radio, "toggled",
759                     G_CALLBACK(on_screenshot_driver_toggled),
760                     GINT_TO_POINTER(index));
761 
762             last = radio;
763             grid_index++;
764         }
765     }
766 
767     /* this is where the various options go per screenshot driver (for example
768      * Koala or Doodle) */
769     screenshot_options_grid = uihelpers_create_grid_with_label(
770             "Driver options", 1);
771 
772     gtk_grid_attach(GTK_GRID(grid), drv_grid, 0, 0, 1, 1);
773     gtk_grid_attach(GTK_GRID(grid), screenshot_options_grid, 1, 0, 1, 1);
774 
775     update_screenshot_options_grid(create_screenshot_param_widget(""));
776 
777     gtk_widget_show_all(grid);
778     return grid;
779 }
780 
781 
782 /** \brief  Create the main 'sound recording' widget
783  *
784  * \return  GtkGrid
785  */
create_sound_widget(void)786 static GtkWidget *create_sound_widget(void)
787 {
788     GtkWidget *grid;
789     GtkWidget *drv_grid;
790     GtkWidget *radio;
791     GtkWidget *last;
792     GSList *group = NULL;
793     int index;
794 
795     grid = gtk_grid_new();
796     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
797     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
798 
799     drv_grid = uihelpers_create_grid_with_label("Driver", 1);
800     last = NULL;
801     for (index = 0; audio_driver_list[index].name != NULL; index++) {
802         const char *display = audio_driver_list[index].display;
803 
804         radio = gtk_radio_button_new_with_label(group, display);
805         g_object_set(radio, "margin-left", 8, NULL);
806         gtk_radio_button_join_group(GTK_RADIO_BUTTON(radio),
807                 GTK_RADIO_BUTTON(last));
808         gtk_grid_attach(GTK_GRID(drv_grid), radio, 0, index + 1, 1, 1);
809 
810         g_signal_connect(radio, "toggled",
811                 G_CALLBACK(on_audio_driver_toggled),
812                 GINT_TO_POINTER(index));
813 
814         last = radio;
815     }
816 
817     gtk_grid_attach(GTK_GRID(grid), drv_grid, 0, 0, 1, 1);
818 
819     gtk_widget_show_all(grid);
820     return grid;
821 }
822 
823 
824 /** \brief  Create the main 'video recording' widget
825  *
826  * \return  GtkGrid
827  */
create_video_widget(void)828 static GtkWidget *create_video_widget(void)
829 {
830     GtkWidget *grid;
831     GtkWidget *label;
832     GtkWidget *combo;
833     int index;
834 
835     GtkWidget *selection_grid;
836     GtkWidget *options_grid;
837 
838     grid = gtk_grid_new();
839     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
840     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
841 
842     label = gtk_label_new("Video driver");
843     g_object_set(label, "margin-left", 16, NULL);
844 
845     combo = gtk_combo_box_text_new();
846     for (index = 0; video_driver_list[index].name != NULL; index++) {
847         const char *display = video_driver_list[index].display;
848         const char *name = video_driver_list[index].name;
849 
850         if (driver_is_video(name)) {
851             gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo), name, display);
852         }
853     }
854     gtk_widget_set_hexpand(combo, TRUE);
855     gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);
856 
857     selection_grid = uihelpers_create_grid_with_label("Driver selection", 2);
858     gtk_grid_set_column_spacing(GTK_GRID(selection_grid), 16);
859     gtk_grid_set_row_spacing(GTK_GRID(selection_grid), 8);
860     gtk_grid_attach(GTK_GRID(selection_grid), label, 0, 1, 1, 1);
861     gtk_grid_attach(GTK_GRID(selection_grid), combo, 1, 1, 1, 1);
862     gtk_widget_show_all(selection_grid);
863 
864     gtk_grid_attach(GTK_GRID(grid), selection_grid, 0, 0, 1, 1);
865 
866     /* grid around ffmpeg/quicktime options */
867     options_grid = uihelpers_create_grid_with_label("Driver options", 1);
868     gtk_grid_set_column_spacing(GTK_GRID(options_grid), 16);
869     gtk_grid_set_row_spacing(GTK_GRID(options_grid), 8);
870 
871 /* XXX: this obviously needs a cleaner solution which also handles QuickTime
872  *      on MacOS
873  */
874 #ifdef HAVE_FFMPEG
875     gtk_grid_attach(GTK_GRID(options_grid), ffmpeg_widget_create(), 0, 1, 1,1);
876 #endif
877     video_driver_options_grid = options_grid;
878 
879 
880 
881     gtk_grid_attach(GTK_GRID(grid), options_grid, 0, 1, 1, 1);
882 
883 
884     gtk_widget_show_all(grid);
885     return grid;
886 }
887 
888 
889 /** \brief  Create the content widget for the dialog
890  *
891  * Contains a GtkStack and GtkStackSwitcher to switch between 'screenshot',
892  * 'sound' and 'video' sub widgets.
893  *
894  * \return  GtkGrid
895  */
create_content_widget(void)896 static GtkWidget *create_content_widget(void)
897 {
898     GtkWidget *grid;
899     GtkWidget *switcher;
900 
901     grid = gtk_grid_new();
902     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
903     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
904 
905     stack = gtk_stack_new();
906     gtk_stack_add_titled(GTK_STACK(stack), create_screenshot_widget(),
907             CHILD_SCREENSHOT, "Screenshot");
908     gtk_stack_add_titled(GTK_STACK(stack), create_sound_widget(),
909             CHILD_SOUND, "Sound recording");
910     gtk_stack_add_titled(GTK_STACK(stack), create_video_widget(),
911             CHILD_VIDEO, "Video recording");
912     gtk_stack_set_transition_type(GTK_STACK(stack),
913             GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
914     gtk_stack_set_transition_duration(GTK_STACK(stack), 1000);
915     /* avoid resizing the dialog when switching */
916     gtk_stack_set_homogeneous(GTK_STACK(stack), TRUE);
917 
918     switcher = gtk_stack_switcher_new();
919     gtk_widget_set_halign(switcher, GTK_ALIGN_CENTER);
920     gtk_widget_set_hexpand(switcher, TRUE);
921     gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(switcher), GTK_STACK(stack));
922     /* make all titles of the switcher the same size */
923     gtk_box_set_homogeneous(GTK_BOX(switcher), TRUE);
924 
925     gtk_widget_show_all(stack);
926     gtk_widget_show_all(switcher);
927 
928     gtk_grid_attach(GTK_GRID(grid), switcher, 0, 0, 1, 1);
929     gtk_grid_attach(GTK_GRID(grid), stack, 0, 1, 1, 1);
930 
931     gtk_widget_show_all(grid);
932     return grid;
933 }
934 
935 
936 
937 /** \brief  Show dialog to save screenshot, record sound and/or video
938  *
939  * \param[in]   parent  parent widget (unused)
940  * \param[in]   data    extra data (unused)
941  */
uimedia_dialog_show(GtkWidget * parent,gpointer data)942 void uimedia_dialog_show(GtkWidget *parent, gpointer data)
943 {
944     GtkWidget *dialog;
945     GtkWidget *content;
946 
947     /*
948      * Pause emulation, the ui_emulation_is_paused() check is required since
949      * the ui_pause_emulation() function is a little weird: when passed FALSE
950      * it unpauses, when passed TRUE it toggles the paused state.
951      */
952     if (!ui_emulation_is_paused()) {
953         ui_pause_emulation(TRUE);
954     }
955 
956     /* create driver list */
957     if (machine_class != VICE_MACHINE_VSID) {
958         create_video_driver_list();
959     }
960 
961     dialog = gtk_dialog_new_with_buttons(
962             "Record media file",
963             ui_get_active_window(),
964             GTK_DIALOG_MODAL,
965             "Save", RESPONSE_SAVE,
966             "Close", GTK_RESPONSE_DELETE_EVENT,
967             NULL);
968 
969     /* add content widget */
970     content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
971     if (machine_class != VICE_MACHINE_VSID) {
972         gtk_container_add(GTK_CONTAINER(content), create_content_widget());
973     } else {
974         gtk_container_add(GTK_CONTAINER(content), create_sound_widget());
975     }
976 
977     gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
978     g_signal_connect(dialog, "response", G_CALLBACK(on_response), NULL);
979     g_signal_connect(dialog, "destroy", G_CALLBACK(on_dialog_destroy), NULL);
980 
981     gtk_widget_show_all(dialog);
982 }
983 
984 
985 /** \brief  Stop audio or video recording, if active
986  *
987  * \return  TRUE, so the emulated machine doesn't get the shortcut key
988  */
uimedia_stop_recording(GtkWidget * parent,gpointer data)989 gboolean uimedia_stop_recording(GtkWidget *parent, gpointer data)
990 {
991     debug_gtk3("Stopping media recording.");
992 
993     /* stop sound recording, if active */
994     if (sound_is_recording()) {
995         sound_stop_recording();
996     }
997     /* stop video recording */
998     if (screenshot_is_recording()) {
999         screenshot_stop_recording();
1000     }
1001     return TRUE;
1002 }
1003