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/about_dialog.h"
23 #include "wx/content_view.h"
24 #include "wx/dcpomatic_button.h"
25 #include "wx/playlist_editor_config_dialog.h"
26 #include "wx/wx_signal_manager.h"
27 #include "wx/wx_util.h"
28 #include "lib/config.h"
29 #include "lib/constants.h"
30 #include "lib/cross.h"
31 #include "lib/dcp_content.h"
34 #include "lib/spl_entry.h"
35 #include <dcp/filesystem.h>
36 #include <dcp/warnings.h>
37 LIBDCP_DISABLE_WARNINGS
38 #include <wx/imaglist.h>
39 #include <wx/listctrl.h>
40 #include <wx/preferences.h>
41 #include <wx/spinctrl.h>
43 LIBDCP_ENABLE_WARNINGS
49 using std::make_shared;
51 using std::shared_ptr;
56 using boost::optional;
57 using std::dynamic_pointer_cast;
58 #if BOOST_VERSION >= 106100
59 using namespace boost::placeholders;
65 save_playlist(shared_ptr<const SPL> playlist)
67 if (auto dir = Config::instance()->player_playlist_directory()) {
68 playlist->write(*dir / (playlist->id() + ".xml"));
73 class ContentDialog : public wxDialog, public ContentStore
76 ContentDialog (wxWindow* parent)
77 : wxDialog (parent, wxID_ANY, _("Add content"), wxDefaultPosition, wxSize(800, 640))
78 , _content_view (new ContentView(this))
80 _content_view->update ();
82 auto overall_sizer = new wxBoxSizer (wxVERTICAL);
83 SetSizer (overall_sizer);
85 overall_sizer->Add (_content_view, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
87 auto buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
89 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
92 overall_sizer->Layout ();
94 _content_view->Bind(wxEVT_LIST_ITEM_ACTIVATED, boost::bind(&ContentDialog::EndModal, this, wxID_OK));
95 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&ContentView::update, _content_view));
98 shared_ptr<Content> selected () const
100 return _content_view->selected ();
103 shared_ptr<Content> get (string digest) const override
105 return _content_view->get (digest);
109 ContentView* _content_view;
110 boost::signals2::scoped_connection _config_changed_connection;
118 PlaylistList (wxPanel* parent, ContentStore* content_store)
119 : _sizer (new wxBoxSizer(wxVERTICAL))
120 , _content_store (content_store)
123 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
124 label->SetLabelMarkup (_("<b>Playlists</b>"));
125 _sizer->Add (label, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
127 _list = new wxListCtrl (
128 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
131 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 840);
132 _list->AppendColumn (_("Length"), wxLIST_FORMAT_LEFT, 100);
134 auto button_sizer = new wxBoxSizer (wxVERTICAL);
135 _new = new Button (parent, _("New"));
136 button_sizer->Add (_new, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
137 _delete = new Button (parent, _("Delete"));
138 button_sizer->Add (_delete, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
140 auto list = new wxBoxSizer (wxHORIZONTAL);
141 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
142 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
148 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistList::selection_changed, this));
149 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistList::selection_changed, this));
150 _new->Bind (wxEVT_BUTTON, bind(&PlaylistList::new_playlist, this));
151 _delete->Bind (wxEVT_BUTTON, bind(&PlaylistList::delete_playlist, this));
161 shared_ptr<SignalSPL> first_playlist () const
163 if (_playlists.empty()) {
167 return _playlists.front ();
170 boost::signals2::signal<void (shared_ptr<SignalSPL>)> Edit;
173 void setup_sensitivity()
175 _delete->Enable(static_cast<bool>(selected()));
178 void add_playlist_to_view (shared_ptr<const SignalSPL> playlist)
181 item.SetId (_list->GetItemCount());
182 long const N = _list->InsertItem (item);
183 _list->SetItem (N, 0, std_to_wx(playlist->name()));
186 void add_playlist_to_model (shared_ptr<SignalSPL> playlist)
188 _playlists.push_back (playlist);
189 playlist->Changed.connect(bind(&PlaylistList::changed, this, weak_ptr<SignalSPL>(playlist), _1));
192 void changed(weak_ptr<SignalSPL> wp, SignalSPL::Change change)
194 auto playlist = wp.lock ();
200 case SignalSPL::Change::NAME:
203 for (auto i: _playlists) {
205 _list->SetItem (N, 0, std_to_wx(i->name()));
211 case SignalSPL::Change::CONTENT:
212 save_playlist(playlist);
217 void load_playlists ()
219 auto path = Config::instance()->player_playlist_directory();
224 _list->DeleteAllItems ();
227 for (auto i: dcp::filesystem::directory_iterator(*path)) {
228 auto spl = make_shared<SignalSPL>();
230 spl->read (i, _content_store);
231 add_playlist_to_model (spl);
236 for (auto i: _playlists) {
237 add_playlist_to_view (i);
243 auto dir = Config::instance()->player_playlist_directory();
245 error_dialog(_parent, _("No playlist folder is specified in preferences. Please set one and then try again."));
249 shared_ptr<SignalSPL> spl (new SignalSPL(wx_to_std(_("New Playlist"))));
250 add_playlist_to_model (spl);
251 add_playlist_to_view (spl);
252 _list->SetItemState (_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
255 boost::optional<int> selected() const
257 long int selected = _list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
258 if (selected < 0 || selected >= int(_playlists.size())) {
265 void delete_playlist ()
267 auto index = selected();
272 auto dir = Config::instance()->player_playlist_directory();
277 dcp::filesystem::remove(*dir / (_playlists[*index]->id() + ".xml"));
278 _list->DeleteItem(*index);
279 _playlists.erase(_playlists.begin() + *index);
281 Edit(shared_ptr<SignalSPL>());
284 void selection_changed ()
286 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
287 if (selected < 0 || selected >= int(_playlists.size())) {
288 Edit (shared_ptr<SignalSPL>());
290 Edit (_playlists[selected]);
300 vector<shared_ptr<SignalSPL>> _playlists;
301 ContentStore* _content_store;
306 class PlaylistContent
309 PlaylistContent (wxPanel* parent, ContentDialog* content_dialog)
310 : _content_dialog (content_dialog)
311 , _sizer (new wxBoxSizer(wxVERTICAL))
313 auto title = new wxBoxSizer (wxHORIZONTAL);
314 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
315 label->SetLabelMarkup (_("<b>Playlist:</b>"));
316 title->Add (label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
317 _name = new wxTextCtrl (parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(400, -1));
318 title->Add (_name, 0, wxRIGHT, DCPOMATIC_SIZER_GAP);
319 _save_name = new Button(parent, _("Save"));
320 title->Add(_save_name);
321 _sizer->Add (title, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
323 auto list = new wxBoxSizer (wxHORIZONTAL);
325 _list = new wxListCtrl (
326 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
329 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 400);
330 _list->AppendColumn (_("CPL"), wxLIST_FORMAT_LEFT, 350);
331 _list->AppendColumn (_("Type"), wxLIST_FORMAT_LEFT, 100);
332 _list->AppendColumn (_("Encrypted"), wxLIST_FORMAT_CENTRE, 90);
334 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
336 auto button_sizer = new wxBoxSizer (wxVERTICAL);
337 _up = new Button (parent, _("Up"));
338 _down = new Button (parent, _("Down"));
339 _add = new Button (parent, _("Add"));
340 _remove = new Button (parent, _("Remove"));
341 button_sizer->Add (_up, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
342 button_sizer->Add (_down, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
343 button_sizer->Add (_add, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
344 button_sizer->Add (_remove, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
346 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
350 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistContent::setup_sensitivity, this));
351 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistContent::setup_sensitivity, this));
352 _name->Bind (wxEVT_TEXT, bind(&PlaylistContent::name_changed, this));
353 _save_name->bind(&PlaylistContent::save_name_clicked, this);
354 _up->Bind (wxEVT_BUTTON, bind(&PlaylistContent::up_clicked, this));
355 _down->Bind (wxEVT_BUTTON, bind(&PlaylistContent::down_clicked, this));
356 _add->Bind (wxEVT_BUTTON, bind(&PlaylistContent::add_clicked, this));
357 _remove->Bind (wxEVT_BUTTON, bind(&PlaylistContent::remove_clicked, this));
367 void set (shared_ptr<SignalSPL> playlist)
369 _playlist = playlist;
370 _list->DeleteAllItems ();
372 for (auto i: _playlist->get()) {
375 _name->SetValue (std_to_wx(_playlist->name()));
377 _name->SetValue (wxT(""));
379 setup_sensitivity ();
382 shared_ptr<SignalSPL> playlist () const
389 void save_name_clicked()
392 _playlist->set_name(wx_to_std(_name->GetValue()));
393 save_playlist(_playlist);
403 void add (SPLEntry e)
406 item.SetId (_list->GetItemCount());
407 long const N = _list->InsertItem (item);
411 void set_item (long N, SPLEntry e)
413 _list->SetItem (N, 0, std_to_wx(e.name));
414 _list->SetItem (N, 1, std_to_wx(e.id));
415 _list->SetItem (N, 2, std_to_wx(e.kind->name()));
416 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
419 void setup_sensitivity ()
421 bool const have_list = static_cast<bool>(_playlist);
422 int const num_selected = _list->GetSelectedItemCount ();
423 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
424 _name->Enable (have_list);
425 _save_name->Enable(_playlist && _playlist->name() != wx_to_std(_name->GetValue()));
426 _list->Enable (have_list);
427 _up->Enable (have_list && selected > 0);
428 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
429 _add->Enable (have_list);
430 _remove->Enable (have_list && num_selected > 0);
435 int const r = _content_dialog->ShowModal ();
437 auto content = _content_dialog->selected ();
439 SPLEntry e (content);
441 DCPOMATIC_ASSERT (_playlist);
449 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
454 DCPOMATIC_ASSERT (_playlist);
456 _playlist->swap(s, s - 1);
458 set_item (s - 1, (*_playlist)[s-1]);
459 set_item (s, (*_playlist)[s]);
464 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
465 if (s > (_list->GetItemCount() - 1)) {
469 DCPOMATIC_ASSERT (_playlist);
471 _playlist->swap(s, s + 1);
473 set_item (s + 1, (*_playlist)[s+1]);
474 set_item (s, (*_playlist)[s]);
477 void remove_clicked ()
479 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
484 DCPOMATIC_ASSERT (_playlist);
485 _playlist->remove (s);
486 _list->DeleteItem (s);
489 ContentDialog* _content_dialog;
498 shared_ptr<SignalSPL> _playlist;
502 class DOMFrame : public wxFrame
505 explicit DOMFrame (wxString const & title)
506 : wxFrame (nullptr, wxID_ANY, title)
507 , _content_dialog (new ContentDialog(this))
508 , _config_dialog (nullptr)
510 auto bar = new wxMenuBar;
514 /* Use a panel as the only child of the Frame so that we avoid
515 the dark-grey background on Windows.
517 auto overall_panel = new wxPanel (this, wxID_ANY);
518 auto sizer = new wxBoxSizer (wxVERTICAL);
520 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
521 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
523 sizer->Add (_playlist_list->sizer());
524 sizer->Add (_playlist_content->sizer());
526 overall_panel->SetSizer (sizer);
528 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
530 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
531 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
532 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
534 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this));
541 /* false here allows the close handler to veto the close request */
547 auto d = make_wx<AboutDialog>(this);
551 void edit_preferences ()
553 if (!_config_dialog) {
554 _config_dialog = create_playlist_editor_config_dialog ();
556 _config_dialog->Show (this);
559 void change_playlist (shared_ptr<SignalSPL> playlist)
561 auto old = _playlist_content->playlist ();
565 _playlist_content->set (playlist);
568 void setup_menu (wxMenuBar* m)
570 auto file = new wxMenu;
572 file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
573 file->Append (wxID_EXIT, _("&Exit"));
575 file->Append (wxID_EXIT, _("&Quit"));
579 auto edit = new wxMenu;
580 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
583 auto help = new wxMenu;
585 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
587 help->Append (wxID_ABOUT, _("About"));
590 m->Append (file, _("&File"));
592 m->Append (edit, _("&Edit"));
594 m->Append (help, _("&Help"));
598 void config_changed ()
601 Config::instance()->write_config();
602 } catch (exception& e) {
606 _("Could not write to config file at %s. Your changes have not been saved."),
607 std_to_wx (Config::instance()->cinemas_file().string()).data()
613 ContentDialog* _content_dialog;
614 PlaylistList* _playlist_list;
615 PlaylistContent* _playlist_content;
616 wxPreferencesEditor* _config_dialog;
617 boost::signals2::scoped_connection _config_changed_connection;
622 * @brief The magic App class for wxWidgets.
624 class App : public wxApp
634 bool OnInit () override
637 wxInitAllImageHandlers ();
638 SetAppName (_("DCP-o-matic Playlist Editor"));
640 if (!wxApp::OnInit()) {
644 #ifdef DCPOMATIC_LINUX
645 unsetenv ("UBUNTU_MENUPROXY");
649 make_foreground_application ();
652 dcpomatic_setup_path_encoding ();
654 /* Enable i18n; this will create a Config object
655 to look for a force-configured language. This Config
656 object will be wrong, however, because dcpomatic_setup
657 hasn't yet been called and there aren't any filters etc.
660 dcpomatic_setup_i18n ();
662 /* Set things up, including filters etc.
663 which will now be internationalised correctly.
667 /* Force the configuration to be re-loaded correctly next
672 _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
673 SetTopWindow (_frame);
677 signal_manager = new wxSignalManager (this);
678 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
684 error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
688 /* An unhandled exception has occurred inside the main event loop */
689 bool OnExceptionInMainLoop () override
693 } catch (FileError& e) {
697 _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
698 std_to_wx (e.what()),
699 std_to_wx (e.file().string().c_str ())
702 } catch (exception& e) {
706 _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
707 std_to_wx (e.what ())
711 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
714 /* This will terminate the program */
718 void OnUnhandledException () override
720 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
725 signal_manager->ui_idle ();