2 Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
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.
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.
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/>.
22 #include "wx/wx_util.h"
23 #include "wx/wx_signal_manager.h"
24 #include "wx/content_view.h"
25 #include "wx/dcpomatic_button.h"
26 #include "wx/about_dialog.h"
27 #include "wx/playlist_editor_config_dialog.h"
28 #include "lib/config.h"
29 #include "lib/cross.h"
30 #include "lib/dcp_content.h"
33 #include "lib/spl_entry.h"
36 #include <wx/listctrl.h>
37 #include <wx/imaglist.h>
38 #include <wx/spinctrl.h>
39 #include <wx/preferences.h>
45 using std::make_shared;
47 using std::shared_ptr;
52 using boost::optional;
53 using std::dynamic_pointer_cast;
54 #if BOOST_VERSION >= 106100
55 using namespace boost::placeholders;
59 class ContentDialog : public wxDialog, public ContentStore
62 ContentDialog (wxWindow* parent)
63 : wxDialog (parent, wxID_ANY, _("Add content"), wxDefaultPosition, wxSize(800, 640))
64 , _content_view (new ContentView(this))
66 _content_view->update ();
68 auto overall_sizer = new wxBoxSizer (wxVERTICAL);
69 SetSizer (overall_sizer);
71 overall_sizer->Add (_content_view, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
73 auto buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
75 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
78 overall_sizer->Layout ();
80 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&ContentView::update, _content_view));
83 shared_ptr<Content> selected () const
85 return _content_view->selected ();
88 shared_ptr<Content> get (string digest) const
90 return _content_view->get (digest);
94 ContentView* _content_view;
95 boost::signals2::scoped_connection _config_changed_connection;
103 PlaylistList (wxPanel* parent, ContentStore* content_store)
104 : _sizer (new wxBoxSizer(wxVERTICAL))
105 , _content_store (content_store)
107 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
108 label->SetLabelMarkup (_("<b>Playlists</b>"));
109 _sizer->Add (label, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
111 _list = new wxListCtrl (
112 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
115 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 840);
116 _list->AppendColumn (_("Length"), wxLIST_FORMAT_LEFT, 100);
118 auto button_sizer = new wxBoxSizer (wxVERTICAL);
119 _new = new Button (parent, _("New"));
120 button_sizer->Add (_new, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
121 _delete = new Button (parent, _("Delete"));
122 button_sizer->Add (_delete, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
124 auto list = new wxBoxSizer (wxHORIZONTAL);
125 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
126 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
132 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistList::selection_changed, this));
133 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistList::selection_changed, this));
134 _new->Bind (wxEVT_BUTTON, bind(&PlaylistList::new_playlist, this));
135 _delete->Bind (wxEVT_BUTTON, bind(&PlaylistList::delete_playlist, this));
143 shared_ptr<SignalSPL> first_playlist () const
145 if (_playlists.empty()) {
149 return _playlists.front ();
152 boost::signals2::signal<void (shared_ptr<SignalSPL>)> Edit;
155 void add_playlist_to_view (shared_ptr<const SignalSPL> playlist)
158 item.SetId (_list->GetItemCount());
159 long const N = _list->InsertItem (item);
160 _list->SetItem (N, 0, std_to_wx(playlist->name()));
163 void add_playlist_to_model (shared_ptr<SignalSPL> playlist)
165 _playlists.push_back (playlist);
166 playlist->NameChanged.connect (bind(&PlaylistList::name_changed, this, weak_ptr<SignalSPL>(playlist)));
169 void name_changed (weak_ptr<SignalSPL> wp)
171 auto playlist = wp.lock ();
177 for (auto i: _playlists) {
179 _list->SetItem (N, 0, std_to_wx(i->name()));
185 void load_playlists ()
187 auto path = Config::instance()->player_playlist_directory();
192 _list->DeleteAllItems ();
194 for (auto i: boost::filesystem::directory_iterator(*path)) {
195 auto spl = make_shared<SignalSPL>();
197 spl->read (i, _content_store);
198 add_playlist_to_model (spl);
202 for (auto i: _playlists) {
203 add_playlist_to_view (i);
209 shared_ptr<SignalSPL> spl (new SignalSPL(wx_to_std(_("New Playlist"))));
210 add_playlist_to_model (spl);
211 add_playlist_to_view (spl);
212 _list->SetItemState (_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
215 void delete_playlist ()
217 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
218 if (selected < 0 || selected >= int(_playlists.size())) {
222 auto dir = Config::instance()->player_playlist_directory();
227 boost::filesystem::remove (*dir / (_playlists[selected]->id() + ".xml"));
228 _list->DeleteItem (selected);
229 _playlists.erase (_playlists.begin() + selected);
231 Edit (shared_ptr<SignalSPL>());
234 void selection_changed ()
236 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
237 if (selected < 0 || selected >= int(_playlists.size())) {
238 Edit (shared_ptr<SignalSPL>());
240 Edit (_playlists[selected]);
248 vector<shared_ptr<SignalSPL>> _playlists;
249 ContentStore* _content_store;
253 class PlaylistContent
256 PlaylistContent (wxPanel* parent, ContentDialog* content_dialog)
257 : _content_dialog (content_dialog)
258 , _sizer (new wxBoxSizer(wxVERTICAL))
260 auto title = new wxBoxSizer (wxHORIZONTAL);
261 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
262 label->SetLabelMarkup (_("<b>Playlist:</b>"));
263 title->Add (label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
264 _name = new wxTextCtrl (parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(400, -1));
265 title->Add (_name, 0, wxRIGHT, DCPOMATIC_SIZER_GAP);
266 _sizer->Add (title, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
268 auto list = new wxBoxSizer (wxHORIZONTAL);
270 _list = new wxListCtrl (
271 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
274 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 400);
275 _list->AppendColumn (_("CPL"), wxLIST_FORMAT_LEFT, 350);
276 _list->AppendColumn (_("Type"), wxLIST_FORMAT_LEFT, 100);
277 _list->AppendColumn (_("Encrypted"), wxLIST_FORMAT_CENTRE, 90);
279 auto images = new wxImageList (16, 16);
282 tick_icon.LoadFile (bitmap_path("tick"), wxBITMAP_TYPE_PNG);
283 no_tick_icon.LoadFile (bitmap_path("no_tick"), wxBITMAP_TYPE_PNG);
284 images->Add (tick_icon);
285 images->Add (no_tick_icon);
287 _list->SetImageList (images, wxIMAGE_LIST_SMALL);
289 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
291 auto button_sizer = new wxBoxSizer (wxVERTICAL);
292 _up = new Button (parent, _("Up"));
293 _down = new Button (parent, _("Down"));
294 _add = new Button (parent, _("Add"));
295 _remove = new Button (parent, _("Remove"));
296 button_sizer->Add (_up, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
297 button_sizer->Add (_down, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
298 button_sizer->Add (_add, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
299 button_sizer->Add (_remove, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
301 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
305 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistContent::setup_sensitivity, this));
306 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistContent::setup_sensitivity, this));
307 _name->Bind (wxEVT_TEXT, bind(&PlaylistContent::name_changed, this));
308 _up->Bind (wxEVT_BUTTON, bind(&PlaylistContent::up_clicked, this));
309 _down->Bind (wxEVT_BUTTON, bind(&PlaylistContent::down_clicked, this));
310 _add->Bind (wxEVT_BUTTON, bind(&PlaylistContent::add_clicked, this));
311 _remove->Bind (wxEVT_BUTTON, bind(&PlaylistContent::remove_clicked, this));
319 void set (shared_ptr<SignalSPL> playlist)
321 _playlist = playlist;
322 _list->DeleteAllItems ();
324 for (auto i: _playlist->get()) {
327 _name->SetValue (std_to_wx(_playlist->name()));
329 _name->SetValue (wxT(""));
331 setup_sensitivity ();
334 shared_ptr<SignalSPL> playlist () const
344 _playlist->set_name (wx_to_std(_name->GetValue()));
348 void add (SPLEntry e)
351 item.SetId (_list->GetItemCount());
352 long const N = _list->InsertItem (item);
356 void set_item (long N, SPLEntry e)
358 _list->SetItem (N, 0, std_to_wx(e.name));
359 _list->SetItem (N, 1, std_to_wx(e.id));
360 _list->SetItem (N, 2, std_to_wx(dcp::content_kind_to_string(e.kind)));
361 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
364 void setup_sensitivity ()
366 bool const have_list = static_cast<bool>(_playlist);
367 int const num_selected = _list->GetSelectedItemCount ();
368 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
369 _name->Enable (have_list);
370 _list->Enable (have_list);
371 _up->Enable (have_list && selected > 0);
372 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
373 _add->Enable (have_list);
374 _remove->Enable (have_list && num_selected > 0);
379 int const r = _content_dialog->ShowModal ();
381 auto content = _content_dialog->selected ();
383 SPLEntry e (content);
385 DCPOMATIC_ASSERT (_playlist);
393 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
398 DCPOMATIC_ASSERT (_playlist);
400 auto tmp = (*_playlist)[s];
401 (*_playlist)[s] = (*_playlist)[s-1];
402 (*_playlist)[s-1] = tmp;
404 set_item (s - 1, (*_playlist)[s-1]);
405 set_item (s, (*_playlist)[s]);
410 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
411 if (s > (_list->GetItemCount() - 1)) {
415 DCPOMATIC_ASSERT (_playlist);
417 auto tmp = (*_playlist)[s];
418 (*_playlist)[s] = (*_playlist)[s+1];
419 (*_playlist)[s+1] = tmp;
421 set_item (s + 1, (*_playlist)[s+1]);
422 set_item (s, (*_playlist)[s]);
425 void remove_clicked ()
427 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
432 DCPOMATIC_ASSERT (_playlist);
433 _playlist->remove (s);
434 _list->DeleteItem (s);
437 ContentDialog* _content_dialog;
445 shared_ptr<SignalSPL> _playlist;
449 class DOMFrame : public wxFrame
452 explicit DOMFrame (wxString const & title)
453 : wxFrame (nullptr, wxID_ANY, title)
454 , _content_dialog (new ContentDialog(this))
455 , _config_dialog (nullptr)
457 auto bar = new wxMenuBar;
461 /* Use a panel as the only child of the Frame so that we avoid
462 the dark-grey background on Windows.
464 auto overall_panel = new wxPanel (this, wxID_ANY);
465 auto sizer = new wxBoxSizer (wxVERTICAL);
467 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
468 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
470 sizer->Add (_playlist_list->sizer());
471 sizer->Add (_playlist_content->sizer());
473 overall_panel->SetSizer (sizer);
475 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
477 _playlist_content->set (_playlist_list->first_playlist());
479 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
480 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
481 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
483 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this));
490 /* false here allows the close handler to veto the close request */
496 auto d = new AboutDialog (this);
501 void edit_preferences ()
503 if (!_config_dialog) {
504 _config_dialog = create_playlist_editor_config_dialog ();
506 _config_dialog->Show (this);
509 void change_playlist (shared_ptr<SignalSPL> playlist)
511 auto old = _playlist_content->playlist ();
515 _playlist_content->set (playlist);
518 void save_playlist (shared_ptr<SignalSPL> playlist)
520 auto dir = Config::instance()->player_playlist_directory();
522 error_dialog (this, _("No playlist folder is specified in preferences. Please set one and then try again."));
525 playlist->write (*dir / (playlist->id() + ".xml"));
528 void setup_menu (wxMenuBar* m)
530 auto file = new wxMenu;
532 file->Append (wxID_EXIT, _("&Exit"));
534 file->Append (wxID_EXIT, _("&Quit"));
538 auto edit = new wxMenu;
539 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
542 auto help = new wxMenu;
544 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
546 help->Append (wxID_ABOUT, _("About"));
549 m->Append (file, _("&File"));
551 m->Append (edit, _("&Edit"));
553 m->Append (help, _("&Help"));
557 void config_changed ()
560 Config::instance()->write_config();
561 } catch (exception& e) {
565 _("Could not write to config file at %s. Your changes have not been saved."),
566 std_to_wx (Config::instance()->cinemas_file().string()).data()
572 ContentDialog* _content_dialog;
573 PlaylistList* _playlist_list;
574 PlaylistContent* _playlist_content;
575 wxPreferencesEditor* _config_dialog;
576 boost::signals2::scoped_connection _config_changed_connection;
581 * @brief The magic App class for wxWidgets.
583 class App : public wxApp
596 wxInitAllImageHandlers ();
597 SetAppName (_("DCP-o-matic Playlist Editor"));
599 if (!wxApp::OnInit()) {
603 #ifdef DCPOMATIC_LINUX
604 unsetenv ("UBUNTU_MENUPROXY");
608 make_foreground_application ();
611 dcpomatic_setup_path_encoding ();
613 /* Enable i18n; this will create a Config object
614 to look for a force-configured language. This Config
615 object will be wrong, however, because dcpomatic_setup
616 hasn't yet been called and there aren't any filters etc.
619 dcpomatic_setup_i18n ();
621 /* Set things up, including filters etc.
622 which will now be internationalised correctly.
626 /* Force the configuration to be re-loaded correctly next
631 _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
632 SetTopWindow (_frame);
636 signal_manager = new wxSignalManager (this);
637 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
643 error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
647 /* An unhandled exception has occurred inside the main event loop */
648 bool OnExceptionInMainLoop ()
652 } catch (FileError& e) {
656 _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
657 std_to_wx (e.what()),
658 std_to_wx (e.file().string().c_str ())
661 } catch (exception& e) {
665 _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
666 std_to_wx (e.what ())
670 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
673 /* This will terminate the program */
677 void OnUnhandledException ()
679 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
684 signal_manager->ui_idle ();