Restore time zone to Cinema and improve UI to use it (#2473).
[dcpomatic.git] / src / wx / wx_util.cc
index 2bd64cc304f6b7ec5cb7ae8bcf496c68579e6b12..e85b74cc81468ad23c57e1bba28a1f513bc56d7a 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
+
 /** @file src/wx/wx_util.cc
  *  @brief Some utility functions and classes.
  */
 
-#include "wx_util.h"
+
 #include "file_picker_ctrl.h"
-#include "static_text.h"
+#include "language_tag_widget.h"
 #include "password_entry.h"
+#include "region_subtag_widget.h"
+#include "static_text.h"
+#include "wx_ptr.h"
+#include "wx_util.h"
+#include "wx_variant.h"
 #include "lib/config.h"
-#include "lib/job_manager.h"
-#include "lib/util.h"
 #include "lib/cross.h"
 #include "lib/job.h"
-#include "lib/warnings.h"
+#include "lib/job_manager.h"
+#include "lib/util.h"
+#include "lib/variant.h"
+#include "lib/version.h"
 #include <dcp/locale_convert.h>
-DCPOMATIC_DISABLE_WARNINGS
-#include <wx/spinctrl.h>
-#include <wx/splash.h>
-#include <wx/progdlg.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
 #include <wx/filepicker.h>
+#include <wx/progdlg.h>
 #include <wx/sizer.h>
-DCPOMATIC_ENABLE_WARNINGS
+#include <wx/spinctrl.h>
+#include <wx/splash.h>
+LIBDCP_ENABLE_WARNINGS
 #include <boost/thread.hpp>
 
+
 using std::string;
 using std::vector;
 using std::pair;
@@ -50,6 +59,7 @@ using boost::optional;
 using dcp::locale_convert;
 using namespace dcpomatic;
 
+
 wxStaticText *
 #ifdef __WXOSX__
 create_label (wxWindow* p, wxString t, bool left)
@@ -66,19 +76,22 @@ create_label (wxWindow* p, wxString t, bool)
 }
 
 
+#ifdef __WXOSX__
 static
+void
 setup_osx_flags (wxSizer* s, bool left, int& flags)
 {
        if (left) {
-               auto box = dynamic_cast<wxBoxSizer>(s);
+               auto box = dynamic_cast<wxBoxSizer*>(s);
                if (!box || box->GetOrientation() != wxHORIZONTAL) {
                        flags |= wxALIGN_RIGHT;
                }
        }
 }
+#endif
 
 
-/** Add a wxStaticText to a wxSizer, aligning it at vertical centre.
+/** Add a wxStaticText to a wxSizer.
  *  @param s Sizer to add to.
  *  @param p Parent window for the wxStaticText.
  *  @param t Text for the wxStaticText.
@@ -93,10 +106,11 @@ add_label_to_sizer (wxSizer* s, wxWindow* p, wxString t, bool left, int prop, in
        setup_osx_flags (s, left, flags);
 #endif
        auto m = create_label (p, t, left);
-       s->Add (m, prop, flags, 6);
+       s->Add (m, prop, flags, DCPOMATIC_SIZER_GAP);
        return m;
 }
 
+
 wxStaticText *
 #ifdef __WXOSX__
 add_label_to_sizer (wxSizer* s, wxStaticText* t, bool left, int prop, int flags)
@@ -107,22 +121,24 @@ add_label_to_sizer (wxSizer* s, wxStaticText* t, bool, int prop, int flags)
 #ifdef __WXOSX__
        setup_osx_flags (s, left, flags);
 #endif
-       s->Add (t, prop, flags, 6);
+       s->Add (t, prop, flags, DCPOMATIC_SIZER_GAP);
        return t;
 }
 
+
 wxStaticText *
-add_label_to_sizer (wxGridBagSizer* s, wxWindow* p, wxString t, bool left, wxGBPosition pos, wxGBSpan span)
+add_label_to_sizer(wxGridBagSizer* s, wxWindow* p, wxString t, bool left, wxGBPosition pos, wxGBSpan span, bool indent)
 {
-       int flags = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT;
+       int flags = wxALIGN_CENTER_VERTICAL | wxLEFT;
 #ifdef __WXOSX__
        setup_osx_flags (s, left, flags);
 #endif
        auto m = create_label (p, t, left);
-       s->Add (m, pos, span, flags);
+       s->Add(m, pos, span, flags, indent ? DCPOMATIC_SIZER_X_GAP : 0);
        return m;
 }
 
+
 wxStaticText *
 #ifdef __WXOSX__
 add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool left, wxGBPosition pos, wxGBSpan span)
@@ -138,6 +154,7 @@ add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool, wxGBPosition pos,
        return t;
 }
 
+
 /** Pop up an error dialogue box.
  *  @param parent Parent.
  *  @param m Message.
@@ -146,16 +163,16 @@ add_label_to_sizer (wxGridBagSizer* s, wxStaticText* t, bool, wxGBPosition pos,
 void
 error_dialog (wxWindow* parent, wxString m, optional<wxString> e)
 {
-       auto d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxOK | wxICON_ERROR);
+       auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxOK | wxICON_ERROR);
        if (e) {
                wxString em = *e;
                em[0] = wxToupper (em[0]);
                d->SetExtendedMessage (em);
        }
        d->ShowModal ();
-       d->Destroy ();
 }
 
+
 /** Pop up an error dialogue box.
  *  @param parent Parent.
  *  @param m Message.
@@ -163,19 +180,17 @@ error_dialog (wxWindow* parent, wxString m, optional<wxString> e)
 void
 message_dialog (wxWindow* parent, wxString m)
 {
-       auto d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxOK | wxICON_INFORMATION);
+       auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxOK | wxICON_INFORMATION);
        d->ShowModal ();
-       d->Destroy ();
 }
 
+
 /** @return true if the user answered "yes" */
 bool
 confirm_dialog (wxWindow* parent, wxString m)
 {
-       wxMessageDialog* d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxYES_NO | wxICON_QUESTION);
-       int const r = d->ShowModal ();
-       d->Destroy ();
-       return r == wxID_YES;
+       auto d = make_wx<wxMessageDialog>(parent, m, variant::wx::dcpomatic(), wxYES_NO | wxICON_QUESTION);
+       return d->ShowModal() == wxID_YES;
 }
 
 
@@ -185,9 +200,10 @@ confirm_dialog (wxWindow* parent, wxString m)
 string
 wx_to_std (wxString s)
 {
-       return string (s.ToUTF8 ());
+       return string (s.ToUTF8());
 }
 
+
 /** @param s STL string.
  *  @return Corresponding wxWidgets string.
  */
@@ -197,26 +213,29 @@ std_to_wx (string s)
        return wxString (s.c_str(), wxConvUTF8);
 }
 
+
 string
 string_client_data (wxClientData* o)
 {
        return wx_to_std (dynamic_cast<wxStringClientData*>(o)->GetData());
 }
 
+
 void
 checked_set (FilePickerCtrl* widget, boost::filesystem::path value)
 {
-       if (widget->GetPath() != std_to_wx (value.string())) {
+       if (widget->path() != value) {
                if (value.empty()) {
                        /* Hack to make wxWidgets clear the control when we are passed
                           an empty value.
                        */
                        value = " ";
                }
-               widget->SetPath (std_to_wx (value.string()));
+               widget->set_path(value);
        }
 }
 
+
 void
 checked_set (wxDirPickerCtrl* widget, boost::filesystem::path value)
 {
@@ -231,6 +250,7 @@ checked_set (wxDirPickerCtrl* widget, boost::filesystem::path value)
        }
 }
 
+
 void
 checked_set (wxSpinCtrl* widget, int value)
 {
@@ -239,6 +259,7 @@ checked_set (wxSpinCtrl* widget, int value)
        }
 }
 
+
 void
 checked_set (wxSpinCtrlDouble* widget, double value)
 {
@@ -248,6 +269,7 @@ checked_set (wxSpinCtrlDouble* widget, double value)
        }
 }
 
+
 void
 checked_set (wxChoice* widget, int value)
 {
@@ -256,6 +278,7 @@ checked_set (wxChoice* widget, int value)
        }
 }
 
+
 void
 checked_set (wxChoice* widget, string value)
 {
@@ -273,15 +296,16 @@ checked_set (wxChoice* widget, string value)
        }
 }
 
+
 void
-checked_set (wxChoice* widget, vector<pair<string, string> > items)
+checked_set (wxChoice* widget, vector<pair<string, string>> items)
 {
-       vector<pair<string, string> > current;
+       vector<pair<string, string>> current;
        for (unsigned int i = 0; i < widget->GetCount(); ++i) {
                current.push_back (
-                       make_pair (
-                               wx_to_std (widget->GetString (i)),
-                               string_client_data (widget->GetClientObject (i))
+                       make_pair(
+                               wx_to_std(widget->GetString(i)),
+                               widget->GetClientData() ? string_client_data(widget->GetClientObject(i)) : ""
                                )
                        );
        }
@@ -296,6 +320,7 @@ checked_set (wxChoice* widget, vector<pair<string, string> > items)
        }
 }
 
+
 void
 checked_set (wxTextCtrl* widget, string value)
 {
@@ -304,6 +329,7 @@ checked_set (wxTextCtrl* widget, string value)
        }
 }
 
+
 void
 checked_set (PasswordEntry* entry, string value)
 {
@@ -312,6 +338,7 @@ checked_set (PasswordEntry* entry, string value)
        }
 }
 
+
 void
 checked_set (wxTextCtrl* widget, wxString value)
 {
@@ -320,6 +347,7 @@ checked_set (wxTextCtrl* widget, wxString value)
        }
 }
 
+
 void
 checked_set (wxStaticText* widget, string value)
 {
@@ -328,6 +356,7 @@ checked_set (wxStaticText* widget, string value)
        }
 }
 
+
 void
 checked_set (wxStaticText* widget, wxString value)
 {
@@ -336,6 +365,7 @@ checked_set (wxStaticText* widget, wxString value)
        }
 }
 
+
 void
 checked_set (wxCheckBox* widget, bool value)
 {
@@ -344,6 +374,7 @@ checked_set (wxCheckBox* widget, bool value)
        }
 }
 
+
 void
 checked_set (wxRadioButton* widget, bool value)
 {
@@ -352,6 +383,93 @@ checked_set (wxRadioButton* widget, bool value)
        }
 }
 
+
+void
+checked_set(LanguageTagWidget* widget, dcp::LanguageTag value)
+{
+       if (widget->get() != value) {
+               widget->set(value);
+       }
+}
+
+
+void
+checked_set(LanguageTagWidget* widget, optional<dcp::LanguageTag> value)
+{
+       if (widget->get() != value) {
+               widget->set(value);
+       }
+}
+
+
+void
+checked_set(RegionSubtagWidget* widget, optional<dcp::LanguageTag::RegionSubtag> value)
+{
+       if (widget->get() != value) {
+               widget->set(value);
+       }
+}
+
+
+#ifdef DCPOMATIC_OSX
+
+void
+dcpomatic_setup_i18n()
+{
+       wxLog::EnableLogging();
+
+       auto get_locale_value = [](CFLocaleKey key) {
+               CFLocaleRef cflocale = CFLocaleCopyCurrent();
+               auto value = (CFStringRef) CFLocaleGetValue(cflocale, key);
+               char buffer[64];
+               CFStringGetCString(value, buffer, sizeof(buffer), kCFStringEncodingUTF8);
+               CFRelease(cflocale);
+               return string(buffer);
+       };
+
+       auto translations = new wxTranslations();
+
+       auto config_lang = Config::instance()->language();
+       if (config_lang && !config_lang->empty()) {
+               translations->SetLanguage(std_to_wx(*config_lang));
+       } else {
+               /* We want to use the user's preferred language.  It seems that if we use the wxWidgets default we will get the
+                * language for the locale, which may not be what we want (e.g. for a machine in Germany, configured for DE locale,
+                * but with the preferred language set to English).
+                *
+                * Instead, the the language code from macOS then get the corresponding canonical language string with region,
+                * which wxTranslations::SetLanguage will accept.
+                */
+               auto const language_code = get_locale_value(kCFLocaleLanguageCode);
+               /* Ideally this would be wxUILocale (as wxLocale is deprecated) but we want to keep this building
+                * with the old wxWidgets we use for the older macOS builds.
+                */
+               auto const info = wxLocale::FindLanguageInfo(std_to_wx(language_code));
+               if (info) {
+#if wxCHECK_VERSION(3, 1, 6)
+                       translations->SetLanguage(info->GetCanonicalWithRegion());
+#else
+                       translations->SetLanguage(info->CanonicalName);
+#endif
+               }
+       }
+
+#ifdef DCPOMATIC_DEBUG
+       wxFileTranslationsLoader::AddCatalogLookupPathPrefix(wxT("build/src/wx/mo"));
+       wxFileTranslationsLoader::AddCatalogLookupPathPrefix(wxT("build/src/tools/mo"));
+#endif
+
+       translations->AddStdCatalog();
+       translations->AddCatalog(wxT("libdcpomatic2-wx"));
+       translations->AddCatalog(wxT("dcpomatic2"));
+
+       wxTranslations::Set(translations);
+
+       dcpomatic_setup_gettext_i18n(config_lang.get_value_or(""));
+}
+
+#else
+
 void
 dcpomatic_setup_i18n ()
 {
@@ -386,8 +504,9 @@ dcpomatic_setup_i18n ()
                locale->AddCatalog (wxT ("wxstd3"));
 #endif
 
-               locale->AddCatalog (wxT ("libdcpomatic2-wx"));
-               locale->AddCatalog (wxT ("dcpomatic2"));
+               locale->AddCatalog(wxT("wxstd"));
+               locale->AddCatalog(wxT("libdcpomatic2-wx"));
+               locale->AddCatalog(wxT("dcpomatic2"));
 
                if (!locale->IsOk()) {
                        delete locale;
@@ -400,24 +519,30 @@ dcpomatic_setup_i18n ()
        }
 }
 
+#endif
+
+
 int
 wx_get (wxSpinCtrl* w)
 {
        return w->GetValue ();
 }
 
+
 int
 wx_get (wxChoice* w)
 {
        return w->GetSelection ();
 }
 
+
 double
 wx_get (wxSpinCtrlDouble* w)
 {
        return w->GetValue ();
 }
 
+
 /** @param s String of the form Context|String
  *  @return translation, or String if no translation is available.
  */
@@ -436,6 +561,7 @@ context_translation (wxString s)
        return t;
 }
 
+
 wxString
 time_to_timecode (DCPTime t, double fps)
 {
@@ -450,37 +576,57 @@ time_to_timecode (DCPTime t, double fps)
        return wxString::Format (wxT("%02d:%02d:%02d.%02d"), h, m, s, f);
 }
 
+
 void
 setup_audio_channels_choice (wxChoice* choice, int minimum)
 {
-       vector<pair<string, string> > items;
+       vector<pair<string, string>> items;
        for (int i = minimum; i <= 16; i += 2) {
                if (i == 2) {
-                       items.push_back (make_pair (wx_to_std (_("2 - stereo")), locale_convert<string> (i)));
+                       items.push_back (make_pair(wx_to_std(_("2 - stereo")), locale_convert<string>(i)));
                } else if (i == 4) {
-                       items.push_back (make_pair (wx_to_std (_("4 - L/C/R/Lfe")), locale_convert<string> (i)));
+                       items.push_back (make_pair(wx_to_std(_("4 - L/C/R/Lfe")), locale_convert<string>(i)));
                } else if (i == 6) {
-                       items.push_back (make_pair (wx_to_std (_("6 - 5.1")), locale_convert<string> (i)));
+                       items.push_back (make_pair(wx_to_std(_("6 - 5.1")), locale_convert<string>(i)));
                } else if (i == 8) {
-                       items.push_back (make_pair (wx_to_std (_("8 - 5.1/HI/VI")), locale_convert<string> (i)));
+                       items.push_back (make_pair(wx_to_std(_("8 - 5.1/HI/VI")), locale_convert<string>(i)));
                } else if (i == 12) {
-                       items.push_back (make_pair (wx_to_std (_("12 - 7.1/HI/VI")), locale_convert<string> (i)));
+                       items.push_back (make_pair(wx_to_std(_("12 - 7.1/HI/VI")), locale_convert<string>(i)));
                } else {
-                       items.push_back (make_pair (locale_convert<string> (i), locale_convert<string> (i)));
+                       items.push_back (make_pair(locale_convert<string> (i), locale_convert<string>(i)));
                }
        }
 
        checked_set (choice, items);
 }
 
-wxSplashScreen *
+
+wxSplashScreen*
 maybe_show_splash ()
 {
+       if (!variant::show_splash()) {
+               return nullptr;
+       }
+
        wxSplashScreen* splash = nullptr;
+
        try {
                wxBitmap bitmap;
-               if (bitmap.LoadFile(bitmap_path("splash"), wxBITMAP_TYPE_PNG)) {
-                       splash = new wxSplashScreen (bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, 0, -1);
+               if (bitmap.LoadFile(bitmap_path("splash.png"), wxBITMAP_TYPE_PNG)) {
+                       {
+                               /* This wxMemoryDC must be destroyed before bitmap can be used elsewhere */
+                               wxMemoryDC dc(bitmap);
+                               auto const version = wxString::Format("%s (%s)", dcpomatic_version, dcpomatic_git_commit);
+                               auto screen_size = dc.GetSize();
+                               auto text_size = dc.GetTextExtent(version);
+                               dc.DrawText(version, (screen_size.GetWidth() - text_size.GetWidth()) / 2, 236);
+                       }
+#ifdef DCPOMATIC_WINDOWS
+                       /* Having wxSTAY_ON_TOP means error dialogues hide behind the splash screen on Windows, no matter what I try */
+                       splash = new wxSplashScreen(bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, nullptr, -1, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE | wxFRAME_NO_TASKBAR);
+#else
+                       splash = new wxSplashScreen(bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, nullptr, -1);
+#endif
                        wxYield ();
                }
        } catch (boost::filesystem::filesystem_error& e) {
@@ -490,6 +636,7 @@ maybe_show_splash ()
        return splash;
 }
 
+
 double
 calculate_mark_interval (double mark_interval)
 {
@@ -543,35 +690,35 @@ display_progress (wxString title, wxString task)
 int
 get_offsets (vector<Offset>& offsets)
 {
-       offsets.push_back (Offset(_("UTC-11"),  -11,  0));
-       offsets.push_back (Offset(_("UTC-10"),  -10,  0));
-       offsets.push_back (Offset(_("UTC-9"),    -9,  0));
-       offsets.push_back (Offset(_("UTC-8"),    -8,  0));
-       offsets.push_back (Offset(_("UTC-7"),    -7,  0));
-       offsets.push_back (Offset(_("UTC-6"),    -6,  0));
-       offsets.push_back (Offset(_("UTC-5"),    -5,  0));
-       offsets.push_back (Offset(_("UTC-4:30"), -4, 30));
-       offsets.push_back (Offset(_("UTC-4"),    -4,  0));
-       offsets.push_back (Offset(_("UTC-3:30"), -3, 30));
-       offsets.push_back (Offset(_("UTC-3"),    -3,  0));
-       offsets.push_back (Offset(_("UTC-2"),    -2,  0));
-       offsets.push_back (Offset(_("UTC-1"),    -1,  0));
+       offsets.push_back({_("UTC-11"),  dcp::UTCOffset(-11,   0)});
+       offsets.push_back({_("UTC-10"),  dcp::UTCOffset(-10,   0)});
+       offsets.push_back({_("UTC-9"),   dcp::UTCOffset( -9,   0)});
+       offsets.push_back({_("UTC-8"),   dcp::UTCOffset( -8,   0)});
+       offsets.push_back({_("UTC-7"),   dcp::UTCOffset( -7,   0)});
+       offsets.push_back({_("UTC-6"),   dcp::UTCOffset( -6,   0)});
+       offsets.push_back({_("UTC-5"),   dcp::UTCOffset( -5,   0)});
+       offsets.push_back({_("UTC-4:30"),dcp::UTCOffset( -4, -30)});
+       offsets.push_back({_("UTC-4"),   dcp::UTCOffset( -4,   0)});
+       offsets.push_back({_("UTC-3:30"),dcp::UTCOffset( -3, -30)});
+       offsets.push_back({_("UTC-3"),   dcp::UTCOffset( -3,   0)});
+       offsets.push_back({_("UTC-2"),   dcp::UTCOffset( -2,   0)});
+       offsets.push_back({_("UTC-1"),   dcp::UTCOffset( -1,   0)});
        int utc = offsets.size();
-       offsets.push_back (Offset(_("UTC")  ,     0,  0));
-       offsets.push_back (Offset(_("UTC+1"),     1,  0));
-       offsets.push_back (Offset(_("UTC+2"),     2,  0));
-       offsets.push_back (Offset(_("UTC+3"),     3,  0));
-       offsets.push_back (Offset(_("UTC+4"),     4,  0));
-       offsets.push_back (Offset(_("UTC+5"),     5,  0));
-       offsets.push_back (Offset(_("UTC+5:30"),  5, 30));
-       offsets.push_back (Offset(_("UTC+6"),     6,  0));
-       offsets.push_back (Offset(_("UTC+7"),     7,  0));
-       offsets.push_back (Offset(_("UTC+8"),     8,  0));
-       offsets.push_back (Offset(_("UTC+9"),     9,  0));
-       offsets.push_back (Offset(_("UTC+9:30"),  9, 30));
-       offsets.push_back (Offset(_("UTC+10"),   10,  0));
-       offsets.push_back (Offset(_("UTC+11"),   11,  0));
-       offsets.push_back (Offset(_("UTC+12"),   12,  0));
+       offsets.push_back({_("UTC")  ,   dcp::UTCOffset(  0,   0)});
+       offsets.push_back({_("UTC+1"),   dcp::UTCOffset(  1,   0)});
+       offsets.push_back({_("UTC+2"),   dcp::UTCOffset(  2,   0)});
+       offsets.push_back({_("UTC+3"),   dcp::UTCOffset(  3,   0)});
+       offsets.push_back({_("UTC+4"),   dcp::UTCOffset(  4,   0)});
+       offsets.push_back({_("UTC+5"),   dcp::UTCOffset(  5,   0)});
+       offsets.push_back({_("UTC+5:30"),dcp::UTCOffset(  5,  30)});
+       offsets.push_back({_("UTC+6"),   dcp::UTCOffset(  6,   0)});
+       offsets.push_back({_("UTC+7"),   dcp::UTCOffset(  7,   0)});
+       offsets.push_back({_("UTC+8"),   dcp::UTCOffset(  8,   0)});
+       offsets.push_back({_("UTC+9"),   dcp::UTCOffset(  9,   0)});
+       offsets.push_back({_("UTC+9:30"),dcp::UTCOffset(  9,  30)});
+       offsets.push_back({_("UTC+10"),  dcp::UTCOffset( 10,   0)});
+       offsets.push_back({_("UTC+11"),  dcp::UTCOffset( 11,   0)});
+       offsets.push_back({_("UTC+12"),  dcp::UTCOffset( 12,   0)});
 
        return utc;
 }
@@ -590,22 +737,97 @@ bitmap_path (string name)
        } else {
                base = resources_path();
        }
+
+       if (!boost::filesystem::exists(base / name)) {
+               base = path / boost::filesystem::path("osx/preferences");
+       }
 #else
        base = resources_path();
 #endif
 
-       boost::filesystem::path p = base / String::compose("%1.png", name);
+       auto p = base / name;
        return std_to_wx (p.string());
 }
 
 
+wxString
+icon_path(string name)
+{
+       return gui_is_dark() ? bitmap_path(String::compose("%1_white.png", name)) : bitmap_path(String::compose("%1_black.png", name));
+}
+
+
 wxSize
 small_button_size (wxWindow* parent, wxString text)
 {
        wxClientDC dc (parent);
        auto size = dc.GetTextExtent (text);
        size.SetHeight (-1);
-       size.IncBy (24, 0);
+       size.IncBy (32, 0);
        return size;
 }
 
+
+bool
+gui_is_dark ()
+{
+#if defined(DCPOMATIC_OSX) && wxCHECK_VERSION(3, 1, 0)
+       auto appearance = wxSystemSettings::GetAppearance();
+       return appearance.IsDark();
+#else
+       return false;
+#endif
+}
+
+
+#if wxCHECK_VERSION(3,1,0)
+double
+dpi_scale_factor (wxWindow* window)
+{
+       return window->GetDPIScaleFactor();
+}
+#else
+double
+dpi_scale_factor (wxWindow*)
+{
+       return 1;
+}
+#endif
+
+
+
+int
+search_ctrl_height ()
+{
+#ifdef __WXGTK3__
+       return 30;
+#else
+       return -1;
+#endif
+}
+
+
+void
+report_config_load_failure(wxWindow* parent, Config::LoadFailure what)
+{
+       switch (what) {
+       case Config::LoadFailure::CONFIG:
+               message_dialog(parent, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
+               break;
+       case Config::LoadFailure::CINEMAS:
+               message_dialog(
+                       parent,
+                       _(wxString::Format("The cinemas list for creating KDMs (cinemas.xml) failed to load.  Please check the numbered backup files in %s",
+                                          std_to_wx(Config::instance()->cinemas_file().parent_path().string())))
+                       );
+               break;
+       case Config::LoadFailure::DKDM_RECIPIENTS:
+               message_dialog(
+                       parent,
+                       _(wxString::Format("The recipients list for creating DKDMs (dkdm_recipients.xml) failed to load.  Please check the numbered backup files in %s",
+                                          std_to_wx(Config::instance()->dkdm_recipients_file().parent_path().string())))
+                       );
+               break;
+       }
+}
+