mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Code refactoring for better maintainability
This commit is contained in:
@@ -478,6 +478,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SkipPartCanvas.hpp
|
||||
GUI/Search.cpp
|
||||
GUI/Search.hpp
|
||||
GUI/SettingsIndex.cpp
|
||||
GUI/SettingsIndex.hpp
|
||||
GUI/Selection.cpp
|
||||
GUI/Selection.hpp
|
||||
GUI/SelectMachine.cpp
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "Notebook.hpp"
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "Search.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
@@ -526,11 +526,11 @@ void ActionRegistry::materialize_setting_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories
|
||||
// map is populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
||||
// Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is
|
||||
// populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
||||
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
|
||||
// including those above the user's current mode, and can prompt to switch before jumping.
|
||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_modes_options();
|
||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().settings_index().all_options();
|
||||
|
||||
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
|
||||
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
|
||||
|
||||
@@ -248,7 +248,7 @@ void OptionsGroup::append_line(const Line& line)
|
||||
// affordance for it. Settings tabs only; the searcher already exists by the time tabs are built.
|
||||
if (m_use_custom_ctrl && !line.label_path.empty())
|
||||
for (const auto& opt : line.get_options())
|
||||
wxGetApp().sidebar().get_searcher().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path);
|
||||
wxGetApp().sidebar().settings_index().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path);
|
||||
|
||||
if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty()))
|
||||
return;
|
||||
@@ -656,7 +656,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
|
||||
m_opt_map.emplace(opt_id, pair);
|
||||
|
||||
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
|
||||
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
|
||||
wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
|
||||
|
||||
return Option(*m_config->def()->get(opt_key), opt_id);
|
||||
}
|
||||
|
||||
@@ -6419,6 +6419,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
|
||||
return p->searcher;
|
||||
}
|
||||
|
||||
Search::SettingsIndex& Sidebar::settings_index()
|
||||
{
|
||||
return p->searcher.index();
|
||||
}
|
||||
|
||||
std::string& Sidebar::get_search_line()
|
||||
{
|
||||
return p->searcher.search_string();
|
||||
|
||||
@@ -282,6 +282,7 @@ public:
|
||||
std::vector<std::string>& types,
|
||||
std::vector<size_t>* config_indices = nullptr);
|
||||
Search::OptionsSearcher& get_searcher();
|
||||
Search::SettingsIndex& settings_index();
|
||||
std::string& get_search_line();
|
||||
void update_printer_thumbnail();
|
||||
|
||||
|
||||
+6
-213
@@ -62,107 +62,12 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt)
|
||||
}
|
||||
}
|
||||
|
||||
std::string Option::opt_key() const { return into_u8(key).substr(2); }
|
||||
|
||||
void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const
|
||||
{
|
||||
*label_ = marked_label.c_str();
|
||||
*tooltip_ = tooltip.c_str();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
|
||||
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
|
||||
{
|
||||
T *opt_cur = static_cast<T *>(config->option(opt_key));
|
||||
cnt = opt_cur->values.size();
|
||||
return;
|
||||
|
||||
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
|
||||
}
|
||||
|
||||
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
|
||||
|
||||
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode, std::string tooltip) {
|
||||
const GroupAndCategory &gc = groups_and_categories[key];
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
|
||||
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
if (!label.IsEmpty())
|
||||
dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
|
||||
_(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(),
|
||||
false, opt_mode, std::move(tooltip), gc.path});
|
||||
};
|
||||
|
||||
for (std::string opt_key : config->keys()) {
|
||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||
const bool in_filtered = opt.mode <= mode;
|
||||
|
||||
int cnt = 0;
|
||||
|
||||
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
|
||||
switch (config->option(opt_key)->type()) {
|
||||
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
|
||||
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
|
||||
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
|
||||
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
|
||||
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
|
||||
// BBS
|
||||
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
|
||||
opt_key += "#0";
|
||||
|
||||
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
|
||||
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto add = [&](const std::string &k) {
|
||||
if (in_filtered)
|
||||
emplace(options, k, label, opt.mode, into_u8(_(opt.tooltip)));
|
||||
emplace(options_all_modes, k, label, opt.mode, into_u8(_(opt.tooltip)));
|
||||
};
|
||||
if (cnt == 0)
|
||||
add(key);
|
||||
else
|
||||
for (int i = 0; i < cnt; ++i)
|
||||
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
|
||||
add(key + "#" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsSearcher::sort_options()
|
||||
{
|
||||
// Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar
|
||||
// search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently
|
||||
// diverging in order or flags.
|
||||
auto sort_and_mark = [](std::vector<Option> &v) {
|
||||
std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
|
||||
Option *last = nullptr;
|
||||
for (auto &opt : v) {
|
||||
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
|
||||
last->multi_category = true;
|
||||
opt.multi_category = true;
|
||||
}
|
||||
last = &opt;
|
||||
}
|
||||
};
|
||||
sort_and_mark(options);
|
||||
sort_and_mark(options_all_modes);
|
||||
}
|
||||
|
||||
// Mark a string using ColorMarkerStart and ColorMarkerEnd symbols
|
||||
static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt)
|
||||
{
|
||||
@@ -247,7 +152,8 @@ bool OptionsSearcher::search(const std::string &search, bool force /* = false*/,
|
||||
return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local;
|
||||
};
|
||||
|
||||
std::vector<uint16_t> matches, matches2;
|
||||
std::vector<uint16_t> matches, matches2;
|
||||
const std::vector<Option> &options = m_index.options();
|
||||
for (size_t i = 0; i < options.size(); i++) {
|
||||
const Option &opt = options[i];
|
||||
if (full_list) {
|
||||
@@ -319,117 +225,21 @@ OptionsSearcher::~OptionsSearcher() {}
|
||||
|
||||
void OptionsSearcher::init(std::vector<InputInfo> input_values)
|
||||
{
|
||||
options.clear();
|
||||
options_all_modes.clear();
|
||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||
sort_options();
|
||||
m_index.init(std::move(input_values));
|
||||
|
||||
search(search_line, true, search_type);
|
||||
}
|
||||
|
||||
void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
// options_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means
|
||||
// both views are empty - the mode-filtered options can be empty while the all-modes view is not.
|
||||
if (options.empty() && options_all_modes.empty()) return;
|
||||
|
||||
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
|
||||
options_all_modes.erase(std::remove_if(options_all_modes.begin(), options_all_modes.end(), [type](Option opt) { return opt.type == type; }),
|
||||
options_all_modes.end());
|
||||
|
||||
append_options(config, type, mode);
|
||||
|
||||
sort_options();
|
||||
|
||||
search(search_line, true, search_type);
|
||||
if (m_index.apply(config, type, mode))
|
||||
search(search_line, true, search_type);
|
||||
}
|
||||
|
||||
const Option &OptionsSearcher::get_option(size_t pos_in_filter) const
|
||||
{
|
||||
assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1));
|
||||
return options[found[pos_in_filter].option_idx];
|
||||
}
|
||||
|
||||
const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
|
||||
{
|
||||
std::string opt_key2 = opt_key;
|
||||
if (auto n = opt_key.find('#'); n != std::string::npos) {
|
||||
variant_index = std::atoi(opt_key.c_str() + n + 1);
|
||||
opt_key2 = opt_key.substr(0, n);
|
||||
}
|
||||
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
// assert(it != options.end());
|
||||
if (it == options.end()) { variant_index = -2 ; return options[0]; }
|
||||
if (it->opt_key() == opt_key2) {
|
||||
variant_index = -1;
|
||||
} else {
|
||||
const std::string opt_key3 = opt_key2 + "#";
|
||||
it = std::lower_bound(it, options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))}));
|
||||
if (it == options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) {
|
||||
variant_index = -2; // Not found
|
||||
return options[0];
|
||||
}
|
||||
auto it2 = it;
|
||||
++it2;
|
||||
if (it2 != options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
|
||||
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
|
||||
variant_index = -2;
|
||||
}
|
||||
|
||||
return options[it - options.begin()];
|
||||
}
|
||||
|
||||
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
|
||||
{
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = opt_key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = opt_key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
wxString category = gc.category;
|
||||
if (type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
|
||||
std::string opt_idx = opt_key.substr(opt_key.find("#") + 1);
|
||||
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
|
||||
}
|
||||
|
||||
return Option{boost::nowide::widen(get_key(opt_key, type)),
|
||||
type,
|
||||
(label + suffix).ToStdWstring(),
|
||||
(_(label) + suffix_local).ToStdWstring(),
|
||||
gc.group.ToStdWstring(),
|
||||
_(gc.group).ToStdWstring(),
|
||||
into_u8(gc.icon),
|
||||
gc.category.ToStdWstring(),
|
||||
GUI::Tab::translate_category(category, type).ToStdWstring()};
|
||||
}
|
||||
|
||||
Option OptionsSearcher::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
|
||||
{
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(key)}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
if (it == options.end()) return options[0];
|
||||
if (it->key == boost::nowide::widen(key)) return options[it - options.begin()];
|
||||
if (groups_and_categories.find(key) == groups_and_categories.end()) {
|
||||
size_t pos = key.find('#');
|
||||
if (pos == std::string::npos) return options[it - options.begin()];
|
||||
|
||||
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
|
||||
|
||||
if (groups_and_categories.find(zero_opt_key) == groups_and_categories.end()) return options[it - options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, groups_and_categories.at(zero_opt_key));
|
||||
}
|
||||
|
||||
const GroupAndCategory &gc = groups_and_categories.at(key);
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return options[it - options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, gc);
|
||||
return m_index.option_at(found[pos_in_filter].option_idx);
|
||||
}
|
||||
|
||||
void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn)
|
||||
@@ -456,23 +266,6 @@ void OptionsSearcher::dlg_msw_rescale()
|
||||
{
|
||||
if (search_dialog) search_dialog->msw_rescale();
|
||||
}
|
||||
|
||||
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
|
||||
{
|
||||
// Update fields in place so a page rebuild (get_option after set_path) doesn't drop the
|
||||
// previously recorded wiki path.
|
||||
GroupAndCategory &gc = groups_and_categories[get_key(opt_key, type)];
|
||||
gc.group = group;
|
||||
gc.category = category;
|
||||
gc.icon = icon;
|
||||
}
|
||||
|
||||
void OptionsSearcher::set_path(const std::string &opt_key, Preset::Type type, const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return;
|
||||
groups_and_categories[get_key(opt_key, type)].path = path;
|
||||
}
|
||||
//------------------------------------------
|
||||
// SearchItem
|
||||
//------------------------------------------
|
||||
|
||||
+10
-72
@@ -19,6 +19,7 @@
|
||||
#include "wxExtensions.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "Widgets/ScrolledWindow.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/PopupWindow.hpp"
|
||||
@@ -34,45 +35,6 @@ namespace Search {
|
||||
|
||||
class SearchDialog;
|
||||
|
||||
struct InputInfo
|
||||
{
|
||||
DynamicPrintConfig *config{nullptr};
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
ConfigOptionMode mode{comSimple};
|
||||
};
|
||||
|
||||
struct GroupAndCategory
|
||||
{
|
||||
wxString group;
|
||||
wxString category;
|
||||
wxString icon; // icon of the group's own header, or empty
|
||||
std::string path; // wiki path (Line::label_path) of the option's line, or empty
|
||||
};
|
||||
|
||||
struct Option
|
||||
{
|
||||
// bool operator<(const Option& other) const { return other.label > this->label; }
|
||||
bool operator<(const Option &other) const { return other.key > this->key; }
|
||||
|
||||
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
|
||||
// though for some languages (Chinese?) it may not work correctly.
|
||||
std::wstring key;
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
std::wstring label;
|
||||
std::wstring label_local;
|
||||
std::wstring group;
|
||||
std::wstring group_local;
|
||||
std::string group_icon; // SVG base name of the group's own header icon, or empty
|
||||
std::wstring category;
|
||||
std::wstring category_local;
|
||||
bool multi_category { false };
|
||||
ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt
|
||||
std::string tooltip; // localized ConfigOptionDef::tooltip, or empty
|
||||
std::string wiki_path; // Line::label_path for the option's row, or empty
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
|
||||
struct FoundOption
|
||||
{
|
||||
// UTF8 encoding, to be consumed by ImGUI by reference.
|
||||
@@ -96,28 +58,19 @@ struct OptionViewParameters
|
||||
|
||||
class OptionsSearcher
|
||||
{
|
||||
std::string search_line;
|
||||
Preset::Type search_type = Preset::TYPE_INVALID;
|
||||
SettingsIndex m_index;
|
||||
|
||||
std::map<std::string, GroupAndCategory> groups_and_categories;
|
||||
PrinterTechnology printer_technology;
|
||||
|
||||
std::vector<Option> options{};
|
||||
// Every option regardless of the current UI mode (Simple/Advanced/Expert/Developer), for the
|
||||
// Speed Dial. The sidebar search keeps using the mode-filtered `options`.
|
||||
std::vector<Option> options_all_modes{};
|
||||
std::string search_line;
|
||||
Preset::Type search_type = Preset::TYPE_INVALID;
|
||||
PrinterTechnology printer_technology;
|
||||
std::vector<FoundOption> found{};
|
||||
|
||||
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
void sort_options();
|
||||
void sort_found()
|
||||
{
|
||||
std::sort(found.begin(), found.end(),
|
||||
[](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); });
|
||||
};
|
||||
|
||||
size_t options_size() const { return options.size(); }
|
||||
size_t found_size() const { return found.size(); }
|
||||
|
||||
public:
|
||||
@@ -128,44 +81,29 @@ public:
|
||||
OptionsSearcher();
|
||||
~OptionsSearcher();
|
||||
|
||||
SettingsIndex & index() { return m_index; }
|
||||
const SettingsIndex &index() const { return m_index; }
|
||||
|
||||
// Rebuild the catalog and re-run the current query so the cached results track it.
|
||||
void init(std::vector<InputInfo> input_values);
|
||||
void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
bool search();
|
||||
bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID);
|
||||
|
||||
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
|
||||
const wxString &icon = wxEmptyString);
|
||||
|
||||
// Records the wiki path of an option's row (Line::label_path) so the Speed Dial can offer a
|
||||
// "open wiki" affordance. Empty paths are ignored.
|
||||
void set_path(const std::string &opt_key, Preset::Type type, const std::string &path);
|
||||
|
||||
size_t size() const { return found_size(); }
|
||||
|
||||
const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; }
|
||||
const Option & get_option(size_t pos_in_filter) const;
|
||||
const Option & get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
|
||||
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
|
||||
|
||||
const std::vector<FoundOption> &found_options() { return found; }
|
||||
const GroupAndCategory & get_group_and_category(const std::string &opt_key) { return groups_and_categories[opt_key]; }
|
||||
std::string & search_string() { return search_line; }
|
||||
|
||||
void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; }
|
||||
|
||||
void sort_options_by_key()
|
||||
{
|
||||
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
|
||||
}
|
||||
void sort_options_by_label() { sort_options(); }
|
||||
|
||||
void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn);
|
||||
void dlg_sys_color_changed();
|
||||
void dlg_msw_rescale();
|
||||
|
||||
// Every option across all UI modes (Developer included), regardless of the current mode.
|
||||
// Used by the Speed Dial so it can list settings the user would have to switch mode to edit.
|
||||
const std::vector<Option>& all_modes_options() const { return options_all_modes; }
|
||||
};
|
||||
|
||||
//------------------------------------------
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "SettingsIndex.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Tab.hpp"
|
||||
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using GUI::into_u8;
|
||||
|
||||
namespace Search {
|
||||
|
||||
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
|
||||
|
||||
std::string Option::opt_key() const { return into_u8(key).substr(2); }
|
||||
|
||||
template<class T>
|
||||
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
|
||||
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
|
||||
{
|
||||
T *opt_cur = static_cast<T *>(config->option(opt_key));
|
||||
cnt = opt_cur->values.size();
|
||||
return;
|
||||
|
||||
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
|
||||
}
|
||||
|
||||
// Single assembler for an indexed Option, shared by append_options() and create_option(), so a new
|
||||
// Option field is only wired up in one place.
|
||||
static Option make_option(const std::string &key, Preset::Type type, const wxString &label, const GroupAndCategory &gc,
|
||||
ConfigOptionMode mode, const std::string &tooltip, bool rewrite_extruder_category)
|
||||
{
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
wxString category = gc.category;
|
||||
if (rewrite_extruder_category && type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
|
||||
std::string opt_idx = key.substr(key.find("#") + 1);
|
||||
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
|
||||
}
|
||||
|
||||
return Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(),
|
||||
gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(),
|
||||
GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path};
|
||||
}
|
||||
|
||||
void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
for (std::string opt_key : config->keys()) {
|
||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||
const bool in_filtered = opt.mode <= mode;
|
||||
|
||||
int cnt = 0;
|
||||
|
||||
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
|
||||
switch (config->option(opt_key)->type()) {
|
||||
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
|
||||
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
|
||||
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
|
||||
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
|
||||
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
|
||||
// BBS
|
||||
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
|
||||
opt_key += "#0";
|
||||
|
||||
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
|
||||
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto add = [&](const std::string &k) {
|
||||
const GroupAndCategory &gc = m_groups_and_categories[k];
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty() || label.IsEmpty()) return;
|
||||
|
||||
const std::string tooltip = into_u8(_(opt.tooltip));
|
||||
if (in_filtered)
|
||||
m_options.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
|
||||
m_all_modes.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
|
||||
};
|
||||
if (cnt == 0)
|
||||
add(key);
|
||||
else
|
||||
for (int i = 0; i < cnt; ++i)
|
||||
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
|
||||
add(key + "#" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsIndex::sort_options()
|
||||
{
|
||||
// Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar
|
||||
// search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently
|
||||
// diverging in order or flags.
|
||||
auto sort_and_mark = [](std::vector<Option> &v) {
|
||||
std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
|
||||
Option *last = nullptr;
|
||||
for (auto &opt : v) {
|
||||
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
|
||||
last->multi_category = true;
|
||||
opt.multi_category = true;
|
||||
}
|
||||
last = &opt;
|
||||
}
|
||||
};
|
||||
sort_and_mark(m_options);
|
||||
sort_and_mark(m_all_modes);
|
||||
}
|
||||
|
||||
void SettingsIndex::init(std::vector<InputInfo> input_values)
|
||||
{
|
||||
m_options.clear();
|
||||
m_all_modes.clear();
|
||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||
sort_options();
|
||||
}
|
||||
|
||||
bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
// m_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means both
|
||||
// views are empty - the mode-filtered m_options can be empty while m_all_modes is not.
|
||||
if (m_options.empty() && m_all_modes.empty()) return false;
|
||||
|
||||
m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [type](Option opt) { return opt.type == type; }), m_options.end());
|
||||
m_all_modes.erase(std::remove_if(m_all_modes.begin(), m_all_modes.end(), [type](Option opt) { return opt.type == type; }), m_all_modes.end());
|
||||
|
||||
append_options(config, type, mode);
|
||||
|
||||
sort_options();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
|
||||
{
|
||||
std::string opt_key2 = opt_key;
|
||||
if (auto n = opt_key.find('#'); n != std::string::npos) {
|
||||
variant_index = std::atoi(opt_key.c_str() + n + 1);
|
||||
opt_key2 = opt_key.substr(0, n);
|
||||
}
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
// assert(it != options.end());
|
||||
if (it == m_options.end()) { variant_index = -2 ; return m_options[0]; }
|
||||
if (it->opt_key() == opt_key2) {
|
||||
variant_index = -1;
|
||||
} else {
|
||||
const std::string opt_key3 = opt_key2 + "#";
|
||||
it = std::lower_bound(it, m_options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))}));
|
||||
if (it == m_options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) {
|
||||
variant_index = -2; // Not found
|
||||
return m_options[0];
|
||||
}
|
||||
auto it2 = it;
|
||||
++it2;
|
||||
if (it2 != m_options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
|
||||
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
|
||||
variant_index = -2;
|
||||
}
|
||||
|
||||
return m_options[it - m_options.begin()];
|
||||
}
|
||||
|
||||
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
|
||||
{
|
||||
return make_option(get_key(opt_key, type), type, label, gc, comSimple, std::string(), true);
|
||||
}
|
||||
|
||||
Option SettingsIndex::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
|
||||
{
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(key)}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
if (it == m_options.end()) return m_options[0];
|
||||
if (it->key == boost::nowide::widen(key)) return m_options[it - m_options.begin()];
|
||||
if (m_groups_and_categories.find(key) == m_groups_and_categories.end()) {
|
||||
size_t pos = key.find('#');
|
||||
if (pos == std::string::npos) return m_options[it - m_options.begin()];
|
||||
|
||||
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
|
||||
|
||||
if (m_groups_and_categories.find(zero_opt_key) == m_groups_and_categories.end()) return m_options[it - m_options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, m_groups_and_categories.at(zero_opt_key));
|
||||
}
|
||||
|
||||
const GroupAndCategory &gc = m_groups_and_categories.at(key);
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return m_options[it - m_options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, gc);
|
||||
}
|
||||
|
||||
void SettingsIndex::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
|
||||
{
|
||||
// Update fields in place so a page rebuild (get_option after set_path) doesn't drop the
|
||||
// previously recorded wiki path.
|
||||
GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)];
|
||||
gc.group = group;
|
||||
gc.category = category;
|
||||
gc.icon = icon;
|
||||
}
|
||||
|
||||
void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return;
|
||||
m_groups_and_categories[get_key(opt_key, type)].path = path;
|
||||
}
|
||||
|
||||
} // namespace Search
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef slic3r_SettingsIndex_hpp_
|
||||
#define slic3r_SettingsIndex_hpp_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/string.h>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/Preset.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Search {
|
||||
|
||||
struct InputInfo
|
||||
{
|
||||
DynamicPrintConfig *config{nullptr};
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
ConfigOptionMode mode{comSimple};
|
||||
};
|
||||
|
||||
struct GroupAndCategory
|
||||
{
|
||||
wxString group;
|
||||
wxString category;
|
||||
wxString icon; // icon of the group's own header, or empty
|
||||
std::string path; // wiki path (Line::label_path) of the option's line, or empty
|
||||
};
|
||||
|
||||
struct Option
|
||||
{
|
||||
// bool operator<(const Option& other) const { return other.label > this->label; }
|
||||
bool operator<(const Option &other) const { return other.key > this->key; }
|
||||
|
||||
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
|
||||
// though for some languages (Chinese?) it may not work correctly.
|
||||
std::wstring key;
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
std::wstring label;
|
||||
std::wstring label_local;
|
||||
std::wstring group;
|
||||
std::wstring group_local;
|
||||
std::string group_icon; // SVG base name of the group's own header icon, or empty
|
||||
std::wstring category;
|
||||
std::wstring category_local;
|
||||
bool multi_category { false };
|
||||
ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt
|
||||
std::string tooltip; // localized ConfigOptionDef::tooltip, or empty
|
||||
std::string wiki_path; // Line::label_path for the option's row, or empty
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
|
||||
// Catalog of settings and their metadata. Owns the group/category registry populated by the
|
||||
// settings pages, plus two views of the options: the mode-filtered view the sidebar search
|
||||
// queries, and every option regardless of mode for the Speed Dial.
|
||||
class SettingsIndex
|
||||
{
|
||||
std::map<std::string, GroupAndCategory> m_groups_and_categories;
|
||||
|
||||
std::vector<Option> m_options; // mode-filtered view used by the sidebar search
|
||||
std::vector<Option> m_all_modes; // every option regardless of mode, for the Speed Dial
|
||||
|
||||
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
void sort_options();
|
||||
|
||||
public:
|
||||
void init(std::vector<InputInfo> input_values);
|
||||
// Rebuild the given type's options; returns false when the index was never initialised.
|
||||
bool apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
|
||||
const wxString &icon = wxEmptyString);
|
||||
void set_path(const std::string &opt_key, Preset::Type type, const std::string &path);
|
||||
|
||||
const std::vector<Option> &options() const { return m_options; }
|
||||
const std::vector<Option> &all_options() const { return m_all_modes; }
|
||||
const Option & option_at(size_t pos) const { return m_options[pos]; }
|
||||
|
||||
const Option &get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
|
||||
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
|
||||
|
||||
const GroupAndCategory &get_group_and_category(const std::string &opt_key) { return m_groups_and_categories[opt_key]; }
|
||||
|
||||
void sort_options_by_key()
|
||||
{
|
||||
std::sort(m_options.begin(), m_options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
|
||||
}
|
||||
void sort_options_by_label() { sort_options(); }
|
||||
};
|
||||
|
||||
} // namespace Search
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SettingsIndex_hpp_
|
||||
@@ -5049,8 +5049,8 @@ void TabPrinter::build_fff()
|
||||
}
|
||||
|
||||
// Register by hand so the UnsavedChanges dialog can render a row for it.
|
||||
wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title,
|
||||
optgroup->config_category(), optgroup->icon);
|
||||
wxGetApp().sidebar().settings_index().add_key("printer_agent", m_type, optgroup->title,
|
||||
optgroup->config_category(), optgroup->icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5761,11 +5761,11 @@ if (is_marlin_flavor)
|
||||
} else if (m_extruders_count_old == 1) {
|
||||
first_extruder_title = wxString::Format("Extruder %d", 1);
|
||||
}
|
||||
auto & searcher = wxGetApp().sidebar().get_searcher();
|
||||
auto & index = wxGetApp().sidebar().settings_index();
|
||||
for (auto &group : m_pages[n_before_extruders]->m_optgroups) {
|
||||
group->set_config_category_and_type(first_extruder_title, m_type);
|
||||
for (auto &opt : group->opt_map())
|
||||
searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon);
|
||||
index.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon);
|
||||
}
|
||||
|
||||
Thaw();
|
||||
@@ -7867,10 +7867,10 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent)
|
||||
}));
|
||||
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area");
|
||||
searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon);
|
||||
searcher.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon);
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
const Search::GroupAndCategory& gc = index.get_group_and_category("printable_area");
|
||||
index.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon);
|
||||
index.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon);
|
||||
}
|
||||
|
||||
return sizer;
|
||||
|
||||
@@ -1485,12 +1485,12 @@ std::string UnsavedChangesDialog::subreplace(std::string resource_str, std::stri
|
||||
|
||||
void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config, int from, int to)
|
||||
{
|
||||
Search::OptionsSearcher &searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex &index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
for (const std::string &opt_key : config->keys()) {
|
||||
int variant_index = -2;
|
||||
const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
|
||||
const Search::Option &option = index.get_option(opt_key, type, variant_index);
|
||||
auto category = option.category_local;
|
||||
auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key));
|
||||
std::string value_from = opt->vserialize()[from];
|
||||
@@ -1502,8 +1502,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
|
||||
|
||||
void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* presets_)
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
// list of the presets with unsaved changes
|
||||
std::vector<PresetCollection*> presets_list;
|
||||
@@ -1558,11 +1558,11 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
|
||||
for (const std::string& opt_key : dirty_options) {
|
||||
int variant_index = -2;
|
||||
const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
|
||||
const Search::Option &option = index.get_option(opt_key, type, variant_index);
|
||||
if (option.opt_key() != opt_key && variant_index < -1) {
|
||||
// When founded option isn't the correct one.
|
||||
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
|
||||
// because of they don't exist in searcher
|
||||
// because of they don't exist in the index
|
||||
continue;
|
||||
}
|
||||
auto category = option.category_local;
|
||||
@@ -1590,8 +1590,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
}
|
||||
}
|
||||
|
||||
// Revert sort of searcher back
|
||||
searcher.sort_options_by_label();
|
||||
// Revert sort of index back
|
||||
index.sort_options_by_label();
|
||||
}
|
||||
|
||||
void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
@@ -2043,8 +2043,8 @@ void DiffPresetDialog::update_bottom_info(wxString bottom_info)
|
||||
|
||||
void DiffPresetDialog::update_tree()
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
m_tree->Clear();
|
||||
wxString bottom_info = "";
|
||||
@@ -2124,14 +2124,14 @@ void DiffPresetDialog::update_tree()
|
||||
wxString right_val = get_string_value(opt_key, right_congig);
|
||||
|
||||
const std::string lookup_key = get_pure_opt_key(opt_key);
|
||||
Search::Option option = searcher.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
|
||||
Search::Option option = index.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
|
||||
if (get_pure_opt_key(option.opt_key()) != lookup_key)
|
||||
option = searcher.get_option(opt_key, get_full_label(opt_key, left_config), type);
|
||||
option = index.get_option(opt_key, get_full_label(opt_key, left_config), type);
|
||||
if (get_pure_opt_key(option.opt_key()) != lookup_key) {
|
||||
// When the found option is not the requested one.
|
||||
// This can happen for dirty_options such as:
|
||||
// "default_print_profile", "printer_model", "printer_settings_id",
|
||||
// because they do not exist in the searcher.
|
||||
// because they do not exist in the index.
|
||||
continue;
|
||||
}
|
||||
m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
|
||||
@@ -2155,8 +2155,8 @@ void DiffPresetDialog::update_tree()
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// Revert sort of searcher back
|
||||
searcher.sort_options_by_label();
|
||||
// Revert sort of index back
|
||||
index.sort_options_by_label();
|
||||
}
|
||||
|
||||
void DiffPresetDialog::on_dpi_changed(const wxRect&)
|
||||
|
||||
Reference in New Issue
Block a user