X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=futatabi%2Fmainwindow.cpp;h=4e53590426cc38d21c348251e7ca3d7507a67f7d;hb=56255d64099f0fbaa5271bcb246bc9510fd0e5d8;hp=57693ef91b2f47ade0a4a3b143af5dc68c3e73c0;hpb=5a1069f76339d67e0549091724b901468356fa2c;p=nageru diff --git a/futatabi/mainwindow.cpp b/futatabi/mainwindow.cpp index 57693ef..4e53590 100644 --- a/futatabi/mainwindow.cpp +++ b/futatabi/mainwindow.cpp @@ -2,6 +2,7 @@ #include "shared/aboutdialog.h" #include "clip_list.h" +#include "export.h" #include "shared/disk_space_estimator.h" #include "flags.h" #include "frame_on_disk.h" @@ -11,6 +12,7 @@ #include "ui_mainwindow.h" #include +#include #include #include #include @@ -30,6 +32,17 @@ static PlayList *playlist_clips; extern int64_t current_pts; +template +void replace_model(QTableView *view, Model **model, Model *new_model, MainWindow *main_window) +{ + QItemSelectionModel *old_selection_model = view->selectionModel(); + view->setModel(new_model); + delete *model; + delete old_selection_model; + *model = new_model; + main_window->connect(new_model, &Model::any_content_changed, main_window, &MainWindow::content_changed); +} + MainWindow::MainWindow() : ui(new Ui::MainWindow), db(global_flags.working_directory + "/futatabi.db") @@ -39,8 +52,14 @@ MainWindow::MainWindow() // The menus. connect(ui->exit_action, &QAction::triggered, this, &MainWindow::exit_triggered); + connect(ui->export_cliplist_clip_multitrack_action, &QAction::triggered, this, &MainWindow::export_cliplist_clip_multitrack_triggered); + connect(ui->export_playlist_clip_interpolated_action, &QAction::triggered, this, &MainWindow::export_playlist_clip_interpolated_triggered); connect(ui->manual_action, &QAction::triggered, this, &MainWindow::manual_triggered); connect(ui->about_action, &QAction::triggered, this, &MainWindow::about_triggered); + connect(ui->undo_action, &QAction::triggered, this, &MainWindow::undo_triggered); + connect(ui->redo_action, &QAction::triggered, this, &MainWindow::redo_triggered); + ui->undo_action->setEnabled(false); + ui->redo_action->setEnabled(false); global_disk_space_estimator = new DiskSpaceEstimator(bind(&MainWindow::report_disk_space, this, _1, _2)); disk_free_label = new QLabel(this); @@ -48,6 +67,7 @@ MainWindow::MainWindow() ui->menuBar->setCornerWidget(disk_free_label); StateProto state = db.get_state(); + undo_stack.push_back(state); // The undo stack always has the current state on top. cliplist_clips = new ClipList(state.clip_list()); ui->clip_list->setModel(cliplist_clips); @@ -126,8 +146,8 @@ MainWindow::MainWindow() this, &MainWindow::playlist_selection_changed); playlist_selection_changed(); // First time set-up. - preview_player.reset(new Player(ui->preview_display, /*also_output_to_stream=*/false)); - live_player.reset(new Player(ui->live_display, /*also_output_to_stream=*/true)); + preview_player.reset(new Player(ui->preview_display, Player::NO_STREAM_OUTPUT)); + live_player.reset(new Player(ui->live_display, Player::HTTPD_STREAM_OUTPUT)); live_player->set_done_callback([this]{ post_to_main_thread([this]{ live_player_clip_done(); @@ -144,6 +164,7 @@ MainWindow::MainWindow() defer_timeout = new QTimer(this); defer_timeout->setSingleShot(true); connect(defer_timeout, &QTimer::timeout, this, &MainWindow::defer_timer_expired); + ui->undo_action->setEnabled(true); connect(ui->clip_list->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::clip_list_selection_changed); @@ -340,6 +361,17 @@ void MainWindow::content_changed() void MainWindow::state_changed(const StateProto &state) { db.store_state(state); + + redo_stack.clear(); + ui->redo_action->setEnabled(false); + + undo_stack.push_back(state); + ui->undo_action->setEnabled(undo_stack.size() > 1); + + // Make sure it doesn't grow without bounds. + while (undo_stack.size() >= 100) { + undo_stack.pop_front(); + } } void MainWindow::play_clicked() @@ -414,25 +446,11 @@ void MainWindow::live_player_clip_progress(const map &progress) { playlist_clips->set_progress(progress); - // Look at the last clip and then start counting from there. - assert(!progress.empty()); - auto last_it = progress.end(); - --last_it; - double remaining = 0.0; - double last_fade_time_seconds = 0.0; - for (size_t row = last_it->first; row < playlist_clips->size(); ++row) { - const Clip clip = *playlist_clips->clip(row); - double clip_length = double(clip.pts_out - clip.pts_in) / TIMEBASE / 0.5; // FIXME: stop hardcoding speed. - if (row == last_it->first) { - // A clip we're playing: Subtract the part we've already played. - remaining = clip_length * (1.0 - last_it->second); - } else { - // A clip we haven't played yet: Subtract the part that's overlapping - // with a previous clip (due to fade). - remaining += max(clip_length - last_fade_time_seconds, 0.0); - } - last_fade_time_seconds = min(clip_length, clip.fade_time_seconds); + vector clips; + for (size_t row = 0; row < playlist_clips->size(); ++row) { + clips.push_back(*playlist_clips->clip(row)); } + double remaining = compute_time_left(clips, progress); set_output_status(format_duration(remaining) + " left"); } @@ -465,9 +483,9 @@ void set_pts_in(int64_t pts, int64_t current_pts, ClipProxy &clip) bool MainWindow::eventFilter(QObject *watched, QEvent *event) { constexpr int dead_zone_pixels = 3; // To avoid that simple clicks get misinterpreted. - constexpr int scrub_sensitivity = 100; // pts units per pixel. - constexpr int wheel_sensitivity = 100; // pts units per degree. constexpr int camera_degrees_per_pixel = 15; // One click of most mice. + int scrub_sensitivity = 100; // pts units per pixel. + int wheel_sensitivity = 100; // pts units per degree. unsigned stream_idx = ui->preview_display->get_stream_idx(); @@ -530,8 +548,16 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) scrub_x_origin = mouse->x(); scrub_type = type; } else if (event->type() == QEvent::MouseMove) { + QMouseEvent *mouse = (QMouseEvent *)event; + if (mouse->modifiers() & Qt::KeyboardModifier::ShiftModifier) { + scrub_sensitivity *= 10; + wheel_sensitivity *= 10; + } + if (mouse->modifiers() & Qt::KeyboardModifier::AltModifier) { // Note: Shift + Alt cancel each other out. + scrub_sensitivity /= 10; + wheel_sensitivity /= 10; + } if (scrubbing) { - QMouseEvent *mouse = (QMouseEvent *)event; int offset = mouse->x() - scrub_x_origin; int adjusted_offset; if (offset >= dead_zone_pixels) { @@ -577,6 +603,16 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) } } else if (event->type() == QEvent::Wheel) { QWheelEvent *wheel = (QWheelEvent *)event; + int angle_delta = wheel->angleDelta().y(); + if (wheel->modifiers() & Qt::KeyboardModifier::ShiftModifier) { + scrub_sensitivity *= 10; + wheel_sensitivity *= 10; + } + if (wheel->modifiers() & Qt::KeyboardModifier::AltModifier) { // Note: Shift + Alt cancel each other out. + scrub_sensitivity /= 10; + wheel_sensitivity /= 10; + angle_delta = wheel->angleDelta().x(); // Qt ickiness. + } QTableView *destination; int in_column, out_column, camera_column; @@ -619,19 +655,19 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) } if (column == in_column) { current_change_id += "in:" + to_string(row); - int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity; + int64_t pts = clip->pts_in + angle_delta * wheel_sensitivity; set_pts_in(pts, current_pts, clip); preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); } else if (column == out_column) { current_change_id += "out:" + to_string(row); - int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity; + int64_t pts = clip->pts_out + angle_delta * wheel_sensitivity; pts = std::max(pts, clip->pts_in); pts = std::min(pts, current_pts); clip->pts_out = pts; preview_single_frame(pts, stream_idx, LAST_BEFORE); } else if (column == camera_column) { current_change_id += "camera:" + to_string(row); - int angle_degrees = wheel->angleDelta().y(); + int angle_degrees = angle_delta; if (last_mousewheel_camera_row == row) { angle_degrees += leftover_angle_degrees; } @@ -754,6 +790,58 @@ void MainWindow::exit_triggered() close(); } +void MainWindow::export_cliplist_clip_multitrack_triggered() +{ + QItemSelectionModel *selected = ui->clip_list->selectionModel(); + if (!selected->hasSelection()) { + QMessageBox msgbox; + msgbox.setText("No clip selected in the clip list. Select one and try exporting again."); + msgbox.exec(); + return; + } + + QModelIndex index = selected->currentIndex(); + Clip clip = *cliplist_clips->clip(index.row()); + QString filename = QFileDialog::getSaveFileName(this, + "Export multitrack clip", QString(), tr("Matroska video files (*.mkv)")); + if (filename.isNull()) { + // Cancel. + return; + } + if (!filename.endsWith(".mkv")) { + filename += ".mkv"; + } + export_multitrack_clip(filename.toStdString(), clip); +} + +void MainWindow::export_playlist_clip_interpolated_triggered() +{ + QItemSelectionModel *selected = ui->playlist->selectionModel(); + if (!selected->hasSelection()) { + QMessageBox msgbox; + msgbox.setText("No clip selected in the playlist. Select one and try exporting again."); + msgbox.exec(); + return; + } + + QString filename = QFileDialog::getSaveFileName(this, + "Export interpolated clip", QString(), tr("Matroska video files (*.mkv)")); + if (filename.isNull()) { + // Cancel. + return; + } + if (!filename.endsWith(".mkv")) { + filename += ".mkv"; + } + + vector clips; + QModelIndexList rows = selected->selectedRows(); + for (QModelIndex index : rows) { + clips.push_back(*playlist_clips->clip(index.row())); + } + export_interpolated_clip(filename.toStdString(), clips); +} + void MainWindow::manual_triggered() { if (!QDesktopServices::openUrl(QUrl("https://nageru.sesse.net/doc/"))) { @@ -768,6 +856,53 @@ void MainWindow::about_triggered() AboutDialog("Futatabi", "Multicamera slow motion video server").exec(); } +void MainWindow::undo_triggered() +{ + // Finish any deferred action. + if (defer_timeout->isActive()) { + defer_timeout->stop(); + state_changed(deferred_state); + } + + StateProto redo_state; + *redo_state.mutable_clip_list() = cliplist_clips->serialize(); + *redo_state.mutable_play_list() = playlist_clips->serialize(); + redo_stack.push_back(std::move(redo_state)); + ui->redo_action->setEnabled(true); + + assert(undo_stack.size() > 1); + + // Pop off the current state, which is always at the top of the stack. + undo_stack.pop_back(); + + StateProto state = undo_stack.back(); + ui->undo_action->setEnabled(undo_stack.size() > 1); + + replace_model(ui->clip_list, &cliplist_clips, new ClipList(state.clip_list()), this); + replace_model(ui->playlist, &playlist_clips, new PlayList(state.play_list()), this); + + db.store_state(state); +} + +void MainWindow::redo_triggered() +{ + assert(!redo_stack.empty()); + + ui->undo_action->setEnabled(true); + ui->redo_action->setEnabled(true); + + undo_stack.push_back(std::move(redo_stack.back())); + redo_stack.pop_back(); + ui->undo_action->setEnabled(true); + ui->redo_action->setEnabled(!redo_stack.empty()); + + const StateProto &state = undo_stack.back(); + replace_model(ui->clip_list, &cliplist_clips, new ClipList(state.clip_list()), this); + replace_model(ui->playlist, &playlist_clips, new PlayList(state.play_list()), this); + + db.store_state(state); +} + void MainWindow::highlight_camera_input(int stream_idx) { if (stream_idx == 0) {