Supporters update.
[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 "id.h"
24 #include "markers.h"
25 #include "markers_panel.h"
26 #include "wx_util.h"
27 #include "lib/film.h"
28 #include <dcp/warnings.h>
29 LIBDCP_DISABLE_WARNINGS
30 #include <wx/graphics.h>
31 #include <wx/tipwin.h>
32 LIBDCP_ENABLE_WARNINGS
33 #include <boost/bind/bind.hpp>
34 #include <boost/version.hpp>
35
36
37 using std::shared_ptr;
38 using std::weak_ptr;
39 #if BOOST_VERSION >= 106100
40 using namespace boost::placeholders;
41 #endif
42
43
44 enum {
45         ID_move_marker_to_current_position = DCPOMATIC_MARKERS_PANEL_MENU,
46         ID_remove_marker,
47         ID_add_marker,
48         /* Leave some space after this one as we use an ID for each marker type
49          * starting with ID_add_base.
50          */
51         ID_add_base,
52 };
53
54
55 static constexpr auto line_to_label_gap = 2;
56
57
58 MarkersPanel::MarkersPanel(wxWindow* parent, FilmViewer& viewer)
59         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 16))
60         , _viewer (viewer)
61 {
62         Bind (wxEVT_PAINT, boost::bind(&MarkersPanel::paint, this));
63         Bind (wxEVT_MOTION, boost::bind(&MarkersPanel::mouse_moved, this, _1));
64
65         Bind (wxEVT_LEFT_DOWN, boost::bind(&MarkersPanel::mouse_left_down, this));
66         Bind (wxEVT_RIGHT_DOWN, boost::bind(&MarkersPanel::mouse_right_down, this, _1));
67
68         Bind (wxEVT_MENU, boost::bind(&MarkersPanel::move_marker_to_current_position, this), ID_move_marker_to_current_position);
69         Bind (wxEVT_MENU, boost::bind(&MarkersPanel::remove_marker, this), ID_remove_marker);
70         Bind(wxEVT_MENU, boost::bind(&MarkersPanel::add_marker, this, _1), ID_add_base, ID_add_base + all_editable_markers().size() + uneditable_markers);
71 }
72
73
74 void
75 MarkersPanel::set_film (weak_ptr<Film> weak_film)
76 {
77         _film = weak_film;
78         auto film = weak_film.lock ();
79         if (film) {
80                 film->Change.connect (boost::bind(&MarkersPanel::film_changed, this, _1, _2));
81                 update_from_film (film);
82         }
83 }
84
85
86 void
87 MarkersPanel::film_changed(ChangeType type, FilmProperty property)
88 {
89         if (type != ChangeType::DONE) {
90                 return;
91         }
92
93         auto film = _film.lock();
94         if (!film) {
95                 return;
96         }
97
98         if (property == FilmProperty::MARKERS || property == FilmProperty::CONTENT || property == FilmProperty::CONTENT_ORDER || property == FilmProperty::VIDEO_FRAME_RATE) {
99                 update_from_film (film);
100         }
101 }
102
103
104 void
105 MarkersPanel::update_from_film (shared_ptr<Film> film)
106 {
107         _markers.clear ();
108         for (auto const& marker: film->markers()) {
109                 _markers[marker.first] = Marker(
110                         marker.second,
111                         marker.first == dcp::Marker::FFOC ||
112                         marker.first == dcp::Marker::FFTC ||
113                         marker.first == dcp::Marker::FFOI ||
114                         marker.first == dcp::Marker::FFEC ||
115                         marker.first == dcp::Marker::FFMC
116                         );
117
118         }
119         Refresh ();
120 }
121
122
123 int
124 MarkersPanel::position (dcpomatic::DCPTime time, int width) const
125 {
126 #ifdef DCPOMATIC_LINUX
127         /* Number of pixels between the left/right bounding box edge of a wxSlider
128          * and the start of the "track".
129          */
130         auto constexpr end_gap = 12;
131 #else
132         auto constexpr end_gap = 0;
133 #endif
134         auto film = _film.lock ();
135         if (!film) {
136                 return 0;
137         }
138
139         return end_gap + time.get() * (width - end_gap * 2) / film->length().get();
140 }
141
142
143 void
144 MarkersPanel::mouse_moved (wxMouseEvent& ev)
145 {
146         _over = boost::none;
147
148         auto film = _film.lock ();
149         if (!film) {
150                 return;
151         }
152
153         auto const panel_width = GetSize().GetWidth();
154 #if !defined(DCPOMATIC_LINUX)
155         auto const panel_height = GetSize().GetHeight();
156         auto const factor = GetContentScaleFactor();
157 #endif
158
159         auto const x = ev.GetPosition().x;
160         for (auto const& marker: _markers) {
161                 auto const pos = position(marker.second.time, panel_width);
162                 auto const width = marker.second.width ? marker.second.width : 4;
163                 auto const x1 = marker.second.line_before_label ? pos : pos - width - line_to_label_gap;
164                 auto const x2 = marker.second.line_before_label ? pos + width + line_to_label_gap : pos;
165                 if (x1 <= x && x < x2) {
166                         _over = marker.first;
167 /* Tooltips flicker really badly on Wayland for some reason, so only do this on Windows/macOS for now */
168 #if !defined(DCPOMATIC_LINUX)
169                         if (!_tip) {
170                                 auto mouse = ClientToScreen (ev.GetPosition());
171                                 auto rect = wxRect(mouse.x, mouse.y, width * factor, panel_height * factor);
172                                 auto hmsf = marker.second.time.split(film->video_frame_rate());
173                                 char timecode_buffer[64];
174                                 snprintf (timecode_buffer, sizeof(timecode_buffer), " %02d:%02d:%02d:%02d", hmsf.h, hmsf.m, hmsf.s, hmsf.f);
175                                 auto tip_text = dcp::marker_to_string(marker.first) + std::string(timecode_buffer);
176                                 _tip = new wxTipWindow (this, std_to_wx(tip_text), 100, &_tip, &rect);
177                         }
178 #endif
179                 }
180         }
181 }
182
183
184 void
185 MarkersPanel::paint ()
186 {
187         wxPaintDC dc (this);
188
189         std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
190         if (!gc) {
191                 return;
192         }
193
194         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
195         gc->SetPen (wxPen(wxColour(200, 0, 0)));
196         gc->SetFont (gc->CreateFont(*wxSMALL_FONT, wxColour(200, 0, 0)));
197
198         auto const panel_width = GetSize().GetWidth();
199         auto const panel_height = GetSize().GetHeight();
200
201         for (auto& marker: _markers) {
202                 auto label = std_to_wx(dcp::marker_to_string(marker.first));
203                 if (marker.second.width == 0) {
204                         /* We don't know the width of this marker label yet, so calculate it now */
205                         wxDouble width, height, descent, external_leading;
206                         gc->GetTextExtent (label, &width, &height, &descent, &external_leading);
207                         marker.second.width = width;
208                 }
209                 auto line = gc->CreatePath ();
210                 auto const pos = position(marker.second.time, panel_width);
211                 line.MoveToPoint (pos, 0);
212                 line.AddLineToPoint (pos, panel_height);
213                 gc->StrokePath (line);
214
215                 auto label_x = 0;
216
217                 if (GetLayoutDirection() == wxLayout_RightToLeft) {
218                         auto matrix = dc.GetTransformMatrix();
219                         matrix.Translate(0, 0);
220                         matrix.Mirror(wxHORIZONTAL);
221                         dc.SetTransformMatrix(matrix);
222                         label_x = marker.second.line_before_label ? (pos + line_to_label_gap + marker.second.width) : (pos - line_to_label_gap);
223                         label_x = -label_x;
224                 } else {
225                         label_x = marker.second.line_before_label ? (pos + line_to_label_gap) : (pos - line_to_label_gap - marker.second.width);
226                 }
227
228                 gc->DrawText(label, label_x, 0);
229
230                 dc.ResetTransformMatrix();
231         }
232 }
233
234
235 void
236 MarkersPanel::mouse_left_down ()
237 {
238         if (_over) {
239                 _viewer.seek(_markers[*_over].time, true);
240         }
241 }
242
243
244 void
245 MarkersPanel::mouse_right_down (wxMouseEvent& ev)
246 {
247         wxMenu menu;
248         if (_over) {
249                 menu.Append (ID_move_marker_to_current_position, wxString::Format(_("Move %s marker to current position"), wx_to_std(dcp::marker_to_string(*_over))));
250                 menu.Append (ID_remove_marker, wxString::Format(_("Remove %s marker"), wx_to_std(dcp::marker_to_string(*_over))));
251         }
252
253         auto add_marker = new wxMenu ();
254         for (auto const& marker: all_editable_markers()) {
255                 add_marker->Append (static_cast<int>(ID_add_base) + static_cast<int>(marker.second), marker.first);
256         }
257         menu.Append (ID_add_marker, _("Add or move marker to current position"), add_marker);
258
259         _menu_marker = _over;
260         PopupMenu (&menu, ev.GetPosition());
261 }
262
263
264 void
265 MarkersPanel::move_marker_to_current_position ()
266 {
267         auto film = _film.lock ();
268         if (!film || !_menu_marker) {
269                 return;
270         }
271
272         film->set_marker(*_menu_marker, _viewer.position());
273 }
274
275
276 void
277 MarkersPanel::remove_marker ()
278 {
279         auto film = _film.lock ();
280         if (!film || !_menu_marker) {
281                 return;
282         }
283
284         film->unset_marker(*_menu_marker);
285 }
286
287
288 void
289 MarkersPanel::add_marker (wxCommandEvent& ev)
290 {
291         auto film = _film.lock ();
292         if (!film) {
293                 return;
294         }
295
296         auto marker = static_cast<dcp::Marker>(ev.GetId() - ID_add_base);
297         film->set_marker(marker, _viewer.position());
298 }
299