]> git.sesse.net Git - nageru/blob - mainwindow.cpp
Make it possible to switch camera angles by clicking on the preview displays.
[nageru] / mainwindow.cpp
1 #include "mainwindow.h"
2
3 #include "clip_list.h"
4 #include "player.h"
5 #include "post_to_main_thread.h"
6 #include "ui_mainwindow.h"
7
8 #include <string>
9 #include <vector>
10
11 #include <QMouseEvent>
12 #include <QWheelEvent>
13 #include <QShortcut>
14
15 using namespace std;
16
17 MainWindow *global_mainwindow = nullptr;
18 ClipList *cliplist_clips;
19 PlayList *playlist_clips;
20
21 extern int64_t current_pts;
22 extern mutex frame_mu;
23 extern vector<int64_t> frames[MAX_STREAMS];
24
25 MainWindow::MainWindow()
26         : ui(new Ui::MainWindow)
27 {
28         global_mainwindow = this;
29         ui->setupUi(this);
30
31         cliplist_clips = new ClipList();
32         ui->clip_list->setModel(cliplist_clips);
33
34         playlist_clips = new PlayList();
35         ui->playlist->setModel(playlist_clips);
36
37         // For scrubbing in the pts columns.
38         ui->clip_list->viewport()->installEventFilter(this);
39         ui->playlist->viewport()->installEventFilter(this);
40
41         QShortcut *cue_in = new QShortcut(QKeySequence(Qt::Key_A), this);
42         connect(cue_in, &QShortcut::activated, ui->cue_in_btn, &QPushButton::click);
43         connect(ui->cue_in_btn, &QPushButton::clicked, this, &MainWindow::cue_in_clicked);
44
45         QShortcut *cue_out = new QShortcut(QKeySequence(Qt::Key_S), this);
46         connect(cue_out, &QShortcut::activated, ui->cue_out_btn, &QPushButton::click);
47         connect(ui->cue_out_btn, &QPushButton::clicked, this, &MainWindow::cue_out_clicked);
48
49         QShortcut *queue = new QShortcut(QKeySequence(Qt::Key_Q), this);
50         connect(queue, &QShortcut::activated, ui->queue_btn, &QPushButton::click);
51         connect(ui->queue_btn, &QPushButton::clicked, this, &MainWindow::queue_clicked);
52
53         QShortcut *preview = new QShortcut(QKeySequence(Qt::Key_W), this);
54         connect(preview, &QShortcut::activated, ui->preview_btn, &QPushButton::click);
55         connect(ui->preview_btn, &QPushButton::clicked, this, &MainWindow::preview_clicked);
56
57         QShortcut *play = new QShortcut(QKeySequence(Qt::Key_Space), this);
58         connect(play, &QShortcut::activated, ui->play_btn, &QPushButton::click);
59         connect(ui->play_btn, &QPushButton::clicked, this, &MainWindow::play_clicked);
60
61         QShortcut *preview_1 = new QShortcut(QKeySequence(Qt::Key_1), this);
62         connect(preview_1, &QShortcut::activated, ui->preview_1_btn, &QPushButton::click);
63         connect(ui->input1_display, &JPEGFrameView::clicked, ui->preview_1_btn, &QPushButton::click);
64         connect(ui->preview_1_btn, &QPushButton::clicked, [this]{ preview_angle_clicked(0); });
65
66         QShortcut *preview_2 = new QShortcut(QKeySequence(Qt::Key_2), this);
67         connect(preview_2, &QShortcut::activated, ui->preview_2_btn, &QPushButton::click);
68         connect(ui->input2_display, &JPEGFrameView::clicked, ui->preview_2_btn, &QPushButton::click);
69         connect(ui->preview_2_btn, &QPushButton::clicked, [this]{ preview_angle_clicked(1); });
70
71         QShortcut *preview_3 = new QShortcut(QKeySequence(Qt::Key_3), this);
72         connect(preview_3, &QShortcut::activated, ui->preview_3_btn, &QPushButton::click);
73         connect(ui->input3_display, &JPEGFrameView::clicked, ui->preview_3_btn, &QPushButton::click);
74         connect(ui->preview_3_btn, &QPushButton::clicked, [this]{ preview_angle_clicked(2); });
75
76         QShortcut *preview_4 = new QShortcut(QKeySequence(Qt::Key_4), this);
77         connect(preview_4, &QShortcut::activated, ui->preview_4_btn, &QPushButton::click);
78         connect(ui->input4_display, &JPEGFrameView::clicked, ui->preview_4_btn, &QPushButton::click);
79         connect(ui->preview_4_btn, &QPushButton::clicked, [this]{ preview_angle_clicked(3); });
80
81         connect(ui->playlist_duplicate_btn, &QPushButton::clicked, this, &MainWindow::playlist_duplicate);
82
83         // TODO: support the delete key iff the widget has focus?
84         connect(ui->playlist_remove_btn, &QPushButton::clicked, this, &MainWindow::playlist_remove);
85
86         // TODO: support drag-and-drop.
87         connect(ui->playlist_move_up_btn, &QPushButton::clicked, [this]{ playlist_move(-1); });
88         connect(ui->playlist_move_down_btn, &QPushButton::clicked, [this]{ playlist_move(1); });
89
90         connect(ui->playlist->selectionModel(), &QItemSelectionModel::selectionChanged,
91                 this, &MainWindow::playlist_selection_changed);
92         playlist_selection_changed();  // First time set-up.
93
94         preview_player = new Player(ui->preview_display, /*also_output_to_stream=*/false);
95         live_player = new Player(ui->live_display, /*also_output_to_stream=*/true);
96         live_player->set_done_callback([this]{
97                 post_to_main_thread([this]{
98                         live_player_clip_done();
99                 });
100         });
101 }
102
103 void MainWindow::cue_in_clicked()
104 {
105         if (!cliplist_clips->empty() && cliplist_clips->back()->pts_out < 0) {
106                 cliplist_clips->back()->pts_in = current_pts;
107                 return;
108         }
109         Clip clip;
110         clip.pts_in = current_pts;
111         cliplist_clips->add_clip(clip);
112         playlist_selection_changed();
113 }
114
115 void MainWindow::cue_out_clicked()
116 {
117         if (!cliplist_clips->empty()) {
118                 cliplist_clips->back()->pts_out = current_pts;
119                 // TODO: select the row in the clip list?
120         }
121 }
122
123 void MainWindow::queue_clicked()
124 {
125         if (cliplist_clips->empty()) {
126                 return;
127         }
128
129         QItemSelectionModel *selected = ui->clip_list->selectionModel();
130         if (!selected->hasSelection()) {
131                 Clip clip = *cliplist_clips->back();
132                 clip.stream_idx = 0;
133                 playlist_clips->add_clip(clip);
134                 return;
135         }
136
137         QModelIndex index = selected->currentIndex();
138         if (index.column() >= int(ClipList::Column::CAMERA_1) &&
139             index.column() <= int(ClipList::Column::CAMERA_4)) {
140                 Clip clip = *cliplist_clips->clip(index.row());
141                 clip.stream_idx = index.column() - int(ClipList::Column::CAMERA_1);
142                 playlist_clips->add_clip(clip);
143                 playlist_selection_changed();
144         }
145 }
146
147 void MainWindow::preview_clicked()
148 {
149         if (cliplist_clips->empty()) return;
150
151         QItemSelectionModel *selected = ui->clip_list->selectionModel();
152         if (!selected->hasSelection()) {
153                 preview_player->play_clip(*cliplist_clips->back(), 0);
154                 return;
155         }
156
157         QModelIndex index = selected->currentIndex();
158         if (index.column() >= int(ClipList::Column::CAMERA_1) &&
159             index.column() <= int(ClipList::Column::CAMERA_4)) {
160                 unsigned stream_idx = index.column() - int(ClipList::Column::CAMERA_1);
161                 preview_player->play_clip(*cliplist_clips->clip(index.row()), stream_idx);
162         }
163 }
164
165 void MainWindow::preview_angle_clicked(unsigned stream_idx)
166 {
167         preview_player->override_angle(stream_idx);
168
169         // Change the selection if we were previewing a clip from the clip list.
170         // (The only other thing we could be showing is a pts scrub, and if so,
171         // that would be selected.)
172         QItemSelectionModel *selected = ui->clip_list->selectionModel();
173         if (selected->hasSelection()) {
174                 QModelIndex cell = selected->selectedIndexes()[0];
175                 int column = int(ClipList::Column::CAMERA_1) + stream_idx;
176                 selected->setCurrentIndex(cell.sibling(cell.row(), column), QItemSelectionModel::ClearAndSelect);
177         }
178 }
179
180 void MainWindow::playlist_duplicate()
181 {
182         QItemSelectionModel *selected = ui->playlist->selectionModel();
183         if (!selected->hasSelection()) {
184                 // Should have been grayed out, but OK.
185                 return;
186         }
187         QModelIndexList rows = selected->selectedRows();
188         int first = rows.front().row(), last = rows.back().row();
189         playlist_clips->duplicate_clips(first, last);
190         playlist_selection_changed();
191 }
192
193 void MainWindow::playlist_remove()
194 {
195         QItemSelectionModel *selected = ui->playlist->selectionModel();
196         if (!selected->hasSelection()) {
197                 // Should have been grayed out, but OK.
198                 return;
199         }
200         QModelIndexList rows = selected->selectedRows();
201         int first = rows.front().row(), last = rows.back().row();
202         playlist_clips->erase_clips(first, last);
203
204         // TODO: select the next one in the list?
205
206         playlist_selection_changed();
207 }
208
209 void MainWindow::playlist_move(int delta)
210 {
211         QItemSelectionModel *selected = ui->playlist->selectionModel();
212         if (!selected->hasSelection()) {
213                 // Should have been grayed out, but OK.
214                 return;
215         }
216
217         QModelIndexList rows = selected->selectedRows();
218         int first = rows.front().row(), last = rows.back().row();
219         if ((delta == -1 && first == 0) ||
220             (delta == 1 && size_t(last) == playlist_clips->size() - 1)) {
221                 // Should have been grayed out, but OK.
222                 return;
223         }
224
225         playlist_clips->move_clips(first, last, delta);
226         playlist_selection_changed();
227 }
228
229 void MainWindow::play_clicked()
230 {
231         if (playlist_clips->empty()) return;
232
233         QItemSelectionModel *selected = ui->playlist->selectionModel();
234         int row;
235         if (!selected->hasSelection()) {
236                 row = 0;
237         } else {
238                 row = selected->selectedRows(0)[0].row();
239         }
240
241         const Clip &clip = *playlist_clips->clip(row);
242         live_player->play_clip(clip, clip.stream_idx);
243         playlist_clips->set_currently_playing(row);
244         playlist_selection_changed();
245 }
246
247 void MainWindow::live_player_clip_done()
248 {
249         int row = playlist_clips->get_currently_playing();
250         if (row != -1 && row < int(playlist_clips->size()) - 1) {
251                 ++row;
252                 const Clip &clip = *playlist_clips->clip(row);
253                 live_player->play_clip(clip, clip.stream_idx);
254                 playlist_clips->set_currently_playing(row);
255         } else {
256                 playlist_clips->set_currently_playing(-1);
257         }
258 }
259
260 void MainWindow::resizeEvent(QResizeEvent *event)
261 {
262         QMainWindow::resizeEvent(event);
263
264         // Ask for a relayout, but only after the event loop is done doing relayout
265         // on everything else.
266         QMetaObject::invokeMethod(this, "relayout", Qt::QueuedConnection);
267 }
268
269 void MainWindow::relayout()
270 {
271         ui->live_display->setMinimumHeight(ui->live_display->width() * 9 / 16);
272 }
273
274 bool MainWindow::eventFilter(QObject *watched, QEvent *event)
275 {
276         constexpr int dead_zone_pixels = 3;  // To avoid that simple clicks get misinterpreted.
277         constexpr int scrub_sensitivity = 100;  // pts units per pixel.
278         constexpr int wheel_sensitivity = 100;  // pts units per degree.
279         constexpr int camera_degrees_per_pixel = 15;  // One click of most mice.
280
281         unsigned stream_idx = ui->preview_display->get_stream_idx();
282
283         if (event->type() != QEvent::Wheel) {
284                 last_mousewheel_camera_row = -1;
285         }
286
287         if (event->type() == QEvent::MouseButtonPress) {
288                 QMouseEvent *mouse = (QMouseEvent *)event;
289
290                 QTableView *destination;
291                 ScrubType type;
292
293                 if (watched == ui->clip_list->viewport()) {
294                         destination = ui->clip_list;
295                         type = SCRUBBING_CLIP_LIST;
296                 } else if (watched == ui->playlist->viewport()) {
297                         destination = ui->playlist;
298                         type = SCRUBBING_PLAYLIST;
299                 } else {
300                         return false;
301                 }
302                 int column = destination->columnAt(mouse->x());
303                 int row = destination->rowAt(mouse->y());
304                 if (column == -1 || row == -1) return false;
305
306                 if (type == SCRUBBING_CLIP_LIST) {
307                         if (ClipList::Column(column) == ClipList::Column::IN) {
308                                 scrub_pts_origin = cliplist_clips->clip(row)->pts_in;
309                                 preview_single_frame(scrub_pts_origin, stream_idx, FIRST_AT_OR_AFTER);
310                         } else if (ClipList::Column(column) == ClipList::Column::OUT) {
311                                 scrub_pts_origin = cliplist_clips->clip(row)->pts_out;
312                                 preview_single_frame(scrub_pts_origin, stream_idx, LAST_BEFORE);
313                         } else {
314                                 return false;
315                         }
316                 } else {
317                         if (PlayList::Column(column) == PlayList::Column::IN) {
318                                 scrub_pts_origin = playlist_clips->clip(row)->pts_in;
319                                 preview_single_frame(scrub_pts_origin, stream_idx, FIRST_AT_OR_AFTER);
320                         } else if (PlayList::Column(column) == PlayList::Column::OUT) {
321                                 scrub_pts_origin = playlist_clips->clip(row)->pts_out;
322                                 preview_single_frame(scrub_pts_origin, stream_idx, LAST_BEFORE);
323                         } else {
324                                 return false;
325                         }
326                 }
327
328                 scrubbing = true;
329                 scrub_row = row;
330                 scrub_column = column;
331                 scrub_x_origin = mouse->x();
332                 scrub_type = type;
333         } else if (event->type() == QEvent::MouseMove) {
334                 if (scrubbing) {
335                         QMouseEvent *mouse = (QMouseEvent *)event;
336                         int offset = mouse->x() - scrub_x_origin;
337                         int adjusted_offset;
338                         if (offset >= dead_zone_pixels) {
339                                 adjusted_offset = offset - dead_zone_pixels;
340                         } else if (offset < -dead_zone_pixels) {
341                                 adjusted_offset = offset + dead_zone_pixels;
342                         } else {
343                                 adjusted_offset = 0;
344                         }
345
346                         int64_t pts = scrub_pts_origin + adjusted_offset * scrub_sensitivity;
347
348                         if (scrub_type == SCRUBBING_CLIP_LIST) {
349                                 ClipProxy clip = cliplist_clips->clip(scrub_row);
350                                 if (scrub_column == int(ClipList::Column::IN)) {
351                                         pts = std::max<int64_t>(pts, 0);
352                                         pts = std::min(pts, clip->pts_out);
353                                         clip->pts_in = pts;
354                                         preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER);
355                                 } else {
356                                         pts = std::max(pts, clip->pts_in);
357                                         pts = std::min(pts, current_pts);
358                                         clip->pts_out = pts;
359                                         preview_single_frame(pts, stream_idx, LAST_BEFORE);
360                                 }
361                         } else {
362                                 ClipProxy clip = playlist_clips->clip(scrub_row);
363                                 if (scrub_column == int(PlayList::Column::IN)) {
364                                         pts = std::max<int64_t>(pts, 0);
365                                         pts = std::min(pts, clip->pts_out);
366                                         clip->pts_in = pts;
367                                         preview_single_frame(pts, clip->stream_idx, FIRST_AT_OR_AFTER);
368                                 } else {
369                                         pts = std::max(pts, clip->pts_in);
370                                         pts = std::min(pts, current_pts);
371                                         clip->pts_out = pts;
372                                         preview_single_frame(pts, clip->stream_idx, LAST_BEFORE);
373                                 }
374                         }
375
376                         return true;  // Don't use this mouse movement for selecting things.
377                 }
378         } else if (event->type() == QEvent::Wheel) {
379                 QWheelEvent *wheel = (QWheelEvent *)event;
380
381                 QTableView *destination;
382                 int in_column, out_column, camera_column;
383                 if (watched == ui->clip_list->viewport()) {
384                         destination = ui->clip_list;
385                         in_column = int(ClipList::Column::IN);
386                         out_column = int(ClipList::Column::OUT);
387                         camera_column = -1;
388                         last_mousewheel_camera_row = -1;
389                 } else if (watched == ui->playlist->viewport()) {
390                         destination = ui->playlist;
391                         in_column = int(PlayList::Column::IN);
392                         out_column = int(PlayList::Column::OUT);
393                         camera_column = int(PlayList::Column::CAMERA);
394                 } else {
395                         last_mousewheel_camera_row = -1;
396                         return false;
397                 }
398                 int column = destination->columnAt(wheel->x());
399                 int row = destination->rowAt(wheel->y());
400                 if (column == -1 || row == -1) return false;
401
402                 ClipProxy clip = (watched == ui->clip_list->viewport()) ?
403                         cliplist_clips->clip(row) : playlist_clips->clip(row);
404                 if (watched == ui->playlist->viewport()) {
405                         stream_idx = clip->stream_idx;
406                 }
407
408                 if (column != camera_column) {
409                         last_mousewheel_camera_row = -1;
410                 }
411                 if (column == in_column) {
412                         int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity;
413                         pts = std::max<int64_t>(pts, 0);
414                         pts = std::min(pts, clip->pts_out);
415                         clip->pts_in = pts;
416                         preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER);
417                 } else if (column == out_column) {
418                         int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity;
419                         pts = std::max(pts, clip->pts_in);
420                         pts = std::min(pts, current_pts);
421                         clip->pts_out = pts;
422                         preview_single_frame(pts, stream_idx, LAST_BEFORE);
423                 } else if (column == camera_column) {
424                         int angle_degrees = wheel->angleDelta().y();
425                         if (last_mousewheel_camera_row == row) {
426                                 angle_degrees += leftover_angle_degrees;
427                         }
428
429                         int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel;
430                         stream_idx = std::max(stream_idx, 0);
431                         stream_idx = std::min(stream_idx, NUM_CAMERAS - 1);
432                         clip->stream_idx = stream_idx;
433
434                         last_mousewheel_camera_row = row;
435                         leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel;
436
437                         // Don't update the live view, that's rarely what the operator wants.
438                 }
439         } else if (event->type() == QEvent::MouseButtonRelease) {
440                 scrubbing = false;
441         }
442         return false;
443 }
444
445 void MainWindow::preview_single_frame(int64_t pts, unsigned stream_idx, MainWindow::Rounding rounding)
446 {
447         if (rounding == LAST_BEFORE) {
448                 lock_guard<mutex> lock(frame_mu);
449                 if (frames[stream_idx].empty()) return;
450                 auto it = lower_bound(frames[stream_idx].begin(), frames[stream_idx].end(), pts);
451                 if (it != frames[stream_idx].end()) {
452                         pts = *it;
453                 }
454         } else {
455                 assert(rounding == FIRST_AT_OR_AFTER);
456                 lock_guard<mutex> lock(frame_mu);
457                 if (frames[stream_idx].empty()) return;
458                 auto it = upper_bound(frames[stream_idx].begin(), frames[stream_idx].end(), pts - 1);
459                 if (it != frames[stream_idx].end()) {
460                         pts = *it;
461                 }
462         }
463
464         Clip fake_clip;
465         fake_clip.pts_in = pts;
466         fake_clip.pts_out = pts + 1;
467         preview_player->play_clip(fake_clip, stream_idx);
468 }
469
470 void MainWindow::playlist_selection_changed()
471 {
472         QItemSelectionModel *selected = ui->playlist->selectionModel();
473         bool any_selected = selected->hasSelection();
474         ui->playlist_duplicate_btn->setEnabled(any_selected);
475         ui->playlist_remove_btn->setEnabled(any_selected);
476         ui->playlist_move_up_btn->setEnabled(
477                 any_selected && selected->selectedRows().front().row() > 0);
478         ui->playlist_move_down_btn->setEnabled(
479                 any_selected && selected->selectedRows().back().row() < int(playlist_clips->size()) - 1);
480         ui->play_btn->setEnabled(!playlist_clips->empty());
481 }