mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 03:11:19 +00:00
Support searching the Speed Dial by English names in any UI language (#16369)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user