Support searching the Speed Dial by English names in any UI language (#16369)

This commit is contained in:
SoftFever
2026-10-11 21:29:06 +08:00
committed by GitHub
parent 7369674d8f
commit bd125d32da
16 changed files with 517 additions and 319 deletions
+103 -56
View File
@@ -206,19 +206,52 @@ constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
std::string setting_type_context(Preset::Type type)
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs. Untranslated:
// the caller translates it for the displayed breadcrumb and keeps it for the English one.
const char* setting_type_context(Preset::Type type)
{
switch (type) {
case Preset::TYPE_FILAMENT:
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
case Preset::TYPE_PRINTER: return _u8L("Printer");
case Preset::TYPE_SLA_MATERIAL: return L("Filament");
case Preset::TYPE_PRINTER: return L("Printer");
case Preset::TYPE_PRINT:
case Preset::TYPE_SLA_PRINT:
default: return _u8L("Process");
default: return L("Process");
}
}
// A setting's text in one language. The displayed text and its English search aliases both come from
// here, so in an English UI the two are equal and no alias is sent.
struct SettingText
{
std::string title; // the label the settings row draws
std::string full_label; // the descriptive ConfigOptionDef name when it differs from title: a
// search-only alias ("overhang reversal" still finds "Reverse on even")
std::string source; // the full settings path "Process : Quality : Layers". The JS renders
// group || source and searches every field, so putting the whole path in
// source both displays it and makes it matchable by any segment.
};
SettingText setting_text(const std::wstring& def_label, const std::string& display_label, const wxString& row_label, bool row_multi,
const std::string& type_name, const std::wstring& category, const std::wstring& group)
{
SettingText text;
const std::string def = boost::nowide::narrow(def_label);
text.title = into_u8(Search::resolve_setting_title(from_u8(display_label), row_label, row_multi));
if (text.title.empty())
text.title = def;
if (text.title != def)
text.full_label = def;
std::wstring path = boost::nowide::widen(type_name);
if (!category.empty())
path += L" : " + category;
if (!group.empty())
path += L" : " + group;
text.source = boost::nowide::narrow(path);
return text;
}
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
const char* mode_key(ConfigOptionMode mode)
{
@@ -305,8 +338,9 @@ struct PlateAction : AppAction
PlateAction(int index, std::string title, std::string source_name)
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index)
{
this->kind = AppActionKind::Command;
this->group = _u8L("Plate");
this->kind = AppActionKind::Command;
this->group_en = L("Plate");
this->group = I18N::translate_utf8(this->group_en);
}
AppActionRunResult run(const std::string& /*param*/) const override
@@ -328,8 +362,9 @@ struct RecentProjectAction : AppAction
RecentProjectAction(std::string path, std::string title, std::string source)
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
{
this->kind = AppActionKind::Command;
this->group = _u8L("Recent Projects");
this->kind = AppActionKind::Command;
this->group_en = L("Recent Projects");
this->group = I18N::translate_utf8(this->group_en);
}
AppActionRunResult run(const std::string& /*param*/) const override
@@ -342,6 +377,30 @@ struct RecentProjectAction : AppAction
} // namespace
nlohmann::json action_json(const AppAction& a)
{
nlohmann::json j({{"id", a.id()},
{"title", a.title()},
{"full_label", a.full_label},
{"source", a.source_name()},
{"group", a.group},
{"kind", a.kind == AppActionKind::Plugin ? "plugin" : "command"},
{"input", a.input},
{"icon", a.icon},
{"mode", mode_key(a.required_mode)},
{"desc", a.tooltip},
{"wiki", !a.help_url.empty()}});
auto add_alias = [&j](const char* key, const std::string& english, const std::string& local) {
if (!english.empty() && english != local)
j[key] = english;
};
add_alias("title_en", a.title_en, a.title());
add_alias("group_en", a.group_en, a.group);
add_alias("source_en", a.source_en, a.source_name());
add_alias("full_label_en", a.full_label_en, a.full_label);
return j;
}
ActionRegistry::~ActionRegistry() = default;
void ActionRegistry::init()
@@ -430,7 +489,8 @@ void ActionRegistry::relocalize_builtins()
// Drop only the built-in commands; plugins and the dynamically materialised families are
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
// re-seeds favourites/stats from config, so key-based ids keep their pinned state. make_action()
// translates in the new locale.
std::vector<std::string> stale;
for (const auto& [id, action] : m_actions)
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
@@ -438,7 +498,6 @@ void ActionRegistry::relocalize_builtins()
for (const std::string& id : stale)
remove(id);
NativeCommands::rebuild_catalog();
for (const NativeCommand& c : NativeCommands::catalog())
upsert(NativeCommands::make_action(c));
}
@@ -672,28 +731,22 @@ void ActionRegistry::materialize_setting_actions()
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
seen.insert(id);
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
// ("overhang reversal" still finds "Reverse on even").
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
if (title.empty())
title = search_label;
// The localized text the page shows, and its English search aliases from the untranslated row
// label, def name and breadcrumb.
const char* type_context = setting_type_context(opt.type);
SettingText local = setting_text(opt.label_local.empty() ? opt.label : opt.label_local, opt.display_label, row.label, row.multi,
I18N::translate_utf8(type_context), opt.category_local, opt.group_local);
SettingText english = setting_text(opt.label, opt.display_label_en, row.label_en, row.multi, type_context,
Tab::translate_category(opt.category, opt.type, false).ToStdWstring(), opt.group);
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
// renders group || source and searches source + " " + group, so putting the whole path in
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
if (!opt.category_local.empty())
path += L" : " + opt.category_local;
if (!opt.group_local.empty())
path += L" : " + opt.group_local;
// title = the label the settings row draws; group stays empty so the source path (above) is
// the single display/search breadcrumb rather than being duplicated.
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
boost::nowide::narrow(path), opt.mode);
if (title != search_label)
action->full_label = search_label;
// group stays empty so the source path is the single display/search breadcrumb rather than
// being duplicated.
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, local.title, std::string(), opt.category, local.source,
opt.mode);
action->full_label = std::move(local.full_label);
action->title_en = std::move(english.title);
action->full_label_en = std::move(english.full_label);
action->source_en = std::move(english.source);
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
// the one shown next to it in the page. Fall back to the page/category icon for groups
@@ -751,13 +804,14 @@ void ActionRegistry::materialize_plate_actions()
seen.insert(id);
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
std::string title(_u8L("Go to Plate"));
title += " " + std::to_string(i + 1);
const std::string name = plate->get_plate_name();
const char* go_to_plate = L("Go to Plate");
std::string suffix = " " + std::to_string(i + 1);
const std::string name = plate->get_plate_name();
if (!name.empty())
title += " (" + name + ")";
suffix += " (" + name + ")";
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
auto action = std::make_unique<PlateAction>(int(i), I18N::translate_utf8(go_to_plate) + suffix, kOrcaSourceName);
action->title_en = go_to_plate + suffix;
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
@@ -868,23 +922,9 @@ nlohmann::json ActionRegistry::snapshot()
return a->id() < b->id();
});
auto action_to_json = [](const AppAction* a) {
return nlohmann::json({{"id", a->id()},
{"title", a->title()},
{"full_label", a->full_label},
{"source", a->source_name()},
{"group", a->group},
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
{"input", a->input},
{"icon", a->icon},
{"mode", mode_key(a->required_mode)},
{"desc", a->tooltip},
{"wiki", !a->help_url.empty()}});
};
nlohmann::json actions = nlohmann::json::array();
for (const AppAction* a : sorted)
actions.push_back(action_to_json(a));
actions.push_back(action_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
@@ -917,7 +957,7 @@ nlohmann::json ActionRegistry::snapshot()
recent.resize(recent_limit);
nlohmann::json recent_json = nlohmann::json::array();
for (const AppAction* a : recent)
recent_json.push_back(action_to_json(a));
recent_json.push_back(action_json(*a));
return {{"actions", std::move(actions)},
{"favourites", std::move(favourites)},
@@ -937,14 +977,21 @@ nlohmann::json ActionRegistry::tab_options() const
MainFrame* mf = wxGetApp().mainframe;
if (!mf || !mf->m_tabpanel)
return out;
// English labels of the built-in pages whose id is not already their English name, so the tab
// picker can be searched in English in any UI language.
static const std::unordered_map<std::string, const char*> english_labels{
{TAB_ID_MONITOR, L("Device")}, {TAB_ID_MONITOR_WEB, L("Device (Web)")}, {TAB_ID_MULTI_DEVICE, L("Multi-device")}};
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->GetPageLabel(i).ToStdString()},
{"icon", notebook->GetPageIcon(i)}});
const std::string title = notebook->GetPageLabel(i).ToStdString();
nlohmann::json tab({{"id", id.ToStdString()}, {"title", title}, {"icon", notebook->GetPageIcon(i)}});
if (auto it = english_labels.find(id.ToStdString()); it != english_labels.end() && title != it->second)
tab["title_en"] = it->second;
out.push_back(std::move(tab));
}
return out;
}