mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 09:50:59 +00:00
248 lines
12 KiB
C++
248 lines
12 KiB
C++
#pragma once
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <libslic3r/Config.hpp>
|
|
|
|
#include <wx/string.h>
|
|
#include <wx/thread.h>
|
|
|
|
#include <cassert>
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace Slic3r { namespace GUI {
|
|
|
|
// How a source's action set changed. Drives the registry's refresh handlers.
|
|
enum class ActionChange { Added, Removed };
|
|
|
|
// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
|
|
enum class AppActionKind { Plugin, Command };
|
|
|
|
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
|
|
// actions translate their runner-specific result into this generic shape.
|
|
struct AppActionRunResult
|
|
{
|
|
enum class Level { Success, Info, Error, Busy };
|
|
|
|
Level level = Level::Info;
|
|
wxString message; // empty = "nothing worth showing"
|
|
};
|
|
|
|
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
|
|
// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first
|
|
// argument (which would make calls like AppAction("orca_command", ...) ambiguous).
|
|
struct AppActionId
|
|
{
|
|
std::string id;
|
|
};
|
|
|
|
// A speed-dial action: identity + user-state seeded from config + how to run itself.
|
|
// Abstract base - the only virtual is run(); concrete subclasses know how to run
|
|
// and what their source is.
|
|
// note: named AppAction, not Action - Slic3r::GUI::Action is already taken by
|
|
// UnsavedChangesDialog's exit-action enum, and this header reaches most GUI TUs.
|
|
struct AppAction
|
|
{
|
|
const std::string& id() const { return m_id; }
|
|
const std::string& title() const { return m_title; }
|
|
const std::string& source_key() const { return m_source_key; } // stable source identity
|
|
const std::string& source_name() const { return m_source_name; } // source display name
|
|
|
|
// Builds the stable id "<prefix>:<title>:<source_key>". why: one place owns the
|
|
// format - both the base ctor and the one raw-key lookup (removing a capability
|
|
// without an action object) go through this; ids are never parsed back apart.
|
|
static std::string compose_id(std::string_view prefix, std::string_view title, std::string_view source_key)
|
|
{
|
|
std::string out;
|
|
out.reserve(prefix.size() + title.size() + source_key.size() + 2);
|
|
out.append(prefix).append(1, ':').append(title).append(1, ':').append(source_key);
|
|
return out;
|
|
}
|
|
|
|
// seeded from AppConfig for the snapshot / sort:
|
|
bool favourite = false;
|
|
int count = 0;
|
|
long long last = 0; // epoch seconds
|
|
|
|
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
|
|
// source name); Command sets a section label (e.g. "Commands", "Mode").
|
|
AppActionKind kind = AppActionKind::Plugin;
|
|
std::string group;
|
|
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
|
|
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
|
|
std::string input;
|
|
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
|
|
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
|
|
std::string icon;
|
|
// Settings mode required to edit this action (SettingActions only). The palette prompts before
|
|
// running an action whose mode is above the user's current mode. comSimple for everything else.
|
|
ConfigOptionMode required_mode = comSimple;
|
|
// Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip).
|
|
std::string tooltip;
|
|
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
|
|
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
|
|
std::string full_label;
|
|
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
|
|
std::string help_url;
|
|
|
|
virtual ~AppAction() = default;
|
|
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
|
// commands (e.g. a layer percentage); plugins ignore it.
|
|
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
|
|
|
|
protected:
|
|
// The definition is constructor-set and immutable. Refreshes replace an action
|
|
// instead of mutating identity after the registry has indexed it by id.
|
|
// why: source_key (not the display name) carries identity, so renaming the source's
|
|
// display name leaves the id - and its persisted stats/favourite - intact.
|
|
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
|
|
: m_id(compose_id(prefix, title, source_key))
|
|
, m_title(std::move(title))
|
|
, m_source_key(std::move(source_key))
|
|
, m_source_name(std::move(source_name))
|
|
{}
|
|
|
|
// Explicit-id ctor: for actions whose id must NOT be derived from the display title
|
|
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
|
|
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
|
|
: m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
|
|
{}
|
|
|
|
private:
|
|
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
|
|
std::string m_title; // display name
|
|
std::string m_source_key; // stable identity of the action's source (e.g. plugin_key)
|
|
std::string m_source_name; // display name of the action's source
|
|
};
|
|
|
|
// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native
|
|
// command catalog and the dynamically materialised setting/plate/recent actions so every built-in
|
|
// action re-keys together.
|
|
inline constexpr const char* kOrcaSourceKey = "orca";
|
|
inline constexpr const char* kOrcaSourceName = "OrcaSlicer";
|
|
|
|
// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch
|
|
// first. Developer settings are handled as a separate prompt by the Speed Dial.
|
|
inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode)
|
|
{
|
|
return setting_mode > current_mode;
|
|
}
|
|
|
|
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
|
|
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
|
|
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
|
|
|
|
// Self-contained sink and single owner of runnable actions for the app session.
|
|
//
|
|
// Workflow:
|
|
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
|
|
// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
|
|
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
|
|
// which upsert()/remove() actions. The registry keeps the only action list and
|
|
// restores persisted user state as actions arrive.
|
|
// 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of
|
|
// snapshot(), because their membership follows live state (the current configs, plate list, recents).
|
|
// 4. Consumers use by_id(), snapshot(), and run() without knowing the source.
|
|
//
|
|
// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
|
|
// dispatch, and materialises the dynamic families inline rather than behind a source interface.
|
|
class ActionRegistry
|
|
{
|
|
public:
|
|
~ActionRegistry();
|
|
|
|
// Subscribes to the plugin loader and enumerates its current actions. Call once
|
|
// on the UI thread after the plugin system is up; wires the initial list and live
|
|
// updates together.
|
|
void init();
|
|
|
|
// Rebuilds the built-in command actions in the current UI locale. The command catalog copies
|
|
// translated titles/groups at construction, so after a live language switch the stored titles
|
|
// are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so
|
|
// favourites/run history survive. UI thread only. No-op before init().
|
|
void relocalize_builtins();
|
|
|
|
// Takes ownership, seeds persisted state, then inserts the action or replaces
|
|
// the action with the same id. A null action is ignored.
|
|
void upsert(std::unique_ptr<AppAction> action);
|
|
|
|
// Removes the action with this id. Missing ids are a harmless no-op.
|
|
void remove(const std::string& id);
|
|
|
|
// Always-clean read surface. UI thread only.
|
|
const AppAction* by_id(const std::string& id) const;
|
|
|
|
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
|
|
static constexpr size_t kFavLimit = 10;
|
|
|
|
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
|
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
|
|
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
|
|
// caller can surface a "favourites are full" hint instead of silently dropping the pin.
|
|
bool set_favourite(const std::string& id, bool on);
|
|
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
|
|
|
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
|
|
// visible bar the palette renders.
|
|
std::vector<std::string> favourite_ids() const;
|
|
|
|
// Run-confirm gate, keyed by action id (per-action "don't ask again").
|
|
bool should_ask(const std::string& id) const;
|
|
void suppress_ask(const std::string& id);
|
|
|
|
// Footer expand/collapse preference. Global (applies to every action) and persisted; absent
|
|
// means expanded, so a fresh config picks the richer default with no migration.
|
|
bool tooltip_expanded() const;
|
|
void set_tooltip_expanded(bool expanded);
|
|
|
|
// Flat, frecency-sorted snapshot for the webview:
|
|
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
|
|
nlohmann::json snapshot();
|
|
|
|
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
|
|
// as [{id,title,icon},...], using the page's real label (not the compact-blanked button text).
|
|
// Live by construction - built-in tabs (Home/Prepare/Preview/Device/Project/Calibration) and
|
|
// plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a page appears/disappears with
|
|
// the notebook. Plugin tabs hidden in the overflow menu (many plugins) aren't separate pages and
|
|
// are not listed. Call on the UI thread; null-safe.
|
|
nlohmann::json tab_options() const;
|
|
|
|
private:
|
|
void seed_state(AppAction& a) const; // favourite/stats from config
|
|
AppAction* find(const std::string& id);
|
|
|
|
// Read the persisted stats blob + capped favourite list once for a materialisation pass.
|
|
void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const;
|
|
|
|
// (Re)materialise the current visible config settings as SettingActions from the live
|
|
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
|
|
// Called at the top of snapshot() so the palette always reflects the current configs.
|
|
void materialize_setting_actions();
|
|
|
|
// (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate
|
|
// directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no
|
|
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
|
|
void materialize_plate_actions();
|
|
|
|
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
|
|
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
|
|
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
|
|
void materialize_recent_project_actions();
|
|
|
|
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
|
|
// one plugin's whole action set; refresh_capability touches a single capability.
|
|
void refresh_source(const std::string& plugin_key, ActionChange change);
|
|
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
|
|
|
|
bool m_started = false; // init() runs exactly once; guards double-subscription
|
|
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
|
};
|
|
|
|
}} // namespace Slic3r::GUI
|