Cleanup: Use StringRef for UI search menu code
Also use Vector to store menu search items instead of a linked list. And extend the change into the autocomplete API slightly. The main benefit is to avoid measuring the length of strings over and over, but the code also gets simpler.
This commit is contained in:
@@ -85,7 +85,7 @@ void context_path_add_generic(Vector<ContextPathItem> &path,
|
||||
|
||||
void template_breadcrumbs(uiLayout &layout, Span<ContextPathItem> context_path);
|
||||
|
||||
void attribute_search_add_items(StringRefNull str,
|
||||
void attribute_search_add_items(StringRef str,
|
||||
bool can_create_attribute,
|
||||
Span<const nodes::geo_eval_log::GeometryAttributeInfo *> infos,
|
||||
uiSearchItems *items,
|
||||
|
||||
@@ -1730,7 +1730,7 @@ void UI_but_func_identity_compare_set(uiBut *but, uiButIdentityCompareFunc cmp_f
|
||||
* \return false if there is nothing to add.
|
||||
*/
|
||||
bool UI_search_item_add(uiSearchItems *items,
|
||||
const char *name,
|
||||
blender::StringRef name,
|
||||
void *poin,
|
||||
int iconid,
|
||||
int but_flag,
|
||||
@@ -1953,7 +1953,7 @@ struct AutoComplete;
|
||||
#define AUTOCOMPLETE_PARTIAL_MATCH 2
|
||||
|
||||
AutoComplete *UI_autocomplete_begin(const char *startname, size_t maxncpy);
|
||||
void UI_autocomplete_update_name(AutoComplete *autocpl, const char *name);
|
||||
void UI_autocomplete_update_name(AutoComplete *autocpl, blender::StringRef name);
|
||||
int UI_autocomplete_end(AutoComplete *autocpl, char *autoname);
|
||||
|
||||
/* Button drag-data (interface_drag.cc).
|
||||
|
||||
@@ -5107,7 +5107,7 @@ AutoComplete *UI_autocomplete_begin(const char *startname, size_t maxncpy)
|
||||
return autocpl;
|
||||
}
|
||||
|
||||
void UI_autocomplete_update_name(AutoComplete *autocpl, const char *name)
|
||||
void UI_autocomplete_update_name(AutoComplete *autocpl, const StringRef name)
|
||||
{
|
||||
char *truncate = autocpl->truncate;
|
||||
const char *startname = autocpl->startname;
|
||||
@@ -5124,7 +5124,7 @@ void UI_autocomplete_update_name(AutoComplete *autocpl, const char *name)
|
||||
autocpl->matches++;
|
||||
/* first match */
|
||||
if (truncate[0] == 0) {
|
||||
BLI_strncpy(truncate, name, autocpl->maxncpy);
|
||||
name.copy_utf8_truncated(truncate, autocpl->maxncpy);
|
||||
}
|
||||
else {
|
||||
/* remove from truncate what is not in bone->name */
|
||||
|
||||
@@ -1062,7 +1062,7 @@ static void ui_apply_but_funcs_after(bContext *C)
|
||||
after.opcontext,
|
||||
(after.opptr) ? &opptr : nullptr,
|
||||
nullptr,
|
||||
after.drawstr.c_str());
|
||||
after.drawstr);
|
||||
}
|
||||
|
||||
if (after.opptr) {
|
||||
@@ -4342,7 +4342,7 @@ static void ui_but_extra_operator_icon_apply(bContext *C, uiBut *but, uiButExtra
|
||||
op_icon->optype_params->opcontext,
|
||||
op_icon->optype_params->opptr,
|
||||
nullptr,
|
||||
nullptr);
|
||||
"");
|
||||
|
||||
/* Force recreation of extra operator icons (pseudo update). */
|
||||
ui_but_extra_operator_icons_free(but);
|
||||
|
||||
@@ -396,7 +396,7 @@ static bool add_collection_search_item(CollItemSearch &cis,
|
||||
}
|
||||
|
||||
return UI_search_item_add(items,
|
||||
cis.name.c_str(),
|
||||
cis.name,
|
||||
cis.data,
|
||||
cis.iconid,
|
||||
cis.has_sep_char ? int(UI_BUT_HAS_SEP_CHAR) : 0,
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
#include "interface_intern.hh"
|
||||
#include "interface_regions_intern.hh"
|
||||
|
||||
using blender::StringRef;
|
||||
|
||||
#define MENU_BORDER int(0.3f * U.widget_unit)
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
@@ -96,7 +98,7 @@ struct uiSearchboxData {
|
||||
#define SEARCH_ITEMS 10
|
||||
|
||||
bool UI_search_item_add(uiSearchItems *items,
|
||||
const char *name,
|
||||
const StringRef name,
|
||||
void *poin,
|
||||
int iconid,
|
||||
const int but_flag,
|
||||
@@ -104,7 +106,7 @@ bool UI_search_item_add(uiSearchItems *items,
|
||||
{
|
||||
/* hijack for autocomplete */
|
||||
if (items->autocpl) {
|
||||
UI_autocomplete_update_name(items->autocpl, name + name_prefix_offset);
|
||||
UI_autocomplete_update_name(items->autocpl, name.drop_prefix(name_prefix_offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,7 +135,7 @@ bool UI_search_item_add(uiSearchItems *items,
|
||||
}
|
||||
|
||||
if (items->names) {
|
||||
BLI_strncpy(items->names[items->totitem], name, items->maxstrlen);
|
||||
name.copy_utf8_truncated(items->names[items->totitem], items->maxstrlen);
|
||||
}
|
||||
if (items->pointers) {
|
||||
items->pointers[items->totitem] = poin;
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "UI_resources.hh"
|
||||
#include "UI_string_search.hh"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
using blender::nodes::geo_eval_log::GeometryAttributeInfo;
|
||||
|
||||
namespace blender::ui {
|
||||
@@ -43,16 +45,15 @@ static StringRef attribute_domain_string(const bke::AttrDomain domain)
|
||||
|
||||
static bool attribute_search_item_add(uiSearchItems *items, const GeometryAttributeInfo &item)
|
||||
{
|
||||
const StringRef data_type_name = attribute_data_type_string(*item.data_type);
|
||||
const StringRef domain_name = attribute_domain_string(*item.domain);
|
||||
std::string search_item_text = domain_name + " " + UI_MENU_ARROW_SEP + item.name + UI_SEP_CHAR +
|
||||
data_type_name;
|
||||
|
||||
std::string search_item_text = fmt::format("{} " UI_MENU_ARROW_SEP "{}" UI_SEP_CHAR_S "{}",
|
||||
attribute_domain_string(*item.domain),
|
||||
item.name,
|
||||
attribute_data_type_string(*item.data_type));
|
||||
return UI_search_item_add(
|
||||
items, search_item_text.c_str(), (void *)&item, ICON_NONE, UI_BUT_HAS_SEP_CHAR, 0);
|
||||
items, search_item_text, (void *)&item, ICON_NONE, UI_BUT_HAS_SEP_CHAR, 0);
|
||||
}
|
||||
|
||||
void attribute_search_add_items(StringRefNull str,
|
||||
void attribute_search_add_items(StringRef str,
|
||||
const bool can_create_attribute,
|
||||
Span<const GeometryAttributeInfo *> infos,
|
||||
uiSearchItems *seach_items,
|
||||
@@ -71,12 +72,8 @@ void attribute_search_add_items(StringRefNull str,
|
||||
}
|
||||
if (!contained) {
|
||||
dummy_info.name = str;
|
||||
UI_search_item_add(seach_items,
|
||||
str.c_str(),
|
||||
&dummy_info,
|
||||
can_create_attribute ? ICON_ADD : ICON_NONE,
|
||||
0,
|
||||
0);
|
||||
UI_search_item_add(
|
||||
seach_items, str, &dummy_info, can_create_attribute ? ICON_ADD : ICON_NONE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +81,12 @@ void attribute_search_add_items(StringRefNull str,
|
||||
/* Allow clearing the text field when the string is empty, but not on the first pass,
|
||||
* or opening an attribute field for the first time would show this search item. */
|
||||
dummy_info.name = str;
|
||||
UI_search_item_add(seach_items, str.c_str(), &dummy_info, ICON_X, 0, 0);
|
||||
UI_search_item_add(seach_items, str, &dummy_info, ICON_X, 0, 0);
|
||||
}
|
||||
|
||||
/* Don't filter when the menu is first opened, but still run the search
|
||||
* so the items are in the same order they will appear in while searching. */
|
||||
const char *string = is_first ? "" : str.c_str();
|
||||
const StringRef string = is_first ? "" : str;
|
||||
|
||||
ui::string_search::StringSearch<const GeometryAttributeInfo> search;
|
||||
for (const GeometryAttributeInfo *item : infos) {
|
||||
|
||||
@@ -74,16 +74,15 @@ struct MenuSearch_Context {
|
||||
|
||||
struct MenuSearch_Parent {
|
||||
MenuSearch_Parent *parent;
|
||||
const char *drawstr;
|
||||
StringRef drawstr;
|
||||
|
||||
/** Set while writing menu items only. */
|
||||
MenuSearch_Parent *temp_child;
|
||||
};
|
||||
|
||||
struct MenuSearch_Item {
|
||||
MenuSearch_Item *next = nullptr, *prev = nullptr;
|
||||
const char *drawstr = nullptr;
|
||||
const char *drawwstr_full = nullptr;
|
||||
StringRef drawstr;
|
||||
StringRef drawwstr_full;
|
||||
int icon = 0;
|
||||
int state = 0;
|
||||
float weight = 0.0f;
|
||||
@@ -120,7 +119,7 @@ struct MenuSearch_Item {
|
||||
|
||||
struct MenuSearch_Data {
|
||||
/** MenuSearch_Item */
|
||||
ListBase items;
|
||||
blender::Vector<std::reference_wrapper<MenuSearch_Item>> items;
|
||||
/** Use for all small allocations. */
|
||||
blender::ResourceScope scope;
|
||||
|
||||
@@ -131,11 +130,10 @@ struct MenuSearch_Data {
|
||||
} context_menu_data;
|
||||
};
|
||||
|
||||
static int menu_item_sort_by_drawstr_full(const void *menu_item_a_v, const void *menu_item_b_v)
|
||||
static bool menu_item_sort_by_drawstr_full(const MenuSearch_Item &menu_item_a,
|
||||
const MenuSearch_Item &menu_item_b)
|
||||
{
|
||||
const MenuSearch_Item *menu_item_a = (MenuSearch_Item *)menu_item_a_v;
|
||||
const MenuSearch_Item *menu_item_b = (MenuSearch_Item *)menu_item_b_v;
|
||||
return strcmp(menu_item_a->drawwstr_full, menu_item_b->drawwstr_full);
|
||||
return menu_item_a.drawwstr_full < menu_item_b.drawwstr_full;
|
||||
}
|
||||
|
||||
static bool menu_items_from_ui_create_item_from_button(MenuSearch_Data *data,
|
||||
@@ -226,10 +224,10 @@ static bool menu_items_from_ui_create_item_from_button(MenuSearch_Data *data,
|
||||
"" :
|
||||
StringRef(but->drawstr).drop_prefix(sep_index);
|
||||
std::string drawstr = std::string("(") + drawstr_override + ")" + drawstr_suffix;
|
||||
item->drawstr = scope.linear_allocator().copy_string(drawstr).c_str();
|
||||
item->drawstr = scope.linear_allocator().copy_string(drawstr);
|
||||
}
|
||||
else {
|
||||
item->drawstr = scope.linear_allocator().copy_string(but->drawstr).c_str();
|
||||
item->drawstr = scope.linear_allocator().copy_string(but->drawstr);
|
||||
}
|
||||
|
||||
item->icon = ui_but_icon(but);
|
||||
@@ -240,7 +238,7 @@ static bool menu_items_from_ui_create_item_from_button(MenuSearch_Data *data,
|
||||
item->wm_context = wm_context;
|
||||
item->menu_parent = menu_parent;
|
||||
|
||||
BLI_addtail(&data->items, item);
|
||||
data->items.append(*item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -363,7 +361,7 @@ static void menu_types_add_from_keymap_items(bContext *C,
|
||||
static void menu_items_from_all_operators(bContext *C, MenuSearch_Data *data)
|
||||
{
|
||||
/* Add to temporary list so we can sort them separately. */
|
||||
ListBase operator_items = {nullptr, nullptr};
|
||||
blender::Vector<std::reference_wrapper<MenuSearch_Item>> operator_items;
|
||||
|
||||
ResourceScope &scope = data->scope;
|
||||
for (wmOperatorType *ot : WM_operatortypes_registered_get()) {
|
||||
@@ -374,9 +372,9 @@ static void menu_items_from_all_operators(bContext *C, MenuSearch_Data *data)
|
||||
if (WM_operator_poll(C, ot)) {
|
||||
const char *ot_ui_name = CTX_IFACE_(ot->translation_context, ot->name);
|
||||
|
||||
MenuSearch_Item *item = &scope.construct<MenuSearch_Item>();
|
||||
item->data = MenuSearch_Item::OperatorData();
|
||||
auto &op_data = std::get<MenuSearch_Item::OperatorData>(item->data);
|
||||
MenuSearch_Item &item = scope.construct<MenuSearch_Item>();
|
||||
item.data = MenuSearch_Item::OperatorData();
|
||||
auto &op_data = std::get<MenuSearch_Item::OperatorData>(item.data);
|
||||
op_data.type = ot;
|
||||
op_data.opcontext = WM_OP_INVOKE_DEFAULT;
|
||||
op_data.context = nullptr;
|
||||
@@ -387,18 +385,18 @@ static void menu_items_from_all_operators(bContext *C, MenuSearch_Data *data)
|
||||
|
||||
SNPRINTF(uiname, "%s " UI_MENU_ARROW_SEP "%s", idname_as_py, ot_ui_name);
|
||||
|
||||
item->drawwstr_full = scope.linear_allocator().copy_string(uiname).c_str();
|
||||
item->drawstr = ot_ui_name;
|
||||
item.drawwstr_full = scope.linear_allocator().copy_string(uiname);
|
||||
item.drawstr = ot_ui_name;
|
||||
|
||||
item->wm_context = nullptr;
|
||||
item.wm_context = nullptr;
|
||||
|
||||
BLI_addtail(&operator_items, item);
|
||||
operator_items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
BLI_listbase_sort(&operator_items, menu_item_sort_by_drawstr_full);
|
||||
std::sort(operator_items.begin(), operator_items.end(), menu_item_sort_by_drawstr_full);
|
||||
|
||||
BLI_movelisttolist(&data->items, &operator_items);
|
||||
data->items.extend(data->items);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -724,9 +722,8 @@ static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
|
||||
}
|
||||
str_buf.append(StringRef(drawstr, drawstr_len));
|
||||
fmt::format_to(fmt::appender(str_buf), " ({})", drawstr_sep + 1);
|
||||
menu_parent->drawstr = scope.linear_allocator()
|
||||
.copy_string(StringRef(str_buf.data(), str_buf.size()))
|
||||
.c_str();
|
||||
menu_parent->drawstr = scope.linear_allocator().copy_string(
|
||||
StringRef(str_buf.data(), str_buf.size()));
|
||||
str_buf.clear();
|
||||
}
|
||||
else {
|
||||
@@ -737,7 +734,7 @@ static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
|
||||
drawstr_is_empty = true;
|
||||
}
|
||||
}
|
||||
menu_parent->drawstr = scope.linear_allocator().copy_string(drawstr).c_str();
|
||||
menu_parent->drawstr = scope.linear_allocator().copy_string(drawstr);
|
||||
}
|
||||
menu_parent->parent = current_menu.self_as_parent;
|
||||
|
||||
@@ -786,7 +783,7 @@ static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
|
||||
|
||||
if (poll_success) {
|
||||
MenuSearch_Parent *menu_parent = &scope.construct<MenuSearch_Parent>();
|
||||
menu_parent->drawstr = scope.linear_allocator().copy_string(but->drawstr).c_str();
|
||||
menu_parent->drawstr = scope.linear_allocator().copy_string(but->drawstr);
|
||||
menu_parent->parent = current_menu.self_as_parent;
|
||||
|
||||
LISTBASE_FOREACH (uiBut *, sub_but, &sub_block->buttons) {
|
||||
@@ -824,38 +821,38 @@ static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
|
||||
* that could be moved into the parent menu. */
|
||||
|
||||
/* Set names as full paths. */
|
||||
LISTBASE_FOREACH (MenuSearch_Item *, item, &data->items) {
|
||||
for (MenuSearch_Item &item : data->items) {
|
||||
BLI_assert(str_buf.size() == 0);
|
||||
|
||||
if (include_all_areas) {
|
||||
fmt::format_to(fmt::appender(str_buf),
|
||||
"{}: ",
|
||||
(item->wm_context != nullptr) ?
|
||||
space_type_ui_items[item->wm_context->space_type_ui_index].name :
|
||||
(item.wm_context != nullptr) ?
|
||||
space_type_ui_items[item.wm_context->space_type_ui_index].name :
|
||||
global_menu_prefix);
|
||||
}
|
||||
|
||||
if (item->menu_parent != nullptr) {
|
||||
MenuSearch_Parent *menu_parent = item->menu_parent;
|
||||
if (item.menu_parent != nullptr) {
|
||||
MenuSearch_Parent *menu_parent = item.menu_parent;
|
||||
menu_parent->temp_child = nullptr;
|
||||
while (menu_parent && menu_parent->parent) {
|
||||
menu_parent->parent->temp_child = menu_parent;
|
||||
menu_parent = menu_parent->parent;
|
||||
}
|
||||
while (menu_parent) {
|
||||
str_buf.append(StringRef(menu_parent->drawstr));
|
||||
str_buf.append(menu_parent->drawstr);
|
||||
str_buf.append(StringRef(" " UI_MENU_ARROW_SEP " "));
|
||||
menu_parent = menu_parent->temp_child;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const char *drawstr = menu_display_name_map.lookup_default(item->mt, nullptr);
|
||||
const char *drawstr = menu_display_name_map.lookup_default(item.mt, nullptr);
|
||||
if (drawstr == nullptr) {
|
||||
drawstr = CTX_IFACE_(item->mt->translation_context, item->mt->label);
|
||||
drawstr = CTX_IFACE_(item.mt->translation_context, item.mt->label);
|
||||
}
|
||||
str_buf.append(StringRef(drawstr));
|
||||
|
||||
wmKeyMapItem *kmi = menu_to_kmi.lookup_default(item->mt, nullptr);
|
||||
wmKeyMapItem *kmi = menu_to_kmi.lookup_default(item.mt, nullptr);
|
||||
if (kmi != nullptr) {
|
||||
std::string kmi_str = WM_keymap_item_to_string(kmi, false).value_or("");
|
||||
fmt::format_to(fmt::appender(str_buf), " ({})", kmi_str);
|
||||
@@ -864,17 +861,17 @@ static MenuSearch_Data *menu_items_from_ui_create(bContext *C,
|
||||
str_buf.append(StringRef(" " UI_MENU_ARROW_SEP " "));
|
||||
}
|
||||
|
||||
str_buf.append(StringRef(item->drawstr));
|
||||
str_buf.append(item.drawstr);
|
||||
|
||||
item->drawwstr_full =
|
||||
scope.linear_allocator().copy_string(StringRef(str_buf.data(), str_buf.size())).c_str();
|
||||
item.drawwstr_full = scope.linear_allocator().copy_string(
|
||||
StringRef(str_buf.data(), str_buf.size()));
|
||||
str_buf.clear();
|
||||
}
|
||||
|
||||
/* Finally sort menu items.
|
||||
*
|
||||
* NOTE: we might want to keep the in-menu order, for now sort all. */
|
||||
BLI_listbase_sort(&data->items, menu_item_sort_by_drawstr_full);
|
||||
std::sort(data->items.begin(), data->items.end(), menu_item_sort_by_drawstr_full);
|
||||
|
||||
if (include_all_areas) {
|
||||
CTX_wm_area_set(C, area_init);
|
||||
@@ -977,8 +974,8 @@ static void menu_search_update_fn(const bContext * /*C*/,
|
||||
|
||||
blender::ui::string_search::StringSearch<MenuSearch_Item> search;
|
||||
|
||||
LISTBASE_FOREACH (MenuSearch_Item *, item, &data->items) {
|
||||
search.add(item->drawwstr_full, item, item->weight);
|
||||
for (MenuSearch_Item &item : data->items) {
|
||||
search.add(item.drawwstr_full, &item, item.weight);
|
||||
}
|
||||
|
||||
const blender::Vector<MenuSearch_Item *> filtered_items = search.query(str);
|
||||
|
||||
@@ -69,7 +69,7 @@ static void operator_search_update_fn(const bContext *C,
|
||||
name += *kmi_str;
|
||||
}
|
||||
|
||||
if (!UI_search_item_add(items, name.c_str(), ot, ICON_NONE, 0, 0)) {
|
||||
if (!UI_search_item_add(items, name, ot, ICON_NONE, 0, 0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ static void link_drag_search_update_fn(
|
||||
const Vector<SocketLinkOperation *> filtered_items = search.query(string);
|
||||
|
||||
for (SocketLinkOperation *item : filtered_items) {
|
||||
if (!UI_search_item_add(items, item->name.c_str(), item, ICON_NONE, 0, 0)) {
|
||||
if (!UI_search_item_add(items, item->name, item, ICON_NONE, 0, 0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -953,7 +953,7 @@ void WM_operator_name_call_ptr_with_depends_on_cursor(bContext *C,
|
||||
wmOperatorCallContext opcontext,
|
||||
PointerRNA *properties,
|
||||
const wmEvent *event,
|
||||
const char *drawstr);
|
||||
blender::StringRef drawstr);
|
||||
|
||||
/**
|
||||
* Similar to the function above except its uses ID properties used for key-maps and macros.
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
|
||||
#include "RE_pipeline.h"
|
||||
|
||||
using blender::StringRef;
|
||||
|
||||
/**
|
||||
* When a gizmo is highlighted and uses click/drag events,
|
||||
* this prevents mouse button press events from being passed through to other key-maps
|
||||
@@ -2047,7 +2049,7 @@ void WM_operator_name_call_ptr_with_depends_on_cursor(bContext *C,
|
||||
wmOperatorCallContext opcontext,
|
||||
PointerRNA *properties,
|
||||
const wmEvent *event,
|
||||
const char *drawstr)
|
||||
const StringRef drawstr)
|
||||
{
|
||||
bool depends_on_cursor = WM_operator_depends_on_cursor(*C, *ot, properties);
|
||||
|
||||
@@ -2071,16 +2073,15 @@ void WM_operator_name_call_ptr_with_depends_on_cursor(bContext *C,
|
||||
ScrArea *area = WM_OP_CONTEXT_HAS_AREA(opcontext) ? CTX_wm_area(C) : nullptr;
|
||||
|
||||
{
|
||||
char header_text[UI_MAX_DRAW_STR];
|
||||
SNPRINTF(header_text,
|
||||
"%s %s",
|
||||
IFACE_("Input pending "),
|
||||
(drawstr && drawstr[0]) ? drawstr : CTX_IFACE_(ot->translation_context, ot->name));
|
||||
std::string header_text = fmt::format(
|
||||
"{} {}",
|
||||
IFACE_("Input pending "),
|
||||
drawstr.is_empty() ? CTX_IFACE_(ot->translation_context, ot->name) : drawstr);
|
||||
if (area != nullptr) {
|
||||
ED_area_status_text(area, header_text);
|
||||
ED_area_status_text(area, header_text.c_str());
|
||||
}
|
||||
else {
|
||||
ED_workspace_status_text(C, header_text);
|
||||
ED_workspace_status_text(C, header_text.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user