Merge 1.0 in.
authorCarl Hetherington <cth@carlh.net>
Tue, 17 Sep 2013 22:39:05 +0000 (23:39 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 17 Sep 2013 22:39:05 +0000 (23:39 +0100)
13 files changed:
1  2 
src/lib/config.cc
src/lib/config.h
src/lib/exceptions.h
src/lib/film.cc
src/lib/film.h
src/lib/util.cc
src/tools/dcpomatic.cc
src/wx/cinema_dialog.cc
src/wx/film_editor.cc
src/wx/film_editor.h
src/wx/kdm_dialog.cc
src/wx/screen_dialog.cc
src/wx/wscript

diff --combined src/lib/config.cc
index a74c36f73c66e1c0d42ab25e88ae0d6ced9c0cd0,9f981c61918b111874858a1af5847a8b8c8c174b..5b96d108ccdfdd51a9d08595eb1e6c0c9b9d710b
  #include <fstream>
  #include <glib.h>
  #include <boost/filesystem.hpp>
+ #include <libdcp/colour_matrix.h>
+ #include <libcxml/cxml.h>
  #include "config.h"
  #include "server.h"
  #include "scaler.h"
  #include "filter.h"
+ #include "ratio.h"
+ #include "dcp_content_type.h"
  #include "sound_processor.h"
- #include "cinema.h"
+ #include "colour_conversion.h"
+ #include "i18n.h"
  
  using std::vector;
  using std::ifstream;
  using std::string;
  using std::ofstream;
  using std::list;
+ using std::max;
  using boost::shared_ptr;
+ using boost::lexical_cast;
+ using boost::optional;
  
  Config* Config::_instance = 0;
  
  /** Construct default configuration */
  Config::Config ()
-       : _num_local_encoding_threads (2)
+       : _num_local_encoding_threads (max (2U, boost::thread::hardware_concurrency()))
        , _server_port (6192)
-       , _reference_scaler (Scaler::from_id ("bicubic"))
-       , _tms_path (".")
-       , _sound_processor (SoundProcessor::from_id ("dolby_cp750"))
+       , _tms_path (N_("."))
+       , _sound_processor (SoundProcessor::from_id (N_("dolby_cp750")))
+       , _default_still_length (10)
+       , _default_container (Ratio::from_id ("185"))
+       , _default_dcp_content_type (DCPContentType::from_dci_name ("TST"))
+       , _default_j2k_bandwidth (200000000)
  {
-       ifstream f (read_file().c_str ());
-       string line;
+       _allowed_dcp_frame_rates.push_back (24);
+       _allowed_dcp_frame_rates.push_back (25);
+       _allowed_dcp_frame_rates.push_back (30);
+       _allowed_dcp_frame_rates.push_back (48);
+       _allowed_dcp_frame_rates.push_back (50);
+       _allowed_dcp_frame_rates.push_back (60);
+       _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6));
+       _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, libdcp::colour_matrix::srgb_to_xyz, 2.6));
+ }
+ void
+ Config::read ()
+ {
+       if (!boost::filesystem::exists (file (false))) {
+               read_old_metadata ();
+               return;
+       }
  
-       shared_ptr<Cinema> cinema;
-       shared_ptr<Screen> screen;
+       cxml::Document f ("Config");
+       f.read_file (file (false));
+       optional<string> c;
+       _num_local_encoding_threads = f.number_child<int> ("NumLocalEncodingThreads");
+       _default_directory = f.string_child ("DefaultDirectory");
+       _server_port = f.number_child<int> ("ServerPort");
        
+       list<shared_ptr<cxml::Node> > servers = f.node_children ("Server");
+       for (list<shared_ptr<cxml::Node> >::iterator i = servers.begin(); i != servers.end(); ++i) {
+               _servers.push_back (ServerDescription (*i));
+       }
+       _tms_ip = f.string_child ("TMSIP");
+       _tms_path = f.string_child ("TMSPath");
+       _tms_user = f.string_child ("TMSUser");
+       _tms_password = f.string_child ("TMSPassword");
+       c = f.optional_string_child ("SoundProcessor");
+       if (c) {
+               _sound_processor = SoundProcessor::from_id (c.get ());
+       }
+       _language = f.optional_string_child ("Language");
+       c = f.optional_string_child ("DefaultContainer");
+       if (c) {
+               _default_container = Ratio::from_id (c.get ());
+       }
+       c = f.optional_string_child ("DefaultDCPContentType");
+       if (c) {
+               _default_dcp_content_type = DCPContentType::from_dci_name (c.get ());
+       }
+       _dcp_metadata.issuer = f.optional_string_child ("DCPMetadataIssuer").get_value_or ("");
+       _dcp_metadata.creator = f.optional_string_child ("DCPMetadataCreator").get_value_or ("");
+       _default_dci_metadata = DCIMetadata (f.node_child ("DCIMetadata"));
+       _default_still_length = f.optional_number_child<int>("DefaultStillLength").get_value_or (10);
+       _default_j2k_bandwidth = f.optional_number_child<int>("DefaultJ2KBandwidth").get_value_or (200000000);
+       list<shared_ptr<cxml::Node> > cc = f.node_children ("ColourConversion");
+       if (!cc.empty ()) {
+               _colour_conversions.clear ();
+       }
+       
+       for (list<shared_ptr<cxml::Node> >::iterator i = cc.begin(); i != cc.end(); ++i) {
+               _colour_conversions.push_back (PresetColourConversion (*i));
+       }
+ }
+ void
+ Config::read_old_metadata ()
+ {
+       ifstream f (file(true).c_str ());
+       string line;
++
        while (getline (f, line)) {
                if (line.empty ()) {
                        continue;
                string const k = line.substr (0, s);
                string const v = line.substr (s + 1);
  
-               if (k == "num_local_encoding_threads") {
+               if (k == N_("num_local_encoding_threads")) {
                        _num_local_encoding_threads = atoi (v.c_str ());
-               } else if (k == "default_directory") {
+               } else if (k == N_("default_directory")) {
                        _default_directory = v;
-               } else if (k == "server_port") {
+               } else if (k == N_("server_port")) {
                        _server_port = atoi (v.c_str ());
-               } else if (k == "reference_scaler") {
-                       _reference_scaler = Scaler::from_id (v);
-               } else if (k == "reference_filter") {
-                       _reference_filters.push_back (Filter::from_id (v));
-               } else if (k == "server") {
-                       _servers.push_back (ServerDescription::create_from_metadata (v));
-               } else if (k == "tms_ip") {
+               } else if (k == N_("server")) {
+                       optional<ServerDescription> server = ServerDescription::create_from_metadata (v);
+                       if (server) {
+                               _servers.push_back (server.get ());
+                       }
+               } else if (k == N_("tms_ip")) {
                        _tms_ip = v;
-               } else if (k == "tms_path") {
+               } else if (k == N_("tms_path")) {
                        _tms_path = v;
-               } else if (k == "tms_user") {
+               } else if (k == N_("tms_user")) {
                        _tms_user = v;
-               } else if (k == "tms_password") {
+               } else if (k == N_("tms_password")) {
                        _tms_password = v;
-               } else if (k == "sound_processor") {
+               } else if (k == N_("sound_processor")) {
                        _sound_processor = SoundProcessor::from_id (v);
-               } else if (k == "cinema") {
-                       if (cinema) {
-                               _cinemas.push_back (cinema);
-                       }
-                       cinema.reset (new Cinema (v, ""));
-               } else if (k == "cinema_email") {
-                       assert (cinema);
-                       cinema->email = v;
-               } else if (k == "screen") {
-                       assert (cinema);
-                       if (screen) {
-                               cinema->screens.push_back (screen);
-                       }
-                       screen.reset (new Screen (v, shared_ptr<libdcp::Certificate> ()));
-               } else if (k == "screen_certificate") {
-                       assert (screen);
-                       shared_ptr<Certificate> c (new libdcp::Certificate);
-                       c->set_from_string (v);
+               } else if (k == "language") {
+                       _language = v;
+               } else if (k == "default_container") {
+                       _default_container = Ratio::from_id (v);
+               } else if (k == "default_dcp_content_type") {
+                       _default_dcp_content_type = DCPContentType::from_dci_name (v);
+               } else if (k == "dcp_metadata_issuer") {
+                       _dcp_metadata.issuer = v;
+               } else if (k == "dcp_metadata_creator") {
+                       _dcp_metadata.creator = v;
+               } else if (k == "dcp_metadata_issue_date") {
+                       _dcp_metadata.issue_date = v;
                }
-       }
  
-       if (cinema) {
-               _cinemas.push_back (cinema);
+               _default_dci_metadata.read_old_metadata (k, v);
        }
  }
  
  /** @return Filename to write configuration to */
  string
- Config::write_file () const
+ Config::file (bool old) const
  {
        boost::filesystem::path p;
        p /= g_get_user_config_dir ();
-       p /= "dvdomatic";
-       boost::filesystem::create_directory (p);
-       p /= "config";
-       return p.string ();
- }
- string
- Config::read_file () const
- {
-       if (boost::filesystem::exists (write_file ())) {
-               return write_file ();
+       boost::system::error_code ec;
+       boost::filesystem::create_directory (p, ec);
+       if (old) {
+               p /= ".dvdomatic";
+       } else {
+               p /= "dcpomatic.xml";
        }
-       
-       boost::filesystem::path p;
-       p /= g_get_user_config_dir ();
-       p /= ".dvdomatic";
        return p.string ();
  }
  
 +string
 +Config::crypt_chain_directory () const
 +{
 +      boost::filesystem::path p;
 +      p /= g_get_user_config_dir ();
 +      p /= "dvdomatic";
 +      p /= "crypt";
 +      boost::filesystem::create_directories (p);
 +      return p.string ();
 +}
 +
  /** @return Singleton instance */
  Config *
  Config::instance ()
  {
        if (_instance == 0) {
                _instance = new Config;
+               try {
+                       _instance->read ();
+               } catch (...) {
+                       /* configuration load failed; never mind, just
+                          stick with the default.
+                       */
+               }
        }
  
        return _instance;
  void
  Config::write () const
  {
-       ofstream f (write_file().c_str ());
-       f << "num_local_encoding_threads " << _num_local_encoding_threads << "\n"
-         << "default_directory " << _default_directory << "\n"
-         << "server_port " << _server_port << "\n"
-         << "reference_scaler " << _reference_scaler->id () << "\n";
+       xmlpp::Document doc;
+       xmlpp::Element* root = doc.create_root_node ("Config");
  
-       for (vector<Filter const *>::const_iterator i = _reference_filters.begin(); i != _reference_filters.end(); ++i) {
-               f << "reference_filter " << (*i)->id () << "\n";
-       }
+       root->add_child("NumLocalEncodingThreads")->add_child_text (lexical_cast<string> (_num_local_encoding_threads));
+       root->add_child("DefaultDirectory")->add_child_text (_default_directory);
+       root->add_child("ServerPort")->add_child_text (lexical_cast<string> (_server_port));
        
-       for (vector<ServerDescription*>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) {
-               f << "server " << (*i)->as_metadata () << "\n";
+       for (vector<ServerDescription>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) {
+               i->as_xml (root->add_child ("Server"));
        }
  
-       f << "tms_ip " << _tms_ip << "\n";
-       f << "tms_path " << _tms_path << "\n";
-       f << "tms_user " << _tms_user << "\n";
-       f << "tms_password " << _tms_password << "\n";
-       f << "sound_processor " << _sound_processor->id () << "\n";
+       root->add_child("TMSIP")->add_child_text (_tms_ip);
+       root->add_child("TMSPath")->add_child_text (_tms_path);
+       root->add_child("TMSUser")->add_child_text (_tms_user);
+       root->add_child("TMSPassword")->add_child_text (_tms_password);
+       if (_sound_processor) {
+               root->add_child("SoundProcessor")->add_child_text (_sound_processor->id ());
+       }
+       if (_language) {
+               root->add_child("Language")->add_child_text (_language.get());
+       }
+       if (_default_container) {
+               root->add_child("DefaultContainer")->add_child_text (_default_container->id ());
+       }
+       if (_default_dcp_content_type) {
+               root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->dci_name ());
+       }
+       root->add_child("DCPMetadataIssuer")->add_child_text (_dcp_metadata.issuer);
+       root->add_child("DCPMetadataCreator")->add_child_text (_dcp_metadata.creator);
  
-       for (list<shared_ptr<Cinema> >::const_iterator i = _cinemas.begin(); i != _cinemas.end(); ++i) {
-               f << "cinema " << (*i)->name << "\n";
-               f << "cinema_email " << (*i)->email << "\n";
-               for (list<shared_ptr<Screen> >::iterator j = (*i)->screens.begin(); j != (*i)->screens.end(); ++j) {
-                       f << "screen " << (*j)->name << "\n";
-               }
+       _default_dci_metadata.as_xml (root->add_child ("DCIMetadata"));
+       root->add_child("DefaultStillLength")->add_child_text (lexical_cast<string> (_default_still_length));
+       root->add_child("DefaultJ2KBandwidth")->add_child_text (lexical_cast<string> (_default_j2k_bandwidth));
+       for (vector<PresetColourConversion>::const_iterator i = _colour_conversions.begin(); i != _colour_conversions.end(); ++i) {
+               i->as_xml (root->add_child ("ColourConversion"));
        }
+       doc.write_to_file_formatted (file (false));
  }
  
  string
@@@ -206,3 -280,10 +292,10 @@@ Config::default_directory_or (string a
  
        return _default_directory;
  }
+ void
+ Config::drop ()
+ {
+       delete _instance;
+       _instance = 0;
+ }
diff --combined src/lib/config.h
index ee4e4eaec49cd2a098eb04b66a4b49b517d4cee4,bce6bfd7e2707ecbd7a196ee60ac69f4d1df5426..48eabd54ca3dc33b6f395f34c8981256413ce9f0
   *  @brief Class holding configuration.
   */
  
- #ifndef DVDOMATIC_CONFIG_H
- #define DVDOMATIC_CONFIG_H
+ #ifndef DCPOMATIC_CONFIG_H
+ #define DCPOMATIC_CONFIG_H
  
  #include <vector>
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
+ #include <libdcp/metadata.h>
+ #include "dci_metadata.h"
+ #include "colour_conversion.h"
+ #include "server.h"
  
  class ServerDescription;
  class Scaler;
  class Filter;
  class SoundProcessor;
+ class DCPContentType;
+ class Ratio;
 +class Cinema;
  
  /** @class Config
   *  @brief A singleton class holding configuration.
   */
- class Config
+ class Config : public boost::noncopyable
  {
  public:
  
        }
  
        /** @return J2K encoding servers to use */
-       std::vector<ServerDescription*> servers () const {
+       std::vector<ServerDescription> servers () const {
                return _servers;
        }
  
-       Scaler const * reference_scaler () const {
-               return _reference_scaler;
-       }
-       std::vector<Filter const *> reference_filters () const {
-               return _reference_filters;
-       }
        /** @return The IP address of a TMS that we can copy DCPs to */
        std::string tms_ip () const {
                return _tms_ip;
                return _sound_processor;
        }
  
 +      std::list<boost::shared_ptr<Cinema> > cinemas () const {
 +              return _cinemas;
 +      }
++      
+       std::list<int> allowed_dcp_frame_rates () const {
+               return _allowed_dcp_frame_rates;
+       }
+       
+       DCIMetadata default_dci_metadata () const {
+               return _default_dci_metadata;
+       }
+       boost::optional<std::string> language () const {
+               return _language;
+       }
+       int default_still_length () const {
+               return _default_still_length;
+       }
+       Ratio const * default_container () const {
+               return _default_container;
+       }
+       DCPContentType const * default_dcp_content_type () const {
+               return _default_dcp_content_type;
+       }
+       libdcp::XMLMetadata dcp_metadata () const {
+               return _dcp_metadata;
+       }
+       int default_j2k_bandwidth () const {
+               return _default_j2k_bandwidth;
+       }
+       std::vector<PresetColourConversion> colour_conversions () const {
+               return _colour_conversions;
+       }
  
        /** @param n New number of local encoding threads */
        void set_num_local_encoding_threads (int n) {
        }
  
        /** @param s New list of servers */
-       void set_servers (std::vector<ServerDescription*> s) {
+       void set_servers (std::vector<ServerDescription> s) {
                _servers = s;
        }
  
                _tms_password = p;
        }
  
 +      void add_cinema (boost::shared_ptr<Cinema> c) {
 +              _cinemas.push_back (c);
 +      }
 +
 +      void remove_cinema (boost::shared_ptr<Cinema> c) {
 +              _cinemas.remove (c);
 +      }
++
+       void set_allowed_dcp_frame_rates (std::list<int> const & r) {
+               _allowed_dcp_frame_rates = r;
+       }
+       void set_default_dci_metadata (DCIMetadata d) {
+               _default_dci_metadata = d;
+       }
+       void set_language (std::string l) {
+               _language = l;
+       }
+       void unset_language () {
+               _language = boost::none;
+       }
+       void set_default_still_length (int s) {
+               _default_still_length = s;
+       }
+       void set_default_container (Ratio const * c) {
+               _default_container = c;
+       }
+       void set_default_dcp_content_type (DCPContentType const * t) {
+               _default_dcp_content_type = t;
+       }
+       void set_dcp_metadata (libdcp::XMLMetadata m) {
+               _dcp_metadata = m;
+       }
+       void set_default_j2k_bandwidth (int b) {
+               _default_j2k_bandwidth = b;
+       }
+       void set_colour_conversions (std::vector<PresetColourConversion> const & c) {
+               _colour_conversions = c;
+       }
        
        void write () const;
  
 +      std::string crypt_chain_directory () const;
 +
        static Config* instance ();
+       static void drop ();
  
  private:
        Config ();
-       std::string read_file () const;
-       std::string write_file () const;
+       std::string file (bool) const;
+       void read ();
+       void read_old_metadata ();
  
        /** number of threads to use for J2K encoding on the local machine */
        int _num_local_encoding_threads;
        int _server_port;
  
        /** J2K encoding servers to use */
-       std::vector<ServerDescription *> _servers;
+       std::vector<ServerDescription> _servers;
        /** Scaler to use for the "A" part of A/B comparisons */
        Scaler const * _reference_scaler;
        /** Filters to use for the "A" part of A/B comparisons */
        std::string _tms_password;
        /** Our sound processor */
        SoundProcessor const * _sound_processor;
+       std::list<int> _allowed_dcp_frame_rates;
+       /** Default DCI metadata for newly-created Films */
+       DCIMetadata _default_dci_metadata;
+       boost::optional<std::string> _language;
+       int _default_still_length;
+       Ratio const * _default_container;
+       DCPContentType const * _default_dcp_content_type;
+       libdcp::XMLMetadata _dcp_metadata;
+       int _default_j2k_bandwidth;
+       std::vector<PresetColourConversion> _colour_conversions;
  
 +      std::list<boost::shared_ptr<Cinema> > _cinemas;
 +
        /** Singleton instance, or 0 */
        static Config* _instance;
  };
diff --combined src/lib/exceptions.h
index 06177863ab27e5720125c91b0320827c627251b7,f587f055fde8fa87fffc5e6f66f69ce9ac8b4365..b04d973dc7a62197571ce7d6697917f2023b23e0
  
  */
  
+ #ifndef DCPOMATIC_EXCEPTIONS_H
+ #define DCPOMATIC_EXCEPTIONS_H
  /** @file  src/exceptions.h
   *  @brief Our exceptions.
   */
  
  #include <stdexcept>
- #include <sstream>
  #include <cstring>
+ #include <boost/exception/all.hpp>
+ #include <boost/filesystem.hpp>
+ #include <boost/thread.hpp>
+ extern "C" {
+ #include <libavutil/pixfmt.h>
+ }
  
  /** @class StringError
   *  @brief A parent class for exceptions using messages held in a std::string
@@@ -80,7 -88,7 +88,7 @@@ public
        /** @param m Error message.
         *  @param f Name of the file that this exception concerns.
         */
-       FileError (std::string m, std::string f)
+       FileError (std::string m, boost::filesystem::path f)
                : StringError (m)
                , _file (f)
        {}
        virtual ~FileError () throw () {}
  
        /** @return name of the file that this exception concerns */
-       std::string file () const {
+       boost::filesystem::path file () const {
                return _file;
        }
  
  private:
        /** name of the file that this exception concerns */
-       std::string _file;
+       boost::filesystem::path _file;
  };
        
  
@@@ -105,9 -113,7 +113,7 @@@ class OpenFileError : public FileErro
  {
  public:
        /** @param f File that we were trying to open */
-       OpenFileError (std::string f)
-               : FileError ("could not open file " + f, f)
-       {}
+       OpenFileError (boost::filesystem::path f);
  };
  
  /** @class CreateFileError.
@@@ -117,9 -123,7 +123,7 @@@ class CreateFileError : public FileErro
  {
  public:
        /** @param f File that we were trying to create */
-       CreateFileError (std::string f)
-               : FileError ("could not create file " + f, f)
-       {}
+       CreateFileError (boost::filesystem::path f);
  };
  
  
@@@ -132,16 -136,7 +136,7 @@@ public
        /** @param f File that we were trying to read from.
         *  @param e errno value, or 0.
         */
-       ReadFileError (std::string f, int e = 0)
-               : FileError ("", f)
-       {
-               std::stringstream s;
-               s << "could not read from file " << f;
-               if (e) {
-                       s << " (" << strerror (e) << ")";
-               }
-               _what = s.str ();
-       }
+       ReadFileError (boost::filesystem::path f, int e = 0);
  };
  
  /** @class WriteFileError.
@@@ -153,16 -148,7 +148,7 @@@ public
        /** @param f File that we were trying to write to.
         *  @param e errno value, or 0.
         */
-       WriteFileError (std::string f, int e)
-               : FileError ("", f)
-       {
-               std::stringstream s;
-               s << "could not write to file " << f;
-               if (e) {
-                       s << " (" << strerror (e) << ")";
-               }
-               _what = s.str ();
-       }
+       WriteFileError (boost::filesystem::path f, int e);
  };
  
  /** @class SettingError.
@@@ -197,9 -183,7 +183,7 @@@ class MissingSettingError : public Sett
  {
  public:
        /** @param s Name of setting that was required */
-       MissingSettingError (std::string s)
-               : SettingError (s, "missing required setting " + s)
-       {}
+       MissingSettingError (std::string s);
  };
  
  /** @class BadSettingError
@@@ -225,10 -209,37 +209,45 @@@ public
        {}
  };
  
 +class KDMError : public StringError
 +{
 +public:
 +      KDMError (std::string s)
 +              : StringError (s)
 +      {}
 +};
++
+ class PixelFormatError : public StringError
+ {
+ public:
+       PixelFormatError (std::string o, AVPixelFormat f);
+ };
+ class ExceptionStore
+ {
+ public:
+       bool thrown () const {
+               boost::mutex::scoped_lock lm (_mutex);
+               return _exception;
+       }
+       
+       void rethrow () {
+               boost::mutex::scoped_lock lm (_mutex);
+               boost::rethrow_exception (_exception);
+       }
+ protected:    
+       
+       void store_current () {
+               boost::mutex::scoped_lock lm (_mutex);
+               _exception = boost::current_exception ();
+       }
+ private:
+       boost::exception_ptr _exception;
+       mutable boost::mutex _mutex;
+ };
+       
+ #endif
diff --combined src/lib/film.cc
index fb3423bb42c1c35707798ca0c4f23c363e3e5c86,940e94fa7631053ffe1ac3070fbed1f0e7e855aa..e885fe5fd350f804fb8e0e8ec489005da6b4501d
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  #include <boost/lexical_cast.hpp>
  #include <boost/date_time.hpp>
  #include <libxml++/libxml++.h>
- #include <libdcp/certificates.h>
- #include "cinema.h"
+ #include <libcxml/cxml.h>
 +#include <libdcp/crypt_chain.h>
++#include <libdcp/cpl.h>
  #include "film.h"
- #include "format.h"
  #include "job.h"
- #include "filter.h"
- #include "transcoder.h"
  #include "util.h"
  #include "job_manager.h"
- #include "ab_transcode_job.h"
  #include "transcode_job.h"
  #include "scp_dcp_job.h"
- #include "make_dcp_job.h"
  #include "log.h"
- #include "options.h"
  #include "exceptions.h"
  #include "examine_content_job.h"
  #include "scaler.h"
- #include "decoder_factory.h"
  #include "config.h"
- #include "check_hashes_job.h"
  #include "version.h"
  #include "ui_signaller.h"
- #include "video_decoder.h"
- #include "audio_decoder.h"
- #include "external_audio_decoder.h"
+ #include "playlist.h"
+ #include "player.h"
+ #include "dcp_content_type.h"
+ #include "ratio.h"
+ #include "cross.h"
++#include "cinema.h"
+ #include "i18n.h"
  
  using std::string;
  using std::stringstream;
@@@ -69,52 -63,53 +66,54 @@@ using std::ofstream
  using std::setfill;
  using std::min;
  using std::make_pair;
- using std::list;
+ using std::endl;
  using std::cout;
+ using std::list;
  using boost::shared_ptr;
+ using boost::weak_ptr;
  using boost::lexical_cast;
+ using boost::dynamic_pointer_cast;
  using boost::to_upper_copy;
  using boost::ends_with;
  using boost::starts_with;
  using boost::optional;
+ using libdcp::Size;
  
- int const Film::state_version = 1;
+ int const Film::state_version = 4;
  
- /** Construct a Film object in a given directory, reading any metadata
-  *  file that exists in that directory.  An exception will be thrown if
-  *  must_exist is true and the specified directory does not exist.
+ /** Construct a Film object in a given directory.
   *
-  *  @param d Film directory.
-  *  @param must_exist true to throw an exception if does not exist.
+  *  @param dir Film directory.
   */
  
- Film::Film (string d, bool must_exist)
-       : _use_dci_name (true)
-       , _trust_content_header (true)
-       , _dcp_content_type (0)
-       , _format (0)
+ Film::Film (boost::filesystem::path dir)
+       : _playlist (new Playlist)
+       , _use_dci_name (true)
+       , _dcp_content_type (Config::instance()->default_dcp_content_type ())
+       , _container (Config::instance()->default_container ())
+       , _resolution (RESOLUTION_2K)
        , _scaler (Scaler::from_id ("bicubic"))
-       , _dcp_trim_start (0)
-       , _dcp_trim_end (0)
-       , _dcp_ab (false)
-       , _use_content_audio (true)
-       , _audio_gain (0)
-       , _audio_delay (0)
-       , _still_duration (10)
        , _with_subtitles (false)
-       , _subtitle_offset (0)
-       , _subtitle_scale (1)
 +      , _encrypted (false)
-       , _colour_lut (0)
-       , _j2k_bandwidth (200000000)
-       , _frames_per_second (0)
+       , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
+       , _dci_metadata (Config::instance()->default_dci_metadata ())
+       , _video_frame_rate (24)
+       , _audio_channels (MAX_AUDIO_CHANNELS)
+       , _three_d (false)
+       , _sequence_video (true)
+       , _interop (false)
        , _dirty (false)
  {
+       set_dci_date_today ();
+       _playlist->Changed.connect (bind (&Film::playlist_changed, this));
+       _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2));
+       
        /* Make state.directory a complete path without ..s (where possible)
           (Code swiped from Adam Bowen on stackoverflow)
        */
        
-       boost::filesystem::path p (boost::filesystem::system_complete (d));
+       boost::filesystem::path p (boost::filesystem::system_complete (dir));
        boost::filesystem::path result;
        for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
                if (*i == "..") {
        }
  
        set_directory (result.string ());
-       
-       if (!boost::filesystem::exists (directory())) {
-               if (must_exist) {
-                       throw OpenFileError (directory());
-               } else {
-                       boost::filesystem::create_directory (directory());
-               }
-       }
+       _log.reset (new FileLog (file ("log")));
  
-       _external_audio_stream = ExternalAudioStream::create ();
-       
-       if (must_exist) {
-               read_metadata ();
-       }
-       _log = new FileLog (file ("log"));
-       set_dci_date_today ();
+       _playlist->set_sequence_video (_sequence_video);
  }
  
- Film::Film (Film const & o)
-       : boost::enable_shared_from_this<Film> (o)
-       , _log (0)
-       , _directory         (o._directory)
-       , _name              (o._name)
-       , _use_dci_name      (o._use_dci_name)
-       , _content           (o._content)
-       , _trust_content_header (o._trust_content_header)
-       , _dcp_content_type  (o._dcp_content_type)
-       , _format            (o._format)
-       , _crop              (o._crop)
-       , _filters           (o._filters)
-       , _scaler            (o._scaler)
-       , _dcp_trim_start    (o._dcp_trim_start)
-       , _dcp_trim_end      (o._dcp_trim_end)
-       , _reel_size         (o._reel_size)
-       , _dcp_ab            (o._dcp_ab)
-       , _content_audio_stream (o._content_audio_stream)
-       , _external_audio    (o._external_audio)
-       , _use_content_audio (o._use_content_audio)
-       , _audio_gain        (o._audio_gain)
-       , _audio_delay       (o._audio_delay)
-       , _still_duration    (o._still_duration)
-       , _subtitle_stream   (o._subtitle_stream)
-       , _with_subtitles    (o._with_subtitles)
-       , _subtitle_offset   (o._subtitle_offset)
-       , _subtitle_scale    (o._subtitle_scale)
-       , _encrypted         (o._encrypted)
-       , _colour_lut        (o._colour_lut)
-       , _j2k_bandwidth     (o._j2k_bandwidth)
-       , _audio_language    (o._audio_language)
-       , _subtitle_language (o._subtitle_language)
-       , _territory         (o._territory)
-       , _rating            (o._rating)
-       , _studio            (o._studio)
-       , _facility          (o._facility)
-       , _package_type      (o._package_type)
-       , _size              (o._size)
-       , _length            (o._length)
-       , _content_digest    (o._content_digest)
-       , _content_audio_streams (o._content_audio_streams)
-       , _external_audio_stream (o._external_audio_stream)
-       , _subtitle_streams  (o._subtitle_streams)
-       , _frames_per_second (o._frames_per_second)
-       , _dirty             (o._dirty)
+ string
+ Film::video_identifier () const
  {
+       assert (container ());
+       LocaleGuard lg;
  
- }
+       stringstream s;
+       s << container()->id()
+         << "_" << resolution_to_string (_resolution)
+         << "_" << _playlist->video_identifier()
+         << "_" << _video_frame_rate
+         << "_" << scaler()->id()
+         << "_" << j2k_bandwidth();
  
- Film::~Film ()
- {
-       delete _log;
+       if (_interop) {
+               s << "_I";
+       } else {
+               s << "_S";
+       }
+       if (_three_d) {
+               s << "_3D";
+       }
+       return s.str ();
  }
          
- /** @return The path to the directory to write JPEG2000 files to */
+ /** @return The path to the directory to write video frame info files to */
  string
- Film::j2k_dir () const
+ Film::info_dir () const
  {
-       assert (format());
        boost::filesystem::path p;
+       p /= "info";
+       p /= video_identifier ();
+       return dir (p.string());
+ }
  
-       /* Start with j2c */
-       p /= "j2c";
+ string
+ Film::internal_video_mxf_dir () const
+ {
+       return dir ("video");
+ }
  
-       pair<string, string> f = Filter::ffmpeg_strings (filters());
+ string
+ Film::internal_video_mxf_filename () const
+ {
+       return video_identifier() + ".mxf";
+ }
  
-       /* Write stuff to specify the filter / post-processing settings that are in use,
-          so that we don't get confused about J2K files generated using different
-          settings.
-       */
-       stringstream s;
-       s << format()->id()
-         << "_" << content_digest()
-         << "_" << crop().left << "_" << crop().right << "_" << crop().top << "_" << crop().bottom
-         << "_" << f.first << "_" << f.second
-         << "_" << scaler()->id()
-         << "_" << j2k_bandwidth()
-         << "_" << boost::lexical_cast<int> (colour_lut());
+ string
+ Film::video_mxf_filename () const
+ {
+       return filename_safe_name() + "_video.mxf";
+ }
  
-       p /= s.str ();
+ string
+ Film::audio_mxf_filename () const
+ {
+       return filename_safe_name() + "_audio.mxf";
+ }
  
-       /* Similarly for the A/B case */
-       if (dcp_ab()) {
-               stringstream s;
-               pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters());
-               s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second;
-               p /= s.str ();
+ string
+ Film::filename_safe_name () const
+ {
+       string const n = name ();
+       string o;
+       for (size_t i = 0; i < n.length(); ++i) {
+               if (isalnum (n[i])) {
+                       o += n[i];
+               } else {
+                       o += "_";
+               }
        }
-       
-       return dir (p.string());
+       return o;
  }
  
- /** Add suitable Jobs to the JobManager to create a DCP for this Film.
-  *  @param true to transcode, false to use the WAV and J2K files that are already there.
-  */
+ boost::filesystem::path
+ Film::audio_analysis_path (shared_ptr<const AudioContent> c) const
+ {
+       boost::filesystem::path p = dir ("analysis");
+       p /= c->digest();
+       return p;
+ }
+ /** Add suitable Jobs to the JobManager to create a DCP for this Film */
  void
- Film::make_dcp (bool transcode)
+ Film::make_dcp ()
  {
        set_dci_date_today ();
        
        if (dcp_name().find ("/") != string::npos) {
-               throw BadSettingError ("name", "cannot contain slashes");
+               throw BadSettingError (_("name"), _("cannot contain slashes"));
        }
        
-       log()->log (String::compose ("DVD-o-matic %1 git %2 using %3", dvdomatic_version, dvdomatic_git_commit, dependency_version_summary()));
+       log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
  
        {
                char buffer[128];
                gethostname (buffer, sizeof (buffer));
                log()->log (String::compose ("Starting to make DCP on %1", buffer));
        }
-       
-       log()->log (String::compose ("Content is %1; type %2", content_path(), (content_type() == STILL ? "still" : "video")));
-       log()->log (String::compose ("Content length %1", length().get()));
-       log()->log (String::compose ("Content digest %1", content_digest()));
+       ContentList cl = content ();
+       for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) {
+               log()->log (String::compose ("Content: %1", (*i)->technical_summary()));
+       }
+       log()->log (String::compose ("DCP video rate %1 fps", video_frame_rate()));
        log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads()));
        log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth()));
- #ifdef DVDOMATIC_DEBUG
-       log()->log ("DVD-o-matic built in debug mode.");
+ #ifdef DCPOMATIC_DEBUG
+       log()->log ("DCP-o-matic built in debug mode.");
  #else
-       log()->log ("DVD-o-matic built in optimised mode.");
+       log()->log ("DCP-o-matic built in optimised mode.");
  #endif
  #ifdef LIBDCP_DEBUG
        log()->log ("libdcp built in debug mode.");
  #else
        log()->log ("libdcp built in optimised mode.");
  #endif
-       pair<string, int> const c = cpu_info ();
-       log()->log (String::compose ("CPU: %1, %2 processors", c.first, c.second));
+       log()->log (String::compose ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ()));
+       list<pair<string, string> > const m = mount_info ();
+       for (list<pair<string, string> >::const_iterator i = m.begin(); i != m.end(); ++i) {
+               log()->log (String::compose ("Mount: %1 %2", i->first, i->second));
+       }
        
-       if (format() == 0) {
-               throw MissingSettingError ("format");
+       if (container() == 0) {
+               throw MissingSettingError (_("container"));
        }
  
-       if (content().empty ()) {
-               throw MissingSettingError ("content");
+       if (content().empty()) {
+               throw StringError (_("You must add some content to the DCP before creating it"));
        }
  
        if (dcp_content_type() == 0) {
-               throw MissingSettingError ("content type");
+               throw MissingSettingError (_("content type"));
        }
  
        if (name().empty()) {
-               throw MissingSettingError ("name");
-       }
-       shared_ptr<EncodeOptions> oe (new EncodeOptions (j2k_dir(), ".j2c", dir ("wavs")));
-       oe->out_size = format()->dcp_size ();
-       oe->padding = format()->dcp_padding (shared_from_this ());
-       if (dcp_length ()) {
-               oe->video_range = make_pair (dcp_trim_start(), dcp_trim_start() + dcp_length().get());
-               if (audio_stream()) {
-                       oe->audio_range = make_pair (
-                               video_frames_to_audio_frames (
-                                       oe->video_range.get().first,
-                                       dcp_audio_sample_rate (audio_stream()->sample_rate()),
-                                       dcp_frame_rate (frames_per_second()).frames_per_second
-                                       ),
-                               
-                               video_frames_to_audio_frames (
-                                       oe->video_range.get().second,
-                                       dcp_audio_sample_rate (audio_stream()->sample_rate()),
-                                       dcp_frame_rate (frames_per_second()).frames_per_second
-                                       )
-                               );
-               }
-                       
-       }
-       
-       oe->video_skip = dcp_frame_rate (frames_per_second()).skip;
-       shared_ptr<DecodeOptions> od (new DecodeOptions);
-       od->decode_subtitles = with_subtitles ();
-       shared_ptr<Job> r;
-       if (transcode) {
-               if (dcp_ab()) {
-                       r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ())));
-               } else {
-                       r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ())));
-               }
+               throw MissingSettingError (_("name"));
        }
  
-       r = JobManager::instance()->add (shared_ptr<Job> (new CheckHashesJob (shared_from_this(), od, oe, r)));
-       JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (shared_from_this(), oe, r)));
- }
- /** Start a job to examine our content file */
- void
- Film::examine_content ()
- {
-       if (_examine_content_job) {
-               return;
-       }
-       _examine_content_job.reset (new ExamineContentJob (shared_from_this(), shared_ptr<Job> ()));
-       _examine_content_job->Finished.connect (bind (&Film::examine_content_finished, this));
-       JobManager::instance()->add (_examine_content_job);
- }
- void
- Film::examine_content_finished ()
- {
-       _examine_content_job.reset ();
+       JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
  }
  
  /** Start a job to send our DCP to the configured TMS */
  void
  Film::send_dcp_to_tms ()
  {
-       shared_ptr<Job> j (new SCPDCPJob (shared_from_this(), shared_ptr<Job> ()));
+       shared_ptr<Job> j (new SCPDCPJob (shared_from_this()));
        JobManager::instance()->add (j);
  }
  
  int
  Film::encoded_frames () const
  {
-       if (format() == 0) {
+       if (container() == 0) {
                return 0;
        }
  
        int N = 0;
-       for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (j2k_dir ()); i != boost::filesystem::directory_iterator(); ++i) {
+       for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (info_dir ()); i != boost::filesystem::directory_iterator(); ++i) {
                ++N;
                boost::this_thread::interruption_point ();
        }
  void
  Film::write_metadata () const
  {
-       boost::mutex::scoped_lock lm (_state_mutex);
+       if (!boost::filesystem::exists (directory())) {
+               boost::filesystem::create_directory (directory());
+       }
+       
+       LocaleGuard lg;
  
        boost::filesystem::create_directories (directory());
  
-       string const m = file ("metadata");
-       ofstream f (m.c_str ());
-       if (!f.good ()) {
-               throw CreateFileError (m);
-       }
+       xmlpp::Document doc;
+       xmlpp::Element* root = doc.create_root_node ("Metadata");
  
-       f << "version " << state_version << "\n";
+       root->add_child("Version")->add_child_text (lexical_cast<string> (state_version));
+       root->add_child("Name")->add_child_text (_name);
+       root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0");
  
-       /* User stuff */
-       f << "name " << _name << "\n";
-       f << "use_dci_name " << _use_dci_name << "\n";
-       f << "content " << _content << "\n";
-       f << "trust_content_header " << (_trust_content_header ? "1" : "0") << "\n";
        if (_dcp_content_type) {
-               f << "dcp_content_type " << _dcp_content_type->pretty_name () << "\n";
-       }
-       if (_format) {
-               f << "format " << _format->as_metadata () << "\n";
-       }
-       f << "left_crop " << _crop.left << "\n";
-       f << "right_crop " << _crop.right << "\n";
-       f << "top_crop " << _crop.top << "\n";
-       f << "bottom_crop " << _crop.bottom << "\n";
-       for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
-               f << "filter " << (*i)->id () << "\n";
-       }
-       f << "scaler " << _scaler->id () << "\n";
-       f << "dcp_trim_start " << _dcp_trim_start << "\n";
-       f << "dcp_trim_end " << _dcp_trim_end << "\n";
-       if (_reel_size) {
-               f << "reel_size " << _reel_size.get() << "\n";
-       }
-       f << "dcp_ab " << (_dcp_ab ? "1" : "0") << "\n";
-       if (_content_audio_stream) {
-               f << "selected_content_audio_stream " << _content_audio_stream->to_string() << "\n";
-       }
-       for (vector<string>::const_iterator i = _external_audio.begin(); i != _external_audio.end(); ++i) {
-               f << "external_audio " << *i << "\n";
-       }
-       f << "use_content_audio " << (_use_content_audio ? "1" : "0") << "\n";
-       f << "audio_gain " << _audio_gain << "\n";
-       f << "audio_delay " << _audio_delay << "\n";
-       f << "still_duration " << _still_duration << "\n";
-       if (_subtitle_stream) {
-               f << "selected_subtitle_stream " << _subtitle_stream->to_string() << "\n";
+               root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ());
        }
-       f << "with_subtitles " << _with_subtitles << "\n";
-       f << "subtitle_offset " << _subtitle_offset << "\n";
-       f << "subtitle_scale " << _subtitle_scale << "\n";
-       f << "encrypted " << _encrypted << "\n";
-       f << "colour_lut " << _colour_lut << "\n";
-       f << "j2k_bandwidth " << _j2k_bandwidth << "\n";
-       f << "audio_language " << _audio_language << "\n";
-       f << "subtitle_language " << _subtitle_language << "\n";
-       f << "territory " << _territory << "\n";
-       f << "rating " << _rating << "\n";
-       f << "studio " << _studio << "\n";
-       f << "facility " << _facility << "\n";
-       f << "package_type " << _package_type << "\n";
-       f << "width " << _size.width << "\n";
-       f << "height " << _size.height << "\n";
-       f << "length " << _length.get_value_or(0) << "\n";
-       f << "content_digest " << _content_digest << "\n";
-       for (vector<shared_ptr<AudioStream> >::const_iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
-               f << "content_audio_stream " << (*i)->to_string () << "\n";
-       }
-       f << "external_audio_stream " << _external_audio_stream->to_string() << "\n";
  
-       for (vector<shared_ptr<SubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) {
-               f << "subtitle_stream " << (*i)->to_string () << "\n";
+       if (_container) {
+               root->add_child("Container")->add_child_text (_container->id ());
        }
  
-       f << "frames_per_second " << _frames_per_second << "\n";
+       root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution));
+       root->add_child("Scaler")->add_child_text (_scaler->id ());
+       root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
+       root->add_child("J2KBandwidth")->add_child_text (lexical_cast<string> (_j2k_bandwidth));
+       _dci_metadata.as_xml (root->add_child ("DCIMetadata"));
+       root->add_child("VideoFrameRate")->add_child_text (lexical_cast<string> (_video_frame_rate));
+       root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date));
+       root->add_child("AudioChannels")->add_child_text (lexical_cast<string> (_audio_channels));
+       root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
+       root->add_child("SequenceVideo")->add_child_text (_sequence_video ? "1" : "0");
+       root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
++      root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
+       _playlist->as_xml (root->add_child ("Playlist"));
+       doc.write_to_file_formatted (file ("metadata.xml"));
        
        _dirty = false;
  }
  void
  Film::read_metadata ()
  {
-       boost::mutex::scoped_lock lm (_state_mutex);
-       _external_audio.clear ();
-       _content_audio_streams.clear ();
-       _subtitle_streams.clear ();
-       boost::optional<int> version;
-       /* Backward compatibility things */
-       boost::optional<int> audio_sample_rate;
-       boost::optional<int> audio_stream_index;
-       boost::optional<int> subtitle_stream_index;
+       LocaleGuard lg;
  
-       ifstream f (file ("metadata").c_str());
-       if (!f.good()) {
-               throw OpenFileError (file("metadata"));
+       if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
+               throw StringError (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
        }
-       
-       multimap<string, string> kv = read_key_value (f);
  
-       /* We need version before anything else */
-       multimap<string, string>::iterator v = kv.find ("version");
-       if (v != kv.end ()) {
-               version = atoi (v->second.c_str());
-       }
+       cxml::Document f ("Metadata");
+       f.read_file (file ("metadata.xml"));
        
-       for (multimap<string, string>::const_iterator i = kv.begin(); i != kv.end(); ++i) {
-               string const k = i->first;
-               string const v = i->second;
+       _name = f.string_child ("Name");
+       _use_dci_name = f.bool_child ("UseDCIName");
  
-               if (k == "audio_sample_rate") {
-                       audio_sample_rate = atoi (v.c_str());
+       {
+               optional<string> c = f.optional_string_child ("DCPContentType");
+               if (c) {
+                       _dcp_content_type = DCPContentType::from_dci_name (c.get ());
                }
+       }
  
-               /* User-specified stuff */
-               if (k == "name") {
-                       _name = v;
-               } else if (k == "use_dci_name") {
-                       _use_dci_name = (v == "1");
-               } else if (k == "content") {
-                       _content = v;
-               } else if (k == "trust_content_header") {
-                       _trust_content_header = (v == "1");
-               } else if (k == "dcp_content_type") {
-                       _dcp_content_type = DCPContentType::from_pretty_name (v);
-               } else if (k == "format") {
-                       _format = Format::from_metadata (v);
-               } else if (k == "left_crop") {
-                       _crop.left = atoi (v.c_str ());
-               } else if (k == "right_crop") {
-                       _crop.right = atoi (v.c_str ());
-               } else if (k == "top_crop") {
-                       _crop.top = atoi (v.c_str ());
-               } else if (k == "bottom_crop") {
-                       _crop.bottom = atoi (v.c_str ());
-               } else if (k == "filter") {
-                       _filters.push_back (Filter::from_id (v));
-               } else if (k == "scaler") {
-                       _scaler = Scaler::from_id (v);
-               } else if (k == "dcp_trim_start") {
-                       _dcp_trim_start = atoi (v.c_str ());
-               } else if (k == "dcp_trim_end") {
-                       _dcp_trim_end = atoi (v.c_str ());
-               } else if (k == "reel_size") {
-                       _reel_size = boost::lexical_cast<uint64_t> (v);
-               } else if (k == "dcp_ab") {
-                       _dcp_ab = (v == "1");
-               } else if (k == "selected_content_audio_stream" || (!version && k == "selected_audio_stream")) {
-                       if (!version) {
-                               audio_stream_index = atoi (v.c_str ());
-                       } else {
-                               _content_audio_stream = audio_stream_factory (v, version);
-                       }
-               } else if (k == "external_audio") {
-                       _external_audio.push_back (v);
-               } else if (k == "use_content_audio") {
-                       _use_content_audio = (v == "1");
-               } else if (k == "audio_gain") {
-                       _audio_gain = atof (v.c_str ());
-               } else if (k == "audio_delay") {
-                       _audio_delay = atoi (v.c_str ());
-               } else if (k == "still_duration") {
-                       _still_duration = atoi (v.c_str ());
-               } else if (k == "selected_subtitle_stream") {
-                       if (!version) {
-                               subtitle_stream_index = atoi (v.c_str ());
-                       } else {
-                               _subtitle_stream = subtitle_stream_factory (v, version);
-                       }
-               } else if (k == "with_subtitles") {
-                       _with_subtitles = (v == "1");
-               } else if (k == "subtitle_offset") {
-                       _subtitle_offset = atoi (v.c_str ());
-               } else if (k == "subtitle_scale") {
-                       _subtitle_scale = atof (v.c_str ());
-               } else if (k == "encrypted") {
-                       _encrypted = (v == "1");
-               } else if (k == "colour_lut") {
-                       _colour_lut = atoi (v.c_str ());
-               } else if (k == "j2k_bandwidth") {
-                       _j2k_bandwidth = atoi (v.c_str ());
-               } else if (k == "audio_language") {
-                       _audio_language = v;
-               } else if (k == "subtitle_language") {
-                       _subtitle_language = v;
-               } else if (k == "territory") {
-                       _territory = v;
-               } else if (k == "rating") {
-                       _rating = v;
-               } else if (k == "studio") {
-                       _studio = v;
-               } else if (k == "facility") {
-                       _facility = v;
-               } else if (k == "package_type") {
-                       _package_type = v;
-               }
-               
-               /* Cached stuff */
-               if (k == "width") {
-                       _size.width = atoi (v.c_str ());
-               } else if (k == "height") {
-                       _size.height = atoi (v.c_str ());
-               } else if (k == "length") {
-                       int const vv = atoi (v.c_str ());
-                       if (vv) {
-                               _length = vv;
-                       }
-               } else if (k == "content_digest") {
-                       _content_digest = v;
-               } else if (k == "content_audio_stream" || (!version && k == "audio_stream")) {
-                       _content_audio_streams.push_back (audio_stream_factory (v, version));
-               } else if (k == "external_audio_stream") {
-                       _external_audio_stream = audio_stream_factory (v, version);
-               } else if (k == "subtitle_stream") {
-                       _subtitle_streams.push_back (subtitle_stream_factory (v, version));
-               } else if (k == "frames_per_second") {
-                       _frames_per_second = atof (v.c_str ());
+       {
+               optional<string> c = f.optional_string_child ("Container");
+               if (c) {
+                       _container = Ratio::from_id (c.get ());
                }
        }
  
-       if (!version) {
-               if (audio_sample_rate) {
-                       /* version < 1 didn't specify sample rate in the audio streams, so fill it in here */
-                       for (vector<shared_ptr<AudioStream> >::iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
-                               (*i)->set_sample_rate (audio_sample_rate.get());
-                       }
-               }
+       _resolution = string_to_resolution (f.string_child ("Resolution"));
+       _scaler = Scaler::from_id (f.string_child ("Scaler"));
+       _with_subtitles = f.bool_child ("WithSubtitles");
+       _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
+       _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata"));
+       _video_frame_rate = f.number_child<int> ("VideoFrameRate");
+       _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate"));
+       _audio_channels = f.number_child<int> ("AudioChannels");
+       _sequence_video = f.bool_child ("SequenceVideo");
+       _three_d = f.bool_child ("ThreeD");
+       _interop = f.bool_child ("Interop");
  
-               /* also the selected stream was specified as an index */
-               if (audio_stream_index && audio_stream_index.get() >= 0 && audio_stream_index.get() < (int) _content_audio_streams.size()) {
-                       _content_audio_stream = _content_audio_streams[audio_stream_index.get()];
-               }
+       _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"));
  
-               /* similarly the subtitle */
-               if (subtitle_stream_index && subtitle_stream_index.get() >= 0 && subtitle_stream_index.get() < (int) _subtitle_streams.size()) {
-                       _subtitle_stream = _subtitle_streams[subtitle_stream_index.get()];
-               }
-       }
-               
        _dirty = false;
  }
  
- Size
- Film::cropped_size (Size s) const
- {
-       boost::mutex::scoped_lock lm (_state_mutex);
-       s.width -= _crop.left + _crop.right;
-       s.height -= _crop.top + _crop.bottom;
-       return s;
- }
  /** Given a directory name, return its full path within the Film's directory.
   *  The directory (and its parents) will be created if they do not exist.
   */
  string
  Film::dir (string d) const
  {
-       boost::mutex::scoped_lock lm (_directory_mutex);
        boost::filesystem::path p;
        p /= _directory;
        p /= d;
+       
        boost::filesystem::create_directories (p);
+       
        return p.string ();
  }
  
  /** Given a file or directory name, return its full path within the Film's directory.
-  *  _directory_mutex must not be locked on entry.
+  *  Any required parent directories will be created.
   */
  string
  Film::file (string f) const
  {
-       boost::mutex::scoped_lock lm (_directory_mutex);
        boost::filesystem::path p;
        p /= _directory;
        p /= f;
-       return p.string ();
- }
- /** @return full path of the content (actual video) file
-  *  of the Film.
-  */
- string
- Film::content_path () const
- {
-       boost::mutex::scoped_lock lm (_state_mutex);
-       if (boost::filesystem::path(_content).has_root_directory ()) {
-               return _content;
-       }
-       return file (_content);
- }
  
- ContentType
- Film::content_type () const
- {
-       if (boost::filesystem::is_directory (_content)) {
-               /* Directory of images, we assume */
-               return VIDEO;
-       }
-       if (still_image_file (_content)) {
-               return STILL;
-       }
-       return VIDEO;
- }
- /** @return The sampling rate that we will resample the audio to */
- int
- Film::target_audio_sample_rate () const
- {
-       if (!audio_stream()) {
-               return 0;
-       }
+       boost::filesystem::create_directories (p.parent_path ());
        
-       /* Resample to a DCI-approved sample rate */
-       double t = dcp_audio_sample_rate (audio_stream()->sample_rate());
-       DCPFrameRate dfr = dcp_frame_rate (frames_per_second ());
-       /* Compensate for the fact that video will be rounded to the
-          nearest integer number of frames per second.
-       */
-       if (dfr.run_fast) {
-               t *= _frames_per_second * dfr.skip / dfr.frames_per_second;
-       }
-       return rint (t);
- }
- boost::optional<int>
- Film::dcp_length () const
- {
-       if (content_type() == STILL) {
-               return _still_duration * frames_per_second();
-       }
-       
-       if (!length()) {
-               return boost::optional<int> ();
-       }
-       return length().get() - dcp_trim_start() - dcp_trim_end();
+       return p.string ();
  }
  
  /** @return a DCI-compliant name for a DCP of this film */
  string
- Film::dci_name () const
+ Film::dci_name (bool if_created_now) const
  {
        stringstream d;
  
                fixed_name = fixed_name.substr (0, 14);
        }
  
-       d << fixed_name << "_";
+       d << fixed_name;
  
        if (dcp_content_type()) {
-               d << dcp_content_type()->dci_name() << "_";
+               d << "_" << dcp_content_type()->dci_name();
+               d << "-" << dci_metadata().content_version;
+       }
+       if (three_d ()) {
+               d << "-3D";
+       }
+       if (video_frame_rate() != 24) {
+               d << "-" << video_frame_rate();
        }
  
-       if (format()) {
-               d << format()->dci_name() << "_";
+       if (container()) {
+               d << "_" << container()->dci_name();
        }
  
-       if (!audio_language().empty ()) {
-               d << audio_language();
-               if (!subtitle_language().empty() && with_subtitles()) {
-                       d << "-" << subtitle_language();
+       DCIMetadata const dm = dci_metadata ();
+       if (!dm.audio_language.empty ()) {
+               d << "_" << dm.audio_language;
+               if (!dm.subtitle_language.empty() && with_subtitles()) {
+                       d << "-" << dm.subtitle_language;
                } else {
                        d << "-XX";
                }
-                       
-               d << "_";
        }
  
-       if (!territory().empty ()) {
-               d << territory();
-               if (!rating().empty ()) {
-                       d << "-" << rating();
+       if (!dm.territory.empty ()) {
+               d << "_" << dm.territory;
+               if (!dm.rating.empty ()) {
+                       d << "-" << dm.rating;
                }
-               d << "_";
        }
  
-       switch (audio_channels()) {
+       switch (audio_channels ()) {
        case 1:
-               d << "10_";
+               d << "_10";
                break;
        case 2:
-               d << "20_";
+               d << "_20";
                break;
-       case 6:
-               d << "51_";
+       case 3:
+               d << "_30";
+               break;
+       case 4:
+               d << "_40";
                break;
-       case 8:
-               d << "71_";
+       case 5:
+               d << "_50";
+               break;
+       case 6:
+               d << "_51";
                break;
        }
  
-       d << "2K_";
+       d << "_" << resolution_to_string (_resolution);
  
-       if (!studio().empty ()) {
-               d << studio() << "_";
+       if (!dm.studio.empty ()) {
+               d << "_" << dm.studio;
        }
  
-       d << boost::gregorian::to_iso_string (_dci_date) << "_";
+       if (if_created_now) {
+               d << "_" << boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ());
+       } else {
+               d << "_" << boost::gregorian::to_iso_string (_dci_date);
+       }
  
-       if (!facility().empty ()) {
-               d << facility() << "_";
+       if (!dm.facility.empty ()) {
+               d << "_" << dm.facility;
        }
  
-       if (!package_type().empty ()) {
-               d << package_type();
+       if (!dm.package_type.empty ()) {
+               d << "_" << dm.package_type;
        }
  
        return d.str ();
  
  /** @return name to give the DCP */
  string
- Film::dcp_name () const
+ Film::dcp_name (bool if_created_now) const
  {
        if (use_dci_name()) {
-               return dci_name ();
+               return dci_name (if_created_now);
        }
  
        return name();
  void
  Film::set_directory (string d)
  {
-       boost::mutex::scoped_lock lm (_state_mutex);
        _directory = d;
        _dirty = true;
  }
  void
  Film::set_name (string n)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _name = n;
-       }
+       _name = n;
        signal_changed (NAME);
  }
  
  void
  Film::set_use_dci_name (bool u)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _use_dci_name = u;
-       }
+       _use_dci_name = u;
        signal_changed (USE_DCI_NAME);
  }
  
- void
- Film::set_content (string c)
- {
-       string check = directory ();
- #if BOOST_FILESYSTEM_VERSION == 3
-       boost::filesystem::path slash ("/");
-       string platform_slash = slash.make_preferred().string ();
- #else
- #ifdef DVDOMATIC_WINDOWS
-       string platform_slash = "\\";
- #else
-       string platform_slash = "/";
- #endif
- #endif        
-       if (!ends_with (check, platform_slash)) {
-               check += platform_slash;
-       }
-       
-       if (boost::filesystem::path(c).has_root_directory () && starts_with (c, check)) {
-               c = c.substr (_directory.length() + 1);
-       }
-       string old_content;
-       
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               if (c == _content) {
-                       return;
-               }
-               old_content = _content;
-               _content = c;
-       }
-       /* Reset streams here in case the new content doesn't have one or the other */
-       _content_audio_stream = shared_ptr<AudioStream> ();
-       _subtitle_stream = shared_ptr<SubtitleStream> ();
-       /* Start off using content audio */
-       set_use_content_audio (true);
-       /* Create a temporary decoder so that we can get information
-          about the content.
-       */
-       try {
-               shared_ptr<DecodeOptions> o (new DecodeOptions);
-               Decoders d = decoder_factory (shared_from_this(), o, 0);
-               
-               set_size (d.video->native_size ());
-               set_frames_per_second (d.video->frames_per_second ());
-               set_subtitle_streams (d.video->subtitle_streams ());
-               if (d.audio) {
-                       set_content_audio_streams (d.audio->audio_streams ());
-               }
-               /* Start off with the first audio and subtitle streams */
-               if (d.audio && !d.audio->audio_streams().empty()) {
-                       set_content_audio_stream (d.audio->audio_streams().front());
-               }
-               
-               if (!d.video->subtitle_streams().empty()) {
-                       set_subtitle_stream (d.video->subtitle_streams().front());
-               }
-               
-               {
-                       boost::mutex::scoped_lock lm (_state_mutex);
-                       _content = c;
-               }
-               
-               signal_changed (CONTENT);
-               
-               examine_content ();
-       } catch (...) {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _content = old_content;
-               throw;
-       }
-       /* Default format */
-       switch (content_type()) {
-       case STILL:
-               set_format (Format::from_id ("var-185"));
-               break;
-       case VIDEO:
-               set_format (Format::from_id ("185"));
-               break;
-       }
-       /* Still image DCPs must use external audio */
-       if (content_type() == STILL) {
-               set_use_content_audio (false);
-       }
- }
- void
- Film::set_trust_content_header (bool t)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _trust_content_header = t;
-       }
-       
-       signal_changed (TRUST_CONTENT_HEADER);
-       if (!_trust_content_header && !content().empty()) {
-               /* We just said that we don't trust the content's header */
-               examine_content ();
-       }
- }
-              
  void
  Film::set_dcp_content_type (DCPContentType const * t)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _dcp_content_type = t;
-       }
+       _dcp_content_type = t;
        signal_changed (DCP_CONTENT_TYPE);
  }
  
  void
- Film::set_format (Format const * f)
+ Film::set_container (Ratio const * c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _format = f;
-       }
-       signal_changed (FORMAT);
+       _container = c;
+       signal_changed (CONTAINER);
  }
  
  void
- Film::set_crop (Crop c)
+ Film::set_resolution (Resolution r)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _crop = c;
-       }
-       signal_changed (CROP);
+       _resolution = r;
+       signal_changed (RESOLUTION);
  }
  
  void
- Film::set_left_crop (int c)
+ Film::set_scaler (Scaler const * s)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               
-               if (_crop.left == c) {
-                       return;
-               }
-               
-               _crop.left = c;
-       }
-       signal_changed (CROP);
+       _scaler = s;
+       signal_changed (SCALER);
  }
  
  void
- Film::set_right_crop (int c)
+ Film::set_with_subtitles (bool w)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               if (_crop.right == c) {
-                       return;
-               }
-               
-               _crop.right = c;
-       }
-       signal_changed (CROP);
+       _with_subtitles = w;
+       signal_changed (WITH_SUBTITLES);
  }
  
  void
- Film::set_top_crop (int c)
+ Film::set_j2k_bandwidth (int b)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               if (_crop.top == c) {
-                       return;
-               }
-               
-               _crop.top = c;
-       }
-       signal_changed (CROP);
+       _j2k_bandwidth = b;
+       signal_changed (J2K_BANDWIDTH);
  }
  
  void
- Film::set_bottom_crop (int c)
+ Film::set_dci_metadata (DCIMetadata m)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               if (_crop.bottom == c) {
-                       return;
-               }
-               
-               _crop.bottom = c;
-       }
-       signal_changed (CROP);
+       _dci_metadata = m;
+       signal_changed (DCI_METADATA);
  }
  
  void
- Film::set_filters (vector<Filter const *> f)
+ Film::set_video_frame_rate (int f)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _filters = f;
-       }
-       signal_changed (FILTERS);
+       _video_frame_rate = f;
+       signal_changed (VIDEO_FRAME_RATE);
  }
  
  void
- Film::set_scaler (Scaler const * s)
+ Film::set_audio_channels (int c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _scaler = s;
-       }
-       signal_changed (SCALER);
+       _audio_channels = c;
+       signal_changed (AUDIO_CHANNELS);
  }
  
  void
- Film::set_dcp_trim_start (int t)
+ Film::set_three_d (bool t)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _dcp_trim_start = t;
-       }
-       signal_changed (DCP_TRIM_START);
+       _three_d = t;
+       signal_changed (THREE_D);
  }
  
  void
- Film::set_dcp_trim_end (int t)
+ Film::set_interop (bool i)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _dcp_trim_end = t;
-       }
-       signal_changed (DCP_TRIM_END);
+       _interop = i;
+       signal_changed (INTEROP);
  }
  
  void
- Film::set_reel_size (uint64_t s)
+ Film::signal_changed (Property p)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _reel_size = s;
-       }
-       signal_changed (REEL_SIZE);
- }
+       _dirty = true;
  
- void
- Film::unset_reel_size ()
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _reel_size = boost::optional<uint64_t> ();
+       switch (p) {
+       case Film::CONTENT:
+               set_video_frame_rate (_playlist->best_dcp_frame_rate ());
+               break;
+       case Film::VIDEO_FRAME_RATE:
+       case Film::SEQUENCE_VIDEO:
+               _playlist->maybe_sequence_video ();
+               break;
+       default:
+               break;
        }
-       signal_changed (REEL_SIZE);
- }
  
- void
- Film::set_dcp_ab (bool a)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _dcp_ab = a;
+       if (ui_signaller) {
+               ui_signaller->emit (boost::bind (boost::ref (Changed), p));
        }
-       signal_changed (DCP_AB);
  }
  
  void
- Film::set_content_audio_stream (shared_ptr<AudioStream> s)
+ Film::set_dci_date_today ()
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _content_audio_stream = s;
-       }
-       signal_changed (CONTENT_AUDIO_STREAM);
+       _dci_date = boost::gregorian::day_clock::local_day ();
  }
  
- void
- Film::set_external_audio (vector<string> a)
+ string
+ Film::info_path (int f, Eyes e) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _external_audio = a;
-       }
+       boost::filesystem::path p;
+       p /= info_dir ();
  
-       shared_ptr<DecodeOptions> o (new DecodeOptions);
-       shared_ptr<ExternalAudioDecoder> decoder (new ExternalAudioDecoder (shared_from_this(), o, 0));
-       if (decoder->audio_stream()) {
-               _external_audio_stream = decoder->audio_stream ();
+       stringstream s;
+       s.width (8);
+       s << setfill('0') << f;
+       if (e == EYES_LEFT) {
+               s << ".L";
+       } else if (e == EYES_RIGHT) {
+               s << ".R";
        }
+       s << ".md5";
        
-       signal_changed (EXTERNAL_AUDIO);
+       p /= s.str();
+       /* info_dir() will already have added any initial bit of the path,
+          so don't call file() on this.
+       */
+       return p.string ();
  }
  
- void
- Film::set_use_content_audio (bool e)
+ string
+ Film::j2c_path (int f, Eyes e, bool t) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _use_content_audio = e;
-       }
+       boost::filesystem::path p;
+       p /= "j2c";
+       p /= video_identifier ();
  
-       signal_changed (USE_CONTENT_AUDIO);
- }
+       stringstream s;
+       s.width (8);
+       s << setfill('0') << f;
  
- void
- Film::set_audio_gain (float g)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _audio_gain = g;
+       if (e == EYES_LEFT) {
+               s << ".L";
+       } else if (e == EYES_RIGHT) {
+               s << ".R";
        }
-       signal_changed (AUDIO_GAIN);
- }
+       
+       s << ".j2c";
  
- void
- Film::set_audio_delay (int d)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _audio_delay = d;
+       if (t) {
+               s << ".tmp";
        }
-       signal_changed (AUDIO_DELAY);
- }
  
- void
- Film::set_still_duration (int d)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _still_duration = d;
-       }
-       signal_changed (STILL_DURATION);
+       p /= s.str();
+       return file (p.string ());
  }
  
- void
- Film::set_subtitle_stream (shared_ptr<SubtitleStream> s)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _subtitle_stream = s;
-       }
-       signal_changed (SUBTITLE_STREAM);
- }
+ /** Make an educated guess as to whether we have a complete DCP
+  *  or not.
+  *  @return true if we do.
+  */
  
- void
- Film::set_with_subtitles (bool w)
+ bool
+ Film::have_dcp () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _with_subtitles = w;
+       try {
+               libdcp::DCP dcp (dir (dcp_name()));
+               dcp.read ();
+       } catch (...) {
+               return false;
        }
-       signal_changed (WITH_SUBTITLES);
- }
  
- void
- Film::set_subtitle_offset (int o)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _subtitle_offset = o;
-       }
-       signal_changed (SUBTITLE_OFFSET);
+       return true;
  }
  
- void
- Film::set_subtitle_scale (float s)
+ shared_ptr<Player>
+ Film::make_player () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _subtitle_scale = s;
-       }
-       signal_changed (SUBTITLE_SCALE);
+       return shared_ptr<Player> (new Player (shared_from_this (), _playlist));
  }
  
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _encrypted = e;
-       }
 +void
 +Film::set_encrypted (bool e)
 +{
- void
- Film::set_colour_lut (int i)
++      _encrypted = e;
 +      signal_changed (ENCRYPTED);
 +}
 +
+ shared_ptr<Playlist>
+ Film::playlist () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _colour_lut = i;
-       }
-       signal_changed (COLOUR_LUT);
+       return _playlist;
  }
  
- void
- Film::set_j2k_bandwidth (int b)
+ ContentList
+ Film::content () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _j2k_bandwidth = b;
-       }
-       signal_changed (J2K_BANDWIDTH);
+       return _playlist->content ();
  }
  
  void
- Film::set_audio_language (string l)
+ Film::examine_and_add_content (shared_ptr<Content> c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _audio_language = l;
-       }
-       signal_changed (DCI_METADATA);
+       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
+       j->Finished.connect (bind (&Film::maybe_add_content, this, boost::weak_ptr<Job> (j), boost::weak_ptr<Content> (c)));
+       JobManager::instance()->add (j);
  }
  
  void
- Film::set_subtitle_language (string l)
+ Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _subtitle_language = l;
+       shared_ptr<Job> job = j.lock ();
+       if (!job || !job->finished_ok ()) {
+               return;
        }
-       signal_changed (DCI_METADATA);
- }
- void
- Film::set_territory (string t)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _territory = t;
+       
+       shared_ptr<Content> content = c.lock ();
+       if (content) {
+               add_content (content);
        }
-       signal_changed (DCI_METADATA);
  }
  
  void
- Film::set_rating (string r)
+ Film::add_content (shared_ptr<Content> c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _rating = r;
+       /* Add video content after any existing content */
+       if (dynamic_pointer_cast<VideoContent> (c)) {
+               c->set_position (_playlist->video_end ());
        }
-       signal_changed (DCI_METADATA);
+       _playlist->add (c);
  }
  
  void
- Film::set_studio (string s)
+ Film::remove_content (shared_ptr<Content> c)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _studio = s;
-       }
-       signal_changed (DCI_METADATA);
+       _playlist->remove (c);
  }
  
- void
- Film::set_facility (string f)
+ Time
+ Film::length () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _facility = f;
-       }
-       signal_changed (DCI_METADATA);
+       return _playlist->length ();
  }
  
- void
- Film::set_package_type (string p)
+ bool
+ Film::has_subtitles () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _package_type = p;
-       }
-       signal_changed (DCI_METADATA);
+       return _playlist->has_subtitles ();
  }
  
- void
- Film::set_size (Size s)
+ OutputVideoFrame
+ Film::best_video_frame_rate () const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _size = s;
-       }
-       signal_changed (SIZE);
+       return _playlist->best_dcp_frame_rate ();
  }
  
  void
- Film::set_length (SourceFrame l)
+ Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _length = l;
+       if (p == VideoContentProperty::VIDEO_FRAME_RATE) {
+               set_video_frame_rate (_playlist->best_dcp_frame_rate ());
+       } 
+       if (ui_signaller) {
+               ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p));
        }
-       signal_changed (LENGTH);
  }
  
  void
- Film::unset_length ()
+ Film::playlist_changed ()
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _length = boost::none;
-       }
-       signal_changed (LENGTH);
+       signal_changed (CONTENT);
  }     
  
- void
- Film::set_content_digest (string d)
+ OutputAudioFrame
+ Film::time_to_audio_frames (Time t) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _content_digest = d;
-       }
-       _dirty = true;
+       return t * audio_frame_rate () / TIME_HZ;
  }
  
- void
- Film::set_content_audio_streams (vector<shared_ptr<AudioStream> > s)
+ OutputVideoFrame
+ Film::time_to_video_frames (Time t) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _content_audio_streams = s;
-       }
-       signal_changed (CONTENT_AUDIO_STREAMS);
+       return t * video_frame_rate () / TIME_HZ;
  }
  
- void
- Film::set_subtitle_streams (vector<shared_ptr<SubtitleStream> > s)
+ Time
+ Film::audio_frames_to_time (OutputAudioFrame f) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _subtitle_streams = s;
-       }
-       signal_changed (SUBTITLE_STREAMS);
+       return f * TIME_HZ / audio_frame_rate ();
  }
  
- void
- Film::set_frames_per_second (float f)
+ Time
+ Film::video_frames_to_time (OutputVideoFrame f) const
  {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _frames_per_second = f;
-       }
-       signal_changed (FRAMES_PER_SECOND);
- }
-       
- void
- Film::signal_changed (Property p)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _dirty = true;
-       }
-       if (ui_signaller) {
-               ui_signaller->emit (boost::bind (boost::ref (Changed), p));
-       }
+       return f * TIME_HZ / video_frame_rate ();
  }
  
- int
- Film::audio_channels () const
+ OutputAudioFrame
+ Film::audio_frame_rate () const
  {
-       shared_ptr<AudioStream> s = audio_stream ();
-       if (!s) {
-               return 0;
-       }
-       return s->channels ();
+       /* XXX */
+       return 48000;
  }
  
  void
- Film::set_dci_date_today ()
+ Film::set_sequence_video (bool s)
  {
-       _dci_date = boost::gregorian::day_clock::local_day ();
+       _sequence_video = s;
+       _playlist->set_sequence_video (s);
+       signal_changed (SEQUENCE_VIDEO);
  }
  
- boost::shared_ptr<AudioStream>
- Film::audio_stream () const
+ libdcp::Size
+ Film::full_frame () const
  {
-       if (use_content_audio()) {
-               return _content_audio_stream;
+       switch (_resolution) {
+       case RESOLUTION_2K:
+               return libdcp::Size (2048, 1080);
+       case RESOLUTION_4K:
+               return libdcp::Size (4096, 2160);
        }
  
-       return _external_audio_stream;
+       assert (false);
+       return libdcp::Size ();
  }
-               shared_ptr<xmlpp::Document> kdm = dcp.cpls().front()->make_kdm (chain, signer_key.string(), (*i)->certificate, from, until);
 +
 +void
 +Film::make_kdms (
 +      list<shared_ptr<Screen> > screens,
 +      boost::posix_time::ptime from,
 +      boost::posix_time::ptime until,
 +      string directory
 +      ) const
 +{
 +      string const cd = Config::instance()->crypt_chain_directory ();
 +      if (boost::filesystem::is_empty (cd)) {
 +              libdcp::make_crypt_chain (cd);
 +      }
 +
 +      libdcp::CertificateChain chain;
 +
 +      {
 +              boost::filesystem::path p (cd);
 +              p /= "ca.self-signed.pem";
 +              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p.string ())));
 +      }
 +
 +      {
 +              boost::filesystem::path p (cd);
 +              p /= "intermediate.signed.pem";
 +              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p.string ())));
 +      }
 +
 +      {
 +              boost::filesystem::path p (cd);
 +              p /= "leaf.signed.pem";
 +              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p.string ())));
 +      }
 +
 +      boost::filesystem::path signer_key (cd);
 +      signer_key /= "leaf.key";
 +
 +      /* Find the DCP to make the KDM for */
 +      string const dir = this->directory ();
 +      list<string> dcps;
 +      for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) {
 +              if (boost::filesystem::is_directory (*i) && i->path().leaf() != "j2c" && i->path().leaf() != "wavs") {
 +                      dcps.push_back (i->path().string());
 +              }
 +      }
 +
 +      if (dcps.empty()) {
 +              throw KDMError ("Could not find DCP to make KDM for");
 +      } else if (dcps.size() > 1) {
 +              throw KDMError ("More than one possible DCP to make KDM for");
 +      }
 +
 +      for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
 +
 +              libdcp::DCP dcp (dcps.front ());
 +              dcp.read ();
 +              
 +              /* XXX: single CPL only */
++              shared_ptr<xmlpp::Document> kdm = dcp.cpls().front()->make_kdm (
++                      chain, signer_key.string(), (*i)->certificate, from, until, _interop, libdcp::MXFMetadata (), Config::instance()->dcp_metadata ()
++                      );
 +
 +              boost::filesystem::path out = directory;
 +              out /= "kdm.xml";
 +              kdm->write_to_file_formatted (out.string());
 +      }
 +}
 +      
diff --combined src/lib/film.h
index 1a78e9d34a606e62037c259316e01eeb47dac2b8,f5b29466afe44574d033ae504d605f43bd6d49ba..809eabdaa9a1257cae980be9ce68a2c0b78e8452
  */
  
  /** @file  src/film.h
-  *  @brief A representation of a piece of video (with sound), including naming,
-  *  the source content file, and how it should be presented in a DCP.
+  *  @brief A representation of some audio and video content, and details of
+  *  how they should be presented in a DCP.
   */
  
- #ifndef DVDOMATIC_FILM_H
- #define DVDOMATIC_FILM_H
+ #ifndef DCPOMATIC_FILM_H
+ #define DCPOMATIC_FILM_H
  
  #include <string>
  #include <vector>
  #include <inttypes.h>
- #include <boost/thread/mutex.hpp>
- #include <boost/thread.hpp>
  #include <boost/signals2.hpp>
  #include <boost/enable_shared_from_this.hpp>
- #include <boost/date_time/posix_time/posix_time.hpp>
- extern "C" {
- #include <libavcodec/avcodec.h>
- }
- #include "dcp_content_type.h"
+ #include <boost/filesystem.hpp>
  #include "util.h"
- #include "stream.h"
+ #include "types.h"
+ #include "dci_metadata.h"
  
- class Format;
- class Job;
- class Filter;
+ class DCPContentType;
  class Log;
- class ExamineContentJob;
- class ExternalAudioStream;
+ class Content;
+ class Player;
+ class Playlist;
+ class AudioContent;
+ class Scaler;
 +class Screen;
  
  /** @class Film
-  *  @brief A representation of a video, maybe with sound.
   *
-  *  A representation of a piece of video (maybe with sound), including naming,
-  *  the source content file, and how it should be presented in a DCP.
+  *  @brief A representation of some audio and video content, and details of
+  *  how they should be presented in a DCP.
+  *
+  *  The content of a Film is held in a Playlist (created and managed by the Film).
   */
- class Film : public boost::enable_shared_from_this<Film>
+ class Film : public boost::enable_shared_from_this<Film>, public boost::noncopyable
  {
  public:
-       Film (std::string d, bool must_exist = true);
-       Film (Film const &);
-       ~Film ();
+       Film (boost::filesystem::path);
  
-       std::string j2k_dir () const;
+       std::string info_dir () const;
+       std::string j2c_path (int, Eyes, bool) const;
+       std::string info_path (int, Eyes) const;
+       std::string internal_video_mxf_dir () const;
+       std::string internal_video_mxf_filename () const;
+       boost::filesystem::path audio_analysis_path (boost::shared_ptr<const AudioContent>) const;
  
-       void examine_content ();
-       void send_dcp_to_tms ();
+       std::string video_mxf_filename () const;
+       std::string audio_mxf_filename () const;
  
-       void make_dcp (bool);
+       void send_dcp_to_tms ();
+       void make_dcp ();
  
        /** @return Logger.
         *  It is safe to call this from any thread.
         */
-       Log* log () const {
+       boost::shared_ptr<Log> log () const {
                return _log;
        }
  
        std::string file (std::string f) const;
        std::string dir (std::string d) const;
  
-       std::string content_path () const;
-       ContentType content_type () const;
-       
-       int target_audio_sample_rate () const;
-       
-       void write_metadata () const;
        void read_metadata ();
+       void write_metadata () const;
  
-       Size cropped_size (Size) const;
-       boost::optional<int> dcp_length () const;
-       std::string dci_name () const;
-       std::string dcp_name () const;
+       std::string dci_name (bool if_created_now) const;
+       std::string dcp_name (bool if_created_now = false) const;
  
        /** @return true if our state has changed since we last saved it */
        bool dirty () const {
                return _dirty;
        }
  
-       int audio_channels () const;
+       libdcp::Size full_frame () const;
  
-       void set_dci_date_today ();
+       bool have_dcp () const;
+       boost::shared_ptr<Player> make_player () const;
+       boost::shared_ptr<Playlist> playlist () const;
+       OutputAudioFrame audio_frame_rate () const;
+       OutputAudioFrame time_to_audio_frames (Time) const;
+       OutputVideoFrame time_to_video_frames (Time) const;
+       Time video_frames_to_time (OutputVideoFrame) const;
+       Time audio_frames_to_time (OutputAudioFrame) const;
+       /* Proxies for some Playlist methods */
+       ContentList content () const;
+       Time length () const;
+       bool has_subtitles () const;
+       OutputVideoFrame best_video_frame_rate () const;
  
 +      void make_kdms (
 +              std::list<boost::shared_ptr<Screen> >,
 +              boost::posix_time::ptime from,
 +              boost::posix_time::ptime until,
 +              std::string directory
 +              ) const;
 +
        /** Identifiers for the parts of our state;
            used for signalling changes.
        */
                NONE,
                NAME,
                USE_DCI_NAME,
+               /** The playlist's content list has changed (i.e. content has been added, moved around or removed) */
                CONTENT,
-               TRUST_CONTENT_HEADER,
                DCP_CONTENT_TYPE,
-               FORMAT,
-               CROP,
-               FILTERS,
+               CONTAINER,
+               RESOLUTION,
                SCALER,
-               DCP_TRIM_START,
-               DCP_TRIM_END,
-               REEL_SIZE,
-               DCP_AB,
-               CONTENT_AUDIO_STREAM,
-               EXTERNAL_AUDIO,
-               USE_CONTENT_AUDIO,
-               AUDIO_GAIN,
-               AUDIO_DELAY,
-               STILL_DURATION,
-               SUBTITLE_STREAM,
                WITH_SUBTITLES,
-               SUBTITLE_OFFSET,
-               SUBTITLE_SCALE,
 +              ENCRYPTED,
-               COLOUR_LUT,
                J2K_BANDWIDTH,
                DCI_METADATA,
-               SIZE,
-               LENGTH,
-               CONTENT_AUDIO_STREAMS,
-               SUBTITLE_STREAMS,
-               FRAMES_PER_SECOND,
+               VIDEO_FRAME_RATE,
+               AUDIO_CHANNELS,
+               /** The setting of _three_d has been changed */
+               THREE_D,
+               SEQUENCE_VIDEO,
+               INTEROP,
        };
  
  
        /* GET */
  
        std::string directory () const {
-               boost::mutex::scoped_lock lm (_directory_mutex);
                return _directory;
        }
  
        std::string name () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _name;
        }
  
        bool use_dci_name () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _use_dci_name;
        }
  
-       std::string content () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _content;
-       }
-       bool trust_content_header () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _trust_content_header;
-       }
        DCPContentType const * dcp_content_type () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _dcp_content_type;
        }
  
-       Format const * format () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _format;
+       Ratio const * container () const {
+               return _container;
        }
  
-       Crop crop () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _crop;
-       }
-       std::vector<Filter const *> filters () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _filters;
+       Resolution resolution () const {
+               return _resolution;
        }
  
        Scaler const * scaler () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _scaler;
        }
  
-       SourceFrame dcp_trim_start () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _dcp_trim_start;
-       }
-       SourceFrame dcp_trim_end () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _dcp_trim_end;
-       }
-       boost::optional<uint64_t> reel_size () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _reel_size;
-       }
-       
-       bool dcp_ab () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _dcp_ab;
-       }
-       boost::shared_ptr<AudioStream> content_audio_stream () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _content_audio_stream;
-       }
-       std::vector<std::string> external_audio () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _external_audio;
-       }
-       bool use_content_audio () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _use_content_audio;
-       }
-       
-       float audio_gain () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _audio_gain;
-       }
-       int audio_delay () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _audio_delay;
-       }
-       int still_duration () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _still_duration;
-       }
-       boost::shared_ptr<SubtitleStream> subtitle_stream () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _subtitle_stream;
-       }
        bool with_subtitles () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _with_subtitles;
        }
  
-       int subtitle_offset () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _subtitle_offset;
-       }
-       float subtitle_scale () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _subtitle_scale;
-       }
 +      bool encrypted () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
 +              return _encrypted;
 +      }
 +
-       int colour_lut () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _colour_lut;
-       }
        int j2k_bandwidth () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
                return _j2k_bandwidth;
        }
  
-       std::string audio_language () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _audio_language;
-       }
-       
-       std::string subtitle_language () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _subtitle_language;
-       }
-       
-       std::string territory () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _territory;
-       }
-       
-       std::string rating () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _rating;
-       }
-       
-       std::string studio () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _studio;
-       }
-       
-       std::string facility () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _facility;
-       }
-       
-       std::string package_type () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _package_type;
+       DCIMetadata dci_metadata () const {
+               return _dci_metadata;
        }
  
-       Size size () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _size;
+       /** @return The frame rate of the DCP */
+       int video_frame_rate () const {
+               return _video_frame_rate;
        }
  
-       boost::optional<SourceFrame> length () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _length;
+       int audio_channels () const {
+               return _audio_channels;
        }
-       
-       std::string content_digest () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _content_digest;
+       bool three_d () const {
+               return _three_d;
        }
-       
-       std::vector<boost::shared_ptr<AudioStream> > content_audio_streams () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _content_audio_streams;
+       bool sequence_video () const {
+               return _sequence_video;
        }
  
-       std::vector<boost::shared_ptr<SubtitleStream> > subtitle_streams () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _subtitle_streams;
+       bool interop () const {
+               return _interop;
        }
        
-       float frames_per_second () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               if (content_type() == STILL) {
-                       return 24;
-               }
-               
-               return _frames_per_second;
-       }
-       boost::shared_ptr<AudioStream> audio_stream () const;
  
-       
        /* SET */
  
        void set_directory (std::string);
        void set_name (std::string);
        void set_use_dci_name (bool);
-       void set_content (std::string);
-       void set_trust_content_header (bool);
+       void examine_and_add_content (boost::shared_ptr<Content>);
+       void add_content (boost::shared_ptr<Content>);
+       void remove_content (boost::shared_ptr<Content>);
        void set_dcp_content_type (DCPContentType const *);
-       void set_format (Format const *);
-       void set_crop (Crop);
-       void set_left_crop (int);
-       void set_right_crop (int);
-       void set_top_crop (int);
-       void set_bottom_crop (int);
-       void set_filters (std::vector<Filter const *>);
+       void set_container (Ratio const *);
+       void set_resolution (Resolution);
        void set_scaler (Scaler const *);
-       void set_dcp_trim_start (int);
-       void set_dcp_trim_end (int);
-       void set_reel_size (uint64_t);
-       void unset_reel_size ();
-       void set_dcp_ab (bool);
-       void set_content_audio_stream (boost::shared_ptr<AudioStream>);
-       void set_external_audio (std::vector<std::string>);
-       void set_use_content_audio (bool);
-       void set_audio_gain (float);
-       void set_audio_delay (int);
-       void set_still_duration (int);
-       void set_subtitle_stream (boost::shared_ptr<SubtitleStream>);
        void set_with_subtitles (bool);
-       void set_subtitle_offset (int);
-       void set_subtitle_scale (float);
 +      void set_encrypted (bool);
-       void set_colour_lut (int);
        void set_j2k_bandwidth (int);
-       void set_audio_language (std::string);
-       void set_subtitle_language (std::string);
-       void set_territory (std::string);
-       void set_rating (std::string);
-       void set_studio (std::string);
-       void set_facility (std::string);
-       void set_package_type (std::string);
-       void set_size (Size);
-       void set_length (SourceFrame);
-       void unset_length ();
-       void set_content_digest (std::string);
-       void set_content_audio_streams (std::vector<boost::shared_ptr<AudioStream> >);
-       void set_subtitle_streams (std::vector<boost::shared_ptr<SubtitleStream> >);
-       void set_frames_per_second (float);
-       /** Emitted when some property has changed */
+       void set_dci_metadata (DCIMetadata);
+       void set_video_frame_rate (int);
+       void set_audio_channels (int);
+       void set_three_d (bool);
+       void set_dci_date_today ();
+       void set_sequence_video (bool);
+       void set_interop (bool);
+       /** Emitted when some property has of the Film has changed */
        mutable boost::signals2::signal<void (Property)> Changed;
  
+       /** Emitted when some property of our content has changed */
+       mutable boost::signals2::signal<void (boost::weak_ptr<Content>, int)> ContentChanged;
        /** Current version number of the state file */
        static int const state_version;
  
  private:
-       
-       /** Log to write to */
-       Log* _log;
-       /** Any running ExamineContentJob, or 0 */
-       boost::shared_ptr<ExamineContentJob> _examine_content_job;
-       /** The date that we should use in a DCI name */
-       boost::gregorian::date _dci_date;
  
        void signal_changed (Property);
-       void examine_content_finished ();
+       std::string video_identifier () const;
+       void playlist_changed ();
+       void playlist_content_changed (boost::weak_ptr<Content>, int);
+       std::string filename_safe_name () const;
+       void maybe_add_content (boost::weak_ptr<Job>, boost::weak_ptr<Content>);
+       /** Log to write to */
+       boost::shared_ptr<Log> _log;
+       boost::shared_ptr<Playlist> _playlist;
  
        /** Complete path to directory containing the film metadata;
         *  must not be relative.
         */
        std::string _directory;
-       /** Mutex for _directory */
-       mutable boost::mutex _directory_mutex;
        
-       /** Name for DVD-o-matic */
+       /** Name for DCP-o-matic */
        std::string _name;
        /** True if a auto-generated DCI-compliant name should be used for our DCP */
        bool _use_dci_name;
-       /** File or directory containing content; may be relative to our directory
-        *  or an absolute path.
-        */
-       std::string _content;
-       /** If this is true, we will believe the length specified by the content
-        *  file's header; if false, we will run through the whole content file
-        *  the first time we see it in order to obtain the length.
-        */
-       bool _trust_content_header;
        /** The type of content that this Film represents (feature, trailer etc.) */
        DCPContentType const * _dcp_content_type;
-       /** The format to present this Film in (flat, scope, etc.) */
-       Format const * _format;
-       /** The crop to apply to the source */
-       Crop _crop;
-       /** Video filters that should be used when generating DCPs */
-       std::vector<Filter const *> _filters;
+       /** The container to put this Film in (flat, scope, etc.) */
+       Ratio const * _container;
+       /** DCP resolution (2K or 4K) */
+       Resolution _resolution;
        /** Scaler algorithm to use */
        Scaler const * _scaler;
-       /** Frames to trim off the start of the DCP */
-       int _dcp_trim_start;
-       /** Frames to trim off the end of the DCP */
-       int _dcp_trim_end;
-       /** Approximate target reel size in bytes; if not set, use a single reel */
-       boost::optional<uint64_t> _reel_size;
-       /** true to create an A/B comparison DCP, where the left half of the image
-           is the video without any filters or post-processing, and the right half
-           has the specified filters and post-processing.
-       */
-       bool _dcp_ab;
-       /** The audio stream to use from our content */
-       boost::shared_ptr<AudioStream> _content_audio_stream;
-       /** List of filenames of external audio files, in channel order
-           (L, R, C, Lfe, Ls, Rs)
-       */
-       std::vector<std::string> _external_audio;
-       /** true to use audio from our content file; false to use external audio */
-       bool _use_content_audio;
-       /** Gain to apply to audio in dB */
-       float _audio_gain;
-       /** Delay to apply to audio (positive moves audio later) in milliseconds */
-       int _audio_delay;
-       /** Duration to make still-sourced films (in seconds) */
-       int _still_duration;
-       boost::shared_ptr<SubtitleStream> _subtitle_stream;
        /** True if subtitles should be shown for this film */
        bool _with_subtitles;
-       /** y offset for placing subtitles, in source pixels; +ve is further down
-           the frame, -ve is further up.
-       */
-       int _subtitle_offset;
-       /** scale factor to apply to subtitles */
-       float _subtitle_scale;
 +      bool _encrypted;
-       /** index of colour LUT to use when converting RGB to XYZ.
-        *  0: sRGB
-        *  1: Rec 709
-        */
-       int _colour_lut;
        /** bandwidth for J2K files in bits per second */
        int _j2k_bandwidth;
-       
-       /* DCI naming stuff */
-       std::string _audio_language;
-       std::string _subtitle_language;
-       std::string _territory;
-       std::string _rating;
-       std::string _studio;
-       std::string _facility;
-       std::string _package_type;
-       /* Data which are cached to speed things up */
-       /** Size, in pixels, of the source (ignoring cropping) */
-       Size _size;
-       /** The length of the source, in video frames (as far as we know) */
-       boost::optional<SourceFrame> _length;
-       /** MD5 digest of our content file */
-       std::string _content_digest;
-       /** The audio streams in our content */
-       std::vector<boost::shared_ptr<AudioStream> > _content_audio_streams;
-       /** A stream to represent possible external audio (will always exist) */
-       boost::shared_ptr<AudioStream> _external_audio_stream;
-       /** the subtitle streams that we can use */
-       std::vector<boost::shared_ptr<SubtitleStream> > _subtitle_streams;
-       /** Frames per second of the source */
-       float _frames_per_second;
+       /** DCI naming stuff */
+       DCIMetadata _dci_metadata;
+       /** Frames per second to run our DCP at */
+       int _video_frame_rate;
+       /** The date that we should use in a DCI name */
+       boost::gregorian::date _dci_date;
+       /** Number of audio channels to put in the DCP */
+       int _audio_channels;
+       /** If true, the DCP will be written in 3D mode; otherwise in 2D.
+           This will be regardless of what content is on the playlist.
+       */
+       bool _three_d;
+       bool _sequence_video;
+       bool _interop;
  
        /** true if our state has changed since we last saved it */
        mutable bool _dirty;
  
-       /** Mutex for all state except _directory */
-       mutable boost::mutex _state_mutex;
        friend class paths_test;
+       friend class film_metadata_test;
  };
  
  #endif
diff --combined src/lib/util.cc
index ef6f46575f32ada59bb6a7923d0fa56277db5cbf,4180bbfd7e5dc50dd22a4d986e8fe9c0e92b37ac..b8bc1fc9e467680646f924c2ab3385d195911efb
@@@ -26,7 -26,8 +26,8 @@@
  #include <iomanip>
  #include <iostream>
  #include <fstream>
- #ifdef DVDOMATIC_POSIX
+ #include <climits>
+ #ifdef DCPOMATIC_POSIX
  #include <execinfo.h>
  #include <cxxabi.h>
  #endif
  #include <boost/lexical_cast.hpp>
  #include <boost/thread.hpp>
  #include <boost/filesystem.hpp>
+ #include <glib.h>
  #include <openjpeg.h>
  #include <openssl/md5.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
  #include <libdcp/version.h>
 +#include <libdcp/util.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
  #include "util.h"
  #include "exceptions.h"
  #include "scaler.h"
- #include "format.h"
  #include "dcp_content_type.h"
  #include "filter.h"
  #include "sound_processor.h"
+ #include "config.h"
+ #include "ratio.h"
+ #include "job.h"
+ #ifdef DCPOMATIC_WINDOWS
+ #include "stack.hpp"
+ #endif
  
- using namespace std;
- using namespace boost;
- thread::id ui_thread;
+ #include "i18n.h"
+ using std::string;
+ using std::stringstream;
+ using std::setfill;
+ using std::ostream;
+ using std::endl;
+ using std::vector;
+ using std::hex;
+ using std::setw;
+ using std::ifstream;
+ using std::ios;
+ using std::min;
+ using std::max;
+ using std::list;
+ using std::multimap;
+ using std::istream;
+ using std::numeric_limits;
+ using std::pair;
+ using std::ofstream;
+ using boost::shared_ptr;
+ using boost::thread;
+ using boost::lexical_cast;
+ using boost::optional;
+ using libdcp::Size;
+ static boost::thread::id ui_thread;
+ static boost::filesystem::path backtrace_file;
  
  /** Convert some number of seconds to a string representation
   *  in hours, minutes and seconds.
@@@ -81,11 -111,11 +112,11 @@@ seconds_to_hms (int s
        m -= (h * 60);
  
        stringstream hms;
-       hms << h << ":";
+       hms << h << N_(":");
        hms.width (2);
-       hms << setfill ('0') << m << ":";
+       hms << std::setfill ('0') << m << N_(":");
        hms.width (2);
-       hms << setfill ('0') << s;
+       hms << std::setfill ('0') << s;
  
        return hms.str ();
  }
@@@ -105,40 -135,40 +136,40 @@@ seconds_to_approximate_hms (int s
        
        if (h > 0) {
                if (m > 30) {
-                       ap << (h + 1) << " hours";
+                       ap << (h + 1) << N_(" ") << _("hours");
                } else {
                        if (h == 1) {
-                               ap << "1 hour";
+                               ap << N_("1 ") << _("hour");
                        } else {
-                               ap << h << " hours";
+                               ap << h << N_(" ") << _("hours");
                        }
                }
        } else if (m > 0) {
                if (m == 1) {
-                       ap << "1 minute";
+                       ap << N_("1 ") << _("minute");
                } else {
-                       ap << m << " minutes";
+                       ap << m << N_(" ") << _("minutes");
                }
        } else {
-               ap << s << " seconds";
+               ap << s << N_(" ") << _("seconds");
        }
  
        return ap.str ();
  }
  
- #ifdef DVDOMATIC_POSIX
+ #ifdef DCPOMATIC_POSIX
  /** @param l Mangled C++ identifier.
   *  @return Demangled version.
   */
  static string
  demangle (string l)
  {
-       string::size_type const b = l.find_first_of ("(");
+       string::size_type const b = l.find_first_of (N_("("));
        if (b == string::npos) {
                return l;
        }
  
-       string::size_type const p = l.find_last_of ("+");
+       string::size_type const p = l.find_last_of (N_("+"));
        if (p == string::npos) {
                return l;
        }
@@@ -172,16 -202,12 +203,12 @@@ voi
  stacktrace (ostream& out, int levels)
  {
        void *array[200];
-       size_t size;
-       char **strings;
-       size_t i;
-      
-       size = backtrace (array, 200);
-       strings = backtrace_symbols (array, size);
+       size_t size = backtrace (array, 200);
+       char** strings = backtrace_symbols (array, size);
       
        if (strings) {
-               for (i = 0; i < size && (levels == 0 || i < size_t(levels)); i++) {
-                       out << "  " << demangle (strings[i]) << endl;
+               for (size_t i = 0; i < size && (levels == 0 || i < size_t(levels)); i++) {
+                       out << N_("  ") << demangle (strings[i]) << "\n";
                }
                
                free (strings);
@@@ -196,7 -222,7 +223,7 @@@ static strin
  ffmpeg_version_to_string (int v)
  {
        stringstream s;
-       s << ((v & 0xff0000) >> 16) << "." << ((v & 0xff00) >> 8) << "." << (v & 0xff);
+       s << ((v & 0xff0000) >> 16) << N_(".") << ((v & 0xff00) >> 8) << N_(".") << (v & 0xff);
        return s.str ();
  }
  
@@@ -205,16 -231,16 +232,16 @@@ strin
  dependency_version_summary ()
  {
        stringstream s;
-       s << "libopenjpeg " << opj_version () << ", "
-         << "libavcodec " << ffmpeg_version_to_string (avcodec_version()) << ", "
-         << "libavfilter " << ffmpeg_version_to_string (avfilter_version()) << ", "
-         << "libavformat " << ffmpeg_version_to_string (avformat_version()) << ", "
-         << "libavutil " << ffmpeg_version_to_string (avutil_version()) << ", "
-         << "libpostproc " << ffmpeg_version_to_string (postproc_version()) << ", "
-         << "libswscale " << ffmpeg_version_to_string (swscale_version()) << ", "
-         << MagickVersion << ", "
-         << "libssh " << ssh_version (0) << ", "
-         << "libdcp " << libdcp::version << " git " << libdcp::git_commit;
+       s << N_("libopenjpeg ") << opj_version () << N_(", ")
+         << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
+         << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
+         << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
+         << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
+         << N_("libpostproc ") << ffmpeg_version_to_string (postproc_version()) << N_(", ")
+         << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
+         << MagickVersion << N_(", ")
+         << N_("libssh ") << ssh_version (0) << N_(", ")
+         << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit;
  
        return s.str ();
  }
@@@ -225,33 -251,82 +252,82 @@@ seconds (struct timeval t
        return t.tv_sec + (double (t.tv_usec) / 1e6);
  }
  
- /** Call the required functions to set up DVD-o-matic's static arrays, etc.
+ #ifdef DCPOMATIC_WINDOWS
+ LONG WINAPI exception_handler(struct _EXCEPTION_POINTERS *)
+ {
+       dbg::stack s;
+       ofstream f (backtrace_file.string().c_str());
+       std::copy(s.begin(), s.end(), std::ostream_iterator<dbg::stack_frame>(f, "\n"));
+       return EXCEPTION_CONTINUE_SEARCH;
+ }
+ #endif
+ /** Call the required functions to set up DCP-o-matic's static arrays, etc.
   *  Must be called from the UI thread, if there is one.
   */
  void
- dvdomatic_setup ()
+ dcpomatic_setup ()
  {
-       libdcp::init ();
+ #ifdef DCPOMATIC_WINDOWS
+       backtrace_file /= g_get_user_config_dir ();
+       backtrace_file /= "backtrace.txt";
+       SetUnhandledExceptionFilter(exception_handler);
+ #endif        
+       
+       avfilter_register_all ();
        
-       Format::setup_formats ();
+       Ratio::setup_ratios ();
        DCPContentType::setup_dcp_content_types ();
        Scaler::setup_scalers ();
        Filter::setup_filters ();
        SoundProcessor::setup_sound_processors ();
  
-       ui_thread = this_thread::get_id ();
+       ui_thread = boost::this_thread::get_id ();
  }
  
- /** @param start Start position for the crop within the image.
-  *  @param size Size of the cropped area.
-  *  @return FFmpeg crop filter string.
-  */
- string
- crop_string (Position start, Size size)
+ #ifdef DCPOMATIC_WINDOWS
+ boost::filesystem::path
+ mo_path ()
  {
-       stringstream s;
-       s << "crop=" << size.width << ":" << size.height << ":" << start.x << ":" << start.y;
-       return s.str ();
+       wchar_t buffer[512];
+       GetModuleFileName (0, buffer, 512 * sizeof(wchar_t));
+       boost::filesystem::path p (buffer);
+       p = p.parent_path ();
+       p = p.parent_path ();
+       p /= "locale";
+       return p;
+ }
+ #endif
+ void
+ dcpomatic_setup_gettext_i18n (string lang)
+ {
+ #ifdef DCPOMATIC_POSIX
+       lang += ".UTF8";
+ #endif
+       if (!lang.empty ()) {
+               /* Override our environment language; this is essential on
+                  Windows.
+               */
+               char cmd[64];
+               snprintf (cmd, sizeof(cmd), "LANGUAGE=%s", lang.c_str ());
+               putenv (cmd);
+               snprintf (cmd, sizeof(cmd), "LANG=%s", lang.c_str ());
+               putenv (cmd);
+       }
+       setlocale (LC_ALL, "");
+       textdomain ("libdcpomatic");
+ #ifdef DCPOMATIC_WINDOWS
+       bindtextdomain ("libdcpomatic", mo_path().string().c_str());
+       bind_textdomain_codeset ("libdcpomatic", "UTF8");
+ #endif        
+ #ifdef DCPOMATIC_POSIX
+       bindtextdomain ("libdcpomatic", POSIX_LOCALE_PREFIX);
+ #endif
  }
  
  /** @param s A string.
@@@ -266,7 -341,7 +342,7 @@@ split_at_spaces_considering_quotes (str
        for (string::size_type i = 0; i < s.length(); ++i) {
                if (s[i] == ' ' && !in_quotes) {
                        out.push_back (c);
-                       c = "";
+                       c = N_("");
                } else if (s[i] == '"') {
                        in_quotes = !in_quotes;
                } else {
@@@ -289,7 -364,7 +365,7 @@@ md5_digest (void const * data, int size
        
        stringstream s;
        for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << hex << setfill('0') << setw(2) << ((int) digest[i]);
+               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
        }
  
        return s.str ();
   *  @return MD5 digest of file's contents.
   */
  string
- md5_digest (string file)
+ md5_digest (boost::filesystem::path file)
  {
-       ifstream f (file.c_str(), ios::binary);
+       ifstream f (file.string().c_str(), std::ios::binary);
        if (!f.good ()) {
-               throw OpenFileError (file);
+               throw OpenFileError (file.string());
        }
        
-       f.seekg (0, ios::end);
+       f.seekg (0, std::ios::end);
        int bytes = f.tellg ();
-       f.seekg (0, ios::beg);
+       f.seekg (0, std::ios::beg);
  
        int const buffer_size = 64 * 1024;
        char buffer[buffer_size];
  
        stringstream s;
        for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << hex << setfill('0') << setw(2) << ((int) digest[i]);
+               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
        }
  
        return s.str ();
  }
  
- /** @param fps Arbitrary frames-per-second value.
-  *  @return DCPFrameRate for this frames-per-second.
-  */
- DCPFrameRate
- dcp_frame_rate (float fps)
+ /** @param job Optional job for which to report progress */
+ string
+ md5_digest_directory (boost::filesystem::path directory, shared_ptr<Job> job)
  {
-       DCPFrameRate dfr;
+       int const buffer_size = 64 * 1024;
+       char buffer[buffer_size];
  
-       dfr.run_fast = (fps != rint (fps));
-       dfr.frames_per_second = rint (fps);
-       dfr.skip = 1;
+       MD5_CTX md5_context;
+       MD5_Init (&md5_context);
  
-       /* XXX: somewhat arbitrary */
-       if (fps == 50) {
-               dfr.frames_per_second = 25;
-               dfr.skip = 2;
+       int files = 0;
+       if (job) {
+               for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) {
+                       ++files;
+               }
        }
  
-       return dfr;
- }
+       int j = 0;
+       for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) {
+               ifstream f (i->path().string().c_str(), std::ios::binary);
+               if (!f.good ()) {
+                       throw OpenFileError (i->path().string());
+               }
+       
+               f.seekg (0, std::ios::end);
+               int bytes = f.tellg ();
+               f.seekg (0, std::ios::beg);
+               while (bytes > 0) {
+                       int const t = min (bytes, buffer_size);
+                       f.read (buffer, t);
+                       MD5_Update (&md5_context, buffer, t);
+                       bytes -= t;
+               }
  
- /** @param An arbitrary sampling rate.
-  *  @return The appropriate DCP-approved sampling rate (48kHz or 96kHz).
-  */
- int
- dcp_audio_sample_rate (int fs)
- {
-       if (fs <= 48000) {
-               return 48000;
+               if (job) {
+                       job->set_progress (float (j) / files);
+                       ++j;
+               }
        }
  
-       return 96000;
- }
+       unsigned char digest[MD5_DIGEST_LENGTH];
+       MD5_Final (digest, &md5_context);
  
- int
- dcp_audio_channels (int f)
- {
-       if (f == 1) {
-               /* The source is mono, so to put the mono channel into
-                  the centre we need to generate a 5.1 soundtrack.
-               */
-               return 6;
+       stringstream s;
+       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
+               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
        }
  
-       return f;
+       return s.str ();
  }
  
bool operator== (Size const & a, Size const & b)
+ static bool
about_equal (float a, float b)
  {
-       return (a.width == b.width && a.height == b.height);
- }
+       /* A film of F seconds at f FPS will be Ff frames;
+          Consider some delta FPS d, so if we run the same
+          film at (f + d) FPS it will last F(f + d) seconds.
  
- bool operator!= (Size const & a, Size const & b)
- {
-       return !(a == b);
- }
+          Hence the difference in length over the length of the film will
+          be F(f + d) - Ff frames
+           = Ff + Fd - Ff frames
+           = Fd frames
+           = Fd/f seconds
+  
+          So if we accept a difference of 1 frame, ie 1/f seconds, we can
+          say that
  
- bool operator== (Crop const & a, Crop const & b)
- {
-       return (a.left == b.left && a.right == b.right && a.top == b.top && a.bottom == b.bottom);
- }
+          1/f = Fd/f
+       ie 1 = Fd
+       ie d = 1/F
+  
+          So for a 3hr film, ie F = 3 * 60 * 60 = 10800, the acceptable
+          FPS error is 1/F ~= 0.0001 ~= 10-e4
+       */
  
- bool operator!= (Crop const & a, Crop const & b)
- {
-       return !(a == b);
+       return (fabs (a - b) < 1e-4);
  }
  
- /** @param index Colour LUT index.
-  *  @return Human-readable name.
+ /** @param An arbitrary audio frame rate.
+  *  @return The appropriate DCP-approved frame rate (48kHz or 96kHz).
   */
- string
colour_lut_index_to_name (int index)
+ int
dcp_audio_frame_rate (int fs)
  {
-       switch (index) {
-       case 0:
-               return "sRGB";
-       case 1:
-               return "Rec 709";
+       if (fs <= 48000) {
+               return 48000;
        }
  
-       assert (false);
-       return "";
+       return 96000;
  }
  
- Socket::Socket ()
+ Socket::Socket (int timeout)
        : _deadline (_io_service)
        , _socket (_io_service)
-       , _buffer_data (0)
+       , _timeout (timeout)
  {
-       _deadline.expires_at (posix_time::pos_infin);
+       _deadline.expires_at (boost::posix_time::pos_infin);
        check ();
  }
  
  void
  Socket::check ()
  {
-       if (_deadline.expires_at() <= asio::deadline_timer::traits_type::now ()) {
+       if (_deadline.expires_at() <= boost::asio::deadline_timer::traits_type::now ()) {
                _socket.close ();
-               _deadline.expires_at (posix_time::pos_infin);
+               _deadline.expires_at (boost::posix_time::pos_infin);
        }
  
        _deadline.async_wait (boost::bind (&Socket::check, this));
  }
  
- /** Blocking connect with timeout.
+ /** Blocking connect.
   *  @param endpoint End-point to connect to.
-  *  @param timeout Time-out in seconds.
   */
  void
- Socket::connect (asio::ip::basic_resolver_entry<asio::ip::tcp> const & endpoint, int timeout)
+ Socket::connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint)
  {
-       _deadline.expires_from_now (posix_time::seconds (timeout));
-       system::error_code ec = asio::error::would_block;
-       _socket.async_connect (endpoint, lambda::var(ec) = lambda::_1);
+       _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
+       boost::system::error_code ec = boost::asio::error::would_block;
+       _socket.async_connect (endpoint, boost::lambda::var(ec) = boost::lambda::_1);
        do {
                _io_service.run_one();
-       } while (ec == asio::error::would_block);
+       } while (ec == boost::asio::error::would_block);
  
        if (ec || !_socket.is_open ()) {
-               throw NetworkError ("connect timed out");
+               throw NetworkError (_("connect timed out"));
        }
  }
  
- /** Blocking write with timeout.
+ /** Blocking write.
   *  @param data Buffer to write.
   *  @param size Number of bytes to write.
-  *  @param timeout Time-out, in seconds.
   */
  void
- Socket::write (uint8_t const * data, int size, int timeout)
+ Socket::write (uint8_t const * data, int size)
  {
-       _deadline.expires_from_now (posix_time::seconds (timeout));
-       system::error_code ec = asio::error::would_block;
+       _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
+       boost::system::error_code ec = boost::asio::error::would_block;
  
-       asio::async_write (_socket, asio::buffer (data, size), lambda::var(ec) = lambda::_1);
+       boost::asio::async_write (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1);
+       
        do {
                _io_service.run_one ();
-       } while (ec == asio::error::would_block);
-       if (ec) {
-               throw NetworkError ("write timed out");
-       }
- }
- /** Blocking read with timeout.
-  *  @param data Buffer to read to.
-  *  @param size Number of bytes to read.
-  *  @param timeout Time-out, in seconds.
-  */
- int
- Socket::read (uint8_t* data, int size, int timeout)
- {
-       _deadline.expires_from_now (posix_time::seconds (timeout));
-       system::error_code ec = asio::error::would_block;
+       } while (ec == boost::asio::error::would_block);
  
-       int amount_read = 0;
-       _socket.async_read_some (
-               asio::buffer (data, size),
-               (lambda::var(ec) = lambda::_1, lambda::var(amount_read) = lambda::_2)
-               );
-       do {
-               _io_service.run_one ();
-       } while (ec == asio::error::would_block);
-       
        if (ec) {
-               amount_read = 0;
+               throw NetworkError (ec.message ());
        }
-       return amount_read;
  }
  
- /** Mark some data as being `consumed', so that it will not be returned
-  *  as data again.
-  *  @param size Amount of data to consume, in bytes.
-  */
  void
- Socket::consume (int size)
+ Socket::write (uint32_t v)
  {
-       assert (_buffer_data >= size);
-       
-       _buffer_data -= size;
-       if (_buffer_data > 0) {
-               /* Shift still-valid data to the start of the buffer */
-               memmove (_buffer, _buffer + size, _buffer_data);
-       }
+       v = htonl (v);
+       write (reinterpret_cast<uint8_t*> (&v), 4);
  }
  
- /** Read a definite amount of data from our socket, and mark
-  *  it as consumed.
-  *  @param data Where to put the data.
+ /** Blocking read.
+  *  @param data Buffer to read to.
   *  @param size Number of bytes to read.
   */
  void
- Socket::read_definite_and_consume (uint8_t* data, int size, int timeout)
- {
-       int const from_buffer = min (_buffer_data, size);
-       if (from_buffer > 0) {
-               /* Get data from our buffer */
-               memcpy (data, _buffer, from_buffer);
-               consume (from_buffer);
-               /* Update our output state */
-               data += from_buffer;
-               size -= from_buffer;
-       }
-       /* read() the rest */
-       while (size > 0) {
-               int const n = read (data, size, timeout);
-               if (n <= 0) {
-                       throw NetworkError ("could not read");
-               }
-               data += n;
-               size -= n;
-       }
- }
- /** Read as much data as is available, up to some limit.
-  *  @param data Where to put the data.
-  *  @param size Maximum amount of data to read.
-  */
- void
- Socket::read_indefinite (uint8_t* data, int size, int timeout)
+ Socket::read (uint8_t* data, int size)
  {
-       assert (size < int (sizeof (_buffer)));
+       _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
+       boost::system::error_code ec = boost::asio::error::would_block;
  
-       /* Amount of extra data we need to read () */
-       int to_read = size - _buffer_data;
-       while (to_read > 0) {
-               /* read as much of it as we can (into our buffer) */
-               int const n = read (_buffer + _buffer_data, to_read, timeout);
-               if (n <= 0) {
-                       throw NetworkError ("could not read");
-               }
+       boost::asio::async_read (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1);
  
-               to_read -= n;
-               _buffer_data += n;
+       do {
+               _io_service.run_one ();
+       } while (ec == boost::asio::error::would_block);
+       
+       if (ec) {
+               throw NetworkError (ec.message ());
        }
-       assert (_buffer_data >= size);
-       /* copy data into the output buffer */
-       assert (size >= _buffer_data);
-       memcpy (data, _buffer, size);
  }
  
- /** @param other A Rect.
-  *  @return The intersection of this with `other'.
-  */
- Rect
- Rect::intersection (Rect const & other) const
+ uint32_t
+ Socket::read_uint32 ()
  {
-       int const tx = max (x, other.x);
-       int const ty = max (y, other.y);
-       
-       return Rect (
-               tx, ty,
-               min (x + width, other.x + other.width) - tx,
-               min (y + height, other.y + other.height) - ty
-               );
+       uint32_t v;
+       read (reinterpret_cast<uint8_t *> (&v), 4);
+       return ntohl (v);
  }
  
  /** Round a number up to the nearest multiple of another number.
@@@ -611,12 -608,6 +609,6 @@@ stride_round_up (int c, int const * str
        return a - (a % t);
  }
  
- int
- stride_lookup (int c, int const * stride)
- {
-       return stride[c];
- }
  /** Read a sequence of key / value pairs from a text stream;
   *  the keys are the first words on the line, and the values are
   *  the remainder of the line following the key.  Lines beginning
@@@ -658,13 -649,13 +650,13 @@@ strin
  get_required_string (multimap<string, string> const & kv, string k)
  {
        if (kv.count (k) > 1) {
-               throw StringError ("unexpected multiple keys in key-value set");
+               throw StringError (N_("unexpected multiple keys in key-value set"));
        }
  
        multimap<string, string>::const_iterator i = kv.find (k);
        
        if (i == kv.end ()) {
-               throw StringError (String::compose ("missing key %1 in key-value set", k));
+               throw StringError (String::compose (_("missing key %1 in key-value set"), k));
        }
  
        return i->second;
@@@ -688,12 -679,12 +680,12 @@@ strin
  get_optional_string (multimap<string, string> const & kv, string k)
  {
        if (kv.count (k) > 1) {
-               throw StringError ("unexpected multiple keys in key-value set");
+               throw StringError (N_("unexpected multiple keys in key-value set"));
        }
  
        multimap<string, string>::const_iterator i = kv.find (k);
        if (i == kv.end ()) {
-               return "";
+               return N_("");
        }
  
        return i->second;
@@@ -703,7 -694,7 +695,7 @@@ in
  get_optional_int (multimap<string, string> const & kv, string k)
  {
        if (kv.count (k) > 1) {
-               throw StringError ("unexpected multiple keys in key-value set");
+               throw StringError (N_("unexpected multiple keys in key-value set"));
        }
  
        multimap<string, string>::const_iterator i = kv.find (k);
        return lexical_cast<int> (i->second);
  }
  
- /** Construct an AudioBuffers.  Audio data is undefined after this constructor.
-  *  @param channels Number of channels.
-  *  @param frames Number of frames to reserve space for.
-  */
- AudioBuffers::AudioBuffers (int channels, int frames)
-       : _channels (channels)
-       , _frames (frames)
-       , _allocated_frames (frames)
- {
-       _data = new float*[_channels];
-       for (int i = 0; i < _channels; ++i) {
-               _data[i] = new float[frames];
-       }
- }
- /** Copy constructor.
-  *  @param other Other AudioBuffers; data is copied.
-  */
- AudioBuffers::AudioBuffers (AudioBuffers const & other)
-       : _channels (other._channels)
-       , _frames (other._frames)
-       , _allocated_frames (other._frames)
- {
-       _data = new float*[_channels];
-       for (int i = 0; i < _channels; ++i) {
-               _data[i] = new float[_frames];
-               memcpy (_data[i], other._data[i], _frames * sizeof (float));
-       }
- }
- /** AudioBuffers destructor */
- AudioBuffers::~AudioBuffers ()
+ /** Trip an assert if the caller is not in the UI thread */
+ void
+ ensure_ui_thread ()
  {
-       for (int i = 0; i < _channels; ++i) {
-               delete[] _data[i];
-       }
-       delete[] _data;
+       assert (boost::this_thread::get_id() == ui_thread);
  }
  
- /** @param c Channel index.
-  *  @return Buffer for this channel.
+ /** @param v Content video frame.
+  *  @param audio_sample_rate Source audio sample rate.
+  *  @param frames_per_second Number of video frames per second.
+  *  @return Equivalent number of audio frames for `v'.
   */
- float*
- AudioBuffers::data (int c) const
+ int64_t
+ video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second)
  {
-       assert (c >= 0 && c < _channels);
-       return _data[c];
+       return ((int64_t) v * audio_sample_rate / frames_per_second);
  }
  
- /** Set the number of frames that these AudioBuffers will report themselves
-  *  as having.
-  *  @param f Frames; must be less than or equal to the number of allocated frames.
-  */
- void
- AudioBuffers::set_frames (int f)
+ string
+ audio_channel_name (int c)
  {
-       assert (f <= _allocated_frames);
-       _frames = f;
- }
+       assert (MAX_AUDIO_CHANNELS == 6);
  
- /** Make all samples on all channels silent */
- void
- AudioBuffers::make_silent ()
- {
-       for (int i = 0; i < _channels; ++i) {
-               make_silent (i);
-       }
+       /* TRANSLATORS: these are the names of audio channels; Lfe (sub) is the low-frequency
+          enhancement channel (sub-woofer)./
+       */
+       string const channels[] = {
+               _("Left"),
+               _("Right"),
+               _("Centre"),
+               _("Lfe (sub)"),
+               _("Left surround"),
+               _("Right surround"),
+       };
+       return channels[c];
  }
  
- /** Make all samples on a given channel silent.
-  *  @param c Channel.
-  */
- void
- AudioBuffers::make_silent (int c)
+ FrameRateConversion::FrameRateConversion (float source, int dcp)
+       : skip (false)
+       , repeat (false)
+       , change_speed (false)
  {
-       assert (c >= 0 && c < _channels);
-       
-       for (int i = 0; i < _frames; ++i) {
-               _data[c][i] = 0;
+       if (fabs (source / 2.0 - dcp) < (fabs (source - dcp))) {
+               skip = true;
+       } else if (fabs (source * 2 - dcp) < fabs (source - dcp)) {
+               repeat = true;
        }
- }
  
- /** Copy data from another AudioBuffers to this one.  All channels are copied.
-  *  @param from AudioBuffers to copy from; must have the same number of channels as this.
-  *  @param frames_to_copy Number of frames to copy.
-  *  @param read_offset Offset to read from in `from'.
-  *  @param write_offset Offset to write to in `to'.
-  */
- void
- AudioBuffers::copy_from (AudioBuffers* from, int frames_to_copy, int read_offset, int write_offset)
- {
-       assert (from->channels() == channels());
+       change_speed = !about_equal (source * factor(), dcp);
  
-       assert (from);
-       assert (read_offset >= 0 && (read_offset + frames_to_copy) <= from->_allocated_frames);
-       assert (write_offset >= 0 && (write_offset + frames_to_copy) <= _allocated_frames);
+       if (!skip && !repeat && !change_speed) {
+               description = _("Content and DCP have the same rate.\n");
+       } else {
+               if (skip) {
+                       description = _("DCP will use every other frame of the content.\n");
+               } else if (repeat) {
+                       description = _("Each content frame will be doubled in the DCP.\n");
+               }
  
-       for (int i = 0; i < _channels; ++i) {
-               memcpy (_data[i] + write_offset, from->_data[i] + read_offset, frames_to_copy * sizeof(float));
+               if (change_speed) {
+                       float const pc = dcp * 100 / (source * factor());
+                       description += String::compose (_("DCP will run at %1%% of the content speed.\n"), pc);
+               }
        }
  }
  
- /** Move audio data around.
-  *  @param from Offset to move from.
-  *  @param to Offset to move to.
-  *  @param frames Number of frames to move.
-  */
-     
- void
- AudioBuffers::move (int from, int to, int frames)
+ LocaleGuard::LocaleGuard ()
+       : _old (0)
  {
-       if (frames == 0) {
-               return;
-       }
-       
-       assert (from >= 0);
-       assert (from < _frames);
-       assert (to >= 0);
-       assert (to < _frames);
-       assert (frames > 0);
-       assert (frames <= _frames);
-       assert ((from + frames) <= _frames);
-       assert ((to + frames) <= _frames);
-       
-       for (int i = 0; i < _channels; ++i) {
-               memmove (_data[i] + to, _data[i] + from, frames * sizeof(float));
-       }
- }
+       char const * old = setlocale (LC_NUMERIC, 0);
  
- /** Trip an assert if the caller is not in the UI thread */
- void
- ensure_ui_thread ()
- {
-       assert (this_thread::get_id() == ui_thread);
+       if (old) {
+               _old = strdup (old);
+               if (strcmp (_old, "C")) {
+                       setlocale (LC_NUMERIC, "C");
+               }
+       }
  }
  
- /** @param v Source video frame.
-  *  @param audio_sample_rate Source audio sample rate.
-  *  @param frames_per_second Number of video frames per second.
-  *  @return Equivalent number of audio frames for `v'.
-  */
- int64_t
- video_frames_to_audio_frames (SourceFrame v, float audio_sample_rate, float frames_per_second)
+ LocaleGuard::~LocaleGuard ()
  {
-       return ((int64_t) v * audio_sample_rate / frames_per_second);
+       setlocale (LC_NUMERIC, _old);
+       free (_old);
  }
  
- /** @param f Filename.
-  *  @return true if this file is a still image, false if it is something else.
-  */
  bool
still_image_file (string f)
valid_image_file (boost::filesystem::path f)
  {
- #if BOOST_FILESYSTEM_VERSION == 3
-       string ext = boost::filesystem::path(f).extension().string();
- #else
-       string ext = boost::filesystem::path(f).extension();
- #endif
+       string ext = f.extension().string();
        transform (ext.begin(), ext.end(), ext.begin(), ::tolower);
-       
-       return (ext == ".tif" || ext == ".tiff" || ext == ".jpg" || ext == ".jpeg" || ext == ".png");
+       return (ext == ".tif" || ext == ".tiff" || ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".tga");
  }
  
- /** @return A pair containing CPU model name and the number of processors */
- pair<string, int>
- cpu_info ()
- {
-       pair<string, int> info;
-       info.second = 0;
-       
- #ifdef DVDOMATIC_POSIX
-       ifstream f ("/proc/cpuinfo");
-       while (f.good ()) {
-               string l;
-               getline (f, l);
-               if (boost::algorithm::starts_with (l, "model name")) {
-                       string::size_type const c = l.find (':');
-                       if (c != string::npos) {
-                               info.first = l.substr (c + 2);
-                       }
-               } else if (boost::algorithm::starts_with (l, "processor")) {
-                       ++info.second;
-               }
-       }
- #endif        
-       return info;
- }
diff --combined src/tools/dcpomatic.cc
index 0000000000000000000000000000000000000000,98501d3bb13d9e4f178a42d2defd02fe803e97ab..f61ef19e2408478e30a1174e00c6cf7d95ef6db4
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,596 +1,624 @@@
 -              bool const have_dcp = film && film->have_dcp();
+ /*
+     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     This program 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.
+     This program 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 this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ #include <iostream>
+ #include <fstream>
+ #include <boost/filesystem.hpp>
+ #ifdef __WXMSW__
+ #include <shellapi.h>
+ #endif
+ #ifdef __WXOSX__
+ #include <ApplicationServices/ApplicationServices.h>
+ #endif
+ #include <wx/generic/aboutdlgg.h>
+ #include <wx/stdpaths.h>
+ #include <wx/cmdline.h>
+ #include "wx/film_viewer.h"
+ #include "wx/film_editor.h"
+ #include "wx/job_manager_view.h"
+ #include "wx/config_dialog.h"
+ #include "wx/job_wrapper.h"
+ #include "wx/wx_util.h"
+ #include "wx/new_film_dialog.h"
+ #include "wx/properties_dialog.h"
+ #include "wx/wx_ui_signaller.h"
+ #include "wx/about_dialog.h"
++#include "wx/kdm_dialog.h"
+ #include "lib/film.h"
+ #include "lib/config.h"
+ #include "lib/util.h"
+ #include "lib/version.h"
+ #include "lib/ui_signaller.h"
+ #include "lib/log.h"
+ #include "lib/job_manager.h"
+ #include "lib/transcode_job.h"
++#include "lib/exceptions.h"
+ using std::cout;
+ using std::string;
+ using std::wstring;
+ using std::stringstream;
+ using std::map;
+ using std::make_pair;
+ using std::list;
+ using std::exception;
+ using std::ofstream;
+ using boost::shared_ptr;
+ using boost::dynamic_pointer_cast;
+ static FilmEditor* film_editor = 0;
+ static FilmViewer* film_viewer = 0;
+ static shared_ptr<Film> film;
+ static std::string log_level;
+ static std::string film_to_load;
+ static std::string film_to_create;
+ static wxMenu* jobs_menu = 0;
+ static void set_menu_sensitivity ();
+ class FilmChangedDialog
+ {
+ public:
+       FilmChangedDialog ()
+       {
+               _dialog = new wxMessageDialog (
+                       0,
+                       wxString::Format (_("Save changes to film \"%s\" before closing?"), std_to_wx (film->name ()).data()),
+                       _("Film changed"),
+                       wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
+                       );
+       }
+       ~FilmChangedDialog ()
+       {
+               _dialog->Destroy ();
+       }
+       int run ()
+       {
+               return _dialog->ShowModal ();
+       }
+ private:
+       /* Not defined */
+       FilmChangedDialog (FilmChangedDialog const &);
+       
+       wxMessageDialog* _dialog;
+ };
+ void
+ maybe_save_then_delete_film ()
+ {
+       if (!film) {
+               return;
+       }
+                       
+       if (film->dirty ()) {
+               FilmChangedDialog d;
+               switch (d.run ()) {
+               case wxID_NO:
+                       break;
+               case wxID_YES:
+                       film->write_metadata ();
+                       break;
+               }
+       }
+       
+       film.reset ();
+ }
+ #define ALWAYS                  0x0
+ #define NEEDS_FILM              0x1
+ #define NOT_DURING_DCP_CREATION 0x2
+ map<wxMenuItem*, int> menu_items;
+       
+ void
+ add_item (wxMenu* menu, wxString text, int id, int sens)
+ {
+       wxMenuItem* item = menu->Append (id, text);
+       menu_items.insert (make_pair (item, sens));
+ }
+ void
+ set_menu_sensitivity ()
+ {
+       list<shared_ptr<Job> > jobs = JobManager::instance()->get ();
+       list<shared_ptr<Job> >::iterator i = jobs.begin();
+       while (i != jobs.end() && dynamic_pointer_cast<TranscodeJob> (*i) == 0) {
+               ++i;
+       }
+       bool const dcp_creation = (i != jobs.end ());
+       for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
+               bool enabled = true;
+               if ((j->second & NEEDS_FILM) && film == 0) {
+                       enabled = false;
+               }
+               if ((j->second & NOT_DURING_DCP_CREATION) && dcp_creation) {
+                       enabled = false;
+               }
+               
+               j->first->Enable (enabled);
+       }
+ }
+ enum {
+       ID_file_new = 1,
+       ID_file_open,
+       ID_file_save,
+       ID_file_properties,
+       ID_jobs_make_dcp,
++      ID_jobs_make_kdms,
+       ID_jobs_send_dcp_to_tms,
+       ID_jobs_show_dcp,
+ };
+ void
+ setup_menu (wxMenuBar* m)
+ {
+       wxMenu* file = new wxMenu;
+       add_item (file, _("New..."), ID_file_new, ALWAYS);
+       add_item (file, _("&Open..."), ID_file_open, ALWAYS);
+       file->AppendSeparator ();
+       add_item (file, _("&Save"), ID_file_save, NEEDS_FILM);
+       file->AppendSeparator ();
+       add_item (file, _("&Properties..."), ID_file_properties, NEEDS_FILM);
+ #ifndef __WXOSX__     
+       file->AppendSeparator ();
+ #endif
+ #ifdef __WXOSX__      
+       add_item (file, _("&Exit"), wxID_EXIT, ALWAYS);
+ #else
+       add_item (file, _("&Quit"), wxID_EXIT, ALWAYS);
+ #endif        
+       
+ #ifdef __WXOSX__      
+       add_item (file, _("&Preferences..."), wxID_PREFERENCES, ALWAYS);
+ #else
+       wxMenu* edit = new wxMenu;
+       add_item (edit, _("&Preferences..."), wxID_PREFERENCES, ALWAYS);
+ #endif        
+       jobs_menu = new wxMenu;
+       add_item (jobs_menu, _("&Make DCP"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
++      add_item (jobs_menu, _("Make &KDMs..."), ID_jobs_make_kdms, NEEDS_FILM);
+       add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION);
+       add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
+       wxMenu* help = new wxMenu;
+ #ifdef __WXOSX__      
+       add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS);
+ #else 
+       add_item (help, _("About"), wxID_ABOUT, ALWAYS);
+ #endif        
+       m->Append (file, _("&File"));
+ #ifndef __WXOSX__     
+       m->Append (edit, _("&Edit"));
+ #endif        
+       m->Append (jobs_menu, _("&Jobs"));
+       m->Append (help, _("&Help"));
+ }
+ class Frame : public wxFrame
+ {
+ public:
+       Frame (wxString const & title)
+               : wxFrame (NULL, -1, title)
+       {
+               wxMenuBar* bar = new wxMenuBar;
+               setup_menu (bar);
+               SetMenuBar (bar);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_new, this),             ID_file_new);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_open, this),            ID_file_open);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_save, this),            ID_file_save);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_properties, this),      ID_file_properties);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_exit, this),            wxID_EXIT);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::edit_preferences, this),     wxID_PREFERENCES);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_dcp, this),        ID_jobs_make_dcp);
++              Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_kdms, this),       ID_jobs_make_kdms);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_send_dcp_to_tms, this), ID_jobs_send_dcp_to_tms);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_show_dcp, this),        ID_jobs_show_dcp);
+               Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::help_about, this),           wxID_ABOUT);
+               Bind (wxEVT_MENU_OPEN, boost::bind (&Frame::menu_opened, this, _1));
+               Bind (wxEVT_CLOSE_WINDOW, boost::bind (&Frame::close, this, _1));
+               /* Use a panel as the only child of the Frame so that we avoid
+                  the dark-grey background on Windows.
+               */
+               wxPanel* overall_panel = new wxPanel (this, wxID_ANY);
+               film_editor = new FilmEditor (film, overall_panel);
+               film_viewer = new FilmViewer (film, overall_panel);
+               JobManagerView* job_manager_view = new JobManagerView (overall_panel, static_cast<JobManagerView::Buttons> (0));
+               wxBoxSizer* right_sizer = new wxBoxSizer (wxVERTICAL);
+               right_sizer->Add (film_viewer, 2, wxEXPAND | wxALL, 6);
+               right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6);
+               wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL);
+               main_sizer->Add (film_editor, 1, wxEXPAND | wxALL, 6);
+               main_sizer->Add (right_sizer, 2, wxEXPAND | wxALL, 6);
+               set_menu_sensitivity ();
+               film_editor->FileChanged.connect (bind (&Frame::file_changed, this, _1));
+               if (film) {
+                       file_changed (film->directory ());
+               } else {
+                       file_changed ("");
+               }
+               JobManager::instance()->ActiveJobsChanged.connect (boost::bind (set_menu_sensitivity));
+               set_film ();
+               overall_panel->SetSizer (main_sizer);
+       }
+ private:
+       void menu_opened (wxMenuEvent& ev)
+       {
+               if (ev.GetMenu() != jobs_menu) {
+                       return;
+               }
++              bool const have_dcp = false;//film && film->have_dcp();
+               jobs_menu->Enable (ID_jobs_send_dcp_to_tms, have_dcp);
+               jobs_menu->Enable (ID_jobs_show_dcp, have_dcp);
+       }
+       void set_film ()
+       {
+               film_viewer->set_film (film);
+               film_editor->set_film (film);
+               set_menu_sensitivity ();
+       }
+       void file_changed (string f)
+       {
+               stringstream s;
+               s << wx_to_std (_("DCP-o-matic"));
+               if (!f.empty ()) {
+                       s << " - " << f;
+               }
+               
+               SetTitle (std_to_wx (s.str()));
+       }
+       
+       void file_new ()
+       {
+               NewFilmDialog* d = new NewFilmDialog (this);
+               int const r = d->ShowModal ();
+               
+               if (r == wxID_OK) {
+                       if (boost::filesystem::is_directory (d->get_path()) && !boost::filesystem::is_empty(d->get_path())) {
+                               if (!confirm_dialog (
+                                           this,
+                                           std_to_wx (
+                                                   String::compose (wx_to_std (_("The directory %1 already exists and is not empty.  "
+                                                                                 "Are you sure you want to use it?")),
+                                                                    d->get_path().c_str())
+                                                   )
+                                           )) {
+                                       return;
+                               }
+                       } else if (boost::filesystem::is_regular_file (d->get_path())) {
+                               error_dialog (
+                                       this,
+                                       String::compose (wx_to_std (_("%1 already exists as a file, so you cannot use it for a new film.")), d->get_path().c_str())
+                                       );
+                               return;
+                       }
+                       
+                       maybe_save_then_delete_film ();
+                       film.reset (new Film (d->get_path ()));
+                       film->write_metadata ();
+                       film->log()->set_level (log_level);
+                       film->set_name (boost::filesystem::path (d->get_path()).filename().generic_string());
+                       set_film ();
+               }
+               
+               d->Destroy ();
+       }
+       void file_open ()
+       {
+               wxDirDialog* c = new wxDirDialog (this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
+               int r;
+               while (1) {
+                       r = c->ShowModal ();
+                       if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
+                               error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
+                       } else {
+                               break;
+                       }
+               }
+                       
+               if (r == wxID_OK) {
+                       maybe_save_then_delete_film ();
+                       try {
+                               film.reset (new Film (wx_to_std (c->GetPath ())));
+                               film->read_metadata ();
+                               film->log()->set_level (log_level);
+                               set_film ();
+                       } catch (std::exception& e) {
+                               wxString p = c->GetPath ();
+                               wxCharBuffer b = p.ToUTF8 ();
+                               error_dialog (this, wxString::Format (_("Could not open film at %s (%s)"), p.data(), std_to_wx (e.what()).data()));
+                       }
+               }
+               c->Destroy ();
+       }
+       void file_save ()
+       {
+               film->write_metadata ();
+       }
+       void file_properties ()
+       {
+               PropertiesDialog* d = new PropertiesDialog (this, film);
+               d->ShowModal ();
+               d->Destroy ();
+       }
+       
+       void file_exit ()
+       {
+               if (!should_close ()) {
+                       return;
+               }
+               
+               maybe_save_then_delete_film ();
+               Close (true);
+       }
+       void edit_preferences ()
+       {
+               ConfigDialog* d = new ConfigDialog (this);
+               d->ShowModal ();
+               d->Destroy ();
+               Config::instance()->write ();
+       }
+       void jobs_make_dcp ()
+       {
+               JobWrapper::make_dcp (this, film);
+       }
++
++      void jobs_make_kdms ()
++      {
++              if (!film) {
++                      return;
++              }
++              
++              KDMDialog* d = new KDMDialog (this);
++              if (d->ShowModal () == wxID_OK) {
++                      try {
++                              film->make_kdms (
++                                      d->screens (),
++                                      d->from (),
++                                      d->until (),
++                                      d->directory ()
++                                      );
++                      } catch (KDMError& e) {
++                              error_dialog (this, e.what ());
++                      }
++              }
++              
++              d->Destroy ();
++      }
+       
+       void jobs_send_dcp_to_tms ()
+       {
+               film->send_dcp_to_tms ();
+       }
+       void jobs_show_dcp ()
+       {
+ #ifdef __WXMSW__
+               string d = film->directory();
+               wstring w;
+               w.assign (d.begin(), d.end());
+               ShellExecute (0, L"open", w.c_str(), 0, 0, SW_SHOWDEFAULT);
+ #else
+               int r = system ("which nautilus");
+               if (WEXITSTATUS (r) == 0) {
+                       r = system (string ("nautilus " + film->directory()).c_str ());
+                       if (WEXITSTATUS (r)) {
+                               error_dialog (this, _("Could not show DCP (could not run nautilus)"));
+                       }
+               } else {
+                       int r = system ("which konqueror");
+                       if (WEXITSTATUS (r) == 0) {
+                               r = system (string ("konqueror " + film->directory()).c_str ());
+                               if (WEXITSTATUS (r)) {
+                                       error_dialog (this, _("Could not show DCP (could not run konqueror)"));
+                               }
+                       }
+               }
+ #endif                
+       }
+       void help_about ()
+       {
+               AboutDialog* d = new AboutDialog (this);
+               d->ShowModal ();
+               d->Destroy ();
+       }
+       bool should_close ()
+       {
+               if (!JobManager::instance()->work_to_do ()) {
+                       return true;
+               }
+               wxMessageDialog* d = new wxMessageDialog (
+                       0,
+                       _("There are unfinished jobs; are you sure you want to quit?"),
+                       _("Unfinished jobs"),
+                       wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
+                       );
+               bool const r = d->ShowModal() == wxID_YES;
+               d->Destroy ();
+               return r;
+       }
+               
+       void close (wxCloseEvent& ev)
+       {
+               if (!should_close ()) {
+                       ev.Veto ();
+                       return;
+               }
+               ev.Skip ();
+       }       
+ };
+ #if wxMINOR_VERSION == 9
+ static const wxCmdLineEntryDesc command_line_description[] = {
+       { wxCMD_LINE_OPTION, "l", "log", "set log level (silent, verbose or timing)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
+ };
+ #else
+ static const wxCmdLineEntryDesc command_line_description[] = {
+       { wxCMD_LINE_OPTION, wxT("l"), wxT("log"), wxT("set log level (silent, verbose or timing)"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_SWITCH, wxT("n"), wxT("new"), wxT("create new film"), wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_PARAM, 0, 0, wxT("film to load or create"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
+       { wxCMD_LINE_NONE, wxT(""), wxT(""), wxT(""), wxCmdLineParamType (0), 0 }
+ };
+ #endif
+ class App : public wxApp
+ {
+       bool OnInit ()
+       try
+       {
+               if (!wxApp::OnInit()) {
+                       return false;
+               }
+               
+ #ifdef DCPOMATIC_LINUX        
+               unsetenv ("UBUNTU_MENUPROXY");
+ #endif
+ #ifdef __WXOSX__              
+               ProcessSerialNumber serial;
+               GetCurrentProcess (&serial);
+               TransformProcessType (&serial, kProcessTransformToForegroundApplication);
+ #endif                
+               wxInitAllImageHandlers ();
+               /* Enable i18n; this will create a Config object
+                  to look for a force-configured language.  This Config
+                  object will be wrong, however, because dcpomatic_setup
+                  hasn't yet been called and there aren't any scalers, filters etc.
+                  set up yet.
+               */
+               dcpomatic_setup_i18n ();
+               /* Set things up, including scalers / filters etc.
+                  which will now be internationalised correctly.
+               */
+               dcpomatic_setup ();
+               /* Force the configuration to be re-loaded correctly next
+                  time it is needed.
+               */
+               Config::drop ();
+               if (!film_to_load.empty() && boost::filesystem::is_directory (film_to_load)) {
+                       try {
+                               film.reset (new Film (film_to_load));
+                               film->read_metadata ();
+                               film->log()->set_level (log_level);
+                       } catch (exception& e) {
+                               error_dialog (0, std_to_wx (String::compose (wx_to_std (_("Could not load film %1 (%2)")), film_to_load, e.what())));
+                       }
+               }
+               if (!film_to_create.empty ()) {
+                       film.reset (new Film (film_to_create));
+                       film->write_metadata ();
+                       film->log()->set_level (log_level);
+                       film->set_name (boost::filesystem::path (film_to_create).filename().generic_string ());
+               }
+               Frame* f = new Frame (_("DCP-o-matic"));
+               SetTopWindow (f);
+               f->Maximize ();
+               f->Show ();
+               ui_signaller = new wxUISignaller (this);
+               this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
+               return true;
+       }
+       catch (exception& e)
+       {
+               error_dialog (0, wxString::Format ("DCP-o-matic could not start: %s", e.what ()));
+               return true;
+       }
+       void OnInitCmdLine (wxCmdLineParser& parser)
+       {
+               parser.SetDesc (command_line_description);
+               parser.SetSwitchChars (wxT ("-"));
+       }
+       bool OnCmdLineParsed (wxCmdLineParser& parser)
+       {
+               if (parser.GetParamCount() > 0) {
+                       if (parser.Found (wxT ("new"))) {
+                               film_to_create = wx_to_std (parser.GetParam (0));
+                       } else {
+                               film_to_load = wx_to_std (parser.GetParam(0));
+                       }
+               }
+               wxString log;
+               if (parser.Found (wxT ("log"), &log)) {
+                       log_level = wx_to_std (log);
+               }
+               return true;
+       }
+       void idle ()
+       {
+               ui_signaller->ui_idle ();
+       }
+ };
+ IMPLEMENT_APP (App)
diff --combined src/wx/cinema_dialog.cc
index 9e3b1507df7cea016bcb3f5f3f265ad6eefcaed3,0000000000000000000000000000000000000000..2c0b0b4a4f5b5fe90c7311b261301d8e403f44a6
mode 100644,000000..100644
--- /dev/null
@@@ -1,62 -1,0 +1,62 @@@
-       add_label_to_sizer (table, this, "Name");
 +/*
 +    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +
 +    This program 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.
 +
 +    This program 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 this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "cinema_dialog.h"
 +#include "wx_util.h"
 +
 +using std::string;
 +
 +CinemaDialog::CinemaDialog (wxWindow* parent, string title, string name, string email)
 +      : wxDialog (parent, wxID_ANY, std_to_wx (title))
 +{
 +      wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6);
 +      table->AddGrowableCol (1, 1);
 +
-       add_label_to_sizer (table, this, "Email address for KDM delivery");
++      add_label_to_sizer (table, this, "Name", true);
 +      _name = new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (256, -1));
 +      table->Add (_name, 1, wxEXPAND);
 +
++      add_label_to_sizer (table, this, "Email address for KDM delivery", true);
 +      _email = new wxTextCtrl (this, wxID_ANY, std_to_wx (email), wxDefaultPosition, wxSize (256, -1));
 +      table->Add (_email, 1, wxEXPAND);
 +
 +      wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
 +      overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
 +      
 +      wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
 +      if (buttons) {
 +              overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
 +      }
 +
 +      SetSizer (overall_sizer);
 +      overall_sizer->Layout ();
 +      overall_sizer->SetSizeHints (this);
 +}
 +
 +string
 +CinemaDialog::name () const
 +{
 +      return wx_to_std (_name->GetValue());
 +}
 +
 +string
 +CinemaDialog::email () const
 +{
 +      return wx_to_std (_email->GetValue());
 +}
diff --combined src/wx/film_editor.cc
index 9326227a32b3c6baf333578bad5061d95549e4a2,bcc63c735be79e4f217a5743da6007ca4bc60031..56b6973757e0f7be59a52f892060a4223b4123a9
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  #include <iomanip>
  #include <wx/wx.h>
  #include <wx/notebook.h>
+ #include <wx/listctrl.h>
  #include <boost/thread.hpp>
  #include <boost/filesystem.hpp>
  #include <boost/lexical_cast.hpp>
- #include "lib/format.h"
  #include "lib/film.h"
  #include "lib/transcode_job.h"
  #include "lib/exceptions.h"
- #include "lib/ab_transcode_job.h"
  #include "lib/job_manager.h"
  #include "lib/filter.h"
+ #include "lib/ratio.h"
  #include "lib/config.h"
- #include "lib/ffmpeg_decoder.h"
- #include "lib/external_audio_decoder.h"
- #include "filter_dialog.h"
+ #include "lib/still_image_content.h"
+ #include "lib/moving_image_content.h"
+ #include "lib/ffmpeg_content.h"
+ #include "lib/sndfile_content.h"
+ #include "lib/dcp_content_type.h"
+ #include "lib/sound_processor.h"
+ #include "lib/scaler.h"
+ #include "timecode.h"
  #include "wx_util.h"
  #include "film_editor.h"
- #include "gain_calculator_dialog.h"
- #include "sound_processor.h"
- #include "dci_name_dialog.h"
- #include "scaler.h"
+ #include "dci_metadata_dialog.h"
+ #include "timeline_dialog.h"
+ #include "timing_panel.h"
+ #include "subtitle_panel.h"
+ #include "audio_panel.h"
+ #include "video_panel.h"
  
  using std::string;
  using std::cout;
@@@ -54,533 -61,294 +61,309 @@@ using std::fixed
  using std::setprecision;
  using std::list;
  using std::vector;
+ using std::max;
  using boost::shared_ptr;
+ using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
+ using boost::lexical_cast;
  
  /** @param f Film to edit */
  FilmEditor::FilmEditor (shared_ptr<Film> f, wxWindow* parent)
        : wxPanel (parent)
-       , _film (f)
+       , _menu (f, this)
        , _generally_sensitive (true)
+       , _timeline_dialog (0)
  {
        wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       SetSizer (s);
-       _notebook = new wxNotebook (this, wxID_ANY);
-       s->Add (_notebook, 1);
-       make_film_panel ();
-       _notebook->AddPage (_film_panel, _("Film"), true);
-       make_video_panel ();
-       _notebook->AddPage (_video_panel, _("Video"), false);
-       make_audio_panel ();
-       _notebook->AddPage (_audio_panel, _("Audio"), false);
-       make_subtitle_panel ();
-       _notebook->AddPage (_subtitle_panel, _("Subtitles"), false);
-       set_film (_film);
+       _main_notebook = new wxNotebook (this, wxID_ANY);
+       s->Add (_main_notebook, 1);
+       make_content_panel ();
+       _main_notebook->AddPage (_content_panel, _("Content"), true);
+       make_dcp_panel ();
+       _main_notebook->AddPage (_dcp_panel, _("DCP"), false);
+       
+       set_film (f);
        connect_to_widgets ();
  
        JobManager::instance()->ActiveJobsChanged.connect (
                bind (&FilmEditor::active_jobs_changed, this, _1)
                );
        
-       setup_visibility ();
-       setup_formats ();
+       SetSizerAndFit (s);
  }
  
  void
- FilmEditor::make_film_panel ()
+ FilmEditor::make_dcp_panel ()
  {
-       _film_panel = new wxPanel (_notebook);
-       _film_sizer = new wxFlexGridSizer (2, 4, 4);
-       wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL);
-       pad->Add (_film_sizer, 0, wxALL, 8);
-       _film_panel->SetSizer (pad);
-       add_label_to_sizer (_film_sizer, _film_panel, "Name");
-       _name = new wxTextCtrl (_film_panel, wxID_ANY);
-       _film_sizer->Add (_name, 1, wxEXPAND);
-       add_label_to_sizer (_film_sizer, _film_panel, "DCP Name");
-       _dcp_name = new wxStaticText (_film_panel, wxID_ANY, wxT (""));
-       _film_sizer->Add (_dcp_name, 0, wxALIGN_CENTER_VERTICAL | wxSHRINK);
-       _use_dci_name = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Use DCI name"));
-       _film_sizer->Add (_use_dci_name, 1, wxEXPAND);
-       _edit_dci_button = new wxButton (_film_panel, wxID_ANY, wxT ("Details..."));
-       _film_sizer->Add (_edit_dci_button, 0);
-       add_label_to_sizer (_film_sizer, _film_panel, "Content");
-       _content = new wxFilePickerCtrl (_film_panel, wxID_ANY, wxT (""), wxT ("Select Content File"), wxT("*.*"));
-       _film_sizer->Add (_content, 1, wxEXPAND);
-       _trust_content_header = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Trust content's header"));
-       video_control (_trust_content_header);
-       _film_sizer->Add (_trust_content_header, 1);
-       _film_sizer->AddSpacer (0);
-       add_label_to_sizer (_film_sizer, _film_panel, "Content Type");
-       _dcp_content_type = new wxComboBox (_film_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
-       _film_sizer->Add (_dcp_content_type);
-       video_control (add_label_to_sizer (_film_sizer, _film_panel, "Frames Per Second"));
-       _frames_per_second = new wxStaticText (_film_panel, wxID_ANY, wxT (""));
-       _film_sizer->Add (video_control (_frames_per_second), 1, wxALIGN_CENTER_VERTICAL);
+       _dcp_panel = new wxPanel (_main_notebook);
+       _dcp_sizer = new wxBoxSizer (wxVERTICAL);
+       _dcp_panel->SetSizer (_dcp_sizer);
+       wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _dcp_sizer->Add (grid, 0, wxEXPAND | wxALL, 8);
+       int r = 0;
        
-       video_control (add_label_to_sizer (_film_sizer, _film_panel, "Original Size"));
-       _original_size = new wxStaticText (_film_panel, wxID_ANY, wxT (""));
-       _film_sizer->Add (video_control (_original_size), 1, wxALIGN_CENTER_VERTICAL);
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Name"), true, wxGBPosition (r, 0));
+       _name = new wxTextCtrl (_dcp_panel, wxID_ANY);
+       grid->Add (_name, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND | wxLEFT | wxRIGHT);
+       ++r;
        
-       video_control (add_label_to_sizer (_film_sizer, _film_panel, "Length"));
-       _length = new wxStaticText (_film_panel, wxID_ANY, wxT (""));
-       _film_sizer->Add (video_control (_length), 1, wxALIGN_CENTER_VERTICAL);
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("DCP Name"), true, wxGBPosition (r, 0));
+       _dcp_name = new wxStaticText (_dcp_panel, wxID_ANY, wxT (""));
+       grid->Add (_dcp_name, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       ++r;
+       int flags = wxALIGN_CENTER_VERTICAL;
+ #ifdef __WXOSX__
+       flags |= wxALIGN_RIGHT;
+ #endif        
+       _use_dci_name = new wxCheckBox (_dcp_panel, wxID_ANY, _("Use DCI name"));
+       grid->Add (_use_dci_name, wxGBPosition (r, 0), wxDefaultSpan, flags);
+       _edit_dci_button = new wxButton (_dcp_panel, wxID_ANY, _("Details..."));
+       grid->Add (_edit_dci_button, wxGBPosition (r, 1), wxDefaultSpan);
+       ++r;
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Container"), true, wxGBPosition (r, 0));
+       _container = new wxChoice (_dcp_panel, wxID_ANY);
+       grid->Add (_container, wxGBPosition (r, 1), wxDefaultSpan, wxEXPAND);
+       ++r;
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Content Type"), true, wxGBPosition (r, 0));
+       _dcp_content_type = new wxChoice (_dcp_panel, wxID_ANY);
+       grid->Add (_dcp_content_type, wxGBPosition (r, 1));
+       ++r;
  
        {
-               video_control (add_label_to_sizer (_film_sizer, _film_panel, "Trim frames"));
+               add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Frame Rate"), true, wxGBPosition (r, 0));
                wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               video_control (add_label_to_sizer (s, _film_panel, "Start"));
-               _dcp_trim_start = new wxSpinCtrl (_film_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (video_control (_dcp_trim_start));
-               video_control (add_label_to_sizer (s, _film_panel, "End"));
-               _dcp_trim_end = new wxSpinCtrl (_film_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (video_control (_dcp_trim_end));
-               _film_sizer->Add (s);
+               _frame_rate = new wxChoice (_dcp_panel, wxID_ANY);
+               s->Add (_frame_rate, 1, wxALIGN_CENTER_VERTICAL);
+               _best_frame_rate = new wxButton (_dcp_panel, wxID_ANY, _("Use best"));
+               s->Add (_best_frame_rate, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND);
+               grid->Add (s, wxGBPosition (r, 1));
        }
+       ++r;
  
-       _encrypted = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Encrypted"));
-       _film_sizer->Add (_encrypted, 1);
-       _film_sizer->AddSpacer (0);
++      _encrypted = new wxCheckBox (_dcp_panel, wxID_ANY, wxT ("Encrypted"));
++      grid->Add (_encrypted, wxGBPosition (r, 0), wxGBSpan (1, 2));
++      ++r;
 +
-       _multiple_reels = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Make multiple reels"));
-       _film_sizer->Add (_multiple_reels);
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Audio channels"), true, wxGBPosition (r, 0));
+       _audio_channels = new wxSpinCtrl (_dcp_panel, wxID_ANY);
+       grid->Add (_audio_channels, wxGBPosition (r, 1));
+       ++r;
  
-       {
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _reel_size = new wxSpinCtrl (_film_panel, wxID_ANY);
-               s->Add (_reel_size);
-               add_label_to_sizer (s, _film_panel, "Gb each");
-               _film_sizer->Add (s);
-       }
+       _three_d = new wxCheckBox (_dcp_panel, wxID_ANY, _("3D"));
+       grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2));
+       ++r;
  
-       _dcp_ab = new wxCheckBox (_film_panel, wxID_ANY, wxT ("A/B"));
-       video_control (_dcp_ab);
-       _film_sizer->Add (_dcp_ab, 1);
-       _film_sizer->AddSpacer (0);
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Resolution"), true, wxGBPosition (r, 0));
+       _resolution = new wxChoice (_dcp_panel, wxID_ANY);
+       grid->Add (_resolution, wxGBPosition (r, 1));
+       ++r;
  
-       /* STILL-only stuff */
        {
-               still_control (add_label_to_sizer (_film_sizer, _film_panel, "Duration"));
+               add_label_to_grid_bag_sizer (grid, _dcp_panel, _("JPEG2000 bandwidth"), true, wxGBPosition (r, 0));
                wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _still_duration = new wxSpinCtrl (_film_panel);
-               still_control (_still_duration);
-               s->Add (_still_duration, 1, wxEXPAND);
-               still_control (add_label_to_sizer (s, _film_panel, "s"));
-               _film_sizer->Add (s);
-       }
-       vector<DCPContentType const *> const ct = DCPContentType::all ();
-       for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
-               _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
-       }
-       _reel_size->SetRange(1, 1000);
- }
- void
- FilmEditor::connect_to_widgets ()
- {
-       _name->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (FilmEditor::name_changed), 0, this);
-       _use_dci_name->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::use_dci_name_toggled), 0, this);
-       _edit_dci_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::edit_dci_button_clicked), 0, this);
-       _format->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::format_changed), 0, this);
-       _content->Connect (wxID_ANY, wxEVT_COMMAND_FILEPICKER_CHANGED, wxCommandEventHandler (FilmEditor::content_changed), 0, this);
-       _trust_content_header->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::trust_content_header_changed), 0, this);
-       _left_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::left_crop_changed), 0, this);
-       _right_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::right_crop_changed), 0, this);
-       _top_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::top_crop_changed), 0, this);
-       _bottom_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::bottom_crop_changed), 0, this);
-       _filters_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::edit_filters_clicked), 0, this);
-       _scaler->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::scaler_changed), 0, this);
-       _dcp_content_type->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::dcp_content_type_changed), 0, this);
-       _dcp_ab->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::dcp_ab_toggled), 0, this);
-       _encrypted->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::encrypted_toggled), 0, this);
-       _still_duration->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::still_duration_changed), 0, this);
-       _dcp_trim_start->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::dcp_trim_start_changed), 0, this);
-       _dcp_trim_end->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::dcp_trim_end_changed), 0, this);
-       _multiple_reels->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::multiple_reels_toggled), 0, this);
-       _reel_size->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::reel_size_changed), 0, this);
-       _with_subtitles->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::with_subtitles_toggled), 0, this);
-       _subtitle_offset->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::subtitle_offset_changed), 0, this);
-       _subtitle_scale->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::subtitle_scale_changed), 0, this);
-       _colour_lut->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::colour_lut_changed), 0, this);
-       _j2k_bandwidth->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::j2k_bandwidth_changed), 0, this);
-       _subtitle_stream->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::subtitle_stream_changed), 0, this);
-       _audio_stream->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::audio_stream_changed), 0, this);
-       _audio_gain->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::audio_gain_changed), 0, this);
-       _audio_gain_calculate_button->Connect (
-               wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::audio_gain_calculate_button_clicked), 0, this
-               );
-       _audio_delay->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::audio_delay_changed), 0, this);
-       _use_content_audio->Connect (wxID_ANY, wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler (FilmEditor::use_audio_changed), 0, this);
-       _use_external_audio->Connect (wxID_ANY, wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler (FilmEditor::use_audio_changed), 0, this);
-       for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
-               _external_audio[i]->Connect (
-                       wxID_ANY, wxEVT_COMMAND_FILEPICKER_CHANGED, wxCommandEventHandler (FilmEditor::external_audio_changed), 0, this
-                       );
-       }
- }
- void
- FilmEditor::make_video_panel ()
- {
-       _video_panel = new wxPanel (_notebook);
-       _video_sizer = new wxFlexGridSizer (2, 4, 4);
-       wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL);
-       pad->Add (_video_sizer, 0, wxALL, 8);
-       _video_panel->SetSizer (pad);
-       add_label_to_sizer (_video_sizer, _video_panel, "Format");
-       _format = new wxComboBox (_video_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
-       _video_sizer->Add (_format);
-       {
-               add_label_to_sizer (_video_sizer, _video_panel, "Crop");
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               add_label_to_sizer (s, _video_panel, "L");
-               _left_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (_left_crop, 0);
-               add_label_to_sizer (s, _video_panel, "R");
-               _right_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (_right_crop, 0);
-               add_label_to_sizer (s, _video_panel, "T");
-               _top_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (_top_crop, 0);
-               add_label_to_sizer (s, _video_panel, "B");
-               _bottom_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1));
-               s->Add (_bottom_crop, 0);
-               _video_sizer->Add (s);
+               _j2k_bandwidth = new wxSpinCtrl (_dcp_panel, wxID_ANY);
+               s->Add (_j2k_bandwidth, 1);
+               add_label_to_sizer (s, _dcp_panel, _("MBps"), false);
+               grid->Add (s, wxGBPosition (r, 1));
        }
+       ++r;
  
-       /* VIDEO-only stuff */
-       {
-               video_control (add_label_to_sizer (_video_sizer, _video_panel, "Filters"));
-               wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _filters = new wxStaticText (_video_panel, wxID_ANY, wxT ("None"));
-               video_control (_filters);
-               s->Add (_filters, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 6);
-               _filters_button = new wxButton (_video_panel, wxID_ANY, wxT ("Edit..."));
-               video_control (_filters_button);
-               s->Add (_filters_button, 0);
-               _video_sizer->Add (s, 1);
-       }
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Standard"), true, wxGBPosition (r, 0));
+       _standard = new wxChoice (_dcp_panel, wxID_ANY);
+       grid->Add (_standard, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       ++r;
  
-       video_control (add_label_to_sizer (_video_sizer, _video_panel, "Scaler"));
-       _scaler = new wxComboBox (_video_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
-       _video_sizer->Add (video_control (_scaler), 1);
+       add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Scaler"), true, wxGBPosition (r, 0));
+       _scaler = new wxChoice (_dcp_panel, wxID_ANY);
+       grid->Add (_scaler, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       ++r;
  
        vector<Scaler const *> const sc = Scaler::all ();
        for (vector<Scaler const *>::const_iterator i = sc.begin(); i != sc.end(); ++i) {
                _scaler->Append (std_to_wx ((*i)->name()));
        }
  
-       add_label_to_sizer (_video_sizer, _video_panel, "Colour look-up table");
-       _colour_lut = new wxComboBox (_video_panel, wxID_ANY);
-       for (int i = 0; i < 2; ++i) {
-               _colour_lut->Append (std_to_wx (colour_lut_index_to_name (i)));
-       }
-       _colour_lut->SetSelection (0);
-       _video_sizer->Add (_colour_lut, 1, wxEXPAND);
-       {
-               add_label_to_sizer (_video_sizer, _video_panel, "JPEG2000 bandwidth");
-               wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _j2k_bandwidth = new wxSpinCtrl (_video_panel, wxID_ANY);
-               s->Add (_j2k_bandwidth, 1);
-               add_label_to_sizer (s, _video_panel, "MBps");
-               _video_sizer->Add (s, 1);
+       vector<Ratio const *> const ratio = Ratio::all ();
+       for (vector<Ratio const *>::const_iterator i = ratio.begin(); i != ratio.end(); ++i) {
+               _container->Append (std_to_wx ((*i)->nickname ()));
        }
  
-       _left_crop->SetRange (0, 1024);
-       _top_crop->SetRange (0, 1024);
-       _right_crop->SetRange (0, 1024);
-       _bottom_crop->SetRange (0, 1024);
-       _still_duration->SetRange (1, 60 * 60);
-       _dcp_trim_start->SetRange (0, 100);
-       _dcp_trim_end->SetRange (0, 100);
-       _j2k_bandwidth->SetRange (50, 250);
- }
- void
- FilmEditor::make_audio_panel ()
- {
-       _audio_panel = new wxPanel (_notebook);
-       _audio_sizer = new wxFlexGridSizer (2, 4, 4);
-       wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL);
-       pad->Add (_audio_sizer, 0, wxALL, 8);
-       _audio_panel->SetSizer (pad);
-       {
-               video_control (add_label_to_sizer (_audio_sizer, _audio_panel, "Audio Gain"));
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _audio_gain = new wxSpinCtrl (_audio_panel);
-               s->Add (video_control (_audio_gain), 1);
-               video_control (add_label_to_sizer (s, _audio_panel, "dB"));
-               _audio_gain_calculate_button = new wxButton (_audio_panel, wxID_ANY, _("Calculate..."));
-               video_control (_audio_gain_calculate_button);
-               s->Add (_audio_gain_calculate_button, 1, wxEXPAND);
-               _audio_sizer->Add (s);
+       vector<DCPContentType const *> const ct = DCPContentType::all ();
+       for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
+               _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
        }
  
-       {
-               video_control (add_label_to_sizer (_audio_sizer, _audio_panel, "Audio Delay"));
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _audio_delay = new wxSpinCtrl (_audio_panel);
-               s->Add (video_control (_audio_delay), 1);
-               video_control (add_label_to_sizer (s, _audio_panel, "ms"));
-               _audio_sizer->Add (s);
+       list<int> const dfr = Config::instance()->allowed_dcp_frame_rates ();
+       for (list<int>::const_iterator i = dfr.begin(); i != dfr.end(); ++i) {
+               _frame_rate->Append (std_to_wx (boost::lexical_cast<string> (*i)));
        }
  
-       {
-               _use_content_audio = new wxRadioButton (_audio_panel, wxID_ANY, _("Use content's audio"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
-               _audio_sizer->Add (video_control (_use_content_audio));
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _audio_stream = new wxComboBox (_audio_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
-               s->Add (video_control (_audio_stream), 1);
-               _audio = new wxStaticText (_audio_panel, wxID_ANY, wxT (""));
-               s->Add (video_control (_audio), 1, wxALIGN_CENTER_VERTICAL | wxLEFT, 8);
-               _audio_sizer->Add (s, 1, wxEXPAND);
-       }
+       _audio_channels->SetRange (0, MAX_AUDIO_CHANNELS);
+       _j2k_bandwidth->SetRange (1, 250);
  
-       _use_external_audio = new wxRadioButton (_audio_panel, wxID_ANY, _("Use external audio"));
-       _audio_sizer->Add (_use_external_audio);
-       _audio_sizer->AddSpacer (0);
-       assert (MAX_AUDIO_CHANNELS == 6);
-       char const * channels[] = {
-               "Left",
-               "Right",
-               "Centre",
-               "Lfe (sub)",
-               "Left surround",
-               "Right surround"
-       };
-       for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
-               add_label_to_sizer (_audio_sizer, _audio_panel, channels[i]);
-               _external_audio[i] = new wxFilePickerCtrl (_audio_panel, wxID_ANY, wxT (""), wxT ("Select Audio File"), wxT ("*.wav"));
-               _audio_sizer->Add (_external_audio[i], 1, wxEXPAND);
-       }
+       _resolution->Append (_("2K"));
+       _resolution->Append (_("4K"));
  
-       _audio_gain->SetRange (-60, 60);
-       _audio_delay->SetRange (-1000, 1000);
+       _standard->Append (_("SMPTE"));
+       _standard->Append (_("Interop"));
  }
  
  void
- FilmEditor::make_subtitle_panel ()
+ FilmEditor::connect_to_widgets ()
  {
-       _subtitle_panel = new wxPanel (_notebook);
-       _subtitle_sizer = new wxFlexGridSizer (2, 4, 4);
-       wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL);
-       pad->Add (_subtitle_sizer, 0, wxALL, 8);
-       _subtitle_panel->SetSizer (pad);
-       _with_subtitles = new wxCheckBox (_subtitle_panel, wxID_ANY, wxT("With Subtitles"));
-       video_control (_with_subtitles);
-       _subtitle_sizer->Add (_with_subtitles, 1);
-       
-       _subtitle_stream = new wxComboBox (_subtitle_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
-       _subtitle_sizer->Add (video_control (_subtitle_stream));
-       video_control (add_label_to_sizer (_subtitle_sizer, _subtitle_panel, "Subtitle Offset"));
-       _subtitle_offset = new wxSpinCtrl (_subtitle_panel);
-       _subtitle_sizer->Add (video_control (_subtitle_offset), 1);
+       _name->Bind             (wxEVT_COMMAND_TEXT_UPDATED,          boost::bind (&FilmEditor::name_changed, this));
+       _use_dci_name->Bind     (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::use_dci_name_toggled, this));
+       _edit_dci_button->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::edit_dci_button_clicked, this));
+       _container->Bind        (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::container_changed, this));
+       _content->Bind          (wxEVT_COMMAND_LIST_ITEM_SELECTED,    boost::bind (&FilmEditor::content_selection_changed, this));
+       _content->Bind          (wxEVT_COMMAND_LIST_ITEM_DESELECTED,  boost::bind (&FilmEditor::content_selection_changed, this));
+       _content->Bind          (wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, boost::bind (&FilmEditor::content_right_click, this, _1));
+       _content_add_file->Bind (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_add_file_clicked, this));
+       _content_add_folder->Bind (wxEVT_COMMAND_BUTTON_CLICKED,      boost::bind (&FilmEditor::content_add_folder_clicked, this));
+       _content_remove->Bind   (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_remove_clicked, this));
+       _content_timeline->Bind (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_timeline_clicked, this));
+       _scaler->Bind           (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::scaler_changed, this));
+       _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::dcp_content_type_changed, this));
+       _frame_rate->Bind       (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::frame_rate_changed, this));
+       _best_frame_rate->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::best_frame_rate_clicked, this));
+       _audio_channels->Bind   (wxEVT_COMMAND_SPINCTRL_UPDATED,      boost::bind (&FilmEditor::audio_channels_changed, this));
+       _j2k_bandwidth->Bind    (wxEVT_COMMAND_SPINCTRL_UPDATED,      boost::bind (&FilmEditor::j2k_bandwidth_changed, this));
+       _resolution->Bind       (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::resolution_changed, this));
+       _sequence_video->Bind   (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::sequence_video_changed, this));
+       _three_d->Bind          (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::three_d_changed, this));
+       _standard->Bind         (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::standard_changed, this));
+ }
+ void
+ FilmEditor::make_content_panel ()
+ {
+       _content_panel = new wxPanel (_main_notebook);
+       _content_sizer = new wxBoxSizer (wxVERTICAL);
+       _content_panel->SetSizer (_content_sizer);
  
        {
-               video_control (add_label_to_sizer (_subtitle_sizer, _subtitle_panel, "Subtitle Scale"));
                wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _subtitle_scale = new wxSpinCtrl (_subtitle_panel);
-               s->Add (video_control (_subtitle_scale));
-               video_control (add_label_to_sizer (s, _subtitle_panel, "%"));
-               _subtitle_sizer->Add (s);
-       }
-       _subtitle_offset->SetRange (-1024, 1024);
-       _subtitle_scale->SetRange (1, 1000);
- }
- /** Called when the left crop widget has been changed */
- void
- FilmEditor::left_crop_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
-       _film->set_left_crop (_left_crop->GetValue ());
- }
- /** Called when the right crop widget has been changed */
- void
- FilmEditor::right_crop_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
-       _film->set_right_crop (_right_crop->GetValue ());
- }
- /** Called when the top crop widget has been changed */
- void
- FilmEditor::top_crop_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
+               
+               _content = new wxListCtrl (_content_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER | wxLC_SINGLE_SEL);
+               s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
  
-       _film->set_top_crop (_top_crop->GetValue ());
- }
+               _content->InsertColumn (0, wxT(""));
+               _content->SetColumnWidth (0, 512);
  
- /** Called when the bottom crop value has been changed */
- void
- FilmEditor::bottom_crop_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
+               wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
+               _content_add_file = new wxButton (_content_panel, wxID_ANY, _("Add file(s)..."));
+               b->Add (_content_add_file, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               _content_add_folder = new wxButton (_content_panel, wxID_ANY, _("Add folder..."));
+               b->Add (_content_add_folder, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               _content_remove = new wxButton (_content_panel, wxID_ANY, _("Remove"));
+               b->Add (_content_remove, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               _content_timeline = new wxButton (_content_panel, wxID_ANY, _("Timeline..."));
+               b->Add (_content_timeline, 1, wxEXPAND | wxLEFT | wxRIGHT);
  
-       _film->set_bottom_crop (_bottom_crop->GetValue ());
- }
+               s->Add (b, 0, wxALL, 4);
  
- /** Called when the content filename has been changed */
- void
- FilmEditor::content_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
+               _content_sizer->Add (s, 0.75, wxEXPAND | wxALL, 6);
        }
  
-       try {
-               _film->set_content (wx_to_std (_content->GetPath ()));
-       } catch (std::exception& e) {
-               _content->SetPath (std_to_wx (_film->directory ()));
-               error_dialog (this, String::compose ("Could not set content: %1", e.what ()));
-       }
- }
+       _sequence_video = new wxCheckBox (_content_panel, wxID_ANY, _("Keep video in sequence"));
+       _content_sizer->Add (_sequence_video);
  
- void
- FilmEditor::trust_content_header_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
+       _content_notebook = new wxNotebook (_content_panel, wxID_ANY);
+       _content_sizer->Add (_content_notebook, 1, wxEXPAND | wxTOP, 6);
  
-       _film->set_trust_content_header (_trust_content_header->GetValue ());
- }
- void
- FilmEditor::multiple_reels_toggled (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
-       if (_multiple_reels->GetValue()) {
-               _film->set_reel_size (_reel_size->GetValue() * 1e9);
-       } else {
-               _film->unset_reel_size ();
-       }
-       setup_reel_control_sensitivity ();
+       _video_panel = new VideoPanel (this);
+       _panels.push_back (_video_panel);
+       _audio_panel = new AudioPanel (this);
+       _panels.push_back (_audio_panel);
+       _subtitle_panel = new SubtitlePanel (this);
+       _panels.push_back (_subtitle_panel);
+       _timing_panel = new TimingPanel (this);
+       _panels.push_back (_timing_panel);
  }
  
+ /** Called when the name widget has been changed */
  void
- FilmEditor::reel_size_changed (wxCommandEvent &)
+ FilmEditor::name_changed ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_reel_size (static_cast<uint64_t> (_reel_size->GetValue()) * 1e9);
+       _film->set_name (string (_name->GetValue().mb_str()));
  }
  
- /** Called when the DCP A/B switch has been toggled */
  void
- FilmEditor::dcp_ab_toggled (wxCommandEvent &)
+ FilmEditor::j2k_bandwidth_changed ()
  {
        if (!_film) {
                return;
        }
        
-       _film->set_dcp_ab (_dcp_ab->GetValue ());
+       _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1e6);
  }
  
- FilmEditor::encrypted_toggled (wxCommandEvent &)
 +void
++FilmEditor::encrypted_toggled ()
 +{
 +      if (!_film) {
 +              return;
 +      }
 +
 +      _film->set_encrypted (_encrypted->GetValue ());
 +}
 +                             
 +/** Called when the name widget has been changed */
  void
- FilmEditor::name_changed (wxCommandEvent &)
+ FilmEditor::frame_rate_changed ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_name (string (_name->GetValue().mb_str()));
+       _film->set_video_frame_rate (
+               boost::lexical_cast<int> (
+                       wx_to_std (_frame_rate->GetString (_frame_rate->GetSelection ()))
+                       )
+               );
  }
  
  void
- FilmEditor::subtitle_offset_changed (wxCommandEvent &)
+ FilmEditor::audio_channels_changed ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_subtitle_offset (_subtitle_offset->GetValue ());
+       _film->set_audio_channels (_audio_channels->GetValue ());
  }
  
  void
- FilmEditor::subtitle_scale_changed (wxCommandEvent &)
+ FilmEditor::resolution_changed ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_subtitle_scale (_subtitle_scale->GetValue() / 100.0);
- }
- void
- FilmEditor::colour_lut_changed (wxCommandEvent &)
- {
-       if (!_film) {
-               return;
-       }
-       
-       _film->set_colour_lut (_colour_lut->GetSelection ());
+       _film->set_resolution (_resolution->GetSelection() == 0 ? RESOLUTION_2K : RESOLUTION_4K);
  }
  
  void
- FilmEditor::j2k_bandwidth_changed (wxCommandEvent &)
+ FilmEditor::standard_changed ()
  {
        if (!_film) {
                return;
        }
-       
-       _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1e6);
- }     
  
+       _film->set_interop (_standard->GetSelection() == 1);
+ }
  
  /** Called when the metadata stored in the Film object has changed;
   *  so that we can update the GUI.
@@@ -596,201 -364,148 +379,151 @@@ FilmEditor::film_changed (Film::Propert
        }
  
        stringstream s;
+       for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
+               (*i)->film_changed (p);
+       }
                
        switch (p) {
        case Film::NONE:
                break;
        case Film::CONTENT:
-               checked_set (_content, _film->content ());
-               setup_visibility ();
-               setup_formats ();
-               setup_subtitle_control_sensitivity ();
-               setup_streams ();
+               setup_content ();
                break;
-       case Film::TRUST_CONTENT_HEADER:
-               checked_set (_trust_content_header, _film->trust_content_header ());
+       case Film::CONTAINER:
+               setup_container ();
                break;
-       case Film::SUBTITLE_STREAMS:
-               setup_subtitle_control_sensitivity ();
-               setup_streams ();
-               break;
-       case Film::CONTENT_AUDIO_STREAMS:
-               setup_streams ();
-               break;
-       case Film::FORMAT:
-       {
-               int n = 0;
-               vector<Format const *>::iterator i = _formats.begin ();
-               while (i != _formats.end() && *i != _film->format ()) {
-                       ++i;
-                       ++n;
-               }
-               if (i == _formats.end()) {
-                       checked_set (_format, -1);
-               } else {
-                       checked_set (_format, n);
-               }
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               break;
-       }
-       case Film::CROP:
-               checked_set (_left_crop, _film->crop().left);
-               checked_set (_right_crop, _film->crop().right);
-               checked_set (_top_crop, _film->crop().top);
-               checked_set (_bottom_crop, _film->crop().bottom);
-               break;
-       case Film::FILTERS:
-       {
-               pair<string, string> p = Filter::ffmpeg_strings (_film->filters ());
-               if (p.first.empty () && p.second.empty ()) {
-                       _filters->SetLabel (_("None"));
-               } else {
-                       string const b = p.first + " " + p.second;
-                       _filters->SetLabel (std_to_wx (b));
-               }
-               _film_sizer->Layout ();
-               break;
-       }
        case Film::NAME:
                checked_set (_name, _film->name());
-               _film->set_dci_date_today ();
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               break;
-       case Film::FRAMES_PER_SECOND:
-               s << fixed << setprecision(2) << _film->frames_per_second();
-               _frames_per_second->SetLabel (std_to_wx (s.str ()));
+               setup_dcp_name ();
                break;
-       case Film::SIZE:
-               if (_film->size().width == 0 && _film->size().height == 0) {
-                       _original_size->SetLabel (wxT (""));
-               } else {
-                       s << _film->size().width << " x " << _film->size().height;
-                       _original_size->SetLabel (std_to_wx (s.str ()));
-               }
-               break;
-       case Film::LENGTH:
-               if (_film->frames_per_second() > 0 && _film->length()) {
-                       s << _film->length().get() << " frames; " << seconds_to_hms (_film->length().get() / _film->frames_per_second());
-               } else if (_film->length()) {
-                       s << _film->length().get() << " frames";
-               } 
-               _length->SetLabel (std_to_wx (s.str ()));
-               if (_film->length()) {
-                       _dcp_trim_start->SetRange (0, _film->length().get());
-                       _dcp_trim_end->SetRange (0, _film->length().get());
-               }
+       case Film::WITH_SUBTITLES:
+               setup_dcp_name ();
                break;
        case Film::DCP_CONTENT_TYPE:
                checked_set (_dcp_content_type, DCPContentType::as_index (_film->dcp_content_type ()));
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               break;
-       case Film::DCP_AB:
-               checked_set (_dcp_ab, _film->dcp_ab ());
+               setup_dcp_name ();
                break;
        case Film::SCALER:
                checked_set (_scaler, Scaler::as_index (_film->scaler ()));
                break;
-       case Film::DCP_TRIM_START:
-               checked_set (_dcp_trim_start, _film->dcp_trim_start());
-               break;
-       case Film::DCP_TRIM_END:
-               checked_set (_dcp_trim_end, _film->dcp_trim_end());
-               break;
-       case Film::REEL_SIZE:
-               if (_film->reel_size()) {
-                       checked_set (_multiple_reels, true);
-                       checked_set (_reel_size, _film->reel_size().get() / 1e9);
-               } else {
-                       checked_set (_multiple_reels, false);
-               }
-               setup_reel_control_sensitivity ();
-               break;
-       case Film::AUDIO_GAIN:
-               checked_set (_audio_gain, _film->audio_gain ());
-               break;
-       case Film::AUDIO_DELAY:
-               checked_set (_audio_delay, _film->audio_delay ());
-               break;
-       case Film::STILL_DURATION:
-               checked_set (_still_duration, _film->still_duration ());
-               break;
-       case Film::WITH_SUBTITLES:
-               checked_set (_with_subtitles, _film->with_subtitles ());
-               setup_subtitle_control_sensitivity ();
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               break;
-       case Film::SUBTITLE_OFFSET:
-               checked_set (_subtitle_offset, _film->subtitle_offset ());
-               break;
-       case Film::SUBTITLE_SCALE:
-               checked_set (_subtitle_scale, _film->subtitle_scale() * 100);
-               break;
 +      case Film::ENCRYPTED:
 +              checked_set (_encrypted, _film->encrypted ());
 +              break;
-       case Film::COLOUR_LUT:
-               checked_set (_colour_lut, _film->colour_lut ());
+       case Film::RESOLUTION:
+               checked_set (_resolution, _film->resolution() == RESOLUTION_2K ? 0 : 1);
+               setup_dcp_name ();
                break;
        case Film::J2K_BANDWIDTH:
                checked_set (_j2k_bandwidth, double (_film->j2k_bandwidth()) / 1e6);
                break;
        case Film::USE_DCI_NAME:
                checked_set (_use_dci_name, _film->use_dci_name ());
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
+               setup_dcp_name ();
                break;
        case Film::DCI_METADATA:
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
+               setup_dcp_name ();
                break;
-       case Film::CONTENT_AUDIO_STREAM:
-               if (_film->content_audio_stream()) {
-                       checked_set (_audio_stream, _film->content_audio_stream()->to_string());
+       case Film::VIDEO_FRAME_RATE:
+       {
+               bool done = false;
+               for (unsigned int i = 0; i < _frame_rate->GetCount(); ++i) {
+                       if (wx_to_std (_frame_rate->GetString(i)) == boost::lexical_cast<string> (_film->video_frame_rate())) {
+                               checked_set (_frame_rate, i);
+                               done = true;
+                               break;
+                       }
+               }
+               if (!done) {
+                       checked_set (_frame_rate, -1);
                }
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               setup_audio_details ();
-               setup_audio_control_sensitivity ();
+               _best_frame_rate->Enable (_film->best_video_frame_rate () != _film->video_frame_rate ());
                break;
-       case Film::USE_CONTENT_AUDIO:
-               checked_set (_use_content_audio, _film->use_content_audio());
-               checked_set (_use_external_audio, !_film->use_content_audio());
-               _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
-               setup_audio_details ();
-               setup_audio_control_sensitivity ();
+       }
+       case Film::AUDIO_CHANNELS:
+               _audio_channels->SetValue (_film->audio_channels ());
+               setup_dcp_name ();
                break;
-       case Film::SUBTITLE_STREAM:
-               if (_film->subtitle_stream()) {
-                       checked_set (_subtitle_stream, _film->subtitle_stream()->to_string());
-               }
+       case Film::SEQUENCE_VIDEO:
+               checked_set (_sequence_video, _film->sequence_video ());
                break;
-       case Film::EXTERNAL_AUDIO:
-       {
-               vector<string> a = _film->external_audio ();
-               for (size_t i = 0; i < a.size() && i < MAX_AUDIO_CHANNELS; ++i) {
-                       checked_set (_external_audio[i], a[i]);
-               }
-               setup_audio_details ();
+       case Film::THREE_D:
+               checked_set (_three_d, _film->three_d ());
+               setup_dcp_name ();
+               break;
+       case Film::INTEROP:
+               checked_set (_standard, _film->interop() ? 1 : 0);
                break;
        }
+ }
+ void
+ FilmEditor::film_content_changed (weak_ptr<Content> weak_content, int property)
+ {
+       ensure_ui_thread ();
+       
+       if (!_film) {
+               /* We call this method ourselves (as well as using it as a signal handler)
+                  so _film can be 0.
+               */
+               return;
+       }
+       shared_ptr<Content> content = weak_content.lock ();
+       if (!content || content != selected_content ()) {
+               return;
+       }
+       for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
+               (*i)->film_content_changed (content, property);
+       }
+       if (property == FFmpegContentProperty::AUDIO_STREAM) {
+               setup_dcp_name ();
        }
  }
  
- /** Called when the format widget has been changed */
  void
- FilmEditor::format_changed (wxCommandEvent &)
+ FilmEditor::setup_container ()
+ {
+       int n = 0;
+       vector<Ratio const *> ratios = Ratio::all ();
+       vector<Ratio const *>::iterator i = ratios.begin ();
+       while (i != ratios.end() && *i != _film->container ()) {
+               ++i;
+               ++n;
+       }
+       
+       if (i == ratios.end()) {
+               checked_set (_container, -1);
+       } else {
+               checked_set (_container, n);
+       }
+       
+       setup_dcp_name ();
+ }     
+ /** Called when the container widget has been changed */
+ void
+ FilmEditor::container_changed ()
  {
        if (!_film) {
                return;
        }
  
-       int const n = _format->GetSelection ();
+       int const n = _container->GetSelection ();
        if (n >= 0) {
-               assert (n < int (_formats.size()));
-               _film->set_format (_formats[n]);
+               vector<Ratio const *> ratios = Ratio::all ();
+               assert (n < int (ratios.size()));
+               _film->set_container (ratios[n]);
        }
  }
  
  /** Called when the DCP content type widget has been changed */
  void
- FilmEditor::dcp_content_type_changed (wxCommandEvent &)
+ FilmEditor::dcp_content_type_changed ()
  {
        if (!_film) {
                return;
  void
  FilmEditor::set_film (shared_ptr<Film> f)
  {
-       _film = f;
+       set_general_sensitivity (f != 0);
  
-       set_things_sensitive (_film != 0);
+       if (_film == f) {
+               return;
+       }
+       
+       _film = f;
  
        if (_film) {
                _film->Changed.connect (bind (&FilmEditor::film_changed, this, _1));
+               _film->ContentChanged.connect (bind (&FilmEditor::film_content_changed, this, _1, _2));
        }
  
        if (_film) {
        } else {
                FileChanged ("");
        }
-       
        film_changed (Film::NAME);
        film_changed (Film::USE_DCI_NAME);
        film_changed (Film::CONTENT);
-       film_changed (Film::TRUST_CONTENT_HEADER);
        film_changed (Film::DCP_CONTENT_TYPE);
-       film_changed (Film::FORMAT);
-       film_changed (Film::CROP);
-       film_changed (Film::FILTERS);
+       film_changed (Film::CONTAINER);
+       film_changed (Film::RESOLUTION);
        film_changed (Film::SCALER);
-       film_changed (Film::DCP_TRIM_START);
-       film_changed (Film::DCP_TRIM_END);
-       film_changed (Film::REEL_SIZE);
-       film_changed (Film::DCP_AB);
-       film_changed (Film::CONTENT_AUDIO_STREAM);
-       film_changed (Film::EXTERNAL_AUDIO);
-       film_changed (Film::USE_CONTENT_AUDIO);
-       film_changed (Film::AUDIO_GAIN);
-       film_changed (Film::AUDIO_DELAY);
-       film_changed (Film::STILL_DURATION);
        film_changed (Film::WITH_SUBTITLES);
-       film_changed (Film::SUBTITLE_OFFSET);
-       film_changed (Film::SUBTITLE_SCALE);
 +      film_changed (Film::ENCRYPTED);
-       film_changed (Film::COLOUR_LUT);
        film_changed (Film::J2K_BANDWIDTH);
        film_changed (Film::DCI_METADATA);
-       film_changed (Film::SIZE);
-       film_changed (Film::LENGTH);
-       film_changed (Film::CONTENT_AUDIO_STREAMS);
-       film_changed (Film::SUBTITLE_STREAMS);
-       film_changed (Film::FRAMES_PER_SECOND);
+       film_changed (Film::VIDEO_FRAME_RATE);
+       film_changed (Film::AUDIO_CHANNELS);
+       film_changed (Film::SEQUENCE_VIDEO);
+       film_changed (Film::THREE_D);
+       film_changed (Film::INTEROP);
+       if (!_film->content().empty ()) {
+               set_selection (_film->content().front ());
+       }
+       content_selection_changed ();
  }
  
- /** Updates the sensitivity of lots of widgets to a given value.
-  *  @param s true to make sensitive, false to make insensitive.
-  */
  void
- FilmEditor::set_things_sensitive (bool s)
+ FilmEditor::set_general_sensitivity (bool s)
  {
        _generally_sensitive = s;
-       
+       /* Stuff in the Content / DCP tabs */
        _name->Enable (s);
        _use_dci_name->Enable (s);
        _edit_dci_button->Enable (s);
-       _format->Enable (s);
        _content->Enable (s);
-       _trust_content_header->Enable (s);
-       _left_crop->Enable (s);
-       _right_crop->Enable (s);
-       _top_crop->Enable (s);
-       _bottom_crop->Enable (s);
-       _filters_button->Enable (s);
-       _scaler->Enable (s);
-       _audio_stream->Enable (s);
+       _content_add_file->Enable (s);
+       _content_add_folder->Enable (s);
+       _content_remove->Enable (s);
+       _content_timeline->Enable (s);
        _dcp_content_type->Enable (s);
-       _dcp_trim_start->Enable (s);
-       _dcp_trim_end->Enable (s);
-       _multiple_reels->Enable (s);
-       _reel_size->Enable (s);
-       _dcp_ab->Enable (s);
 +      _encrypted->Enable (s);
-       _colour_lut->Enable (s);
+       _frame_rate->Enable (s);
+       _audio_channels->Enable (s);
        _j2k_bandwidth->Enable (s);
-       _audio_gain->Enable (s);
-       _audio_gain_calculate_button->Enable (s);
-       _audio_delay->Enable (s);
-       _still_duration->Enable (s);
-       setup_subtitle_control_sensitivity ();
-       setup_audio_control_sensitivity ();
-       setup_reel_control_sensitivity ();
- }
+       _container->Enable (s);
+       _best_frame_rate->Enable (s && _film && _film->best_video_frame_rate () != _film->video_frame_rate ());
+       _sequence_video->Enable (s);
+       _resolution->Enable (s);
+       _scaler->Enable (s);
+       _three_d->Enable (s);
+       _standard->Enable (s);
  
- /** Called when the `Edit filters' button has been clicked */
- void
- FilmEditor::edit_filters_clicked (wxCommandEvent &)
- {
-       FilterDialog* d = new FilterDialog (this, _film->filters());
-       d->ActiveChanged.connect (bind (&Film::set_filters, _film, _1));
-       d->ShowModal ();
-       d->Destroy ();
+       /* Set the panels in the content notebook */
+       for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
+               (*i)->Enable (s);
+       }
  }
  
  /** Called when the scaler widget has been changed */
  void
- FilmEditor::scaler_changed (wxCommandEvent &)
+ FilmEditor::scaler_changed ()
  {
        if (!_film) {
                return;
  }
  
  void
- FilmEditor::audio_gain_changed (wxCommandEvent &)
+ FilmEditor::use_dci_name_toggled ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_audio_gain (_audio_gain->GetValue ());
+       _film->set_use_dci_name (_use_dci_name->GetValue ());
  }
  
  void
- FilmEditor::audio_delay_changed (wxCommandEvent &)
+ FilmEditor::edit_dci_button_clicked ()
  {
        if (!_film) {
                return;
        }
  
-       _film->set_audio_delay (_audio_delay->GetValue ());
- }
- wxControl *
- FilmEditor::video_control (wxControl* c)
- {
-       _video_controls.push_back (c);
-       return c;
- }
- wxControl *
- FilmEditor::still_control (wxControl* c)
- {
-       _still_controls.push_back (c);
-       return c;
+       DCIMetadataDialog* d = new DCIMetadataDialog (this, _film->dci_metadata ());
+       d->ShowModal ();
+       _film->set_dci_metadata (d->dci_metadata ());
+       d->Destroy ();
  }
  
  void
- FilmEditor::setup_visibility ()
+ FilmEditor::active_jobs_changed (bool a)
  {
-       ContentType c = VIDEO;
-       if (_film) {
-               c = _film->content_type ();
-       }
-       for (list<wxControl*>::iterator i = _video_controls.begin(); i != _video_controls.end(); ++i) {
-               (*i)->Show (c == VIDEO);
-       }
-       for (list<wxControl*>::iterator i = _still_controls.begin(); i != _still_controls.end(); ++i) {
-               (*i)->Show (c == STILL);
-       }
-       _notebook->InvalidateBestSize ();
-       
-       _film_sizer->Layout ();
-       _film_sizer->SetSizeHints (_film_panel);
-       _video_sizer->Layout ();
-       _video_sizer->SetSizeHints (_video_panel);
-       _audio_sizer->Layout ();
-       _audio_sizer->SetSizeHints (_audio_panel);
-       _subtitle_sizer->Layout ();
-       _subtitle_sizer->SetSizeHints (_subtitle_panel);
-       _notebook->Fit ();
-       Fit ();
+       set_general_sensitivity (!a);
  }
  
  void
- FilmEditor::still_duration_changed (wxCommandEvent &)
+ FilmEditor::setup_dcp_name ()
  {
-       if (!_film) {
-               return;
+       string s = _film->dcp_name (true);
+       if (s.length() > 28) {
+               _dcp_name->SetLabel (std_to_wx (s.substr (0, 28)) + N_("..."));
+               _dcp_name->SetToolTip (std_to_wx (s));
+       } else {
+               _dcp_name->SetLabel (std_to_wx (s));
        }
-       _film->set_still_duration (_still_duration->GetValue ());
  }
  
  void
- FilmEditor::dcp_trim_start_changed (wxCommandEvent &)
+ FilmEditor::best_frame_rate_clicked ()
  {
        if (!_film) {
                return;
        }
-       _film->set_dcp_trim_start (_dcp_trim_start->GetValue ());
+       
+       _film->set_video_frame_rate (_film->best_video_frame_rate ());
  }
  
  void
- FilmEditor::dcp_trim_end_changed (wxCommandEvent &)
+ FilmEditor::setup_content ()
  {
-       if (!_film) {
-               return;
+       string selected_summary;
+       int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (s != -1) {
+               selected_summary = wx_to_std (_content->GetItemText (s));
+       }
+       
+       _content->DeleteAllItems ();
+       ContentList content = _film->content ();
+       for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
+               int const t = _content->GetItemCount ();
+               _content->InsertItem (t, std_to_wx ((*i)->summary ()));
+               if ((*i)->summary() == selected_summary) {
+                       _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
+               }
        }
  
-       _film->set_dcp_trim_end (_dcp_trim_end->GetValue ());
+       if (selected_summary.empty () && !content.empty ()) {
+               /* Select the item of content if none was selected before */
+               _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
+       }
  }
  
  void
- FilmEditor::audio_gain_calculate_button_clicked (wxCommandEvent &)
+ FilmEditor::content_add_file_clicked ()
  {
-       GainCalculatorDialog* d = new GainCalculatorDialog (this);
-       d->ShowModal ();
+       wxFileDialog* d = new wxFileDialog (this, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE);
+       int const r = d->ShowModal ();
+       d->Destroy ();
  
-       if (d->wanted_fader() == 0 || d->actual_fader() == 0) {
-               d->Destroy ();
+       if (r != wxID_OK) {
                return;
        }
-       
-       _audio_gain->SetValue (
-               Config::instance()->sound_processor()->db_for_fader_change (
-                       d->wanted_fader (),
-                       d->actual_fader ()
-                       )
-               );
  
-       /* This appears to be necessary, as the change is not signalled,
-          I think.
-       */
-       wxCommandEvent dummy;
-       audio_gain_changed (dummy);
-       
-       d->Destroy ();
- }
+       wxArrayString paths;
+       d->GetPaths (paths);
  
- void
- FilmEditor::setup_formats ()
- {
-       ContentType c = VIDEO;
+       /* XXX: check for lots of files here and do something */
  
-       if (_film) {
-               c = _film->content_type ();
-       }
-       
-       _formats.clear ();
-       vector<Format const *> fmt = Format::all ();
-       for (vector<Format const *>::iterator i = fmt.begin(); i != fmt.end(); ++i) {
-               if (c == VIDEO && dynamic_cast<FixedFormat const *> (*i)) {
-                       _formats.push_back (*i);
-               } else if (c == STILL && dynamic_cast<VariableFormat const *> (*i)) {
-                       _formats.push_back (*i);
+       for (unsigned int i = 0; i < paths.GetCount(); ++i) {
+               boost::filesystem::path p (wx_to_std (paths[i]));
+               shared_ptr<Content> c;
+               if (valid_image_file (p)) {
+                       c.reset (new StillImageContent (_film, p));
+               } else if (SndfileContent::valid_file (p)) {
+                       c.reset (new SndfileContent (_film, p));
+               } else {
+                       c.reset (new FFmpegContent (_film, p));
                }
-       }
  
-       _format->Clear ();
-       for (vector<Format const *>::iterator i = _formats.begin(); i != _formats.end(); ++i) {
-               _format->Append (std_to_wx ((*i)->name ()));
+               _film->examine_and_add_content (c);
        }
-       _film_sizer->Layout ();
  }
  
  void
- FilmEditor::with_subtitles_toggled (wxCommandEvent &)
+ FilmEditor::content_add_folder_clicked ()
  {
-       if (!_film) {
+       wxDirDialog* d = new wxDirDialog (this, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
+       int const r = d->ShowModal ();
+       d->Destroy ();
+       
+       if (r != wxID_OK) {
                return;
        }
  
-       _film->set_with_subtitles (_with_subtitles->GetValue ());
+       _film->examine_and_add_content (
+               shared_ptr<MovingImageContent> (
+                       new MovingImageContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ())))
+                       )
+               );
  }
  
  void
- FilmEditor::setup_subtitle_control_sensitivity ()
+ FilmEditor::content_remove_clicked ()
  {
-       bool h = false;
-       if (_generally_sensitive && _film) {
-               h = !_film->subtitle_streams().empty();
+       shared_ptr<Content> c = selected_content ();
+       if (c) {
+               _film->remove_content (c);
        }
-       
-       _with_subtitles->Enable (h);
  
-       bool j = false;
-       if (_film) {
-               j = _film->with_subtitles ();
-       }
-       
-       _subtitle_stream->Enable (j);
-       _subtitle_offset->Enable (j);
-       _subtitle_scale->Enable (j);
+       content_selection_changed ();
  }
  
  void
- FilmEditor::setup_audio_control_sensitivity ()
+ FilmEditor::content_selection_changed ()
  {
-       _use_content_audio->Enable (_generally_sensitive);
-       _use_external_audio->Enable (_generally_sensitive);
-       
-       bool const source = _generally_sensitive && _use_content_audio->GetValue();
-       bool const external = _generally_sensitive && _use_external_audio->GetValue();
+       setup_content_sensitivity ();
+       shared_ptr<Content> s = selected_content ();
  
-       _audio_stream->Enable (source);
-       for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
-               _external_audio[i]->Enable (external);
-       }
+       /* All other sensitivity in content panels should be triggered by
+          one of these.
+       */
+       film_content_changed (s, ContentProperty::POSITION);
+       film_content_changed (s, ContentProperty::LENGTH);
+       film_content_changed (s, ContentProperty::TRIM_START);
+       film_content_changed (s, ContentProperty::TRIM_END);
+       film_content_changed (s, VideoContentProperty::VIDEO_CROP);
+       film_content_changed (s, VideoContentProperty::VIDEO_RATIO);
+       film_content_changed (s, VideoContentProperty::VIDEO_FRAME_TYPE);
+       film_content_changed (s, VideoContentProperty::COLOUR_CONVERSION);
+       film_content_changed (s, AudioContentProperty::AUDIO_GAIN);
+       film_content_changed (s, AudioContentProperty::AUDIO_DELAY);
+       film_content_changed (s, AudioContentProperty::AUDIO_MAPPING);
+       film_content_changed (s, FFmpegContentProperty::AUDIO_STREAM);
+       film_content_changed (s, FFmpegContentProperty::AUDIO_STREAMS);
+       film_content_changed (s, FFmpegContentProperty::SUBTITLE_STREAM);
+       film_content_changed (s, FFmpegContentProperty::SUBTITLE_STREAMS);
+       film_content_changed (s, FFmpegContentProperty::FILTERS);
+       film_content_changed (s, SubtitleContentProperty::SUBTITLE_OFFSET);
+       film_content_changed (s, SubtitleContentProperty::SUBTITLE_SCALE);
  }
  
+ /** Set up broad sensitivity based on the type of content that is selected */
  void
- FilmEditor::use_dci_name_toggled (wxCommandEvent &)
+ FilmEditor::setup_content_sensitivity ()
  {
-       if (!_film) {
-               return;
-       }
+       _content_add_file->Enable (_generally_sensitive);
+       _content_add_folder->Enable (_generally_sensitive);
  
-       _film->set_use_dci_name (_use_dci_name->GetValue ());
+       shared_ptr<Content> selection = selected_content ();
+       _content_remove->Enable (selection && _generally_sensitive);
+       _content_timeline->Enable (_generally_sensitive);
+       _video_panel->Enable    (selection && dynamic_pointer_cast<VideoContent>  (selection) && _generally_sensitive);
+       _audio_panel->Enable    (selection && dynamic_pointer_cast<AudioContent>  (selection) && _generally_sensitive);
+       _subtitle_panel->Enable (selection && dynamic_pointer_cast<FFmpegContent> (selection) && _generally_sensitive);
+       _timing_panel->Enable   (selection && _generally_sensitive);
  }
  
- void
- FilmEditor::edit_dci_button_clicked (wxCommandEvent &)
+ shared_ptr<Content>
+ FilmEditor::selected_content ()
  {
-       if (!_film) {
-               return;
+       int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (s == -1) {
+               return shared_ptr<Content> ();
        }
  
-       DCINameDialog* d = new DCINameDialog (this, _film);
-       d->ShowModal ();
-       d->Destroy ();
+       ContentList c = _film->content ();
+       if (s < 0 || size_t (s) >= c.size ()) {
+               return shared_ptr<Content> ();
+       }
+       
+       return c[s];
  }
  
- void
- FilmEditor::setup_streams ()
+ shared_ptr<VideoContent>
+ FilmEditor::selected_video_content ()
  {
-       _audio_stream->Clear ();
-       vector<shared_ptr<AudioStream> > a = _film->content_audio_streams ();
-       for (vector<shared_ptr<AudioStream> >::iterator i = a.begin(); i != a.end(); ++i) {
-               shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (*i);
-               assert (ffa);
-               _audio_stream->Append (std_to_wx (ffa->name()), new wxStringClientData (std_to_wx (ffa->to_string ())));
-       }
-       
-       if (_film->use_content_audio() && _film->audio_stream()) {
-               checked_set (_audio_stream, _film->audio_stream()->to_string());
+       shared_ptr<Content> c = selected_content ();
+       if (!c) {
+               return shared_ptr<VideoContent> ();
        }
  
-       _subtitle_stream->Clear ();
-       vector<shared_ptr<SubtitleStream> > s = _film->subtitle_streams ();
-       for (vector<shared_ptr<SubtitleStream> >::iterator i = s.begin(); i != s.end(); ++i) {
-               _subtitle_stream->Append (std_to_wx ((*i)->name()), new wxStringClientData (std_to_wx ((*i)->to_string ())));
-       }
      if (_film->subtitle_stream()) {
-               checked_set (_subtitle_stream, _film->subtitle_stream()->to_string());
-       } else {
-               _subtitle_stream->SetValue (wxT (""));
+       return dynamic_pointer_cast<VideoContent> (c);
+ }
+ shared_ptr<AudioContent>
+ FilmEditor::selected_audio_content ()
+ {
+       shared_ptr<Content> c = selected_content ();
+       if (!c) {
+               return shared_ptr<AudioContent> ();
        }
+       return dynamic_pointer_cast<AudioContent> (c);
  }
  
- void
- FilmEditor::audio_stream_changed (wxCommandEvent &)
+ shared_ptr<SubtitleContent>
+ FilmEditor::selected_subtitle_content ()
  {
-       if (!_film) {
-               return;
+       shared_ptr<Content> c = selected_content ();
+       if (!c) {
+               return shared_ptr<SubtitleContent> ();
        }
  
-       _film->set_content_audio_stream (
-               audio_stream_factory (
-                       string_client_data (_audio_stream->GetClientObject (_audio_stream->GetSelection ())),
-                       Film::state_version
-                       )
-               );
+       return dynamic_pointer_cast<SubtitleContent> (c);
  }
  
  void
- FilmEditor::subtitle_stream_changed (wxCommandEvent &)
+ FilmEditor::content_timeline_clicked ()
  {
-       if (!_film) {
-               return;
+       if (_timeline_dialog) {
+               _timeline_dialog->Destroy ();
+               _timeline_dialog = 0;
        }
-       _film->set_subtitle_stream (
-               subtitle_stream_factory (
-                       string_client_data (_subtitle_stream->GetClientObject (_subtitle_stream->GetSelection ())),
-                       Film::state_version
-                       )
-               );
+       
+       _timeline_dialog = new TimelineDialog (this, _film);
+       _timeline_dialog->Show ();
  }
  
  void
- FilmEditor::setup_audio_details ()
+ FilmEditor::set_selection (weak_ptr<Content> wc)
  {
-       if (!_film->audio_stream()) {
-               _audio->SetLabel (wxT (""));
-       } else {
-               stringstream s;
-               if (_film->audio_stream()->channels() == 1) {
-                       s << "1 channel";
+       ContentList content = _film->content ();
+       for (size_t i = 0; i < content.size(); ++i) {
+               if (content[i] == wc.lock ()) {
+                       _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
                } else {
-                       s << _film->audio_stream()->channels () << " channels";
+                       _content->SetItemState (i, 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
                }
-               s << ", " << _film->audio_stream()->sample_rate() << "Hz";
-               _audio->SetLabel (std_to_wx (s.str ()));
        }
  }
  
  void
- FilmEditor::active_jobs_changed (bool a)
+ FilmEditor::sequence_video_changed ()
  {
-       set_things_sensitive (!a);
+       if (!_film) {
+               return;
+       }
+       
+       _film->set_sequence_video (_sequence_video->GetValue ());
  }
  
  void
- FilmEditor::use_audio_changed (wxCommandEvent &)
+ FilmEditor::content_right_click (wxListEvent& ev)
  {
-       _film->set_use_content_audio (_use_content_audio->GetValue());
+       ContentList cl;
+       cl.push_back (selected_content ());
+       _menu.popup (cl, ev.GetPoint ());
  }
  
  void
- FilmEditor::external_audio_changed (wxCommandEvent &)
+ FilmEditor::three_d_changed ()
  {
-       vector<string> a;
-       for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
-               a.push_back (wx_to_std (_external_audio[i]->GetPath()));
+       if (!_film) {
+               return;
        }
  
-       _film->set_external_audio (a);
- }
- void
- FilmEditor::setup_reel_control_sensitivity ()
- {
-       _reel_size->Enable (_multiple_reels->GetValue ());
+       _film->set_three_d (_three_d->GetValue ());
  }
diff --combined src/wx/film_editor.h
index b990ec40db748ffdc202c01a398660f16a02907a,8ecec3ec76801b6612d1a196a6a9465bde774a91..bb217211ca7719c0e58ba4a8cb22854726ec8961
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -16,7 -16,7 +16,7 @@@
      Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  
  */
+  
  /** @file src/film_editor.h
   *  @brief A wx widget to edit a film's metadata, and perform various functions.
   */
  #include <wx/collpane.h>
  #include <boost/signals2.hpp>
  #include "lib/film.h"
+ #include "content_menu.h"
  
  class wxNotebook;
+ class wxListCtrl;
+ class wxListEvent;
  class Film;
+ class TimelineDialog;
+ class Ratio;
+ class Timecode;
+ class FilmEditorPanel;
+ class SubtitleContent;
  
  /** @class FilmEditor
   *  @brief A wx widget to edit a film's metadata, and perform various functions.
@@@ -41,146 -48,106 +48,108 @@@ public
        FilmEditor (boost::shared_ptr<Film>, wxWindow *);
  
        void set_film (boost::shared_ptr<Film>);
-       void setup_visibility ();
+       void set_selection (boost::weak_ptr<Content>);
  
        boost::signals2::signal<void (std::string)> FileChanged;
  
+       /* Stuff for panels */
+       
+       wxNotebook* content_notebook () const {
+               return _content_notebook;
+       }
+       boost::shared_ptr<Film> film () const {
+               return _film;
+       }
+       boost::shared_ptr<Content> selected_content ();
+       boost::shared_ptr<VideoContent> selected_video_content ();
+       boost::shared_ptr<AudioContent> selected_audio_content ();
+       boost::shared_ptr<SubtitleContent> selected_subtitle_content ();
+       
  private:
-       void make_film_panel ();
-       void make_video_panel ();
-       void make_audio_panel ();
-       void make_subtitle_panel ();
+       void make_dcp_panel ();
+       void make_content_panel ();
        void connect_to_widgets ();
        
        /* Handle changes to the view */
-       void name_changed (wxCommandEvent &);
-       void use_dci_name_toggled (wxCommandEvent &);
-       void edit_dci_button_clicked (wxCommandEvent &);
-       void left_crop_changed (wxCommandEvent &);
-       void right_crop_changed (wxCommandEvent &);
-       void top_crop_changed (wxCommandEvent &);
-       void bottom_crop_changed (wxCommandEvent &);
-       void content_changed (wxCommandEvent &);
-       void trust_content_header_changed (wxCommandEvent &);
-       void format_changed (wxCommandEvent &);
-       void dcp_trim_start_changed (wxCommandEvent &);
-       void dcp_trim_end_changed (wxCommandEvent &);
-       void multiple_reels_toggled (wxCommandEvent &);
-       void reel_size_changed (wxCommandEvent &);
-       void dcp_content_type_changed (wxCommandEvent &);
-       void encrypted_toggled (wxCommandEvent &);
-       void dcp_ab_toggled (wxCommandEvent &);
-       void scaler_changed (wxCommandEvent &);
-       void audio_gain_changed (wxCommandEvent &);
-       void audio_gain_calculate_button_clicked (wxCommandEvent &);
-       void audio_delay_changed (wxCommandEvent &);
-       void with_subtitles_toggled (wxCommandEvent &);
-       void subtitle_offset_changed (wxCommandEvent &);
-       void subtitle_scale_changed (wxCommandEvent &);
-       void colour_lut_changed (wxCommandEvent &);
-       void j2k_bandwidth_changed (wxCommandEvent &);
-       void still_duration_changed (wxCommandEvent &);
-       void audio_stream_changed (wxCommandEvent &);
-       void subtitle_stream_changed (wxCommandEvent &);
-       void use_audio_changed (wxCommandEvent &);
-       void external_audio_changed (wxCommandEvent &);
+       void name_changed ();
+       void use_dci_name_toggled ();
+       void edit_dci_button_clicked ();
+       void content_selection_changed ();
+       void content_add_file_clicked ();
+       void content_add_folder_clicked ();
+       void content_remove_clicked ();
+       void container_changed ();
+       void dcp_content_type_changed ();
+       void scaler_changed ();
+       void j2k_bandwidth_changed ();
+       void frame_rate_changed ();
+       void best_frame_rate_clicked ();
+       void content_timeline_clicked ();
+       void audio_channels_changed ();
+       void resolution_changed ();
+       void sequence_video_changed ();
+       void content_right_click (wxListEvent &);
+       void three_d_changed ();
+       void standard_changed ();
++      void encrypted_toggled ();
  
        /* Handle changes to the model */
        void film_changed (Film::Property);
+       void film_content_changed (boost::weak_ptr<Content>, int);
  
-       /* Button clicks */
-       void edit_filters_clicked (wxCommandEvent &);
-       void set_things_sensitive (bool);
-       void setup_formats ();
-       void setup_subtitle_control_sensitivity ();
-       void setup_audio_control_sensitivity ();
-       void setup_reel_control_sensitivity ();
-       void setup_streams ();
-       void setup_audio_details ();
+       void set_general_sensitivity (bool);
+       void setup_dcp_name ();
+       void setup_content ();
+       void setup_container ();
+       void setup_content_sensitivity ();
        
-       wxControl* video_control (wxControl *);
-       wxControl* still_control (wxControl *);
        void active_jobs_changed (bool);
  
-       wxNotebook* _notebook;
-       wxPanel* _film_panel;
-       wxSizer* _film_sizer;
-       wxPanel* _video_panel;
-       wxSizer* _video_sizer;
-       wxPanel* _audio_panel;
-       wxSizer* _audio_sizer;
-       wxPanel* _subtitle_panel;
-       wxSizer* _subtitle_sizer;
+       FilmEditorPanel* _video_panel;
+       FilmEditorPanel* _audio_panel;
+       FilmEditorPanel* _subtitle_panel;
+       FilmEditorPanel* _timing_panel;
+       std::list<FilmEditorPanel *> _panels;
+       wxNotebook* _main_notebook;
+       wxNotebook* _content_notebook;
+       wxPanel* _dcp_panel;
+       wxSizer* _dcp_sizer;
+       wxPanel* _content_panel;
+       wxSizer* _content_sizer;
  
        /** The film we are editing */
        boost::shared_ptr<Film> _film;
-       /** The Film's name */
        wxTextCtrl* _name;
        wxStaticText* _dcp_name;
        wxCheckBox* _use_dci_name;
+       wxChoice* _container;
+       wxListCtrl* _content;
+       wxButton* _content_add_file;
+       wxButton* _content_add_folder;
+       wxButton* _content_remove;
+       wxButton* _content_earlier;
+       wxButton* _content_later;
+       wxButton* _content_timeline;
+       wxCheckBox* _sequence_video;
        wxButton* _edit_dci_button;
-       /** The Film's format */
-       wxComboBox* _format;
-       /** The Film's content file */
-       wxFilePickerCtrl* _content;
-       wxCheckBox* _trust_content_header;
-       /** The Film's left crop */
-       wxSpinCtrl* _left_crop;
-       /** The Film's right crop */
-       wxSpinCtrl* _right_crop;
-       /** The Film's top crop */
-       wxSpinCtrl* _top_crop;
-       /** The Film's bottom crop */
-       wxSpinCtrl* _bottom_crop;
-       /** Currently-applied filters */
-       wxStaticText* _filters;
-       /** Button to open the filters dialogue */
-       wxButton* _filters_button;
-       /** The Film's scaler */
-       wxComboBox* _scaler;
-       wxRadioButton* _use_content_audio;
-       wxComboBox* _audio_stream;
-       wxRadioButton* _use_external_audio;
-       wxFilePickerCtrl* _external_audio[MAX_AUDIO_CHANNELS];
-       /** The Film's audio gain */
-       wxSpinCtrl* _audio_gain;
-       /** A button to open the gain calculation dialogue */
-       wxButton* _audio_gain_calculate_button;
-       /** The Film's audio delay */
-       wxSpinCtrl* _audio_delay;
-       wxCheckBox* _with_subtitles;
-       wxComboBox* _subtitle_stream;
-       wxSpinCtrl* _subtitle_offset;
-       wxSpinCtrl* _subtitle_scale;
-       wxComboBox* _colour_lut;
-       wxSpinCtrl* _j2k_bandwidth;
-       /** The Film's DCP content type */
-       wxComboBox* _dcp_content_type;
-       /** The Film's frames per second */
-       wxStaticText* _frames_per_second;
-       /** The Film's original size */
-       wxStaticText* _original_size;
-       /** The Film's length */
-       wxStaticText* _length;
-       /** The Film's audio details */
-       wxStaticText* _audio;
-       /** The Film's duration for still sources */
-       wxSpinCtrl* _still_duration;
-       wxSpinCtrl* _dcp_trim_start;
-       wxSpinCtrl* _dcp_trim_end;
+       wxChoice* _scaler;
+       wxSpinCtrl* _j2k_bandwidth;
+       wxChoice* _dcp_content_type;
+       wxChoice* _frame_rate;
+       wxSpinCtrl* _audio_channels;
+       wxButton* _best_frame_rate;
+       wxCheckBox* _three_d;
+       wxChoice* _resolution;
+       wxChoice* _standard;
 +      wxCheckBox* _encrypted;
-       wxCheckBox* _multiple_reels;
-       wxSpinCtrl* _reel_size;
-       /** Selector to generate an A/B comparison DCP */
-       wxCheckBox* _dcp_ab;
  
-       std::list<wxControl*> _video_controls;
-       std::list<wxControl*> _still_controls;
+       ContentMenu _menu;
  
-       std::vector<Format const *> _formats;
+       std::vector<Ratio const *> _ratios;
  
        bool _generally_sensitive;
+       TimelineDialog* _timeline_dialog;
  };
diff --combined src/wx/kdm_dialog.cc
index d94c130575ab270a478d54971640f01bf328d37b,0000000000000000000000000000000000000000..a9f63cffce971e06dd65a8d1c9d6d2f0cbab2a29
mode 100644,000000..100644
--- /dev/null
@@@ -1,376 -1,0 +1,373 @@@
-       add_label_to_sizer (vertical, this, "Make KDMs for");
 +/*
 +    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +
 +    This program 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.
 +
 +    This program 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 this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <wx/treectrl.h>
 +#include <wx/datectrl.h>
 +#include <wx/timectrl.h>
 +#include "lib/cinema.h"
 +#include "lib/config.h"
 +#include "kdm_dialog.h"
 +#include "cinema_dialog.h"
 +#include "screen_dialog.h"
 +#include "wx_util.h"
 +#ifdef __WXMSW__
 +#include "dir_picker_ctrl.h"
 +#else
 +#include <wx/filepicker.h>
 +#endif
 +
 +using std::string;
 +using std::map;
 +using std::list;
 +using std::pair;
 +using std::make_pair;
 +using boost::shared_ptr;
 +
 +KDMDialog::KDMDialog (wxWindow* parent)
 +      : wxDialog (parent, wxID_ANY, _("Make KDMs"))
 +{
 +      wxBoxSizer* vertical = new wxBoxSizer (wxVERTICAL);
-       target_buttons->Add (_add_cinema, 1, 0, 6);
 +      wxBoxSizer* targets = new wxBoxSizer (wxHORIZONTAL);
 +      
 +      _targets = new wxTreeCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_HIDE_ROOT | wxTR_MULTIPLE | wxTR_HAS_BUTTONS);
 +      targets->Add (_targets, 1, wxEXPAND | wxALL, 6);
 +
 +      _root = _targets->AddRoot ("Foo");
 +
 +      list<shared_ptr<Cinema> > c = Config::instance()->cinemas ();
 +      for (list<shared_ptr<Cinema> >::iterator i = c.begin(); i != c.end(); ++i) {
 +              add_cinema (*i);
 +      }
 +
 +      _targets->ExpandAll ();
 +
 +      wxBoxSizer* target_buttons = new wxBoxSizer (wxVERTICAL);
 +
 +      _add_cinema = new wxButton (this, wxID_ANY, _("Add Cinema..."));
-       target_buttons->Add (_edit_cinema, 1, 0, 6);
++      target_buttons->Add (_add_cinema, 1, wxEXPAND, 6);
 +      _edit_cinema = new wxButton (this, wxID_ANY, _("Edit Cinema..."));
-       target_buttons->Add (_remove_cinema, 1, 0, 6);
++      target_buttons->Add (_edit_cinema, 1, wxEXPAND, 6);
 +      _remove_cinema = new wxButton (this, wxID_ANY, _("Remove Cinema"));
-       target_buttons->Add (_add_screen, 1, 0, 6);
++      target_buttons->Add (_remove_cinema, 1, wxEXPAND, 6);
 +      
 +      _add_screen = new wxButton (this, wxID_ANY, _("Add Screen..."));
-       target_buttons->Add (_edit_screen, 1, 0, 6);
++      target_buttons->Add (_add_screen, 1, wxEXPAND, 6);
 +      _edit_screen = new wxButton (this, wxID_ANY, _("Edit Screen..."));
-       target_buttons->Add (_remove_screen, 1, 0, 6);
++      target_buttons->Add (_edit_screen, 1, wxEXPAND, 6);
 +      _remove_screen = new wxButton (this, wxID_ANY, _("Remove Screen"));
-       add_label_to_sizer (table, this, "From");
++      target_buttons->Add (_remove_screen, 1, wxEXPAND, 6);
 +
 +      targets->Add (target_buttons, 0, 0, 6);
 +
 +      vertical->Add (targets, 1, wxEXPAND | wxALL, 6);
 +
 +      wxFlexGridSizer* table = new wxFlexGridSizer (3, 2, 6);
-       add_label_to_sizer (table, this, "Until");
++      add_label_to_sizer (table, this, "From", true);
 +      _from_date = new wxDatePickerCtrl (this, wxID_ANY);
 +      table->Add (_from_date, 1, wxEXPAND);
 +      _from_time = new wxTimePickerCtrl (this, wxID_ANY);
 +      table->Add (_from_time, 1, wxEXPAND);
 +      
-       add_label_to_sizer (table, this, "Write to");
++      add_label_to_sizer (table, this, "Until", true);
 +      _until_date = new wxDatePickerCtrl (this, wxID_ANY);
 +      table->Add (_until_date, 1, wxEXPAND);
 +      _until_time = new wxTimePickerCtrl (this, wxID_ANY);
 +      table->Add (_until_time, 1, wxEXPAND);
 +
-       wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
++      add_label_to_sizer (table, this, "Write to", true);
 +
 +#ifdef __WXMSW__
 +      _folder = new DirPickerCtrl (this);
 +#else 
 +      _folder = new wxDirPickerCtrl (this, wxDD_DIR_MUST_EXIST);
 +#endif
 +
 +      table->Add (_folder, 1, wxEXPAND);
 +      
 +      vertical->Add (table, 0, wxEXPAND | wxALL, 6);
 +
++      wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
 +      if (buttons) {
 +              vertical->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
 +      }
 +
 +      _targets->Connect (wxID_ANY, wxEVT_COMMAND_TREE_SEL_CHANGED, wxCommandEventHandler (KDMDialog::targets_selection_changed), 0, this);
 +
 +      _add_cinema->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::add_cinema_clicked), 0, this);
 +      _edit_cinema->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::edit_cinema_clicked), 0, this);
 +      _remove_cinema->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::remove_cinema_clicked), 0, this);
 +
 +      _add_screen->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::add_screen_clicked), 0, this);
 +      _edit_screen->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::edit_screen_clicked), 0, this);
 +      _remove_screen->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (KDMDialog::remove_screen_clicked), 0, this);
 +
 +      setup_sensitivity ();
 +      
 +      SetSizer (vertical);
 +      vertical->Layout ();
 +      vertical->SetSizeHints (this);
 +}
 +
 +list<pair<wxTreeItemId, shared_ptr<Cinema> > >
 +KDMDialog::selected_cinemas () const
 +{
 +      wxArrayTreeItemIds s;
 +      _targets->GetSelections (s);
 +
 +      list<pair<wxTreeItemId, shared_ptr<Cinema> > > c;
 +      for (size_t i = 0; i < s.GetCount(); ++i) {
 +              map<wxTreeItemId, shared_ptr<Cinema> >::const_iterator j = _cinemas.find (s[i]);
 +              if (j != _cinemas.end ()) {
 +                      c.push_back (make_pair (j->first, j->second));
 +              }
 +      }
 +
 +      return c;
 +}
 +
 +list<pair<wxTreeItemId, shared_ptr<Screen> > >
 +KDMDialog::selected_screens () const
 +{
 +      wxArrayTreeItemIds s;
 +      _targets->GetSelections (s);
 +
 +      list<pair<wxTreeItemId, shared_ptr<Screen> > > c;
 +      for (size_t i = 0; i < s.GetCount(); ++i) {
 +              map<wxTreeItemId, shared_ptr<Screen> >::const_iterator j = _screens.find (s[i]);
 +              if (j != _screens.end ()) {
 +                      c.push_back (make_pair (j->first, j->second));
 +              }
 +      }
 +
 +      return c;
 +}
 +
 +void
 +KDMDialog::targets_selection_changed (wxCommandEvent &)
 +{
 +      setup_sensitivity ();
 +}
 +
 +void
 +KDMDialog::setup_sensitivity ()
 +{
 +      bool const sc = selected_cinemas().size() == 1;
 +      bool const ss = selected_screens().size() == 1;
 +      
 +      _edit_cinema->Enable (sc);
 +      _remove_cinema->Enable (sc);
 +      
 +      _add_screen->Enable (sc);
 +      _edit_screen->Enable (ss);
 +      _remove_screen->Enable (ss);
 +}
 +
 +void
 +KDMDialog::add_cinema (shared_ptr<Cinema> c)
 +{
 +      _cinemas[_targets->AppendItem (_root, std_to_wx (c->name))] = c;
 +
 +      for (list<shared_ptr<Screen> >::iterator i = c->screens.begin(); i != c->screens.end(); ++i) {
 +              add_screen (c, *i);
 +      }
 +}
 +
 +void
 +KDMDialog::add_screen (shared_ptr<Cinema> c, shared_ptr<Screen> s)
 +{
 +      map<wxTreeItemId, shared_ptr<Cinema> >::const_iterator i = _cinemas.begin();
 +      while (i != _cinemas.end() && i->second != c) {
 +              ++i;
 +      }
 +
 +      if (i == _cinemas.end()) {
 +              return;
 +      }
 +
 +      _screens[_targets->AppendItem (i->first, std_to_wx (s->name))] = s;
 +}
 +
 +void
 +KDMDialog::add_cinema_clicked (wxCommandEvent &)
 +{
 +      CinemaDialog* d = new CinemaDialog (this, "Add Cinema");
 +      d->ShowModal ();
 +
 +      shared_ptr<Cinema> c (new Cinema (d->name(), d->email()));
 +      Config::instance()->add_cinema (c);
 +      add_cinema (c);
 +
 +      Config::instance()->write ();
 +      
 +      d->Destroy ();
 +}
 +
 +void
 +KDMDialog::edit_cinema_clicked (wxCommandEvent &)
 +{
 +      if (selected_cinemas().size() != 1) {
 +              return;
 +      }
 +
 +      pair<wxTreeItemId, shared_ptr<Cinema> > c = selected_cinemas().front();
 +      
 +      CinemaDialog* d = new CinemaDialog (this, "Edit cinema", c.second->name, c.second->email);
 +      d->ShowModal ();
 +
 +      c.second->name = d->name ();
 +      c.second->email = d->email ();
 +      _targets->SetItemText (c.first, std_to_wx (d->name()));
 +
 +      Config::instance()->write ();
 +
 +      d->Destroy ();  
 +}
 +
 +void
 +KDMDialog::remove_cinema_clicked (wxCommandEvent &)
 +{
 +      if (selected_cinemas().size() != 1) {
 +              return;
 +      }
 +
 +      pair<wxTreeItemId, shared_ptr<Cinema> > c = selected_cinemas().front();
 +
 +      Config::instance()->remove_cinema (c.second);
 +      _targets->Delete (c.first);
 +
 +      Config::instance()->write ();   
 +}
 +
 +void
 +KDMDialog::add_screen_clicked (wxCommandEvent &)
 +{
 +      if (selected_cinemas().size() != 1) {
 +              return;
 +      }
 +
 +      shared_ptr<Cinema> c = selected_cinemas().front().second;
 +      
 +      ScreenDialog* d = new ScreenDialog (this, "Add Screen");
 +      d->ShowModal ();
 +
 +      shared_ptr<Screen> s (new Screen (d->name(), d->certificate()));
 +      c->screens.push_back (s);
 +      add_screen (c, s);
 +
 +      Config::instance()->write ();
 +
 +      d->Destroy ();
 +}
 +
 +void
 +KDMDialog::edit_screen_clicked (wxCommandEvent &)
 +{
 +      if (selected_screens().size() != 1) {
 +              return;
 +      }
 +
 +      pair<wxTreeItemId, shared_ptr<Screen> > s = selected_screens().front();
 +      
 +      ScreenDialog* d = new ScreenDialog (this, "Edit screen", s.second->name, s.second->certificate);
 +      d->ShowModal ();
 +
 +      s.second->name = d->name ();
 +      s.second->certificate = d->certificate ();
 +      _targets->SetItemText (s.first, std_to_wx (d->name()));
 +
 +      Config::instance()->write ();
 +
 +      d->Destroy ();
 +}
 +
 +void
 +KDMDialog::remove_screen_clicked (wxCommandEvent &)
 +{
 +      if (selected_screens().size() != 1) {
 +              return;
 +      }
 +
 +      pair<wxTreeItemId, shared_ptr<Screen> > s = selected_screens().front();
 +
 +      map<wxTreeItemId, shared_ptr<Cinema> >::iterator i = _cinemas.begin ();
 +      while (i != _cinemas.end() && find (i->second->screens.begin(), i->second->screens.end(), s.second) == i->second->screens.end()) {
 +              ++i;
 +      }
 +
 +      if (i == _cinemas.end()) {
 +              return;
 +      }
 +
 +      i->second->screens.remove (s.second);
 +      _targets->Delete (s.first);
 +
 +      Config::instance()->write ();
 +}
 +
 +list<shared_ptr<Screen> >
 +KDMDialog::screens () const
 +{
 +      list<shared_ptr<Screen> > s;
 +
 +      list<pair<wxTreeItemId, shared_ptr<Cinema> > > cinemas = selected_cinemas ();
 +      for (list<pair<wxTreeItemId, shared_ptr<Cinema> > >::iterator i = cinemas.begin(); i != cinemas.end(); ++i) {
 +              for (list<shared_ptr<Screen> >::iterator j = i->second->screens.begin(); j != i->second->screens.end(); ++j) {
 +                      s.push_back (*j);
 +              }
 +      }
 +
 +      list<pair<wxTreeItemId, shared_ptr<Screen> > > screens = selected_screens ();
 +      for (list<pair<wxTreeItemId, shared_ptr<Screen> > >::iterator i = screens.begin(); i != screens.end(); ++i) {
 +              s.push_back (i->second);
 +      }
 +
 +      s.sort ();
 +      s.unique ();
 +
 +      return s;
 +}
 +
 +boost::posix_time::ptime
 +KDMDialog::from () const
 +{
 +      return posix_time (_from_date, _from_time);
 +}
 +
 +boost::posix_time::ptime
 +KDMDialog::posix_time (wxDatePickerCtrl* date_picker, wxTimePickerCtrl* time_picker)
 +{
 +      wxDateTime const date = date_picker->GetValue ();
 +      wxDateTime const time = time_picker->GetValue ();
 +      return boost::posix_time::ptime (
 +              boost::gregorian::date (date.GetYear(), date.GetMonth() + 1, date.GetDay()),
 +              boost::posix_time::time_duration (time.GetHour(), time.GetMinute(), time.GetSecond())
 +              );
 +}
 +
 +boost::posix_time::ptime
 +KDMDialog::until () const
 +{
 +      return posix_time (_until_date, _until_time);
 +}
 +
 +string
 +KDMDialog::directory () const
 +{
 +      return wx_to_std (_folder->GetPath ());
 +}
diff --combined src/wx/screen_dialog.cc
index 910dece9d0dcc9a05c48b7055c5bb99f79e7b9dc,0000000000000000000000000000000000000000..7ff5197137cd27e6d06c25e3d0326fb684d34bf0
mode 100644,000000..100644
--- /dev/null
@@@ -1,97 -1,0 +1,97 @@@
-       add_label_to_sizer (table, this, "Name");
 +/*
 +    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +
 +    This program 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.
 +
 +    This program 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 this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <wx/filepicker.h>
 +#include <wx/validate.h>
 +#include <libdcp/exceptions.h>
 +#include "lib/compose.hpp"
 +#include "screen_dialog.h"
 +#include "wx_util.h"
 +
 +using std::string;
 +using std::cout;
 +using boost::shared_ptr;
 +
 +ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, shared_ptr<libdcp::Certificate> certificate)
 +      : wxDialog (parent, wxID_ANY, std_to_wx (title))
 +      , _certificate (certificate)
 +{
 +      wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6);
 +      table->AddGrowableCol (1, 1);
 +
-       add_label_to_sizer (table, this, "Certificate");
++      add_label_to_sizer (table, this, "Name", true);
 +      _name = new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (320, -1));
 +      table->Add (_name, 1, wxEXPAND);
 +
++      add_label_to_sizer (table, this, "Certificate", true);
 +      _certificate_load = new wxButton (this, wxID_ANY, wxT ("Load from file..."));
 +      table->Add (_certificate_load, 1, wxEXPAND);
 +
 +      table->AddSpacer (0);
 +      _certificate_text = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, wxSize (320, 256), wxTE_MULTILINE | wxTE_READONLY);
 +      if (certificate) {
 +              _certificate_text->SetValue (certificate->certificate ());
 +      }
 +      wxFont font = wxSystemSettings::GetFont (wxSYS_ANSI_FIXED_FONT);
 +      font.SetPointSize (font.GetPointSize() / 2);
 +      _certificate_text->SetFont (font);
 +      table->Add (_certificate_text, 1, wxEXPAND);
 +
 +      wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
 +      overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
 +      
 +      wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
 +      if (buttons) {
 +              overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
 +      }
 +
 +      SetSizer (overall_sizer);
 +      overall_sizer->Layout ();
 +      overall_sizer->SetSizeHints (this);
 +
 +      _certificate_load->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ScreenDialog::load_certificate), 0, this);
 +}
 +
 +string
 +ScreenDialog::name () const
 +{
 +      return wx_to_std (_name->GetValue());
 +}
 +
 +shared_ptr<libdcp::Certificate>
 +ScreenDialog::certificate () const
 +{
 +      return _certificate;
 +}
 +
 +void
 +ScreenDialog::load_certificate (wxCommandEvent &)
 +{
 +      wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
 +      d->ShowModal ();
 +      
 +      try {
 +              _certificate.reset (new libdcp::Certificate (wx_to_std (d->GetPath ())));
 +              _certificate_text->SetValue (_certificate->certificate ());
 +      } catch (libdcp::MiscError& e) {
 +              error_dialog (this, String::compose ("Could not read certificate file (%1)", e.what()));
 +      }
 +
 +      d->Destroy ();
 +}
diff --combined src/wx/wscript
index 82d9d3738a7cb4282c5637033f64e35b9412004f,9c3ecdd71eacf10f9cc682055cfe66bc7eb94e86..8f35e2facea49aa784b761ef4dbb3cc341f3b537
@@@ -1,5 -1,63 +1,66 @@@
+ import os
+ import glob
+ from waflib import Logs
+ import i18n
+ sources = """
+           about_dialog.cc
+           audio_dialog.cc
+           audio_mapping_view.cc
+           audio_panel.cc
+           audio_plot.cc
++          cinema_dialog.cc
+           colour_conversion_editor.cc
+           config_dialog.cc
+           content_colour_conversion_dialog.cc
+           content_menu.cc
+           dci_metadata_dialog.cc
+           dir_picker_ctrl.cc
+           film_editor.cc
+           film_editor_panel.cc
+           film_viewer.cc
+           filter_dialog.cc
+           filter_editor.cc
+           gain_calculator_dialog.cc
+           job_manager_view.cc
+           job_wrapper.cc
++          kdm_dialog.cc
+           new_film_dialog.cc
+           preset_colour_conversion_dialog.cc
+           properties_dialog.cc
+           repeat_dialog.cc
++          screen_dialog.cc
+           server_dialog.cc
+           subtitle_panel.cc
+           timecode.cc
+           timeline.cc
+           timeline_dialog.cc
+           timing_panel.cc
+           video_panel.cc
+           wx_util.cc
+           wx_ui_signaller.cc
+           """
  def configure(conf):
-     conf.check_cfg(package = '', path = 'wx-config', args = '--cppflags --cxxflags --libs', uselib_store = 'WXWIDGETS', mandatory = True)
+     args = '--cppflags --cxxflags'
+     if not conf.env.STATIC:
+         args += ' --libs'
+     conf.check_cfg(msg='Checking for wxWidgets', package='', path=conf.options.wx_config, args=args,
+                    uselib_store='WXWIDGETS', mandatory=True)
+     if conf.env.STATIC:
+       # wx-config returns its static libraries as full paths, without -l prefixes, which confuses
+         # check_cfg(), so just hard-code it all.
+         conf.env.STLIB_WXWIDGETS = ['wx_gtk2u_xrc-2.9', 'wx_gtk2u_qa-2.9', 'wx_baseu_net-2.9', 'wx_gtk2u_html-2.9',
+                                     'wx_gtk2u_adv-2.9', 'wx_gtk2u_core-2.9', 'wx_baseu_xml-2.9', 'wx_baseu-2.9']
+         conf.env.LIB_WXWIDGETS = ['tiff', 'SM', 'dl', 'jpeg', 'png', 'X11']
+  
+     conf.in_msg = 1
+     wx_version = conf.check_cfg(package='', path=conf.options.wx_config, args='--version').strip()
+     conf.im_msg = 0
+     if wx_version != '2.9.4' and wx_version != '2.9.5':
+         conf.fatal('wxwidgets version 2.9.4 or 2.9.5 is required; %s found' % wx_version)
  
  def build(bld):
      if bld.env.STATIC:
      else:
          obj = bld(features = 'cxx cxxshlib')
  
-     obj.name   = 'libdvdomatic-wx'
-     obj.includes = [ '..' ]
-     obj.export_includes = ['.']
+     obj.name   = 'libdcpomatic-wx'
#    obj.includes = [ '..' ]
+     obj.export_includes = ['..']
      obj.uselib = 'WXWIDGETS'
-     obj.use = 'libdvdomatic'
-     obj.source = """
-                  config_dialog.cc
-                  dci_name_dialog.cc
-                  dir_picker_ctrl.cc
-                  film_editor.cc
-                  film_viewer.cc
-                  filter_dialog.cc
-                  filter_view.cc
-                  gain_calculator_dialog.cc
-                  job_manager_view.cc
-                  job_wrapper.cc
-                  kdm_dialog.cc
-                  cinema_dialog.cc
-                  new_film_dialog.cc
-                  screen_dialog.cc
-                  properties_dialog.cc
-                  server_dialog.cc
-                  wx_util.cc
-                  wx_ui_signaller.cc
-                  """
-     obj.target = 'dvdomatic-wx'
+     if bld.env.TARGET_LINUX:
+         obj.uselib += ' GTK'
+     obj.use = 'libdcpomatic'
+     obj.source = sources
+     obj.target = 'dcpomatic-wx'
+     i18n.po_to_mo(os.path.join('src', 'wx'), 'libdcpomatic-wx', bld)
+ def pot(bld):
+     i18n.pot(os.path.join('src', 'wx'), sources, 'libdcpomatic-wx')
+ def pot_merge(bld):
+     i18n.pot_merge(os.path.join('src', 'wx'), 'libdcpomatic-wx')