open-story-teller/story-editor/src/story_graph_model.h
2023-05-23 09:31:10 +02:00

224 lines
6.5 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023-2099 Anthony Rabine <anthony@rabine.fr>
#pragma once
#include "src/story_project.h"
#include <QtCore/QJsonObject>
#include <QtCore/QPointF>
#include <QtCore/QSize>
#include <QtNodes/AbstractGraphModel>
#include <QtNodes/StyleCollection>
#include <QtNodes/NodeDelegateModel>
#include <QtNodes/NodeDelegateModelRegistry>
#include <QAudioOutput>
#include <QMediaPlayer>
#include <thread>
#include <queue>
#include <condition_variable>
using QtNodes::ConnectionStyle;
using QtNodes::NodeRole;
using QtNodes::StyleCollection;
using QtNodes::NodeDelegateModelRegistry;
using ConnectionId = QtNodes::ConnectionId;
using ConnectionPolicy = QtNodes::ConnectionPolicy;
using NodeFlag = QtNodes::NodeFlag;
using NodeId = QtNodes::NodeId;
using NodeRole = QtNodes::NodeRole;
using PortIndex = QtNodes::PortIndex;
using PortRole = QtNodes::PortRole;
using PortType = QtNodes::PortType;
using StyleCollection = QtNodes::StyleCollection;
using QtNodes::NodeDelegateModel;
using QtNodes::InvalidNodeId;
class PortAddRemoveWidget;
#include "story_node_base.h"
#include "story_project.h"
template <typename T>
class ThreadSafeQueue {
std::mutex mutex;
std::condition_variable cond_var;
std::queue<T> queue;
public:
void push(T&& item) {
{
std::lock_guard lock(mutex);
queue.push(item);
}
cond_var.notify_one();
}
T& front() {
std::unique_lock lock(mutex);
cond_var.wait(lock, [&]{ return !queue.empty(); });
return queue.front();
}
void pop() {
std::lock_guard lock(mutex);
queue.pop();
}
};
struct AudioCommand {
std::string order;
std::string filename;
};
/**
* The class implements a bare minimum required to demonstrate a model-based
* graph.
*/
class StoryGraphModel : public QtNodes::AbstractGraphModel
{
Q_OBJECT
public:
public:
StoryGraphModel(StoryProject &project);
~StoryGraphModel() override;
std::set<QString> categories() { return m_categories; }
void addModel(const QString &name, const QString &category) {
m_modelNames[name] = category;
m_categories.insert(category);
}
std::unordered_map<QString, QString> models() { return m_modelNames; }
std::unordered_set<NodeId> allNodeIds() const override;
std::unordered_set<ConnectionId> allConnectionIds(NodeId const nodeId) const override;
std::unordered_set<ConnectionId> connections(NodeId nodeId,
PortType portType,
PortIndex portIndex) const override;
bool connectionExists(ConnectionId const connectionId) const override;
NodeId addNode(QString const nodeType = QString()) override;
NodeId addNode(std::shared_ptr<StoryNodeBase> model);
/**
* Connection is possible when graph contains no connectivity data
* in both directions `Out -> In` and `In -> Out`.
*/
bool connectionPossible(ConnectionId const connectionId) const override;
void addConnection(ConnectionId const connectionId) override;
bool nodeExists(NodeId const nodeId) const override;
QVariant nodeData(NodeId nodeId, NodeRole role) const override;
bool setNodeData(NodeId nodeId, NodeRole role, QVariant value) override;
QVariant portData(NodeId nodeId,
PortType portType,
PortIndex portIndex,
PortRole role) const override;
bool setPortData(NodeId nodeId,
PortType portType,
PortIndex portIndex,
QVariant const &value,
PortRole role = PortRole::Data) override;
bool deleteConnection(ConnectionId const connectionId) override;
bool deleteNode(NodeId const nodeId) override;
void addPort(NodeId nodeId, PortType portType, PortIndex portIndex);
void removePort(NodeId nodeId, PortType portType, PortIndex first);
NodeId newNodeId() override { return _nextNodeId++; }
template<class Derived>
void registerNode(const std::string& name) {
m_registry.insert(typename Registry::value_type(name, Factory<Derived>::create_func));
}
std::shared_ptr<StoryNodeBase> createNode(const std::string& key) {
typename Registry::const_iterator i = m_registry.find(key);
if (i == m_registry.end()) {
throw std::invalid_argument(std::string(__PRETTY_FUNCTION__) +
": key not registered");
}
else return i->second(*this);
}
StoryProject &GetProject() { return m_project; };
QString GetImagesDir() const;
QString GetSoundsDir() const;
QString BuildFullImagePath(const QString &fileName) const;
QString BuildFullSoundPath(const QString &fileName) const;
void Clear();
void SetInternalData(NodeId nodeId, nlohmann::json &j);
nlohmann::json Save() const;
void Load(const nlohmann::json &j);
nlohmann::json SaveNode(NodeId const) const;
void LoadNode(const nlohmann::json &nodeJson); // Creates a new node
std::string Build();
// Centralized for wide usage
void PlaySound(const QString &fileName);
NodeId FindFirstNode() const;
signals:
void sigChooseFile(NodeId id, const QString &type);
void sigAudioStopped();
private:
StoryProject &m_project;
std::unordered_set<NodeId> _nodeIds;
// Key: widget name, value: category
std::unordered_map<QString, QString> m_modelNames;
std::set<QString> m_categories;
template<class NodeType>
struct Factory {
static std::shared_ptr<StoryNodeBase> create_func(StoryGraphModel &model) {
return std::make_shared<NodeType>(model);
}
};
typedef std::shared_ptr<StoryNodeBase> (*GenericCreator)(StoryGraphModel &model);
typedef std::map<std::string, GenericCreator> Registry;
Registry m_registry;
/// [Important] This is a user defined data structure backing your model.
/// In your case it could be anything else representing a graph, for example, a
/// table. Or a collection of structs with pointers to each other. Or an
/// abstract syntax tree, you name it.
std::unordered_set<ConnectionId> _connectivity;
std::unordered_map<NodeId, std::shared_ptr<StoryNodeBase>> _models;
/// A convenience variable needed for generating unique node ids.
unsigned int _nextNodeId{0};
std::thread m_audioThread;
ThreadSafeQueue<AudioCommand> m_audioQueue;
void AudioThread();
};