Initial Commit of speed dial improvement

This commit is contained in:
Lam Wei Lun
2026-09-07 14:00:28 +08:00
parent 8d44b680bd
commit b32f28e712
15 changed files with 1441 additions and 566 deletions
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",
+31
View File
@@ -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;
+338 -52
View File
@@ -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
+42 -3
View File
@@ -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);
-6
View File
@@ -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':
{
-1
View File
@@ -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);
+2 -2
View File
@@ -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 });
+18 -1
View File
@@ -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&) {
+13 -2
View File
@@ -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
-4
View File
@@ -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(); });
+116 -33
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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;
+25 -21
View File
@@ -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