#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"
#include "ui_mainwindow.h"
#include <QDesktopServices>
+#include <QFileDialog>
#include <QMessageBox>
#include <QMouseEvent>
#include <QShortcut>
// 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);
this, &MainWindow::playlist_selection_changed);
playlist_selection_changed(); // First time set-up.
- preview_player = new Player(ui->preview_display, /*also_output_to_stream=*/false);
- live_player = 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();
this, &MainWindow::clip_list_selection_changed);
}
+MainWindow::~MainWindow()
+{
+ // Empty so that we can forward-declare Player in the .h file.
+}
+
void MainWindow::cue_in_clicked()
{
if (!cliplist_clips->empty() && cliplist_clips->back()->pts_out < 0) {
clip.pts_in = current_pts;
cliplist_clips->add_clip(clip);
playlist_selection_changed();
+ ui->clip_list->scrollToBottom();
}
void MainWindow::cue_out_clicked()
if (clip.pts_out != -1) {
playlist_clips->add_clip(clip);
playlist_selection_changed();
+ ui->playlist->scrollToBottom();
}
return;
}
if (clip.pts_out != -1) {
playlist_clips->add_clip(clip);
playlist_selection_changed();
+ ui->playlist->scrollToBottom();
+ if (!ui->playlist->selectionModel()->hasSelection()) {
+ // TODO: Figure out why this doesn't always seem to actually select the row.
+ QModelIndex bottom = playlist_clips->index(playlist_clips->size() - 1, 0);
+ ui->playlist->setCurrentIndex(bottom);
+ }
}
}
{
// playlist_clips can only be accessed on the main thread.
// Hopefully, we won't have to wait too long for this to come back.
+ //
+ // TODO: If MainWindow is in the process of being destroyed and waiting
+ // for Player to shut down, we could have a deadlock here.
promise<pair<Clip, size_t>> clip_promise;
future<pair<Clip, size_t>> clip = clip_promise.get_future();
post_to_main_thread([this, &clip_promise] {
{
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<Clip> 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");
}
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();
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) {
}
} 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;
}
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;
}
lock_guard<mutex> lock(frame_mu);
if (frames[stream_idx].empty())
return;
- auto it = lower_bound(frames[stream_idx].begin(), frames[stream_idx].end(), pts,
- [](const FrameOnDisk &frame, int64_t pts) { return frame.pts < pts; });
+ auto it = find_last_frame_before(frames[stream_idx], pts);
if (it != frames[stream_idx].end()) {
pts = it->pts;
}
lock_guard<mutex> lock(frame_mu);
if (frames[stream_idx].empty())
return;
- auto it = upper_bound(frames[stream_idx].begin(), frames[stream_idx].end(), pts - 1,
- [](int64_t pts, const FrameOnDisk &frame) { return pts < frame.pts; });
+ auto it = find_first_frame_at_or_after(frames[stream_idx], pts);
if (it != frames[stream_idx].end()) {
pts = it->pts;
}
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;
+ }
+
+ QModelIndex index = selected->currentIndex();
+ Clip clip = *playlist_clips->clip(index.row());
+ 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";
+ }
+ export_interpolated_clip(filename.toStdString(), clip);
+}
+
void MainWindow::manual_triggered()
{
if (!QDesktopServices::openUrl(QUrl("https://nageru.sesse.net/doc/"))) {