From 333a06cac722c1005128373c6279bdf47c526672 Mon Sep 17 00:00:00 2001 From: zetaPRIME Date: Tue, 25 Dec 2018 01:54:23 -0500 Subject: [PATCH] L I C E N S E and SO MUCH PATCHBOARD STUFF graph i/o ports, breadcrumb nav, graph serialization, etc. etc. etc. --- notes | 68 +++++++---- readme.md | 5 + xybrid/audio/audioengine.h | 3 +- xybrid/config/colorscheme.cpp | 2 +- xybrid/config/colorscheme.h | 2 +- xybrid/config/pluginregistry.cpp | 92 +++++++++++++- xybrid/config/pluginregistry.h | 4 +- xybrid/data/graph.cpp | 125 +++++++++++++++++++ xybrid/data/graph.h | 8 ++ xybrid/data/node.cpp | 27 +++++ xybrid/data/node.h | 34 +++++- xybrid/data/porttypes.cpp | 40 +++++++ xybrid/data/porttypes.h | 9 ++ xybrid/data/project.cpp | 1 + xybrid/fileops.cpp | 12 ++ xybrid/gadgets/ioport.cpp | 146 +++++++++++++++++++++++ xybrid/gadgets/ioport.h | 32 +++++ xybrid/main.cpp | 3 + xybrid/mainwindow.cpp | 75 +++++++----- xybrid/mainwindow.h | 1 + xybrid/mainwindow.ui | 22 +++- xybrid/ui/breadcrumbview.cpp | 58 +++++++++ xybrid/ui/breadcrumbview.h | 38 ++++++ xybrid/ui/patchboard/nodeobject.cpp | 100 +++++++++++----- xybrid/ui/patchboard/nodeobject.h | 18 ++- xybrid/ui/patchboard/patchboardscene.cpp | 55 ++++----- xybrid/ui/patchboard/patchboardscene.h | 2 + xybrid/ui/patterneditoritemdelegate.cpp | 24 ++-- xybrid/ui/patterneditorview.cpp | 3 +- xybrid/uisocket.h | 3 + xybrid/util/strings.h | 8 ++ xybrid/xybrid.pro | 11 +- 32 files changed, 887 insertions(+), 144 deletions(-) create mode 100644 xybrid/data/porttypes.cpp create mode 100644 xybrid/gadgets/ioport.cpp create mode 100644 xybrid/gadgets/ioport.h create mode 100644 xybrid/ui/breadcrumbview.cpp create mode 100644 xybrid/ui/breadcrumbview.h diff --git a/notes b/notes index e8684e0..bda3856 100644 --- a/notes +++ b/notes @@ -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 of maps 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 diff --git a/readme.md b/readme.md index 8ed20af..2c57252 100644 --- a/readme.md +++ b/readme.md @@ -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/). diff --git a/xybrid/audio/audioengine.h b/xybrid/audio/audioengine.h index 05b91a3..ced110c 100644 --- a/xybrid/audio/audioengine.h +++ b/xybrid/audio/audioengine.h @@ -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; diff --git a/xybrid/config/colorscheme.cpp b/xybrid/config/colorscheme.cpp index 9f8a290..92029d4 100644 --- a/xybrid/config/colorscheme.cpp +++ b/xybrid/config/colorscheme.cpp @@ -1,4 +1,4 @@ #include "colorscheme.h" using Xybrid::Config::ColorScheme; -ColorScheme ColorScheme::current; +ColorScheme Xybrid::Config::colorScheme; diff --git a/xybrid/config/colorscheme.h b/xybrid/config/colorscheme.h index d0d513b..89089b2 100644 --- a/xybrid/config/colorscheme.h +++ b/xybrid/config/colorscheme.h @@ -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; } diff --git a/xybrid/config/pluginregistry.cpp b/xybrid/config/pluginregistry.cpp index d0fc60f..222c70c 100644 --- a/xybrid/config/pluginregistry.cpp +++ b/xybrid/config/pluginregistry.cpp @@ -7,9 +7,14 @@ using namespace Xybrid::Config; #include -#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> 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> plugins; + + std::string priorityCategories[] { + "Gadget", "Instrument", "Effect" + }; } bool PluginRegistry::enqueueRegistration(std::function f) { @@ -49,8 +58,85 @@ std::shared_ptr PluginRegistry::createInstance(const std::string& id) { return n; } -void PluginRegistry::populatePluginMenu(QMenu* m, std::function)> f) { - for (auto& i : plugins) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] { +void PluginRegistry::populatePluginMenu(QMenu* m, std::function)> f, Graph* g) { + std::map>> 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"%1​0-%1​F").arg(n)); + auto* mos = mo->addMenu(QString(u8"%1​0-%1​F").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(createInstance("ioport")); + n->setPort(Port::Input, dt, static_cast(i)); + f(n); + })->setEnabled(!g->port(Port::Input, dt, static_cast(i))); + mos->addAction(nn, [f, dt, i] { + auto n = std::static_pointer_cast(createInstance("ioport")); + n->setPort(Port::Output, dt, static_cast(i)); + f(n); + })->setEnabled(!g->port(Port::Output, dt, static_cast(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); diff --git a/xybrid/config/pluginregistry.h b/xybrid/config/pluginregistry.h index 29fdbcb..cce6c8f 100644 --- a/xybrid/config/pluginregistry.h +++ b/xybrid/config/pluginregistry.h @@ -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()> createInstance; + bool hidden = false; PluginInfo() = default; virtual ~PluginInfo() = default; @@ -28,6 +30,6 @@ namespace Xybrid::Config { void init(); std::shared_ptr createInstance(const std::string& id); - void populatePluginMenu(QMenu*, std::function)>); + void populatePluginMenu(QMenu*, std::function)>, Data::Graph* = nullptr); } } diff --git a/xybrid/data/graph.cpp b/xybrid/data/graph.cpp index 048ab0d..b145677 100644 --- a/xybrid/data/graph.cpp +++ b/xybrid/data/graph.cpp @@ -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 +#include +#include +#include + namespace { + std::shared_ptr inf; bool c = PluginRegistry::enqueueRegistration([] { auto i = std::make_shared(); i->id = "graph"; i->displayName = "Subgraph"; i->createInstance = []{ return std::make_shared(); }; 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 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(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(chm.value("x").toInteger()); + ch->y = static_cast(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(c[0].toInteger())]; + auto in = children[static_cast(c[3].toInteger())]; + auto op = on->port(Port::Output, static_cast(dtm.keyToValue(c[1].toString().toStdString().c_str())), static_cast(c[2].toInteger())); + auto ip = in->port(Port::Input, static_cast(dtm.keyToValue(c[4].toString().toStdString().c_str())), static_cast(c[5].toInteger())); + if (op && ip) op->connect(ip); + } + } +} + +void Graph::onParent(std::shared_ptr) { + // 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); +} diff --git a/xybrid/data/graph.h b/xybrid/data/graph.h index 923cc9c..87971f6 100644 --- a/xybrid/data/graph.h +++ b/xybrid/data/graph.h @@ -5,11 +5,19 @@ namespace Xybrid::Data { class Graph : public Node { public: + Graph(); + ~Graph() override = default; + std::vector> 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) override; + void onDoubleClick() override; }; } diff --git a/xybrid/data/node.cpp b/xybrid/data/node.cpp index 28203a5..73198ea 100644 --- a/xybrid/data/node.cpp +++ b/xybrid/data/node.cpp @@ -29,6 +29,7 @@ bool Port::connect(std::shared_ptr 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::makePort(DataType dt) { void Node::parentTo(std::shared_ptr 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 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> Node::dependencies() const { + std::unordered_set> 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 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; } diff --git a/xybrid/data/node.h b/xybrid/data/node.h index 90de681..3db9ab0 100644 --- a/xybrid/data/node.h +++ b/xybrid/data/node.h @@ -3,11 +3,17 @@ #include #include #include +#include #include #include +#include + +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 { + 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 owner; std::vector> connections; - std::weak_ptr passthroughFor; + std::weak_ptr passthroughTo; Type type; // TODO: figure out passthrough? uint8_t index; size_t tickUpdatedOn = static_cast(-1); @@ -54,6 +65,7 @@ namespace Xybrid::Data { class Node : public std::enable_shared_from_this { public: + Project* project; std::weak_ptr parent; int x{}, y{}; std::string name; @@ -62,6 +74,8 @@ namespace Xybrid::Data { std::shared_ptr plugin; + QPointer obj; + virtual ~Node() = default; void parentTo(std::shared_ptr); @@ -70,7 +84,25 @@ namespace Xybrid::Data { std::shared_ptr addPort(Port::Type, Port::DataType, uint8_t); void removePort(Port::Type, Port::DataType, uint8_t); + std::unordered_set> dependencies() const; + bool dependsOn(std::shared_ptr); + + 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) { } + virtual void onParent(std::shared_ptr) { } + + virtual void onDoubleClick() { } + // something something customChrome? }; + } +Q_DECLARE_METATYPE(Xybrid::Data::Port) diff --git a/xybrid/data/porttypes.cpp b/xybrid/data/porttypes.cpp new file mode 100644 index 0000000..4f43a6d --- /dev/null +++ b/xybrid/data/porttypes.cpp @@ -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(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(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(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(audioEngine->tickAlloc(s*2)); + bufR = bufL + s; + memset(bufL, 0, s*2); // clear buffers + } +} diff --git a/xybrid/data/porttypes.h b/xybrid/data/porttypes.h index 65edacd..3a18bb4 100644 --- a/xybrid/data/porttypes.h +++ b/xybrid/data/porttypes.h @@ -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; } }; } diff --git a/xybrid/data/project.cpp b/xybrid/data/project.cpp index f69c13c..aab22da 100644 --- a/xybrid/data/project.cpp +++ b/xybrid/data/project.cpp @@ -10,6 +10,7 @@ using namespace Xybrid::Audio; Project::Project() { rootGraph = std::make_shared(); + rootGraph->project = this; } Project::~Project() { diff --git a/xybrid/fileops.cpp b/xybrid/fileops.cpp index 44ca7b6..1c3677c 100644 --- a/xybrid/fileops.cpp +++ b/xybrid/fileops.cpp @@ -1,6 +1,7 @@ #include "fileops.h" #include "uisocket.h" +#include "data/graph.h" #include @@ -95,6 +96,12 @@ bool Xybrid::FileOps::saveProject(std::shared_ptr 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::FileOps::loadProject(QString file } } + { /* Graph */ } { + QCborMap g = main.value("graph").toMap(); + if (!g.isEmpty()) project->rootGraph->loadData(g); + } + return project; } diff --git a/xybrid/gadgets/ioport.cpp b/xybrid/gadgets/ioport.cpp new file mode 100644 index 0000000..9b361c2 --- /dev/null +++ b/xybrid/gadgets/ioport.cpp @@ -0,0 +1,146 @@ +#include "ioport.h" +using Xybrid::Gadgets::IOPort; +using namespace Xybrid::Data; + +#include +#include +#include + +#include +#include +#include + +#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(); + i->id = "ioport"; + i->displayName = "I/O Port"; + i->hidden = true; + i->createInstance = []{ return std::make_shared(); }; + 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(tm.keyToValue(st.c_str())), + static_cast(dtm.keyToValue(sdt.c_str())), + static_cast(pm.value("index").toInteger()) + ); +} + +void IOPort::onUnparent(std::shared_ptr) { remove(); } +void IOPort::onParent(std::shared_ptr) { 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); +} + diff --git a/xybrid/gadgets/ioport.h b/xybrid/gadgets/ioport.h new file mode 100644 index 0000000..07605bc --- /dev/null +++ b/xybrid/gadgets/ioport.h @@ -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) override; + void onParent(std::shared_ptr) override; + + void onGadgetCreated() override; + + void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override; + }; +} diff --git a/xybrid/main.cpp b/xybrid/main.cpp index ab526dd..d45c6d5 100644 --- a/xybrid/main.cpp +++ b/xybrid/main.cpp @@ -1,6 +1,7 @@ #include "mainwindow.h" #include "audio/audioengine.h" #include "config/pluginregistry.h" +#include "data/graph.h" #include @@ -10,6 +11,8 @@ #include int main(int argc, char *argv[]) { + qRegisterMetaType(); + QApplication a(argc, argv); // enable antialiasing on accelerated graphicsview diff --git a/xybrid/mainwindow.cpp b/xybrid/mainwindow.cpp index 847df0f..b1cd99c 100644 --- a/xybrid/mainwindow.cpp +++ b/xybrid/mainwindow.cpp @@ -13,10 +13,13 @@ using Xybrid::MainWindow; #include #include #include +#include #include #include +#include + #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(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(ui->patternEditor->model())->updateColumnDisplay(); }); + connect(socket, &UISocket::openGraph, this, [this](Graph* g) { + if (!g) return; + auto gg = std::static_pointer_cast(g->shared_from_this()); + QString name = QString::fromStdString(gg->name); + if (name.isEmpty()) name = QString::fromStdString(gg->pluginName()); + ui->patchboardBreadcrumbs->push(name, this, [this, gg] { + openGraph(gg); + }); + }); // and from audio engine connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction]() { @@ -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->sequence.push_back(project->newPattern().get()); - // TEMP add some stuff - { - auto g1 = PluginRegistry::createInstance("graph");//std::make_shared(); - 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& 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); } diff --git a/xybrid/mainwindow.h b/xybrid/mainwindow.h index 6df76c4..3096527 100644 --- a/xybrid/mainwindow.h +++ b/xybrid/mainwindow.h @@ -14,6 +14,7 @@ namespace Ui { namespace Xybrid { class MainWindow : public QMainWindow { + friend class Data::Graph; Q_OBJECT public: diff --git a/xybrid/mainwindow.ui b/xybrid/mainwindow.ui index b844f7c..c2c1c92 100644 --- a/xybrid/mainwindow.ui +++ b/xybrid/mainwindow.ui @@ -135,7 +135,7 @@ 16777215 - 24 + 28 @@ -248,10 +248,23 @@ 0 + + + + + 16777215 + 28 + + + + Qt::NoFocus + + + - QGraphicsView::FullViewportUpdate + QGraphicsView::MinimalViewportUpdate @@ -372,6 +385,11 @@ QTableView
ui/patterneditorview.h
+ + Xybrid::UI::BreadcrumbView + QTableView +
ui/breadcrumbview.h
+
diff --git a/xybrid/ui/breadcrumbview.cpp b/xybrid/ui/breadcrumbview.cpp new file mode 100644 index 0000000..7f5de0b --- /dev/null +++ b/xybrid/ui/breadcrumbview.cpp @@ -0,0 +1,58 @@ +#include "breadcrumbview.h" +using namespace Xybrid::UI; + +#include +#include + +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(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 action) { + mdl->actions.resize(std::min(mdl->actions.size(), static_cast(currentIndex().column()) + 1)); + mdl->actions.push_back(std::make_unique(text, bind)); + emit mdl->layoutChanged(); + connect(mdl->actions.back().get(), &QAction::triggered, bind, action); + this->setCurrentIndex(mdl->index(0, static_cast(mdl->actions.size()) - 1)); +} + +BreadcrumbModel::BreadcrumbModel(QWidget* parent) : QAbstractTableModel(parent) { + //actions.push_back(std::make_unique("Root Graph", this)); +} + +int BreadcrumbModel::rowCount(const QModelIndex&) const { + return 1; +} + +int BreadcrumbModel::columnCount(const QModelIndex&) const { + return static_cast(actions.size()); +} + +QVariant BreadcrumbModel::data(const QModelIndex& index, int role) const { + if (actions.size() <= static_cast(index.column())) return QVariant(); // safety + if (role == Qt::DisplayRole) { + return actions[static_cast(index.column())]->text(); + } + if (role == Qt::TextAlignmentRole) return Qt::AlignHCenter + Qt::AlignVCenter; + return QVariant(); +} diff --git a/xybrid/ui/breadcrumbview.h b/xybrid/ui/breadcrumbview.h new file mode 100644 index 0000000..456f4ad --- /dev/null +++ b/xybrid/ui/breadcrumbview.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace Xybrid::UI { + class BreadcrumbView; + class BreadcrumbModel : public QAbstractTableModel { + friend class BreadcrumbView; + Q_OBJECT + + std::vector> 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 action); + }; +} diff --git a/xybrid/ui/patchboard/nodeobject.cpp b/xybrid/ui/patchboard/nodeobject.cpp index 419be8f..97e8580 100644 --- a/xybrid/ui/patchboard/nodeobject.cpp +++ b/xybrid/ui/patchboard/nodeobject.cpp @@ -18,21 +18,17 @@ using Xybrid::Data::Port; #include #include #include +#include + +#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& 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& 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; diff --git a/xybrid/ui/patchboard/nodeobject.h b/xybrid/ui/patchboard/nodeobject.h index 0c7c78d..69a396c 100644 --- a/xybrid/ui/patchboard/nodeobject.h +++ b/xybrid/ui/patchboard/nodeobject.h @@ -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&); ~PortObject() override; @@ -76,8 +79,8 @@ namespace Xybrid::UI { friend class PortObject; std::shared_ptr node; - std::unique_ptr inputPortContainer = nullptr; - std::unique_ptr 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 inputPortContainer = nullptr; + std::unique_ptr outputPortContainer = nullptr; + + bool customChrome = false; + bool canRename = true; + NodeObject(const std::shared_ptr&); + 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& getNode() const { return node; } void promptDelete(); + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override; void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override; diff --git a/xybrid/ui/patchboard/patchboardscene.cpp b/xybrid/ui/patchboard/patchboardscene.cpp index e42911d..dd0fa1e 100644 --- a/xybrid/ui/patchboard/patchboardscene.cpp +++ b/xybrid/ui/patchboard/patchboardscene.cpp @@ -8,6 +8,7 @@ using Xybrid::UI::PatchboardScene; #include #include #include +#include #include @@ -24,34 +25,17 @@ PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptrhorizontalScrollBar(), &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(); - n->x = static_cast(p.x()); - n->y = static_cast(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(vrect.left()); + graph->viewY = static_cast(vrect.top()); + resizeQueued = false; } void PatchboardScene::refresh() { diff --git a/xybrid/ui/patchboard/patchboardscene.h b/xybrid/ui/patchboard/patchboardscene.h index 1158130..e9ac218 100644 --- a/xybrid/ui/patchboard/patchboardscene.h +++ b/xybrid/ui/patchboard/patchboardscene.h @@ -12,6 +12,8 @@ namespace Xybrid::UI { std::shared_ptr graph; QGraphicsView* view; + bool resizeQueued = false; + void queueResize(); void autoSetSize(); public: diff --git a/xybrid/ui/patterneditoritemdelegate.cpp b/xybrid/ui/patterneditoritemdelegate.cpp index 3daafb7..bf17b36 100644 --- a/xybrid/ui/patterneditoritemdelegate.cpp +++ b/xybrid/ui/patterneditoritemdelegate.cpp @@ -93,19 +93,19 @@ namespace { void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { { /* background */ } { auto p = const_cast(static_cast(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); } diff --git a/xybrid/ui/patterneditorview.cpp b/xybrid/ui/patterneditorview.cpp index b104dcd..d15cf9e 100644 --- a/xybrid/ui/patterneditorview.cpp +++ b/xybrid/ui/patterneditorview.cpp @@ -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)); } diff --git a/xybrid/uisocket.h b/xybrid/uisocket.h index f292ace..349ab4e 100644 --- a/xybrid/uisocket.h +++ b/xybrid/uisocket.h @@ -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*); }; } diff --git a/xybrid/util/strings.h b/xybrid/util/strings.h index 2ffe86a..3093b70 100644 --- a/xybrid/util/strings.h +++ b/xybrid/util/strings.h @@ -1,9 +1,17 @@ #pragma once #include +#include namespace Xybrid::Util { template 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 + inline QString enumName(T t) { return QVariant::fromValue(t).toString(); } } diff --git a/xybrid/xybrid.pro b/xybrid/xybrid.pro index e867923..a7f377d 100644 --- a/xybrid/xybrid.pro +++ b/xybrid/xybrid.pro @@ -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