xybrid/xybrid/mainwindow.cpp

466 lines
19 KiB
C++

#include "mainwindow.h"
#include "ui_mainwindow.h"
using Xybrid::MainWindow;
#include <QDebug>
#include <QKeyEvent>
#include <QShortcut>
#include <QTabWidget>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QWindow>
#include <QUndoStack>
#include <QTimer>
#include <QOpenGLWidget>
#include <QGLWidget>
#include <QScroller>
#include <QGraphicsTextItem>
#include <QJsonObject>
#include "data/graph.h"
#include "util/strings.h"
#include "util/lambdaeventfilter.h"
#include "fileops.h"
#include "ui/patternlistmodel.h"
#include "ui/patternsequencermodel.h"
#include "ui/patterneditoritemdelegate.h"
#include "ui/patchboard/patchboardscene.h"
#include "editing/projectcommands.h"
#include "config/pluginregistry.h"
#include "audio/audioengine.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
using Xybrid::Data::Graph;
using Xybrid::Data::Node;
using Xybrid::Data::Port;
using Xybrid::UI::PatternListModel;
using Xybrid::UI::PatternSequencerModel;
using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorItemDelegate;
using Xybrid::UI::PatchboardScene;
using namespace Xybrid::Editing;
using namespace Xybrid::Config;
using namespace Xybrid::Audio;
namespace {
constexpr const auto projectFilter = u8"Xybrid project (*.xyp)\nAll files (*)";
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow) {
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
// remove tab containing system widgets
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_));
undoStack = new QUndoStack(this);
//undoStack->setUndoLimit(256);
connect(undoStack, &QUndoStack::cleanChanged, this, [this](bool) {
updateTitle();
});
auto* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
undoAction->setShortcuts(QKeySequence::Undo);
ui->menuEdit->addAction(undoAction);
auto* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
redoAction->setShortcuts(QKeySequence::Redo);
ui->menuEdit->addAction(redoAction);
auto* t = ui->tabWidget;
t->setCornerWidget(ui->menuBar);
t->setCornerWidget(ui->logo, Qt::TopLeftCorner);
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
// prevent right pane of pattern view from being collapsed
ui->patternViewSplitter->setCollapsible(1, false);
connect(ui->patternViewSplitter, &QSplitter::splitterMoved, this, [this](int, int) {
// and when the list is collapsed, make sure header size is updated
ui->patternEditor->updateHeader();
});
{ /* Set up pattern list */ } {
// model
ui->patternList->setModel(new PatternListModel(ui->patternList, this));
// events
// on selection change
connect(ui->patternList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex& old) {
if (index == old) return; // no actual change
size_t idx = static_cast<size_t>(index.row());
if (idx >= project->patterns.size()) return;
this->selectPatternForEditing(project->patterns[idx].get());
});
// on click
connect(ui->patternList, &QListView::clicked, this, [this]() { // deselect on sequencer when list clicked
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, -1));
});
// rightclick menu
connect(ui->patternList, &QListView::customContextMenuRequested, this, [this](const QPoint& pt) {
size_t idx = static_cast<size_t>(ui->patternList->indexAt(pt).row());
std::shared_ptr<Pattern> p = nullptr;
if (idx < project->patterns.size()) p = project->patterns[idx];
QMenu* menu = new QMenu(this);
menu->addAction("New Pattern", this, [this, idx]() {
(new ProjectPatternAddCommand(project, static_cast<int>(idx)))->commit();
});
if (p) {
menu->addAction("Duplicate Pattern", this, [this, p, idx]() {
(new ProjectPatternAddCommand(project, static_cast<int>(idx) + 1, -1, p))->commit();
});
menu->addSeparator();
menu->addAction("Delete Pattern", this, [this, p]() {
if (QMessageBox::warning(this, "Are you sure?", QString("Remove pattern %1?").arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
(new ProjectPatternDeleteCommand(project, p))->commit();
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(ui->patternList->mapToGlobal(pt));
});//*/
}
{ /* Set up sequencer */ } {
// model
ui->patternSequencer->setModel(new PatternSequencerModel(ui->patternSequencer, this));
// some metrics that the designer doesn't seem to like doing
ui->patternSequencer->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
ui->patternSequencer->horizontalHeader()->setDefaultSectionSize(24);
ui->patternSequencer->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
ui->patternSequencer->verticalHeader()->setDefaultSectionSize(24);
// events
// on selection change
connect(ui->patternSequencer->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex&) {
size_t idx = static_cast<size_t>(index.column());
if (idx >= project->sequence.size()) return;
this->selectPatternForEditing(project->sequence[idx]);
});
// rightclick menu
connect(ui->patternSequencer, &QTableView::customContextMenuRequested, this, [this](const QPoint& pt) {
size_t idx = static_cast<size_t>(ui->patternSequencer->indexAt(pt).column());
QMenu* menu = new QMenu(this);
menu->addAction("Insert Pattern", this, [this, idx]() {
if (!editingPattern->validFor(project)) return; // nope
int si = static_cast<int>(std::min(idx, project->sequence.size()));
auto* c = new ProjectSequencerDeltaCommand(project);
c->seq.insert(c->seq.begin() + si, editingPattern.get());
c->seqSel = si+1;
c->commit();
});
menu->addAction("Insert Separator", this, [this, idx]() {
int si = static_cast<int>(std::min(idx, project->sequence.size()));
auto* c = new ProjectSequencerDeltaCommand(project);
c->seq.insert(c->seq.begin() + si, nullptr);
c->seqSel = si+1;
c->commit();
});
if (idx < project->sequence.size()) menu->addAction("Remove", this, [this, idx]() {
auto* c = new ProjectSequencerDeltaCommand(project);
c->seq.erase(c->seq.begin() + static_cast<ptrdiff_t>(idx));
c->seqSel = static_cast<int>(idx)-1;
c->commit();
});
menu->addSeparator();
menu->addAction("Create New Pattern", this, [this, idx]() {
int si = static_cast<int>(std::min(idx, project->sequence.size()));
(new ProjectPatternAddCommand(project, -1, si))->commit();
});
if (idx < project->sequence.size() && project->sequence[idx]) {
menu->addAction("Duplicate Pattern", this, [this, idx, p = project->patterns[project->sequence[idx]->index]]() {
int si = static_cast<int>(std::min(idx + 1, project->sequence.size()));
(new ProjectPatternAddCommand(project, static_cast<int>(p->index) + 1, si, p))->commit();
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(ui->patternSequencer->mapToGlobal(pt));
});//*/
}
{ /* Set up keyboard shortcuts for pattern view */ } {
// Ctrl+PgUp/Down - previous or next pattern in sequencer
connect(new QShortcut(QKeySequence("Ctrl+PgUp"), ui->pattern), &QShortcut::activated, this, [this]() {
auto i = ui->patternSequencer->currentIndex();
if (!i.isValid()) {
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(ui->patternSequencer->horizontalHeader()->count() - 1, 0));
return;
}
auto count = ui->patternSequencer->horizontalHeader()->count();
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() - 1) % count));
});
connect(new QShortcut(QKeySequence("Ctrl+PgDown"), ui->pattern), &QShortcut::activated, this, [this]() {
auto i = ui->patternSequencer->currentIndex();
if (!i.isValid()) {
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, 0));
return;
}
auto count = ui->patternSequencer->horizontalHeader()->count();
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() + 1) % count));
});
// TEMP - play/stop
connect(new QShortcut(QKeySequence("Ctrl+P"), this), &QShortcut::activated, this, [this]() {
if (audioEngine->playbackMode() == AudioEngine::Playing) audioEngine->stop();
else audioEngine->play(project);
});
/* tmp test
connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, this, [this]() {
auto inp = QInputDialog::getText(this, "yes", "yes");
WId id = inp.toULongLong();
auto* w = QWindow::fromWinId(id);
auto* w2 = new QWindow();
auto* wc = QWidget::createWindowContainer(w2);
w->setParent(w2);
ui->patchboard->layout()->addWidget(wc);
});
*/
}
{ /* Set up patchboard view */ } {
//ui->patchboardView->setDragMode(QGraphicsView::DragMode::RubberBandDrag);
auto* view = ui->patchboardView;
bool enableHWAccel = false; // disabled because QOpenGLWidget has some huge lag issues in this context
if (enableHWAccel) {
auto* vp = new QOpenGLWidget();
view->setViewport(vp); // enable hardware acceleration
}
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
glEnable(GL_MULTISAMPLE);
glEnable(GL_LINE_SMOOTH);
//QGL::FormatOption::Rgba
view->setAlignment(Qt::AlignTop | Qt::AlignLeft);
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setAttribute(Qt::WA_AcceptTouchEvents, true);
QScroller::grabGesture(view, QScroller::MiddleMouseButtonGesture);
{
auto prop = QScroller::scroller(view)->scrollerProperties();
prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
prop.setScrollMetric(QScrollerProperties::AxisLockThreshold, 1);
QScroller::scroller(view)->setScrollerProperties(prop);
QScroller::scroller(view)->setSnapPositionsX({});
QScroller::scroller(view)->setSnapPositionsY({});
}
// event filter to make drag-to-select only happen on left click
view->viewport()->installEventFilter(new LambdaEventFilter(view, [view](QObject* w, QEvent* e) {
if (e->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(e);
// initiate drag
if (me->button() == Qt::LeftButton) {
view->setDragMode(QGraphicsView::RubberBandDrag);
}
} else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end
QTimer::singleShot(1, [view] {
view->setDragMode(QGraphicsView::NoDrag);
});
}
return w->QObject::eventFilter(w, e);
}));
}
// Set up signaling from project to UI
socket = new UISocket();
socket->setParent(this);
socket->window = this;
socket->undoStack = undoStack;
connect(socket, &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
connect(socket, &UISocket::patternUpdated, this, [this](Pattern* p) {
if (editingPattern.get() != p) return;
ui->patternEditor->refresh();
});
connect(socket, &UISocket::rowUpdated, this, [this](Pattern* p, int ch, int r) {
if (editingPattern.get() != p) return;
const auto cpc = PatternEditorModel::colsPerChannel;
auto ind = ui->patternEditor->model()->index(r, ch * cpc);
emit ui->patternEditor->model()->dataChanged(ind, ind.siblingAtColumn((ch+1)*cpc-1));
static_cast<PatternEditorModel*>(ui->patternEditor->model())->updateColumnDisplay();
});
connect(socket, &UISocket::openGraph, this, [this](Graph* g) {
if (!g) return;
auto gg = std::static_pointer_cast<Graph>(g->shared_from_this());
QString name = QString::fromStdString(gg->name);
if (name.isEmpty()) name = QString::fromStdString(gg->pluginName());
ui->patchboardBreadcrumbs->push(name, this, [this, gg] {
openGraph(gg);
});
});
// and from audio engine
connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction]() {
bool locked = project->editingLocked();
undoAction->setEnabled(!locked);
redoAction->setEnabled(!locked);
});
// and start with a new project
menuFileNew();
}
MainWindow::~MainWindow() {
delete ui;
}
bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
if (event->type() == QEvent::KeyPress) {
auto ke = static_cast<QKeyEvent*>(event);
qDebug() << QString("key pressed: %1").arg((ke->key()));
if (ke->key() == Qt::Key_Tab) {
qDebug() << QString("tab");
auto* t = this->ui->tabWidget;
t->setTabPosition(QTabWidget::TabPosition::South);
t->setCurrentIndex(t->currentIndex() + 1);
return true;
}
}
return false;
}
void MainWindow::menuFileNew() {
auto hold = project; // keep alive until done
if (audioEngine->playingProject() == project) audioEngine->stop();
project = std::make_shared<Project>();
project->sequence.push_back(project->newPattern().get());
onNewProjectLoaded();
}
void MainWindow::menuFileOpen() {
auto fileName = QFileDialog::getOpenFileName(this, "Open project...", QString(), projectFilter);
if (fileName.isEmpty()) return; // canceled
auto np = FileOps::loadProject(fileName);
if (!np) {
QMessageBox::critical(this, "Error", "Error loading project");
return;
}
if (audioEngine->playingProject() == project) audioEngine->stop();
project = np;
onNewProjectLoaded();
}
void MainWindow::menuFileSave() {
if (project->fileName.isEmpty()) menuFileSaveAs();
else {
FileOps::saveProject(project);
undoStack->setClean();
}
}
void MainWindow::menuFileSaveAs() {
auto fileName = QFileDialog::getSaveFileName(this, "Save project as...", QString(), projectFilter);
if (fileName.isEmpty()) return; // canceled
FileOps::saveProject(project, fileName);
undoStack->setClean();
updateTitle();
}
void MainWindow::onNewProjectLoaded() {
undoStack->clear();
project->socket = socket;
updatePatternLists();
patternSelection(0);
sequenceSelection(-1);
for (size_t i = 0; i < project->sequence.size(); i++) { // find first non-spacer in sequence, else fall back to first pattern numerically
if (project->sequence[i] == nullptr) continue;
sequenceSelection(static_cast<int>(i));
break;
}
//openGraph(project->rootGraph);
ui->patchboardBreadcrumbs->clear();
ui->patchboardBreadcrumbs->push("/", this, [this, gg = project->rootGraph] {
openGraph(gg);
});
updateTitle();
}
int MainWindow::patternSelection(int n) {
auto i = ui->patternList->currentIndex();
if (n >= -1) ui->patternList->setCurrentIndex(ui->patternList->model()->index(n, 0));
return i.isValid() ? i.row() : -1;
}
int MainWindow::sequenceSelection(int n) {
auto i = ui->patternSequencer->currentIndex();
if (n >= -1) ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, n));
return i.isValid() ? i.column() : -1;
}
void MainWindow::playbackPosition(int seq, int row) {
sequenceSelection(seq);
auto mi = ui->patternEditor->currentIndex().siblingAtRow(row);
if (!mi.isValid()) mi = ui->patternEditor->model()->index(row, 0);
ui->patternEditor->setCurrentIndex(mi);
ui->patternEditor->selectionModel()->select(QItemSelection(mi.siblingAtColumn(0), mi.siblingAtColumn(ui->patternEditor->horizontalHeader()->count()-1)), QItemSelectionModel::SelectionFlag::ClearAndSelect);
ui->patternEditor->scrollTo(mi, QAbstractItemView::PositionAtCenter);
}
void MainWindow::updatePatternLists() {
emit ui->patternList->model()->layoutChanged();
emit ui->patternSequencer->model()->layoutChanged();
if (auto i = ui->patternList->currentIndex(); i.isValid() && !ui->patternSequencer->currentIndex().isValid())
selectPatternForEditing(project->patterns[static_cast<size_t>(i.row())].get()); // make sure pattern editor matches selection
if (editingPattern && !editingPattern->validFor(project)) // if current pattern invalidated, select new one
selectPatternForEditing(project->patterns[std::min(editingPattern->index, project->patterns.size() - 1)].get());
}
void MainWindow::updateTitle() {
QString title = u8"Xybrid - %1%2";
if (project->fileName.isEmpty()) title = title.arg("(new project)");
else title = title.arg(QFileInfo(project->fileName).baseName());
if (undoStack->isClean()) title = title.arg("");
else title = title.arg("*");
this->setWindowTitle(title);
}
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
if (!pattern || pattern == editingPattern.get()) return false; // no u
if (pattern->project != project.get()) return false; // wrong project
if (project->patterns.size() <= pattern->index) return false; // invalid id
auto sp = project->patterns[pattern->index];
if (sp.get() != pattern) return false; // wrong index
auto hold = editingPattern; // keep alive until done
editingPattern = sp;
ui->patternEditor->setPattern(editingPattern);
ui->patternList->setCurrentIndex(ui->patternList->model()->index(static_cast<int>(editingPattern->index), 0));
return true;
}
void MainWindow::openGraph(const std::shared_ptr<Data::Graph>& g) {
if (!g) return; // invalid
QPointF scrollPt(g->viewX, g->viewY);
ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g));
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 0);
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 1);
}