mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
Initial Commit of speed dial improvement
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -22,20 +22,92 @@ assert.equal(
|
||||
"duplicate labels should use the opaque id without interpreting its contents"
|
||||
);
|
||||
|
||||
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps the action list hidden");
|
||||
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps the action list hidden");
|
||||
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list");
|
||||
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps recent/empty list");
|
||||
assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions");
|
||||
|
||||
// commandList: an empty query shows recents; a typed query filters all actions.
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [],
|
||||
"empty query + no recents shows nothing");
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""),
|
||||
[duplicateActions[0]],
|
||||
"empty query shows the recent list");
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions,
|
||||
"a typed query filters actions (both identical titles match) instead of showing recents");
|
||||
|
||||
// filterTabs (tab phase): an empty query keeps the whole list; a typed query filters by title/id.
|
||||
const tabOptions = [
|
||||
{ id: "home", title: "Home" },
|
||||
{ id: "prepare", title: "Prepare" },
|
||||
{ id: "monitor", title: "Device" },
|
||||
{ id: "project", title: "Project" }
|
||||
];
|
||||
assert.deepEqual(ctx.filterTabs(tabOptions, ""), tabOptions,
|
||||
"empty query keeps the whole tab list");
|
||||
assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1,
|
||||
"a typed query filters tabs by title");
|
||||
assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1,
|
||||
"a typed query matches a tab title");
|
||||
assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [],
|
||||
"a typed query with no match returns an empty list");
|
||||
|
||||
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
|
||||
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
|
||||
"an empty title falls back to the title-cased id");
|
||||
assert.equal(ctx.tabTitle({ id: "home" }), "Home",
|
||||
"a missing title falls back to the title-cased id");
|
||||
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
|
||||
"a populated title is kept as-is");
|
||||
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
|
||||
"a leading space from the Notebook button label is trimmed so the icon letter shows");
|
||||
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
|
||||
"an untitled tab still matches a typed query via the id/title fallback");
|
||||
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
|
||||
"a leading-space tab title still matches a typed query");
|
||||
|
||||
// settingCode: a setting tile uses the leaf option-name initial (label's last " : "-segment),
|
||||
// escalated by category, then a stable ordinal - so settings aren't all a generic "S".
|
||||
const settings = [
|
||||
{ opt_key: "layer_height", label: "Quality : Layer Height", category: "Quality", type: 0 },
|
||||
{ opt_key: "smooth", label: "Quality : Smooth", category: "Quality", type: 0 }
|
||||
];
|
||||
assert.equal(ctx.leafLabel({ label: "Quality : Layer Height", opt_key: "x" }), "Layer Height",
|
||||
"leafLabel takes the last segment of the label");
|
||||
assert.equal(ctx.settingCode(settings[0], settings), "L",
|
||||
"a unique leaf initial yields a single letter");
|
||||
// renderSettingRow iterates settingsResults, so the coded item is always a member of the list.
|
||||
const infillQ = { opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 };
|
||||
const infillS = { opt_key: "b", label: "Supports : Infill", category: "Supports", type: 0 };
|
||||
const sameLeaf = [infillQ, infillS];
|
||||
assert.equal(ctx.settingCode(infillQ, sameLeaf), "QI",
|
||||
"a colliding leaf initial escalates to category + initial");
|
||||
assert.equal(ctx.settingCode(infillS, sameLeaf), "SI",
|
||||
"a different category disambiguates the same leaf initial");
|
||||
const dupQ = [
|
||||
{ opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 },
|
||||
{ opt_key: "b", label: "Quality : Infill", category: "Quality", type: 0 }
|
||||
];
|
||||
assert.equal(ctx.settingCode(dupQ[0], dupQ), "QI1",
|
||||
"both title and category collide -> stable ordinal by opt_key");
|
||||
assert.equal(ctx.settingCode(dupQ[1], dupQ), "QI2",
|
||||
"the ordinal advances by opt_key order");
|
||||
|
||||
// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise).
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], ""),
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""),
|
||||
null,
|
||||
"Enter with an empty query must not resolve to an action the blank list never showed"
|
||||
"Enter with an empty query and no recents must not resolve to an action the list never showed"
|
||||
);
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], "rep"),
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"),
|
||||
"0123456789abcdef",
|
||||
"a typed query resolves the list selection"
|
||||
);
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""),
|
||||
"0123456789abcdef",
|
||||
"Enter with an empty query resolves the recent entry"
|
||||
);
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""),
|
||||
"fedcba9876543210",
|
||||
|
||||
@@ -55,6 +55,28 @@ body {
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
|
||||
/* Hover-revealed remove button in the tile's corner; sits inside the tile bounds so overflow:hidden clips it. */
|
||||
.fav-tile { position: relative; }
|
||||
.fav-unpin {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
background: rgba(127,127,127,.35);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
.fav-tile:hover .fav-unpin,
|
||||
.fav-tile.sel .fav-unpin { opacity: 1; }
|
||||
.fav-unpin:hover { background: rgba(127,127,127,.6); }
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@@ -127,6 +149,15 @@ body {
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
}
|
||||
.dial-count[hidden] { display: none; }
|
||||
/* Section header inside the list (e.g. the "Recent" row above the recent entries). */
|
||||
.dial-group {
|
||||
padding: 8px 8px 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
}
|
||||
.dial-list {
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
#include "ActionRegistry.hpp"
|
||||
|
||||
#include "GCodeViewer.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "IMSlider.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "Notebook.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "Search.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
@@ -28,14 +42,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
|
||||
}
|
||||
|
||||
nlohmann::json read_section(const char* key, nlohmann::json fallback)
|
||||
{
|
||||
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
|
||||
}
|
||||
{ return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
|
||||
|
||||
void write_section(const char* key, const nlohmann::json& j)
|
||||
{
|
||||
wxGetApp().app_config->set(kConfigSection, key, j.dump());
|
||||
}
|
||||
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
|
||||
|
||||
std::vector<std::string> read_string_array(const char* key)
|
||||
{
|
||||
@@ -53,7 +62,7 @@ double frecency_score(int count, long long last, long long now)
|
||||
if (count <= 0)
|
||||
return 0.0;
|
||||
constexpr double HALF_LIFE_DAYS = 30.0;
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
|
||||
}
|
||||
|
||||
@@ -79,26 +88,25 @@ struct PluginScriptAction : AppAction
|
||||
// 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);
|
||||
}
|
||||
{ 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))
|
||||
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
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
std::string error;
|
||||
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
||||
if (!error.empty())
|
||||
return {AppActionRunResult::Level::Error, from_u8(error)};
|
||||
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
|
||||
result.message.empty() ? fallback : from_u8(result.message)};
|
||||
@@ -107,8 +115,7 @@ struct PluginScriptAction : AppAction
|
||||
|
||||
// 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)
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (!manager.is_plugin_loaded(plugin_key))
|
||||
@@ -119,8 +126,167 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
|
||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||
}
|
||||
|
||||
// ---- built-in command actions (the speed dial "commands" section) ------
|
||||
|
||||
constexpr const char* kCommandPrefix = "orca_command";
|
||||
constexpr const char* kOrcaSourceKey = "orca";
|
||||
constexpr const char* kOrcaSourceName = "OrcaSlicer";
|
||||
|
||||
// Jump the preview to a layer selected by a 0-100 percent of the layer range. Best-effort:
|
||||
// switches to the preview tab and requests a slice (select_view_3D("Preview", false)); if the
|
||||
// slicer result is already present the slider is repositioned immediately, otherwise the user
|
||||
// can re-run after slicing.
|
||||
void go_to_layer(Plater* plater, const std::string& param)
|
||||
{
|
||||
if (!plater)
|
||||
return;
|
||||
double pct = 50.0;
|
||||
try {
|
||||
pct = std::stod(param);
|
||||
} catch (const std::exception&) {}
|
||||
pct = std::clamp(pct, 0.0, 100.0);
|
||||
|
||||
GLCanvas3D* canvas = plater->get_current_canvas3D();
|
||||
if (!canvas)
|
||||
return;
|
||||
GCodeViewer& viewer = canvas->get_gcode_viewer();
|
||||
IMSlider* layers = viewer.get_layers_slider();
|
||||
IMSlider* moves = viewer.get_moves_slider();
|
||||
if (!layers || layers->GetMaxValue() <= 0)
|
||||
return; // no slice result yet - the slice request above will populate it
|
||||
|
||||
const double max = double(layers->GetMaxValue());
|
||||
const int target = int(std::lround(pct / 100.0 * max));
|
||||
layers->SetHigherValue(target);
|
||||
// In "one layer" mode the lower handle follows the higher one (mirrors arrow-key nav).
|
||||
if (layers->is_one_layer())
|
||||
layers->SetLowerValue(target);
|
||||
layers->set_as_dirty();
|
||||
if (moves) {
|
||||
moves->SetHigherValue(moves->GetMaxValue());
|
||||
moves->set_as_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch a built-in command. The CommandAction stays a thin value; the actual GUI work
|
||||
// lives here so it can touch the live app state.
|
||||
AppActionRunResult run_native_command(const std::string& command_key, const std::string& param)
|
||||
{
|
||||
GUI_App& app = wxGetApp();
|
||||
if (app.is_closing())
|
||||
return {};
|
||||
|
||||
Plater* plater = app.plater();
|
||||
|
||||
if (command_key == "save_project") {
|
||||
if (plater)
|
||||
plater->save_project(false);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "save_project_as") {
|
||||
if (plater)
|
||||
plater->save_project(true);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "load_project") {
|
||||
if (plater)
|
||||
plater->load_project();
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "open_preferences") {
|
||||
app.open_preferences();
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "mode_simple" || command_key == "mode_advanced" || command_key == "mode_expert") {
|
||||
const int mode = command_key == "mode_simple" ? comSimple : command_key == "mode_advanced" ? comAdvanced : comExpert;
|
||||
app.save_mode(mode);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "slice_and_preview") {
|
||||
if (plater) {
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (app.mainframe)
|
||||
app.mainframe->select_tab(TAB_ID_PREVIEW);
|
||||
}
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
if (command_key == "go_to_layer") {
|
||||
if (plater) {
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (app.mainframe)
|
||||
app.mainframe->select_tab(TAB_ID_PREVIEW);
|
||||
go_to_layer(plater, param);
|
||||
}
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
// "go_to_setting"/"go_to_tab" are two-phase: the palette collects the option after
|
||||
// activating it, so dispatch here is a no-op (the actual jump goes through the web command).
|
||||
if (command_key == "go_to_setting" || command_key == "go_to_tab")
|
||||
return {AppActionRunResult::Level::Success};
|
||||
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
|
||||
}
|
||||
|
||||
// A built-in command action. source_key is the constant "orca" so a renamed title never
|
||||
// re-keys the action (matches the plugin source-key contract).
|
||||
struct CommandAction : AppAction
|
||||
{
|
||||
std::string command_key;
|
||||
|
||||
CommandAction(std::string command_key, std::string title, std::string group, std::string input = "")
|
||||
: AppAction(kCommandPrefix, std::move(title), kOrcaSourceKey, kOrcaSourceName), command_key(std::move(command_key))
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = std::move(group);
|
||||
this->input = std::move(input);
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& param) const override { return run_native_command(command_key, param); }
|
||||
};
|
||||
|
||||
std::unique_ptr<AppAction> make_command(std::string key, std::string title, std::string group, std::string input = "")
|
||||
{ return std::make_unique<CommandAction>(std::move(key), std::move(title), std::move(group), std::move(input)); }
|
||||
|
||||
// The built-in palette commands, registered once at init().
|
||||
std::vector<std::unique_ptr<AppAction>> native_commands()
|
||||
{
|
||||
std::vector<std::unique_ptr<AppAction>> out;
|
||||
// why: _u8L (std::string) for titles/groups - make_command takes std::string; _L would
|
||||
// return a wxString and silently fail to convert here.
|
||||
out.push_back(make_command("slice_and_preview", _u8L("Slice and Preview"), _u8L("Commands")));
|
||||
// Two-phase commands: activating them collects input in the palette, then runs.
|
||||
out.push_back(make_command("go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "percent"));
|
||||
// The "…" is avoided in the msgid: use ASCII "..." to keep the .pot extraction simple.
|
||||
out.push_back(make_command("go_to_setting", _u8L("Go to setting..."), _u8L("Commands"), "settings"));
|
||||
out.push_back(make_command("go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), "tab"));
|
||||
out.push_back(make_command("load_project", _u8L("Load Project"), _u8L("Commands")));
|
||||
out.push_back(make_command("save_project", _u8L("Save Project"), _u8L("Commands")));
|
||||
out.push_back(make_command("save_project_as", _u8L("Save Project As"), _u8L("Commands")));
|
||||
out.push_back(make_command("open_preferences", _u8L("Preferences"), _u8L("Commands")));
|
||||
out.push_back(make_command("mode_simple", _u8L("Mode: Simple"), _u8L("Mode")));
|
||||
out.push_back(make_command("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode")));
|
||||
out.push_back(make_command("mode_expert", _u8L("Mode: Expert"), _u8L("Mode")));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Replicates Sidebar's get_search_inputs(): the configs of every tab supporting the current
|
||||
// printer technology, in the current UI mode.
|
||||
std::vector<Search::InputInfo> settings_inputs()
|
||||
{
|
||||
std::vector<Search::InputInfo> ret;
|
||||
GUI_App& app = wxGetApp();
|
||||
if (!app.preset_bundle)
|
||||
return ret;
|
||||
auto print_tech = app.preset_bundle->printers.get_selected_preset().printer_technology();
|
||||
for (Tab* tab : app.tabs_list)
|
||||
if (tab && tab->supports_printer_technology(print_tech))
|
||||
ret.emplace_back(Search::InputInfo{tab->get_config(), tab->type(), app.get_mode()});
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ActionRegistry::~ActionRegistry() = default;
|
||||
|
||||
void ActionRegistry::init()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
@@ -151,18 +317,12 @@ void ActionRegistry::init()
|
||||
// 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.
|
||||
manager.subscribe_on_load_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_capability_load_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Added);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
|
||||
manager.subscribe_on_capability_unload_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Removed);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
|
||||
|
||||
// enumerate current script capabilities
|
||||
std::unordered_map<std::string, std::string> source_names;
|
||||
@@ -173,12 +333,18 @@ void ActionRegistry::init()
|
||||
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& source_name = it == source_names.end() ? key : it->second;
|
||||
if (auto action = make_action(key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
|
||||
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
|
||||
// by prefix, so this is order-independent.
|
||||
for (auto& action : native_commands())
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
||||
@@ -198,7 +364,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
if (change == ActionChange::Removed)
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const std::string source_name = find_loaded_source_name(manager, plugin_key);
|
||||
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
@@ -208,8 +374,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
|
||||
ActionChange change)
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
@@ -233,7 +398,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
|
||||
return;
|
||||
|
||||
seed_state(*action);
|
||||
std::string id = action->id();
|
||||
std::string id = action->id();
|
||||
std::shared_ptr<AppAction> stored = std::move(action);
|
||||
m_actions.insert_or_assign(std::move(id), std::move(stored));
|
||||
}
|
||||
@@ -246,11 +411,11 @@ void ActionRegistry::remove(const std::string& id)
|
||||
|
||||
void ActionRegistry::seed_state(AppAction& a) const
|
||||
{
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end();
|
||||
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
auto it = stats.find(a.id());
|
||||
auto it = stats.find(a.id());
|
||||
if (it != stats.end() && it->is_object()) {
|
||||
a.count = it->value("count", 0);
|
||||
a.last = it->value("last", 0LL);
|
||||
@@ -269,14 +434,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
|
||||
return it == m_actions.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
AppAction* ActionRegistry::find(const std::string& id)
|
||||
{
|
||||
return const_cast<AppAction*>(by_id(id));
|
||||
}
|
||||
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
|
||||
|
||||
// ---- dispatch + write-through ----------------------------------------------
|
||||
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
auto it = m_actions.find(id);
|
||||
@@ -286,13 +448,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
// nested event loop; a queued source refresh can erase the entry while the
|
||||
// keep-alive preserves the action until run returns.
|
||||
std::shared_ptr<AppAction> keep = it->second;
|
||||
AppActionRunResult o = keep->run();
|
||||
AppActionRunResult o = keep->run(param);
|
||||
if (o.level == AppActionRunResult::Level::Busy)
|
||||
return o;
|
||||
|
||||
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
stats = nlohmann::json::object();
|
||||
nlohmann::json& e = stats[id];
|
||||
if (!e.is_object())
|
||||
@@ -300,7 +462,10 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
e["count"] = e.value("count", 0) + 1;
|
||||
e["last"] = (long long) std::time(nullptr);
|
||||
write_section("stats", stats);
|
||||
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; }
|
||||
if (AppAction* live = find(id)) {
|
||||
live->count = e["count"];
|
||||
live->last = e["last"];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -325,8 +490,7 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
||||
std::vector<std::string> next;
|
||||
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
|
||||
for (const auto& id : ids)
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
|
||||
std::find(next.begin(), next.end(), id) == next.end())
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
|
||||
next.push_back(id);
|
||||
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
|
||||
for (const auto& id : cur)
|
||||
@@ -374,17 +538,139 @@ nlohmann::json ActionRegistry::snapshot() const
|
||||
return a->id() < b->id();
|
||||
});
|
||||
|
||||
auto action_to_json = [](const AppAction* a) {
|
||||
return nlohmann::json({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"source", a->source_name()},
|
||||
{"group", a->group},
|
||||
{"input", a->input},
|
||||
{"shortcut", ""}});
|
||||
};
|
||||
|
||||
nlohmann::json actions = nlohmann::json::array();
|
||||
for (const AppAction* a : sorted)
|
||||
actions.push_back({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"source", a->source_name()},
|
||||
{"shortcut", ""}});
|
||||
actions.push_back(action_to_json(a));
|
||||
|
||||
// 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
|
||||
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
|
||||
nlohmann::json favourites(read_string_array("favourite_actions"));
|
||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
|
||||
|
||||
// Recent = the last-N launched actions by recency (only actions with a run history).
|
||||
constexpr size_t kRecentLimit = 5;
|
||||
std::vector<const AppAction*> recent;
|
||||
for (const auto& entry : m_actions)
|
||||
if (entry.second->last > 0)
|
||||
recent.push_back(entry.second.get());
|
||||
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
|
||||
if (a->last != b->last)
|
||||
return a->last > b->last;
|
||||
return a->id() < b->id();
|
||||
});
|
||||
if (recent.size() > kRecentLimit)
|
||||
recent.resize(kRecentLimit);
|
||||
nlohmann::json recent_json = nlohmann::json::array();
|
||||
for (const AppAction* a : recent)
|
||||
recent_json.push_back(action_to_json(a));
|
||||
|
||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}, {"recent", std::move(recent_json)}};
|
||||
}
|
||||
|
||||
nlohmann::json ActionRegistry::settings_search(const std::string& query)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
std::string q = boost::trim_copy(query);
|
||||
// Empty query: show the recently-jumped-to settings instead of a blank list.
|
||||
if (q.empty())
|
||||
return settings_recent();
|
||||
|
||||
// Use the sidebar's live searcher. It is the instance Tab registration (add_key) populates
|
||||
// with each option's group/category, and it carries the current printer technology. A fresh
|
||||
// OptionsSearcher has an empty groups_and_categories, so init()/append_options() drops every
|
||||
// option and the search returns nothing.
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.init(settings_inputs());
|
||||
searcher.search(q, true);
|
||||
auto& found = searcher.found_options();
|
||||
|
||||
constexpr size_t kLimit = 20;
|
||||
const size_t n = std::min<size_t>(kLimit, found.size());
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto& opt = searcher.get_option(i);
|
||||
// Clean plain label "category : group : label" - OptionsSearcher's own label string
|
||||
// carries ImGui icon control chars + <b>/</b> markup (SUPPORTS_MARKUP), which render
|
||||
// as garbage in the webview. Build it from the Option's localized strings instead.
|
||||
std::wstring plain;
|
||||
const std::wstring* prev = nullptr;
|
||||
for (const std::wstring* const s : {&opt.category_local, &opt.group_local, &opt.label_local})
|
||||
if (s != nullptr && !s->empty() && (prev == nullptr || *prev != *s)) {
|
||||
if (!plain.empty())
|
||||
plain += L" : ";
|
||||
plain += *s;
|
||||
prev = s;
|
||||
}
|
||||
out.push_back({{"opt_key", opt.opt_key()},
|
||||
{"type", int(opt.type)},
|
||||
{"label", boost::nowide::narrow(plain)},
|
||||
{"category", boost::nowide::narrow(opt.category)},
|
||||
{"group", boost::nowide::narrow(opt.group)}});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
||||
|
||||
nlohmann::json ActionRegistry::tab_options() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
if (!wxTheApp || wxGetApp().is_closing())
|
||||
return out;
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf || !mf->m_tabpanel)
|
||||
return out;
|
||||
Notebook* notebook = mf->m_tabpanel;
|
||||
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
|
||||
const wxString id = notebook->GetPageName(i);
|
||||
if (id.empty())
|
||||
continue;
|
||||
out.push_back({{"id", id.ToStdString()}, {"title", notebook->GetPageText(i).ToStdString()}});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- settings recents (persisted, most-recent-first, capped at 8) -----------
|
||||
|
||||
nlohmann::json ActionRegistry::settings_recent() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
return read_section("recent_settings", nlohmann::json::array());
|
||||
}
|
||||
|
||||
void ActionRegistry::record_setting_recent(
|
||||
const std::string& opt_key, int type, const std::string& label, const std::string& category, const std::string& group)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
if (opt_key.empty())
|
||||
return;
|
||||
|
||||
constexpr size_t kLimit = 8;
|
||||
auto arr = read_section("recent_settings", nlohmann::json::array());
|
||||
if (!arr.is_array())
|
||||
arr = nlohmann::json::array();
|
||||
auto same = [&](const nlohmann::json& e) {
|
||||
return e.is_object() && e.value("opt_key", std::string()) == opt_key && e.value("type", int(-1)) == type;
|
||||
};
|
||||
|
||||
nlohmann::json next = nlohmann::json::array();
|
||||
next.push_back({{"opt_key", opt_key}, {"type", type}, {"label", label}, {"category", category}, {"group", group}});
|
||||
for (const auto& e : arr)
|
||||
if (!same(e))
|
||||
next.push_back(e);
|
||||
if (next.size() > kLimit)
|
||||
next.erase(next.begin() + long(kLimit), next.end());
|
||||
write_section("recent_settings", next);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -18,6 +18,9 @@ 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 palette section + dispatch.
|
||||
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
|
||||
@@ -56,8 +59,18 @@ struct AppAction
|
||||
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: "settings" (jump to a config option)
|
||||
// or "percent" (jump to layer by a 0-100 value). Empty = run immediately on activation.
|
||||
std::string input;
|
||||
|
||||
virtual ~AppAction() = default;
|
||||
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
|
||||
// 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
|
||||
@@ -92,6 +105,8 @@ private:
|
||||
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.
|
||||
@@ -108,7 +123,7 @@ public:
|
||||
const AppAction* by_id(const std::string& id) const;
|
||||
|
||||
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
||||
AppActionRunResult run(const std::string& id); // runs + bumps stats
|
||||
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
|
||||
void set_favourite(const std::string& id, bool on);
|
||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||
|
||||
@@ -116,9 +131,33 @@ public:
|
||||
bool should_ask(const std::string& id) const;
|
||||
void suppress_ask(const std::string& id);
|
||||
|
||||
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
|
||||
// Flat, frecency-sorted snapshot for the webview:
|
||||
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
|
||||
nlohmann::json snapshot() const;
|
||||
|
||||
// "Go to setting..." Speed Dial helper: query the current print/filament/printer
|
||||
// config options via the sidebar's live OptionsSearcher (the instance Tab registration
|
||||
// populates with group/category, and which carries the current printer technology) and
|
||||
// return the top matches as JSON. The searcher is re-seeded from the current configs +
|
||||
// user mode on every call so the result always reflects what the sidebar's own search
|
||||
// would show. An empty/whitespace query returns the recent settings list (below), and the
|
||||
// page shows a "type to search" hint when there are no recents.
|
||||
nlohmann::json settings_search(const std::string& query);
|
||||
|
||||
// Recently-jumped-to settings, persisted (most-recent-first, capped at 8). Returns the
|
||||
// stored JSON array [{opt_key,type,label,category,group},...]; record_setting_recent()
|
||||
// prepends an entry (deduped by opt_key+type) and re-persists.
|
||||
nlohmann::json settings_recent() const;
|
||||
void record_setting_recent(const std::string& opt_key, int type, const std::string& label,
|
||||
const std::string& category, const std::string& group);
|
||||
|
||||
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
|
||||
// as [{id,title},...]. 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);
|
||||
|
||||
@@ -1039,7 +1039,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
|
||||
@@ -3502,11 +3501,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
|
||||
break;
|
||||
}
|
||||
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
|
||||
case ' ': {
|
||||
if (m_canvas_type == ECanvasType::CanvasView3D)
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_OPEN_SPEED_DIAL));
|
||||
break;
|
||||
}
|
||||
case 'A':
|
||||
case 'a':
|
||||
{
|
||||
|
||||
@@ -166,7 +166,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
|
||||
|
||||
@@ -197,6 +197,8 @@ void KBShortcutsDialog::fill_shortcuts()
|
||||
|
||||
// Switch table page
|
||||
{ ctrl + L("Tab"), L("Switch table page")},
|
||||
// Open speed dial
|
||||
{ ctrl + "K", L("Open speed dial") },
|
||||
//DEL
|
||||
#ifdef __APPLE__
|
||||
{"fn+⌫", L("Delete Selected")},
|
||||
@@ -267,8 +269,6 @@ void KBShortcutsDialog::fill_shortcuts()
|
||||
{ "O", L("Zoom out") },
|
||||
{ "V", L("Toggle printable for object/part") },
|
||||
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
|
||||
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
|
||||
|
||||
};
|
||||
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
|
||||
|
||||
|
||||
@@ -701,6 +701,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
}
|
||||
return;}
|
||||
#endif
|
||||
// Orca: open the speed dial from any page. CmdDown() = Ctrl on Win/Linux, Cmd on macOS.
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'K') { wxGetApp().open_speed_dial(); return; }
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
|
||||
m_plater->apply_background_progress();
|
||||
@@ -3346,6 +3348,11 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().open_preferences();
|
||||
},
|
||||
"", nullptr, []() { return true; }, this, 1);
|
||||
parent_menu->AppendSeparator();
|
||||
append_menu_item(
|
||||
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
//parent_menu->Insert(1, preference_item);
|
||||
#endif
|
||||
// Help menu
|
||||
@@ -3370,7 +3377,13 @@ void MainFrame::init_menubar_as_editor()
|
||||
auto top_menu = m_topbar->GetTopMenu();
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
append_menu_item(
|
||||
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + ctrl + "K", "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
|
||||
[this](wxCommandEvent &) {
|
||||
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
|
||||
@@ -3508,6 +3521,10 @@ void MainFrame::init_menubar_as_editor()
|
||||
#else
|
||||
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
|
||||
fileMenu->AppendSeparator();
|
||||
append_menu_item(
|
||||
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "",
|
||||
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
append_menu_item(
|
||||
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
|
||||
[this](wxCommandEvent&) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Widgets/Label.hpp"
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
|
||||
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
|
||||
|
||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
||||
{
|
||||
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
|
||||
Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
|
||||
btn->SetCornerRadius(0);
|
||||
|
||||
if (bmp_name.empty() && bmp.IsOk())
|
||||
btn->SetIcon(bmp);
|
||||
|
||||
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
|
||||
// original spacing between a tab's icon and its caption.
|
||||
{
|
||||
wxClientDC dc(btn);
|
||||
dc.SetFont(btn->GetFont());
|
||||
int space_w = 0;
|
||||
dc.GetTextExtent(" ", &space_w, nullptr);
|
||||
btn->SetIconSpacing(5 + space_w);
|
||||
}
|
||||
|
||||
int em = em_unit(this);
|
||||
//BBS set size for button
|
||||
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
@@ -245,7 +256,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
|
||||
int em = em_unit(this);
|
||||
Button* btn = m_pageButtons[n];
|
||||
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n]));
|
||||
btn->SetLabel(compact ? "" : m_pageLabels[n]);
|
||||
}
|
||||
|
||||
wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
|
||||
@@ -7567,10 +7567,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
|
||||
if (this->q->is_view3D_shown())
|
||||
wxGetApp().open_speed_dial();
|
||||
});
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
|
||||
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
||||
|
||||
@@ -9,20 +9,26 @@
|
||||
#include "Plater.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <libslic3r/Preset.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <wx/display.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <gtk/gtk.h>
|
||||
#endif
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
// ADJUST WIDTH HERE (DIP px). Fixed dialog width; was 360, now 1.5x. Height is not set here -
|
||||
// the dialog auto-resizes to the page content (see resize_to_content + the list max-height in style.css).
|
||||
constexpr int kPopupWidth = 540;
|
||||
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
|
||||
constexpr int kPopupWidth = 540;
|
||||
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
|
||||
constexpr int kPopupMaxHeight = 282;
|
||||
|
||||
int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
||||
@@ -33,23 +39,49 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
||||
|
||||
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
||||
|
||||
// Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container.
|
||||
// (browser()->SetFocus() grabs focus on the container and doesn't reach the web content,
|
||||
// so typing only works after the user clicks.) On Linux the native backend is the
|
||||
// WebKitWebView widget; grab focus there directly. Elsewhere SetFocus() is correct.
|
||||
void focus_webview(wxWebView* browser, bool page_ready)
|
||||
{
|
||||
if (!browser)
|
||||
return;
|
||||
#ifdef __linux__
|
||||
if (void* nb = browser->GetNativeBackend())
|
||||
gtk_widget_grab_focus((GtkWidget*) nb);
|
||||
#else
|
||||
browser->SetFocus();
|
||||
#endif
|
||||
if (page_ready)
|
||||
browser->RunScript("focusInput();");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
|
||||
: WebViewHostDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
||||
wxBORDER_NONE | wxFRAME_NO_TASKBAR)
|
||||
: WebViewHostDialog(parent,
|
||||
wxID_ANY,
|
||||
wxEmptyString,
|
||||
wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT)
|
||||
{
|
||||
SetBackgroundColour(bg_color());
|
||||
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
|
||||
if (!event.GetActive() && IsShown())
|
||||
// Focus the WebKit widget exactly when the WM makes the popup the active window
|
||||
// (modeless focus is granted asynchronously, so a focus request made right after
|
||||
// Show() is dropped). Also re-corrects focus on every re-open.
|
||||
if (event.GetActive() && IsShown())
|
||||
focus_webview(browser(), m_page_ready);
|
||||
else if (!event.GetActive() && IsShown())
|
||||
Hide();
|
||||
event.Skip();
|
||||
});
|
||||
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString,
|
||||
wxSize(kPopupWidth, kPopupMaxHeight), wxSize(kPopupWidth, kPopupMinHeight))) {
|
||||
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight),
|
||||
wxSize(kPopupWidth, kPopupMinHeight))) {
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")),
|
||||
wxSizerFlags().Border(wxALL, 20));
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20));
|
||||
SetSizer(sizer);
|
||||
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
|
||||
}
|
||||
@@ -61,8 +93,7 @@ void SpeedDialWebDialog::request_show()
|
||||
{
|
||||
if (IsShown()) {
|
||||
Raise();
|
||||
if (browser())
|
||||
browser()->SetFocus();
|
||||
focus_webview(browser(), m_page_ready);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,8 +101,9 @@ void SpeedDialWebDialog::request_show()
|
||||
Raise();
|
||||
if (m_page_ready)
|
||||
send_actions();
|
||||
if (browser())
|
||||
browser()->SetFocus();
|
||||
// Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is
|
||||
// what makes typing reach the search field immediately on open.
|
||||
focus_webview(browser(), m_page_ready);
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
@@ -95,8 +127,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
|
||||
if (command == "request_actions") {
|
||||
m_page_ready = true;
|
||||
send_actions();
|
||||
}
|
||||
else if (command == "toggle_favourite")
|
||||
} else if (command == "toggle_favourite")
|
||||
wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false));
|
||||
else if (command == "reorder_favourites") {
|
||||
std::vector<std::string> ids;
|
||||
@@ -105,13 +136,61 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
|
||||
if (id.is_string())
|
||||
ids.push_back(id.get<std::string>());
|
||||
wxGetApp().action_registry().reorder_favourites(ids);
|
||||
}
|
||||
else if (command == "run_action")
|
||||
run_action(payload.value("id", ""), payload.value("title", ""));
|
||||
else if (command == "resize")
|
||||
} else if (command == "run_action")
|
||||
run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", ""));
|
||||
else if (command == "go_to_setting") {
|
||||
// "Go to setting..." second phase: the page hands back the option it matched.
|
||||
const std::string opt_key = payload.value("opt_key", "");
|
||||
if (!opt_key.empty()) {
|
||||
const int type = json_int_or(payload, "type", int(Preset::TYPE_INVALID));
|
||||
const std::string label = payload.value("label", "");
|
||||
const std::string group = payload.value("group", "");
|
||||
const std::string cat = payload.value("category", "");
|
||||
// Track it in the palette's recent-settings list before jumping (persisted).
|
||||
wxGetApp().action_registry().record_setting_recent(opt_key, type, label, cat, group);
|
||||
Hide();
|
||||
wxGetApp().sidebar().jump_to_option(opt_key, Preset::Type(type), from_u8(cat).ToStdWstring());
|
||||
}
|
||||
} else if (command == "search_settings")
|
||||
search_settings(payload.value("q", ""));
|
||||
else if (command == "search_tabs")
|
||||
search_tabs();
|
||||
else if (command == "go_to_tab") {
|
||||
// "Go to tab..." second phase: the page hands back the tab id it matched.
|
||||
const std::string tab_id = payload.value("id", "");
|
||||
if (!tab_id.empty()) {
|
||||
Hide();
|
||||
if (wxGetApp().mainframe)
|
||||
wxGetApp().mainframe->select_tab(from_u8(tab_id));
|
||||
}
|
||||
} else if (command == "resize")
|
||||
resize_to_content(json_int_or(payload, "height", 0));
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::search_tabs()
|
||||
{
|
||||
// Round-trip is async because the webview delivers script messages synchronously on the
|
||||
// GTK/macOS stack; defer the (cheap) enumeration and push the result back to the page.
|
||||
wxGetApp().CallAfter([this, alive = m_alive]() {
|
||||
if (!alive->load(std::memory_order_acquire))
|
||||
return;
|
||||
auto tabs = wxGetApp().action_registry().tab_options();
|
||||
call_web_handler({{"command", "tab_results"}, {"tabs", std::move(tabs)}});
|
||||
});
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::search_settings(const std::string& query)
|
||||
{
|
||||
// Round-trip is async because the webview delivers script messages synchronously on the
|
||||
// GTK/macOS stack; defer the (cheap) search and push the result back to the page.
|
||||
wxGetApp().CallAfter([this, alive = m_alive, query]() {
|
||||
if (!alive->load(std::memory_order_acquire))
|
||||
return;
|
||||
auto results = wxGetApp().action_registry().settings_search(query);
|
||||
call_web_handler({{"command", "settings_results"}, {"results", std::move(results)}});
|
||||
});
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::resize_to_content(int height)
|
||||
{
|
||||
if (height <= 0)
|
||||
@@ -127,14 +206,15 @@ void SpeedDialWebDialog::resize_to_content(int height)
|
||||
Layout();
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title)
|
||||
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title, const std::string& param)
|
||||
{
|
||||
ActionRegistry& reg = wxGetApp().action_registry();
|
||||
const AppAction* a = reg.by_id(id);
|
||||
const AppAction* a = reg.by_id(id);
|
||||
if (!a)
|
||||
return;
|
||||
|
||||
const bool ask = reg.should_ask(id);
|
||||
// Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately.
|
||||
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
|
||||
const std::string atitle = a->title();
|
||||
if (IsModal())
|
||||
EndModal(wxID_CANCEL);
|
||||
@@ -143,8 +223,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
||||
|
||||
if (ask) {
|
||||
const wxString label = title.empty() ? from_u8(atitle) : from_u8(title);
|
||||
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label),
|
||||
_L("Run plugin"), wxOK | wxCANCEL);
|
||||
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), _L("Run plugin"), wxOK | wxCANCEL);
|
||||
dlg.ShowCheckBox(_L("Don't ask again for this action"));
|
||||
if (dlg.ShowModal() != wxID_OK)
|
||||
return;
|
||||
@@ -152,18 +231,21 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
||||
wxGetApp().action_registry().suppress_ask(id);
|
||||
}
|
||||
|
||||
wxGetApp().CallAfter([id] {
|
||||
wxGetApp().CallAfter([id, param] {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
AppActionRunResult result = wxGetApp().action_registry().run(id);
|
||||
AppActionRunResult result = wxGetApp().action_registry().run(id, param);
|
||||
if (result.level == AppActionRunResult::Level::Busy)
|
||||
return;
|
||||
if (!result.message.IsEmpty() && wxGetApp().plater())
|
||||
wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
result.level == AppActionRunResult::Level::Error ? NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(result.message));
|
||||
wxGetApp()
|
||||
.plater()
|
||||
->get_notification_manager()
|
||||
->push_notification(NotificationType::CustomNotification,
|
||||
result.level == AppActionRunResult::Level::Error ?
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(result.message));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,7 +254,8 @@ void SpeedDialWebDialog::send_actions()
|
||||
nlohmann::json snap = wxGetApp().action_registry().snapshot();
|
||||
call_web_handler({{"command", "list_actions"},
|
||||
{"actions", std::move(snap["actions"])},
|
||||
{"favourites", std::move(snap["favourites"])}});
|
||||
{"favourites", std::move(snap["favourites"])},
|
||||
{"recent", std::move(snap["recent"])}});
|
||||
}
|
||||
|
||||
}}
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -20,8 +20,10 @@ private:
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void resize_to_content(int height);
|
||||
void run_action(const std::string& id, const std::string& title);
|
||||
void run_action(const std::string& id, const std::string& title, const std::string& param = "");
|
||||
void send_actions();
|
||||
void search_settings(const std::string& query);
|
||||
void search_tabs();
|
||||
|
||||
bool m_page_ready{false};
|
||||
// Guards the CallAfter in on_script_message across dialog destruction, same as
|
||||
|
||||
+109
-121
@@ -25,36 +25,29 @@ END_EVENT_TABLE()
|
||||
* calling Refresh()/Update().
|
||||
*/
|
||||
|
||||
Button::Button()
|
||||
: paddingSize(10, 8)
|
||||
Button::Button() : paddingSize(10, 8)
|
||||
{
|
||||
background_color = StateColor(
|
||||
std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
||||
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
||||
std::make_pair(0x009688, (int) StateColor::Checked),
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
||||
text_color = StateColor(
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled),
|
||||
std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
||||
background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
||||
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
||||
std::make_pair(0x009688, (int) StateColor::Checked),
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
||||
text_color = StateColor(std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled), std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
||||
}
|
||||
|
||||
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
||||
: Button()
|
||||
{
|
||||
Create(parent, text, icon, style, iconSize, btn_id);
|
||||
}
|
||||
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) : Button()
|
||||
{ Create(parent, text, icon, style, iconSize, btn_id); }
|
||||
|
||||
bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
||||
{
|
||||
StaticBox::Create(parent, btn_id, wxDefaultPosition, wxDefaultSize, style);
|
||||
state_handler.attach(std::vector<StateColor const*>{&text_color});
|
||||
state_handler.update_binds();
|
||||
//BBS set default font
|
||||
// BBS set default font
|
||||
SetFont(Label::Body_14);
|
||||
wxWindow::SetLabel(text);
|
||||
if (!icon.IsEmpty()) {
|
||||
//BBS set button icon default size to 20
|
||||
// BBS set button icon default size to 20
|
||||
this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize > 0 ? iconSize : 20);
|
||||
}
|
||||
messureSize();
|
||||
@@ -82,14 +75,12 @@ void Button::SetIcon(const wxString& icon)
|
||||
{
|
||||
auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt());
|
||||
if (!icon.IsEmpty()) {
|
||||
//BBS set button icon default size to 20
|
||||
// BBS set button icon default size to 20
|
||||
if (!tmpBitmap.bmp().IsSameAs(this->active_icon.bmp())) {
|
||||
this->active_icon = tmpBitmap;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
this->active_icon = ScalableBitmap();
|
||||
Refresh();
|
||||
}
|
||||
@@ -97,7 +88,7 @@ void Button::SetIcon(const wxString& icon)
|
||||
|
||||
void Button::SetIcon(const wxBitmap& icon)
|
||||
{
|
||||
this->active_icon = ScalableBitmap();
|
||||
this->active_icon = ScalableBitmap();
|
||||
this->active_icon.bmp() = icon;
|
||||
messureSize();
|
||||
Refresh();
|
||||
@@ -121,6 +112,13 @@ void Button::SetPaddingSize(const wxSize& size)
|
||||
messureSize();
|
||||
}
|
||||
|
||||
void Button::SetIconSpacing(int spacing)
|
||||
{
|
||||
m_icon_spacing = spacing;
|
||||
messureSize();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Button::SetTextColor(StateColor const& color)
|
||||
{
|
||||
text_color = color;
|
||||
@@ -128,7 +126,7 @@ void Button::SetTextColor(StateColor const& color)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Button::SetTextColorNormal(wxColor const &color)
|
||||
void Button::SetTextColorNormal(wxColor const& color)
|
||||
{
|
||||
text_color.setColorForStates(color, 0);
|
||||
Refresh();
|
||||
@@ -145,22 +143,22 @@ bool Button::Enable(bool enable)
|
||||
return result;
|
||||
}
|
||||
|
||||
void Button::SetCanFocus(bool canFocus) {
|
||||
void Button::SetCanFocus(bool canFocus)
|
||||
{
|
||||
StaticBox::SetCanFocus(canFocus);
|
||||
this->canFocus = canFocus;
|
||||
}
|
||||
|
||||
void Button::SetValue(bool state)
|
||||
{
|
||||
if (GetValue() == state) return;
|
||||
if (GetValue() == state)
|
||||
return;
|
||||
state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked);
|
||||
}
|
||||
|
||||
bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; }
|
||||
|
||||
void Button::SetCenter(bool isCenter)
|
||||
{
|
||||
this->isCenter = isCenter; }
|
||||
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
|
||||
|
||||
void Button::SetVertical(bool vertical)
|
||||
{
|
||||
@@ -177,38 +175,33 @@ wxString btn_disabled[10] = {"#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFD
|
||||
|
||||
void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
||||
{
|
||||
if (type == ButtonType::Compact) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(8,3)));
|
||||
if (type == ButtonType::Compact) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(8, 3)));
|
||||
this->SetCornerRadius(this->FromDIP(8));
|
||||
this->SetFont(Label::Body_10);
|
||||
}
|
||||
else if (type == ButtonType::Window) {
|
||||
this->SetSize(FromDIP(wxSize(58,24)));
|
||||
this->SetMinSize(FromDIP(wxSize(58,24)));
|
||||
} else if (type == ButtonType::Window) {
|
||||
this->SetSize(FromDIP(wxSize(58, 24)));
|
||||
this->SetMinSize(FromDIP(wxSize(58, 24)));
|
||||
this->SetCornerRadius(this->FromDIP(12));
|
||||
this->SetFont(Label::Body_12);
|
||||
}
|
||||
else if (type == ButtonType::Choice) {
|
||||
this->SetMinSize(FromDIP(wxSize(100,32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12,8)));
|
||||
} else if (type == ButtonType::Choice) {
|
||||
this->SetMinSize(FromDIP(wxSize(100, 32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
else if (type == ButtonType::Parameter) {
|
||||
this->SetMinSize(FromDIP(wxSize(120,26)));
|
||||
this->SetSize(FromDIP(wxSize(120,26)));
|
||||
} else if (type == ButtonType::Parameter) {
|
||||
this->SetMinSize(FromDIP(wxSize(120, 26)));
|
||||
this->SetSize(FromDIP(wxSize(120, 26)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
else if (type == ButtonType::Icon) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(5,5)));
|
||||
this->SetMinSize(FromDIP(wxSize(26,26)));
|
||||
this->SetSize(FromDIP(wxSize(26,26)));
|
||||
} else if (type == ButtonType::Icon) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(5, 5)));
|
||||
this->SetMinSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
}
|
||||
else if (type == ButtonType::Expanded) {
|
||||
this->SetMinSize(FromDIP(wxSize(-1,32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12,8)));
|
||||
} else if (type == ButtonType::Expanded) {
|
||||
this->SetMinSize(FromDIP(wxSize(-1, 32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
@@ -217,39 +210,33 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
||||
|
||||
bool is_dark = StateColor::darkModeColorFor("#FFFFFF") != wxColour("#FFFFFF");
|
||||
|
||||
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
|
||||
style == ButtonStyle::Confirm ? btn_confirm :
|
||||
style == ButtonStyle::Alert ? btn_alert :
|
||||
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
|
||||
style == ButtonStyle::Confirm ? btn_confirm :
|
||||
style == ButtonStyle::Alert ? btn_alert :
|
||||
style == ButtonStyle::Disabled ? btn_disabled :
|
||||
btn_regular ;
|
||||
btn_regular;
|
||||
|
||||
auto bg_color = StateColor(
|
||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[1]), (int)StateColor::Pressed),
|
||||
std::pair(wxColour(clr_arr[2]), (int)StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal),
|
||||
std::pair(wxColour(clr_arr[4]), (int)StateColor::Enabled)
|
||||
);
|
||||
auto bg_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed),
|
||||
std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal),
|
||||
std::pair(wxColour(clr_arr[4]), (int) StateColor::Enabled));
|
||||
bg_color.setTakeFocusedAsHovered(false);
|
||||
this->SetBackgroundColor(bg_color);
|
||||
wxColour focus_clr = clr_arr[is_dark ? 8 : 9];
|
||||
auto border_color = StateColor(
|
||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[2]), (int)(StateColor::Hovered | ~StateColor::Focused)),
|
||||
std::pair(wxColour(focus_clr ), (int)StateColor::Focused),
|
||||
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal)
|
||||
);
|
||||
auto border_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)),
|
||||
std::pair(wxColour(focus_clr), (int) StateColor::Focused),
|
||||
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal));
|
||||
border_color.setTakeFocusedAsHovered(false);
|
||||
this->SetBorderColor(border_color);
|
||||
this->SetTextColor(StateColor(
|
||||
std::pair(wxColour(clr_arr[5]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[7]), (int)StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[6]), (int)StateColor::Normal)
|
||||
));
|
||||
this->SetTextColor(StateColor(std::pair(wxColour(clr_arr[5]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[6]), (int) StateColor::Normal)));
|
||||
|
||||
m_has_style = true;
|
||||
m_style = style;
|
||||
m_type = type;
|
||||
m_style = style;
|
||||
m_type = type;
|
||||
}
|
||||
|
||||
void Button::Rescale()
|
||||
@@ -260,7 +247,7 @@ void Button::Rescale()
|
||||
|
||||
messureSize();
|
||||
|
||||
if(m_has_style)
|
||||
if (m_has_style)
|
||||
SetStyle(m_style, m_type);
|
||||
|
||||
Refresh();
|
||||
@@ -281,7 +268,7 @@ void Button::paintEvent(wxPaintEvent& evt)
|
||||
void Button::render(wxDC& dc)
|
||||
{
|
||||
StaticBox::render(dc);
|
||||
int states = state_handler.states();
|
||||
int states = state_handler.states();
|
||||
wxSize size = GetSize();
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
// calc content size
|
||||
@@ -289,22 +276,22 @@ void Button::render(wxDC& dc)
|
||||
wxSize textSize = this->textSize.GetSize();
|
||||
|
||||
const ScalableBitmap& icon = active_icon;
|
||||
wxSize padding = this->paddingSize;
|
||||
int spacing = 5;
|
||||
wxSize padding = this->paddingSize;
|
||||
int spacing = m_icon_spacing;
|
||||
// Wrap text
|
||||
auto text = GetLabel();
|
||||
if (vertical && textSize.x + padding.x * 2 > size.x) {
|
||||
Label::split_lines(dc, size.x - padding.x * 2, text, text, 2);
|
||||
textSize = dc.GetMultiLineTextExtent(text);
|
||||
if (padding.x * 2 + textSize.x > size.x) {
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
|
||||
textSize = dc.GetMultiLineTextExtent(text);
|
||||
}
|
||||
}
|
||||
auto szContent = textSize;
|
||||
if (icon.bmp().IsOk()) {
|
||||
if (szContent.y > 0) {
|
||||
//BBS norrow size between text and icon
|
||||
// BBS norrow size between text and icon
|
||||
if (vertical)
|
||||
szContent.y += spacing;
|
||||
else
|
||||
@@ -313,10 +300,12 @@ void Button::render(wxDC& dc)
|
||||
szIcon = icon.GetBmpSize();
|
||||
if (vertical) {
|
||||
szContent.y += szIcon.y;
|
||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
||||
if (szIcon.x > szContent.x)
|
||||
szContent.x = szIcon.x;
|
||||
} else {
|
||||
szContent.x += szIcon.x;
|
||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
||||
if (szIcon.y > szContent.y)
|
||||
szContent.y = szIcon.y;
|
||||
}
|
||||
if (szContent.x > size.x) {
|
||||
int d = std::min(padding.x, (szContent.x - size.x) / 2);
|
||||
@@ -325,10 +314,11 @@ void Button::render(wxDC& dc)
|
||||
}
|
||||
}
|
||||
// move to center
|
||||
wxRect rcContent = { {0, 0}, size };
|
||||
wxRect rcContent = {{0, 0}, size};
|
||||
if (isCenter) {
|
||||
wxSize offset = (size - szContent) / 2;
|
||||
if (offset.x < 0) offset.x = 0;
|
||||
if (offset.x < 0)
|
||||
offset.x = 0;
|
||||
rcContent.Deflate(offset.x, offset.y);
|
||||
}
|
||||
// start draw
|
||||
@@ -339,7 +329,7 @@ void Button::render(wxDC& dc)
|
||||
else
|
||||
pt.y += (rcContent.height - szIcon.y) / 2;
|
||||
dc.DrawBitmap(icon.bmp(), pt);
|
||||
//BBS norrow size between text and icon
|
||||
// BBS norrow size between text and icon
|
||||
if (vertical) {
|
||||
pt.y += szIcon.y + spacing;
|
||||
pt.x = rcContent.x;
|
||||
@@ -373,19 +363,21 @@ void Button::messureSize()
|
||||
wxSize szContent = textSize.GetSize();
|
||||
if (this->active_icon.bmp().IsOk()) {
|
||||
if (szContent.y > 0) {
|
||||
//BBS norrow size between text and icon
|
||||
// BBS narrow size between text and icon
|
||||
if (vertical)
|
||||
szContent.y += 5;
|
||||
szContent.y += m_icon_spacing;
|
||||
else
|
||||
szContent.x += 5;
|
||||
szContent.x += m_icon_spacing;
|
||||
}
|
||||
wxSize szIcon = this->active_icon.GetBmpSize();
|
||||
if (vertical) {
|
||||
szContent.y += szIcon.y;
|
||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
||||
if (szIcon.x > szContent.x)
|
||||
szContent.x = szIcon.x;
|
||||
} else {
|
||||
szContent.x += szIcon.x;
|
||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
||||
if (szIcon.y > szContent.y)
|
||||
szContent.y = szIcon.y;
|
||||
}
|
||||
}
|
||||
wxSize size = szContent + paddingSize * 2;
|
||||
@@ -429,13 +421,13 @@ void Button::mouseReleased(wxMouseEvent& event)
|
||||
}
|
||||
}
|
||||
|
||||
void Button::mouseCaptureLost(wxMouseCaptureLostEvent &event)
|
||||
void Button::mouseCaptureLost(wxMouseCaptureLostEvent& event)
|
||||
{
|
||||
wxMouseEvent evt;
|
||||
mouseReleased(evt);
|
||||
}
|
||||
|
||||
void Button::keyDownUp(wxKeyEvent &event)
|
||||
void Button::keyDownUp(wxKeyEvent& event)
|
||||
{
|
||||
if (event.GetKeyCode() == WXK_SPACE || event.GetKeyCode() == WXK_RETURN) {
|
||||
wxMouseEvent evt(event.GetEventType() == wxEVT_KEY_UP ? wxEVT_LEFT_UP : wxEVT_LEFT_DOWN);
|
||||
@@ -444,8 +436,8 @@ void Button::keyDownUp(wxKeyEvent &event)
|
||||
return;
|
||||
}
|
||||
if (event.GetEventType() == wxEVT_KEY_DOWN &&
|
||||
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT
|
||||
|| event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
||||
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT ||
|
||||
event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
||||
HandleAsNavigationKey(event);
|
||||
else
|
||||
event.Skip();
|
||||
@@ -462,7 +454,9 @@ void Button::sendButtonEvent()
|
||||
|
||||
WXLRESULT Button::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||
{
|
||||
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTMESSAGE; }
|
||||
if (nMsg == WM_GETDLGCODE) {
|
||||
return DLGC_WANTMESSAGE;
|
||||
}
|
||||
if (nMsg == WM_KEYDOWN) {
|
||||
wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam));
|
||||
switch (wParam) {
|
||||
@@ -483,8 +477,7 @@ void Button::EnableTooltipEvenDisabled()
|
||||
{
|
||||
#if defined(_MSC_VER) || defined(_WIN32)
|
||||
auto parent = this->GetParent();
|
||||
if (parent)
|
||||
{
|
||||
if (parent) {
|
||||
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
|
||||
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
|
||||
};
|
||||
@@ -494,22 +487,21 @@ void Button::EnableTooltipEvenDisabled()
|
||||
void Button::OnParentMotion(wxMouseEvent& event)
|
||||
{
|
||||
auto parent = this->GetParent();
|
||||
if (!parent) return event.Skip();
|
||||
if (!parent)
|
||||
return event.Skip();
|
||||
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxRect screen_rect = this->GetScreenRect();
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos))
|
||||
{
|
||||
if (!tipWindow)
|
||||
{
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) {
|
||||
if (!tipWindow) {
|
||||
tipWindow = wxTipWindow::New(this, tip);
|
||||
if (!tipWindow) return event.Skip();
|
||||
if (!tipWindow)
|
||||
return event.Skip();
|
||||
tipWindow->Enable(false);
|
||||
}
|
||||
|
||||
if (tipWindow->GetLabel() != tip)
|
||||
{
|
||||
if (tipWindow->GetLabel() != tip) {
|
||||
tipWindow->SetLabel(tip);
|
||||
}
|
||||
|
||||
@@ -517,11 +509,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
||||
// using wxGetMousePosition() which returns (0,0) on Wayland.
|
||||
tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize());
|
||||
tipWindow->Popup();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tipWindow)
|
||||
{
|
||||
} else {
|
||||
if (tipWindow) {
|
||||
tipWindow->Dismiss();
|
||||
tipWindow->Destroy();
|
||||
tipWindow = nullptr;
|
||||
@@ -534,15 +523,14 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
||||
void Button::OnParentLeave(wxMouseEvent& event)
|
||||
{
|
||||
auto parent = this->GetParent();
|
||||
if (!parent) return event.Skip();
|
||||
if (!parent)
|
||||
return event.Skip();
|
||||
|
||||
if (tipWindow)
|
||||
{
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
if (tipWindow) {
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxRect screen_rect = this->GetScreenRect();
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!screen_rect.Contains(pos))
|
||||
{
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!screen_rect.Contains(pos)) {
|
||||
tipWindow->Dismiss();
|
||||
tipWindow->Destroy();
|
||||
tipWindow = nullptr;
|
||||
|
||||
@@ -8,24 +8,25 @@
|
||||
class ButtonProps
|
||||
{
|
||||
public:
|
||||
static int ChoiceButtonGap(){return 10;};
|
||||
static int WindowButtonGap(){return 10;};
|
||||
static int ChoiceButtonGap() { return 10; };
|
||||
static int WindowButtonGap() { return 10; };
|
||||
};
|
||||
|
||||
enum class ButtonStyle{
|
||||
enum class ButtonStyle {
|
||||
Regular,
|
||||
Confirm,
|
||||
Alert,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
enum class ButtonType{
|
||||
Compact , // Font10 FullyRounded For spaces with less areas
|
||||
Window , // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
||||
Choice , // Font14 Semi-Rounded For dialog/window choice buttons
|
||||
enum class ButtonType {
|
||||
Compact, // Font10 FullyRounded For spaces with less areas
|
||||
Window, // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
||||
Choice, // Font14 Semi-Rounded For dialog/window choice buttons
|
||||
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
|
||||
Icon , // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while creation of button
|
||||
Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||
Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while
|
||||
// creation of button
|
||||
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||
};
|
||||
|
||||
class Button : public StaticBox
|
||||
@@ -34,17 +35,18 @@ class Button : public StaticBox
|
||||
wxRect textSize;
|
||||
wxSize minSize; // set by outer
|
||||
wxSize paddingSize;
|
||||
int m_icon_spacing = 5;
|
||||
ScalableBitmap active_icon;
|
||||
|
||||
StateColor text_color;
|
||||
StateColor text_color;
|
||||
|
||||
bool pressedDown = false;
|
||||
bool m_selected = true;
|
||||
bool canFocus = true;
|
||||
bool canFocus = true;
|
||||
bool isCenter = true;
|
||||
bool vertical = false;
|
||||
|
||||
static const int buttonWidth = 200;
|
||||
static const int buttonWidth = 200;
|
||||
static const int buttonHeight = 50;
|
||||
|
||||
public:
|
||||
@@ -66,21 +68,23 @@ public:
|
||||
|
||||
void SetPaddingSize(const wxSize& size);
|
||||
|
||||
void SetIconSpacing(int spacing);
|
||||
|
||||
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
|
||||
|
||||
void SetTextColor(StateColor const &color);
|
||||
void SetTextColor(StateColor const& color);
|
||||
|
||||
void SetTextColorNormal(wxColor const &color);
|
||||
void SetTextColorNormal(wxColor const& color);
|
||||
|
||||
void SetSelected(bool selected = true) { m_selected = selected; }
|
||||
|
||||
// Only meant to be used by inspector, not public API
|
||||
ButtonStyle GetStyle() const { return m_style; }
|
||||
ButtonType GetType() const { return m_type; }
|
||||
bool IsSelected() const { return m_selected; }
|
||||
ButtonType GetType() const { return m_type; }
|
||||
bool IsSelected() const { return m_selected; }
|
||||
|
||||
bool Enable(bool enable = true) override;
|
||||
void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled
|
||||
void EnableTooltipEvenDisabled(); // The tip will be shown even if the button is disabled
|
||||
|
||||
void SetCanFocus(bool canFocus) override;
|
||||
|
||||
@@ -104,7 +108,7 @@ protected:
|
||||
private:
|
||||
bool m_has_style = false;
|
||||
ButtonStyle m_style;
|
||||
ButtonType m_type;
|
||||
ButtonType m_type;
|
||||
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
|
||||
@@ -115,10 +119,10 @@ private:
|
||||
// some useful events
|
||||
void mouseDown(wxMouseEvent& event);
|
||||
void mouseReleased(wxMouseEvent& event);
|
||||
void mouseCaptureLost(wxMouseCaptureLostEvent &event);
|
||||
void keyDownUp(wxKeyEvent &event);
|
||||
void mouseCaptureLost(wxMouseCaptureLostEvent& event);
|
||||
void keyDownUp(wxKeyEvent& event);
|
||||
|
||||
//
|
||||
//
|
||||
void sendButtonEvent();
|
||||
|
||||
// parent motion
|
||||
|
||||
Reference in New Issue
Block a user