L I C E N S E and SO MUCH PATCHBOARD STUFF

graph i/o ports, breadcrumb nav, graph serialization, etc. etc. etc.
portability/boost
zetaPRIME 2018-12-25 01:54:23 -05:00
parent f78c3da03d
commit 333a06cac7
32 changed files with 887 additions and 144 deletions

68
notes
View File

@ -45,52 +45,72 @@ project data {
TODO {
immediate frontburner {
group 1 {
- make selection follow pattern move where applicable
- strut command in pattern editor (mostly selection agnostic)
neeeeext {
hook the graph up to the audio engine! {
recursive dependency queue resolution
done/readiness test (process() wrapper?)
}
hook up commands to the graph {
figure out how to do note numbers
^ vector sized by channel count rounded up to next 16
^^ on switching to new graph... map of (hashes of) named channels??
assemble wire command queues (probaby some minor trickiness) and push into ports
NOTE! note number and port have to be combined in tracking (have to know what port to send the note-offs to)
}
then implement multithreading! :D
}
group 2 {
- skeleton graph and node
- skeleton audio engine
skeleton plugin registry - stuff into config namespace?
}
for passthrough {
passthroughLink attribute to keep track of whether a passthrough exists
^ build the logic into output pull()?
gadget that links up to parent's things
maybe call Graph::process as the end of its own subqueue??
}
... how to handle graph port naming and numbering?
have them fully dependent on the internal gadget??
- maybe make inputs/outputs into an (ordered) map<datatype> of map<uchar>s
audio engine invokes workers, then QThread::wait()s on them
# fix how qt5.12 broke header text (removed elide for now)
add metadata and pattern properties (artist, song title, project bpm; pattern name, length etc.)
give node helper functions to move ports (index-wise) and collapse them (eliminate numbering holes)
node event that fires on port connect/disconnect (to enable, say, automatic collapsing, and changing of behavior by how many connections there are)
}
at some point {
misc features needed before proper release {
at *least* js plugin support, with lua+lv2 highly preferable
SAMPLES and SAMPLING
gadget widgets (w/container) - at least a knob with nice range and such
add metadata and pattern properties (artist, song title, project bpm; pattern name, length etc.)
pattern cut+copy+paste
different context menu for multiple selected nodes
pack/unpack selection to/from subgraph
import/export subgraph as file (*.xyg)
proper playback controls and indicators
instrument previewing
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
make everything relevant check if editing is locked
make the save routine displace the old file and write a new one
multi-document, single-instance (QLocalServer etc.)
open file from command line argument
^ multi-document, single-instance (QLocalServer etc.)
}
gadgets and bundled things {
(the simple things:)
gain and panning gadget
volume meter
Polyplexer (splits a single command input into several monophonic outputs and keeps track of individual notes between them)
probably three sorts of sampler (quick drum sequencer, quick single-sample "wavetable", then the full-on tracker sampler later on)
}
}
dumb per-cycle atomic memory allocator from fixed pool for port buffer allocations
can also set up a tlsf pool per worker; prefix allocations with single byte identifier indicating which one they came from,
and defer freeing operations via message queues
- dumb per-cycle atomic memory allocator from fixed pool for port buffer allocations
? can also set up a tlsf pool per worker; prefix allocations with single byte identifier indicating which one they came from,
? and defer freeing operations via message queues
resampler object {
one used internally for each note

View File

@ -4,3 +4,8 @@ something something, actual readme coming later
## Build dependencies:
- Qt 5.12 or later
## License
The Software (all C/C++ code and bundled Lua or JavaScript, excepting where otherwise specified) is made available under the terms of the [GNU Lesser General Public License, v2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) or later.
The Assets (images and image project files (`*.xcf`), as well as all bundled example projects, excepting where otherwise specified) are licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).

View File

@ -60,7 +60,8 @@ namespace Xybrid::Audio {
void stop();
void* tickAlloc(size_t size);
inline size_t curTickSize() { return buffer[0].size(); }
inline size_t curTickId() const { return tickId; }
inline size_t curTickSize() const { return buffer[0].size(); }
// QIODevice functions
qint64 readData(char* data, qint64 maxlen) override;

View File

@ -1,4 +1,4 @@
#include "colorscheme.h"
using Xybrid::Config::ColorScheme;
ColorScheme ColorScheme::current;
ColorScheme Xybrid::Config::colorScheme;

View File

@ -5,7 +5,6 @@
namespace Xybrid::Config {
class ColorScheme {
public:
static ColorScheme current;
ColorScheme() = default;
QColor patternSelection = QColor(127, 63, 255, 63);
@ -20,4 +19,5 @@ namespace Xybrid::Config {
QColor patternFgParamCmd = QColor(191,163,255);
QColor patternFgParamAmt = QColor(191,222,255);
};
extern ColorScheme colorScheme;
}

View File

@ -7,9 +7,14 @@ using namespace Xybrid::Config;
#include <QMenu>
#include "data/node.h"
#include "data/graph.h"
using namespace Xybrid::Data;
#include "gadgets/ioport.h"
using Xybrid::Gadgets::IOPort;
#include "util/strings.h"
namespace {
typedef std::list<std::function<void()>> fqueue; // typedef so QtCreator's auto indent doesn't completely break :|
fqueue& regQueue() {
@ -19,6 +24,10 @@ namespace {
bool& initialized() { static bool b = false; return b; }
std::unordered_map<std::string, std::shared_ptr<PluginInfo>> plugins;
std::string priorityCategories[] {
"Gadget", "Instrument", "Effect"
};
}
bool PluginRegistry::enqueueRegistration(std::function<void ()> f) {
@ -49,8 +58,85 @@ std::shared_ptr<Node> PluginRegistry::createInstance(const std::string& id) {
return n;
}
void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::shared_ptr<Node>)> f) {
for (auto& i : plugins) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::shared_ptr<Node>)> f, Graph* g) {
std::map<std::string, std::map<std::string, std::shared_ptr<PluginInfo>>> cm; // category map
cm.try_emplace(""); // force empty category
for (auto& i : plugins) {
if (i.second->hidden) continue;
cm.try_emplace(i.second->category);
cm[i.second->category][i.second->displayName] = i.second;
}
// I/O ports
if (g) {
auto* mio = m->addMenu("I/O Port");
//auto* mi = mio->addMenu("Input");
//auto* mo = mio->addMenu("Output");
Port::DataType d[] {Port::Audio, Port::Command};
for (auto dt : d) {
auto* mi = mio->addMenu(QString("&%1 In").arg(Util::enumName(dt)));
auto* mo = mio->addMenu(QString("&%1 Out").arg(Util::enumName(dt)));
//mi->setStyleSheet("QMenu { menu-scrollable: 1; }");
//mi->setFixedHeight(256);
for (int ih = 0; ih < 16; ih++) {
QString n = QString::number(ih, 16).toUpper();
auto* mis = mi->addMenu(QString(u8"%10-%1F").arg(n));
auto* mos = mo->addMenu(QString(u8"%10-%1F").arg(n));
for (int il = 0; il < 16; il++) {
int i = ih*16+il;
QString nn = Util::hex(i);
mis->addAction(nn, [f, dt, i] {
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
n->setPort(Port::Input, dt, static_cast<uint8_t>(i));
f(n);
})->setEnabled(!g->port(Port::Input, dt, static_cast<uint8_t>(i)));
mos->addAction(nn, [f, dt, i] {
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
n->setPort(Port::Output, dt, static_cast<uint8_t>(i));
f(n);
})->setEnabled(!g->port(Port::Output, dt, static_cast<uint8_t>(i)));
}
}
//
}
m->addSeparator();
}
// populate priorities into menu
for (auto& pc : priorityCategories) {
if (auto c = cm.find(pc); c != cm.end()) {
auto* ccm = m->addMenu(QString::fromStdString(c->first));
for (auto& i : c->second) {
ccm->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
f(n);
});
}
cm.erase(pc);
}
}
// then any other category
for (auto& c : cm) {
if (c.first.empty() || c.second.empty()) continue;
auto* ccm = m->addMenu(QString::fromStdString(c.first));
for (auto& i : c.second) {
ccm->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
f(n);
});
}
}
m->addSeparator();
for (auto& i : cm[""]) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
f(n);

View File

@ -8,6 +8,7 @@ class QMenu;
namespace Xybrid::Data {
class Node;
class Graph;
}
namespace Xybrid::Config {
@ -17,6 +18,7 @@ namespace Xybrid::Config {
std::string displayName;
std::string category;
std::function<std::shared_ptr<Data::Node>()> createInstance;
bool hidden = false;
PluginInfo() = default;
virtual ~PluginInfo() = default;
@ -28,6 +30,6 @@ namespace Xybrid::Config {
void init();
std::shared_ptr<Data::Node> createInstance(const std::string& id);
void populatePluginMenu(QMenu*, std::function<void(std::shared_ptr<Data::Node>)>);
void populatePluginMenu(QMenu*, std::function<void(std::shared_ptr<Data::Node>)>, Data::Graph* = nullptr);
}
}

View File

@ -4,14 +4,139 @@ using namespace Xybrid::Data;
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "data/project.h"
#include "uisocket.h"
#include "mainwindow.h"
#include "util/strings.h"
#include <QCborMap>
#include <QCborArray>
#include <QMetaType>
#include <QMetaEnum>
namespace {
std::shared_ptr<PluginInfo> inf;
bool c = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "graph";
i->displayName = "Subgraph";
i->createInstance = []{ return std::make_shared<Graph>(); };
PluginRegistry::registerPlugin(i);
inf = i;
});
}
//std::string Graph::pluginName() const { return "Subgraph"; }
Graph::Graph() {
plugin = inf; // harder bind
}
void Graph::saveData(QCborMap& m) {
// graph properties
// ... maybe there will be some at some point
std::unordered_map<Node*, int> indices;
{ /* children */ } {
QCborArray c;
int idx = 0;
for (auto ch : children) {
if (!ch->plugin) continue;
indices[ch.get()] = idx++;
QCborMap chm;
chm.insert(QString("id"), QString::fromStdString(ch->plugin->id));
if (!ch->name.empty()) chm.insert(QString("name"), QString::fromStdString(ch->name));
chm.insert(QString("x"), ch->x);
chm.insert(QString("y"), ch->y);
ch->saveData(chm);
c << chm;
}
m.insert(QString("children"), c);
}
{ /* connections */ } {
// mapped from output to input
// array { oIdx, dataType, pIdx, iIdx, dataType, pIdx }
QCborArray cn;
for (auto ch : children) {
if (!ch->plugin) continue; // already skipped over
int idx = indices[ch.get()];
for (auto dt : ch->outputs) {
for (auto op : dt.second) {
auto o = op.second;
o->cleanConnections(); // let's just do some groundskeeping here
for (auto iw : o->connections) {
auto i = iw.lock();
QCborArray c;
c << idx;
c << Util::enumName(o->dataType());
c << o->index;
c << indices[i->owner.lock().get()];
c << Util::enumName(i->dataType());
c << i->index;
cn << c;
}
}
}
}
m.insert(QString("connections"), cn);
}
}
void Graph::loadData(QCborMap& m) {
auto g = std::static_pointer_cast<Graph>(shared_from_this());
// graph properties (none)
{ /* children */ } {
QCborArray c = m.value("children").toArray();
for (auto chmv : c) {
auto chm = chmv.toMap();
auto ch = PluginRegistry::createInstance(chm.value("id").toString().toStdString());
ch->parentTo(g);
ch->name = chm.value("name").toString().toStdString();
ch->x = static_cast<int>(chm.value("x").toInteger());
ch->y = static_cast<int>(chm.value("y").toInteger());
ch->loadData(chm);
}
}
{ /* connections */ } {
QCborArray cn = m.value("connections").toArray();
auto pmt = QMetaType::metaObjectForType(QMetaType::type("Xybrid::Data::Port"));
auto dtm = pmt->enumerator(pmt->indexOfEnumerator("DataType"));
for (auto cv : cn) {
auto c = cv.toArray();
if (c.empty()) continue;
auto on = children[static_cast<size_t>(c[0].toInteger())];
auto in = children[static_cast<size_t>(c[3].toInteger())];
auto op = on->port(Port::Output, static_cast<Port::DataType>(dtm.keyToValue(c[1].toString().toStdString().c_str())), static_cast<uint8_t>(c[2].toInteger()));
auto ip = in->port(Port::Input, static_cast<Port::DataType>(dtm.keyToValue(c[4].toString().toStdString().c_str())), static_cast<uint8_t>(c[5].toInteger()));
if (op && ip) op->connect(ip);
}
}
}
void Graph::onParent(std::shared_ptr<Graph>) {
// propagate project pointer
for (auto c : children) {
c->project = project;
// let this handle the recursion for us, since this is all this function does
if (c->plugin == inf) c->onParent(c->parent.lock());
}
}
void Graph::onDoubleClick() {
emit project->socket->openGraph(this);
}

View File

@ -5,11 +5,19 @@
namespace Xybrid::Data {
class Graph : public Node {
public:
Graph();
~Graph() override = default;
std::vector<std::shared_ptr<Node>> children;
// position of viewport within graph (not serialized)
int viewX{}, viewY{};
void saveData(QCborMap&) override;
void loadData(QCborMap&) override;
//std::string pluginName() const override;
void onParent(std::shared_ptr<Graph>) override;
void onDoubleClick() override;
};
}

View File

@ -29,6 +29,7 @@ bool Port::connect(std::shared_ptr<Port> p) {
if (!canConnectTo(p->dataType())) return false; // can't hook up to an incompatible data type
for (auto c : connections) if (c.lock() == p) return true; // I guess report success if already connected?
if (singleInput() && connections.size() > 0) return false; // reject multiple connections on single-input ports
if (auto o = owner.lock(), po = p->owner.lock(); !o || !po || po->dependsOn(o)) return false; // no dependency loops!
// actually hook up
connections.emplace_back(p);
p->connections.emplace_back(shared_from_this());
@ -56,11 +57,14 @@ std::shared_ptr<Port> Port::makePort(DataType dt) {
void Node::parentTo(std::shared_ptr<Graph> graph) {
auto t = shared_from_this(); // keep alive during reparenting
if (auto p = parent.lock(); p) {
onUnparent(p);
p->children.erase(std::remove(p->children.begin(), p->children.end(), t), p->children.end());
}
parent = graph;
if (graph) {
project = graph->project;
graph->children.push_back(t);
onParent(graph);
}
}
@ -78,6 +82,7 @@ std::shared_ptr<Port> Node::addPort(Port::Type t, Port::DataType dt, uint8_t idx
auto mdt = m.find(dt);
if (mdt->second.find(idx) == mdt->second.end()) {
auto p = Port::makePort(dt);
p->owner = shared_from_this();
p->type = t;
mdt->second.insert({idx, p});
p->index = idx;
@ -93,4 +98,26 @@ void Node::removePort(Port::Type t, Port::DataType dt, uint8_t idx) {
}
}
std::unordered_set<std::shared_ptr<Node>> Node::dependencies() const {
std::unordered_set<std::shared_ptr<Node>> set;
for (auto& t : inputs) {
for (auto& p : t.second) {
for (auto& c : p.second->connections) {
// if connection still exists, *and its owner* still exists...
if (auto cp = c.lock(); cp) if (auto n = cp->owner.lock(); n) set.insert(n);
}
}
}
return set;
}
bool Node::dependsOn(std::shared_ptr<Node> o) {
auto deps = dependencies();
for (auto d : deps) {
if (d == o) return true;
if (d->dependsOn(o)) return true;
}
return false;
}
std::string Node::pluginName() const { if (!plugin) return "(unknown plugin)"; return plugin->displayName; }

View File

@ -3,11 +3,17 @@
#include <memory>
#include <vector>
#include <map>
#include <unordered_set>
#include <string>
#include <QPointer>
#include <QObject>
class QPainter;
class QStyleOptionGraphicsItem;
namespace Xybrid::UI {
class NodeObject;
class PortObject;
}
@ -16,20 +22,25 @@ namespace Xybrid::Config {
}
namespace Xybrid::Data {
class Project;
class Graph;
class Node;
class Port : public std::enable_shared_from_this<Port> {
Q_GADGET
public:
enum Type : uint8_t {
Input, Output
};
Q_ENUM(Type)
enum DataType : uint8_t {
Audio, Command, MIDI, Parameter
};
Q_ENUM(DataType)
std::weak_ptr<Node> owner;
std::vector<std::weak_ptr<Port>> connections;
std::weak_ptr<Port> passthroughFor;
std::weak_ptr<Port> passthroughTo;
Type type; // TODO: figure out passthrough?
uint8_t index;
size_t tickUpdatedOn = static_cast<size_t>(-1);
@ -54,6 +65,7 @@ namespace Xybrid::Data {
class Node : public std::enable_shared_from_this<Node> {
public:
Project* project;
std::weak_ptr<Graph> parent;
int x{}, y{};
std::string name;
@ -62,6 +74,8 @@ namespace Xybrid::Data {
std::shared_ptr<Config::PluginInfo> plugin;
QPointer<UI::NodeObject> obj;
virtual ~Node() = default;
void parentTo(std::shared_ptr<Graph>);
@ -70,7 +84,25 @@ namespace Xybrid::Data {
std::shared_ptr<Port> addPort(Port::Type, Port::DataType, uint8_t);
void removePort(Port::Type, Port::DataType, uint8_t);
std::unordered_set<std::shared_ptr<Node>> dependencies() const;
bool dependsOn(std::shared_ptr<Node>);
virtual void saveData(QCborMap&) { }
virtual void loadData(QCborMap&) { }
virtual void process() { }
virtual std::string pluginName() const;
virtual void onGadgetCreated() { }
virtual void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) { }
virtual void onRename() { }
virtual void onUnparent(std::shared_ptr<Graph>) { }
virtual void onParent(std::shared_ptr<Graph>) { }
virtual void onDoubleClick() { }
// something something customChrome?
};
}
Q_DECLARE_METATYPE(Xybrid::Data::Port)

40
xybrid/data/porttypes.cpp Normal file
View File

@ -0,0 +1,40 @@
#include "porttypes.h"
using namespace Xybrid::Data;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
void AudioPort::pull() {
auto t = audioEngine->curTickId();
if (tickUpdatedOn == t) return;
tickUpdatedOn = t;
size_t ts = audioEngine->curTickSize();
size_t s = sizeof(float) * ts;
if (type == Input) {
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
bufR = bufL + s;
memset(bufL, 0, s*2); // clear buffers
for (auto c : connections) { // mix
if (auto p = std::static_pointer_cast<AudioPort>(c.lock()); p && p->dataType() == Audio) {
p->pull();
for (size_t i = 0; i < ts; i++) {
bufL[i] += p->bufL[i];
bufR[i] += p->bufR[i];
}
}
}
} else if (auto pt = std::static_pointer_cast<AudioPort>(passthroughTo.lock()); pt && pt->dataType() == Audio) {
// passthrough; ports abound
pt->pull();
bufL = pt->bufL;
bufR = pt->bufR;
} else { // output without valid passthrough, just clear and prepare a blank buffer
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
bufR = bufL + s;
memset(bufL, 0, s*2); // clear buffers
}
}

View File

@ -6,18 +6,27 @@ namespace Xybrid::Data {
class AudioPort : public Port {
public:
float* bufL;
float* bufR;
AudioPort() = default;
~AudioPort() override = default;
Port::DataType dataType() const override { return Port::Audio; }
void pull() override;
};
class CommandPort : public Port {
public:
uint8_t* data;
size_t dataSize;
CommandPort() = default;
~CommandPort() override = default;
Port::DataType dataType() const override { return Port::Command; }
bool singleInput() const override { return true; }
};
}

View File

@ -10,6 +10,7 @@ using namespace Xybrid::Audio;
Project::Project() {
rootGraph = std::make_shared<Graph>();
rootGraph->project = this;
}
Project::~Project() {

View File

@ -1,6 +1,7 @@
#include "fileops.h"
#include "uisocket.h"
#include "data/graph.h"
#include <QDebug>
@ -95,6 +96,12 @@ bool Xybrid::FileOps::saveProject(std::shared_ptr<Project> project, QString file
main.insert(QString("patterns"), ptns);
}
{ /* Graph */ } {
QCborMap g;
project->rootGraph->saveData(g);
main.insert(QString("graph"), g);
}
root << main;
}
@ -187,5 +194,10 @@ std::shared_ptr<Xybrid::Data::Project> Xybrid::FileOps::loadProject(QString file
}
}
{ /* Graph */ } {
QCborMap g = main.value("graph").toMap();
if (!g.isEmpty()) project->rootGraph->loadData(g);
}
return project;
}

146
xybrid/gadgets/ioport.cpp Normal file
View File

@ -0,0 +1,146 @@
#include "ioport.h"
using Xybrid::Gadgets::IOPort;
using namespace Xybrid::Data;
#include <QPainter>
#include <QGraphicsScene>
#include <QStyleOptionGraphicsItem>
#include <QCborMap>
#include <QMetaType>
#include <QMetaEnum>
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "data/graph.h"
#include "ui/patchboard/nodeobject.h"
using namespace Xybrid::UI;
#include "util/strings.h"
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "ioport";
i->displayName = "I/O Port";
i->hidden = true;
i->createInstance = []{ return std::make_shared<IOPort>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
Port::Type opposite(Port::Type t) {
if (t == Port::Input) return Port::Output;
return Port::Input;
}
}
IOPort::IOPort() {
}
void IOPort::remove() {
auto g = parent.lock();
if (!g) return;
if (!portSet) return;
g->removePort(type, dataType, index);
removePort(opposite(type), dataType, index);
}
void IOPort::add() {
auto g = parent.lock();
if (!g) return;
if (!portSet) return;
auto gp = g->addPort(type, dataType, index);
auto p = addPort(opposite(type), dataType, index);
if (gp && p) {
p->name = name;
gp->name = name;
p->passthroughTo = gp;
gp->passthroughTo = p;
}
}
void IOPort::setPort(Port::Type type, Port::DataType dataType, uint8_t index) {
remove();
this->type = type;
this->dataType = dataType;
this->index = index;
portSet = true;
add();
}
void IOPort::onRename() {
auto g = parent.lock();
if (!g) return;
if (!portSet) return;
auto gp = g->port(type, dataType, index);
auto p = port(opposite(type), dataType, index);
if (gp && p) {
p->name = name;
gp->name = name;
}
}
void IOPort::saveData(QCborMap& m) {
QCborMap pm;
pm.insert(QString("type"), Util::enumName(type));
pm.insert(QString("dataType"), Util::enumName(dataType));
pm.insert(QString("index"), index);
m.insert(QString("port"), pm);
}
void IOPort::loadData(QCborMap& m) {
auto pm = m.value("port").toMap();
if (pm.empty()) return;
auto pmt = QMetaType::metaObjectForType(QMetaType::type("Xybrid::Data::Port"));
auto tm = pmt->enumerator(pmt->indexOfEnumerator("Type"));
auto dtm = pmt->enumerator(pmt->indexOfEnumerator("DataType"));
std::string st = pm.value("type").toString().toStdString();
std::string sdt = pm.value("dataType").toString().toStdString();
setPort(
static_cast<Port::Type>(tm.keyToValue(st.c_str())),
static_cast<Port::DataType>(dtm.keyToValue(sdt.c_str())),
static_cast<uint8_t>(pm.value("index").toInteger())
);
}
void IOPort::onUnparent(std::shared_ptr<Graph>) { remove(); }
void IOPort::onParent(std::shared_ptr<Graph>) { add(); }
void IOPort::onGadgetCreated() {
if (!obj) return;
obj->customChrome = true;
qreal ps = (PortObject::portSize + PortObject::portSpacing) * 2;
obj->setGadgetSize(QPointF(ps, ps));
// do this after setting size
auto r = obj->boundingRect();
obj->inputPortContainer->setPos(r.center());
obj->outputPortContainer->setPos(r.center());
}
void IOPort::drawCustomChrome(QPainter* painter, const QStyleOptionGraphicsItem* opt) {
QColor outline = QColor(31, 31, 31);
if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255);
auto r = obj->boundingRect();
auto rf = r - QMarginsF(1, 1, 1, 1);
painter->setPen(Qt::NoPen);
int ro = 180*16 * (1-type);
painter->setBrush(QColor(63, 63, 63));
painter->drawPie(rf, (90+45)*16 + ro, -90*3*16);
painter->setBrush(QColor(95, 95, 95));
painter->drawPie(rf, (90+45)*16 + 180*16 + ro, 90*16);
painter->setBrush(Qt::NoBrush);
painter->setPen(QPen(outline, 2));
painter->drawPie(r, (90+45)*16 + ro, -90*3*16);
}

32
xybrid/gadgets/ioport.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include "data/node.h"
namespace Xybrid::Gadgets {
class IOPort : public Data::Node {
bool portSet = false;
Data::Port::Type type;
Data::Port::DataType dataType;
uint8_t index;
void remove();
void add();
public:
IOPort();
~IOPort() override = default;
void setPort(Data::Port::Type type, Data::Port::DataType dataType, uint8_t index);
void onRename() override;
void saveData(QCborMap&) override;
void loadData(QCborMap&) override;
void onUnparent(std::shared_ptr<Data::Graph>) override;
void onParent(std::shared_ptr<Data::Graph>) override;
void onGadgetCreated() override;
void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
};
}

View File

@ -1,6 +1,7 @@
#include "mainwindow.h"
#include "audio/audioengine.h"
#include "config/pluginregistry.h"
#include "data/graph.h"
#include <vector>
@ -10,6 +11,8 @@
#include <QSurfaceFormat>
int main(int argc, char *argv[]) {
qRegisterMetaType<Xybrid::Data::Port>();
QApplication a(argc, argv);
// enable antialiasing on accelerated graphicsview

View File

@ -13,10 +13,13 @@ using Xybrid::MainWindow;
#include <QUndoStack>
#include <QTimer>
#include <QOpenGLWidget>
#include <QGLWidget>
#include <QScroller>
#include <QGraphicsTextItem>
#include <QJsonObject>
#include "data/graph.h"
#include "util/strings.h"
@ -128,6 +131,7 @@ MainWindow::MainWindow(QWidget *parent) :
(new ProjectPatternDeleteCommand(project, p))->commit();
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(ui->patternList->mapToGlobal(pt));
});//*/
}
@ -187,6 +191,7 @@ MainWindow::MainWindow(QWidget *parent) :
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(ui->patternSequencer->mapToGlobal(pt));
});//*/
}
@ -213,7 +218,7 @@ MainWindow::MainWindow(QWidget *parent) :
});
// TEMP - play/stop
connect(new QShortcut(QKeySequence("Ctrl+P"), ui->pattern), &QShortcut::activated, this, [this]() {
connect(new QShortcut(QKeySequence("Ctrl+P"), this), &QShortcut::activated, this, [this]() {
if (audioEngine->playbackMode() == AudioEngine::Playing) audioEngine->stop();
else audioEngine->play(project);
});
@ -235,8 +240,16 @@ MainWindow::MainWindow(QWidget *parent) :
//ui->patchboardView->setDragMode(QGraphicsView::DragMode::RubberBandDrag);
auto* view = ui->patchboardView;
view->setViewport(new QOpenGLWidget); // enable hardware acceleration
bool enableHWAccel = false; // disabled because QOpenGLWidget has some huge lag issues in this context
if (enableHWAccel) {
auto* vp = new QOpenGLWidget();
view->setViewport(vp); // enable hardware acceleration
}
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
glEnable(GL_MULTISAMPLE);
glEnable(GL_LINE_SMOOTH);
//QGL::FormatOption::Rgba
view->setAlignment(Qt::AlignTop | Qt::AlignLeft);
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@ -258,20 +271,17 @@ MainWindow::MainWindow(QWidget *parent) :
if (e->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(e);
// initiate drag
if (me->button() == Qt::LeftButton) view->setDragMode(QGraphicsView::RubberBandDrag);
if (me->button() == Qt::LeftButton) {
view->setDragMode(QGraphicsView::RubberBandDrag);
}
} else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end
QTimer::singleShot(1, [view] { view->setDragMode(QGraphicsView::NoDrag); });
QTimer::singleShot(1, [view] {
view->setDragMode(QGraphicsView::NoDrag);
});
}
return w->QObject::eventFilter(w, e);
}));
//view->setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu);
/*connect(view, &QGraphicsView::customContextMenuRequested, this, [this, view] {
qDebug() << "context";
//view->viewport->visibleRegion().boundingRect().topLeft()
});*/
}
// Set up signaling from project to UI
@ -291,6 +301,15 @@ MainWindow::MainWindow(QWidget *parent) :
emit ui->patternEditor->model()->dataChanged(ind, ind.siblingAtColumn((ch+1)*cpc-1));
static_cast<PatternEditorModel*>(ui->patternEditor->model())->updateColumnDisplay();
});
connect(socket, &UISocket::openGraph, this, [this](Graph* g) {
if (!g) return;
auto gg = std::static_pointer_cast<Graph>(g->shared_from_this());
QString name = QString::fromStdString(gg->name);
if (name.isEmpty()) name = QString::fromStdString(gg->pluginName());
ui->patchboardBreadcrumbs->push(name, this, [this, gg] {
openGraph(gg);
});
});
// and from audio engine
connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction]() {
@ -301,6 +320,10 @@ MainWindow::MainWindow(QWidget *parent) :
// and start with a new project
menuFileNew();
//auto q = QJsonObject();
//q.insert(QMetaObject::, "frenk");
qDebug() << QVariant::fromValue(Data::Port::Audio).toString();
}
MainWindow::~MainWindow() {
@ -328,24 +351,6 @@ void MainWindow::menuFileNew() {
project = std::make_shared<Project>();
project->sequence.push_back(project->newPattern().get());
// TEMP add some stuff
{
auto g1 = PluginRegistry::createInstance("graph");//std::make_shared<Graph>();
g1->parentTo(project->rootGraph);
g1->x = 64;
g1->y = 64;
g1->addPort(Port::Input, Port::Command, 0);
auto g2 = PluginRegistry::createInstance("graph");
g2->parentTo(project->rootGraph);
g2->x = 444;
g2->y = 22;
g1->addPort(Port::Output, Port::Audio, 0)->connect(g2->addPort(Port::Input, Port::Audio, 0));
g2->addPort(Port::Input, Port::Audio, 1);
g2->addPort(Port::Input, Port::Audio, 2)->name = "Named port";
}
//
onNewProjectLoaded();
}
@ -366,6 +371,7 @@ void MainWindow::menuFileSave() {
if (project->fileName.isEmpty()) menuFileSaveAs();
else {
FileOps::saveProject(project);
undoStack->setClean();
}
}
@ -373,6 +379,8 @@ void MainWindow::menuFileSaveAs() {
auto fileName = QFileDialog::getSaveFileName(this, "Save project as...", QString(), projectFilter);
if (fileName.isEmpty()) return; // canceled
FileOps::saveProject(project, fileName);
undoStack->setClean();
updateTitle();
}
void MainWindow::onNewProjectLoaded() {
@ -388,7 +396,11 @@ void MainWindow::onNewProjectLoaded() {
break;
}
openGraph(project->rootGraph);
//openGraph(project->rootGraph);
ui->patchboardBreadcrumbs->clear();
ui->patchboardBreadcrumbs->push("/", this, [this, gg = project->rootGraph] {
openGraph(gg);
});
updateTitle();
}
@ -450,5 +462,8 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) {
void MainWindow::openGraph(const std::shared_ptr<Data::Graph>& g) {
if (!g) return; // invalid
QPointF scrollPt(g->viewX, g->viewY);
ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g));
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 0);
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 1);
}

View File

@ -14,6 +14,7 @@ namespace Ui {
namespace Xybrid {
class MainWindow : public QMainWindow {
friend class Data::Graph;
Q_OBJECT
public:

View File

@ -135,7 +135,7 @@
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
<height>28</height>
</size>
</property>
<property name="font">
@ -248,10 +248,23 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="Xybrid::UI::BreadcrumbView" name="patchboardBreadcrumbs">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>28</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
</widget>
</item>
<item>
<widget class="QGraphicsView" name="patchboardView">
<property name="viewportUpdateMode">
<enum>QGraphicsView::FullViewportUpdate</enum>
<enum>QGraphicsView::MinimalViewportUpdate</enum>
</property>
</widget>
</item>
@ -372,6 +385,11 @@
<extends>QTableView</extends>
<header>ui/patterneditorview.h</header>
</customwidget>
<customwidget>
<class>Xybrid::UI::BreadcrumbView</class>
<extends>QTableView</extends>
<header>ui/breadcrumbview.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="res/resources.qrc"/>

View File

@ -0,0 +1,58 @@
#include "breadcrumbview.h"
using namespace Xybrid::UI;
#include <QDebug>
#include <QHeaderView>
BreadcrumbView::BreadcrumbView(QWidget* parent) : QTableView (parent) {
mdl = new BreadcrumbModel(this);
setModel(mdl);
horizontalHeader()->setVisible(false);
verticalHeader()->setVisible(false);
horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
connect(selectionModel(), &QItemSelectionModel::currentColumnChanged, this, [this](const QModelIndex& index, const QModelIndex&) {
//qDebug() << "breadcrumb change" << index;
size_t idx = static_cast<size_t>(index.column());
if (mdl->actions.size() <= idx) return;
mdl->actions[idx]->trigger();
if (shrinkOnSelect) mdl->actions.resize(idx+1);
emit mdl->layoutChanged();
});
}
void BreadcrumbView::clear() {
mdl->actions.clear();
emit mdl->layoutChanged();
this->setCurrentIndex(mdl->index(-1, -1));
}
void BreadcrumbView::push(const QString& text, QObject* bind, std::function<void ()> action) {
mdl->actions.resize(std::min(mdl->actions.size(), static_cast<size_t>(currentIndex().column()) + 1));
mdl->actions.push_back(std::make_unique<QAction>(text, bind));
emit mdl->layoutChanged();
connect(mdl->actions.back().get(), &QAction::triggered, bind, action);
this->setCurrentIndex(mdl->index(0, static_cast<int>(mdl->actions.size()) - 1));
}
BreadcrumbModel::BreadcrumbModel(QWidget* parent) : QAbstractTableModel(parent) {
//actions.push_back(std::make_unique<QAction>("Root Graph", this));
}
int BreadcrumbModel::rowCount(const QModelIndex&) const {
return 1;
}
int BreadcrumbModel::columnCount(const QModelIndex&) const {
return static_cast<int>(actions.size());
}
QVariant BreadcrumbModel::data(const QModelIndex& index, int role) const {
if (actions.size() <= static_cast<size_t>(index.column())) return QVariant(); // safety
if (role == Qt::DisplayRole) {
return actions[static_cast<size_t>(index.column())]->text();
}
if (role == Qt::TextAlignmentRole) return Qt::AlignHCenter + Qt::AlignVCenter;
return QVariant();
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <functional>
#include <vector>
#include <QAction>
#include <QTableView>
#include <QAbstractTableModel>
namespace Xybrid::UI {
class BreadcrumbView;
class BreadcrumbModel : public QAbstractTableModel {
friend class BreadcrumbView;
Q_OBJECT
std::vector<std::unique_ptr<QAction>> actions;
BreadcrumbModel(QWidget* parent = nullptr);
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};
class BreadcrumbView : public QTableView {
Q_OBJECT
BreadcrumbModel* mdl;
public:
bool shrinkOnSelect = true;
BreadcrumbView(QWidget* parent = nullptr);
void clear();
void push(const QString& text, QObject* bind, std::function<void()> action);
};
}

View File

@ -18,21 +18,17 @@ using Xybrid::Data::Port;
#include <QTextCursor>
#include <QMenu>
#include <QMessageBox>
#include <QInputDialog>
#include "util/strings.h"
namespace {
const constexpr qreal portSize [[maybe_unused]] = 10;
const constexpr qreal portSpacing [[maybe_unused]] = 3;
const QColor tcolor[] {
QColor(239, 179, 59), // Audio
QColor(163, 95, 191), // Command
QColor(95, 191, 163), // MIDI
QColor(127, 127, 255), // Parameter
};
const QString tname[] {
"Audio", "Command", "MIDI", "Parameter"
};
}
void PortObject::connectTo(Xybrid::UI::PortObject* o) {
@ -56,7 +52,7 @@ void PortObject::setHighlighted(bool h, bool hideLabel) {
bool lv = h && !hideLabel;
if (lv) {
QString txt = QString("%1 %2").arg(tname[port->dataType()].toLower()).arg(QString::number(port->index));
QString txt = QString("%1 %2").arg(Util::enumName(port->dataType()).toLower()).arg(Util::hex(port->index));
if (!port->name.empty()) txt = QString("%1 (%2)").arg(QString::fromStdString(port->name)).arg(txt);
QColor c = tcolor[port->dataType()];
label->setText(txt);
@ -93,6 +89,8 @@ PortObject::PortObject(const std::shared_ptr<Data::Port>& p) {
for (auto c : port->connections) {
if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj);
}
setCursor(Qt::CursorShape::CrossCursor);
}
PortObject::~PortObject() {
@ -100,7 +98,7 @@ PortObject::~PortObject() {
}
void PortObject::mousePressEvent(QGraphicsSceneMouseEvent*) {
setCursor(Qt::ClosedHandCursor);
//setCursor(Qt::CursorShape::CrossCursor);
setHighlighted(true, true);
dragLine.reset(new QGraphicsLineItem());
dragLine->setPen(QPen(tcolor[port->dataType()].lighter(125), 1.5));
@ -110,7 +108,7 @@ void PortObject::mousePressEvent(QGraphicsSceneMouseEvent*) {
}
void PortObject::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
unsetCursor();
//unsetCursor();
dragLine.reset();
auto* i = scene()->itemAt(e->scenePos(), QTransform());
@ -143,6 +141,7 @@ void PortObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
delete c;
}
});//->setEnabled(this->connections.size() != 0);
m->setAttribute(Qt::WA_DeleteOnClose);
m->popup(e->screenPos());
}
@ -164,24 +163,28 @@ QRectF PortObject::boundingRect() const {
NodeObject::NodeObject(const std::shared_ptr<Data::Node>& n) {
node = n;
node->obj = this;
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsFocusable);
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
/*auto* t = new QGraphicsTextItem(QString::fromStdString(n->name), this);
t->setPos(4, 4);
t->setFlag(QGraphicsItem::ItemIsSelectable, false);*/
//t->setPen(QPen(QColor(0, 0, 0), 0.25));
//t->setBrush(QBrush(QColor(255, 255, 255)));
setPos(node->x, node->y);
connect(this, &QGraphicsObject::xChanged, this, &NodeObject::onMoved);
connect(this, &QGraphicsObject::yChanged, this, &NodeObject::onMoved);
//setToolTip(QString::fromStdString(node->name));
createPorts();
node->onGadgetCreated();
}
void NodeObject::setGadgetSize(QPointF p) {
gadgetSize_ = p;
updateGeometry();
}
void NodeObject::promptDelete() {
@ -193,9 +196,30 @@ void NodeObject::promptDelete() {
}
}
void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
node->onDoubleClick();
}
void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
if (!isSelected()) {
for (auto* s : scene()->selectedItems()) s->setSelected(false);
setSelected(true);
}
auto* m = new QMenu();
if (canRename) {
m->addAction("Rename...", this, [this] {
bool ok = false;
auto cn = node->name.empty() ? QString::fromStdString(node->pluginName()) : QString("\"%1\"").arg(QString::fromStdString(node->name));
auto capt = QString("Rename %1:").arg(cn);
auto n = QInputDialog::getText(nullptr, "Rename...", capt, QLineEdit::Normal, QString::fromStdString(node->name), &ok);
if (!ok) return; // canceled
//setToolTip(n);
node->name = n.toStdString();
node->onRename();
});
}
m->addAction("Delete node", this, &NodeObject::promptDelete);
m->setAttribute(Qt::WA_DeleteOnClose);
m->popup(e->screenPos());
}
@ -204,35 +228,48 @@ void NodeObject::bringToTop(bool force) {
}
void NodeObject::createPorts() {
inputPortContainer.reset(new QGraphicsRectItem(this));
outputPortContainer.reset(new QGraphicsRectItem(this));
auto* ipc = new QGraphicsLineItem(this);
auto* opc = new QGraphicsLineItem(this);
inputPortContainer.reset(ipc);
outputPortContainer.reset(opc);
updateGeometry();
QPen p(QColor(95, 95, 95), 2.5);
QPointF inc(0, PortObject::portSize + PortObject::portSpacing);
QPointF cursor = QPointF(0, 0);
for (auto mdt : node->inputs) {
for (auto pp : mdt.second) {
auto* p = new PortObject(pp.second);
p->setParentItem(inputPortContainer.get());
p->setParentItem(ipc);
p->setPos(cursor);
cursor += QPointF(0, portSize + portSpacing);
cursor += inc;
}
}
ipc->setVisible(cursor.y() > 0);
cursor -= inc;
ipc->setLine(QLineF(QPointF(0, 0), cursor));
ipc->setPen(p);
cursor = QPointF(0, 0);
for (auto mdt : node->outputs) {
for (auto pp : mdt.second) {
auto* p = new PortObject(pp.second);
p->setParentItem(outputPortContainer.get());
p->setParentItem(opc);
p->setPos(cursor);
cursor += QPointF(0, portSize + portSpacing);
cursor += inc;
}
}
opc->setVisible(cursor.y() > 0);
cursor -= inc;
opc->setLine(QLineF(QPointF(0, 0), cursor));
opc->setPen(p);
}
void NodeObject::updateGeometry() {
if (inputPortContainer) inputPortContainer->setPos(QPointF(portSize * -.5 - portSpacing, portSize));
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + portSize * .5 + portSpacing, portSize));
qreal pm = PortObject::portSize * .5 + PortObject::portSpacing;
if (inputPortContainer) inputPortContainer->setPos(QPointF(-pm, PortObject::portSize));
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize));
}
void NodeObject::onMoved() {
@ -255,13 +292,16 @@ void NodeObject::focusInEvent(QFocusEvent *) {
}
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
if (customChrome) {
node->drawCustomChrome(painter, opt);
return;
}
QRectF r = boundingRect();
QColor outline = QColor(31, 31, 31);
if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255);
QLinearGradient fill(QPointF(0, 0), QPointF(0, r.height()));
//fill.setCoordinateMode(QLinearGradient::CoordinateMode::ObjectMode);
fill.setColorAt(0, QColor(95, 95, 95));
fill.setColorAt(16.0/r.height(), QColor(63, 63, 63));
fill.setColorAt(1.0 - (1.0 - 16.0/r.height()) / 2, QColor(55, 55, 55));
@ -283,7 +323,8 @@ void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, Q
}
QRectF NodeObject::boundingRect() const {
return QRectF(0, 0, 192, 48);// + QMarginsF(8, 8, 8, 8);
if (customChrome) return QRectF(QPointF(0, 0), gadgetSize_);
return QRectF(0, 0, 192, 36);// + QMarginsF(8, 8, 8, 8);
}
PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
@ -362,8 +403,11 @@ QPainterPath PortConnectionObject::shape(qreal width) const {
auto end = mapFromScene(in->scenePos());
path.moveTo(start);
//QPointF mod(std::max(std::max((end.x() - start.x()) * .64, (start.x() - end.x()) * .24), 64.0), 0);
QPointF out(0, 0);
//path.lineTo(start+out);
QPointF mod(std::max((end.x() - start.x()) * .64, 96.0), 0);
path.cubicTo(start + mod, end - mod, end);
path.cubicTo(start + out + mod, end - mod, end - out);
//path.lineTo(end);
if (width <= 0) return path;
QPainterPathStroker qp;

View File

@ -28,6 +28,9 @@ namespace Xybrid::UI {
enum { Type = UserType + 101 };
int type() const override { return Type; }
static const constexpr qreal portSize = 10;
static const constexpr qreal portSpacing = 3;
PortObject(const std::shared_ptr<Data::Port>&);
~PortObject() override;
@ -76,8 +79,8 @@ namespace Xybrid::UI {
friend class PortObject;
std::shared_ptr<Data::Node> node;
std::unique_ptr<QGraphicsItem> inputPortContainer = nullptr;
std::unique_ptr<QGraphicsItem> outputPortContainer = nullptr;
QPointF gadgetSize_{0, 0};
void onMoved();
void bringToTop(bool force = false);
@ -92,11 +95,22 @@ namespace Xybrid::UI {
enum { Type = UserType + 100 };
int type() const override { return Type; }
std::unique_ptr<QGraphicsItem> inputPortContainer = nullptr;
std::unique_ptr<QGraphicsItem> outputPortContainer = nullptr;
bool customChrome = false;
bool canRename = true;
NodeObject(const std::shared_ptr<Data::Node>&);
inline QPointF gadgetSize() const { return gadgetSize_; }
void setGadgetSize(QPointF);
inline void setGadgetSize(qreal w, qreal h) { setGadgetSize(QPointF(w, h)); }
inline const std::shared_ptr<Data::Node>& getNode() const { return node; }
void promptDelete();
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;

View File

@ -8,6 +8,7 @@ using Xybrid::UI::PatchboardScene;
#include <QGraphicsItem>
#include <QMainWindow>
#include <QMenu>
#include <QTimer>
#include <QGraphicsSceneContextMenuEvent>
@ -24,34 +25,17 @@ PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xy
graph = g;
view = parent;
connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::autoSetSize);
connect(view->verticalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::autoSetSize);
connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::autoSetSize);
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::autoSetSize);
connect(this, &QGraphicsScene::changed, this, &PatchboardScene::autoSetSize);
/*{
auto* t = addEllipse(0, 0, 64, 32);//scene->addText("hello world");
t->setBrush(QBrush(QColor(127,0,255)));
t->setFlag(QGraphicsItem::ItemIsMovable);
t->setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
t->setFlag(QGraphicsItem::ItemIsSelectable);
}
{
auto* t = addText("Hi there!");
//t->setBrush(QBrush(QColor(191,127,255)));
t->setFlag(QGraphicsItem::ItemIsMovable);
t->setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
t->setFlag(QGraphicsItem::ItemIsSelectable);
t->stackBefore(nullptr);
}//*/
connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::queueResize);
connect(view->verticalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::queueResize);
connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::queueResize);
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::queueResize);
connect(this, &QGraphicsScene::changed, this, &PatchboardScene::queueResize);
refresh();
}
void PatchboardScene::drawBackground(QPainter* painter, const QRectF& rect) {
painter->setBrush(QBrush(Config::ColorScheme::current.patternBg));
painter->setBrush(QBrush(Config::colorScheme.patternBg));
painter->setPen(QPen(Qt::PenStyle::NoPen));
painter->drawRect(rect);
@ -76,27 +60,30 @@ void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
n->parentTo(graph);
addItem(new NodeObject(n));
});
}, graph.get());
m->setAttribute(Qt::WA_DeleteOnClose);
m->popup(e->screenPos());
}
/*qDebug() << "context menu requested for scene at" << p;
auto n = std::make_shared<Data::Node>();
n->x = static_cast<int>(p.x());
n->y = static_cast<int>(p.y());
n->parentTo(graph);
addItem(new NodeObject(n));
e->accept();*/
void PatchboardScene::queueResize() {
if (!resizeQueued) {
resizeQueued = true;
QTimer::singleShot(1, this, &PatchboardScene::autoSetSize);
}
}
void PatchboardScene::autoSetSize() {
if (view->scene() != this) return;
auto vrect = view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect();
auto rect = itemsBoundingRect()
.united(view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect())
.united(vrect)
.united(QRectF(0, 0, 1, 1));
rect.setTopLeft(QPointF(0, 0));
setSceneRect(rect);
graph->viewX = static_cast<int>(vrect.left());
graph->viewY = static_cast<int>(vrect.top());
resizeQueued = false;
}
void PatchboardScene::refresh() {

View File

@ -12,6 +12,8 @@ namespace Xybrid::UI {
std::shared_ptr<Data::Graph> graph;
QGraphicsView* view;
bool resizeQueued = false;
void queueResize();
void autoSetSize();
public:

View File

@ -93,19 +93,19 @@ namespace {
void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
{ /* background */ } {
auto p = const_cast<PatternEditorModel*>(static_cast<const PatternEditorModel*>(index.model()))->getPattern();
painter->fillRect(option.rect, ColorScheme::current.patternBg);
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgMeasure);
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgBeat);
painter->fillRect(option.rect, Config::colorScheme.patternBg);
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);
}
// selection/cursor highlight
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, ColorScheme::current.patternSelection);
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, Config::colorScheme.patternSelection);
if (option.state & QStyle::State_HasFocus) {
painter->setPen(ColorScheme::current.patternSelection);
painter->setPen(Config::colorScheme.patternSelection);
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(1,1,-2,-2));
//painter->fillRect(option.rect, ColorScheme::current.patternSelection);
//painter->fillRect(option.rect, Config::colorScheme.patternSelection);
}
// and main data
@ -119,13 +119,13 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
}
if (s == QString("» ")) {
align = Qt::AlignVCenter | Qt::AlignLeft;
painter->setPen(ColorScheme::current.patternFgBlank);
painter->setPen(Config::colorScheme.patternFgBlank);
} else {
if (s == QString(" - ") || s == QString("- ")) painter->setPen(ColorScheme::current.patternFgBlank);
else if (cc == 0) painter->setPen(ColorScheme::current.patternFgPort);
else if (cc == 1) painter->setPen(ColorScheme::current.patternFgNote);
else if (cc % 2 == 0) painter->setPen(ColorScheme::current.patternFgParamCmd);
else painter->setPen(ColorScheme::current.patternFgParamAmt);
if (s == QString(" - ") || s == QString("- ")) painter->setPen(Config::colorScheme.patternFgBlank);
else if (cc == 0) painter->setPen(Config::colorScheme.patternFgPort);
else if (cc == 1) painter->setPen(Config::colorScheme.patternFgNote);
else if (cc % 2 == 0) painter->setPen(Config::colorScheme.patternFgParamCmd);
else painter->setPen(Config::colorScheme.patternFgParamAmt);
}
painter->drawText(option.rect, align, s);
}

View File

@ -172,11 +172,12 @@ void PatternEditorView::headerContextMenu(QPoint pt) {
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name)).arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
(new PatternChannelDeleteCommand(p, idx))->commit();
});
menu->addAction("Rename Channel", this, [this, idx, p]() {
menu->addAction("Rename Channel...", this, [this, idx, p]() {
if (p != mdl->getPattern()) return; // swapped already
startRenameChannel(idx);
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(hdr->mapToGlobal(pt));
}

View File

@ -7,6 +7,7 @@ class QUndoStack;
namespace Xybrid::Data {
class Project;
class Pattern;
class Graph;
}
namespace Xybrid {
@ -21,5 +22,7 @@ namespace Xybrid {
void updatePatternLists();
void patternUpdated(Data::Pattern* pattern);
void rowUpdated(Data::Pattern* pattern, int channel, int row);
void openGraph(Data::Graph*);
};
}

View File

@ -1,9 +1,17 @@
#pragma once
#include <QString>
#include <QVariant>
namespace Xybrid::Util {
template<typename Num> inline QString numAndName(Num num, const std::string& name) {
if (name.empty()) return QString::number(num);
return QString("%1 (\"%2\")").arg(num).arg(QString::fromStdString(name));
}
inline QString hex(int num, int fw = 2) {
return QString("%1").arg(num, fw, 16, QChar('0')).toUpper();
}
template <typename T>
inline QString enumName(T t) { return QVariant::fromValue(t).toString(); }
}

View File

@ -4,7 +4,7 @@
#
#-------------------------------------------------
QT += core gui multimedia
QT += core gui multimedia opengl
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
@ -50,7 +50,10 @@ SOURCES += \
ui/patchboard/patchboardscene.cpp \
ui/patchboard/nodeobject.cpp \
data/graph.cpp \
config/pluginregistry.cpp
config/pluginregistry.cpp \
data/porttypes.cpp \
ui/breadcrumbview.cpp \
gadgets/ioport.cpp
HEADERS += \
mainwindow.h \
@ -76,7 +79,9 @@ HEADERS += \
util/lambdaeventfilter.h \
ui/patchboard/nodeobject.h \
data/porttypes.h \
config/pluginregistry.h
config/pluginregistry.h \
ui/breadcrumbview.h \
gadgets/ioport.h
FORMS += \
mainwindow.ui