Basic SPL support.
authorCarl Hetherington <cth@carlh.net>
Thu, 27 Sep 2018 14:13:15 +0000 (15:13 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 27 Sep 2018 14:13:15 +0000 (15:13 +0100)
src/lib/spl_entry.h [new file with mode: 0644]
src/tools/dcpomatic_player.cc
src/wx/controls.cc
src/wx/controls.h

diff --git a/src/lib/spl_entry.h b/src/lib/spl_entry.h
new file mode 100644 (file)
index 0000000..b7fdaa2
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+    Copyright (C) 2018 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+class SPLEntry
+{
+public:
+       SPLEntry (boost::filesystem::path p)
+               : dcp (p)
+       {}
+
+       /* Length of black before this DCP */
+       DCPTime black_before;
+       boost::filesystem::path dcp;
+};
index 5dcd331ca3bca2346b00b354909d69bd486e7ee1..7313daf0fab9cd64cfb0f1f76af5ecb3426b1eac 100644 (file)
@@ -170,7 +170,7 @@ public:
                _viewer->Seeked.connect (bind(&DOMFrame::playback_seeked, this, _1));
                _viewer->Stopped.connect (bind(&DOMFrame::playback_stopped, this, _1));
                _info = new PlayerInformation (_overall_panel, _viewer);
-               setup_main_sizer (true);
+               setup_main_sizer (Config::instance()->player_mode());
 #ifdef __WXOSX__
                int accelerators = 4;
 #else
@@ -193,19 +193,18 @@ public:
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::forward_frame, this), ID_forward_frame);
 
                UpdateChecker::instance()->StateChanged.connect (boost::bind (&DOMFrame::update_checker_state_changed, this));
-               _controls->DCPDirectorySelected.connect (boost::bind(&DOMFrame::load_dcp, this, _1));
-               _controls->DCPEjected.connect (boost::bind(&DOMFrame::eject_dcp, this));
+               _controls->SPLChanged.connect (boost::bind(&DOMFrame::set_spl, this, _1));
 
                setup_screen ();
        }
 
-       void setup_main_sizer (bool with_viewer)
+       void setup_main_sizer (Config::PlayerMode mode)
        {
                wxSizer* main_sizer = new wxBoxSizer (wxVERTICAL);
-               if (with_viewer) {
+               if (mode != Config::PLAYER_MODE_DUAL) {
                        main_sizer->Add (_viewer->panel(), 1, wxEXPAND | wxALIGN_CENTER_VERTICAL);
                }
-               main_sizer->Add (_controls, 0, wxEXPAND | wxALL, 6);
+               main_sizer->Add (_controls, mode == Config::PLAYER_MODE_DUAL ? 1 : 0, wxEXPAND | wxALL, 6);
                main_sizer->Add (_info, 0, wxEXPAND | wxALL, 6);
                _overall_panel->SetSizer (main_sizer);
                _overall_panel->Layout ();
@@ -293,107 +292,157 @@ public:
                Config::instance()->set_decode_reduction (reduction);
        }
 
-       void eject_dcp ()
+       void load_dcp (boost::filesystem::path dir)
        {
-               _film.reset (new Film (optional<boost::filesystem::path>()));
-               _viewer->set_film (_film);
-               _info->triggered_update ();
+               list<SPLEntry> spl;
+               spl.push_back (SPLEntry(dir));
+               set_spl (spl);
+               Config::instance()->add_to_player_history (dir);
        }
 
-       void load_dcp (boost::filesystem::path dir)
+       optional<dcp::EncryptedKDM> get_kdm_from_url (shared_ptr<DCPContent> dcp)
        {
-               _film.reset (new Film (optional<boost::filesystem::path>()));
-               shared_ptr<DCPContent> dcp;
-               try {
-                       dcp.reset (new DCPContent (_film, dir));
-               } catch (boost::filesystem::filesystem_error& e) {
-                       error_dialog (this, _("Could not load DCP"), std_to_wx (e.what()));
-                       return;
+               ScopedTemporary temp;
+               string url = Config::instance()->kdm_server_url();
+               boost::algorithm::replace_all (url, "{CPL}", *dcp->cpl());
+               optional<dcp::EncryptedKDM> kdm;
+               if (dcp->cpl() && !get_from_url(url, false, temp)) {
+                       try {
+                               kdm = dcp::EncryptedKDM (dcp::file_to_string(temp.file()));
+                               if (kdm->cpl_id() != dcp->cpl()) {
+                                       kdm = boost::none;
+                               }
+                       } catch (std::exception& e) {
+                               /* Hey well */
+                       }
                }
+               return kdm;
+       }
 
-               _film->examine_and_add_content (dcp, true);
-               bool const ok = progress (_("Loading DCP"));
-               if (!ok || !report_errors_from_last_job()) {
-                       return;
+       optional<dcp::EncryptedKDM> get_kdm_from_directory (shared_ptr<DCPContent> dcp)
+       {
+               using namespace boost::filesystem;
+               optional<path> kdm_dir = Config::instance()->player_kdm_directory();
+               if (!kdm_dir) {
+                       return optional<dcp::EncryptedKDM>();
                }
-
-               /* The DCP has been examined and loaded */
-
-               optional<boost::filesystem::path> kdm_dir = Config::instance()->player_kdm_directory();
-               if (dcp->needs_kdm() && kdm_dir) {
-                       /* Look for a KDM */
-
-                       optional<dcp::EncryptedKDM> kdm;
-
-#ifdef DCPOMATIC_VARIANT_SWAROOP
-                       ScopedTemporary temp;
-                       string url = Config::instance()->kdm_server_url();
-                       boost::algorithm::replace_all (url, "{CPL}", "%1");
-                       if (dcp->cpl() && !get_from_url(String::compose(url, *dcp->cpl()), false, temp)) {
+               for (directory_iterator i = directory_iterator(*kdm_dir); i != directory_iterator(); ++i) {
+                       if (file_size(i->path()) < MAX_KDM_SIZE) {
                                try {
-                                       kdm = dcp::EncryptedKDM (dcp::file_to_string(temp.file()));
-                                       if (kdm->cpl_id() != dcp->cpl()) {
-                                               kdm = boost::none;
+                                       dcp::EncryptedKDM kdm (dcp::file_to_string(i->path()));
+                                       if (kdm.cpl_id() == dcp->cpl()) {
+                                               return kdm;
                                        }
                                } catch (std::exception& e) {
                                        /* Hey well */
                                }
                        }
+               }
+               return optional<dcp::EncryptedKDM>();
+       }
+
+       void set_spl (list<SPLEntry> spl)
+       {
+               if (_viewer->playing ()) {
+                       _viewer->stop ();
+               }
+
+               _film.reset (new Film (optional<boost::filesystem::path>()));
+
+               if (spl.empty ()) {
+                       _viewer->set_film (_film);
+                       _info->triggered_update ();
+                       return;
+               }
+
+               /* Start off as Flat */
+               _film->set_container (Ratio::from_id("185"));
+
+               DCPTime position;
+               shared_ptr<DCPContent> first;
+
+               BOOST_FOREACH (SPLEntry i, spl) {
+                       shared_ptr<DCPContent> dcp;
+                       try {
+                               dcp.reset (new DCPContent (_film, i.dcp));
+                       } catch (boost::filesystem::filesystem_error& e) {
+                               error_dialog (this, _("Could not load DCP"), std_to_wx (e.what()));
+                               return;
+                       }
+
+                       if (!first) {
+                               first = dcp;
+                       }
+
+                       _film->examine_and_add_content (dcp, true);
+                       bool const ok = progress (_("Loading DCP"));
+                       if (!ok || !report_errors_from_last_job()) {
+                               return;
+                       }
+
+                       dcp->set_position (position + i.black_before);
+                       position += dcp->length_after_trim() + i.black_before;
+
+                       /* This DCP has been examined and loaded */
+
+                       if (dcp->needs_kdm()) {
+                               optional<dcp::EncryptedKDM> kdm;
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+                               kdm = get_kdm_from_url (dcp);
 #endif
+                               if (!kdm) {
+                                       get_kdm_from_directory (dcp);
+                               }
 
-                       if (!kdm) {
-                               using namespace boost::filesystem;
-                               for (directory_iterator i = directory_iterator(*kdm_dir); i != directory_iterator(); ++i) {
-                                       if (file_size(i->path()) < MAX_KDM_SIZE) {
-                                               try {
-                                                       kdm = dcp::EncryptedKDM(dcp::file_to_string(i->path()));
-                                                       if (kdm->cpl_id() == dcp->cpl()) {
-                                                               break;
-                                                       }
-                                               } catch (std::exception& e) {
-                                                       /* Hey well */
-                                               }
-                                       }
+                               if (kdm) {
+                                       dcp->add_kdm (*kdm);
+                                       dcp->examine (shared_ptr<Job>());
                                }
                        }
 
-                       if (kdm) {
-                               dcp->add_kdm (*kdm);
-                               dcp->examine (shared_ptr<Job>());
+                       BOOST_FOREACH (shared_ptr<TextContent> j, dcp->text) {
+                               j->set_use (true);
                        }
-               }
 
-               setup_from_dcp (dcp);
+                       if (dcp->video) {
+                               Ratio const * r = Ratio::nearest_from_ratio(dcp->video->size().ratio());
+                               if (r->id() == "239") {
+                                       /* Any scope content means we use scope */
+                                       _film->set_container(r);
+                               }
+                       }
 
-               if (dcp->three_d()) {
-                       _film->set_three_d (true);
-               }
+                       /* Any 3D content means we use 3D mode */
+                       if (dcp->three_d()) {
+                               _film->set_three_d (true);
+                       }
 
-               _viewer->set_film (_film);
-               _viewer->seek (DCPTime(), true);
-               _info->triggered_update ();
+                       _viewer->set_film (_film);
+                       _viewer->seek (DCPTime(), true);
+                       _info->triggered_update ();
 
-               Config::instance()->add_to_player_history (dir);
-
-               set_menu_sensitivity ();
+                       set_menu_sensitivity ();
+                       _controls->log (wxString::Format(_("Load DCP %s"), i.dcp.filename().string().c_str()));
+               }
 
                wxMenuItemList old = _cpl_menu->GetMenuItems();
                for (wxMenuItemList::iterator i = old.begin(); i != old.end(); ++i) {
                        _cpl_menu->Remove (*i);
                }
 
-               DCPExaminer ex (dcp);
-               int id = ID_view_cpl;
-               BOOST_FOREACH (shared_ptr<dcp::CPL> i, ex.cpls()) {
-                       wxMenuItem* j = _cpl_menu->AppendRadioItem(
-                               id,
-                               wxString::Format("%s (%s)", std_to_wx(i->annotation_text()).data(), std_to_wx(i->id()).data())
-                               );
-                       j->Check(!dcp->cpl() || i->id() == *dcp->cpl());
-                       ++id;
+               if (spl.size() == 1) {
+                       /* Offer a CPL menu */
+                       DCPExaminer ex (first);
+                       int id = ID_view_cpl;
+                       BOOST_FOREACH (shared_ptr<dcp::CPL> i, ex.cpls()) {
+                               wxMenuItem* j = _cpl_menu->AppendRadioItem(
+                                       id,
+                                       wxString::Format("%s (%s)", std_to_wx(i->annotation_text()).data(), std_to_wx(i->id()).data())
+                                       );
+                               j->Check(!first->cpl() || i->id() == *first->cpl());
+                               ++id;
+                       }
                }
-
-               _controls->log (wxString::Format(_("Load DCP %s"), dir.filename().string().c_str()));
        }
 
 private:
@@ -522,7 +571,15 @@ private:
                        if (!ok || !report_errors_from_last_job()) {
                                return;
                        }
-                       setup_from_dcp (dcp);
+                       BOOST_FOREACH (shared_ptr<TextContent> i, dcp->text) {
+                               i->set_use (true);
+                       }
+                       if (dcp->video) {
+                               Ratio const * r = Ratio::nearest_from_ratio(dcp->video->size().ratio());
+                               if (r) {
+                                       _film->set_container(r);
+                               }
+                       }
                }
 
                c->Destroy ();
@@ -667,7 +724,7 @@ private:
                        }
                }
 
-               setup_main_sizer (_mode != Config::PLAYER_MODE_DUAL);
+               setup_main_sizer (_mode);
        }
 
        void view_closed_captions ()
@@ -861,20 +918,6 @@ private:
                return true;
        }
 
-       void setup_from_dcp (shared_ptr<DCPContent> dcp)
-       {
-               BOOST_FOREACH (shared_ptr<TextContent> i, dcp->text) {
-                       i->set_use (true);
-               }
-
-               if (dcp->video) {
-                       Ratio const * r = Ratio::nearest_from_ratio(dcp->video->size().ratio());
-                       if (r) {
-                               _film->set_container(r);
-                       }
-               }
-       }
-
        wxFrame* _dual_screen;
        bool _update_news_requested;
        PlayerInformation* _info;
index 1f6a30ae955c0bd8f428ea62cc228c673398a4da..6fa5d39e8ec9cf6833ffdec022ea4c0862a2ba88 100644 (file)
@@ -79,14 +79,25 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor
        _dcp_directory->AppendColumn (wxT(""), wxLIST_FORMAT_LEFT, 580);
        e_sizer->Add (_dcp_directory, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
 
-       _log = new wxTextCtrl (this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(-1, 400), wxTE_READONLY | wxTE_MULTILINE);
-       e_sizer->Add (_log, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+       _spl_view = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
+       _spl_view->AppendColumn (wxT(""), wxLIST_FORMAT_LEFT, 580);
+       e_sizer->Add (_spl_view, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+
+       wxBoxSizer* buttons_sizer = new wxBoxSizer (wxVERTICAL);
+       _add_button = new wxButton(this, wxID_ANY, _("Add"));
+       buttons_sizer->Add (_add_button);
+       e_sizer->Add (buttons_sizer, 0, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+
+       _v_sizer->Add (e_sizer, 1, wxEXPAND);
+
+       _log = new wxTextCtrl (this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(-1, 200), wxTE_READONLY | wxTE_MULTILINE);
+       _v_sizer->Add (_log, 0, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
 
        _dcp_directory->Show (false);
+       _spl_view->Show (false);
+       _add_button->Show (false);
        _log->Show (false);
 
-       _v_sizer->Add (e_sizer, 0, wxEXPAND);
-
        wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL);
 
        wxBoxSizer* time_sizer = new wxBoxSizer (wxVERTICAL);
@@ -118,27 +129,29 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor
                _outline_content->Bind (wxEVT_CHECKBOX, boost::bind (&Controls::outline_content_changed, this));
        }
 
-       _slider->Bind           (wxEVT_SCROLL_THUMBTRACK,   boost::bind (&Controls::slider_moved,    this, false));
-       _slider->Bind           (wxEVT_SCROLL_PAGEUP,       boost::bind (&Controls::slider_moved,    this, true));
-       _slider->Bind           (wxEVT_SCROLL_PAGEDOWN,     boost::bind (&Controls::slider_moved,    this, true));
-       _slider->Bind           (wxEVT_SCROLL_CHANGED,      boost::bind (&Controls::slider_released, this));
+       _slider->Bind           (wxEVT_SCROLL_THUMBTRACK,    boost::bind(&Controls::slider_moved,    this, false));
+       _slider->Bind           (wxEVT_SCROLL_PAGEUP,        boost::bind(&Controls::slider_moved,    this, true));
+       _slider->Bind           (wxEVT_SCROLL_PAGEDOWN,      boost::bind(&Controls::slider_moved,    this, true));
+       _slider->Bind           (wxEVT_SCROLL_CHANGED,       boost::bind(&Controls::slider_released, this));
 #ifdef DCPOMATIC_VARIANT_SWAROOP
-       _play_button->Bind      (wxEVT_BUTTON,              boost::bind (&Controls::play_clicked,    this));
-       _pause_button->Bind     (wxEVT_BUTTON,              boost::bind (&Controls::pause_clicked,   this));
-       _stop_button->Bind      (wxEVT_BUTTON,              boost::bind (&Controls::stop_clicked,    this));
+       _play_button->Bind      (wxEVT_BUTTON,               boost::bind(&Controls::play_clicked,    this));
+       _pause_button->Bind     (wxEVT_BUTTON,               boost::bind(&Controls::pause_clicked,   this));
+       _stop_button->Bind      (wxEVT_BUTTON,               boost::bind(&Controls::stop_clicked,    this));
 #else
-       _play_button->Bind      (wxEVT_TOGGLEBUTTON,        boost::bind (&Controls::play_clicked,    this));
+       _play_button->Bind      (wxEVT_TOGGLEBUTTON,         boost::bind(&Controls::play_clicked,    this));
 #endif
-       _rewind_button->Bind    (wxEVT_LEFT_DOWN,           boost::bind (&Controls::rewind_clicked,  this, _1));
-       _back_button->Bind      (wxEVT_LEFT_DOWN,           boost::bind (&Controls::back_clicked,    this, _1));
-       _forward_button->Bind   (wxEVT_LEFT_DOWN,           boost::bind (&Controls::forward_clicked, this, _1));
-       _frame_number->Bind     (wxEVT_LEFT_DOWN,           boost::bind (&Controls::frame_number_clicked, this));
-       _timecode->Bind         (wxEVT_LEFT_DOWN,           boost::bind (&Controls::timecode_clicked, this));
-       _dcp_directory->Bind    (wxEVT_LIST_ITEM_SELECTED,  boost::bind (&Controls::dcp_directory_selected, this));
+       _rewind_button->Bind    (wxEVT_LEFT_DOWN,            boost::bind(&Controls::rewind_clicked,  this, _1));
+       _back_button->Bind      (wxEVT_LEFT_DOWN,            boost::bind(&Controls::back_clicked,    this, _1));
+       _forward_button->Bind   (wxEVT_LEFT_DOWN,            boost::bind(&Controls::forward_clicked, this, _1));
+       _frame_number->Bind     (wxEVT_LEFT_DOWN,            boost::bind(&Controls::frame_number_clicked, this));
+       _timecode->Bind         (wxEVT_LEFT_DOWN,            boost::bind(&Controls::timecode_clicked, this));
+       _dcp_directory->Bind    (wxEVT_LIST_ITEM_SELECTED,   boost::bind(&Controls::setup_sensitivity, this));
+       _dcp_directory->Bind    (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&Controls::setup_sensitivity, this));
        if (_jump_to_selected) {
                _jump_to_selected->Bind (wxEVT_CHECKBOX, boost::bind (&Controls::jump_to_selected_clicked, this));
                _jump_to_selected->SetValue (Config::instance()->jump_to_selected ());
        }
+       _add_button->Bind       (wxEVT_BUTTON,              boost::bind(&Controls::add_clicked, this));
 
        _viewer->PositionChanged.connect (boost::bind(&Controls::position_changed, this));
        _viewer->Started.connect (boost::bind(&Controls::started, this));
@@ -157,6 +170,17 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor
        _config_changed_connection = Config::instance()->Changed.connect (bind(&Controls::config_changed, this, _1));
 }
 
+void
+Controls::add_clicked ()
+{
+       optional<boost::filesystem::path> sel = selected_dcp ();
+       DCPOMATIC_ASSERT (sel);
+       _spl.push_back (SPLEntry(*sel));
+       _spl_view->InsertItem(_spl_view->GetItemCount(), std_to_wx(sel->filename().string()));
+       SPLChanged (_spl);
+       setup_sensitivity ();
+}
+
 void
 Controls::config_changed (int property)
 {
@@ -374,7 +398,8 @@ void
 Controls::setup_sensitivity ()
 {
        /* examine content is the only job which stops the viewer working */
-       bool const c = _film && !_film->content().empty() && (!_active_job || *_active_job != "examine_content");
+       bool const active_job = _active_job && *_active_job != "examine_content";
+       bool const c = ((_film && !_film->content().empty()) || !_spl.empty()) && !active_job;
 
        _slider->Enable (c);
        _rewind_button->Enable (c);
@@ -399,6 +424,20 @@ Controls::setup_sensitivity ()
        if (_eye) {
                _eye->Enable (c && _film->three_d ());
        }
+
+       _add_button->Enable (static_cast<bool>(selected_dcp()));
+}
+
+optional<boost::filesystem::path>
+Controls::selected_dcp () const
+{
+       long int s = _dcp_directory->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (s == -1) {
+               return optional<boost::filesystem::path>();
+       }
+
+       DCPOMATIC_ASSERT (s < int(_dcp_directories.size()));
+       return _dcp_directories[s];
 }
 
 void
@@ -458,7 +497,10 @@ void
 Controls::show_extended_player_controls (bool s)
 {
        _dcp_directory->Show (s);
+       _spl_view->Show (s);
        _log->Show (s);
+       _add_button->Show (s);
+       _v_sizer->Layout ();
 }
 
 void
@@ -478,7 +520,7 @@ Controls::update_dcp_directory ()
                        if (is_directory(*i) && (is_regular_file(*i / "ASSETMAP") || is_regular_file(*i / "ASSETMAP.xml"))) {
                                string const x = i->path().string().substr(dir->string().length() + 1);
                                _dcp_directory->InsertItem(_dcp_directory->GetItemCount(), std_to_wx(x));
-                               _dcp_directories.push_back(x);
+                               _dcp_directories.push_back(*i);
                        }
                } catch (boost::filesystem::filesystem_error& e) {
                        /* Never mind */
@@ -486,18 +528,6 @@ Controls::update_dcp_directory ()
        }
 }
 
-void
-Controls::dcp_directory_selected ()
-{
-       long int s = _dcp_directory->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-       if (s == -1) {
-               return;
-       }
-
-       DCPOMATIC_ASSERT (s < int(_dcp_directories.size()));
-       DCPDirectorySelected (*Config::instance()->player_dcp_directory() / _dcp_directories[s]);
-}
-
 #ifdef DCPOMATIC_VARIANT_SWAROOP
 void
 Controls::pause_clicked ()
@@ -509,7 +539,7 @@ void
 Controls::stop_clicked ()
 {
        _viewer->stop ();
-       DCPEjected ();
+       _viewer->seek (DCPTime(), true);
 }
 #endif
 
index 63e9fb38a3baea12ea711a7f3818f7c92ed47ff2..968ffa92100456be51f13d2434f6eaae13712e68 100644 (file)
@@ -21,6 +21,7 @@
 #include "lib/dcpomatic_time.h"
 #include "lib/types.h"
 #include "lib/film.h"
+#include "lib/spl_entry.h"
 #include <wx/wx.h>
 #include <boost/shared_ptr.hpp>
 #include <boost/signals2.hpp>
@@ -49,8 +50,7 @@ public:
        void show_extended_player_controls (bool s);
        void log (wxString s);
 
-       boost::signals2::signal<void (boost::filesystem::path)> DCPDirectorySelected;
-       boost::signals2::signal<void ()> DCPEjected;
+       boost::signals2::signal<void (std::list<SPLEntry>)> SPLChanged;
 
 private:
        void update_position_label ();
@@ -80,12 +80,13 @@ private:
        void film_changed ();
        void update_dcp_directory ();
        void dcp_directory_changed ();
-       void dcp_directory_selected ();
        void config_changed (int property);
+       boost::optional<boost::filesystem::path> selected_dcp () const;
 #ifdef DCPOMATIC_VARIANT_SWAROOP
        void pause_clicked ();
        void stop_clicked ();
 #endif
+       void add_clicked ();
 
        boost::shared_ptr<Film> _film;
        boost::shared_ptr<FilmViewer> _viewer;
@@ -98,7 +99,9 @@ private:
        wxChoice* _eye;
        wxCheckBox* _jump_to_selected;
        wxListCtrl* _dcp_directory;
+       wxListCtrl* _spl_view;
        wxTextCtrl* _log;
+       wxButton* _add_button;
        std::vector<boost::filesystem::path> _dcp_directories;
        wxSlider* _slider;
        wxButton* _rewind_button;
@@ -114,6 +117,7 @@ private:
        wxToggleButton* _play_button;
 #endif
        boost::optional<std::string> _active_job;
+       std::list<SPLEntry> _spl;
 
        ClosedCaptionsDialog* _closed_captions_dialog;