2 Copyright (C) 2018-2020 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/>.
21 #include "../wx/wx_util.h"
22 #include "../wx/wx_signal_manager.h"
23 #include "../wx/content_view.h"
24 #include "../wx/dcpomatic_button.h"
25 #include "../wx/about_dialog.h"
26 #include "../wx/playlist_editor_config_dialog.h"
27 #include "../lib/util.h"
28 #include "../lib/config.h"
29 #include "../lib/cross.h"
30 #include "../lib/film.h"
31 #include "../lib/dcp_content.h"
32 #include "../lib/spl_entry.h"
33 #include "../lib/spl.h"
35 #include <wx/listctrl.h>
36 #include <wx/imaglist.h>
37 #include <wx/spinctrl.h>
38 #include <wx/preferences.h>
39 #include <boost/foreach.hpp>
47 using boost::optional;
48 using boost::shared_ptr;
49 using boost::weak_ptr;
51 using boost::dynamic_pointer_cast;
53 class ContentDialog : public wxDialog, public ContentStore
56 ContentDialog (wxWindow* parent)
57 : wxDialog (parent, wxID_ANY, _("Add content"), wxDefaultPosition, wxSize(800, 640))
58 , _content_view (new ContentView(this))
60 _content_view->update ();
62 wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
63 SetSizer (overall_sizer);
65 overall_sizer->Add (_content_view, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
67 wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
69 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
72 overall_sizer->Layout ();
75 shared_ptr<Content> selected () const
77 return _content_view->selected ();
80 shared_ptr<Content> get (string digest) const
82 return _content_view->get (digest);
86 ContentView* _content_view;
94 PlaylistList (wxPanel* parent, ContentStore* content_store)
95 : _sizer (new wxBoxSizer(wxVERTICAL))
96 , _content_store (content_store)
98 wxStaticText* label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
99 label->SetLabelMarkup (_("<b>Playlists</b>"));
100 _sizer->Add (label, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
102 _list = new wxListCtrl (
103 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
106 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 840);
107 _list->AppendColumn (_("Length"), wxLIST_FORMAT_LEFT, 100);
109 wxBoxSizer* button_sizer = new wxBoxSizer (wxVERTICAL);
110 _new = new Button (parent, _("New"));
111 button_sizer->Add (_new, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
112 _delete = new Button (parent, _("Delete"));
113 button_sizer->Add (_delete, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
115 wxSizer* list = new wxBoxSizer (wxHORIZONTAL);
116 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
117 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
123 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistList::selection_changed, this));
124 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistList::selection_changed, this));
125 _new->Bind (wxEVT_BUTTON, bind(&PlaylistList::new_playlist, this));
126 _delete->Bind (wxEVT_BUTTON, bind(&PlaylistList::delete_playlist, this));
134 shared_ptr<SignalSPL> first_playlist () const
136 if (_playlists.empty()) {
137 return shared_ptr<SignalSPL>();
140 return _playlists.front ();
143 boost::signals2::signal<void (shared_ptr<SignalSPL>)> Edit;
146 void add_playlist_to_view (shared_ptr<const SignalSPL> playlist)
149 item.SetId (_list->GetItemCount());
150 long const N = _list->InsertItem (item);
151 _list->SetItem (N, 0, std_to_wx(playlist->name()));
154 void add_playlist_to_model (shared_ptr<SignalSPL> playlist)
156 _playlists.push_back (playlist);
157 playlist->NameChanged.connect (bind(&PlaylistList::name_changed, this, weak_ptr<SignalSPL>(playlist)));
160 void name_changed (weak_ptr<SignalSPL> wp)
162 shared_ptr<SignalSPL> playlist = wp.lock ();
168 BOOST_FOREACH (shared_ptr<SignalSPL> i, _playlists) {
170 _list->SetItem (N, 0, std_to_wx(i->name()));
176 void load_playlists ()
178 optional<boost::filesystem::path> path = Config::instance()->player_playlist_directory();
183 _list->DeleteAllItems ();
185 for (boost::filesystem::directory_iterator i(*path); i != boost::filesystem::directory_iterator(); ++i) {
186 shared_ptr<SignalSPL> spl(new SignalSPL);
188 spl->read (*i, _content_store);
189 add_playlist_to_model (spl);
193 BOOST_FOREACH (shared_ptr<SignalSPL> i, _playlists) {
194 add_playlist_to_view (i);
200 shared_ptr<SignalSPL> spl (new SignalSPL(wx_to_std(_("New Playlist"))));
201 add_playlist_to_model (spl);
202 add_playlist_to_view (spl);
203 _list->SetItemState (_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
206 void delete_playlist ()
208 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
209 if (selected < 0 || selected >= int(_playlists.size())) {
213 optional<boost::filesystem::path> dir = Config::instance()->player_playlist_directory();
218 boost::filesystem::remove (*dir / (_playlists[selected]->id() + ".xml"));
219 _list->DeleteItem (selected);
220 _playlists.erase (_playlists.begin() + selected);
222 Edit (shared_ptr<SignalSPL>());
225 void selection_changed ()
227 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
228 if (selected < 0 || selected >= int(_playlists.size())) {
229 Edit (shared_ptr<SignalSPL>());
231 Edit (_playlists[selected]);
239 vector<shared_ptr<SignalSPL> > _playlists;
240 ContentStore* _content_store;
244 class PlaylistContent
247 PlaylistContent (wxPanel* parent, ContentDialog* content_dialog)
248 : _content_dialog (content_dialog)
249 , _sizer (new wxBoxSizer(wxVERTICAL))
251 wxBoxSizer* title = new wxBoxSizer (wxHORIZONTAL);
252 wxStaticText* label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
253 label->SetLabelMarkup (_("<b>Playlist:</b>"));
254 title->Add (label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
255 _name = new wxTextCtrl (parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(400, -1));
256 title->Add (_name, 0, wxRIGHT, DCPOMATIC_SIZER_GAP);
257 _sizer->Add (title, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
259 wxBoxSizer* list = new wxBoxSizer (wxHORIZONTAL);
261 _list = new wxListCtrl (
262 parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
265 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 400);
266 _list->AppendColumn (_("CPL"), wxLIST_FORMAT_LEFT, 350);
267 _list->AppendColumn (_("Type"), wxLIST_FORMAT_LEFT, 100);
268 _list->AppendColumn (_("Encrypted"), wxLIST_FORMAT_CENTRE, 90);
270 wxImageList* images = new wxImageList (16, 16);
274 tick_icon.LoadFile ("tick.png", wxBITMAP_TYPE_PNG_RESOURCE);
275 no_tick_icon.LoadFile ("no_tick.png", wxBITMAP_TYPE_PNG_RESOURCE);
277 boost::filesystem::path tick_path = shared_path() / "tick.png";
278 tick_icon.LoadFile (std_to_wx(tick_path.string()), wxBITMAP_TYPE_PNG);
279 boost::filesystem::path no_tick_path = shared_path() / "no_tick.png";
280 no_tick_icon.LoadFile (std_to_wx(no_tick_path.string()), wxBITMAP_TYPE_PNG);
282 images->Add (tick_icon);
283 images->Add (no_tick_icon);
285 _list->SetImageList (images, wxIMAGE_LIST_SMALL);
287 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
289 wxBoxSizer* button_sizer = new wxBoxSizer (wxVERTICAL);
290 _up = new Button (parent, _("Up"));
291 _down = new Button (parent, _("Down"));
292 _add = new Button (parent, _("Add"));
293 _remove = new Button (parent, _("Remove"));
294 button_sizer->Add (_up, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
295 button_sizer->Add (_down, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
296 button_sizer->Add (_add, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
297 button_sizer->Add (_remove, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
299 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
303 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistContent::setup_sensitivity, this));
304 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistContent::setup_sensitivity, this));
305 _name->Bind (wxEVT_TEXT, bind(&PlaylistContent::name_changed, this));
306 _up->Bind (wxEVT_BUTTON, bind(&PlaylistContent::up_clicked, this));
307 _down->Bind (wxEVT_BUTTON, bind(&PlaylistContent::down_clicked, this));
308 _add->Bind (wxEVT_BUTTON, bind(&PlaylistContent::add_clicked, this));
309 _remove->Bind (wxEVT_BUTTON, bind(&PlaylistContent::remove_clicked, this));
317 void set (shared_ptr<SignalSPL> playlist)
319 _playlist = playlist;
320 _list->DeleteAllItems ();
322 BOOST_FOREACH (SPLEntry i, _playlist->get()) {
325 _name->SetValue (std_to_wx(_playlist->name()));
327 _name->SetValue (wxT(""));
329 setup_sensitivity ();
332 shared_ptr<SignalSPL> playlist () const
342 _playlist->set_name (wx_to_std(_name->GetValue()));
346 void add (SPLEntry e)
349 item.SetId (_list->GetItemCount());
350 long const N = _list->InsertItem (item);
354 void set_item (long N, SPLEntry e)
356 _list->SetItem (N, 0, std_to_wx(e.name));
357 _list->SetItem (N, 1, std_to_wx(e.id));
358 _list->SetItem (N, 2, std_to_wx(dcp::content_kind_to_string(e.kind)));
359 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
362 void setup_sensitivity ()
364 bool const have_list = static_cast<bool>(_playlist);
365 int const num_selected = _list->GetSelectedItemCount ();
366 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
367 _name->Enable (have_list);
368 _list->Enable (have_list);
369 _up->Enable (have_list && selected > 0);
370 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
371 _add->Enable (have_list);
372 _remove->Enable (have_list && num_selected > 0);
377 int const r = _content_dialog->ShowModal ();
379 shared_ptr<Content> content = _content_dialog->selected ();
381 SPLEntry e (content);
383 DCPOMATIC_ASSERT (_playlist);
391 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
396 DCPOMATIC_ASSERT (_playlist);
398 SPLEntry tmp = (*_playlist)[s];
399 (*_playlist)[s] = (*_playlist)[s-1];
400 (*_playlist)[s-1] = tmp;
402 set_item (s - 1, (*_playlist)[s-1]);
403 set_item (s, (*_playlist)[s]);
408 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
409 if (s > (_list->GetItemCount() - 1)) {
413 DCPOMATIC_ASSERT (_playlist);
415 SPLEntry tmp = (*_playlist)[s];
416 (*_playlist)[s] = (*_playlist)[s+1];
417 (*_playlist)[s+1] = tmp;
419 set_item (s + 1, (*_playlist)[s+1]);
420 set_item (s, (*_playlist)[s]);
423 void remove_clicked ()
425 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
430 DCPOMATIC_ASSERT (_playlist);
431 _playlist->remove (s);
432 _list->DeleteItem (s);
435 ContentDialog* _content_dialog;
443 shared_ptr<SignalSPL> _playlist;
447 class DOMFrame : public wxFrame
450 explicit DOMFrame (wxString const & title)
451 : wxFrame (0, -1, title)
452 , _content_dialog (new ContentDialog(this))
455 wxMenuBar* bar = new wxMenuBar;
459 /* Use a panel as the only child of the Frame so that we avoid
460 the dark-grey background on Windows.
462 wxPanel* overall_panel = new wxPanel (this, wxID_ANY);
463 wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL);
465 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
466 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
468 sizer->Add (_playlist_list->sizer());
469 sizer->Add (_playlist_content->sizer());
471 overall_panel->SetSizer (sizer);
473 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
475 _playlist_content->set (_playlist_list->first_playlist());
477 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
478 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
479 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
486 /* false here allows the close handler to veto the close request */
492 AboutDialog* d = new AboutDialog (this);
497 void edit_preferences ()
499 if (!_config_dialog) {
500 _config_dialog = create_playlist_editor_config_dialog ();
502 _config_dialog->Show (this);
505 void change_playlist (shared_ptr<SignalSPL> playlist)
507 shared_ptr<SignalSPL> old = _playlist_content->playlist ();
511 _playlist_content->set (playlist);
514 void save_playlist (shared_ptr<SignalSPL> playlist)
516 optional<boost::filesystem::path> dir = Config::instance()->player_playlist_directory();
518 error_dialog (this, _("No playlist folder is specified in preferences. Please set on and then try again."));
521 playlist->write (*dir / (playlist->id() + ".xml"));
524 void setup_menu (wxMenuBar* m)
526 wxMenu* file = new wxMenu;
528 file->Append (wxID_EXIT, _("&Exit"));
530 file->Append (wxID_EXIT, _("&Quit"));
534 wxMenu* edit = new wxMenu;
535 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
538 wxMenu* help = new wxMenu;
540 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
542 help->Append (wxID_ABOUT, _("About"));
545 m->Append (file, _("&File"));
547 m->Append (edit, _("&Edit"));
549 m->Append (help, _("&Help"));
552 ContentDialog* _content_dialog;
553 PlaylistList* _playlist_list;
554 PlaylistContent* _playlist_content;
555 wxPreferencesEditor* _config_dialog;
559 * @brief The magic App class for wxWidgets.
561 class App : public wxApp
574 wxInitAllImageHandlers ();
575 SetAppName (_("DCP-o-matic Playlist Editor"));
577 if (!wxApp::OnInit()) {
581 #ifdef DCPOMATIC_LINUX
582 unsetenv ("UBUNTU_MENUPROXY");
586 make_foreground_application ();
589 dcpomatic_setup_path_encoding ();
591 /* Enable i18n; this will create a Config object
592 to look for a force-configured language. This Config
593 object will be wrong, however, because dcpomatic_setup
594 hasn't yet been called and there aren't any filters etc.
597 dcpomatic_setup_i18n ();
599 /* Set things up, including filters etc.
600 which will now be internationalised correctly.
604 /* Force the configuration to be re-loaded correctly next
609 _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
610 SetTopWindow (_frame);
614 signal_manager = new wxSignalManager (this);
615 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
621 error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
625 /* An unhandled exception has occurred inside the main event loop */
626 bool OnExceptionInMainLoop ()
630 } catch (FileError& e) {
634 _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
635 std_to_wx (e.what()),
636 std_to_wx (e.file().string().c_str ())
639 } catch (exception& e) {
643 _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
644 std_to_wx (e.what ())
648 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
651 /* This will terminate the program */
655 void OnUnhandledException ()
657 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
662 signal_manager->ui_idle ();