Compare commits

...

18 Commits

Author SHA1 Message Date
zetaPRIME b1449ebcf6 attempt to speed up pattern switching 2019-07-22 03:26:39 -04:00
zetaPRIME 43f6374fc2 Merge branch 'portability/macos' of https://git.foxiepa.ws/foxiepaws/xybrid
macOS compatibility fixes
2019-07-21 19:21:10 -04:00
zetaPRIME ffc7be1783 reimplement sample import using ffmpeg 2019-07-21 16:54:39 -04:00
zetaPRIME 3d678c173b bump up delay time fine tuning 2019-07-21 04:19:23 -04:00
zetaPRIME 914f987487 fix fold indicators 2019-07-21 04:13:16 -04:00
zetaPRIME b380959241 fix remaining off-by-ones in the pattern editor 2019-07-21 02:33:07 -04:00
zetaPRIME 24b7c72ef9 waveform and mod dial for THiCC 2019-07-20 20:24:13 -04:00
zetaPRIME 7af2ec5034 separate extension filters for audio import/export 2019-07-20 15:28:04 -04:00
zetaPRIME 70da8cefd6 pick your export filename; better file save defaults 2019-07-20 15:25:35 -04:00
zetaPRIME 8f55ef577b shade outside pattern 2019-07-20 01:14:45 -04:00
zetaPRIME b65ce423a7 follow cursor, spacer rows, shade rows within fold 2019-07-20 01:09:59 -04:00
zetaPRIME 707a6169a2 use VBR V0 for mp3 export 2019-07-19 18:50:04 -04:00
Rachel Fae Fox (foxiepaws) b91a3ee5d5 Merge branch 'master' of https://gitlab.com/zetaPRIME/xybrid into portability/macos 2019-07-18 06:20:58 -04:00
Rachel Fae Fox (foxiepaws) 994deb8d54 Merge branch 'portability/boost' of https://git.foxiepa.ws/foxiepaws/xybrid into portability/macos 2019-07-18 06:20:33 -04:00
Rachel Fae Fox (foxiepaws) fed1365b14 Modified code style on the resampler to make it easier to read. 2019-07-18 06:19:05 -04:00
Rachel Fae Fox (foxiepaws) d4595bc022 Modifications required to build under OSX
- moved QSurfaceFormat::setDefaultFormat(fmt); higher up in main, as
  osx requires this.
- ifdef'ed out two glEnable calls as they cause Xybrid to segfault on OSX
2019-07-18 05:11:43 -04:00
Rachel Fae Fox (foxiepaws) 2e1f3e04cb WITH_BOOST define added
- some platforms don't support C++17 Special Mathmatical functions,
  most notibly macOS and llvm. This enables this to work by using
  boost for the bessel functions
2019-07-18 05:08:22 -04:00
Rachel Fae Fox (foxiepaws) 3f98a5a68f Clang (and possibly other stricter compilers require that you actually include all of what you use. unlike G++ 2019-07-18 02:32:59 -04:00
25 changed files with 413 additions and 146 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
# ignore build artifacts
build-*/*
*.o
# user config
*.codekit
@ -7,3 +8,4 @@ build-*/*
*.pro.user.*
*.stash
*.autosave

14
notes
View File

@ -32,13 +32,13 @@ parameters {
TODO {
immediate frontburner {
spacer rows on top/bottom of pattern editor (keep centered)
make ctrl+pgup/dn jump to the next *actual pattern*, looping around if necessary
global pan (PXX) for InstrumentCore? *default* pan
distortion effect
single-selection sampler
- node function to release unneeded old data when stopping playback
...
global (default) pan (PXX) for InstrumentCore
add ,XX support to global tempo
}
- actual config file loading/saving
@ -51,9 +51,13 @@ TODO {
bugs to fix {
playback after stopping immediately after a note in the first pattern played sometimes skips that note
things can apparently be hooked up cyclically, which completely breaks the queue
pattern switching is slow when changing (especially increasing) number of rows; set fixed page size to avoid reallocation?
}
misc features needed before proper release {
expand/compact pattern 2x/3x, keeping fold interval
at *least* js plugin support, with lua+lv2 highly preferable
different context menu for multiple selected nodes

View File

@ -236,7 +236,8 @@ void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
param << "-y" << "-f" << "s16le" << "-ac" << "2" << "-ar" << QString::number(sampleRate) << "-i" << "pipe:";
if (!project->title.isEmpty()) param << "-metadata" << qs("title=%1").arg(project->title);
if (!project->artist.isEmpty()) param << "-metadata" << qs("artist=%1").arg(project->artist);
param << "-f" << "mp3" << filename;
param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0"; // specify mp3, vbr v0
param << filename;
enc.start("ffmpeg", param);
enc.waitForStarted();

View File

@ -6,7 +6,7 @@
#include <deque>
#include <unordered_map>
#include <atomic>
#include <array>
#include <QIODevice>
#include <QAudioOutput>
#include <QSemaphore>

View File

@ -7,22 +7,24 @@ namespace Xybrid::Config {
public:
ColorScheme() = default;
QColor patternSelection = QColor(127, 63, 255, 63);
QColor patternFoldIndicator = QColor(95, 95, 95);
QColor patternSelection = {127, 63, 255, 63};
QColor patternFoldIndicator = {95, 95, 95};
QColor patternFoldShade = {0, 0, 0, 31};
QColor patternOuterShade = {0, 0, 0, 63};
QColor patternBg = QColor(23, 23, 23);
QColor patternBgBeat = QColor(31, 31, 31);
QColor patternBgMeasure = QColor(39, 39, 39);
QColor patternBg = {23, 23, 23};
QColor patternBgBeat = {31, 31, 31};
QColor patternBgMeasure = {39, 39, 39};
QColor patternFgBlank = QColor(127, 127, 127);
QColor patternFgPort = QColor(191, 191, 191);
QColor patternFgNote = QColor(255, 255, 255);
QColor patternFgParamCmd = QColor(191,163,255);
QColor patternFgParamAmt = QColor(191,222,255);
QColor patternFgBlank = {127, 127, 127};
QColor patternFgPort = {191, 191, 191};
QColor patternFgNote = {255, 255, 255};
QColor patternFgParamCmd = {191, 163, 255};
QColor patternFgParamAmt = {191, 222, 255};
QColor waveformBg = QColor(23, 23, 23);
QColor waveformBgHighlight = QColor(31, 31, 47);
QColor waveformFgPrimary = QColor(191, 163, 255);
QColor waveformBg = {23, 23, 23};
QColor waveformBgHighlight = {31, 31, 47};
QColor waveformFgPrimary = {191, 163, 255};
};
extern ColorScheme colorScheme;
}

View File

@ -7,7 +7,7 @@
#include <list>
#include <vector>
#include <string>
#include <array>
#include <QString>
namespace Xybrid::Data {

View File

@ -31,6 +31,7 @@ namespace Xybrid::Data {
QString comment;
QString fileName;
QString exportFileName;
double tempo = 140.0;
TimeSignature time;

View File

@ -9,11 +9,18 @@ using namespace Xybrid::Data;
#include <QCborMap>
#include <QCborArray>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QFileInfo>
#include <QAudioDecoder>
#include <QAudioFormat>
#include <QProcess>
#include <QDataStream>
#include <QEventLoop>
#define qs QStringLiteral
@ -88,6 +95,7 @@ bool Sample::changeUuid(QUuid newUuid) {
void Sample::newUuid() { changeUuid(QUuid::createUuid()); }
#ifdef OLD_SAMPLE_IMPORT
namespace {
bool blah [[maybe_unused]];
template<typename T> void insertbuf(std::shared_ptr<Sample> smp, const T* data, size_t len, size_t channels) {
@ -154,6 +162,63 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
return smp;
}
#else
std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
QJsonObject info;
{
// get stream info
QProcess probe;
QStringList param;
param << "-v" << "quiet" << "-show_streams" << "-select_streams" << "a" << "-of" << "json";
param << fileName;
probe.start("ffprobe", param);
probe.waitForFinished();
auto doc = QJsonDocument::fromJson(probe.readAllStandardOutput());
info = doc.object()["streams"].toArray().first().toObject();
}
if (!info.contains("sample_rate") || !info.contains("channels")) return nullptr; // no/invalid audio streams
int channels = info["channels"].toInt();
int sampleRate = info["sample_rate"].toString().toInt(); // for some reason ffprobe stores this as a string??
auto smp = std::make_shared<Sample>();
smp->sampleRate = sampleRate;
// grab raw pcm_f32le via ffmpeg
QByteArray raw;
{
QProcess dec;
QStringList param;
param << "-i" << fileName << "-f" << "f32le" << "-acodec" << "pcm_f32le" << "-";
dec.start("ffmpeg", param);
dec.waitForFinished();
raw = dec.readAllStandardOutput();
}
// pre-size sample data buffers
auto len = static_cast<size_t>((raw.length() / channels) / 4);
smp->data[0].reserve(len);
if (channels > 1) smp->data[1].reserve(len);
// read raw bytes into channels
QDataStream r(&raw, QIODevice::ReadOnly);
size_t chs = static_cast<size_t>(channels);
size_t ch = chs;
float f;
while (!r.atEnd()) {
r.readRawData(reinterpret_cast<char*>(&f), sizeof(f));
ch = (ch+1) % chs;
if (ch < 2) smp->data[ch].push_back(f);
}
// add info
smp->uuid = QUuid::createUuid();
smp->name = QFileInfo(fileName).baseName();
return smp;
}
#endif
namespace {
bool exporting = false;

View File

@ -38,8 +38,11 @@ namespace {
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1);
}
const QString FileOps::Filter::project = u8"Xybrid project (*.xyp);;All files (*)";
const QString FileOps::Filter::node = u8"Xybrid node (*.xyn);;All files (*)";
const QString FileOps::Filter::project = qs("Xybrid project (*.xyp);;All files (*)");
const QString FileOps::Filter::node = qs("Xybrid node (*.xyn);;All files (*)");
const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3, *.ogg, *.flac, *.wav);;MPEG Layer 3 (*.mp3);;All files (*)");
const QString FileOps::Filter::audioOut = qs("MPEG Layer 3 (*.mp3)"); // only supported formats
QString FileOps::showOpenDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter) {
return QFileDialog::getOpenFileName(parent, caption, directory, filter); // just a wrapper for now

View File

@ -16,6 +16,9 @@ namespace Xybrid::FileOps {
namespace Filter {
extern const QString project;
extern const QString node;
extern const QString audioIn;
extern const QString audioOut;
}
QString showOpenDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString());
QString showSaveAsDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString(), const QString& suffix = QString());

View File

@ -13,13 +13,15 @@
int main(int argc, char *argv[]) {
qRegisterMetaType<Xybrid::Data::Port>();
QApplication a(argc, argv);
// enable antialiasing on accelerated graphicsview
QSurfaceFormat fmt;
fmt.setSamples(10);
QSurfaceFormat::setDefaultFormat(fmt);
QApplication a(argc, argv);
// make sure bundled fonts are loaded
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");

View File

@ -121,11 +121,6 @@ MainWindow::MainWindow(QWidget *parent) :
}
});
// TODO: temp: render shortcut
connect(new QShortcut(QKeySequence("Ctrl+E"), this), &QShortcut::activated, this, [this]() {
audioEngine->render(project, Config::Directories::projects % "/testOut.mp3");
});
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
}
@ -325,8 +320,11 @@ MainWindow::MainWindow(QWidget *parent) :
view->setViewport(vp); // enable hardware acceleration
}
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
// Under OSX these cause Xybrid to crash.
#ifndef __APPLE__
glEnable(GL_MULTISAMPLE);
glEnable(GL_LINE_SMOOTH);
#endif
//QGL::FormatOption::Rgba
@ -483,13 +481,37 @@ void MainWindow::menuFileSave() {
}
void MainWindow::menuFileSaveAs() {
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", Config::Directories::projects, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
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);
undoStack->setClean();
updateTitle();
}
}
void MainWindow::menuFileExport() {
if (project->exportFileName.isEmpty()) menuFileExportAs();
else {
audioEngine->render(project, project->exportFileName);
}
}
void MainWindow::menuFileExportAs() {
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::audioOut, "mp3"); !fileName.isEmpty()) {
project->exportFileName = fileName;
audioEngine->render(project, project->exportFileName);
}
}
void MainWindow::menuFileNewWindow() {
auto w = new MainWindow();
w->show();
@ -541,9 +563,9 @@ int MainWindow::sequenceSelection(int n) {
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);
auto mi = ui->patternEditor->currentIndex().siblingAtRow(row+1);
if (!mi.isValid()) mi = ui->patternEditor->model()->index(row, 0);
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);

View File

@ -65,6 +65,9 @@ namespace Xybrid {
void menuFileSave();
void menuFileSaveAs();
void menuFileExport();
void menuFileExportAs();
void menuFileNewWindow();
signals:

View File

@ -835,6 +835,9 @@
<addaction name="actionSave"/>
<addaction name="actionSave_As"/>
<addaction name="separator"/>
<addaction name="actionExport"/>
<addaction name="actionExport_As"/>
<addaction name="separator"/>
<addaction name="actionNew_Window"/>
<addaction name="actionClose_Window"/>
</widget>
@ -898,7 +901,7 @@
</action>
<action name="actionNew_Window">
<property name="text">
<string>New Window</string>
<string>New &amp;Window</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+N</string>
@ -906,12 +909,28 @@
</action>
<action name="actionClose_Window">
<property name="text">
<string>Close Window</string>
<string>&amp;Close Window</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionExport">
<property name="text">
<string>&amp;Export</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
<action name="actionExport_As">
<property name="text">
<string>E&amp;xport As...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+E</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
@ -1032,6 +1051,38 @@
</hint>
</hints>
</connection>
<connection>
<sender>actionExport</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>menuFileExport()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>459</x>
<y>305</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionExport_As</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>menuFileExportAs()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>459</x>
<y>305</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>menuFileNew()</slot>
@ -1039,5 +1090,7 @@
<slot>menuFileSave()</slot>
<slot>menuFileSaveAs()</slot>
<slot>menuFileNewWindow()</slot>
<slot>menuFileExport()</slot>
<slot>menuFileExportAs()</slot>
</slots>
</ui>

View File

@ -3,7 +3,7 @@
#include <memory>
#include <functional>
#include <unordered_map>
#include <array>
#include "nodelib/basics.h"
#include "data/node.h"

View File

@ -1,8 +1,17 @@
#include "resampler.h"
using namespace Xybrid::NodeLib;
#include <cmath>
#include <iostream>
#include <array>
#ifdef WITH_BOOST
#include <boost/math/special_functions/bessel.hpp>
#define cyl_bessel_i boost::math::cyl_bessel_i
#else
#include <cmath>
#define cyl_bessel_i std::cyl_bessel_i
#endif
namespace {
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
@ -17,9 +26,12 @@ namespace {
}
}
// generate
const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
double denom = std::cyl_bessel_i(0, KAISER_BETA);
double denom = cyl_bessel_i(0, KAISER_BETA);
std::array<std::array<double, LUT_TAPS>, LUT_STEPS> t;
t[0] = {0, 0, 0, 1, 0, 0, 0, 0}; // we already know the ideal integer step
@ -27,7 +39,7 @@ const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resam
double sv = static_cast<double>(step) / LUT_STEPS;
for (size_t tap = 0; tap < LUT_TAPS; tap++) {
double x = static_cast<double>(tap) - sv;
t[step][tap] = sinc(x-(LUT_TAPS/2-1)) * (std::cyl_bessel_i(0, KAISER_BETA * std::sqrt(1 - std::pow(((2 * (x+1)) / (LUT_TAPS)) - 1, 2))) / denom);
t[step][tap] = sinc(x-(LUT_TAPS/2-1)) * (cyl_bessel_i(0, KAISER_BETA * std::sqrt(1 - std::pow(((2 * (x+1)) / (LUT_TAPS)) - 1, 2))) / denom);
if (t[step][tap] != t[step][tap]) t[step][tap] = 0; // NaN guard
//std::cout << "tap " << tap << ": " << t[step][tap] << " ";
}

View File

@ -102,10 +102,10 @@ void Delay::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
(new KnobGadget(l))->bind(delayTime)->setLabel("Time")->setRange(0.0, 5.0)->setDefault(0.5);
(new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip("BPM-relative");
(new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001)->setDefault(0.5);
(new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
l->addSpacer();
(new KnobGadget(l))->bind(amount)->setLabel("Amount")->setDefault(0.5);
(new KnobGadget(l))->bind(amount)->setLabel(qs("Amount"))->setDefault(0.5);
l->addSpacer();
(new KnobGadget(l))->bind(feedback)->setLabel("Feedback")->setDefault(0.0);
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setDefault(0.0);
}

View File

@ -20,6 +20,7 @@ using namespace Xybrid::UI;
#include "util/strings.h"
#include <cmath>
#include <array>
#include <QDebug>
#include <QRandomGenerator>
@ -44,7 +45,7 @@ namespace {
else return d;
}
}
[[maybe_unused]] inline double lerp(double p, double a, double b) {
[[maybe_unused]] inline double lerp(double a, double b, double p) {
return b * p + a * (1.0 - p);
}
@ -68,24 +69,54 @@ namespace {
return 0.0;
}
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta) {
[[gnu::optimize("O3")]] double push(double in, double mod, double factor) {
double s = in < 0 ? -1 : 1;
in *= s;
//if (mod < 0) mod = 1.0/-mod;
//else mod += 1.0;
mod = mod < 0 ? lerp(1.0, 1.0/factor, -mod) : lerp(1.0, factor, mod);
return std::pow(in, mod)*s;
}
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta, double mod) {
phase = std::fmod(phase + 0.5, 1.0);
double d = phase;// * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
d = d * 2.0 - 1.0;
d = push(d, -mod, 5);
d -= polyblep(phase, delta);
return d;
}
[[gnu::optimize("O3")]] double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }
[[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double mod) {
double duty = (mod+1.0)/2.0;
double d = 1.0;
if (std::fmod(phase, 1.0) >= duty) d = -1.0;
d += polyblep(std::fmod(phase, 1.0), delta);
d -= polyblep(std::fmod(phase + (1.0 - duty), 1.0), delta);
return d;
}
#pragma GCC diagnostic pop
// wave function list(s)
const constexpr std::array waveFunc = {
&oscSaw,
&oscSine,
&oscPulse,
};
const std::array waveName = {
qs("saw"),
qs("sine"),
qs("pulse"),
};
}
Thicc::Thicc() { }
void Thicc::init() {
double avg = 0;
for (int i = 0; i < 4096; i++) avg += std::abs(std::fmod(static_cast<double>(i) / 4096.0, 1.0) * 2.0 - 1.0);
qDebug() << "average of" << (avg / 4096.0);
addPort(Port::Input, Port::Command, 0);
addPort(Port::Output, Port::Audio, 0);
@ -95,14 +126,16 @@ void Thicc::init() {
note.scratch[0] = static_cast<double>(QRandomGenerator::global()->generate() % 573000);
};
core.globalParam['Q'] = [](const ParamReader& pr) {
/*core.globalParam['Q'] = [](const ParamReader& pr) {
qDebug() << "global recieved" << pr.param() << pr.val();
return true;
};
};*/
core.processNote = [this](Note& note, AudioPort* p) {
double freq;
auto osc = waveFunc[static_cast<size_t>(wave)];
int vc = voices;
double vf = static_cast<double>(vc);
double spr = std::pow(SEMI, detune/(vf/2.0));
@ -118,8 +151,6 @@ void Thicc::init() {
double delta = smpTime * freq;
note.scratch[0] += delta;
// s
double o = 0;
for (int i = 0; i < vc; i++) {
auto ii = static_cast<double>(i);
@ -128,18 +159,13 @@ void Thicc::init() {
double dm = std::pow(spr, dc);
double sg = i % 2 == 0 ? 1.0 : -1.0;
//o += (std::fmod(pc + si * dm, 1.0) * 2.0 - 1.0) * sg;
o += oscSaw(pc + si * dm, delta * dm) * sg;
o += osc(pc + si * dm, delta * dm, mod) * sg;
}
o /= 1.0 + ((vf - 1.0)/3.0);
// e
note.scratch[3] += /*(smpTime / (1.0/20000.0))*/0.5 * (o - note.scratch[3]); // simple low pass;
if (note.scratch[3] != note.scratch[3]) note.scratch[3] = 0; // nan!? WHY
AudioFrame out = o;//note.scratch[3];
o /= 1.0 + ((vf - 1.0)/5.0);
AudioFrame out = o;
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
}
};
@ -152,25 +178,31 @@ void Thicc::process() { core.process(this); }
void Thicc::saveData(QCborMap& m) const {
m[qs("adsr")] = adsr;
m[qs("wave")] = wave;
m[qs("voices")] = voices;
m[qs("mod")] = mod;
m[qs("detune")] = detune;
//m[qs("shift")] = shift;
}
void Thicc::loadData(const QCborMap& m) {
adsr = m.value("adsr");
wave = static_cast<int>(m.value("wave").toInteger(wave));
voices = static_cast<int>(m.value("voices").toInteger(voices));
mod = m.value("mod").toDouble(mod);
detune = m.value("detune").toDouble(detune);
//shift = m.value("shift").toDouble(shift);
}
void Thicc::onGadgetCreated() {
auto wn = [](size_t i) { if (i >= waveName.size()) return QString::number(i); return waveName[i]; };
auto l = new LayoutGadget(obj);
(new KnobGadget(l))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wn)->setRange(0, waveFunc.size()-1, 1, KnobGadget::BigStep);
(new KnobGadget(l))->bind(mod)->setLabel(qs("W. Mod"))->setRange(-1.0, 1.0, 0.01);
l->addSpacer();
(new KnobGadget(l))->bind(voices)->setLabel(qs("Voices"))->setRange(1, 16, 1, KnobGadget::BigStep)->setDefault(1);
(new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setRange(0.0, 1.0, 0.001);
//(new KnobGadget(l))->bind(shift)->setLabel(qs("Shift"))->setRange(0.0, 25.0, 0.01);
l->addSpacer();
KnobGadget::autoCreate(l, adsr);
}

View File

@ -8,10 +8,11 @@ namespace Xybrid::Instruments {
NodeLib::ADSR adsr;
int wave = 0;
int voices = 1;
double mod = 0.0;
double detune = 0.0;
//double shift = 0.0;
public:
Thicc();

View File

@ -91,19 +91,22 @@ namespace {
}
void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
int row = index.row() - 1;
auto mdl = const_cast<PatternEditorModel*>(static_cast<const PatternEditorModel*>(index.model()));
auto p = mdl->getPattern();
{ /* background */ } {
painter->fillRect(option.rect, Config::colorScheme.patternBg);
if (row == -1 || row >= p->rows) painter->fillRect(option.rect, Config::colorScheme.patternOuterShade);
else if (p->fold > 1 && row % p->fold != 0) painter->fillRect(option.rect, Config::colorScheme.patternFoldShade);
if (option.state & QStyle::State_Enabled) {
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgMeasure);
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgBeat);
if (row % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgMeasure);
else if (row % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgBeat);
}
}
// selection/cursor highlight
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, Config::colorScheme.patternSelection);
if (option.state & QStyle::State_HasFocus) {
if (option.state & QStyle::State_HasFocus && p->channels.size() > 0) {
painter->setPen(Config::colorScheme.patternSelection);
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
@ -111,15 +114,15 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
//painter->fillRect(option.rect, Config::colorScheme.patternSelection);
}
if (mdl->folded && p->fold > 1) {
if (row > 0 && mdl->folded && p->fold > 1) {
bool show = false;
int cc = index.column() % PatternEditorModel::colsPerChannel;
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
if (ch < static_cast<int>(p->numChannels())) {
int row = index.row();
int rowEnd = row + p->fold;
for (int i = row + 1; i < rowEnd; i++) {
int rs = row + 1;
int rowEnd = rs + p->fold;
for (int i = rs + 1; i < rowEnd; i++) {
QString v = index.siblingAtRow(i).data(Qt::DisplayRole).toString();
if (!(v.isEmpty() or v == " - " or v == "- " or v == "» ")) { show = true; break; }
}
@ -178,7 +181,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
auto p = m->getPattern();
int cc = index.column() % PatternEditorModel::colsPerChannel;
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
auto* dc = new PatternDeltaCommand(p, ch, index.row());
auto* dc = new PatternDeltaCommand(p, ch, index.row() - 1);
auto& row = dc->row;//p->rowAt(ch, index.row());
auto* sm = static_cast<PatternEditorView*>(parent())->selectionModel();
@ -205,7 +208,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
auto mps = std::min(mpc, static_cast<size_t>(mp));
for (int r = s.y1; r <= s.y2; r++) {
if (multi && mps <= p->rowAt(c, r).numParams()) continue;
auto* dc = new PatternDeltaCommand(p, c, r);
auto* dc = new PatternDeltaCommand(p, c, r-1);
for (size_t i = dc->row.numParams(); i < mps; i++) dc->row.addParam(' ');
if (!multi) dc->row.param(mps) = {' ', 0};
cc->compose(dc);
@ -221,7 +224,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
for (int c = s.ch1; c <= s.ch2; c++) {
for (int r = s.y1; r <= s.y2; r++) {
auto* dc = new PatternDeltaCommand(p, c, r);
auto* dc = new PatternDeltaCommand(p, c, r-1);
if (s.portSelected(c)) dc->row.port = -1;
if (s.noteSelected(c)) dc->row.note = -1;
for (int i = static_cast<int>(dc->row.numParams()) - 1; i >= 0; i--) {

View File

@ -5,16 +5,20 @@ using Xybrid::Data::Pattern;
#include "ui/patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include "util/strings.h"
#include <QDebug>
#include <QString>
#include <QFontMetrics>
#include <QTimer>
#include <QScrollBar>
namespace { // helper functions
int cellWidthBase = -1;
int cellWidthParam;
int cellWidthParamTab;
int headerHeight;
QSize hsz, hhsz;
constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
@ -22,21 +26,21 @@ namespace { // helper functions
constexpr char notemap[] = "C-C#D-D#E-F-F#G-G#A-A#B-";
std::string hexStr(unsigned char *data, uint len) {
std::string s(len * 2, ' ');
for (uint i = 0; i < len; ++i) {
QString hexStr(unsigned char *data, int len) {
QString s(len * 2, ' ');
for (int i = 0; i < len; ++i) {
s[2 * i] = hexmap[(data[i] & 0xF0) >> 4];
s[2 * i + 1] = hexmap[data[i] & 0x0F];
}
return s;
}
std::string byteStr(int t) {
QString byteStr(int t) {
unsigned char c = static_cast<unsigned char>(t & 255);
return hexStr(&c, 1);
}
std::string noteStr(int n) {
std::string s(3, ' ');
QString noteStr(int n) {
QString s(3, ' ');
int nn = n % 12;
int oc = (n - nn) / 12;
s[2] = '0' + static_cast<char>(oc);
@ -63,11 +67,22 @@ QVariant PatternEditorHeaderProxyModel::headerData(int section, Qt::Orientation
PatternEditorModel::PatternEditorModel(QObject *parent)
:QAbstractTableModel(parent) {
hprox = new PatternEditorHeaderProxyModel(parent, this);
auto view = static_cast<PatternEditorView*>(parent);
int rowHeight = [] {
QFontMetrics fm = QFontMetrics(QFont());
return (fm.boundingRect("255").size() + QSize(8, 4)).height();
}();
endHeight = (view->viewport()->height() - rowHeight) / 2;
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, view, rowHeight] {
endHeight = (view->viewport()->height() - rowHeight) / 2;
emit this->layoutChanged();
});
}
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
//if (pattern->channels.size() == 0) return 1;
return pattern->rows + 1;
return pattern->rows + 2;
}
int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
@ -77,34 +92,33 @@ int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) {
if (index.row() >= pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
if (index.row() == 0) return QVariant();
if (index.row() > pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
int cc = index.column() % colsPerChannel;
int ch = (index.column() - cc) / colsPerChannel;
auto& row = pattern->rowAt(ch, index.row());
auto& row = pattern->rowAt(ch, index.row()-1);
if (cc == 0) { // port
if (row.port >= 0 && row.port < 256) return QString::fromStdString(byteStr(row.port));
if (row.port == -2) return QString("(G)");
if (row.port == -3) return QString("L");
return QString(" - ");
if (row.port >= 0 && row.port < 256) return byteStr(row.port);
if (row.port == -2) return qs("(G)");
if (row.port == -3) return qs("L");
return qs(" - ");
} else if (cc == 1) { // note
if (row.note >= 0) return QString::fromStdString(noteStr(row.note));
if (row.note == -2) return QString(" ^ "); // note off
if (row.note == -3) return QString(" x "); // hard cut
return QString(" - ");
if (row.note >= 0) return noteStr(row.note);
if (row.note == -2) return qs(" ^ "); // note off
if (row.note == -3) return qs(" x "); // hard cut
return qs(" - ");
} else {
size_t cp = static_cast<size_t>(((cc - 2) - (cc % 2)) / 2);
//return QString::number((cp));
if (cc % 2 == 0) {
if (row.numParams() > cp) return QString::fromStdString(std::string(1,static_cast<char>(row.params->at(cp)[0])));
if (row.numParams() == cp) return QString("» ");
return QString("");
if (row.numParams() > cp) return QString(1,static_cast<char>(row.params->at(cp)[0]));
if (row.numParams() == cp) return qs("» ");
return qs("");
}
if (row.numParams() > cp) {
if (row.params->at(cp)[0] == ' ') return QString("- ");
return QString::fromStdString(byteStr(row.params->at(cp)[1]));
if (row.params->at(cp)[0] == ' ') return qs("- ");
return byteStr(row.params->at(cp)[1]);
}
return QString("");
//return QString("--");
return qs("");
}
} else if (role == Qt::SizeHintRole) {
if (index.row() >= pattern->rows) return QSize(-1, -1);
@ -115,21 +129,24 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Orientation::Horizontal) return QVariant(); // blank actual-header
if (section >= pattern->rows) return QVariant(); // blank end section
return QString::number(section);
if (section == 0 || section > pattern->rows) return QVariant(); // blank end section
return QString::number(section-1);
} else if (role == Qt::SizeHintRole) {
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
if (orientation == Qt::Orientation::Vertical) {
if (section >= pattern->rows) return QSize(-1, -1); // take no space if no hanging room
return fm.boundingRect("255").size() + QSize(8, 4); // this should fit 0-999
if (orientation == Qt::Orientation::Vertical) if (section == 0 || section > pattern->rows) return QSize(-1, endHeight); // fill ends to center
if (hsz.isEmpty()) {
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
hsz = fm.boundingRect("255").size() + QSize(8, 4); // this should fit 0-999
hhsz = QSize(0, hsz.height());
}
return QSize(0, fm.height() + 4);
return orientation == Qt::Vertical ? hsz : hhsz;
//return QSize(0, fm.height() + 4);
} else if (role == Qt::TextAlignmentRole) return Qt::AlignCenter;
return QVariant();
}
Qt::ItemFlags PatternEditorModel::flags(const QModelIndex &index) const {
if (index.row() >= pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) {
if (index.row() == 0 || index.row() >= pattern->rows + 1 || (pattern->channels.size() > 0 && index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size()))) {
return QAbstractTableModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
}
return QAbstractTableModel::flags(index);
@ -150,19 +167,10 @@ QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) con
if (cellWidthBase <= 0) {
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
headerHeight = fm.height()+4;
cellWidthBase = fm.width(QString("FF")) + fm.width(QString("C#2")) + cellPadding*4;
cellWidthParamTab = fm.width(QString("v")) + cellPadding;
cellWidthParam = cellWidthParamTab + fm.width(QString("FF")) + cellPadding;
}/*
auto& c = pattern->channels.at(static_cast<size_t>(section));
size_t maxParams = 0;
for (auto& r : c.rows) {
if (r.numParams() > maxParams) maxParams = r.numParams();
cellWidthBase = fm.horizontalAdvance(QString("FF")) + fm.horizontalAdvance(QString("C#2")) + cellPadding*4;
cellWidthParamTab = fm.horizontalAdvance(QString("v")) + cellPadding;
cellWidthParam = cellWidthParamTab + fm.horizontalAdvance(QString("FF")) + cellPadding;
}
int width = cellWidthBase;
width += static_cast<int>(maxParams) * cellWidthParam;
if (maxParams < paramSoftCap) width += cellWidthParamTab;
return QSize(width, headerHeight);*/
return QSize(0, headerHeight);
}
return QVariant();
@ -175,12 +183,10 @@ void PatternEditorModel::setPattern(const std::shared_ptr<Pattern>& pattern) {
}
void PatternEditorModel::updateColumnDisplay() {
/*static int qi = 0;
qDebug() << QString("column display request #%1").arg(qi++);//*/
if (pattern == nullptr) return;
auto view = static_cast<PatternEditorView*>(parent());
view->setUpdatesEnabled(false);
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
//view->setUpdatesEnabled(false);
//auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
for (size_t ch = 0; ch < pattern->channels.size(); ch++) {
auto& c = pattern->channels.at(ch);
size_t maxParams = 0;
@ -213,21 +219,23 @@ void PatternEditorModel::updateColumnDisplay() {
int lsw = view->columnWidth(lastShown);
view->setColumnWidth(lastShown, std::max(lsw + 3, minWidth - (chWidth - lsw)));
}
view->setUpdatesEnabled(true);
//view->setUpdatesEnabled(true);
view->updateHeader(true);
}
void PatternEditorModel::updateFold() {
auto view = static_cast<PatternEditorView*>(parent());
view->setUpdatesEnabled(false);
//view->setUpdatesEnabled(false);
int ifold = 1;
if (folded && pattern->fold > 1) ifold = pattern->fold;
int rows = this->rowCount();
int rows = this->rowCount()-2;
view->setRowHidden(0, false);
view->setRowHidden(rows+1, false);
for (int i = 0; i < rows; i++) {
view->setRowHidden(i, false); // dispel any "phantoms" we might end up having
view->setRowHidden(i, (i < pattern->rows && i % ifold != 0));
view->setRowHidden(i+1, false); // dispel any "phantoms" we might end up having
if (i < pattern->rows && i % ifold != 0) view->setRowHidden(i+1, true);
}
view->setUpdatesEnabled(true);
//view->setUpdatesEnabled(true);
}
void PatternEditorModel::toggleFold() {

View File

@ -20,6 +20,7 @@ namespace Xybrid::UI {
bool fitHeaderToName = false;
bool folded = true;
int endHeight = -1;
PatternEditorModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;

View File

@ -71,7 +71,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
horizontalHeader()->setStretchLastSection(true);
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
verticalHeader()->setMinimumSectionSize(0);
verticalHeader()->setStretchLastSection(true);
//verticalHeader()->setStretchLastSection(true);
verticalHeader()->setSectionsClickable(false);
setCornerButtonEnabled(false);
//verticalHeader()->setDefaultAlignment(Qt::AlignTop);
@ -96,6 +96,10 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
setModel(&*mdl);
hdr->setModel(&*mdl->hprox);
// smooth scroll
setVerticalScrollMode(ScrollMode::ScrollPerPixel);
setHorizontalScrollMode(ScrollMode::ScrollPerPixel);
{ /* set up hotkeys */ } {
// transpose notes
auto transpose = [this](int amt, int key = Qt::Key_Alt) {
@ -109,8 +113,8 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
auto ch = Util::channelForColumn(sel.left());
auto last = sel.bottom();
for (auto i = sel.top(); i <= last; i++) {
if (p->rowAt(ch, i).port >= 0) {
auto c = new PatternDeltaCommand(p, ch, i);
if (p->rowAt(ch, i-1).port >= 0) {
auto c = new PatternDeltaCommand(p, ch, i-1);
auto op = c->row.port;
c->row.port = static_cast<int16_t>(std::clamp(op + amt, 0, 255));
if (c->row.port == op) c->cancel();
@ -120,7 +124,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
cc->commit("change note port(s)");
} else if (startCol >= 2 && sel.height() == 1) { // single param
amt = std::clamp(amt, -16, 16);
auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top());
auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top()-1);
auto& r = c->row;
auto par = static_cast<size_t>((sel.left() % Util::colsPerChannel) - 2) / 2;
if (r.numParams() > par) {
@ -134,7 +138,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
for (auto s : sel.indexes()) {
if (s.column() % Util::colsPerChannel != 1) continue;
int ch = Util::channelForColumn(s.column());
auto c = new PatternDeltaCommand(p, ch, s.row());
auto c = new PatternDeltaCommand(p, ch, s.row()-1);
if (c->row.note >= 0) {
c->row.note = static_cast<int16_t>(std::max(c->row.note + amt, 0));
cc->compose(c);
@ -154,15 +158,19 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
// fold
connect(new QShortcut(QKeySequence("Ctrl+Space"), this), &QShortcut::activated, this, [this] {
setUpdatesEnabled(false);
mdl->toggleFold();
auto p = mdl->getPattern();
auto ind = currentIndex();
if (mdl->folded && p->fold > 1 && ind.row() % p->fold != 0) {
ind = ind.siblingAtRow(ind.row() - (ind.row() % p->fold));
int row = ind.row() - 1;
if (mdl->folded && p->fold > 1 && row % p->fold != 0) {
ind = ind.siblingAtRow(1 + row - (row % p->fold));
setCurrentIndex(ind);
if (selectedIndexes().count() <= 1) selectionModel()->select(ind, QItemSelectionModel::SelectionFlag::ClearAndSelect);
}
QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
scrollTo(ind, ScrollHint::PositionAtCenter);
setUpdatesEnabled(true);
//QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
});
// cut/copy/paste
@ -188,7 +196,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
chm << first << last;
for (int r = sel.top(); r <= sel.bottom(); r++) {
QCborArray rm;
auto& row = p->rowAt(ch, r);
auto& row = p->rowAt(ch, r-1);
rm << row.port;
rm << row.note;
if (row.params) for (auto p : *row.params) rm << p[0] << p[1];
@ -215,7 +223,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
auto p = mdl->getPattern();
auto idx = currentIndex();
auto chMin = Util::channelForColumn(idx.column());
auto rMin = idx.row();
auto rMin = idx.row()-1;
auto root = QCborValue::fromCbor(data->data("xybrid-internal/x-pattern-copy")).toArray();
@ -271,7 +279,9 @@ void PatternEditorView::keyPressEvent(QKeyEvent* e) {
return;
}
}
QAbstractItemView::keyPressEvent(e);
auto prevInd = currentIndex();
QTableView::keyPressEvent(e);
if (auto ind = currentIndex(); ind != prevInd) scrollTo(ind, ScrollHint::PositionAtCenter); // cursor position changed, scroll to keep up
if (!e->isAutoRepeat()) {
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
startPreview(Util::unshiftedKey(e->key()));
@ -284,13 +294,22 @@ void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
if (!e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}
void PatternEditorView::mouseReleaseEvent(QMouseEvent* e) {
QTableView::mouseReleaseEvent(e);
auto sm = selectionModel();
if (sm->selectedIndexes().size() <= 1) {
auto ind = indexAt(e->localPos().toPoint());
if (ind.isValid() && model()->flags(ind) & Qt::ItemFlag::ItemIsEnabled) scrollTo(ind, ScrollHint::PositionAtCenter);
}
}
void PatternEditorView::startPreview(int key) {
auto ind = currentIndex();
int cc = ind.column() % PatternEditorModel::colsPerChannel;
int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
if (cc == 1) { // note column
stopPreview(key); // end current preview first, if applicable
auto& r = mdl->getPattern()->rowAt(ch, ind.row());
auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
auto p = mdl->getPattern()->project->shared_from_this();
previewKey[key] = {r.port, audioEngine->preview(p, r.port, r.note)};
}
@ -305,10 +324,19 @@ void PatternEditorView::stopPreview(int key) {
}
void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {
setUpdatesEnabled(false);
mdl->setPattern(pattern);
//horizontalHeader()->isSectionHidden()
//columnCountChanged(0, mdl->columnCount());
rowCountChanged(0, mdl->rowCount());
//rowCountChanged(0, mdl->rowCount());
auto cur = currentIndex();
auto ind = model()->index(std::clamp(cur.row(), 1, pattern->rows), std::clamp(cur.column(), 0, std::max(0, mdl->columnCount() - 2)));
while (isColumnHidden(ind.column())) ind = ind.siblingAtColumn(ind.column()-1);
while (isRowHidden(ind.row())) ind = ind.siblingAtRow(ind.row()-1);
setCurrentIndex(ind);
selectionModel()->select(ind, QItemSelectionModel::ClearAndSelect);
scrollTo(ind, ScrollHint::PositionAtCenter);
setUpdatesEnabled(true);
}
void PatternEditorView::updateGeometries() {

View File

@ -36,6 +36,8 @@ namespace Xybrid::UI {
void keyReleaseEvent(QKeyEvent*) override;
void keyboardSearch(const QString&) override {} // disable accidental search
void mouseReleaseEvent(QMouseEvent*) override;
void setPattern(const std::shared_ptr<Data::Pattern>& pattern);
void updateHeader(bool full = false);

View File

@ -17,6 +17,7 @@ TEMPLATE = app
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
@ -36,6 +37,24 @@ HEADERS += $$files(*.h, true) \
FORMS += $$files(*.ui, true)
RESOURCES += res/resources.qrc
unix:!macx {
}
macx: {
DEFINES += WITH_BOOST
LIBS += -L/usr/local/Cellar/boost/1.70.0/lib/ -lboost_math_tr1
LIBS += -framework OpenGL
QMAKE_CXXFLAGS += -I/usr/local/Cellar/boost/1.70.0/include/
}
# TODO: make this work.
CONFIG (boost) {
DEFINES += WITH_BOOST
LIBS += -L$${BOOSTPATH}
QMAKE_CXXFLAGS += -I$${BOOSTINCLUDE}
}
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin