1219da3f525e83862a7e9065b64f7448c67aba61
[dcpomatic.git] / src / wx / markers_panel.cc
1 /*
2     Copyright (C) 2021-2022 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "film_viewer.h"
23 #include "markers.h"
24 #include "markers_panel.h"
25 #include "wx_util.h"
26 #include <wx/graphics.h>
27 #include <wx/tipwin.h>
28 #include <boost/bind/bind.hpp>
29 #include <boost/version.hpp>
30
31
32 using std::shared_ptr;
33 using std::weak_ptr;
34 #if BOOST_VERSION >= 106100
35 using namespace boost::placeholders;
36 #endif
37
38
39 enum {
40         ID_move_marker_to_current_position,
41         ID_remove_marker,
42         ID_add_marker,
43         /* Leave some space after this one as we use an ID for each marker type
44          * starting with ID_add_base.
45          */
46         ID_add_base,
47 };
48
49
50 static constexpr auto line_to_label_gap = 2;
51
52
53 MarkersPanel::MarkersPanel (wxWindow* parent, weak_ptr<FilmViewer> viewer)
54         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 16))
55         , _viewer (viewer)
56 {
57         Bind (wxEVT_PAINT, boost::bind(&MarkersPanel::paint, this));
58         Bind (wxEVT_MOTION, boost::bind(&MarkersPanel::mouse_moved, this, _1));
59
60         Bind (wxEVT_LEFT_DOWN, boost::bind(&MarkersPanel::mouse_left_down, this));
61         Bind (wxEVT_RIGHT_DOWN, boost::bind(&MarkersPanel::mouse_right_down, this, _1));
62
63         Bind (wxEVT_MENU, boost::bind(&MarkersPanel::move_marker_to_current_position, this), ID_move_marker_to_current_position);
64         Bind (wxEVT_MENU, boost::bind(&MarkersPanel::remove_marker, this), ID_remove_marker);
65         Bind (wxEVT_MENU, boost::bind(&MarkersPanel::add_marker, this, _1), ID_add_base, ID_add_base + all_markers().size());
66 }
67
68
69 void
70 MarkersPanel::set_film (weak_ptr<Film> weak_film)
71 {
72         _film = weak_film;
73         auto film = weak_film.lock ();
74         if (film) {
75                 film->Change.connect (boost::bind(&MarkersPanel::film_changed, this, _1, _2));
76                 update_from_film (film);
77         }
78 }
79
80
81 void
82 MarkersPanel::film_changed (ChangeType type, Film::Property property)
83 {
84         if (type != ChangeType::DONE) {
85                 return;
86         }
87
88         auto film = _film.lock();
89         if (!film) {
90                 return;
91         }
92
93         if (property == Film::Property::MARKERS || property == Film::Property::CONTENT || property == Film::Property::CONTENT_ORDER || property == Film::Property::VIDEO_FRAME_RATE) {
94                 update_from_film (film);
95         }
96 }
97
98
99 void
100 MarkersPanel::update_from_film (shared_ptr<Film> film)
101 {
102         _markers.clear ();
103         for (auto const& marker: film->markers()) {
104                 _markers[marker.first] = Marker(
105                         marker.second,
106                         marker.first == dcp::Marker::FFOC ||
107                         marker.first == dcp::Marker::FFTC ||
108                         marker.first == dcp::Marker::FFOI ||
109                         marker.first == dcp::Marker::FFEC ||
110                         marker.first == dcp::Marker::FFMC
111                         );
112
113         }
114         Refresh ();
115 }
116
117
118 int
119 MarkersPanel::position (dcpomatic::DCPTime time, int width) const
120 {
121 #ifdef DCPOMATIC_LINUX
122         /* Number of pixels between the left/right bounding box edge of a wxSlider
123          * and the start of the "track".
124          */
125         auto constexpr end_gap = 12;
126 #else
127         auto constexpr end_gap = 0;
128 #endif
129         auto film = _film.lock ();
130         if (!film) {
131                 return 0;
132         }
133
134         return end_gap + time.get() * (width - end_gap * 2) / film->length().get();
135 }
136
137
138 void
139 MarkersPanel::mouse_moved (wxMouseEvent& ev)
140 {
141         _over = boost::none;
142
143         auto film = _film.lock ();
144         if (!film) {
145                 return;
146         }
147
148         auto const panel_width = GetSize().GetWidth();
149 #if !defined(DCPOMATIC_LINUX)
150         auto const panel_height = GetSize().GetHeight();
151         auto const factor = GetContentScaleFactor();
152 #endif
153
154         auto const x = ev.GetPosition().x;
155         for (auto const& marker: _markers) {
156                 auto const pos = position(marker.second.time, panel_width);
157                 auto const width = marker.second.width ? marker.second.width : 4;
158                 auto const x1 = marker.second.line_before_label ? pos : pos - width - line_to_label_gap;
159                 auto const x2 = marker.second.line_before_label ? pos + width + line_to_label_gap : pos;
160                 if (x1 <= x && x < x2) {
161                         _over = marker.first;
162 /* Tooltips flicker really badly on Wayland for some reason, so only do this on Windows/macOS for now */
163 #if !defined(DCPOMATIC_LINUX)
164                         if (!_tip) {
165                                 auto mouse = ClientToScreen (ev.GetPosition());
166                                 auto rect = wxRect(mouse.x, mouse.y, width * factor, panel_height * factor);
167                                 auto hmsf = marker.second.time.split(film->video_frame_rate());
168                                 char timecode_buffer[64];
169                                 snprintf (timecode_buffer, sizeof(timecode_buffer), " %02d:%02d:%02d:%02d", hmsf.h, hmsf.m, hmsf.s, hmsf.f);
170                                 auto tip_text = dcp::marker_to_string(marker.first) + std::string(timecode_buffer);
171                                 _tip = new wxTipWindow (this, std_to_wx(tip_text), 100, &_tip, &rect);
172                         }
173 #endif
174                 }
175         }
176 }
177
178
179 void
180 MarkersPanel::paint ()
181 {
182         wxPaintDC dc (this);
183
184         std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
185         if (!gc) {
186                 return;
187         }
188
189         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
190         gc->SetPen (wxPen(wxColour(200, 0, 0)));
191         gc->SetFont (gc->CreateFont(*wxSMALL_FONT, wxColour(200, 0, 0)));
192
193         auto const panel_width = GetSize().GetWidth();
194         auto const panel_height = GetSize().GetHeight();
195
196         for (auto& marker: _markers) {
197                 auto label = std_to_wx(dcp::marker_to_string(marker.first));
198                 if (marker.second.width == 0) {
199                         /* We don't know the width of this marker label yet, so calculate it now */
200                         wxDouble width, height, descent, external_leading;
201                         gc->GetTextExtent (label, &width, &height, &descent, &external_leading);
202                         marker.second.width = width;
203                 }
204                 auto line = gc->CreatePath ();
205                 auto const pos = position(marker.second.time, panel_width);
206                 line.MoveToPoint (pos, 0);
207                 line.AddLineToPoint (pos, panel_height);
208                 gc->StrokePath (line);
209                 if (marker.second.line_before_label) {
210                         gc->DrawText (label, pos + line_to_label_gap, 0);
211                 } else {
212                         gc->DrawText (label, pos - line_to_label_gap - marker.second.width, 0);
213                 }
214         }
215 }
216
217
218 void
219 MarkersPanel::mouse_left_down ()
220 {
221         if (_over) {
222                 auto viewer = _viewer.lock ();
223                 DCPOMATIC_ASSERT (viewer);
224                 viewer->seek (_markers[*_over].time, true);
225         }
226 }
227
228
229 void
230 MarkersPanel::mouse_right_down (wxMouseEvent& ev)
231 {
232         wxMenu menu;
233         if (_over) {
234                 menu.Append (ID_move_marker_to_current_position, wxString::Format(_("Move %s marker to current position"), wx_to_std(dcp::marker_to_string(*_over))));
235                 menu.Append (ID_remove_marker, wxString::Format(_("Remove %s marker"), wx_to_std(dcp::marker_to_string(*_over))));
236         }
237
238         auto add_marker = new wxMenu ();
239         for (auto const& marker: all_markers()) {
240                 add_marker->Append (static_cast<int>(ID_add_base) + static_cast<int>(marker.second), marker.first);
241         }
242         menu.Append (ID_add_marker, _("Add or move marker to current position"), add_marker);
243
244         _menu_marker = _over;
245         PopupMenu (&menu, ev.GetPosition());
246 }
247
248
249 void
250 MarkersPanel::move_marker_to_current_position ()
251 {
252         auto film = _film.lock ();
253         auto viewer = _viewer.lock ();
254         if (!film || !viewer || !_menu_marker) {
255                 return;
256         }
257
258         film->set_marker (*_menu_marker, viewer->position());
259 }
260
261
262 void
263 MarkersPanel::remove_marker ()
264 {
265         auto film = _film.lock ();
266         auto viewer = _viewer.lock ();
267         if (!film || !viewer || !_menu_marker) {
268                 return;
269         }
270
271         film->unset_marker (*_menu_marker);
272 }
273
274
275 void
276 MarkersPanel::add_marker (wxCommandEvent& ev)
277 {
278         auto film = _film.lock ();
279         auto viewer = _viewer.lock ();
280         if (!film || !viewer) {
281                 return;
282         }
283
284         auto marker = static_cast<dcp::Marker>(ev.GetId() - ID_add_base);
285         film->set_marker (marker, viewer->position());
286 }
287