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 OCPOversizeHandling         -vsid
11  * $VICERES OCPUndersizeHandling        -vsid
12  * $VICERES OCPMultiColorHandling       -vsid
13  * $VICERES OCPTEDLumHandling           -vsid
14  * $VICERES KoalaOversizeHandling       -vsid
15  * $VICERES KoalaUndersizeHandling      -vsid
16  * $VICERES KoalaTEDLumHandling         -vsid
17  */
18 
19 /*
20  * This file is part of VICE, the Versatile Commodore Emulator.
21  * See README for copyright notice.
22  *
23  *  This program is free software; you can redistribute it and/or modify
24  *  it under the terms of the GNU General Public License as published by
25  *  the Free Software Foundation; either version 2 of the License, or
26  *  (at your option) any later version.
27  *
28  *  This program is distributed in the hope that it will be useful,
29  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
30  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
31  *  GNU General Public License for more details.
32  *
33  *  You should have received a copy of the GNU General Public License
34  *  along with this program; if not, write to the Free Software
35  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
36  *  02111-1307  USA.
37  */
38 
39 #include "vice.h"
40 #include <gtk/gtk.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <stdint.h>
44 
45 #include "gfxoutput.h"
46 #include "lib.h"
47 #include "machine.h"
48 #include "mainlock.h"
49 #include "openfiledialog.h"
50 #include "resources.h"
51 #include "savefiledialog.h"
52 #include "screenshot.h"
53 #include "selectdirectorydialog.h"
54 #include "sound.h"
55 #include "statusbarrecordingwidget.h"
56 #include "ui.h"
57 #include "uiapi.h"
58 #include "uistatusbar.h"
59 #include "vice_gtk3.h"
60 
61 #ifdef HAVE_FFMPEG
62 #include "ffmpegwidget.h"
63 #endif
64 
65 #include "uimedia.h"
66 
67 
68 /** \brief  Custom response IDs for the dialog
69  */
70 enum {
71     RESPONSE_SAVE = 1   /**< Save button clicked */
72 };
73 
74 
75 /** \brief  Name of the stack child for screenshots
76  */
77 #define CHILD_SCREENSHOT    "Screenshot"
78 
79 /** \brief  Name of the stack child for sound recording
80  */
81 #define CHILD_SOUND         "Sound"
82 
83 /** \brief  Name of the stack child for video recording
84 */
85 #define CHILD_VIDEO         "Video"
86 
87 
88 /** \brief  Struct to hold information on available video recording drivers
89  */
90 typedef struct video_driver_info_s {
91     const char *display;    /**< display string (used in the UI) */
92     const char *name;       /**< driver name */
93     const char *ext;        /**< default file extension */
94 } video_driver_info_t;
95 
96 
97 /** \brief  Struct to hold information on available audio recording drivers
98  */
99 typedef struct audio_driver_info_s {
100     const char *display;    /**< display name (used in the UI) */
101     const char *name;       /**< driver name */
102     const char *ext;        /**< default file extension */
103 } audio_driver_info_t;
104 
105 
106 /** \brief  List of available video drivers, gotten during runtime
107  */
108 static video_driver_info_t *video_driver_list = NULL;
109 
110 
111 /** \brief  Number of available video drivers
112  */
113 static int video_driver_count = 0;
114 
115 
116 /** \brief  Index of currently selected screenshot driver
117  *
118  * Initially set to -1 to select PNG as default in the screenshot driver list.
119  * Set to last selected driver on subsequent uses of the dialog.
120  */
121 static int screenshot_driver_index = -1;
122 
123 
124 #ifdef HAVE_FFMPEG
125 /** \brief  Index of currently selected video driver
126  *
127  */
128 static int video_driver_index = 0;
129 #endif  /* HAVE_FFMPEG */
130 
131 
132 /** \brief  Index of currently selected audio driver
133  *
134  * Initially set to -1 to select WAV as default in the audio driver list.
135  * Set to last selected driver on subsequent uses of the dialog.
136  */
137 static int audio_driver_index = -1;
138 
139 
140 /** \brief  List of available audio recording drivers
141  *
142  * This list is dependent on compile-time options
143  */
144 static audio_driver_info_t audio_driver_list[] = {
145     { "WAV",        "wav",  "wav" },
146     { "AIFF",       "aiff", "aif" },
147     { "VOC",        "voc",  "voc" },
148     { "IFF",        "iff",  "iff" },
149 #ifdef USE_LAMEMP3
150     { "MP3",        "mp3",  "mp3" },
151 #endif
152 #ifdef USE_FLAC
153     { "FLAC",       "flac", "flac" },
154 #endif
155 #ifdef USE_VORBIS
156     { "Ogg/Vorbis", "ogg",  "ogg" },
157 #endif
158     { NULL,         NULL,   NULL }
159 };
160 
161 
162 /** \brief  List of 'oversize modes' for some screenshot output drivers
163  */
164 static vice_gtk3_combo_entry_int_t oversize_modes[] = {
165     { "scale down", 0 },
166     { "crop left top", 1, },
167     { "crop center top", 2 },
168     { "crop right top", 3 },
169     { "crop left center", 4 },
170     { "crop center", 5 },
171     { "crop right center", 6 },
172     { "crop left bottom", 7 },
173     { "crop center bottom", 8 },
174     { "crop right bottom", 9 },
175     { NULL, -1 }
176 };
177 
178 
179 /** \brief  List of 'undersize modes' for some screenshot output drivers
180  */
181 static vice_gtk3_combo_entry_int_t undersize_modes[] = {
182     { "scale up", 0 },
183     { "border size", 1 },
184     { NULL, -1 }
185 };
186 
187 
188 /** \brief  List of multi color modes for some screenshot output drivers
189  */
190 static const vice_gtk3_combo_entry_int_t multicolor_modes[] = {
191     { "B&W", 0 },
192     { "2 colors", 1 },
193     { "4 colors", 2 },
194     { "Grey scale", 3 },
195     { "Best cell colors", 4 },
196     { NULL, -1 }
197 };
198 
199 
200 /** \brief  TED output driver Luma modes
201  */
202 static const vice_gtk3_combo_entry_int_t ted_luma_modes[] = {
203     { "ignore", 0 },
204     { "Best cell colors", 1 },
205     { NULL, -1 },
206 };
207 
208 
209 /* forward declarations of helper functions */
210 static GtkWidget *create_screenshot_param_widget(const char *prefix);
211 static void save_screenshot_handler(GtkWidget *parent);
212 static void save_audio_recording_handler(GtkWidget *parent);
213 static void save_video_recording_handler(GtkWidget *parent);
214 
215 
216 /** \brief  Reference to the GtkStack containing the media types
217  *
218  * Used in the dialog response callback to determine recording mode and params
219  */
220 static GtkWidget *stack;
221 
222 
223 /** \brief  Pause state when activating the dialog
224  */
225 static int old_pause_state = 0;
226 
227 
228 /* references to widgets, used from various event handlers */
229 
230 /** \brief  Screenshot options grid reference */
231 static GtkWidget *screenshot_options_grid = NULL;
232 /** \brief  Screenshot "Oversize" widget reference */
233 static GtkWidget *oversize_widget = NULL;
234 /** \brief  Screenshot "Undersize" widget reference */
235 static GtkWidget *undersize_widget = NULL;
236 /** \brief  Screenshot "Multicolor mode" widget reference */
237 static GtkWidget *multicolor_widget = NULL;
238 /** \brief  Screenshot "TED luma" widget reference */
239 static GtkWidget *ted_luma_widget = NULL;
240 #ifdef HAVE_FFMPEG
241 /** \brief  FFMPEG video driver options grid reference */
242 static GtkWidget *video_driver_options_grid = NULL;
243 #endif
244 
245 
246 /*****************************************************************************
247  *                              Event handlers                               *
248  ****************************************************************************/
249 
250 /** \brief  Handler for the "destroy" event of the dialog
251  *
252  * \param[in]   widget  dialog
253  * \param[in]   data    extra event data (unused)
254  */
on_dialog_destroy(GtkWidget * widget,gpointer data)255 static void on_dialog_destroy(GtkWidget *widget, gpointer data)
256 {
257     if (machine_class != VICE_MACHINE_VSID) {
258         lib_free(video_driver_list);
259     }
260 
261     /* only unpause when the emu wasn't paused when activating the dialog */
262     if (!old_pause_state) {
263         ui_pause_disable();
264     }
265 }
266 
267 
268 /** \brief  Set new widget for the screenshot options, replacing the old one
269  *
270  * Destroys any widget present before setting the \a new widget.
271  *
272  * \param[in]   new     new widget
273  */
update_screenshot_options_grid(GtkWidget * new)274 static void update_screenshot_options_grid(GtkWidget *new)
275 {
276     GtkWidget *old;
277 
278     if (new != NULL) {
279         old = gtk_grid_get_child_at(GTK_GRID(screenshot_options_grid), 0, 1);
280         if (old != NULL) {
281             gtk_widget_destroy(old);
282         }
283         gtk_grid_attach(GTK_GRID(screenshot_options_grid), new, 0, 1, 1, 1);
284     }
285 }
286 
287 
288 /** \brief  Handler for the "response" event of the \a dialog
289  *
290  * \param[in,out]   dialog      dialog triggering the event
291  * \param[in]       response_id response ID
292  * \param[in]       data        parent dialog
293  */
on_response(GtkDialog * dialog,gint response_id,gpointer data)294 static void on_response(GtkDialog *dialog, gint response_id, gpointer data)
295 {
296     const gchar *child_name;
297 
298     switch (response_id) {
299         case GTK_RESPONSE_DELETE_EVENT:
300             mainlock_release();
301             gtk_widget_destroy(GTK_WIDGET(dialog));
302             mainlock_obtain();
303             break;
304 
305         case RESPONSE_SAVE:
306 
307             if (machine_class != VICE_MACHINE_VSID) {
308                 /* stack child name determines what to do next */
309                 child_name = gtk_stack_get_visible_child_name(GTK_STACK(stack));
310 
311                 if (strcmp(child_name, CHILD_SCREENSHOT) == 0) {
312                     save_screenshot_handler(data);
313                 } else if (strcmp(child_name, CHILD_SOUND) == 0) {
314                     save_audio_recording_handler(data);
315                     ui_display_recording(1);
316                 } else if (strcmp(child_name, CHILD_VIDEO) == 0) {
317                     save_video_recording_handler(data);
318                     ui_display_recording(1);
319                 }
320             } else {
321                 save_audio_recording_handler(data);
322                 ui_display_recording(1);
323             }
324 #if 0
325             mainlockk_release();
326             gtk_widget_destroy(GTK_WIDGET(dialog));
327             mainlock_obtain();
328 #endif
329             break;
330 
331         default:
332             break;
333     }
334 }
335 
336 
337 /** \brief  Handler for the "toggled" event of the screenshot driver radios
338  *
339  * \param[in]       widget      radio button riggering the event
340  * \param[in]       data        extra event data (unused)
341  */
on_screenshot_driver_toggled(GtkWidget * widget,gpointer data)342 static void on_screenshot_driver_toggled(GtkWidget *widget, gpointer data)
343 {
344     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
345         int index = GPOINTER_TO_INT(data);
346         screenshot_driver_index = index;
347         update_screenshot_options_grid(
348                 create_screenshot_param_widget(video_driver_list[index].name));
349     }
350 }
351 
352 
353 /** \brief  Handler for the "toggled" event of the audio driver radios
354  *
355  * \param[in]       widget      radio button riggering the event
356  * \param[in]       data        extra event data (unused)
357  */
on_audio_driver_toggled(GtkWidget * widget,gpointer data)358 static void on_audio_driver_toggled(GtkWidget *widget, gpointer data)
359 {
360     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
361         int index = GPOINTER_TO_INT(data);
362         audio_driver_index = index;
363     }
364 }
365 
366 
367 
368 /*****************************************************************************
369  *                              Helpers functions                            *
370  ****************************************************************************/
371 
372 /** \brief  Create a string in the format 'yyyymmddHHMMssffffff' of the current time
373  *
374  * \return  string owned by GLib, free with g_free()
375  */
create_datetime_string(void)376 static gchar *create_datetime_string(void)
377 {
378     GDateTime *d;
379     gint m;
380     gchar *s;
381     gchar *t;
382 
383     d = g_date_time_new_now_local();
384     m = g_date_time_get_microsecond(d);
385     s = g_date_time_format(d, "%Y%m%d%H%M%S");
386     g_date_time_unref(d);
387     t = g_strdup_printf("%s%06d", s, m);
388     g_free(s);
389     return t;
390 }
391 
392 
393 /** \brief  Create a filename based on the current datetime and \a ext
394  *
395  * \param[in]   ext file extension (without the dot)
396  *
397  * \return  heap-allocated string, owned by VICE, free with lib_free()
398  */
create_proposed_screenshot_name(const char * ext)399 static char *create_proposed_screenshot_name(const char *ext)
400 {
401     char *date;
402     char *filename;
403 
404     date = create_datetime_string();
405     filename = lib_msprintf("vice-screen-%s.%s", date, ext);
406     g_free(date);
407     return filename;
408 }
409 
410 
411 
412 /** \brief  Create a filename based on the current datetime and \a ext
413  *
414  * \param[in]   ext file extension (without the dot)
415  *
416  * \return  heap-allocated string, owned by VICE, free with lib_free()
417  */
create_proposed_video_recording_name(const char * ext)418 static char *create_proposed_video_recording_name(const char *ext)
419 {
420     gchar *date;
421     char *filename;
422 
423     date = create_datetime_string();
424     filename = lib_msprintf("vice-video-%s.%s", date, ext);
425     g_free(date);
426     return filename;
427 }
428 
429 
430 
431 /** \brief  Create a filename based on the current datetime and \a ext
432  *
433  * \param[in]   ext file extension (without the dot)
434  *
435  * \return  heap-allocated string, owned by VICE, free with lib_free()
436  */
create_proposed_audio_recording_name(const char * ext)437 static char *create_proposed_audio_recording_name(const char *ext)
438 {
439     gchar *date;
440     char *filename;
441 
442     date = create_datetime_string();
443     filename = lib_msprintf("vice-audio-%s.%s", date, ext);
444     g_free(date);
445     return filename;
446 }
447 
448 
449 /** \brief  Callback for the save-screenshot dialog
450  *
451  * \param[in,out]   dialog      dialog
452  * \param[in,out]   filename    screenshot filename
453  * \param[in]       data        extra data (unused)
454  */
on_save_screenshot_filename(GtkDialog * dialog,gchar * filename,gpointer data)455 static void on_save_screenshot_filename(GtkDialog *dialog,
456                                         gchar *filename,
457                                         gpointer data)
458 {
459     const char *name;
460 
461     name = video_driver_list[screenshot_driver_index].name;
462 
463     if (filename != NULL) {
464 
465         gchar *filename_locale = file_chooser_convert_to_locale(filename);
466 
467         /* TODO: add extension if not present? */
468         if (screenshot_save(name, filename_locale, ui_get_active_canvas()) < 0) {
469             vice_gtk3_message_error("VICE Error",
470                     "Failed to write screenshot file '%s'", filename);
471         }
472         g_free(filename);
473         g_free(filename_locale);
474     }
475     mainlock_release();
476     gtk_widget_destroy(GTK_WIDGET(dialog));
477     mainlock_obtain();
478 }
479 
480 
481 
482 /** \brief  Save a screenshot
483  *
484  * Pops up a save-file dialog with a proposed filename (ie
485  * 'screenshot-197411151210.png'.
486  *
487  * \note    Destroys \a parent along with the dialog
488  *
489  * \param[in,out]   parent  parent dialog
490  */
save_screenshot_handler(GtkWidget * parent)491 static void save_screenshot_handler(GtkWidget *parent)
492 {
493     GtkWidget *dialog;
494     const char *display;
495     char *title;
496     char *proposed;
497     const char *ext;
498 
499     ext = video_driver_list[screenshot_driver_index].ext;
500     display = video_driver_list[screenshot_driver_index].display;
501     title = lib_msprintf("Save %s file", display);
502     proposed = create_proposed_screenshot_name(ext);
503 
504     dialog = vice_gtk3_save_file_dialog(
505             title, proposed, TRUE, NULL,
506             on_save_screenshot_filename,
507             NULL);
508     /* destroy parent dialog when the dialog is destroyed */
509     g_signal_connect_swapped(
510             dialog,
511             "destroy",
512             G_CALLBACK(gtk_widget_destroy),
513             parent);
514 
515     lib_free(proposed);
516     lib_free(title);
517 }
518 
519 
520 /** \brief  Callback for the save-audio dialog
521  *
522  * \param[in,out]   dialog      dialog
523  * \param[in,out]   filename    audio recording filename
524  * \param[in]       data        extra data (unused)
525  */
on_save_audio_filename(GtkDialog * dialog,gchar * filename,gpointer data)526 static void on_save_audio_filename(GtkDialog *dialog,
527                                    gchar *filename,
528                                    gpointer data)
529 {
530     gchar *filename_locale;
531 
532     if (filename != NULL) {
533         filename_locale = file_chooser_convert_to_locale(filename);
534         const char *name = audio_driver_list[audio_driver_index].name;
535         /* XXX: setting resources doesn't exactly help with catching errors */
536         resources_set_string("SoundRecordDeviceArg", filename_locale);
537         resources_set_string("SoundRecordDeviceName", name);
538         g_free(filename);
539         g_free(filename_locale);
540     }
541     mainlock_release();
542     gtk_widget_destroy(GTK_WIDGET(dialog));
543     mainlock_obtain();
544 }
545 
546 
547 
548 /** \brief  Start an audio recording
549  *
550  * Pops up a save-file dialog with a proposed filename (ie
551  * 'audio-recording-197411151210.png'.
552  *
553  * \note    Destroys \a parent along with the dialog
554  *
555  * \param[in,out]   parent dialog
556  */
save_audio_recording_handler(GtkWidget * parent)557 static void save_audio_recording_handler(GtkWidget *parent)
558 {
559     GtkWidget *dialog;
560     const char *display;
561     const char *ext;
562     char *title;
563     char *proposed;
564 
565     ext = audio_driver_list[audio_driver_index].ext;
566     display = audio_driver_list[audio_driver_index].display;
567     title = lib_msprintf("Save %s file", display);
568     proposed = create_proposed_audio_recording_name(ext);
569 
570     dialog = vice_gtk3_save_file_dialog(
571             title, proposed, TRUE, NULL,
572             on_save_audio_filename,
573             NULL);
574 
575     /* destroy parent dialog when the dialog is destroyed */
576     g_signal_connect_swapped(
577             dialog,
578             "destroy",
579             G_CALLBACK(gtk_widget_destroy),
580             parent);
581 
582     lib_free(title);
583     lib_free(proposed);
584 }
585 
586 
587 
588 /** \brief  Callback for the save-video dialog
589  *
590  * \param[in,out]   dialog      dialog
591  * \param[in,out]   filename    video recording filename
592  * \param[in]       data        extra data (unused)
593  */
on_save_video_filename(GtkDialog * dialog,gchar * filename,gpointer data)594 static void on_save_video_filename(GtkDialog *dialog,
595                                    gchar *filename,
596                                    gpointer data)
597 {
598     if (filename != NULL) {
599 
600         const char *driver;
601         int ac;
602         int vc;
603         int ab;
604         int vb;
605         gchar *filename_locale;
606 
607         resources_get_string("FFMPEGFormat", &driver);
608         resources_get_int("FFMPEGVideoCodec", &vc);
609         resources_get_int("FFMPEGVideoBitrate", &vb);
610         resources_get_int("FFMPEGAudioCodec", &ac);
611         resources_get_int("FFMPEGAudioBitrate", &ab);
612 
613         debug_gtk3("Format = '%s'.", driver);
614         debug_gtk3("Video = %d, bitrate %d.", vc, vb);
615         debug_gtk3("Audio = %d, bitrate %d.", ac, ab);
616 
617         ui_pause_disable();
618 
619         filename_locale = file_chooser_convert_to_locale(filename);
620 
621         /* TODO: add extension if not present? */
622         if (screenshot_save("FFMPEG", filename_locale, ui_get_active_canvas()) < 0) {
623             vice_gtk3_message_error("VICE Error",
624                     "Failed to write video file '%s'", filename);
625         }
626         g_free(filename);
627         g_free(filename_locale);
628     }
629     mainlock_release();
630     gtk_widget_destroy(GTK_WIDGET(dialog));
631     mainlock_obtain();
632 }
633 
634 
635 /** \brief  Start recording a video
636  *
637  * Pops up a save-file dialog with a proposed filename (ie
638  * 'video-recording-197411151210.png'.
639  *
640  * \note    Destroys \a parent along with the dialog
641  *
642  * \param[in,out]   parent  parent dialog
643  */
save_video_recording_handler(GtkWidget * parent)644 static void save_video_recording_handler(GtkWidget *parent)
645 {
646     GtkWidget *dialog;
647     const char *ext;
648     char *title;
649     char *proposed;
650 
651 #if 0
652     display = video_driver_list[video_driver_index].display;
653     name = video_driver_list[video_driver_index].name;
654 #endif
655     /* we don't have a format->extension mapping, so the format name itself is
656        better than `video_driver_list[video_driver_index].ext' */
657     resources_get_string("FFMPEGFormat", &ext);
658 
659     title = lib_msprintf("Save %s file", "FFMPEG");
660     proposed = create_proposed_video_recording_name(ext);
661 
662     dialog = vice_gtk3_save_file_dialog(
663             title, proposed, TRUE, NULL,
664             on_save_video_filename, NULL);
665 
666     /* destroy parent dialog when the dialog is destroyed */
667     g_signal_connect_swapped(
668             dialog,
669             "destroy",
670             G_CALLBACK(gtk_widget_destroy),
671             parent);
672 
673     lib_free(proposed);
674     lib_free(title);
675 }
676 
677 
678 /** \brief  Create heap-allocated list of available video drivers
679  *
680  * Queries the gfxoutputdrv subsystem and builds a list of currently available
681  * drivers and stores it in `driver_list`. Must be freed with lib_free() after
682  * use.
683  */
create_video_driver_list(void)684 static void create_video_driver_list(void)
685 {
686     gfxoutputdrv_t *driver;
687     int index;
688 
689     video_driver_count = gfxoutput_num_drivers();
690     video_driver_list = lib_malloc((size_t)(video_driver_count + 1) *
691             sizeof *video_driver_list);
692 
693     index = 0;
694 
695     if (video_driver_count > 0) {
696         driver = gfxoutput_drivers_iter_init();
697         while (driver != NULL) {
698             video_driver_list[index].display = driver->displayname;
699             video_driver_list[index].name = driver->name;
700             video_driver_list[index].ext = driver->default_extension;
701             index++;
702             driver = gfxoutput_drivers_iter_next();
703         }
704     }
705     video_driver_list[index].display = NULL;
706     video_driver_list[index].name = NULL;
707     video_driver_list[index].ext = NULL;
708 }
709 
710 
711 /** \brief  Determine if driver \a name is a video driver
712  *
713  * \param[in]   name    driver name
714  *
715  * \return  bool
716  *
717  * \todo    There has to be a better, more reliable way than this
718  */
driver_is_video(const char * name)719 static int driver_is_video(const char *name)
720 {
721     int result;
722 
723     result = strcmp(name, "FFMPEG") == 0;
724     return result;
725 }
726 
727 
728 /** \brief  Create widget to control screenshot parameters
729  *
730  * This widget exposes controls to alter screenshot parameters, depending on
731  * machine class and output file format
732  *
733  * \param[in]   prefix  resource prefix (either "OCP", "Koala", or ""/NULL)
734  *
735  * \return  GtkGrid
736  */
create_screenshot_param_widget(const char * prefix)737 static GtkWidget *create_screenshot_param_widget(const char *prefix)
738 {
739     GtkWidget *grid;
740     GtkWidget *label;
741     int row;
742     int artstudio = 0;
743     int koala = 0;
744 
745     grid = gtk_grid_new();
746     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
747     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
748 
749     /* according to the standard, doing a strcmp() with one or more `NULL`
750      * arguments is implementation-defined, so better safe than sorry */
751     if (prefix == NULL) {
752         debug_gtk3("some idiot passed NULL.");
753         return grid;
754     }
755 
756     if (strcmp(prefix, "ARTSTUDIO") == 0) {
757         prefix = "OCP";  /* XXX: not strictly required since resource names
758                                     seem to be case insensitive, but better
759                                     safe than sorry */
760         artstudio = 1;
761     } else if (strcmp(prefix, "KOALA") == 0) {
762         prefix = "Koala";
763         koala = 1;
764     }
765 
766     if (!koala && !artstudio) {
767         label = gtk_label_new("No parameters required");
768         g_object_set(label, "margin-left", 16, NULL);
769         gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
770         gtk_widget_show_all(grid);
771         return grid;
772     }
773 
774     /* ${FORMAT}OversizeHandling */
775     label = gtk_label_new("Oversize handling");
776     g_object_set(label, "margin-left", 16, NULL);
777     gtk_widget_set_halign(label, GTK_ALIGN_START);
778     oversize_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
779             "%sOversizeHandling", oversize_modes, prefix);
780     gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
781     gtk_grid_attach(GTK_GRID(grid), oversize_widget, 1, 0, 1, 1);
782 
783     /* ${FORMAT}UndersizeHandling */
784     label = gtk_label_new("Undersize handling");
785     g_object_set(label, "margin-left", 16, NULL);
786     gtk_widget_set_halign(label, GTK_ALIGN_START);
787     undersize_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
788             "%sUndersizeHandling", undersize_modes, prefix);
789     gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
790     gtk_grid_attach(GTK_GRID(grid), undersize_widget, 1, 1, 1, 1);
791 
792     /* ${FORMAT}MultiColorHandling */
793 
794     row = 2;    /* from now on, the widgets depend on machine and image type */
795 
796     /* OCPMultiColorHandling */
797     if (artstudio) {
798         label = gtk_label_new("Multi color handling");
799         g_object_set(label, "margin-left", 16, NULL);
800         gtk_widget_set_halign(label, GTK_ALIGN_START);
801         multicolor_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
802                 "%sMultiColorHandling", multicolor_modes, prefix);
803         gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
804         gtk_grid_attach(GTK_GRID(grid), multicolor_widget, 1, row, 1, 1);
805         row++;
806     }
807 
808     /* ${FORMAT}TEDLumaHandling */
809     if (machine_class == VICE_MACHINE_PLUS4) {
810         label = gtk_label_new("TED luma handling");
811         g_object_set(label, "margin-left", 16, NULL);
812         gtk_widget_set_halign(label, GTK_ALIGN_START);
813         ted_luma_widget = vice_gtk3_resource_combo_box_int_new_sprintf(
814                 "%sTEDLumHandling", ted_luma_modes, prefix);
815         gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
816         gtk_grid_attach(GTK_GRID(grid), ted_luma_widget, 1, row, 1, 1);
817         row++;
818     }
819 
820     gtk_widget_show_all(grid);
821     return grid;
822 }
823 
824 
825 
826 
827 /** \brief  Create the main 'screenshot' widget
828  *
829  * \return  GtkGrid
830  */
create_screenshot_widget(void)831 static GtkWidget *create_screenshot_widget(void)
832 {
833     GtkWidget *grid;
834     GtkWidget *drv_grid;
835     GtkWidget *label;
836     GtkWidget *radio;
837     GtkWidget *last;
838     GSList *group = NULL;
839     int index;
840     int grid_index;
841 
842     grid = gtk_grid_new();
843     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
844     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
845 
846     /* grid without extra row spacing */
847     drv_grid = vice_gtk3_grid_new_spaced_with_label(-1, 0, "Driver", 1);
848     g_object_set(drv_grid, "margin-top", 8, "margin-left", 16, NULL);
849     /* add some padding to the label */
850     label = gtk_grid_get_child_at(GTK_GRID(drv_grid), 0, 0);
851     g_object_set(label, "margin-bottom", 8, NULL);
852 
853     /* add drivers */
854     grid_index = 1;
855     last = NULL;
856     for (index = 0; video_driver_list[index].name != NULL; index++) {
857         const char *display = video_driver_list[index].display;
858         const char *name = video_driver_list[index].name;
859 
860         if (!driver_is_video(name)) {
861             radio = gtk_radio_button_new_with_label(group, display);
862             g_object_set(radio, "margin-left", 8, NULL);
863             gtk_radio_button_join_group(GTK_RADIO_BUTTON(radio),
864                     GTK_RADIO_BUTTON(last));
865             gtk_grid_attach(GTK_GRID(drv_grid), radio, 0, grid_index, 1, 1);
866 
867             /* Make PNG default (doesn't look we have any numeric define to
868              * indicate PNG, so a strcmp() will have to do)
869              * Also trigger the event handler to set the driver index (by
870              * connecting it before this call), so I don't have to manually
871              * set it here, though all this text could have been used to set it.
872              *
873              * Only set PNG first time the dialog is created, on second time
874              * it should use whatever was used before
875              */
876             if (screenshot_driver_index < 0) {
877                 if (strcmp(name, "PNG") == 0) {
878                     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
879                     screenshot_driver_index = index; /* set this driver as
880                                                         currently selected one */
881                 }
882             } else {
883                 if (screenshot_driver_index == index) {
884                     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
885                 }
886             }
887 
888             /* connect signal *after* setting a radio button's state, to avoid
889              * triggering the handler before the UI is properly set up. */
890             g_signal_connect(radio, "toggled",
891                     G_CALLBACK(on_screenshot_driver_toggled),
892                     GINT_TO_POINTER(index));
893 
894             last = radio;
895             grid_index++;
896         }
897     }
898 
899     /* this is where the various options go per screenshot driver (for example
900      * Koala or Artstudio) */
901     screenshot_options_grid = vice_gtk3_grid_new_spaced_with_label(
902             -1, -1, "Driver options", 1);
903     g_object_set(screenshot_options_grid,
904                  "margin-top", 8, "margin-left", 16, NULL);
905     gtk_grid_attach(GTK_GRID(grid), drv_grid, 0, 0, 1, 1);
906     gtk_grid_attach(GTK_GRID(grid), screenshot_options_grid, 1, 0, 1, 1);
907 
908     update_screenshot_options_grid(create_screenshot_param_widget(""));
909 
910     gtk_widget_show_all(grid);
911     return grid;
912 }
913 
914 
915 /** \brief  Create the main 'sound recording' widget
916  *
917  * \return  GtkGrid
918  */
create_sound_widget(void)919 static GtkWidget *create_sound_widget(void)
920 {
921     GtkWidget *grid;
922     GtkWidget *drv_grid;
923     GtkWidget *label;
924     GtkWidget *radio;
925     GtkWidget *last;
926     GSList *group = NULL;
927     int index;
928 
929     grid = gtk_grid_new();
930     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
931     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
932 
933     drv_grid = vice_gtk3_grid_new_spaced_with_label(-1, 0, "Driver", 1);
934     label = gtk_grid_get_child_at(GTK_GRID(drv_grid), 0, 0);
935     g_object_set(label, "margin-bottom", 8, NULL);
936     g_object_set(drv_grid, "margin-top", 8, "margin-left", 16, NULL);
937 
938     last = NULL;
939     for (index = 0; audio_driver_list[index].name != NULL; index++) {
940         const char *display = audio_driver_list[index].display;
941 
942         radio = gtk_radio_button_new_with_label(group, display);
943         g_object_set(radio, "margin-left", 8, NULL);
944         gtk_radio_button_join_group(GTK_RADIO_BUTTON(radio),
945                 GTK_RADIO_BUTTON(last));
946         gtk_grid_attach(GTK_GRID(drv_grid), radio, 0, index + 1, 1, 1);
947 
948         /*
949          * Set default audio driver, or restore previously selected audio
950          * driver.
951          */
952         if (audio_driver_index < 0) {
953             if (index == 0) {   /* 0 == WAV */
954                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
955                 audio_driver_index = index;
956             }
957         } else {
958             if (index == audio_driver_index) {
959                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
960             }
961         }
962 
963         g_signal_connect(radio, "toggled",
964                 G_CALLBACK(on_audio_driver_toggled),
965                 GINT_TO_POINTER(index));
966 
967         last = radio;
968     }
969 
970     gtk_grid_attach(GTK_GRID(grid), drv_grid, 0, 0, 1, 1);
971 
972     gtk_widget_show_all(grid);
973     return grid;
974 }
975 
976 
977 /** \brief  Create the main 'video recording' widget
978  *
979  * \return  GtkGrid
980  */
create_video_widget(void)981 static GtkWidget *create_video_widget(void)
982 {
983     GtkWidget *grid;
984     GtkWidget *label;
985 #ifdef HAVE_FFMPEG
986     GtkWidget *combo;
987     int index;
988     GtkWidget *selection_grid;
989     GtkWidget *options_grid;
990 #endif
991     grid = vice_gtk3_grid_new_spaced(16, 8);
992 
993 
994 #ifdef HAVE_FFMPEG
995     label = gtk_label_new("Video driver");
996     g_object_set(label, "margin-left", 16, NULL);
997 
998     combo = gtk_combo_box_text_new();
999     for (index = 0; video_driver_list[index].name != NULL; index++) {
1000         const char *display = video_driver_list[index].display;
1001         const char *name = video_driver_list[index].name;
1002 
1003         if (driver_is_video(name)) {
1004             gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo), name, display);
1005         }
1006 
1007         if (video_driver_index < 0) {
1008             video_driver_index = 0;
1009             gtk_combo_box_set_active(GTK_COMBO_BOX(combo), index);
1010         } else {
1011             if (video_driver_index == index) {
1012                 gtk_combo_box_set_active(GTK_COMBO_BOX(combo), index);
1013             }
1014         }
1015 
1016     }
1017     gtk_widget_set_hexpand(combo, TRUE);
1018     gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);
1019 
1020     selection_grid = vice_gtk3_grid_new_spaced_with_label
1021         (-1, -1, "Driver selection", 2);
1022     g_object_set(selection_grid, "margin-top", 8, "margin-left", 16, NULL);
1023     gtk_grid_set_column_spacing(GTK_GRID(selection_grid), 16);
1024     gtk_grid_set_row_spacing(GTK_GRID(selection_grid), 8);
1025     gtk_grid_attach(GTK_GRID(selection_grid), label, 0, 1, 1, 1);
1026     gtk_grid_attach(GTK_GRID(selection_grid), combo, 1, 1, 1, 1);
1027     gtk_widget_show_all(selection_grid);
1028 
1029     gtk_grid_attach(GTK_GRID(grid), selection_grid, 0, 0, 1, 1);
1030 
1031     /* grid around ffmpeg */
1032     options_grid = vice_gtk3_grid_new_spaced_with_label(
1033             -1, -1, "Driver options", 1);
1034     g_object_set(options_grid, "margin-top", 8, "margin-left", 16, NULL);
1035     gtk_grid_set_column_spacing(GTK_GRID(options_grid), 16);
1036     gtk_grid_set_row_spacing(GTK_GRID(options_grid), 8);
1037 
1038     gtk_grid_attach(GTK_GRID(options_grid), ffmpeg_widget_create(), 0, 1, 1,1);
1039     video_driver_options_grid = options_grid;
1040 
1041     gtk_grid_attach(GTK_GRID(grid), options_grid, 0, 1, 1, 1);
1042 #else
1043     label = gtk_label_new(NULL);
1044     gtk_label_set_line_wrap_mode(GTK_LABEL(label), PANGO_WRAP_WORD);
1045     g_object_set(G_OBJECT(label),
1046             "margin-left", 16,
1047             "margin-right", 16,
1048             "margin-top", 16, NULL);
1049     gtk_label_set_markup(GTK_LABEL(label),
1050             "Video recording is unavailable due to VICE having being compiled"
1051             " without FFMPEG support.\nPlease recompile with either"
1052             " <tt>--enable-static-ffmpeg</tt> or"
1053             " <tt>--enable-external-ffmpeg</tt>.\n\n"
1054             "If you didn't compile VICE yourself, ask your provider.");
1055     gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
1056 
1057 #endif
1058     gtk_widget_show_all(grid);
1059     return grid;
1060 }
1061 
1062 
1063 /** \brief  Create the content widget for the dialog
1064  *
1065  * Contains a GtkStack and GtkStackSwitcher to switch between 'screenshot',
1066  * 'sound' and 'video' sub widgets.
1067  *
1068  * \return  GtkGrid
1069  */
create_content_widget(void)1070 static GtkWidget *create_content_widget(void)
1071 {
1072     GtkWidget *grid;
1073     GtkWidget *switcher;
1074 
1075     grid = gtk_grid_new();
1076     gtk_grid_set_column_spacing(GTK_GRID(grid), 16);
1077     gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
1078 
1079     stack = gtk_stack_new();
1080     gtk_stack_add_titled(GTK_STACK(stack), create_screenshot_widget(),
1081             CHILD_SCREENSHOT, "Screenshot");
1082     gtk_stack_add_titled(GTK_STACK(stack), create_sound_widget(),
1083             CHILD_SOUND, "Sound recording");
1084     gtk_stack_add_titled(GTK_STACK(stack), create_video_widget(),
1085             CHILD_VIDEO, "Video recording");
1086     gtk_stack_set_transition_type(GTK_STACK(stack),
1087             GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
1088     gtk_stack_set_transition_duration(GTK_STACK(stack), 1000);
1089     /* avoid resizing the dialog when switching */
1090     gtk_stack_set_homogeneous(GTK_STACK(stack), TRUE);
1091 
1092     switcher = gtk_stack_switcher_new();
1093     gtk_widget_set_halign(switcher, GTK_ALIGN_CENTER);
1094     gtk_widget_set_hexpand(switcher, TRUE);
1095     gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(switcher), GTK_STACK(stack));
1096     /* make all titles of the switcher the same size */
1097     gtk_box_set_homogeneous(GTK_BOX(switcher), TRUE);
1098 
1099     gtk_widget_show_all(stack);
1100     gtk_widget_show_all(switcher);
1101 
1102     gtk_grid_attach(GTK_GRID(grid), switcher, 0, 0, 1, 1);
1103     gtk_grid_attach(GTK_GRID(grid), stack, 0, 1, 1, 1);
1104 
1105     gtk_widget_show_all(grid);
1106     return grid;
1107 }
1108 
1109 
1110 
1111 /** \brief  Show dialog to save screenshot, record sound and/or video
1112  *
1113  * \param[in]   parent  parent widget (unused)
1114  * \param[in]   data    extra data (unused)
1115  *
1116  * \return  TRUE
1117  */
ui_media_dialog_show(GtkWidget * parent,gpointer data)1118 gboolean ui_media_dialog_show(GtkWidget *parent, gpointer data)
1119 {
1120     GtkWidget *dialog;
1121     GtkWidget *content;
1122 
1123     /*
1124      * Pause emulation
1125      */
1126 
1127     /* remember pause state before entering the widget */
1128     old_pause_state = ui_pause_active();
1129 
1130     /* pause emulation */
1131     ui_pause_enable();
1132 
1133     /* create driver list */
1134     if (machine_class != VICE_MACHINE_VSID) {
1135         create_video_driver_list();
1136     }
1137 
1138     dialog = gtk_dialog_new_with_buttons(
1139             "Record media file",
1140             ui_get_active_window(),
1141             GTK_DIALOG_MODAL,
1142             "Save", RESPONSE_SAVE,
1143             "Close", GTK_RESPONSE_DELETE_EVENT,
1144             NULL);
1145 
1146     /* add content widget */
1147     content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1148     if (machine_class != VICE_MACHINE_VSID) {
1149         gtk_container_add(GTK_CONTAINER(content), create_content_widget());
1150     } else {
1151         gtk_container_add(GTK_CONTAINER(content), create_sound_widget());
1152     }
1153 
1154     gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
1155     g_signal_connect(dialog, "response", G_CALLBACK(on_response), (gpointer)dialog);
1156     g_signal_connect_unlocked(dialog, "destroy", G_CALLBACK(on_dialog_destroy), NULL);
1157 
1158     gtk_widget_show_all(dialog);
1159     return TRUE;
1160 }
1161 
1162 
1163 /** \brief  Stop audio or video recording, if active
1164  *
1165  * \param[in]   parent      parent widget (unused)
1166  * \param[in]   data        extra event data (unused)
1167  *
1168  * \return  TRUE, so the emulated machine doesn't get the shortcut key
1169  */
ui_media_stop_recording(GtkWidget * parent,gpointer data)1170 gboolean ui_media_stop_recording(GtkWidget *parent, gpointer data)
1171 {
1172     /* stop sound recording, if active */
1173     if (sound_is_recording()) {
1174         sound_stop_recording();
1175     }
1176     /* stop video recording */
1177     if (screenshot_is_recording()) {
1178         screenshot_stop_recording();
1179     }
1180 
1181     ui_display_recording(0);
1182     statusbar_recording_widget_hide_all(ui_statusbar_get_recording_widget(), 10);
1183 
1184     return TRUE;
1185 }
1186 
1187 
1188 /** \brief  Create screenshot with autogenerated filename
1189  *
1190  * Creates a PNG screenshot with an autogenerated filename with an ISO-8601-ish
1191  * timestamp:
1192  * "vice-screenshot-<year><month><day><hour><month><seconds><sec-frac>.png"
1193  */
ui_media_auto_screenshot(void)1194 void ui_media_auto_screenshot(void)
1195 {
1196     char *filename;
1197 
1198     /* remember pause state before entering the widget */
1199     old_pause_state = ui_pause_active();
1200 
1201     /* pause emulation */
1202     ui_pause_enable();
1203 
1204     /* no need for locale bullshit */
1205     filename = create_proposed_screenshot_name("png");
1206     if (screenshot_save("PNG", filename, ui_get_active_canvas()) < 0) {
1207         debug_gtk3("OOPS");
1208     }
1209 
1210     if (!old_pause_state) {
1211         ui_pause_disable();
1212     }
1213 }
1214