Add default audio language configuration (#2375).
[dcpomatic.git] / src / wx / wx_util.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 /** @file src/wx/wx_util.cc
23  *  @brief Some utility functions and classes.
24  */
25
26
27 #include "file_picker_ctrl.h"
28 #include "language_tag_widget.h"
29 #include "password_entry.h"
30 #include "static_text.h"
31 #include "wx_util.h"
32 #include "lib/config.h"
33 #include "lib/cross.h"
34 #include "lib/job.h"
35 #include "lib/job_manager.h"
36 #include "lib/util.h"
37 #include "lib/version.h"
38 #include <dcp/locale_convert.h>
39 #include <dcp/warnings.h>
40 LIBDCP_DISABLE_WARNINGS
41 #include <wx/spinctrl.h>
42 #include <wx/splash.h>
43 #include <wx/progdlg.h>
44 #include <wx/filepicker.h>
45 #include <wx/sizer.h>
46 LIBDCP_ENABLE_WARNINGS
47 #include <boost/thread.hpp>
48
49
50 using std::string;
51 using std::vector;
52 using std::pair;
53 using std::shared_ptr;
54 using boost::optional;
55 using dcp::locale_convert;
56 using namespace dcpomatic;
57
58
59 wxStaticText *
60 #ifdef __WXOSX__
61 create_label (wxWindow* p, wxString t, bool left)
62 #else
63 create_label (wxWindow* p, wxString t, bool)
64 #endif
65 {
66 #ifdef __WXOSX__
67         if (left) {
68                 t += wxT (":");
69         }
70 #endif
71         return new StaticText (p, t);
72 }
73
74
75 #ifdef __WXOSX__
76 static
77 void
78 setup_osx_flags (wxSizer* s, bool left, int& flags)
79 {
80         if (left) {
81                 auto box = dynamic_cast<wxBoxSizer*>(s);
82                 if (!box || box->GetOrientation() != wxHORIZONTAL) {
83                         flags |= wxALIGN_RIGHT;
84                 }
85         }
86 }
87 #endif
88
89
90 /** Add a wxStaticText to a wxSizer.
91  *  @param s Sizer to add to.
92  *  @param p Parent window for the wxStaticText.
93  *  @param t Text for the wxStaticText.
94  *  @param left true if this label is a `left label'; ie the sort
95  *  of label which should be right-aligned on OS X.
96  *  @param prop Proportion to pass when calling Add() on the wxSizer.
97  */
98 wxStaticText *
99 add_label_to_sizer (wxSizer* s, wxWindow* p, wxString t, bool left, int prop, int flags)
100 {
101 #ifdef __WXOSX__
102         setup_osx_flags (s, left, flags);
103 #endif
104         auto m = create_label (p, t, left);
105         s->Add (m, prop, flags, DCPOMATIC_SIZER_GAP);
106         return m;
107 }
108
109
110 wxStaticText *
111 #ifdef __WXOSX__
112 add_label_to_sizer (wxSizer* s, wxStaticText* t, bool left, int prop, int flags)
113 #else
114 add_label_to_sizer (wxSizer* s, wxStaticText* t, bool, int prop, int flags)
115 #endif
116 {
117 #ifdef __WXOSX__
118         setup_osx_flags (s, left, flags);
119 #endif
120         s->Add (t, prop, flags, DCPOMATIC_SIZER_GAP);
121         return t;
122 }
123
124
125 wxStaticText *
126 add_label_to_sizer (wxGridBagSizer* s, wxWindow* p, wxString t, bool left, wxGBPosition pos, wxGBSpan span)
127 {
128         int flags = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT;
129 #ifdef __WXOSX__
130         setup_osx_flags (s, left, flags);
131 #endif
132         auto m = create_label (p, t, left);
133         s->Add (m, pos, span, flags);
134         return m;
135 }
136
137
138 wxStaticText *
139 #ifdef __WXOSX__
140 add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool left, wxGBPosition pos, wxGBSpan span)
141 #else
142 add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool, wxGBPosition pos, wxGBSpan span)
143 #endif
144 {
145         int flags = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT;
146 #ifdef __WXOSX__
147         setup_osx_flags (s, left, flags);
148 #endif
149         s->Add (t, pos, span, flags);
150         return t;
151 }
152
153
154 /** Pop up an error dialogue box.
155  *  @param parent Parent.
156  *  @param m Message.
157  *  @param e Extended message.
158  */
159 void
160 error_dialog (wxWindow* parent, wxString m, optional<wxString> e)
161 {
162         auto d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxOK | wxICON_ERROR);
163         if (e) {
164                 wxString em = *e;
165                 em[0] = wxToupper (em[0]);
166                 d->SetExtendedMessage (em);
167         }
168         d->ShowModal ();
169         d->Destroy ();
170 }
171
172
173 /** Pop up an error dialogue box.
174  *  @param parent Parent.
175  *  @param m Message.
176  */
177 void
178 message_dialog (wxWindow* parent, wxString m)
179 {
180         auto d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxOK | wxICON_INFORMATION);
181         d->ShowModal ();
182         d->Destroy ();
183 }
184
185
186 /** @return true if the user answered "yes" */
187 bool
188 confirm_dialog (wxWindow* parent, wxString m)
189 {
190         auto d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxYES_NO | wxICON_QUESTION);
191         int const r = d->ShowModal ();
192         d->Destroy ();
193         return r == wxID_YES;
194 }
195
196
197 /** @param s wxWidgets string.
198  *  @return Corresponding STL string.
199  */
200 string
201 wx_to_std (wxString s)
202 {
203         return string (s.ToUTF8());
204 }
205
206
207 /** @param s STL string.
208  *  @return Corresponding wxWidgets string.
209  */
210 wxString
211 std_to_wx (string s)
212 {
213         return wxString (s.c_str(), wxConvUTF8);
214 }
215
216
217 string
218 string_client_data (wxClientData* o)
219 {
220         return wx_to_std (dynamic_cast<wxStringClientData*>(o)->GetData());
221 }
222
223
224 void
225 checked_set (FilePickerCtrl* widget, boost::filesystem::path value)
226 {
227         if (widget->GetPath() != std_to_wx (value.string())) {
228                 if (value.empty()) {
229                         /* Hack to make wxWidgets clear the control when we are passed
230                            an empty value.
231                         */
232                         value = " ";
233                 }
234                 widget->SetPath (std_to_wx (value.string()));
235         }
236 }
237
238
239 void
240 checked_set (wxDirPickerCtrl* widget, boost::filesystem::path value)
241 {
242         if (widget->GetPath() != std_to_wx (value.string())) {
243                 if (value.empty()) {
244                         /* Hack to make wxWidgets clear the control when we are passed
245                            an empty value.
246                         */
247                         value = " ";
248                 }
249                 widget->SetPath (std_to_wx (value.string()));
250         }
251 }
252
253
254 void
255 checked_set (wxSpinCtrl* widget, int value)
256 {
257         if (widget->GetValue() != value) {
258                 widget->SetValue (value);
259         }
260 }
261
262
263 void
264 checked_set (wxSpinCtrlDouble* widget, double value)
265 {
266         /* XXX: completely arbitrary epsilon */
267         if (fabs (widget->GetValue() - value) > 1e-16) {
268                 widget->SetValue (value);
269         }
270 }
271
272
273 void
274 checked_set (wxChoice* widget, int value)
275 {
276         if (widget->GetSelection() != value) {
277                 widget->SetSelection (value);
278         }
279 }
280
281
282 void
283 checked_set (wxChoice* widget, string value)
284 {
285         wxClientData* o = nullptr;
286         if (widget->GetSelection() != -1) {
287                 o = widget->GetClientObject (widget->GetSelection ());
288         }
289
290         if (!o || string_client_data(o) != value) {
291                 for (unsigned int i = 0; i < widget->GetCount(); ++i) {
292                         if (string_client_data (widget->GetClientObject (i)) == value) {
293                                 widget->SetSelection (i);
294                         }
295                 }
296         }
297 }
298
299
300 void
301 checked_set (wxChoice* widget, vector<pair<string, string>> items)
302 {
303        vector<pair<string, string>> current;
304        for (unsigned int i = 0; i < widget->GetCount(); ++i) {
305                current.push_back (
306                        make_pair(
307                                wx_to_std(widget->GetString(i)),
308                                widget->GetClientData() ? string_client_data(widget->GetClientObject(i)) : ""
309                                )
310                        );
311        }
312
313        if (current == items) {
314                return;
315        }
316
317        widget->Clear ();
318        for (auto i: items) {
319                widget->Append (std_to_wx(i.first), new wxStringClientData(std_to_wx(i.second)));
320        }
321 }
322
323
324 void
325 checked_set (wxTextCtrl* widget, string value)
326 {
327         if (widget->GetValue() != std_to_wx (value)) {
328                 widget->ChangeValue (std_to_wx (value));
329         }
330 }
331
332
333 void
334 checked_set (PasswordEntry* entry, string value)
335 {
336         if (entry->get() != value) {
337                 entry->set(value);
338         }
339 }
340
341
342 void
343 checked_set (wxTextCtrl* widget, wxString value)
344 {
345         if (widget->GetValue() != value) {
346                 widget->ChangeValue (value);
347         }
348 }
349
350
351 void
352 checked_set (wxStaticText* widget, string value)
353 {
354         if (widget->GetLabel() != std_to_wx (value)) {
355                 widget->SetLabel (std_to_wx (value));
356         }
357 }
358
359
360 void
361 checked_set (wxStaticText* widget, wxString value)
362 {
363         if (widget->GetLabel() != value) {
364                 widget->SetLabel (value);
365         }
366 }
367
368
369 void
370 checked_set (wxCheckBox* widget, bool value)
371 {
372         if (widget->GetValue() != value) {
373                 widget->SetValue (value);
374         }
375 }
376
377
378 void
379 checked_set (wxRadioButton* widget, bool value)
380 {
381         if (widget->GetValue() != value) {
382                 widget->SetValue (value);
383         }
384 }
385
386
387 void
388 checked_set(LanguageTagWidget* widget, dcp::LanguageTag value)
389 {
390         if (widget->get() != value) {
391                 widget->set(value);
392         }
393 }
394
395
396 void
397 checked_set(LanguageTagWidget* widget, optional<dcp::LanguageTag> value)
398 {
399         if (widget->get() != value) {
400                 widget->set(value);
401         }
402 }
403
404
405 void
406 dcpomatic_setup_i18n ()
407 {
408         int language = wxLANGUAGE_DEFAULT;
409
410         auto config_lang = Config::instance()->language ();
411         if (config_lang && !config_lang->empty ()) {
412                 auto const li = wxLocale::FindLanguageInfo (std_to_wx (config_lang.get ()));
413                 if (li) {
414                         language = li->Language;
415                 }
416         }
417
418         wxLocale* locale = nullptr;
419         if (wxLocale::IsAvailable (language)) {
420                 locale = new wxLocale (language, wxLOCALE_LOAD_DEFAULT);
421
422 #ifdef DCPOMATIC_WINDOWS
423                 locale->AddCatalogLookupPathPrefix (std_to_wx (mo_path().string()));
424 #endif
425
426 #ifdef DCPOMATIC_LINUX
427                 locale->AddCatalogLookupPathPrefix (LINUX_LOCALE_PREFIX);
428
429                 /* We have to include the wxWidgets .mo in our distribution,
430                    so we rename it to avoid clashes with any other installation
431                    of wxWidgets.
432                 */
433                 locale->AddCatalog (wxT ("dcpomatic2-wxstd"));
434
435                 /* Fedora 29 (at least) installs wxstd3.mo instead of wxstd.mo */
436                 locale->AddCatalog (wxT ("wxstd3"));
437 #endif
438
439                 locale->AddCatalog (wxT ("libdcpomatic2-wx"));
440                 locale->AddCatalog (wxT ("dcpomatic2"));
441
442                 if (!locale->IsOk()) {
443                         delete locale;
444                         locale = new wxLocale (wxLANGUAGE_ENGLISH);
445                 }
446         }
447
448         if (locale) {
449                 dcpomatic_setup_gettext_i18n (wx_to_std (locale->GetCanonicalName ()));
450         }
451 }
452
453
454 int
455 wx_get (wxSpinCtrl* w)
456 {
457         return w->GetValue ();
458 }
459
460
461 int
462 wx_get (wxChoice* w)
463 {
464         return w->GetSelection ();
465 }
466
467
468 double
469 wx_get (wxSpinCtrlDouble* w)
470 {
471         return w->GetValue ();
472 }
473
474
475 /** @param s String of the form Context|String
476  *  @return translation, or String if no translation is available.
477  */
478 wxString
479 context_translation (wxString s)
480 {
481         auto t = wxGetTranslation (s);
482         if (t == s) {
483                 /* No translation; strip the context */
484                 int c = t.Find (wxT ("|"));
485                 if (c != wxNOT_FOUND) {
486                         t = t.Mid (c + 1);
487                 }
488         }
489
490         return t;
491 }
492
493
494 wxString
495 time_to_timecode (DCPTime t, double fps)
496 {
497         auto w = t.seconds ();
498         int const h = (w / 3600);
499         w -= h * 3600;
500         int const m = (w / 60);
501         w -= m * 60;
502         int const s = floor (w);
503         w -= s;
504         int const f = lrint (w * fps);
505         return wxString::Format (wxT("%02d:%02d:%02d.%02d"), h, m, s, f);
506 }
507
508
509 void
510 setup_audio_channels_choice (wxChoice* choice, int minimum)
511 {
512         vector<pair<string, string>> items;
513         for (int i = minimum; i <= 16; i += 2) {
514                 if (i == 2) {
515                         items.push_back (make_pair(wx_to_std(_("2 - stereo")), locale_convert<string>(i)));
516                 } else if (i == 4) {
517                         items.push_back (make_pair(wx_to_std(_("4 - L/C/R/Lfe")), locale_convert<string>(i)));
518                 } else if (i == 6) {
519                         items.push_back (make_pair(wx_to_std(_("6 - 5.1")), locale_convert<string>(i)));
520                 } else if (i == 8) {
521                         items.push_back (make_pair(wx_to_std(_("8 - 5.1/HI/VI")), locale_convert<string>(i)));
522                 } else if (i == 12) {
523                         items.push_back (make_pair(wx_to_std(_("12 - 7.1/HI/VI")), locale_convert<string>(i)));
524                 } else {
525                         items.push_back (make_pair(locale_convert<string> (i), locale_convert<string>(i)));
526                 }
527         }
528
529         checked_set (choice, items);
530 }
531
532
533 wxSplashScreen *
534 maybe_show_splash ()
535 {
536         wxSplashScreen* splash = nullptr;
537         try {
538                 wxBitmap bitmap;
539                 if (bitmap.LoadFile(bitmap_path("splash.png"), wxBITMAP_TYPE_PNG)) {
540                         {
541                                 /* This wxMemoryDC must be destroyed before bitmap can be used elsewhere */
542                                 wxMemoryDC dc(bitmap);
543                                 auto const version = wxString::Format("%s (%s)", dcpomatic_version, dcpomatic_git_commit);
544                                 auto screen_size = dc.GetSize();
545                                 auto text_size = dc.GetTextExtent(version);
546                                 dc.DrawText(version, (screen_size.GetWidth() - text_size.GetWidth()) / 2, 236);
547                         }
548 #ifdef DCPOMATIC_WINDOWS
549                         /* Having wxSTAY_ON_TOP means error dialogues hide behind the splash screen on Windows, no matter what I try */
550                         splash = new wxSplashScreen (bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, 0, -1, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE | wxFRAME_NO_TASKBAR);
551 #else
552                         splash = new wxSplashScreen (bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, 0, -1);
553 #endif
554                         wxYield ();
555                 }
556         } catch (boost::filesystem::filesystem_error& e) {
557                 /* Maybe we couldn't find the splash image; never mind */
558         }
559
560         return splash;
561 }
562
563
564 double
565 calculate_mark_interval (double mark_interval)
566 {
567         if (mark_interval > 5) {
568                 mark_interval -= lrint (mark_interval) % 5;
569         }
570         if (mark_interval > 10) {
571                 mark_interval -= lrint (mark_interval) % 10;
572         }
573         if (mark_interval > 60) {
574                 mark_interval -= lrint (mark_interval) % 60;
575         }
576         if (mark_interval > 3600) {
577                 mark_interval -= lrint (mark_interval) % 3600;
578         }
579
580         if (mark_interval < 1) {
581                 mark_interval = 1;
582         }
583
584         return mark_interval;
585 }
586
587
588 /** @return false if the task was cancelled */
589 bool
590 display_progress (wxString title, wxString task)
591 {
592         auto jm = JobManager::instance ();
593
594         wxProgressDialog progress (title, task, 100, 0, wxPD_CAN_ABORT);
595
596         bool ok = true;
597
598         while (jm->work_to_do()) {
599                 dcpomatic_sleep_seconds (1);
600                 if (!progress.Pulse()) {
601                         /* user pressed cancel */
602                         for (auto i: jm->get()) {
603                                 i->cancel();
604                         }
605                         ok = false;
606                         break;
607                 }
608         }
609
610         return ok;
611 }
612
613
614 int
615 get_offsets (vector<Offset>& offsets)
616 {
617         offsets.push_back (Offset(_("UTC-11"),  -11,  0));
618         offsets.push_back (Offset(_("UTC-10"),  -10,  0));
619         offsets.push_back (Offset(_("UTC-9"),    -9,  0));
620         offsets.push_back (Offset(_("UTC-8"),    -8,  0));
621         offsets.push_back (Offset(_("UTC-7"),    -7,  0));
622         offsets.push_back (Offset(_("UTC-6"),    -6,  0));
623         offsets.push_back (Offset(_("UTC-5"),    -5,  0));
624         offsets.push_back (Offset(_("UTC-4:30"), -4, 30));
625         offsets.push_back (Offset(_("UTC-4"),    -4,  0));
626         offsets.push_back (Offset(_("UTC-3:30"), -3, 30));
627         offsets.push_back (Offset(_("UTC-3"),    -3,  0));
628         offsets.push_back (Offset(_("UTC-2"),    -2,  0));
629         offsets.push_back (Offset(_("UTC-1"),    -1,  0));
630         int utc = offsets.size();
631         offsets.push_back (Offset(_("UTC")  ,     0,  0));
632         offsets.push_back (Offset(_("UTC+1"),     1,  0));
633         offsets.push_back (Offset(_("UTC+2"),     2,  0));
634         offsets.push_back (Offset(_("UTC+3"),     3,  0));
635         offsets.push_back (Offset(_("UTC+4"),     4,  0));
636         offsets.push_back (Offset(_("UTC+5"),     5,  0));
637         offsets.push_back (Offset(_("UTC+5:30"),  5, 30));
638         offsets.push_back (Offset(_("UTC+6"),     6,  0));
639         offsets.push_back (Offset(_("UTC+7"),     7,  0));
640         offsets.push_back (Offset(_("UTC+8"),     8,  0));
641         offsets.push_back (Offset(_("UTC+9"),     9,  0));
642         offsets.push_back (Offset(_("UTC+9:30"),  9, 30));
643         offsets.push_back (Offset(_("UTC+10"),   10,  0));
644         offsets.push_back (Offset(_("UTC+11"),   11,  0));
645         offsets.push_back (Offset(_("UTC+12"),   12,  0));
646
647         return utc;
648 }
649
650
651 wxString
652 bitmap_path (string name)
653 {
654         boost::filesystem::path base;
655
656 #ifdef DCPOMATIC_DEBUG
657         /* Hack to allow Linux and OS X to find icons when running from the source tree */
658         char* path = getenv ("DCPOMATIC_GRAPHICS");
659         if (path) {
660                 base = path;
661         } else {
662                 base = resources_path();
663         }
664 #else
665         base = resources_path();
666 #endif
667
668         auto p = base / name;
669         return std_to_wx (p.string());
670 }
671
672
673 wxString
674 icon_path(string name)
675 {
676         return gui_is_dark() ? bitmap_path(String::compose("%1_white.png", name)) : bitmap_path(String::compose("%1_black.png", name));
677 }
678
679
680 wxSize
681 small_button_size (wxWindow* parent, wxString text)
682 {
683         wxClientDC dc (parent);
684         auto size = dc.GetTextExtent (text);
685         size.SetHeight (-1);
686         size.IncBy (32, 0);
687         return size;
688 }
689
690
691 bool
692 gui_is_dark ()
693 {
694 #if defined(DCPOMATIC_OSX) && wxCHECK_VERSION(3, 1, 0)
695         auto appearance = wxSystemSettings::GetAppearance();
696         return appearance.IsDark();
697 #else
698         return false;
699 #endif
700 }
701
702
703 #if wxCHECK_VERSION(3,1,0)
704 double
705 dpi_scale_factor (wxWindow* window)
706 {
707         return window->GetDPIScaleFactor();
708 }
709 #else
710 double
711 dpi_scale_factor (wxWindow*)
712 {
713         return 1;
714 }
715 #endif
716
717
718
719 int
720 search_ctrl_height ()
721 {
722 #ifdef __WXGTK3__
723         return 30;
724 #else
725         return -1;
726 #endif
727 }
728
729
730 void
731 report_config_load_failure(wxWindow* parent, Config::LoadFailure what)
732 {
733         switch (what) {
734         case Config::LoadFailure::CONFIG:
735                 message_dialog(parent, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
736                 break;
737         case Config::LoadFailure::CINEMAS:
738                 message_dialog(
739                         parent,
740                         _(wxString::Format("The cinemas list for creating KDMs (cinemas.xml) failed to load.  Please check the numbered backup files in %s",
741                                            std_to_wx(Config::instance()->cinemas_file().parent_path().string())))
742                         );
743                 break;
744         case Config::LoadFailure::DKDM_RECIPIENTS:
745                 message_dialog(
746                         parent,
747                         _(wxString::Format("The recipients list for creating DKDMs (dkdm_recipients.xml) failed to load.  Please check the numbered backup files in %s",
748                                            std_to_wx(Config::instance()->dkdm_recipients_file().parent_path().string())))
749                         );
750                 break;
751         }
752 }
753