mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-21 01:42:09 +00:00
refactor: collapse speed-dial action source abstraction
Only one action source ever existed, so the IActionSource interface and ScriptActionSource are gone. ActionRegistry now subscribes to the plugin loader and enumerates actions directly in init() - no polymorphism for one impl. Replace the opaque FNV-hash SpeedDialActionId with a readable composed id of the form prefix:title:source_key. Split AppAction's single source field into source_key (stable identity, e.g. plugin_key) and source_name (display), so identity and display no longer share one field.
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
#include "ActionRegistry.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "PluginScriptRunner.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -52,20 +57,172 @@ double frecency_score(int count, long long last, long long now)
|
||||
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
|
||||
}
|
||||
|
||||
// ---- script-plugin action source (the one and only source) ------------------
|
||||
|
||||
std::string find_loaded_source_name(PluginLoader& loader, const std::string& plugin_key)
|
||||
{
|
||||
for (const PluginDescriptor& desc : loader.get_all_loaded_plugin_descriptors())
|
||||
if (desc.plugin_key == plugin_key)
|
||||
return desc.name.empty() ? plugin_key : desc.name;
|
||||
return plugin_key;
|
||||
}
|
||||
|
||||
// A runnable script capability exposed as a speed-dial action. source_key = plugin_key
|
||||
// (identity), so a plugin display-name change does not re-key the action.
|
||||
struct PluginScriptAction : AppAction
|
||||
{
|
||||
static constexpr const char* kIdPrefix = "plugin_script_action";
|
||||
|
||||
std::string plugin_key;
|
||||
std::string capability;
|
||||
|
||||
// The id an action for (plugin_key, capability) would have - lets refresh_capability
|
||||
// remove a gone capability without materialising the action.
|
||||
static std::string id_for(const std::string& plugin_key, const std::string& capability)
|
||||
{
|
||||
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
|
||||
}
|
||||
|
||||
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
|
||||
: AppAction(kIdPrefix,
|
||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||
plugin_key_in, // source_key
|
||||
std::move(source_name)),
|
||||
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
|
||||
{}
|
||||
|
||||
AppActionRunResult run() const override
|
||||
{
|
||||
ScriptRunOutcome outcome = run_script_plugin_capability(plugin_key, capability);
|
||||
AppActionRunResult::Level level = AppActionRunResult::Level::Info;
|
||||
switch (outcome.level) {
|
||||
case ScriptRunOutcome::Level::Success: level = AppActionRunResult::Level::Success; break;
|
||||
case ScriptRunOutcome::Level::Error: level = AppActionRunResult::Level::Error; break;
|
||||
case ScriptRunOutcome::Level::Busy: level = AppActionRunResult::Level::Busy; break;
|
||||
case ScriptRunOutcome::Level::Info: break;
|
||||
}
|
||||
return {level, outcome.message};
|
||||
}
|
||||
};
|
||||
|
||||
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
|
||||
// enabled script capability.
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
|
||||
const std::string& source_name)
|
||||
{
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
if (!loader.is_plugin_loaded(plugin_key))
|
||||
return nullptr;
|
||||
auto loaded = loader.get_plugin_capability_by_name(plugin_key, PluginCapabilityType::Script, capability);
|
||||
if (!loaded || !loaded->enabled)
|
||||
return nullptr;
|
||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ActionRegistry::init()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
for (auto& source : m_sources)
|
||||
source->start(*this);
|
||||
assert(!m_started);
|
||||
m_started = true;
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
|
||||
auto on_source = [this](const std::string& plugin_key, ActionChange change) {
|
||||
if (!wxTheApp || wxGetApp().is_closing())
|
||||
return;
|
||||
wxGetApp().CallAfter([this, plugin_key, change] {
|
||||
if (!wxGetApp().is_closing())
|
||||
this->refresh_source(plugin_key, change);
|
||||
});
|
||||
};
|
||||
auto on_capability = [this](const PluginCapabilityIdentifier& capability, ActionChange change) {
|
||||
if (capability.type != PluginCapabilityType::Script || !wxTheApp || wxGetApp().is_closing())
|
||||
return;
|
||||
const std::string plugin_key = capability.plugin_key;
|
||||
const std::string name = capability.name;
|
||||
wxGetApp().CallAfter([this, plugin_key, name, change] {
|
||||
if (!wxGetApp().is_closing())
|
||||
this->refresh_capability(plugin_key, name, change);
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe before enumerating so a concurrent load cannot land between the initial
|
||||
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
|
||||
// id and the m_actions scan in refresh_source is idempotent.
|
||||
loader.subscribe_on_load_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
loader.subscribe_on_unload_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
loader.subscribe_on_capability_load_callback(
|
||||
[on_capability](const PluginCapabilityIdentifier& capability) {
|
||||
on_capability(capability, ActionChange::Added);
|
||||
});
|
||||
loader.subscribe_on_capability_unload_callback(
|
||||
[on_capability](const PluginCapabilityIdentifier& capability) {
|
||||
on_capability(capability, ActionChange::Removed);
|
||||
});
|
||||
|
||||
// enumerate current script capabilities
|
||||
std::unordered_map<std::string, std::string> source_names;
|
||||
for (const PluginDescriptor& desc : loader.get_all_loaded_plugin_descriptors())
|
||||
if (!desc.name.empty())
|
||||
source_names.emplace(desc.plugin_key, desc.name);
|
||||
|
||||
for (const auto& capability : loader.get_plugin_capabilities_by_type(PluginCapabilityType::Script)) {
|
||||
if (!capability || !capability->enabled)
|
||||
continue;
|
||||
auto it = source_names.find(capability->plugin_key);
|
||||
const std::string& source_name = it == source_names.end() ? capability->plugin_key : it->second;
|
||||
if (auto action = make_action(capability->plugin_key, capability->name, source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::add_source(std::unique_ptr<IActionSource> source)
|
||||
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
if (source)
|
||||
m_sources.push_back(std::move(source));
|
||||
|
||||
// Remove this source's current actions. Collect first - erasing from m_actions while
|
||||
// iterating invalidates the iterator. why: m_actions (not the loader) is the source of
|
||||
// truth, so this is correct even after the plugin has already unloaded.
|
||||
std::vector<std::string> stale;
|
||||
for (const auto& [id, action] : m_actions)
|
||||
if (action->source_key() == plugin_key)
|
||||
stale.push_back(id);
|
||||
for (const std::string& id : stale)
|
||||
remove(id);
|
||||
|
||||
if (change == ActionChange::Removed)
|
||||
return;
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
const std::string source_name = find_loaded_source_name(loader, plugin_key);
|
||||
for (const auto& capability : loader.get_plugin_capabilities_by_type(plugin_key, PluginCapabilityType::Script)) {
|
||||
if (!capability || !capability->enabled)
|
||||
continue;
|
||||
if (auto action = make_action(plugin_key, capability->name, source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
|
||||
ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
const std::string id = PluginScriptAction::id_for(plugin_key, capability);
|
||||
if (change == ActionChange::Removed) {
|
||||
remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
if (auto action = make_action(plugin_key, capability, find_loaded_source_name(loader, plugin_key)))
|
||||
upsert(std::move(action));
|
||||
else
|
||||
remove(id);
|
||||
}
|
||||
|
||||
void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
|
||||
@@ -211,8 +368,8 @@ nlohmann::json ActionRegistry::snapshot() const
|
||||
return sa > sb;
|
||||
if (a->title() != b->title())
|
||||
return a->title() < b->title();
|
||||
if (a->source() != b->source())
|
||||
return a->source() < b->source();
|
||||
if (a->source_name() != b->source_name())
|
||||
return a->source_name() < b->source_name();
|
||||
return a->id() < b->id();
|
||||
});
|
||||
|
||||
@@ -220,7 +377,7 @@ nlohmann::json ActionRegistry::snapshot() const
|
||||
for (const AppAction* a : sorted)
|
||||
actions.push_back({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"source", a->source()},
|
||||
{"source", a->source_name()},
|
||||
{"shortcut", ""}});
|
||||
// why: favourites is the ORDERED pin list - it must come from favourite_actions
|
||||
// as stored, not be re-derived from the frecency-sorted actions (that would
|
||||
|
||||
Reference in New Issue
Block a user