#include "mainwindow.h" #include "ui_mainwindow.h" #include "settingsdialog.h" using Xybrid::MainWindow; #include #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 "ui/patchboard/nodeuiscene.h" #include "ui/samplelistmodel.h" #include "editing/compositecommand.h" #include "editing/projectcommands.h" #include "editing/patterncommands.h" #include "config/uistate.h" #include "config/pluginregistry.h" #include "audio/audioengine.h" using Xybrid::Data::Project; using Xybrid::Data::Pattern; using Xybrid::Data::SequenceEntry; 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 Xybrid::UI::NodeUIScene; using Xybrid::UI::SampleListModel; using namespace Xybrid::Editing; using namespace Xybrid::Config; using namespace Xybrid::Audio; namespace { // } std::unordered_set MainWindow::openWindows; MainWindow::MainWindow(QWidget *parent, const QString& fileName) : QMainWindow(parent), ui(new Ui::MainWindow) { socket = new UISocket(); // create this first ui->setupUi(this); // remove tabs containing system widgets ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_)); ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_2)); undoStack = new QUndoStack(this); //undoStack->setUndoLimit(256); connect(undoStack, &QUndoStack::cleanChanged, this, [this](bool) { updateTitle(); }); auto efa = ui->menuEdit->actions().at(0); auto* undoAction = undoStack->createUndoAction(this, tr("&Undo")); undoAction->setShortcuts(QKeySequence::Undo); ui->menuEdit->insertAction(efa, undoAction); auto* redoAction = undoStack->createRedoAction(this, tr("&Redo")); redoAction->setShortcuts(QKeySequence::Redo); ui->menuEdit->insertAction(efa, redoAction); // 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 toolbar */ } { auto* t = ui->tabWidget; t->setCurrentWidget(ui->pattern); // set default regardless of what was edited last in the designer :| t->setCornerWidget(ui->logo, Qt::TopLeftCorner); auto* tb = ui->toolBar; t->setCornerWidget(tb); tb->layout()->addWidget(ui->menuBar); ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaPlay)); // play/stop auto play = new QAction(this); play->setShortcuts({ QKeySequence("Ctrl+P"), QKeySequence("Ctrl+Shift+P") }); ui->playButton->setDefaultAction(play); connect(play, &QAction::triggered, this, [this] { if (audioEngine->playbackMode() == AudioEngine::Playing && audioEngine->playingProject() == project) audioEngine->stop(); else { bool shift = QGuiApplication::keyboardModifiers().testFlag(Qt::KeyboardModifier::ShiftModifier); if (shift) audioEngine->play(project, ui->patternSequencer->currentIndex().column()); else audioEngine->play(project); } }); //ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }"); } { /* Set up floater container */ } { auto fc = ui->floaterContainer; fc->setParent(this); fc->setAttribute(Qt::WA_TransparentForMouseEvents); // click through fc->setFocusPolicy(Qt::NoFocus); // can't be focused fc->move(0, 0); fc->setFixedSize(this->size()); setFloater(); } { /* Set up recent file entries */ } { auto fm = ui->menuFile; auto bfr = ui->actionNew_Window; for (size_t i = 0; i < UIState::MAX_RECENTS; i++) { auto ac = new QAction(fm); ac->setVisible(false); fm->insertAction(bfr, ac); recentFileActions.push_back(ac); QObject::connect(ac, &QAction::triggered, ac, [this, i]() { openRecentProject(i); }); } fm->insertSeparator(bfr); // update list every time we show this menu QObject::connect(fm, &QMenu::aboutToShow, fm, [this]() { auto ri = UIState::recentFiles.begin(); auto sz = UIState::recentFiles.size(); for (size_t i = 0; i < UIState::MAX_RECENTS; i++) { auto ac = recentFileActions[i]; if (isetText(qs("%1 %2").arg(ix, fi.fileName())); ac->setVisible(true); ri++; } else { ac->setVisible(false); ac->setText(QString()); } } }); } { /* 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) { auto ind = ui->patternList->indexAt(pt); size_t idx = static_cast(ind.row()); std::shared_ptr p = nullptr; if (idx < project->patterns.size()) p = project->patterns[idx]; QMenu* menu = new QMenu(this); if (ind.isValid()) menu->addAction("Rename...", this, [this, ind] { ui->patternList->edit(ind); }); 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("Properties...", this, [this, p] { openPatternProperties(p); }); 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 song info pane */ } { ui->expandSongInfo->setIcon(ui->expandSongInfo->style()->standardIcon(QStyle::SP_ToolBarVerticalExtensionButton)); ui->collapseSongInfo->setIcon(ui->expandSongInfo->style()->standardIcon(QStyle::SP_ToolBarVerticalExtensionButton)); connect(ui->expandSongInfo, &QPushButton::pressed, this, [this] { setSongInfoPaneExpanded(true); }); connect(ui->collapseSongInfo, &QPushButton::pressed, this, [this] { setSongInfoPaneExpanded(false); }); connect(ui->editArtist, &QLineEdit::textEdited, this, [this](const QString& s) { project->artist = s; updateTitle(); }); connect(ui->editTitle, &QLineEdit::textEdited, this, [this](const QString& s) { project->title = s; updateTitle(); }); connect(ui->editComment, &QPlainTextEdit::modificationChanged, this, [this](bool b) { if (b) { project->comment = ui->editComment->document()->toPlainText(); ui->editComment->document()->setModified(false); } }); connect(ui->editTempo, qOverload(&QDoubleSpinBox::valueChanged), this, [this](double t) { project->tempo = t; }); } { /* 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].pattern().get()); }); // middle click auto mouselmb = [this, seq = ui->patternSequencer](QObject*, QEvent* e) { if (e->type() == QEvent::MouseButtonRelease) { auto me = static_cast(e); if (me->button() == Qt::MouseButton::MiddleButton) { auto idx = static_cast(seq->indexAt(me->pos()).column()); if (idx >= project->sequence.size()) return false; // nothing to remove auto* c = new ProjectSequencerDeltaCommand(project); c->seq.erase(c->seq.begin() + static_cast(idx)); c->seqSel = static_cast(idx)-1; return c->commit(); } } return false; }; ui->patternSequencer->viewport()->installEventFilter(new LambdaEventFilter(this, mouselmb)); // 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); 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, SequenceEntry::Separator); c->seqSel = si+1; c->commit(); }); menu->addAction("Insert Loop Point", 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, SequenceEntry::LoopStart); c->seqSel = si+1; c->commit(); }); menu->addAction("Insert Loop Trigger", 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, SequenceEntry::LoopTrigger); 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()) if (auto p = project->sequence[idx].pattern(); p) { menu->addAction("Duplicate Pattern", this, [this, idx, p] { int si = static_cast(std::min(idx + 1, project->sequence.size())); (new ProjectPatternAddCommand(project, static_cast(p->index) + 1, si, p))->commit(); }); menu->addSeparator(); menu->addAction("Properties...", this, [this, p] { openPatternProperties(p); }); } 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 auto nxPt = [this](int off) { auto ix = ui->patternSequencer->currentIndex(); int i = ix.column(); int ss = static_cast(project->sequence.size()); if (!ix.isValid()) i = off > 0 ? -1 : 0; for (int c = 0; c < ss; c++) { i = (ss+i+off) % ss; auto& se = project->sequence[static_cast(i)]; if (auto p = se.pattern(); p) { ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, i)); return; } } }; connect(new QShortcut(QKeySequence("Ctrl+PgUp"), ui->pattern), &QShortcut::activated, this, [nxPt] { nxPt(-1); }); connect(new QShortcut(QKeySequence("Ctrl+PgDown"), ui->pattern), &QShortcut::activated, this, [nxPt] { nxPt(1); }); /* 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); // Under OSX these cause Xybrid to crash. #ifndef __APPLE__ glEnable(GL_MULTISAMPLE); glEnable(GL_LINE_SMOOTH); #endif //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] { view->setDragMode(QGraphicsView::NoDrag); }); } return w->QObject::eventFilter(w, e); })); connect(new QShortcut(QKeySequence("Esc"), view), &QShortcut::activated, this, [this] { ui->patchboardBreadcrumbs->up(); }); } { /* Set up sample list */ } { // model auto mdl = new SampleListModel(ui->sampleList, this); ui->sampleList->setModel(mdl); connect(ui->sampleList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this, mdl](const QModelIndex& ind, const QModelIndex& old [[maybe_unused]]) { selectSampleForEditing(mdl->itemAt(ind)); }); // edit pane connect(ui->groupSampleLoop, &QGroupBox::toggled, this, [this](bool on) { if (editingSample) { if (on) { editingSample->loopStart = ui->spinSampleLoopStart->value(); editingSample->loopEnd = ui->spinSampleLoopEnd->value(); } else { editingSample->loopStart = -1; editingSample->loopEnd = -1; } ui->waveformPreview->update(); } }); connect(ui->spinSampleLoopStart, qOverload(&QSpinBox::valueChanged), this, [this](int v) { if (editingSample && ui->groupSampleLoop->isChecked()) { editingSample->loopStart = v; ui->waveformPreview->update(); } }); connect(ui->spinSampleLoopEnd, qOverload(&QSpinBox::valueChanged), this, [this](int v) { if (editingSample && ui->groupSampleLoop->isChecked()) { editingSample->loopEnd = v; ui->waveformPreview->update(); } }); connect(ui->spinSampleNote, qOverload(&QSpinBox::valueChanged), this, [this](int v) { auto sp = ui->spinSampleNote; sp->setSuffix(")"); sp->setPrefix(qs("%1 (").arg(Util::noteName(static_cast(v)))); if (editingSample) editingSample->baseNote = v; }); connect(ui->spinSampleNoteSub, qOverload(&QDoubleSpinBox::valueChanged), this, [this](double v) { auto sp = ui->spinSampleNoteSub; sp->setPrefix(v >= 0.0 ? qs("+") : QString()); if (editingSample) editingSample->subNote = v; }); emit ui->spinSampleNoteSub->valueChanged(0.0); // force refresh } // force fonts to display properly updateFont(); // Set up signaling from project to UI 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 = gg->name; if (name.isEmpty()) name = gg->pluginName(); ui->patchboardBreadcrumbs->push(name, this, [this, gg] { openGraph(gg); }); }); connect(socket, &UISocket::openNodeUI, this, [this](Node* n) { if (!n) return; auto nn = n->shared_from_this(); QString name = nn->name; if (name.isEmpty()) name = nn->pluginName(); ui->patchboardBreadcrumbs->push(name, this, [this, nn] { openNodeUI(nn); }); }); // and from audio engine connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction] { bool locked = project->editingLocked(); undoAction->setEnabled(!locked); redoAction->setEnabled(!locked); bool playingThis = audioEngine->playbackMode() == AudioEngine::Playing && audioEngine->playingProject() == project; if (playingThis) ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaStop)); else ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaPlay)); }); selectSampleForEditing(nullptr); // init blank bool isFirst = false;//openWindows.empty(); openWindows.insert(this); if (fileName.isEmpty()) { // start with a new project menuFileNew(); } else { openProject(fileName, isFirst); if (!project) { if (!isFirst) close(); else menuFileNew(); } } } MainWindow::~MainWindow() { if (audioEngine->playingProject() == project) audioEngine->stop(); delete ui; } MainWindow* MainWindow::projectWindow(const QString &fileName) { if (fileName.isEmpty()) return nullptr; for (auto w : openWindows) if (w->project && w->project->fileName == fileName) return w; return nullptr; } void MainWindow::resizeEvent(QResizeEvent* e) { this->QMainWindow::resizeEvent(e); ui->floaterContainer->setFixedSize(this->size()); } void MainWindow::closeEvent(QCloseEvent* e) { if (promptSave()) { e->ignore(); return; } undoStack->clear(); setAttribute(Qt::WA_DeleteOnClose); // delete when done openWindows.erase(this); // and remove from list now if (openWindows.size() == 0 && SettingsDialog::instance) SettingsDialog::instance->reject(); } 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::tryFocus() { raise(); activateWindow(); } void MainWindow::openProject(const QString& fileName, bool failSilent) { auto pw = projectWindow(fileName); if (pw && pw != this) { pw->tryFocus(); return; } auto np = FileOps::loadProject(fileName); if (!np) { if (!failSilent) QMessageBox::critical(this, qs("Error"), qs("Error loading project \"%1\".").arg(QFileInfo(fileName).fileName())); return; } if (audioEngine->playingProject() == project) audioEngine->stop(); project = np; UIState::addRecentFile(fileName); onNewProjectLoaded(); } void MainWindow::openRecentProject(size_t idx) { if (promptSave()) return; if (idx > UIState::recentFiles.size()) return; auto it = UIState::recentFiles.begin(); for (size_t i = 0; i < idx; i++) it++; openProject(QString(*it)); // need to copy string before opening } bool MainWindow::promptSave() { if (!project) return false; // window closing on open if (!undoStack->isClean()) { auto ftxt = project->fileName.isEmpty() ? qs("unsaved project") : qs("project \"%1\"").arg(QFileInfo(project->fileName).fileName()); auto r = QMessageBox::warning(this, qs("Unsaved changes"), qs("Save changes to %1?").arg(ftxt), static_cast(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel)); if (r == QMessageBox::Cancel || r == QMessageBox::Escape) return true; // signal abort if (r == QMessageBox::Yes) { menuFileSave(); if (project->fileName.isEmpty()) return true; // save-as-new canceled } } return false; } void MainWindow::menuFileNew() { if (promptSave()) return; auto hold = project; // keep alive until done if (audioEngine->playingProject() == project) audioEngine->stop(); project = FileOps::newProject(); onNewProjectLoaded(); } void MainWindow::menuFileOpen() { if (promptSave()) return; if (auto fileName = FileOps::showOpenDialog(this, "Open project...", Config::Directories::projects, FileOps::Filter::project); !fileName.isEmpty()) { openProject(fileName); } } void MainWindow::menuFileSave() { if (project->fileName.isEmpty()) menuFileSaveAs(); else { FileOps::saveProject(project); undoStack->setClean(); } } void MainWindow::menuFileSaveAs() { QString saveDir = Config::Directories::projects; if (!project->fileName.isEmpty()) { QFileInfo f(project->fileName); saveDir = f.dir().filePath(f.baseName()); } if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) { FileOps::saveProject(project, fileName); UIState::addRecentFile(fileName); undoStack->setClean(); updateTitle(); } } void MainWindow::menuFileExport() { if (project->exportFileName.isEmpty()) menuFileExportAs(); else render(); } void MainWindow::menuFileExportAs() { QString saveDir = Config::Directories::projects; if (!project->fileName.isEmpty()) { QFileInfo f(project->fileName); saveDir = f.dir().filePath(f.baseName()).append(".mp3"); } else saveDir = saveDir.append("/untitled.mp3"); if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::audioOut, "mp3"); !fileName.isEmpty()) { project->exportFileName = fileName; render(); } } void MainWindow::render() { std::vector dis { ui->pattern, ui->patchboard, ui->samples, ui->menuBar, ui->playButton }; for (auto w : dis) w->setEnabled(false); setFloater(ui->floaterRendering); audioEngine->render(project, project->exportFileName); if (openWindows.find(this) == openWindows.end()) return; // don't try to update UI if the window has been disposed setFloater(); for (auto w : dis) w->setEnabled(true); } void MainWindow::menuFileNewWindow() { auto w = new MainWindow(); w->show(); } void MainWindow::menuSettings() { SettingsDialog::tryOpen(); } void MainWindow::menuQuit() { auto c = openWindows.size(); if (c > 1) { // prompt if more than just this window auto r = QMessageBox::warning(this, qs("Quit"), qs("Close %1 open projects?").arg(c), static_cast(QMessageBox::Yes | QMessageBox::No)); if (r == QMessageBox::No || r == QMessageBox::Escape) return; } // assemble list std::vector cl; cl.push_back(this); for (auto w : openWindows) if (w != this) cl.push_back(w); // and close for (auto w : cl) w->close(); } 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 actual pattern in sequence, else fall back to first pattern numerically if (!project->sequence[i].pattern()) continue; sequenceSelection(static_cast(i)); break; } //openGraph(project->rootGraph); ui->patchboardBreadcrumbs->clear(); ui->patchboardBreadcrumbs->push("/", this, [this, gg = project->rootGraph] { openGraph(gg); }); ui->editTempo->setValue(project->tempo); ui->editArtist->setText(project->artist); ui->editTitle->setText(project->title); ui->editComment->document()->setPlainText(project->comment); updateTitle(); setSongInfoPaneExpanded(false); if (ui->tabWidget->currentWidget() == ui->patchboard) ui->patchboardView->setFocus(); emit projectLoaded(); } 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); if (ui->patternEditor->isFolded() && editingPattern->fold > 1) row -= row % editingPattern->fold; auto mi = ui->patternEditor->currentIndex().siblingAtRow(row+1); if (!mi.isValid()) mi = ui->patternEditor->model()->index(row+1, 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() { if (!project) return; QString songTitle; if (project->title.isEmpty()) { if (project->fileName.isEmpty()) songTitle = qs("(new project)"); else songTitle = QFileInfo(project->fileName).baseName(); } else { if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist, project->title); else songTitle = project->title; } if (!undoStack->isClean()) songTitle.append(" (modified)"); ui->labelSongInfo->setText(songTitle); setWindowTitle(qs("Xybrid - ") % songTitle); } void MainWindow::updateFont() { QString font = qs("Iosevka Term Light"); double pt = 10.0; QString fstr = qs("font: %2pt \"%1\";").arg(font).arg(pt); QString tfstr = qs("QTableView { %1 }").arg(fstr); QString hfstr = qs("QHeaderView { %1 }").arg(fstr); ui->patternSequencer->setStyleSheet(tfstr); ui->patternEditor->setStyleSheet(tfstr); ui->patternEditor->verticalHeader()->setStyleSheet(hfstr); } void MainWindow::setSongInfoPaneExpanded(bool open) { if (open) { ui->songInfoPane->setCurrentIndex(1); } else { ui->songInfoPane->setCurrentIndex(0); ui->patternEditor->setFocus(Qt::FocusReason::ShortcutFocusReason); } auto s = ui->songInfoPane->currentWidget()->minimumHeight(); ui->songInfoPane->setMinimumHeight(s); ui->songInfoPane->setMaximumHeight(s); } void MainWindow::setFloater(QWidget* w) { auto idx = ui->floaterContainer->indexOf(w); if (idx < 0) idx = 0; ui->floaterContainer->setCurrentIndex(idx); } 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::selectSampleForEditing(std::shared_ptr smp) { if (!smp || smp->project != project.get()) { // no valid sample selected editingSample = nullptr; ui->sampleViewPane->setEnabled(false); ui->sampleInfo->setText(qs("(no sample selected)")); ui->groupSampleLoop->setChecked(false); ui->spinSampleLoopStart->setValue(0); ui->spinSampleLoopEnd->setValue(0); ui->spinSampleNote->setValue(60); ui->spinSampleNoteSub->setValue(0.0); } else { editingSample = nullptr; ui->sampleViewPane->setEnabled(true); ui->sampleInfo->setText( qs("%1 // %2\n%3 %4Hz, %5 frames (%6)") .arg(smp->name.section('/', -1, -1), smp->uuid.toString(), smp->numChannels() == 2 ? qs("Stereo") : qs("Mono")) .arg(smp->sampleRate) .arg(smp->length()) .arg(Util::sampleLength(smp->sampleRate, smp->length())) ); ui->spinSampleLoopStart->setRange(0, smp->length()); ui->spinSampleLoopEnd->setRange(0, smp->length()); if (smp->loopStart < 0) { // loop disabled ui->groupSampleLoop->setChecked(false); ui->spinSampleLoopStart->setValue(0); ui->spinSampleLoopEnd->setValue(smp->length()); } else { ui->groupSampleLoop->setChecked(true); ui->spinSampleLoopStart->setValue(smp->loopStart); ui->spinSampleLoopEnd->setValue(smp->loopEnd); } ui->spinSampleNote->setValue(smp->baseNote); ui->spinSampleNoteSub->setValue(smp->subNote); editingSample = smp; } ui->waveformPreview->setSample(smp); } void MainWindow::openGraph(const std::shared_ptr& g) { if (!g) return; // invalid QPointF scrollPt(g->viewX, g->viewY); //delete ui->patchboardView->scene(); if (auto s = ui->patchboardView->scene(); s) s->deleteLater(); //ui->patchboardView->setScene(nullptr); ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g)); auto sz = ui->patchboardView->viewport()->visibleRegion().boundingRect().size(); ui->patchboardView->centerOn(scrollPt + QPointF(sz.width()/2, sz.height()/2)); } void MainWindow::openNodeUI(const std::shared_ptr& n) { if (!n) return; //delete ui->patchboardView->scene(); if (auto s = ui->patchboardView->scene(); s) s->deleteLater(); ui->patchboardView->setScene(new NodeUIScene(ui->patchboardView, n)); // scene handles initial scroll; don't need to do it here } void MainWindow::openPatternProperties(const std::shared_ptr& p) { if (!p || p->project != project.get()) return; auto dlg = new QDialog(this); dlg->setWindowTitle("Pattern Properties"); dlg->setModal(true); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); //dlg->resize(320, 200); auto dl = new QVBoxLayout(); dlg->setLayout(dl); //dlg->layout()->setMargin(3); auto eName = new QLineEdit(p->name); dl->addWidget(eName); //dlg->layout()->setAlignment(eName, Qt::AlignTop); auto gLength = new QHBoxLayout(); dl->addLayout(gLength); gLength->addWidget(new QLabel("Length")); auto gLengthBox = new QSpinBox(); gLengthBox->setRange(1, 1024); gLengthBox->setValue(p->rows); gLengthBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); gLength->addWidget(gLengthBox); gLength->addWidget(new QLabel("Fold")); auto gFoldBox = new QSpinBox(); gFoldBox->setRange(0, 128); gFoldBox->setValue(p->fold); gFoldBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); gLength->addWidget(gFoldBox); auto gTime = new QHBoxLayout(); dl->addLayout(gTime); auto gTimeBoxW = 60; auto gTimeBoxMax = 32; gTime->addWidget(new QLabel("Beats")); auto gTimeBeats = new QSpinBox(); gTimeBeats->setRange(1, gTimeBoxMax); gTimeBeats->setValue(p->time.beatsPerMeasure); gTimeBeats->setMaximumWidth(gTimeBoxW); gTime->addWidget(gTimeBeats); gTime->addWidget(new QLabel("Rows")); auto gTimeRows = new QSpinBox(); gTimeRows->setRange(1, gTimeBoxMax); gTimeRows->setValue(p->time.rowsPerBeat); gTimeRows->setMaximumWidth(gTimeBoxW); gTime->addWidget(gTimeRows); gTime->addWidget(new QLabel("Ticks")); auto gTimeTicks = new QSpinBox(); gTimeTicks->setRange(1, gTimeBoxMax); gTimeTicks->setValue(p->time.ticksPerRow); gTimeTicks->setMaximumWidth(gTimeBoxW); gTime->addWidget(gTimeTicks); auto bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); dl->addWidget(bbox); connect(bbox, &QDialogButtonBox::rejected, dlg, &QDialog::reject); connect(bbox, &QDialogButtonBox::accepted, dlg, [=] { auto cc = new CompositeCommand(); if (auto n = eName->text(); n != p->name) cc->compose(new PatternRenameCommand(p, n)); if (auto t = Data::TimeSignature(gTimeBeats->value(), gTimeRows->value(), gTimeTicks->value()); t != p->time) cc->compose(new PatternTimeSignatureCommand(p, t)); if (auto nr = gLengthBox->value(); nr != p->rows) { if (nr < p->rows) { // preserve contents beyond cutoff for (auto r = nr; r < p->rows; r++) { for (auto c = 0; c < static_cast(p->numChannels()); c++) { cc->compose(new PatternDeltaCommand(p, c, r)); } } } cc->compose(new PatternLengthCommand(p, nr)); } if (auto f = gFoldBox->value(); f != p->fold) cc->compose(new PatternFoldCommand(p, f)); cc->commit("edit pattern properties"); dlg->accept(); }); dlg->show(); }