1694 lines
47 KiB
C++
1694 lines
47 KiB
C++
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <imgui_stdlib.h>
|
|
|
|
#include <yas/serialize.hpp>
|
|
#include <yas/types/std/vector.hpp>
|
|
#include <yas/types/utility/usertype.hpp>
|
|
|
|
#include "nf7.hh"
|
|
|
|
#include "common/dir_item.hh"
|
|
#include "common/generic_context.hh"
|
|
#include "common/generic_type_info.hh"
|
|
#include "common/gui_context.hh"
|
|
#include "common/gui_file.hh"
|
|
#include "common/gui_popup.hh"
|
|
#include "common/gui_timeline.hh"
|
|
#include "common/gui_window.hh"
|
|
#include "common/memento.hh"
|
|
#include "common/memento_recorder.hh"
|
|
#include "common/node.hh"
|
|
#include "common/ptr_selector.hh"
|
|
#include "common/sequencer.hh"
|
|
#include "common/squashed_history.hh"
|
|
#include "common/yas_nf7.hh"
|
|
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace nf7 {
|
|
namespace {
|
|
|
|
class TL final : public nf7::File, public nf7::DirItem, public nf7::Node {
|
|
public:
|
|
static inline const nf7::GenericTypeInfo<TL> kType = {
|
|
"Sequencer/Timeline", {"nf7::DirItem"}};
|
|
static void UpdateTypeTooltip() noexcept {
|
|
ImGui::TextUnformatted("Timeline data");
|
|
ImGui::Bullet(); ImGui::TextUnformatted("implements nf7::Node");
|
|
}
|
|
|
|
struct Timing;
|
|
|
|
class Item;
|
|
class Layer;
|
|
class Editor;
|
|
|
|
class Session;
|
|
class Lambda;
|
|
|
|
class ConfigModifyCommand;
|
|
|
|
using ItemId = uint64_t;
|
|
|
|
TL(Env& env,
|
|
uint64_t length = 1000,
|
|
std::vector<std::unique_ptr<Layer>>&& layers = {},
|
|
ItemId next = 1,
|
|
const nf7::gui::Window* win = nullptr) noexcept :
|
|
nf7::File(kType, env), nf7::DirItem(nf7::DirItem::kMenu),
|
|
length_(length), layers_(std::move(layers)), next_(next),
|
|
win_(*this, "Timeline Editor", win), tl_("timeline"),
|
|
popup_add_item_(*this), popup_config_(*this) {
|
|
ApplySeqSocketChanges();
|
|
}
|
|
~TL() noexcept {
|
|
history_.Clear();
|
|
}
|
|
|
|
void SetUpAfterDeserialize();
|
|
TL(Env& env, Deserializer& ar) : TL(env) {
|
|
ar(length_, layers_, seq_inputs_, seq_outputs_, win_, tl_);
|
|
SetUpAfterDeserialize();
|
|
}
|
|
void Serialize(Serializer& ar) const noexcept override {
|
|
ar(length_, layers_, seq_inputs_, seq_outputs_, win_, tl_);
|
|
}
|
|
std::unique_ptr<File> Clone(Env& env) const noexcept override;
|
|
|
|
std::shared_ptr<Node::Lambda> CreateLambda(
|
|
const std::shared_ptr<Node::Lambda>&) noexcept override;
|
|
|
|
void Handle(const Event& ev) noexcept;
|
|
void Update() noexcept override;
|
|
void UpdateMenu() noexcept override;
|
|
|
|
void UpdateEditorWindow() noexcept;
|
|
void UpdateLambdaSelector() noexcept;
|
|
void HandleTimelineAction() noexcept;
|
|
|
|
void UpdateParamPanelWindow() noexcept;
|
|
|
|
File::Interface* interface(const std::type_info& t) noexcept override {
|
|
return nf7::InterfaceSelector<nf7::DirItem, nf7::Node>(t).Select(this);
|
|
}
|
|
|
|
private:
|
|
nf7::SquashedHistory history_;
|
|
|
|
std::shared_ptr<TL::Lambda> lambda_;
|
|
std::vector<std::weak_ptr<TL::Lambda>> lambdas_running_;
|
|
|
|
// permanentized params
|
|
uint64_t length_;
|
|
uint64_t cursor_;
|
|
std::vector<std::unique_ptr<Layer>> layers_;
|
|
|
|
std::vector<std::string> seq_inputs_;
|
|
std::vector<std::string> seq_outputs_;
|
|
|
|
ItemId next_;
|
|
|
|
nf7::gui::Window win_;
|
|
nf7::gui::Timeline tl_;
|
|
|
|
|
|
// popup
|
|
struct AddItemPopup final : nf7::gui::Popup {
|
|
public:
|
|
AddItemPopup(TL& f) noexcept :
|
|
Popup("AddItemPopup"),
|
|
owner_(&f),
|
|
factory_(f, [](auto& t) { return t.flags().contains("nf7::Sequencer"); }) {
|
|
}
|
|
|
|
void Open(uint64_t t, TL::Layer& l) noexcept {
|
|
target_time_ = t;
|
|
target_layer_ = &l;
|
|
Popup::Open();
|
|
}
|
|
void Update() noexcept;
|
|
|
|
private:
|
|
TL* const owner_;
|
|
|
|
uint64_t target_time_ = 0;
|
|
TL::Layer* target_layer_ = nullptr;
|
|
|
|
nf7::gui::FileFactory factory_;
|
|
} popup_add_item_;
|
|
struct ConfigPopup final : nf7::gui::Popup {
|
|
public:
|
|
ConfigPopup(TL& f) noexcept :
|
|
Popup("ConfigPopup"), owner_(&f) {
|
|
}
|
|
|
|
void Open() noexcept {
|
|
inputs_ = StringifySocketList(owner_->seq_inputs_);
|
|
outputs_ = StringifySocketList(owner_->seq_outputs_);
|
|
error_ = "";
|
|
Popup::Open();
|
|
}
|
|
void Update() noexcept;
|
|
|
|
private:
|
|
TL* const owner_;
|
|
|
|
std::string inputs_;
|
|
std::string outputs_;
|
|
std::string error_;
|
|
} popup_config_;
|
|
|
|
|
|
// GUI temporary params
|
|
bool param_panel_request_focus_ = false;
|
|
TL::Item* param_panel_target_ = nullptr;
|
|
|
|
std::unordered_set<TL::Item*> selected_;
|
|
|
|
|
|
// layer operation
|
|
void ExecInsertLayer(size_t, std::unique_ptr<TL::Layer>&& = nullptr) noexcept;
|
|
void ExecRemoveLayer(size_t) noexcept;
|
|
|
|
// item timing operation
|
|
void ExecApplyTimingOfSelected() noexcept;
|
|
void ResizeDisplayTimingOfSelected(int64_t begin_diff, int64_t end_diff) noexcept;
|
|
void MoveDisplayTimingOfSelected(int64_t diff) noexcept;
|
|
|
|
// item layer operation
|
|
void ExecApplyLayerOfSelected() noexcept;
|
|
void MoveDisplayLayerOfSelected(int64_t diff) noexcept;
|
|
|
|
// history
|
|
void ExecUnDo() noexcept {
|
|
env().ExecMain(
|
|
std::make_shared<nf7::GenericContext>(*this, "reverting commands to undo"),
|
|
[this]() { history_.UnDo(); });
|
|
}
|
|
void ExecReDo() noexcept {
|
|
env().ExecMain(
|
|
std::make_shared<nf7::GenericContext>(*this, "applying commands to redo"),
|
|
[this]() { history_.ReDo(); });
|
|
}
|
|
|
|
|
|
// instant running
|
|
void MoveCursorTo(uint64_t t) noexcept;
|
|
void AttachLambda(const std::shared_ptr<TL::Lambda>&) noexcept;
|
|
|
|
// socket operation
|
|
void ApplySeqSocketChanges() noexcept {
|
|
input_ = seq_inputs_;
|
|
input_.push_back("_exec");
|
|
|
|
output_ = seq_outputs_;
|
|
output_.push_back("_cursor");
|
|
}
|
|
static std::vector<std::string> ParseSocketSpecifier(std::string_view names) {
|
|
const auto n = names.size();
|
|
|
|
std::vector<std::string> ret;
|
|
size_t begin = 0;
|
|
|
|
for (size_t i = 0; i <= n; ++i) {
|
|
if (i == n || names[i] == '\n') {
|
|
const auto name = names.substr(begin, i-begin);
|
|
if (name.size() > 0) {
|
|
ret.push_back(std::string {name});
|
|
}
|
|
begin = i+1;
|
|
continue;
|
|
}
|
|
}
|
|
ValidateSocketList(ret);
|
|
return ret;
|
|
}
|
|
static void ValidateSocketList(const std::vector<std::string>& a) {
|
|
for (size_t i = 0; i < a.size(); ++i) {
|
|
File::Path::ValidateTerm(a[i]);
|
|
for (size_t j = i+1; j < a.size(); ++j) {
|
|
if (a[i] == a[j]) {
|
|
throw nf7::Exception {"name duplication: "+a[i]};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static std::string StringifySocketList(const std::vector<std::string>& a) noexcept {
|
|
std::stringstream st;
|
|
for (auto& name : a) {
|
|
st << name << '\n';
|
|
}
|
|
return st.str();
|
|
}
|
|
};
|
|
|
|
|
|
struct TL::Timing {
|
|
public:
|
|
static Timing BeginEnd(uint64_t beg, uint64_t end) noexcept {
|
|
return {beg, end-beg};
|
|
}
|
|
static Timing BeginDur(uint64_t beg, uint64_t dur) noexcept {
|
|
return {beg, dur};
|
|
}
|
|
|
|
Timing(uint64_t beg = 0, uint64_t dur = 1) noexcept :
|
|
begin_(beg), dur_(dur) {
|
|
assert(dur_ > 0);
|
|
}
|
|
Timing(const Timing&) = default;
|
|
Timing(Timing&&) = default;
|
|
Timing& operator=(const Timing&) = default;
|
|
Timing& operator=(Timing&&) = default;
|
|
|
|
bool operator==(const TL::Timing& other) const noexcept {
|
|
return begin_ == other.begin_ && dur_ == other.dur_;
|
|
}
|
|
|
|
void serialize(auto& ar) {
|
|
ar(begin_, dur_);
|
|
}
|
|
|
|
bool IsActiveAt(uint64_t t) noexcept {
|
|
return begin() <= t && t < end();
|
|
}
|
|
bool Intersect(const Timing& t) noexcept {
|
|
return begin() < t.end() && t.begin() < end();
|
|
}
|
|
|
|
uint64_t begin() const noexcept { return begin_; }
|
|
uint64_t end() const noexcept { return begin_+dur_; }
|
|
uint64_t dur() const noexcept { return dur_; }
|
|
|
|
private:
|
|
uint64_t begin_, dur_;
|
|
};
|
|
|
|
|
|
class TL::Item final : nf7::Env::Watcher {
|
|
public:
|
|
Item() = delete;
|
|
Item(ItemId id, std::unique_ptr<nf7::File>&& f, const Timing& t) :
|
|
Watcher(f->env()),
|
|
id_(id), file_(std::move(f)),
|
|
seq_(&file_->interfaceOrThrow<nf7::Sequencer>()),
|
|
mem_(file_->interface<nf7::Memento>()),
|
|
timing_(t), display_timing_(t) {
|
|
}
|
|
Item(const Item&) = delete;
|
|
Item(Item&&) = delete;
|
|
Item& operator=(const Item&) = delete;
|
|
Item& operator=(Item&&) = delete;
|
|
|
|
void Save(auto& ar) {
|
|
ar(id_, file_, timing_);
|
|
}
|
|
static std::unique_ptr<TL::Item> Load(auto& ar) noexcept {
|
|
ItemId id;
|
|
std::unique_ptr<nf7::File> file;
|
|
Timing timing;
|
|
ar(id, file, timing);
|
|
return std::make_unique<TL::Item>(id, std::move(file), timing);
|
|
}
|
|
std::unique_ptr<TL::Item> Clone(nf7::Env& env, ItemId id) const {
|
|
return std::make_unique<TL::Item>(id, file_->Clone(env), timing_);
|
|
}
|
|
|
|
void Attach(TL& f, TL::Layer& layer) noexcept {
|
|
assert(!owner_);
|
|
|
|
owner_ = &f;
|
|
MoveTo(layer);
|
|
file_->MoveUnder(f, std::to_string(id_));
|
|
Watch(file_->id());
|
|
}
|
|
void Detach() noexcept {
|
|
assert(owner_);
|
|
|
|
file_->Isolate();
|
|
owner_ = nullptr;
|
|
layer_ = nullptr;
|
|
display_layer_ = nullptr;
|
|
}
|
|
|
|
void MoveTo(TL::Layer& layer) noexcept {
|
|
layer_ = &layer;
|
|
display_layer_ = &layer;
|
|
}
|
|
void DisplayOn(TL::Layer& layer) noexcept {
|
|
display_layer_ = &layer;
|
|
}
|
|
|
|
void Select(bool single = !ImGui::GetIO().KeyCtrl) noexcept {
|
|
if (single) {
|
|
owner_->selected_.clear();
|
|
}
|
|
owner_->selected_.insert(this);
|
|
}
|
|
void Deselect() noexcept {
|
|
owner_->selected_.erase(this);
|
|
}
|
|
|
|
void Update() noexcept;
|
|
|
|
ItemId id() const noexcept { return id_; }
|
|
nf7::File& file() const noexcept { return *file_; }
|
|
TL::Layer& layer() const noexcept { return *layer_; }
|
|
nf7::Sequencer& seq() const noexcept { return *seq_; }
|
|
Timing& timing() noexcept { return timing_; }
|
|
Timing& displayTiming() noexcept { return display_timing_; }
|
|
TL::Layer& displayLayer() noexcept { return *display_layer_; }
|
|
|
|
private:
|
|
TL* owner_ = nullptr;
|
|
TL::Layer* layer_ = nullptr;
|
|
|
|
ItemId id_;
|
|
std::unique_ptr<nf7::File> file_;
|
|
nf7::Sequencer* const seq_;
|
|
nf7::MementoRecorder mem_;
|
|
|
|
Timing timing_;
|
|
|
|
Timing display_timing_;
|
|
TL::Layer* display_layer_ = nullptr;
|
|
|
|
void Handle(const nf7::File::Event& ev) noexcept override {
|
|
switch (ev.type) {
|
|
case nf7::File::Event::kUpdate:
|
|
if (owner_) {
|
|
if (auto cmd = mem_.CreateCommandIf()) {
|
|
owner_->history_.Add(std::move(cmd));
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
class TL::Layer final {
|
|
public:
|
|
class SwapCommand;
|
|
class ModifyCommand;
|
|
class ItemSwapCommand;
|
|
class ItemMoveCommand;
|
|
class ItemTimingSwapCommand;
|
|
|
|
Layer(std::vector<std::unique_ptr<TL::Item>>&& items = {},
|
|
bool enabled = true, float height = 2) noexcept :
|
|
items_(std::move(items)),
|
|
enabled_(enabled), height_(height) {
|
|
}
|
|
Layer(const Layer&) = delete;
|
|
Layer(Layer&&) = delete;
|
|
Layer& operator=(const Layer&) = delete;
|
|
Layer& operator=(Layer&&) = delete;
|
|
|
|
void Save(auto& ar) {
|
|
ar(items_, enabled_, height_);
|
|
}
|
|
static std::unique_ptr<TL::Layer> Load(auto& ar) noexcept {
|
|
std::vector<std::unique_ptr<nf7::TL::Item>> items;
|
|
bool enabled;
|
|
float height;
|
|
ar(items, enabled, height);
|
|
return std::make_unique<TL::Layer>(std::move(items), enabled, height);
|
|
}
|
|
std::unique_ptr<TL::Layer> Clone(Env& env, ItemId& id) const {
|
|
std::vector<std::unique_ptr<TL::Item>> items;
|
|
items.reserve(items_.size());
|
|
for (auto& item : items_) items.push_back(item->Clone(env, id++));
|
|
return std::make_unique<TL::Layer>(std::move(items), enabled_, height_);
|
|
}
|
|
|
|
void Attach(TL& f, TL::Layer* prev, TL::Layer* next) noexcept {
|
|
assert(!owner_);
|
|
|
|
owner_ = &f;
|
|
prev_ = prev;
|
|
next_ = next;
|
|
for (auto& item : items_) item->Attach(f, *this);
|
|
}
|
|
void Detach() noexcept {
|
|
assert(owner_);
|
|
|
|
for (auto& item : items_) item->Detach();
|
|
owner_ = nullptr;
|
|
prev_ = nullptr;
|
|
next_ = nullptr;
|
|
}
|
|
|
|
// Even after this, the item refers previous layer.
|
|
// To replace to new one, call item.MoveTo().
|
|
void MoveItemTo(TL::Layer& target, TL::Item& item) noexcept {
|
|
auto itr = std::find_if(items_.begin(), items_.end(),
|
|
[&](auto& x) { return x.get() == &item; });
|
|
if (itr == items_.end()) return;
|
|
|
|
auto uptr = std::move(*itr);
|
|
items_.erase(itr);
|
|
|
|
target.items_.push_back(std::move(uptr));
|
|
}
|
|
|
|
TL::Item* GetAt(uint64_t t) const noexcept {
|
|
auto itr = std::find_if(
|
|
items_.begin(), items_.end(),
|
|
[t](auto& x) { return x->timing().IsActiveAt(t); });
|
|
return itr != items_.end()? itr->get(): nullptr;
|
|
}
|
|
std::optional<TL::Timing> GetUnselectedIntersectedPeriod(const TL::Timing& t) const noexcept {
|
|
uint64_t begin = UINT64_MAX, end = 0;
|
|
for (auto& item : items_) {
|
|
if (owner_->selected_.contains(item.get())) continue;
|
|
if (item->timing().Intersect(t)) {
|
|
begin = std::min(begin, item->timing().begin());
|
|
end = std::max(end, item->timing().end());
|
|
}
|
|
}
|
|
if (begin < end) {
|
|
return {TL::Timing::BeginEnd(begin, end)};
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
TL::Item* FindItemAfter(uint64_t t, TL::Item* except = nullptr) const noexcept {
|
|
for (auto& item : items_) {
|
|
if (item.get() == except) continue;
|
|
if (t <= item->timing().begin()) {
|
|
return item.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
TL::Item* FindItemBefore(uint64_t t, TL::Item* except = nullptr) const noexcept {
|
|
for (auto itr = items_.rbegin(); itr < items_.rend(); ++itr) {
|
|
if (itr->get() == except) continue;
|
|
if (t >= (*itr)->timing().end()) {
|
|
return itr->get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
TL::Item* FindUnselectedItemAfter(uint64_t t) const noexcept {
|
|
for (auto& item : items_) {
|
|
if (owner_->selected_.contains(item.get())) continue;
|
|
if (t <= item->timing().begin()) {
|
|
return item.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
TL::Item* FindUnselectedItemBefore(uint64_t t) const noexcept {
|
|
for (auto itr = items_.rbegin(); itr < items_.rend(); ++itr) {
|
|
if (owner_->selected_.contains(itr->get())) continue;
|
|
if (t >= (*itr)->timing().end()) {
|
|
return itr->get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
uint64_t GetMinBeginOf(TL::Item& item) const noexcept {
|
|
auto i = FindItemBefore(item.timing().begin(), &item);
|
|
return i? i->timing().end(): 0;
|
|
}
|
|
uint64_t GetMaxEndOf(TL::Item& item) const noexcept {
|
|
auto i = FindItemAfter(item.timing().begin(), &item);
|
|
return i? i->timing().begin(): owner_->length_;
|
|
}
|
|
|
|
void ExecRemoveItem(Item&) noexcept;
|
|
void ExecSetEnabled(bool) noexcept;
|
|
|
|
void UpdateHeader(size_t idx) noexcept;
|
|
|
|
std::span<const std::unique_ptr<TL::Item>> items() const noexcept { return items_; }
|
|
bool enabled() const noexcept { return enabled_; }
|
|
float height() const noexcept { return height_; }
|
|
|
|
size_t index() const noexcept { return index_; }
|
|
float offsetY() const noexcept { return offset_y_; }
|
|
|
|
private:
|
|
TL* owner_ = nullptr;
|
|
|
|
TL::Layer* prev_ = nullptr;
|
|
TL::Layer* next_ = nullptr;
|
|
|
|
// permanentized
|
|
std::vector<std::unique_ptr<TL::Item>> items_;
|
|
bool enabled_;
|
|
float height_;
|
|
|
|
// GUI temporary parameters
|
|
size_t index_;
|
|
float offset_y_;
|
|
};
|
|
void TL::SetUpAfterDeserialize() {
|
|
next_ = 1;
|
|
std::unordered_set<ItemId> ids;
|
|
for (auto& layer : layers_) {
|
|
for (auto& item : layer->items()) {
|
|
if (item->id() == 0) {
|
|
throw nf7::DeserializeException {"item id cannot be zero"};
|
|
}
|
|
if (ids.contains(item->id())) {
|
|
throw nf7::DeserializeException {"id duplication"};
|
|
}
|
|
ids.insert(item->id());
|
|
next_ = std::max(next_, item->id()+1);
|
|
}
|
|
}
|
|
ApplySeqSocketChanges();
|
|
}
|
|
|
|
|
|
class TL::Lambda final : public Node::Lambda,
|
|
public std::enable_shared_from_this<TL::Lambda> {
|
|
public:
|
|
Lambda() = delete;
|
|
Lambda(TL& f, const std::shared_ptr<Node::Lambda>& parent) noexcept :
|
|
Node::Lambda(f, parent), owner_(&f) {
|
|
}
|
|
|
|
void Handle(std::string_view, const nf7::Value&,
|
|
const std::shared_ptr<Node::Lambda>&) noexcept override;
|
|
|
|
std::shared_ptr<TL::Session> CreateSession(uint64_t t) noexcept {
|
|
auto ss = std::make_shared<TL::Session>(shared_from_this(), last_session_, t, vars_);
|
|
last_session_ = ss;
|
|
sessions_.erase(
|
|
std::remove_if(
|
|
sessions_.begin(), sessions_.end(),
|
|
[](auto& x) { return x.expired(); }),
|
|
sessions_.end());
|
|
sessions_.push_back(ss);
|
|
return ss;
|
|
}
|
|
|
|
std::pair<TL::Item*, std::shared_ptr<Sequencer::Lambda>> GetNext(
|
|
uint64_t& layer_idx, uint64_t layer_until, uint64_t t) noexcept {
|
|
if (aborted_ || !env().GetFile(initiator())) {
|
|
return {nullptr, nullptr};
|
|
}
|
|
layer_until = std::min(layer_until, owner_->layers_.size());
|
|
for (; layer_idx < layer_until; ++layer_idx) {
|
|
auto& layer = *owner_->layers_[layer_idx];
|
|
if (!layer.enabled()) {
|
|
continue;
|
|
}
|
|
if (auto item = layer.GetAt(t)) {
|
|
auto itr = lambdas_.find(item->id());
|
|
if (itr == lambdas_.end()) {
|
|
auto la = item->seq().CreateLambda(shared_from_this());
|
|
lambdas_.emplace(item->id(), la);
|
|
return {item, la};
|
|
} else {
|
|
return {item, itr->second};
|
|
}
|
|
}
|
|
}
|
|
return {nullptr, nullptr};
|
|
}
|
|
|
|
void EmitCursor(uint64_t t) noexcept {
|
|
if (auto p = parent()) {
|
|
p->Handle("_cursor", static_cast<nf7::Value::Integer>(t), shared_from_this());
|
|
}
|
|
}
|
|
void EmitResults(const std::unordered_map<std::string, nf7::Value>& vars) noexcept {
|
|
if (!env().GetFile(initiator())) {
|
|
return;
|
|
}
|
|
auto caller = parent();
|
|
for (const auto& name : owner_->seq_outputs_) {
|
|
auto itr = vars.find(name);
|
|
if (itr == vars.end()) continue;
|
|
caller->Handle(name, itr->second, shared_from_this());
|
|
}
|
|
}
|
|
|
|
void Abort() noexcept {
|
|
aborted_ = true;
|
|
for (auto& p : lambdas_) {
|
|
p.second->Abort();
|
|
}
|
|
}
|
|
|
|
bool aborted() const noexcept { return aborted_; }
|
|
std::span<const std::weak_ptr<TL::Session>> sessions() const noexcept {
|
|
return sessions_;
|
|
}
|
|
|
|
private:
|
|
TL* const owner_;
|
|
|
|
std::atomic<bool> aborted_ = false;
|
|
|
|
std::unordered_map<std::string, nf7::Value> vars_;
|
|
std::unordered_map<ItemId, std::shared_ptr<Sequencer::Lambda>> lambdas_;
|
|
|
|
std::weak_ptr<TL::Session> last_session_;
|
|
std::vector<std::weak_ptr<TL::Session>> sessions_;
|
|
};
|
|
std::shared_ptr<Node::Lambda> TL::CreateLambda(
|
|
const std::shared_ptr<Node::Lambda>& parent) noexcept {
|
|
auto la = std::make_shared<TL::Lambda>(*this, parent);
|
|
|
|
lambdas_running_.erase(
|
|
std::remove_if(lambdas_running_.begin(), lambdas_running_.end(),
|
|
[](auto& x) { return x.expired(); }),
|
|
lambdas_running_.end());;
|
|
lambdas_running_.emplace_back(la);
|
|
return la;
|
|
}
|
|
|
|
|
|
class TL::Session final : public Sequencer::Session,
|
|
public std::enable_shared_from_this<TL::Session> {
|
|
public:
|
|
Session(const std::shared_ptr<TL::Lambda>& initiator,
|
|
const std::weak_ptr<Session>& leader,
|
|
uint64_t time, const std::unordered_map<std::string, nf7::Value>& vars) noexcept :
|
|
env_(&initiator->env()), last_active_(std::chrono::system_clock::now()),
|
|
initiator_(initiator), leader_(leader),
|
|
time_(time), vars_(vars) {
|
|
}
|
|
|
|
const nf7::Value* Peek(std::string_view name) noexcept override {
|
|
auto itr = vars_.find(std::string {name});
|
|
return itr != vars_.end()? &itr->second: nullptr;
|
|
}
|
|
std::optional<nf7::Value> Receive(std::string_view name) noexcept override {
|
|
auto itr = vars_.find(std::string {name});
|
|
if (itr == vars_.end()) {
|
|
return std::nullopt;
|
|
}
|
|
auto ret = std::move(itr->second);
|
|
vars_.erase(itr);
|
|
return ret;
|
|
}
|
|
|
|
void Send(std::string_view name, nf7::Value&& v) noexcept override {
|
|
vars_[std::string {name}] = std::move(v);
|
|
}
|
|
|
|
void StartNext() noexcept {
|
|
auto leader = leader_.lock();
|
|
auto initiator = initiator_.lock();
|
|
|
|
if (!initiator || initiator->aborted()) {
|
|
return;
|
|
}
|
|
|
|
uint64_t layer_until = UINT64_MAX;
|
|
if (leader && !leader->done_) {
|
|
layer_until = leader->layer_? leader->layer_-1: 0;
|
|
} else {
|
|
leader = nullptr;
|
|
}
|
|
|
|
auto [item, lambda] = initiator->GetNext(layer_, layer_until, time_);
|
|
if (item) {
|
|
assert(lambda);
|
|
|
|
const auto& t = item->timing();
|
|
info_.time = time_;
|
|
info_.begin = t.begin();
|
|
info_.end = t.end();
|
|
lambda->Run(shared_from_this());
|
|
|
|
last_active_ = std::chrono::system_clock::now();
|
|
++layer_;
|
|
|
|
} else if (leader) {
|
|
assert(!leader->follower_);
|
|
leader->follower_ = shared_from_this();
|
|
|
|
} else {
|
|
done_ = true;
|
|
initiator->EmitResults(vars_);
|
|
}
|
|
|
|
if (auto follower = std::exchange(follower_, nullptr)) {
|
|
follower->StartNext();
|
|
}
|
|
}
|
|
void Finish() noexcept override {
|
|
if (auto initiator = initiator_.lock()) {
|
|
auto self = shared_from_this();
|
|
env_->ExecSub(initiator, [self]() { self->StartNext(); });
|
|
}
|
|
}
|
|
|
|
const Info& info() const noexcept override { return info_; }
|
|
|
|
std::chrono::system_clock::time_point lastActive() const noexcept { return last_active_; }
|
|
bool done() const noexcept { return done_; }
|
|
uint64_t time() const noexcept { return time_; }
|
|
uint64_t layer() const noexcept { return layer_; }
|
|
|
|
private:
|
|
nf7::Env* const env_;
|
|
std::chrono::system_clock::time_point last_active_;
|
|
|
|
std::weak_ptr<TL::Lambda> initiator_;
|
|
|
|
std::weak_ptr<Session> leader_;
|
|
std::shared_ptr<Session> follower_;
|
|
|
|
const uint64_t time_;
|
|
std::unordered_map<std::string, nf7::Value> vars_;
|
|
|
|
bool done_ = false;
|
|
uint64_t layer_ = 0;
|
|
Info info_;
|
|
};
|
|
void TL::Lambda::Handle(std::string_view name, const nf7::Value& v,
|
|
const std::shared_ptr<Node::Lambda>&) noexcept {
|
|
if (name == "_exec") {
|
|
if (!env().GetFile(initiator())) return;
|
|
const auto t_max = owner_->length_-1;
|
|
|
|
uint64_t t;
|
|
if (v.isInteger()) {
|
|
const auto ti = std::clamp(v.integer(), int64_t{0}, static_cast<int64_t>(t_max));
|
|
t = static_cast<uint64_t>(ti);
|
|
} else if (v.isScalar()) {
|
|
const auto tf = std::clamp(v.scalar(), 0., 1.)*static_cast<double>(t_max);
|
|
t = static_cast<uint64_t>(tf);
|
|
} else {
|
|
// TODO: error
|
|
return;
|
|
}
|
|
CreateSession(t)->StartNext();
|
|
} else {
|
|
vars_[std::string {name}] = v;
|
|
}
|
|
}
|
|
void TL::MoveCursorTo(uint64_t time) noexcept {
|
|
cursor_ = std::min(time, length_-1);
|
|
|
|
if (!lambda_) {
|
|
AttachLambda(std::make_shared<TL::Lambda>(*this, nullptr));
|
|
}
|
|
lambda_->CreateSession(time)->StartNext();
|
|
lambda_->EmitCursor(time);
|
|
}
|
|
void TL::AttachLambda(const std::shared_ptr<TL::Lambda>& la) noexcept {
|
|
if (la == lambda_) return;
|
|
if (lambda_ && lambda_->depth() == 0) {
|
|
lambda_->Abort();
|
|
}
|
|
lambda_ = la;
|
|
}
|
|
|
|
|
|
class TL::Editor final : public nf7::Sequencer::Editor {
|
|
public:
|
|
Editor(TL::Item& item) noexcept : item_(&item) {
|
|
}
|
|
// TODO
|
|
|
|
private:
|
|
TL::Item* const item_;
|
|
};
|
|
|
|
|
|
class TL::Layer::SwapCommand final : public nf7::History::Command {
|
|
public:
|
|
SwapCommand(TL& f, size_t idx, std::unique_ptr<TL::Layer>&& layer = nullptr) noexcept :
|
|
file_(&f), idx_(idx), layer_(std::move(layer)) {
|
|
}
|
|
|
|
void Apply() override { Swap(); }
|
|
void Revert() override { Swap(); }
|
|
|
|
private:
|
|
TL* const file_;
|
|
|
|
size_t idx_;
|
|
std::unique_ptr<TL::Layer> layer_;
|
|
|
|
|
|
void Swap() {
|
|
auto& layers = file_->layers_;
|
|
|
|
if (layer_) {
|
|
// insertion
|
|
if (idx_ > layers.size()) {
|
|
throw nf7::Exception {"index refers out of bounds"};
|
|
}
|
|
auto prev = idx_ > 0? layers[idx_-1].get(): nullptr;
|
|
auto next = idx_+1 < layers.size()? layers[idx_+1].get(): nullptr;
|
|
|
|
if (prev) {
|
|
prev->next_ = layer_.get();
|
|
}
|
|
if (next) {
|
|
next->prev_ = layer_.get();
|
|
}
|
|
|
|
layer_->Attach(*file_, prev, next);
|
|
layers.insert(layers.begin()+static_cast<intmax_t>(idx_), std::move(layer_));
|
|
} else {
|
|
// removal
|
|
if (idx_ >= layers.size()) {
|
|
throw nf7::Exception {"index refers out of bounds"};
|
|
}
|
|
layer_ = std::move(layers[idx_]);
|
|
layer_->Detach();
|
|
layers.erase(layers.begin() + static_cast<intmax_t>(idx_));
|
|
}
|
|
}
|
|
};
|
|
void TL::ExecInsertLayer(size_t idx, std::unique_ptr<TL::Layer>&& layer) noexcept {
|
|
if (!layer) {
|
|
layer = std::make_unique<TL::Layer>();
|
|
}
|
|
auto cmd = std::make_unique<TL::Layer::SwapCommand>(*this, idx, std::move(layer));
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "inserting new layer");
|
|
history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
void TL::ExecRemoveLayer(size_t idx) noexcept {
|
|
auto cmd = std::make_unique<TL::Layer::SwapCommand>(*this, idx);
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "removing an existing layer");
|
|
history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
|
|
class TL::Layer::ModifyCommand final : public nf7::History::Command {
|
|
public:
|
|
struct Builder final {
|
|
public:
|
|
Builder(TL::Layer& layer) noexcept :
|
|
prod_(std::make_unique<ModifyCommand>(layer)){
|
|
}
|
|
|
|
Builder& enabled(bool v) {
|
|
prod_->enabled_ = v;
|
|
return *this;
|
|
}
|
|
|
|
std::unique_ptr<ModifyCommand> Build() noexcept {
|
|
return std::move(prod_);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<ModifyCommand> prod_;
|
|
};
|
|
|
|
ModifyCommand(TL::Layer& layer) noexcept : layer_(&layer) {
|
|
}
|
|
|
|
void Apply() noexcept { Exec(); }
|
|
void Revert() noexcept { Exec(); }
|
|
|
|
private:
|
|
TL::Layer* const layer_;
|
|
|
|
std::optional<bool> enabled_;
|
|
|
|
|
|
void Exec() noexcept {
|
|
if (enabled_) {
|
|
std::swap(*enabled_, layer_->enabled_);
|
|
}
|
|
}
|
|
};
|
|
void TL::Layer::ExecSetEnabled(bool v) noexcept {
|
|
auto cmd = TL::Layer::ModifyCommand::Builder(*this).enabled(v).Build();
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*owner_, "toggling if layer is enabled");
|
|
owner_->history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
|
|
class TL::Layer::ItemSwapCommand final : public nf7::History::Command {
|
|
public:
|
|
ItemSwapCommand(Layer& layer, std::unique_ptr<TL::Item>&& item) noexcept :
|
|
layer_(&layer), item_(std::move(item)), ptr_(item_.get()) {
|
|
}
|
|
ItemSwapCommand(Layer& layer, TL::Item& item) noexcept :
|
|
layer_(&layer), ptr_(&item) {
|
|
}
|
|
|
|
void Apply() override { Swap(); }
|
|
void Revert() override { Swap(); }
|
|
|
|
private:
|
|
Layer* const layer_;
|
|
std::unique_ptr<TL::Item> item_;
|
|
TL::Item* const ptr_;
|
|
|
|
void Swap() {
|
|
auto& items = layer_->items_;
|
|
if (item_) {
|
|
const auto& t = item_->timing();
|
|
auto itr = std::find_if(
|
|
items.begin(), items.end(),
|
|
[t = t.begin()](auto& x) { return t <= x->timing().begin(); });
|
|
if (itr != items.end()) {
|
|
if (t.end() > (*itr)->timing().begin()) {
|
|
throw nf7::History::CorruptException {"timing overlap"};
|
|
}
|
|
}
|
|
item_->Attach(*layer_->owner_, *layer_);
|
|
items.insert(itr, std::move(item_));
|
|
} else {
|
|
auto itr = std::find_if(items.begin(), items.end(),
|
|
[ptr = ptr_](auto& x) { return x.get() == ptr; });
|
|
if (itr == items.end()) {
|
|
throw nf7::History::CorruptException {"target item missing"};
|
|
}
|
|
item_ = std::move(*itr);
|
|
item_->Detach();
|
|
items.erase(itr);
|
|
}
|
|
}
|
|
};
|
|
void TL::Layer::ExecRemoveItem(Item& item) noexcept {
|
|
auto cmd = std::make_unique<ItemSwapCommand>(*this, item);
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*owner_, "removing an existing item");
|
|
owner_->history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
|
|
class TL::Layer::ItemTimingSwapCommand final : public nf7::History::Command {
|
|
public:
|
|
ItemTimingSwapCommand(TL::Item& item, TL::Timing timing) noexcept :
|
|
item_(&item), timing_(timing) {
|
|
}
|
|
|
|
void Apply() noexcept override { Exec(); }
|
|
void Revert() noexcept override { Exec(); }
|
|
|
|
private:
|
|
TL::Item* const item_;
|
|
TL::Timing timing_;
|
|
|
|
void Exec() noexcept {
|
|
std::swap(item_->timing(), timing_);
|
|
item_->displayTiming() = item_->timing();
|
|
|
|
// TODO: reorder item
|
|
}
|
|
};
|
|
void TL::ExecApplyTimingOfSelected() noexcept {
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "applying item timing changes");
|
|
for (auto item : selected_) {
|
|
auto cmd = std::make_unique<
|
|
TL::Layer::ItemTimingSwapCommand>(*item, item->displayTiming());
|
|
history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
}
|
|
void TL::ResizeDisplayTimingOfSelected(int64_t begin_diff, int64_t end_diff) noexcept {
|
|
if (begin_diff == 0 && end_diff == 0) {
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<TL::Item*, TL::Timing>> timings;
|
|
timings.reserve(selected_.size());
|
|
for (auto item : selected_) {
|
|
auto& layer = item->displayLayer();
|
|
const auto begin_min = static_cast<int64_t>(layer.GetMinBeginOf(*item));
|
|
const auto end_max = static_cast<int64_t>(layer.GetMaxEndOf(*item));
|
|
|
|
const auto& t = item->displayTiming();
|
|
const auto pbegin = static_cast<int64_t>(t.begin());
|
|
const auto pend = static_cast<int64_t>(t.end());
|
|
|
|
const auto begin = std::clamp(pbegin+begin_diff, begin_min, pend-1);
|
|
const auto end = std::clamp(pend+end_diff, pbegin+1, end_max);
|
|
|
|
auto begin_actual_diff = begin-pbegin;
|
|
auto end_actual_diff = end-pend;
|
|
if ((begin_actual_diff != begin_diff) || (end_actual_diff != end_diff)) {
|
|
ResizeDisplayTimingOfSelected(begin_actual_diff, end_actual_diff);
|
|
return;
|
|
}
|
|
|
|
const auto ubegin = static_cast<uint64_t>(begin);
|
|
const auto uend = static_cast<uint64_t>(end);
|
|
timings.emplace_back(item, TL::Timing::BeginEnd(ubegin, uend));
|
|
}
|
|
for (auto& p : timings) { p.first->displayTiming() = p.second; }
|
|
}
|
|
void TL::MoveDisplayTimingOfSelected(int64_t diff) noexcept {
|
|
if (diff == 0) {
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<TL::Item*, TL::Timing>> timings;
|
|
timings.reserve(selected_.size());
|
|
for (auto item : selected_) {
|
|
const auto& t = item->displayTiming();
|
|
const auto pbegin = static_cast<int64_t>(t.begin());
|
|
const auto pdur = static_cast<int64_t>(t.dur());
|
|
const auto pend = static_cast<int64_t>(t.end());
|
|
const auto len = static_cast<int64_t>(length_);
|
|
|
|
const auto begin = std::clamp(pbegin+diff, int64_t{0}, len-pdur);
|
|
|
|
const auto begin_actual_diff = begin - pbegin;
|
|
if (begin_actual_diff != diff) {
|
|
MoveDisplayTimingOfSelected(begin_actual_diff);
|
|
return;
|
|
}
|
|
|
|
const auto timing = TL::Timing::BeginDur(static_cast<uint64_t>(begin), t.dur());
|
|
|
|
if (auto inter = item->displayLayer().GetUnselectedIntersectedPeriod(timing)) {
|
|
const auto bsnap = static_cast<int64_t>(inter->end()) - pbegin;
|
|
const auto esnap = static_cast<int64_t>(inter->begin()) - pend;
|
|
|
|
const auto snap = std::abs(bsnap) < std::abs(esnap)? bsnap: esnap;
|
|
MoveDisplayTimingOfSelected(snap);
|
|
return;
|
|
}
|
|
timings.emplace_back(item, timing);
|
|
}
|
|
for (auto p : timings) { p.first->displayTiming() = p.second; }
|
|
}
|
|
|
|
class TL::Layer::ItemMoveCommand final : public nf7::History::Command {
|
|
public:
|
|
ItemMoveCommand(TL::Layer& src, TL::Layer& dst, TL::Item& item) noexcept :
|
|
src_(&src), dst_(&dst), item_(&item) {
|
|
}
|
|
void Apply() noexcept override {
|
|
src_->MoveItemTo(*dst_, *item_);
|
|
item_->MoveTo(*dst_);
|
|
}
|
|
void Revert() noexcept override {
|
|
dst_->MoveItemTo(*src_, *item_);
|
|
item_->MoveTo(*src_);
|
|
}
|
|
|
|
private:
|
|
TL::Layer* const src_;
|
|
TL::Layer* const dst_;
|
|
TL::Item* const item_;
|
|
};
|
|
void TL::ExecApplyLayerOfSelected() noexcept {
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "moving items between layers");
|
|
for (auto item : selected_) {
|
|
auto& src = item->layer();
|
|
auto& dst = item->displayLayer();
|
|
if (&src == &dst) {
|
|
continue;
|
|
}
|
|
|
|
auto cmd = std::make_unique<TL::Layer::ItemMoveCommand>(src, dst, *item);
|
|
history_.Add(std::move(cmd));
|
|
env().ExecMain(ctx, [item]() { item->MoveTo(item->displayLayer()); });
|
|
}
|
|
}
|
|
void TL::MoveDisplayLayerOfSelected(int64_t diff) noexcept {
|
|
assert(layers_.size() > 0);
|
|
if (diff == 0) {
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<TL::Item*, TL::Layer*>> layers;
|
|
layers.reserve(selected_.size());
|
|
for (auto item : selected_) {
|
|
const auto current = static_cast<int64_t>(item->displayLayer().index());
|
|
const auto target = std::clamp(
|
|
current+diff, int64_t{0}, static_cast<int64_t>(layers_.size()-1));
|
|
|
|
const auto actual_diff = target - current;
|
|
if (actual_diff != diff) {
|
|
MoveDisplayLayerOfSelected(actual_diff);
|
|
return;
|
|
}
|
|
|
|
auto& layer = *layers_[static_cast<size_t>(target)];
|
|
if (layer.GetUnselectedIntersectedPeriod(item->displayTiming())) {
|
|
MoveDisplayLayerOfSelected(diff > 0? diff-1: diff+1);
|
|
return;
|
|
}
|
|
layers.emplace_back(item, &layer);
|
|
}
|
|
for (auto& p : layers) {
|
|
p.first->displayLayer().MoveItemTo(*p.second, *p.first);
|
|
p.first->DisplayOn(*p.second);
|
|
}
|
|
}
|
|
|
|
|
|
class TL::ConfigModifyCommand final : public nf7::History::Command {
|
|
public:
|
|
struct Builder final {
|
|
public:
|
|
Builder(TL& f) noexcept :
|
|
prod_(std::make_unique<ConfigModifyCommand>(f)) {
|
|
}
|
|
|
|
Builder& length(uint64_t v) noexcept {
|
|
prod_->length_ = v;
|
|
return *this;
|
|
}
|
|
Builder& inputs(std::vector<std::string>&& v) noexcept {
|
|
prod_->seq_inputs_ = std::move(v);
|
|
return *this;
|
|
}
|
|
Builder& outputs(std::vector<std::string>&& v) noexcept {
|
|
prod_->seq_outputs_ = std::move(v);
|
|
return *this;
|
|
}
|
|
|
|
std::unique_ptr<ConfigModifyCommand> Build() noexcept {
|
|
return std::move(prod_);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<ConfigModifyCommand> prod_;
|
|
};
|
|
|
|
ConfigModifyCommand(TL& f) noexcept : owner_(&f) {
|
|
}
|
|
|
|
void Apply() override { Exec(); }
|
|
void Revert() override { Exec(); }
|
|
|
|
private:
|
|
TL* const owner_;
|
|
|
|
std::optional<uint64_t> length_;
|
|
std::optional<std::vector<std::string>> seq_inputs_, seq_outputs_;
|
|
|
|
void Exec() noexcept {
|
|
if (length_) {
|
|
std::swap(owner_->length_, *length_);
|
|
}
|
|
if (seq_inputs_) {
|
|
std::swap(owner_->seq_inputs_, *seq_inputs_);
|
|
}
|
|
if (seq_outputs_) {
|
|
std::swap(owner_->seq_outputs_, *seq_outputs_);
|
|
}
|
|
|
|
if (seq_inputs_ || seq_outputs_) {
|
|
owner_->ApplySeqSocketChanges();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
std::unique_ptr<nf7::File> TL::Clone(nf7::Env& env) const noexcept {
|
|
std::vector<std::unique_ptr<TL::Layer>> layers;
|
|
layers.reserve(layers_.size());
|
|
ItemId next = 1;
|
|
for (const auto& layer : layers_) layers.push_back(layer->Clone(env, next));
|
|
return std::make_unique<TL>(env, length_, std::move(layers), next, &win_);
|
|
}
|
|
void TL::Handle(const Event& ev) noexcept {
|
|
switch (ev.type) {
|
|
case Event::kAdd:
|
|
if (layers_.size() == 0) {
|
|
layers_.reserve(10);
|
|
for (size_t i = 0; i < 10; ++i) {
|
|
layers_.push_back(std::make_unique<TL::Layer>());
|
|
}
|
|
}
|
|
|
|
// update layers
|
|
{
|
|
TL::Layer* q[3] = {layers_[0].get(), nullptr, nullptr};
|
|
for (size_t i = 1; i < layers_.size(); ++i) {
|
|
q[2] = q[1];
|
|
q[1] = q[0];
|
|
q[0] = layers_[i].get();
|
|
q[1]->Attach(*this, q[2], q[0]);
|
|
}
|
|
if (q[0]) q[0]->Attach(*this, q[1], nullptr);
|
|
}
|
|
break;
|
|
case Event::kRemove:
|
|
for (const auto& layer : layers_) layer->Detach();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TL::Update() noexcept {
|
|
for (const auto& layer : layers_) {
|
|
for (const auto& item : layer->items()) {
|
|
item->file().Update();
|
|
}
|
|
}
|
|
|
|
popup_add_item_.Update();
|
|
popup_config_.Update();
|
|
|
|
UpdateEditorWindow();
|
|
UpdateParamPanelWindow();
|
|
|
|
if (history_.Squash()) {
|
|
env().ExecMain(std::make_shared<nf7::GenericContext>(*this),
|
|
[this]() { Touch(); });
|
|
}
|
|
}
|
|
void TL::UpdateMenu() noexcept {
|
|
ImGui::MenuItem("Editor", nullptr, &win_.shown());
|
|
}
|
|
void TL::UpdateEditorWindow() noexcept {
|
|
const auto kInit = []() {
|
|
const auto em = ImGui::GetFontSize();
|
|
ImGui::SetNextWindowSizeConstraints({24*em, 8*em}, {1e8, 1e8});
|
|
};
|
|
|
|
if (win_.Begin(kInit)) {
|
|
UpdateLambdaSelector();
|
|
|
|
// timeline
|
|
if (tl_.Begin(length_)) {
|
|
// layer headers
|
|
for (size_t i = 0; i < layers_.size(); ++i) {
|
|
auto& layer = layers_[i];
|
|
tl_.NextLayerHeader(layer.get(), layer->height());
|
|
ImGui::PushID(layer.get());
|
|
layer->UpdateHeader(i);
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (tl_.BeginBody()) {
|
|
// context menu on timeline
|
|
if (ImGui::BeginPopupContextWindow()) {
|
|
if (ImGui::MenuItem("add new item")) {
|
|
if (auto layer = reinterpret_cast<TL::Layer*>(tl_.mouseLayer())) {
|
|
popup_add_item_.Open(tl_.mouseTime(), *layer);
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("undo", nullptr, false, !!history_.prev())) {
|
|
ExecUnDo();
|
|
}
|
|
if (ImGui::MenuItem("redo", nullptr, false, !!history_.next())) {
|
|
ExecReDo();
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("config")) {
|
|
popup_config_.Open();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// layer body
|
|
for (auto& layer : layers_) {
|
|
tl_.NextLayer(layer.get(), layer->height());
|
|
for (auto& item : layer->items()) {
|
|
const auto& t = item->displayTiming();
|
|
if (tl_.BeginItem(item.get(), t.begin(), t.end())) {
|
|
item->Update();
|
|
}
|
|
tl_.EndItem();
|
|
}
|
|
}
|
|
}
|
|
tl_.EndBody();
|
|
|
|
// mouse curosr
|
|
constexpr auto kFlags =
|
|
ImGuiHoveredFlags_ChildWindows |
|
|
ImGuiHoveredFlags_AllowWhenBlockedByPopup;
|
|
if (ImGui::IsWindowHovered(kFlags)) {
|
|
tl_.Cursor(
|
|
"mouse",
|
|
std::min(length_-1, tl_.GetTimeFromScreenX(ImGui::GetMousePos().x)),
|
|
ImGui::GetColorU32(ImGuiCol_TextDisabled, .5f));
|
|
}
|
|
|
|
// frame cursor
|
|
tl_.Cursor("cursor", cursor_, ImGui::GetColorU32(ImGuiCol_Text, .5f));
|
|
|
|
// end of timeline
|
|
tl_.Cursor("END", length_, ImGui::GetColorU32(ImGuiCol_TextDisabled));
|
|
|
|
// running sessions
|
|
if (lambda_) {
|
|
const auto now = std::chrono::system_clock::now();
|
|
for (auto& wss : lambda_->sessions()) {
|
|
auto ss = wss.lock();
|
|
if (!ss || ss->done()) continue;
|
|
|
|
const auto elapsed =
|
|
static_cast<float>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now - ss->lastActive()).count()) / 1000;
|
|
|
|
const auto alpha = 1.f - std::clamp(elapsed, 0.f, 1.f)*0.6f;
|
|
const auto color = IM_COL32(255, 0, 0, static_cast<uint8_t>(alpha*255));
|
|
|
|
tl_.Cursor("S", ss->time(), color);
|
|
if (ss->layer() > 0) {
|
|
tl_.Arrow(ss->time(), ss->layer()-1, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
HandleTimelineAction();
|
|
}
|
|
tl_.End();
|
|
|
|
// key bindings
|
|
const bool focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
if (focused && !ImGui::IsAnyItemFocused()) {
|
|
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
|
|
if (cursor_ > 0) MoveCursorTo(cursor_-1);
|
|
} else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
|
|
MoveCursorTo(cursor_+1);
|
|
}
|
|
}
|
|
}
|
|
win_.End();
|
|
}
|
|
void TL::UpdateLambdaSelector() noexcept {
|
|
const auto current_lambda =
|
|
lambda_? nf7::gui::GetParentContextDisplayName(*lambda_): "(unselected)";
|
|
if (ImGui::BeginCombo("##lambda", current_lambda.c_str())) {
|
|
if (lambda_) {
|
|
if (ImGui::Selectable("detach from current lambda")) {
|
|
AttachLambda(nullptr);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
for (const auto& wptr : lambdas_running_) {
|
|
auto ptr = wptr.lock();
|
|
if (!ptr) continue;
|
|
|
|
const auto name = nf7::gui::GetParentContextDisplayName(*ptr);
|
|
if (ImGui::Selectable(name.c_str(), ptr == lambda_)) {
|
|
AttachLambda(ptr);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextUnformatted("call stack:");
|
|
ImGui::Indent();
|
|
nf7::gui::ContextStack(*ptr);
|
|
ImGui::Unindent();
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
}
|
|
void TL::HandleTimelineAction() noexcept {
|
|
auto item = reinterpret_cast<TL::Item*>(tl_.actionTarget());
|
|
const auto action_time = tl_.actionTime();
|
|
const auto action_time_i = static_cast<int64_t>(action_time);
|
|
|
|
switch (tl_.action()) {
|
|
case tl_.kSelect:
|
|
assert(item);
|
|
item->Select();
|
|
if (item != std::exchange(param_panel_target_, item)) {
|
|
param_panel_request_focus_ = true;
|
|
}
|
|
break;
|
|
|
|
case tl_.kResizeBegin:
|
|
assert(item);
|
|
ResizeDisplayTimingOfSelected(
|
|
action_time_i - static_cast<int64_t>(item->displayTiming().begin()),
|
|
0);
|
|
break;
|
|
case tl_.kResizeEnd:
|
|
assert(item);
|
|
ResizeDisplayTimingOfSelected(
|
|
0,
|
|
action_time_i +
|
|
static_cast<int64_t>(item->displayTiming().dur()) -
|
|
static_cast<int64_t>(item->displayTiming().end()));
|
|
break;
|
|
case tl_.kResizeBeginDone:
|
|
case tl_.kResizeEndDone:
|
|
assert(item);
|
|
ExecApplyTimingOfSelected();
|
|
break;
|
|
|
|
case tl_.kMove:
|
|
assert(item);
|
|
MoveDisplayTimingOfSelected(
|
|
action_time_i - static_cast<int64_t>(item->displayTiming().begin()));
|
|
if (auto layer = reinterpret_cast<TL::Layer*>(tl_.mouseLayer())) {
|
|
MoveDisplayLayerOfSelected(
|
|
static_cast<int64_t>(layer->index()) -
|
|
static_cast<int64_t>(item->displayLayer().index()));
|
|
}
|
|
break;
|
|
case tl_.kMoveDone:
|
|
assert(item);
|
|
ExecApplyTimingOfSelected();
|
|
ExecApplyLayerOfSelected();
|
|
break;
|
|
|
|
case tl_.kSetTime:
|
|
MoveCursorTo(action_time);
|
|
break;
|
|
|
|
case tl_.kNone:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TL::UpdateParamPanelWindow() noexcept {
|
|
if (!win_.shown()) return;
|
|
|
|
const auto name = abspath().Stringify() + " | Parameter Panel";
|
|
|
|
if (std::exchange(param_panel_request_focus_, false)) {
|
|
ImGui::SetNextWindowFocus();
|
|
}
|
|
if (ImGui::Begin(name.c_str())) {
|
|
if (auto item = param_panel_target_) {
|
|
if (item->seq().flags() & Sequencer::kParamPanel) {
|
|
TL::Editor ed {*item};
|
|
item->seq().UpdateParamPanel(ed);
|
|
} else {
|
|
ImGui::TextUnformatted("item doesn't have parameter panel");
|
|
}
|
|
} else {
|
|
ImGui::TextUnformatted("no item selected");
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void TL::Layer::UpdateHeader(size_t idx) noexcept {
|
|
index_ = idx;
|
|
offset_y_ = ImGui::GetCursorScreenPos().y;
|
|
|
|
const auto em = ImGui::GetFontSize();
|
|
const auto h = height_*em;
|
|
const auto w = owner_->tl_.headerWidth();
|
|
const auto pad = owner_->tl_.padding();
|
|
|
|
auto name = std::to_string(idx);
|
|
if (!enabled_) {
|
|
name = "("+name+")";
|
|
}
|
|
|
|
if (ImGui::Button(name.c_str(), {w, h})) {
|
|
ExecSetEnabled(!enabled_);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::Text("layer [%zu]", idx);
|
|
ImGui::Indent();
|
|
ImGui::Text("enabled: %s", enabled_? "yes": "no");
|
|
ImGui::Unindent();
|
|
ImGui::EndTooltip();
|
|
}
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("insert")) {
|
|
owner_->ExecInsertLayer(idx);
|
|
}
|
|
if (ImGui::MenuItem("remove", nullptr, nullptr, owner_->layers_.size() >= 2)) {
|
|
owner_->ExecRemoveLayer(idx);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("enabled", nullptr, enabled_)) {
|
|
ExecSetEnabled(!enabled_);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::InvisibleButton("resizer", {w, pad*2});
|
|
if (ImGui::IsItemActive()) {
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
|
|
height_ += ImGui::GetIO().MouseDelta.y / em;
|
|
height_ = std::clamp(height_, 1.6f, 8.f);
|
|
} else {
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
|
|
}
|
|
}
|
|
}
|
|
void TL::Item::Update() noexcept {
|
|
assert(owner_);
|
|
assert(layer_);
|
|
|
|
TL::Editor ed {*this};
|
|
const auto sz = ImGui::GetContentRegionMax();
|
|
const bool select = owner_->selected_.contains(this);
|
|
|
|
// popup menu
|
|
if (ImGui::BeginPopupContextWindow()) {
|
|
if (ImGui::IsWindowAppearing()) {
|
|
Select(false);
|
|
}
|
|
if (ImGui::MenuItem("remove")) {
|
|
layer_->ExecRemoveItem(*this);
|
|
}
|
|
if (seq_->flags() & nf7::Sequencer::kMenu) {
|
|
ImGui::Separator();
|
|
seq_->UpdateMenu(ed);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// contents
|
|
if (seq_->flags() & nf7::Sequencer::kCustomItem) {
|
|
seq_->UpdateItem(ed);
|
|
} else {
|
|
ImGui::TextUnformatted(file_->type().name().c_str());
|
|
}
|
|
|
|
// tooltip
|
|
ImGui::SetCursorPos({0, 0});
|
|
ImGui::Dummy(sz);
|
|
if (seq_->flags() & nf7::Sequencer::kTooltip) {
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
seq_->UpdateTooltip(ed);
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
// border
|
|
const auto spos = ImGui::GetWindowPos();
|
|
const auto size = ImGui::GetWindowSize();
|
|
const auto col = ImGui::GetColorU32(
|
|
select? ImGuiCol_TextSelectedBg: ImGuiCol_Text);
|
|
auto d = ImGui::GetWindowDrawList();
|
|
d->AddRect(spos + ImVec2 {0, 1}, spos+size - ImVec2 {0, 1}, col);
|
|
}
|
|
|
|
|
|
void TL::AddItemPopup::Update() noexcept {
|
|
if (Popup::Begin()) {
|
|
ImGui::TextUnformatted("Sequencer/Timeline: adding new item...");
|
|
if (factory_.Update()) {
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
auto& layer = *target_layer_;
|
|
auto time = target_time_;
|
|
|
|
uint64_t dur = static_cast<uint64_t>(4.f / owner_->tl_.zoom());
|
|
if (auto item = layer.FindItemAfter(time)) {
|
|
dur = std::min(dur, item->timing().begin() - time);
|
|
}
|
|
auto file = factory_.type().Create(owner_->env());
|
|
auto timing = TL::Timing::BeginDur(time, dur);
|
|
auto item = std::make_unique<TL::Item>(owner_->next_++, std::move(file), timing);
|
|
auto cmd = std::make_unique<TL::Layer::ItemSwapCommand>(layer, std::move(item));
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*owner_, "adding new item");
|
|
owner_->history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
void TL::ConfigPopup::Update() noexcept {
|
|
if (Popup::Begin()) {
|
|
ImGui::TextUnformatted("Sequencer/Timeline: updating config...");
|
|
ImGui::InputTextMultiline("inputs", &inputs_);
|
|
ImGui::InputTextMultiline("outputs", &outputs_);
|
|
|
|
if (ImGui::Button("ok")) {
|
|
try {
|
|
auto cmd = ConfigModifyCommand::Builder(*owner_).
|
|
inputs(ParseSocketSpecifier(inputs_)).
|
|
outputs(ParseSocketSpecifier(outputs_)).
|
|
Build();
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*owner_, "updating config");
|
|
owner_->history_.Add(std::move(cmd)).ExecApply(ctx);
|
|
} catch (nf7::Exception& e) {
|
|
error_ = e.msg();
|
|
}
|
|
}
|
|
if (error_.size() > 0) {
|
|
ImGui::Bullet();
|
|
ImGui::TextUnformatted(error_.c_str());
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
}
|
|
} // namespace nf7
|
|
|
|
|
|
|
|
namespace yas::detail {
|
|
|
|
template <size_t F>
|
|
struct serializer<
|
|
type_prop::not_a_fundamental,
|
|
ser_case::use_internal_serializer,
|
|
F,
|
|
std::unique_ptr<nf7::TL::Layer>> {
|
|
public:
|
|
template <typename Archive>
|
|
static Archive& save(Archive& ar, const std::unique_ptr<nf7::TL::Layer>& layer) {
|
|
layer->Save(ar);
|
|
return ar;
|
|
}
|
|
template <typename Archive>
|
|
static Archive& load(Archive& ar, std::unique_ptr<nf7::TL::Layer>& layer) {
|
|
layer = nf7::TL::Layer::Load(ar);
|
|
return ar;
|
|
}
|
|
};
|
|
|
|
template <size_t F>
|
|
struct serializer<
|
|
type_prop::not_a_fundamental,
|
|
ser_case::use_internal_serializer,
|
|
F,
|
|
std::unique_ptr<nf7::TL::Item>> {
|
|
public:
|
|
template <typename Archive>
|
|
static Archive& save(Archive& ar, const std::unique_ptr<nf7::TL::Item>& item) {
|
|
item->Save(ar);
|
|
return ar;
|
|
}
|
|
template <typename Archive>
|
|
static Archive& load(Archive& ar, std::unique_ptr<nf7::TL::Item>& item) {
|
|
item = nf7::TL::Item::Load(ar);
|
|
return ar;
|
|
}
|
|
};
|
|
|
|
} // namespace yas::detail
|