997 lines
39 KiB
C++
997 lines
39 KiB
C++
#include "mainwindow.h"
|
|
#include "ui_mainwindow.h"
|
|
#include "settingsdialog.h"
|
|
using Xybrid::MainWindow;
|
|
|
|
#include <QDebug>
|
|
#include <QKeyEvent>
|
|
#include <QShortcut>
|
|
#include <QTabWidget>
|
|
#include <QFileDialog>
|
|
#include <QInputDialog>
|
|
#include <QMessageBox>
|
|
#include <QDialogButtonBox>
|
|
#include <QSpinBox>
|
|
#include <QWindow>
|
|
#include <QUndoStack>
|
|
#include <QTimer>
|
|
#include <QOpenGLWidget>
|
|
|
|
#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 "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*> 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 (i<sz) {
|
|
QFileInfo fi(*ri);
|
|
QString ix = i == 9 ? qs("1&0") : qs("&%1").arg(i+1);
|
|
ac->setText(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<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) {
|
|
auto ind = ui->patternList->indexAt(pt);
|
|
size_t idx = static_cast<size_t>(ind.row());
|
|
std::shared_ptr<Pattern> 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<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("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<double>(&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<size_t>(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<QMouseEvent*>(e);
|
|
if (me->button() == Qt::MouseButton::MiddleButton) {
|
|
auto idx = static_cast<size_t>(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<ptrdiff_t>(idx));
|
|
c->seqSel = static_cast<int>(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<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);
|
|
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, SequenceEntry::Separator);
|
|
c->seqSel = si+1;
|
|
c->commit();
|
|
});
|
|
menu->addAction("Insert Loop Point", 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, SequenceEntry::LoopStart);
|
|
c->seqSel = si+1;
|
|
c->commit();
|
|
});
|
|
menu->addAction("Insert Loop Trigger", 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, 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<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()) if (auto p = project->sequence[idx].pattern(); p) {
|
|
menu->addAction("Duplicate Pattern", this, [this, idx, p] {
|
|
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->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<int>(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<size_t>(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<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] {
|
|
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<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
|
if (editingSample && ui->groupSampleLoop->isChecked()) {
|
|
editingSample->loopStart = v;
|
|
ui->waveformPreview->update();
|
|
}
|
|
});
|
|
connect(ui->spinSampleLoopEnd, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
|
if (editingSample && ui->groupSampleLoop->isChecked()) {
|
|
editingSample->loopEnd = v;
|
|
ui->waveformPreview->update();
|
|
}
|
|
});
|
|
|
|
connect(ui->spinSampleNote, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
|
auto sp = ui->spinSampleNote;
|
|
sp->setSuffix(")");
|
|
sp->setPrefix(qs("%1 (").arg(Util::noteName(static_cast<int16_t>(v))));
|
|
|
|
if (editingSample) editingSample->baseNote = v;
|
|
});
|
|
connect(ui->spinSampleNoteSub, qOverload<double>(&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<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 = 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<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::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::StandardButtons>(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<QWidget*> 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::StandardButtons>(QMessageBox::Yes | QMessageBox::No));
|
|
|
|
if (r == QMessageBox::No || r == QMessageBox::Escape) return;
|
|
}
|
|
|
|
// assemble list
|
|
std::vector<MainWindow*> 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<int>(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<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() {
|
|
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<int>(editingPattern->index), 0));
|
|
|
|
return true;
|
|
}
|
|
|
|
void MainWindow::selectSampleForEditing(std::shared_ptr<Xybrid::Data::Sample> 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<Data::Graph>& 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<Data::Node>& 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<Xybrid::Data::Pattern>& 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<int>(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();
|
|
}
|