#include "mainwindow.h" #include "ui_mainwindow.h" using Xybrid::MainWindow; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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(ui->patternList->indexAt(pt).row()); std::shared_ptr 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(idx)))->commit(); }); if (p) { menu->addAction("Duplicate Pattern", this, [this, p, idx]() { (new ProjectPatternAddCommand(project, static_cast(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(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(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(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(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(idx)); c->seqSel = static_cast(idx)-1; c->commit(); }); menu->addSeparator(); menu->addAction("Create New Pattern", this, [this, idx]() { int si = static_cast(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(std::min(idx + 1, project->sequence.size())); (new ProjectPatternAddCommand(project, static_cast(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(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(ui->patternEditor->model())->updateColumnDisplay(); }); connect(socket, &UISocket::openGraph, this, [this](Graph* g) { if (!g) return; auto gg = std::static_pointer_cast(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(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->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(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(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(editingPattern->index), 0)); return true; } void MainWindow::openGraph(const std::shared_ptr& 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); }