add status bar for plugin/capability status instead of message box

This commit is contained in:
SoftFever
2026-07-03 14:57:35 +08:00
parent e3c6ec309d
commit 6fd1176661
5 changed files with 208 additions and 49 deletions

View File

@@ -18,6 +18,9 @@
<body onLoad="OnInit()"> <body onLoad="OnInit()">
<div class="app"> <div class="app">
<div class="toolbar"> <div class="toolbar">
<button id="open_terminal" class="ButtonStyleRegular ButtonTypeChoice left-btn">
Open Terminal
</button>
<button id="refresh_btn" class="ButtonStyleRegular ButtonTypeChoice"> <button id="refresh_btn" class="ButtonStyleRegular ButtonTypeChoice">
Refresh Refresh
</button> </button>
@@ -27,10 +30,10 @@
<span class="explore-menu-icon" aria-hidden="true"></span> <span class="explore-menu-icon" aria-hidden="true"></span>
</button> </button>
<button id="explore_btn" class="ButtonStyleConfirm ButtonTypeChoice explore-main-btn" type="button"> <button id="explore_btn" class="ButtonStyleConfirm ButtonTypeChoice explore-main-btn" type="button">
Browse plugins Install plugin
</button> </button>
<div id="exploreMenu" class="explore-menu" role="menu" hidden> <div id="exploreMenu" class="explore-menu" role="menu" hidden>
<button type="button" class="explore-menu-item" role="menuitem" data-install-action="explore">Browse plugins</button> <button type="button" class="explore-menu-item" role="menuitem" data-install-action="explore">Install plugin</button>
<button type="button" class="explore-menu-item" role="menuitem" data-install-action="install-local">Install local plugin</button> <button type="button" class="explore-menu-item" role="menuitem" data-install-action="install-local">Install local plugin</button>
</div> </div>
</div> </div>
@@ -129,11 +132,9 @@
</main> </main>
</div> </div>
<div id="AcceptArea"> <div id="statusBar" class="status-bar is-empty" role="status" aria-live="polite">
<button id="open_terminal" class="ButtonStyleRegular ButtonTypeChoice left-btn"> <span class="status-dot" aria-hidden="true"></span>
Open Terminal <span id="statusText" class="status-text"></span>
</button>
<div id="close_btn" class="ButtonStyleRegular ButtonTypeChoice">Close</div>
</div> </div>
<div id="ctxMenu" class="ctx" hidden></div> <div id="ctxMenu" class="ctx" hidden></div>

View File

@@ -1,7 +1,7 @@
const pluginsById = new Map(); const pluginsById = new Map();
const pluginInstallActions = { const pluginInstallActions = {
"explore": { "explore": {
label: "Browse plugins", label: "Install plugin",
command: "open_plugin_hub" command: "open_plugin_hub"
}, },
"install-local": { "install-local": {
@@ -40,7 +40,6 @@ function OnInit() {
exploreMenu?.addEventListener("click", OnExploreMenuClick); exploreMenu?.addEventListener("click", OnExploreMenuClick);
document.getElementById("open_terminal")?.addEventListener("click", () => SendMessage("open_terminal")); document.getElementById("open_terminal")?.addEventListener("click", () => SendMessage("open_terminal"));
document.getElementById("detailUpdateBtn")?.addEventListener("click", UpdateSelectedPlugin); document.getElementById("detailUpdateBtn")?.addEventListener("click", UpdateSelectedPlugin);
document.getElementById("close_btn")?.addEventListener("click", () => SendMessage("close_page"));
document.querySelectorAll("[role='tab']").forEach((tab) => { document.querySelectorAll("[role='tab']").forEach((tab) => {
tab.addEventListener("click", () => ActivateDetailTab(String(tab.dataset.tab || ""))); tab.addEventListener("click", () => ActivateDetailTab(String(tab.dataset.tab || "")));
@@ -216,9 +215,26 @@ function HandleStudio(value) {
if (payload.command === "list_plugins") { if (payload.command === "list_plugins") {
SetSelectedInstallAction(payload.install_action, false); SetSelectedInstallAction(payload.install_action, false);
ApplyPlugins(payload.data || []); ApplyPlugins(payload.data || []);
} else if (payload.command === "status_message") {
ShowStatusMessage(String(payload.message || ""), String(payload.level || "info"));
} }
} }
// Renders the latest plugin/capability operation result in the footer status bar. The result
// persists until the next operation replaces it; the native side already localizes the text.
function ShowStatusMessage(message, level) {
const bar = document.getElementById("statusBar");
const text = document.getElementById("statusText");
if (!bar || !text)
return;
const normalizedLevel = ["success", "warn", "error", "info"].includes(level) ? level : "info";
text.textContent = message;
text.title = message;
bar.classList.remove("is-empty", "level-success", "level-warn", "level-error", "level-info");
bar.classList.add(`level-${normalizedLevel}`);
}
function SafeJsonParse(value) { function SafeJsonParse(value) {
try { try {
return JSON.parse(value); return JSON.parse(value);

View File

@@ -65,9 +65,22 @@ body {
gap: 8px; gap: 8px;
} }
/* Compact toolbar buttons */
.toolbar .ButtonTypeChoice {
height: 26px !important;
line-height: 25px !important;
font-size: 13px !important;
}
/* Only the plain buttons (Open Terminal, Refresh) tighten horizontal padding; the explore dropdown
buttons (grandchildren) keep their own padding/width. */
.toolbar > .ButtonTypeChoice {
padding: 0 10px !important;
}
.explore-dropdown { .explore-dropdown {
--explore-arrow-width: 26px; --explore-arrow-width: 22px;
--explore-action-width: 160px; --explore-action-width: 140px;
position: relative; position: relative;
display: inline-flex; display: inline-flex;
align-items: stretch; align-items: stretch;
@@ -652,8 +665,68 @@ body {
} }
} }
#AcceptArea { /* Footer status bar: single-line, fixed-height strip that shows the latest script run result.
Reuses the shared status color tokens so it themes (light/dark) like the plugin status chips. */
.status-bar {
flex: 0 0 auto; flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
min-height: 28px;
padding: 6px 14px;
box-sizing: border-box;
border-top: 1px solid var(--border);
background: var(--panel);
color: var(--text);
font-size: 13px;
}
.status-dot {
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.status-text {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Idle (no run yet): keep the strip height stable but hide the indicator dot. */
.status-bar.is-empty .status-dot {
visibility: hidden;
}
.status-bar.level-success .status-dot {
background: var(--plugin-status-ok);
}
.status-bar.level-success .status-text {
color: var(--plugin-status-ok);
}
.status-bar.level-error .status-dot {
background: var(--plugin-status-danger);
}
.status-bar.level-error .status-text {
color: var(--plugin-status-danger);
}
.status-bar.level-warn .status-dot {
background: var(--plugin-status-warn);
}
.status-bar.level-warn .status-text {
color: var(--plugin-status-warn);
}
.status-bar.level-info .status-dot {
background: var(--muted);
} }
.ctx { .ctx {

View File

@@ -421,6 +421,28 @@ void PluginsDialog::update_plugin_dialog_ui()
// Called after the shared catalog is already updated, for example from the // Called after the shared catalog is already updated, for example from the
// cloud-plugin state callback. Do not fetch here or the callback can re-enter. // cloud-plugin state callback. Do not fetch here or the callback can re-enter.
send_plugins(); send_plugins();
resolve_pending_activation();
}
void PluginsDialog::resolve_pending_activation()
{
if (m_activating_plugin_key.empty())
return;
PluginLoader& loader = PluginManager::instance().get_loader();
if (loader.is_plugin_load_in_progress(m_activating_plugin_key))
return; // Still loading: keep the "Activating..." message until the load resolves.
const std::string plugin_key = m_activating_plugin_key;
m_activating_plugin_key.clear();
// Mirror the row's status precedence (Error before Activated) so the message matches the list.
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
show_status(wxString::Format(_L("Failed to activate \"%s\"."), plugin_display_name(plugin_key)), "error");
else if (loader.is_plugin_loaded(plugin_key))
show_status(wxString::Format(_L("Activated \"%s\"."), plugin_display_name(plugin_key)), "success");
// Otherwise it ended up inactive (toggled off or cancelled mid-load): stay silent.
} }
void PluginsDialog::on_script_message(const nlohmann::json& payload) void PluginsDialog::on_script_message(const nlohmann::json& payload)
@@ -607,13 +629,17 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
if (!enabled) { if (!enabled) {
if (!manager.get_loader().unload_plugin(plugin_key)) { if (!manager.get_loader().unload_plugin(plugin_key)) {
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key; BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key;
wxMessageBox(_L("Failed to unload plugin."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Failed to unload plugin."), "warn");
send_plugins(); send_plugins();
return; return;
} }
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
if (m_activating_plugin_key == plugin_key)
m_activating_plugin_key.clear();
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Deactivated \"%s\"."), plugin_display_name(plugin_key)), "success");
return; return;
} }
@@ -624,7 +650,7 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) { if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
const std::string install_error = "Unauthorized cloud plugins cannot be installed."; const std::string install_error = "Unauthorized cloud plugins cannot be installed.";
manager.set_plugin_error(plugin_key, install_error); manager.set_plugin_error(plugin_key, install_error);
wxMessageBox(from_u8(install_error), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(from_u8(install_error), "warn");
} }
send_plugins(); send_plugins();
return; return;
@@ -658,7 +684,7 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
const PluginDescriptor* descriptor_ptr = manager.get_catalog().find_valid_plugin_descriptor(plugin_key); const PluginDescriptor* descriptor_ptr = manager.get_catalog().find_valid_plugin_descriptor(plugin_key);
if (!descriptor_ptr) { if (!descriptor_ptr) {
wxMessageBox(_L("Plugin manifest was not found."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Plugin manifest was not found."), "warn");
send_plugins(); send_plugins();
return; return;
} }
@@ -671,6 +697,10 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key;
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key); manager.get_loader().load_plugin(manager.get_catalog(), plugin_key);
send_plugins(); send_plugins();
// Loading is asynchronous: show a pending message now; resolve_pending_activation() turns it into
// Activated/Failed once the loader's completion callback runs update_plugin_dialog_ui().
m_activating_plugin_key = plugin_key;
show_status(wxString::Format(_L("Activating \"%s\"..."), plugin_display_name(plugin_key)), "info");
} }
void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type, void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type,
@@ -705,6 +735,7 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, Plug
} }
send_plugins(); send_plugins();
show_status(wxString::Format(enabled ? _L("Enabled \"%s\".") : _L("Disabled \"%s\"."), from_u8(capability_name)), "success");
} }
void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, const std::string& action) void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, const std::string& action)
@@ -761,7 +792,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
std::transform(extension.begin(), extension.end(), extension.begin(), std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") { if (extension != ".py" && extension != ".whl") {
wxMessageBox(_L("Select a .py or .whl plugin package."), _L("Plugins"), wxOK | wxICON_INFORMATION, this); show_status(_L("Select a .py or .whl plugin package."), "info");
return false; return false;
} }
@@ -769,7 +800,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
bool existing_installation = false; bool existing_installation = false;
auto report_inspection_failure = [&]() { auto report_inspection_failure = [&]() {
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error; BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
wxMessageBox(_L("Failed to install plugin package. See the log for details."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Failed to install plugin package. See the log for details."), "warn");
send_plugins(); send_plugins();
return false; return false;
}; };
@@ -814,12 +845,14 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
if (!installed) { if (!installed) {
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error; BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
wxMessageBox(_L("Failed to install plugin package. See the log for details."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Failed to install plugin package. See the log for details."), "warn");
send_plugins(); send_plugins();
return false; return false;
} }
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path; BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudCatalog); refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudCatalog);
return true; return true;
} }
@@ -849,7 +882,7 @@ bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const st
if (!downloaded) { if (!downloaded) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin download failed. plugin_key=" << plugin_key << " error=" << error; BOOST_LOG_TRIVIAL(error) << "Cloud plugin download failed. plugin_key=" << plugin_key << " error=" << error;
PluginManager::instance().set_plugin_error(plugin_key, error); PluginManager::instance().set_plugin_error(plugin_key, error);
wxMessageBox(from_u8(error), _L("Plugin Download Failed"), wxOK | wxICON_ERROR, this); show_status(error.empty() ? _L("Plugin download failed.") : from_u8(error), "error");
return false; return false;
} }
@@ -857,6 +890,23 @@ bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const st
return true; return true;
} }
void PluginsDialog::show_status(const wxString& message, const char* level)
{
nlohmann::json payload;
payload["command"] = "status_message";
payload["level"] = level;
payload["message"] = into_u8(message);
call_web_handler(payload);
}
wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
{
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && !descriptor.name.empty())
return from_u8(descriptor.name);
return from_u8(plugin_key);
}
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name) void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
{ {
if (plugin_key.empty() || capability_name.empty()) { if (plugin_key.empty() || capability_name.empty()) {
@@ -893,8 +943,7 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
BOOST_LOG_TRIVIAL(info) << "Run script plugin requested from Plugins dialog. plugin_key=" << plugin_key BOOST_LOG_TRIVIAL(info) << "Run script plugin requested from Plugins dialog. plugin_key=" << plugin_key
<< " capability_name=" << capability_name; << " capability_name=" << capability_name;
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& dialog_message, auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
const wxString& dialog_title, long dialog_style) {
const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error; const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error)) if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error))
BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key; BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
@@ -904,19 +953,21 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
send_plugins(); send_plugins();
const wxString message = dialog_message.empty() ? from_u8(normalized_error) : dialog_message; // The row now shows an "Error" status and the Diagnostics tab holds the full text, so surface
wxMessageBox(message, dialog_title, dialog_style, this); // the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
const wxString message = status_message.empty() ? from_u8(normalized_error) : status_message;
show_status(message, "error");
}; };
PluginDescriptor descriptor; PluginDescriptor descriptor;
if (!get_descriptor(plugin_key, descriptor)) { if (!get_descriptor(plugin_key, descriptor)) {
BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key; BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key;
complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."), _L("Plugins"), wxOK | wxICON_WARNING); complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."));
return; return;
} }
if (descriptor.has_error()) { if (descriptor.has_error()) {
complete_with_error(descriptor.normalized_error(), wxString(), _L("Plugins"), wxOK | wxICON_WARNING); complete_with_error(descriptor.normalized_error(), wxString());
return; return;
} }
@@ -929,8 +980,7 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key
<< " is_metadata_valid=" << descriptor.is_metadata_valid() << " is_metadata_valid=" << descriptor.is_metadata_valid()
<< " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type()); << " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type());
complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."), _L("Plugins"), complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."));
wxOK | wxICON_WARNING);
return; return;
} }
@@ -938,7 +988,7 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
if (!manager.get_loader().is_plugin_loaded(plugin_key)) { if (!manager.get_loader().is_plugin_loaded(plugin_key)) {
BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key; BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key;
complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.", complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.",
_L("Load the script plugin before running it."), _L("Plugins"), wxOK | wxICON_INFORMATION); _L("Load the script plugin before running it."));
return; return;
} }
@@ -946,7 +996,7 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
if (!plugin) { if (!plugin) {
BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key; BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key;
complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.", complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.",
_L("The selected plugin is not a runnable script plugin."), _L("Plugins"), wxOK | wxICON_WARNING); _L("The selected plugin is not a runnable script plugin."));
return; return;
} }
@@ -977,29 +1027,29 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
if (!error.empty()) { if (!error.empty()) {
plugin.reset(); plugin.reset();
cap.reset(); cap.reset();
complete_with_error(error, wxString(), _L("Script Plugin Failed"), wxOK | wxICON_ERROR); complete_with_error(error, wxString());
return; return;
} }
BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
<< " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data; << " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data;
const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError; const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError;
const std::string fallback_message = failed ? "Script plugin failed." :
result.status == PluginResult::Skipped ? "Script plugin skipped." :
"Script plugin finished.";
if (failed) { if (failed) {
plugin.reset(); plugin.reset();
cap.reset(); cap.reset();
complete_with_error(result.message.empty() ? fallback_message : result.message, wxString(), _L("Script Plugin"), // complete_with_error normalizes an empty message to "Script plugin failed." and reports via the status bar.
wxOK | wxICON_ERROR); complete_with_error(result.message, wxString());
return; return;
} }
manager.clear_plugin_error(plugin_key); manager.clear_plugin_error(plugin_key);
send_plugins(); send_plugins();
const wxString message = from_u8(result.message.empty() ? fallback_message : result.message);
wxMessageBox(message, _L("Script Plugin"), wxOK | wxICON_INFORMATION, this); const bool skipped = result.status == PluginResult::Skipped;
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
const wxString message = result.message.empty() ? fallback : from_u8(result.message);
show_status(message, skipped ? "info" : "success");
} }
void PluginsDialog::update_plugin(const std::string& plugin_key) void PluginsDialog::update_plugin(const std::string& plugin_key)
@@ -1029,7 +1079,7 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
if (!updated) { if (!updated) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin update failed. plugin_key=" << plugin_key << " error=" << error; BOOST_LOG_TRIVIAL(error) << "Cloud plugin update failed. plugin_key=" << plugin_key << " error=" << error;
wxMessageBox(from_u8(error), _L("Failed to update plugin"), wxOK | wxICON_ERROR, this); show_status(error.empty() ? _L("Failed to update plugin.") : from_u8(error), "error");
send_plugins(); send_plugins();
return; return;
} }
@@ -1037,6 +1087,7 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
// update_cloud_plugin installs the package and updates the in-memory descriptor // update_cloud_plugin installs the package and updates the in-memory descriptor
// (installed=true, update_available=false) on success. // (installed=true, update_available=false) on success.
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Updated \"%s\"."), name), "success");
} }
void PluginsDialog::open_plugin_folder(const PluginDescriptor& plugin) void PluginsDialog::open_plugin_folder(const PluginDescriptor& plugin)
@@ -1044,7 +1095,7 @@ void PluginsDialog::open_plugin_folder(const PluginDescriptor& plugin)
const boost::filesystem::path plugin_folder = resolve_plugin_root_from_descriptor(plugin); const boost::filesystem::path plugin_folder = resolve_plugin_root_from_descriptor(plugin);
if (plugin_folder.empty()) { if (plugin_folder.empty()) {
wxMessageBox(_L("Plugin folder could not be determined."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Plugin folder could not be determined."), "warn");
return; return;
} }
@@ -1101,14 +1152,15 @@ void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
refresh_plugin_catalog_blocking(kFetchCloudCatalog); refresh_plugin_catalog_blocking(kFetchCloudCatalog);
store_plugin_operation_result(state, succeeded, std::move(error)); store_plugin_operation_result(state, succeeded, std::move(error));
}, },
[this, state]() { [this, state, plugin_name]() {
std::string error; std::string error;
if (!take_plugin_operation_result(state, error)) { if (!take_plugin_operation_result(state, error)) {
wxMessageBox(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), _L("Plugins"), wxOK | wxICON_ERROR, this); show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
return; return;
} }
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
}, },
_L("Deleting plugin"), _L("Deleting plugin...")); _L("Deleting plugin"), _L("Deleting plugin..."));
} }
@@ -1130,14 +1182,15 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
const bool succeeded = PluginManager::instance().delete_and_unsubscribe_cloud_plugin(plugin_key, error); const bool succeeded = PluginManager::instance().delete_and_unsubscribe_cloud_plugin(plugin_key, error);
store_plugin_operation_result(state, succeeded, std::move(error)); store_plugin_operation_result(state, succeeded, std::move(error));
}, },
[this, state]() { [this, state, plugin_name]() {
std::string error; std::string error;
if (!take_plugin_operation_result(state, error)) { if (!take_plugin_operation_result(state, error)) {
wxMessageBox(error.empty() ? _L("Failed to unsubscribe plugin.") : from_u8(error), _L("Plugins"), wxOK | wxICON_ERROR, this); show_status(error.empty() ? _L("Failed to unsubscribe plugin.") : from_u8(error), "error");
return; return;
} }
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Unsubscribed \"%s\"."), plugin_name), "success");
}, },
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin...")); _L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
} }
@@ -1150,7 +1203,7 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
PluginManager& manager = PluginManager::instance(); PluginManager& manager = PluginManager::instance();
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key); const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
if (!manager.get_loader().unload_plugin(plugin_key)) { if (!manager.get_loader().unload_plugin(plugin_key)) {
wxMessageBox(_L("Failed to unload plugin."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Failed to unload plugin."), "warn");
send_plugins(); send_plugins();
return; return;
} }
@@ -1158,10 +1211,13 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key, false); manager.get_loader().load_plugin(manager.get_catalog(), plugin_key, false);
if (!was_loaded && !manager.get_loader().unload_plugin(plugin_key)) { if (!was_loaded && !manager.get_loader().unload_plugin(plugin_key)) {
wxMessageBox(_L("Plugin reloaded, but failed to restore the inactive state."), _L("Plugins"), wxOK | wxICON_WARNING, this); show_status(_L("Plugin reloaded, but failed to restore the inactive state."), "warn");
send_plugins();
return;
} }
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
} }
void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin) void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
@@ -1176,7 +1232,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
std::string error; std::string error;
if (plugin.has_local_package()) { if (plugin.has_local_package()) {
if (!manager.delete_plugin(plugin_key, error)) { if (!manager.delete_plugin(plugin_key, error)) {
wxMessageBox(from_u8(error.empty() ? "Failed to delete plugin." : error), _L("Plugins"), wxOK | wxICON_ERROR, this); show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
send_plugins(); send_plugins();
return; return;
} }
@@ -1194,6 +1250,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
} }
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
} }
void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin_key) void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin_key)
@@ -1211,8 +1268,7 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
std::string error; std::string error;
if (!PluginManager::instance().delete_mine_local_and_cloud_plugin(plugin_key, error)) { if (!PluginManager::instance().delete_mine_local_and_cloud_plugin(plugin_key, error)) {
wxMessageBox(error.empty() ? _L("Failed to delete plugin from local and cloud.") : from_u8(error), _L("Plugins"), show_status(error.empty() ? _L("Failed to delete plugin from local and cloud.") : from_u8(error), "error");
wxOK | wxICON_ERROR, this);
return; return;
} }
@@ -1221,6 +1277,7 @@ void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin
// re-syncs the cloud list itself), so a UI refresh is sufficient here — an extra // re-syncs the cloud list itself), so a UI refresh is sufficient here — an extra
// clearing rescan + cloud fetch would be redundant. // clearing rescan + cloud fetch would be redundant.
send_plugins(); send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
} }
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI

View File

@@ -78,6 +78,14 @@ private:
bool install_plugin_package(const std::string& package_path); bool install_plugin_package(const std::string& package_path);
bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name); bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name);
void run_script_plugin(const std::string& plugin_key, const std::string& capability_name); void run_script_plugin(const std::string& plugin_key, const std::string& capability_name);
// Pushes a one-line result into the web footer status bar (level: "success" | "warn" | "error" | "info"),
// used for every plugin/capability operation instead of a modal box so the dialog stays non-disruptive.
void show_status(const wxString& message, const char* level);
// Best-effort human-readable name for a plugin_key (falls back to the key itself).
wxString plugin_display_name(const std::string& plugin_key) const;
// Turns the pending "Activating..." status into "Activated"/"Failed to activate" once an
// asynchronous plugin load reported via update_plugin_dialog_ui() finishes. No-op otherwise.
void resolve_pending_activation();
void update_plugin(const std::string& plugin_key); void update_plugin(const std::string& plugin_key);
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin); void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
@@ -218,6 +226,10 @@ private:
// (message/show_dialog) or the result message box pumps a nested event loop, which could // (message/show_dialog) or the result message box pumps a nested event loop, which could
// re-dispatch the web "run_script_plugin" command and start a second, overlapping run. // re-dispatch the web "run_script_plugin" command and start a second, overlapping run.
bool m_script_running = false; bool m_script_running = false;
// Plugin whose asynchronous activation is in flight, awaited by resolve_pending_activation().
// Empty when no activation is pending.
std::string m_activating_plugin_key;
}; };
} // namespace GUI } // namespace GUI