Basic display of markers above the playback timeline (#1921).
authorCarl Hetherington <cth@carlh.net>
Mon, 10 Jan 2022 11:53:28 +0000 (12:53 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 28 Apr 2022 23:10:11 +0000 (01:10 +0200)
src/wx/controls.cc
src/wx/controls.h
src/wx/markers_panel.cc [new file with mode: 0644]
src/wx/markers_panel.h [new file with mode: 0644]
src/wx/wscript

index ba069126877b95fae069125f1b46b0d3049ee333..a6f70c039528caa9c1197b6fe5e01c0804145034 100644 (file)
@@ -24,6 +24,7 @@
 #include "controls.h"
 #include "dcpomatic_button.h"
 #include "film_viewer.h"
+#include "markers_panel.h"
 #include "playhead_to_frame_dialog.h"
 #include "playhead_to_timecode_dialog.h"
 #include "static_text.h"
@@ -65,6 +66,7 @@ using namespace dcpomatic;
 
 Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor_controls)
        : wxPanel (parent)
+       , _markers (new MarkersPanel(this, viewer))
        , _slider (new wxSlider(this, wxID_ANY, 0, 0, 4096))
        , _viewer (viewer)
        , _slider_being_moved (false)
@@ -110,7 +112,12 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor
        _button_sizer = new wxBoxSizer (wxHORIZONTAL);
        h_sizer->Add (_button_sizer, 0, wxEXPAND);
 
-       h_sizer->Add (_slider, 1, wxEXPAND);
+       {
+               auto box = new wxBoxSizer (wxVERTICAL);
+               box->Add (_markers, 0, wxEXPAND);
+               box->Add (_slider, 0, wxEXPAND);
+               h_sizer->Add (box, 1, wxEXPAND);
+       }
 
        _v_sizer->Add (h_sizer, 0, wxEXPAND | wxALL, 6);
 
@@ -469,6 +476,8 @@ Controls::set_film (shared_ptr<Film> film)
 
        _film = film;
 
+       _markers->set_film (_film);
+
        if (_film) {
                _film_change_connection = _film->Change.connect (boost::bind(&Controls::film_change, this, _1, _2));
        }
index 0341da29009febe7c8878cdaad1981d787e60d3f..5f7dc387a252e549c38ecce3f1ca6c3330f624b3 100644 (file)
@@ -38,7 +38,9 @@ class Content;
 class ContentView;
 class Film;
 class FilmViewer;
+class MarkersPanel;
 class PlayerVideo;
+
 class wxListCtrl;
 class wxToggleButton;
 
@@ -77,6 +79,7 @@ protected:
        wxSizer* _v_sizer;
        wxBoxSizer* _button_sizer;
        std::shared_ptr<Film> _film;
+       MarkersPanel* _markers;
        wxSlider* _slider;
        std::weak_ptr<FilmViewer> _viewer;
        boost::optional<std::string> _active_job;
diff --git a/src/wx/markers_panel.cc b/src/wx/markers_panel.cc
new file mode 100644 (file)
index 0000000..1219da3
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+    Copyright (C) 2021-2022 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "film_viewer.h"
+#include "markers.h"
+#include "markers_panel.h"
+#include "wx_util.h"
+#include <wx/graphics.h>
+#include <wx/tipwin.h>
+#include <boost/bind/bind.hpp>
+#include <boost/version.hpp>
+
+
+using std::shared_ptr;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+enum {
+       ID_move_marker_to_current_position,
+       ID_remove_marker,
+       ID_add_marker,
+       /* Leave some space after this one as we use an ID for each marker type
+        * starting with ID_add_base.
+        */
+       ID_add_base,
+};
+
+
+static constexpr auto line_to_label_gap = 2;
+
+
+MarkersPanel::MarkersPanel (wxWindow* parent, weak_ptr<FilmViewer> viewer)
+       : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 16))
+       , _viewer (viewer)
+{
+       Bind (wxEVT_PAINT, boost::bind(&MarkersPanel::paint, this));
+       Bind (wxEVT_MOTION, boost::bind(&MarkersPanel::mouse_moved, this, _1));
+
+       Bind (wxEVT_LEFT_DOWN, boost::bind(&MarkersPanel::mouse_left_down, this));
+       Bind (wxEVT_RIGHT_DOWN, boost::bind(&MarkersPanel::mouse_right_down, this, _1));
+
+       Bind (wxEVT_MENU, boost::bind(&MarkersPanel::move_marker_to_current_position, this), ID_move_marker_to_current_position);
+       Bind (wxEVT_MENU, boost::bind(&MarkersPanel::remove_marker, this), ID_remove_marker);
+       Bind (wxEVT_MENU, boost::bind(&MarkersPanel::add_marker, this, _1), ID_add_base, ID_add_base + all_markers().size());
+}
+
+
+void
+MarkersPanel::set_film (weak_ptr<Film> weak_film)
+{
+       _film = weak_film;
+       auto film = weak_film.lock ();
+       if (film) {
+               film->Change.connect (boost::bind(&MarkersPanel::film_changed, this, _1, _2));
+               update_from_film (film);
+       }
+}
+
+
+void
+MarkersPanel::film_changed (ChangeType type, Film::Property property)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       auto film = _film.lock();
+       if (!film) {
+               return;
+       }
+
+       if (property == Film::Property::MARKERS || property == Film::Property::CONTENT || property == Film::Property::CONTENT_ORDER || property == Film::Property::VIDEO_FRAME_RATE) {
+               update_from_film (film);
+       }
+}
+
+
+void
+MarkersPanel::update_from_film (shared_ptr<Film> film)
+{
+       _markers.clear ();
+       for (auto const& marker: film->markers()) {
+               _markers[marker.first] = Marker(
+                       marker.second,
+                       marker.first == dcp::Marker::FFOC ||
+                       marker.first == dcp::Marker::FFTC ||
+                       marker.first == dcp::Marker::FFOI ||
+                       marker.first == dcp::Marker::FFEC ||
+                       marker.first == dcp::Marker::FFMC
+                       );
+
+       }
+       Refresh ();
+}
+
+
+int
+MarkersPanel::position (dcpomatic::DCPTime time, int width) const
+{
+#ifdef DCPOMATIC_LINUX
+       /* Number of pixels between the left/right bounding box edge of a wxSlider
+        * and the start of the "track".
+        */
+       auto constexpr end_gap = 12;
+#else
+       auto constexpr end_gap = 0;
+#endif
+       auto film = _film.lock ();
+       if (!film) {
+               return 0;
+       }
+
+       return end_gap + time.get() * (width - end_gap * 2) / film->length().get();
+}
+
+
+void
+MarkersPanel::mouse_moved (wxMouseEvent& ev)
+{
+       _over = boost::none;
+
+       auto film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       auto const panel_width = GetSize().GetWidth();
+#if !defined(DCPOMATIC_LINUX)
+       auto const panel_height = GetSize().GetHeight();
+       auto const factor = GetContentScaleFactor();
+#endif
+
+       auto const x = ev.GetPosition().x;
+       for (auto const& marker: _markers) {
+               auto const pos = position(marker.second.time, panel_width);
+               auto const width = marker.second.width ? marker.second.width : 4;
+               auto const x1 = marker.second.line_before_label ? pos : pos - width - line_to_label_gap;
+               auto const x2 = marker.second.line_before_label ? pos + width + line_to_label_gap : pos;
+               if (x1 <= x && x < x2) {
+                       _over = marker.first;
+/* Tooltips flicker really badly on Wayland for some reason, so only do this on Windows/macOS for now */
+#if !defined(DCPOMATIC_LINUX)
+                       if (!_tip) {
+                               auto mouse = ClientToScreen (ev.GetPosition());
+                               auto rect = wxRect(mouse.x, mouse.y, width * factor, panel_height * factor);
+                               auto hmsf = marker.second.time.split(film->video_frame_rate());
+                               char timecode_buffer[64];
+                               snprintf (timecode_buffer, sizeof(timecode_buffer), " %02d:%02d:%02d:%02d", hmsf.h, hmsf.m, hmsf.s, hmsf.f);
+                               auto tip_text = dcp::marker_to_string(marker.first) + std::string(timecode_buffer);
+                               _tip = new wxTipWindow (this, std_to_wx(tip_text), 100, &_tip, &rect);
+                       }
+#endif
+               }
+       }
+}
+
+
+void
+MarkersPanel::paint ()
+{
+       wxPaintDC dc (this);
+
+       std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
+       if (!gc) {
+               return;
+       }
+
+       gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
+       gc->SetPen (wxPen(wxColour(200, 0, 0)));
+       gc->SetFont (gc->CreateFont(*wxSMALL_FONT, wxColour(200, 0, 0)));
+
+       auto const panel_width = GetSize().GetWidth();
+       auto const panel_height = GetSize().GetHeight();
+
+       for (auto& marker: _markers) {
+               auto label = std_to_wx(dcp::marker_to_string(marker.first));
+               if (marker.second.width == 0) {
+                       /* We don't know the width of this marker label yet, so calculate it now */
+                       wxDouble width, height, descent, external_leading;
+                       gc->GetTextExtent (label, &width, &height, &descent, &external_leading);
+                       marker.second.width = width;
+               }
+               auto line = gc->CreatePath ();
+               auto const pos = position(marker.second.time, panel_width);
+               line.MoveToPoint (pos, 0);
+               line.AddLineToPoint (pos, panel_height);
+               gc->StrokePath (line);
+               if (marker.second.line_before_label) {
+                       gc->DrawText (label, pos + line_to_label_gap, 0);
+               } else {
+                       gc->DrawText (label, pos - line_to_label_gap - marker.second.width, 0);
+               }
+       }
+}
+
+
+void
+MarkersPanel::mouse_left_down ()
+{
+       if (_over) {
+               auto viewer = _viewer.lock ();
+               DCPOMATIC_ASSERT (viewer);
+               viewer->seek (_markers[*_over].time, true);
+       }
+}
+
+
+void
+MarkersPanel::mouse_right_down (wxMouseEvent& ev)
+{
+       wxMenu menu;
+       if (_over) {
+               menu.Append (ID_move_marker_to_current_position, wxString::Format(_("Move %s marker to current position"), wx_to_std(dcp::marker_to_string(*_over))));
+               menu.Append (ID_remove_marker, wxString::Format(_("Remove %s marker"), wx_to_std(dcp::marker_to_string(*_over))));
+       }
+
+       auto add_marker = new wxMenu ();
+       for (auto const& marker: all_markers()) {
+               add_marker->Append (static_cast<int>(ID_add_base) + static_cast<int>(marker.second), marker.first);
+       }
+       menu.Append (ID_add_marker, _("Add or move marker to current position"), add_marker);
+
+       _menu_marker = _over;
+       PopupMenu (&menu, ev.GetPosition());
+}
+
+
+void
+MarkersPanel::move_marker_to_current_position ()
+{
+       auto film = _film.lock ();
+       auto viewer = _viewer.lock ();
+       if (!film || !viewer || !_menu_marker) {
+               return;
+       }
+
+       film->set_marker (*_menu_marker, viewer->position());
+}
+
+
+void
+MarkersPanel::remove_marker ()
+{
+       auto film = _film.lock ();
+       auto viewer = _viewer.lock ();
+       if (!film || !viewer || !_menu_marker) {
+               return;
+       }
+
+       film->unset_marker (*_menu_marker);
+}
+
+
+void
+MarkersPanel::add_marker (wxCommandEvent& ev)
+{
+       auto film = _film.lock ();
+       auto viewer = _viewer.lock ();
+       if (!film || !viewer) {
+               return;
+       }
+
+       auto marker = static_cast<dcp::Marker>(ev.GetId() - ID_add_base);
+       film->set_marker (marker, viewer->position());
+}
+
diff --git a/src/wx/markers_panel.h b/src/wx/markers_panel.h
new file mode 100644 (file)
index 0000000..b88efa9
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/dcpomatic_time.h"
+#include "lib/film.h"
+#include <wx/wx.h>
+#include <map>
+
+
+class wxTipWindow;
+
+
+class MarkersPanel : public wxPanel
+{
+public:
+       MarkersPanel (wxWindow* parent, std::weak_ptr<FilmViewer> viewer);
+
+       void set_film (std::weak_ptr<Film> film);
+
+private:
+       void paint ();
+       void mouse_moved (wxMouseEvent& ev);
+       void mouse_left_down ();
+       void mouse_right_down (wxMouseEvent& ev);
+       int position (dcpomatic::DCPTime time, int width) const;
+       void move_marker_to_current_position ();
+       void remove_marker ();
+       void add_marker (wxCommandEvent& ev);
+       void film_changed (ChangeType type, Film::Property property);
+       void update_from_film (std::shared_ptr<Film> film);
+
+       wxTipWindow* _tip = nullptr;
+
+       class Marker {
+       public:
+               Marker () {}
+
+               Marker (dcpomatic::DCPTime t, bool b)
+                       : time (t)
+                       , line_before_label (b)
+               {}
+
+               dcpomatic::DCPTime time;
+               int width = 0;
+               bool line_before_label = false;
+       };
+
+       std::weak_ptr<Film> _film;
+       std::map<dcp::Marker, Marker> _markers;
+       boost::optional<dcp::Marker> _over;
+       std::weak_ptr<FilmViewer> _viewer;
+       boost::optional<dcp::Marker> _menu_marker;
+};
+
index d2b4e58e228858287514c955fce0050aa2d3063f..276cfc6f06098cb39c07730bb60fbc6de7646cbb 100644 (file)
@@ -99,6 +99,7 @@ sources = """
           make_chain_dialog.cc
           markers.cc
           markers_dialog.cc
+          markers_panel.cc
           message_dialog.cc
           metadata_dialog.cc
           move_to_dialog.cc