mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-06 14:37:36 -06:00
Fix object search bar on macOS and Linux (#9473)
* Remove unused search code * Reimplement the object search bar (SoftFever/OrcaSlicer#7438) * Fix result list when search text is empty * Prevent infinite focus loop * Update layout on Linux * Fix focus on macOS --------- Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
parent
940800b059
commit
66830d2344
3 changed files with 111 additions and 183 deletions
|
@ -380,7 +380,6 @@ struct Sidebar::priv
|
|||
~priv();
|
||||
|
||||
void show_preset_comboboxes();
|
||||
void on_search_update();
|
||||
void jump_to_object(ObjectDataViewModelNode* item);
|
||||
void can_search();
|
||||
|
||||
|
@ -423,15 +422,6 @@ void Sidebar::priv::show_preset_comboboxes()
|
|||
scrolled->Refresh();
|
||||
}
|
||||
|
||||
void Sidebar::priv::on_search_update()
|
||||
{
|
||||
m_object_list->assembly_plate_object_name();
|
||||
|
||||
wxString search_text = m_search_item->GetTextCtrl()->GetValue();
|
||||
m_object_list->GetModel()->search_object(search_text);
|
||||
dia->update_list();
|
||||
}
|
||||
|
||||
void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item)
|
||||
{
|
||||
m_object_list->selected_object(item);
|
||||
|
@ -1108,23 +1098,21 @@ Sidebar::Sidebar(Plater *parent)
|
|||
text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically
|
||||
|
||||
text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) {
|
||||
this->p->on_search_update();
|
||||
if (p->dia->IsShown()) {
|
||||
e.Skip();
|
||||
return;
|
||||
}
|
||||
p->m_search_bar->SetBorderColor(wxColour("#009688"));
|
||||
wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0));
|
||||
#ifndef __WXGTK__
|
||||
pos.y += this->p->m_search_bar->GetRect().height;
|
||||
#else
|
||||
this->p->m_search_item->Enable(false);
|
||||
#endif
|
||||
p->dia->SetPosition(pos);
|
||||
p->dia->Popup();
|
||||
e.Skip(); // required to show caret
|
||||
});
|
||||
text_ctrl->Bind(wxEVT_COMMAND_TEXT_UPDATED, [this](wxCommandEvent&) {
|
||||
this->p->on_search_update();
|
||||
});
|
||||
text_ctrl->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) {
|
||||
p->dia->Dismiss();
|
||||
p->m_search_bar->SetBorderColor(wxColour("#CECECE"));
|
||||
p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when loose focus
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
auto search_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(1));
|
||||
|
@ -1134,6 +1122,13 @@ Sidebar::Sidebar(Plater *parent)
|
|||
search_sizer->Fit(p->m_search_bar);
|
||||
|
||||
p->m_object_list = new ObjectList(p->scrolled);
|
||||
p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) {
|
||||
#ifdef __WXGTK__
|
||||
this->p->m_search_item->Enable(true);
|
||||
#endif
|
||||
this->p->m_search_bar->SetBorderColor(wxColour("#CECECE"));
|
||||
this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close
|
||||
});
|
||||
|
||||
p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0);
|
||||
p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0);
|
||||
|
@ -1143,7 +1138,7 @@ Sidebar::Sidebar(Plater *parent)
|
|||
// Frequently Object Settings
|
||||
p->object_settings = new ObjectSettings(p->scrolled);
|
||||
|
||||
p->dia = new Search::SearchObjectDialog(p->m_object_list, text_ctrl);
|
||||
p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item);
|
||||
#if !NEW_OBJECT_SETTING
|
||||
p->object_settings->Hide();
|
||||
p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10);
|
||||
|
|
|
@ -523,7 +523,7 @@ void SearchItem::on_mouse_left_up(wxMouseEvent &evt)
|
|||
}
|
||||
|
||||
if (m_search_object_dialog) {
|
||||
m_search_object_dialog->Dismiss();
|
||||
m_search_object_dialog->Die();
|
||||
wxCommandEvent event(wxCUSTOMEVT_JUMP_TO_OBJECT);
|
||||
event.SetClientData(m_item);
|
||||
wxPostEvent(GUI::wxGetApp().plater(), event);
|
||||
|
@ -553,9 +553,7 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo
|
|||
|
||||
em = GUI::wxGetApp().em_unit();
|
||||
|
||||
m_text_color = wxColour(38, 46, 48);
|
||||
m_bg_colour = wxColour(255, 255, 255);
|
||||
m_hover_colour = wxColour(248, 248, 248);
|
||||
m_thumb_color = wxColour(196, 196, 196);
|
||||
|
||||
SetFont(GUI::wxGetApp().normal_font());
|
||||
|
@ -582,10 +580,8 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo
|
|||
search_line->SetFont(GUI::wxGetApp().bold_font());
|
||||
#endif
|
||||
|
||||
// default_string = _L("Enter a search term");
|
||||
search_line->Bind(wxEVT_TEXT, &SearchDialog::OnInputText, this);
|
||||
search_line->Bind(wxEVT_LEFT_UP, &SearchDialog::OnLeftUpInTextCtrl, this);
|
||||
search_line->Bind(wxEVT_KEY_DOWN, &SearchDialog::OnKeyDown, this);
|
||||
search_line2 = search_line->GetTextCtrl();
|
||||
|
||||
// scroll window
|
||||
|
@ -681,76 +677,21 @@ void SearchDialog::Die()
|
|||
wxPostEvent(search_line, event);
|
||||
}
|
||||
|
||||
void SearchDialog::ProcessSelection(wxDataViewItem selection)
|
||||
{
|
||||
if (!selection.IsOk()) return;
|
||||
// this->EndModal(wxID_CLOSE);
|
||||
|
||||
// If call GUI::wxGetApp().sidebar.jump_to_option() directly from here,
|
||||
// then mainframe will not have focus and found option will not be "active" (have cursor) as a result
|
||||
// SearchDialog have to be closed and have to lose a focus
|
||||
// and only after that jump_to_option() function can be called
|
||||
// So, post event to plater:
|
||||
wxCommandEvent event(wxCUSTOMEVT_JUMP_TO_OPTION);
|
||||
event.SetInt(search_list_model->GetRow(selection));
|
||||
wxPostEvent(GUI::wxGetApp().plater(), event);
|
||||
}
|
||||
|
||||
void SearchDialog::OnInputText(wxCommandEvent &)
|
||||
{
|
||||
search_line2->SetInsertionPointEnd();
|
||||
wxString input_string = search_line2->GetValue();
|
||||
if (input_string == default_string) input_string.Clear();
|
||||
if (input_string == wxEmptyString) input_string.Clear();
|
||||
searcher->search(into_u8(input_string), true, search_type);
|
||||
update_list();
|
||||
}
|
||||
|
||||
void SearchDialog::OnLeftUpInTextCtrl(wxEvent &event)
|
||||
{
|
||||
if (search_line2->GetValue() == default_string) search_line2->SetValue("");
|
||||
if (search_line2->GetValue() == wxEmptyString) search_line2->SetValue("");
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void SearchDialog::OnKeyDown(wxKeyEvent &event)
|
||||
{
|
||||
event.Skip();
|
||||
/* int key = event.GetKeyCode();
|
||||
|
||||
if (key == WXK_UP || key == WXK_DOWN)
|
||||
{
|
||||
search_list->SetFocus();
|
||||
|
||||
auto item = search_list->GetSelection();
|
||||
|
||||
if (item.IsOk()) {
|
||||
unsigned selection = search_list_model->GetRow(item);
|
||||
|
||||
if (key == WXK_UP && selection > 0)
|
||||
selection--;
|
||||
if (key == WXK_DOWN && selection < unsigned(search_list_model->GetCount() - 1))
|
||||
selection++;
|
||||
|
||||
prevent_list_events = true;
|
||||
search_list->Select(search_list_model->GetItem(selection));
|
||||
prevent_list_events = false;
|
||||
}
|
||||
}
|
||||
|
||||
else if (key == WXK_NUMPAD_ENTER || key == WXK_RETURN)
|
||||
ProcessSelection(search_list->GetSelection());
|
||||
else
|
||||
event.Skip();*/
|
||||
}
|
||||
|
||||
void SearchDialog::OnActivate(wxDataViewEvent &event) { ProcessSelection(event.GetItem()); }
|
||||
|
||||
void SearchDialog::OnSelect(wxDataViewEvent &event)
|
||||
{
|
||||
if (prevent_list_events) return;
|
||||
// if (wxGetMouseState().LeftIsDown())
|
||||
// ProcessSelection(search_list->GetSelection());
|
||||
}
|
||||
|
||||
void SearchDialog::update_list()
|
||||
{
|
||||
#ifndef __WXGTK__
|
||||
|
@ -787,77 +728,12 @@ void SearchDialog::update_list()
|
|||
#ifndef __WXGTK__
|
||||
Thaw();
|
||||
#endif
|
||||
|
||||
// Under OSX model->Clear invoke wxEVT_DATAVIEW_SELECTION_CHANGED, so
|
||||
// set prevent_list_events to true already here
|
||||
// prevent_list_events = true;
|
||||
// search_list_model->Clear();
|
||||
|
||||
/* const std::vector<FoundOption> &filters = searcher->found_options();
|
||||
for (const FoundOption &item : filters)
|
||||
search_list_model->Prepend(item.label);*/
|
||||
|
||||
// select first item, if search_list
|
||||
/*if (search_list_model->GetCount() > 0)
|
||||
search_list->Select(search_list_model->GetItem(0));
|
||||
prevent_list_events = false;*/
|
||||
// Refresh();
|
||||
}
|
||||
|
||||
void SearchDialog::OnCheck(wxCommandEvent &event)
|
||||
{
|
||||
OptionViewParameters ¶ms = searcher->view_params;
|
||||
params.category = check_category->GetValue();
|
||||
|
||||
searcher->search();
|
||||
update_list();
|
||||
}
|
||||
|
||||
void SearchDialog::OnMotion(wxMouseEvent &event)
|
||||
{
|
||||
wxDataViewItem item;
|
||||
wxWindow * win = this;
|
||||
|
||||
// search_list->HitTest(wxGetMousePosition() - win->GetScreenPosition(), item, col);
|
||||
// search_list->Select(item);
|
||||
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void SearchDialog::OnLeftDown(wxMouseEvent &event) { ProcessSelection(search_list->GetSelection()); }
|
||||
|
||||
void SearchDialog::msw_rescale()
|
||||
{
|
||||
/* const int &em = GUI::wxGetApp().em_unit();
|
||||
|
||||
search_list_model->msw_rescale();
|
||||
search_list->GetColumn(SearchListModel::colIcon )->SetWidth(3 * em);
|
||||
search_list->GetColumn(SearchListModel::colMarkedText)->SetWidth(45 * em);
|
||||
|
||||
msw_buttons_rescale(this, em, { wxID_CANCEL });
|
||||
|
||||
const wxSize& size = wxSize(40 * em, 30 * em);
|
||||
SetMinSize(size);
|
||||
|
||||
Fit();
|
||||
Refresh();*/
|
||||
}
|
||||
|
||||
// void SearchDialog::on_sys_color_changed()
|
||||
//{
|
||||
//#ifdef _WIN32
|
||||
// GUI::wxGetApp().UpdateAllStaticTextDarkUI(this);
|
||||
// GUI::wxGetApp().UpdateDarkUI(static_cast<wxButton*>(this->FindWindowById(wxID_CANCEL, this)), true);
|
||||
// for (wxWindow* win : std::vector<wxWindow*> {search_line, search_list, check_category, check_english})
|
||||
// if (win) GUI::wxGetApp().UpdateDarkUI(win);
|
||||
//#endif
|
||||
//
|
||||
// // msw_rescale updates just icons, so use it
|
||||
// search_list_model->msw_rescale();
|
||||
//
|
||||
// Refresh();
|
||||
//}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SearchListModel
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -906,15 +782,16 @@ void SearchListModel::GetValueByRow(wxVariant &variant, unsigned int row, unsign
|
|||
}
|
||||
}
|
||||
|
||||
SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent)
|
||||
: PopupWindow(parent, wxBORDER_NONE), m_object_list(object_list)
|
||||
SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input)
|
||||
: PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS), m_object_list(object_list)
|
||||
{
|
||||
search_line = input;
|
||||
|
||||
Freeze();
|
||||
SetBackgroundColour(wxColour(238, 238, 238));
|
||||
|
||||
em = GUI::wxGetApp().em_unit();
|
||||
|
||||
m_text_color = wxColour(38, 46, 48);
|
||||
m_bg_color = wxColour(255, 255, 255);
|
||||
m_thumb_color = wxColour(196, 196, 196);
|
||||
|
||||
|
@ -933,6 +810,19 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p
|
|||
m_client_panel = new wxPanel(m_border_panel, wxID_ANY, wxDefaultPosition, wxSize(POPUP_WIDTH * em, POPUP_HEIGHT * em), wxTAB_TRAVERSAL);
|
||||
m_client_panel->SetBackgroundColour(m_bg_color);
|
||||
|
||||
// search line
|
||||
#ifdef __WXGTK__
|
||||
search_line = new TextInput(m_client_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
|
||||
search_line->SetBackgroundColour(wxColour(238, 238, 238));
|
||||
search_line->SetForegroundColour(wxColour(43, 52, 54));
|
||||
search_line->SetFont(GUI::wxGetApp().bold_font());
|
||||
#endif
|
||||
|
||||
search_line->Bind(wxEVT_TEXT, &SearchObjectDialog::OnInputText, this);
|
||||
search_line->Bind(wxEVT_LEFT_UP, &SearchObjectDialog::OnLeftUpInTextCtrl, this);
|
||||
search_line2 = search_line->GetTextCtrl();
|
||||
|
||||
|
||||
// scroll window
|
||||
m_scrolledWindow = new ScrolledWindow(m_client_panel, wxID_ANY, wxDefaultPosition, wxSize(POPUP_WIDTH * em - (em + em / 2), POPUP_HEIGHT * em), wxVSCROLL, 6, 6);
|
||||
m_scrolledWindow->SetMarginColor(m_bg_color);
|
||||
|
@ -947,6 +837,10 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p
|
|||
m_listPanel->Fit();
|
||||
m_scrolledWindow->SetScrollbars(1, 1, 0, m_listPanel->GetSize().GetHeight());
|
||||
|
||||
#ifdef __WXGTK__
|
||||
m_sizer_body->Add(search_line, 0, wxEXPAND | wxALL, em / 2);
|
||||
search_line = input;
|
||||
#endif
|
||||
m_sizer_body->Add(m_scrolledWindow, 0, wxEXPAND | wxALL, em);
|
||||
|
||||
m_client_panel->SetSizer(m_sizer_body);
|
||||
|
@ -970,15 +864,70 @@ SearchObjectDialog::~SearchObjectDialog() {}
|
|||
|
||||
void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
|
||||
{
|
||||
update_list();
|
||||
if (m_is_dismissing || this->IsShown()) {
|
||||
return;
|
||||
}
|
||||
search_line2->SetValue(wxString(""));
|
||||
#ifdef __WXOSX__
|
||||
// On macOS we need to remove the focus from the text input before popping up the
|
||||
// dropdown list, otherwise the text input won't be usable
|
||||
m_object_list->SetFocus();
|
||||
#endif
|
||||
PopupWindow::Popup();
|
||||
search_line2->SetFocus();
|
||||
|
||||
m_object_list->assembly_plate_object_name();
|
||||
m_object_list->GetModel()->search_object(wxEmptyString);
|
||||
update_list();
|
||||
}
|
||||
|
||||
void SearchObjectDialog::MSWDismissUnfocusedPopup()
|
||||
{
|
||||
Dismiss();
|
||||
OnDismiss();
|
||||
}
|
||||
|
||||
void SearchObjectDialog::OnDismiss() {}
|
||||
|
||||
void SearchObjectDialog::Dismiss()
|
||||
{
|
||||
auto focus_window = this->GetParent()->HasFocus();
|
||||
auto pos = wxGetMousePosition();
|
||||
auto focus_window = wxWindow::FindFocus();
|
||||
if (!focus_window)
|
||||
PopupWindow::Dismiss();
|
||||
Die();
|
||||
else if (!search_line->GetScreenRect().Contains(pos) && !this->GetScreenRect().Contains(pos)) {
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
void SearchObjectDialog::Die()
|
||||
{
|
||||
m_is_dismissing = true;
|
||||
m_object_list->SetFocus();
|
||||
PopupWindow::Dismiss();
|
||||
wxCommandEvent event(wxCUSTOMEVT_EXIT_SEARCH);
|
||||
wxPostEvent(m_object_list, event);
|
||||
m_is_dismissing = false;
|
||||
}
|
||||
|
||||
void SearchObjectDialog::OnInputText(wxCommandEvent&)
|
||||
{
|
||||
search_line2->SetInsertionPointEnd();
|
||||
wxString input_string = search_line2->GetValue();
|
||||
if (input_string == wxEmptyString)
|
||||
input_string.Clear();
|
||||
|
||||
m_object_list->assembly_plate_object_name();
|
||||
m_object_list->GetModel()->search_object(input_string);
|
||||
|
||||
update_list();
|
||||
}
|
||||
|
||||
void SearchObjectDialog::OnLeftUpInTextCtrl(wxEvent& event)
|
||||
{
|
||||
if (search_line2->GetValue() == wxEmptyString)
|
||||
search_line2->SetValue("");
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void SearchObjectDialog::update_list()
|
||||
|
|
|
@ -184,25 +184,15 @@ class SearchListModel;
|
|||
class SearchDialog : public PopupWindow
|
||||
{
|
||||
public:
|
||||
wxString search_str;
|
||||
wxString default_string;
|
||||
|
||||
bool prevent_list_events{false};
|
||||
|
||||
wxColour m_text_color;
|
||||
wxColour m_bg_colour;
|
||||
wxColour m_hover_colour;
|
||||
wxColour m_bold_colour;
|
||||
wxColour m_thumb_color;
|
||||
|
||||
wxBoxSizer *m_sizer_body{nullptr};
|
||||
wxBoxSizer *m_sizer_main{nullptr};
|
||||
wxBoxSizer *m_sizer_border{nullptr};
|
||||
wxBoxSizer *m_listsizer{nullptr};
|
||||
|
||||
wxWindow *m_border_panel{nullptr};
|
||||
wxWindow *m_client_panel{nullptr};
|
||||
wxWindow *m_listPanel{nullptr};
|
||||
|
||||
wxWindow *m_event_tag{nullptr};
|
||||
wxWindow *m_search_item_tag{nullptr};
|
||||
|
@ -215,23 +205,12 @@ public:
|
|||
wxTextCtrl * search_line2{nullptr};
|
||||
Preset::Type search_type = Preset::TYPE_INVALID;
|
||||
|
||||
wxDataViewCtrl * search_list{nullptr};
|
||||
ScrolledWindow * m_scrolledWindow{nullptr};
|
||||
SearchListModel *search_list_model{nullptr};
|
||||
wxCheckBox * check_category{nullptr};
|
||||
|
||||
OptionsSearcher *searcher{nullptr};
|
||||
|
||||
void OnInputText(wxCommandEvent &event);
|
||||
void OnLeftUpInTextCtrl(wxEvent &event);
|
||||
void OnKeyDown(wxKeyEvent &event);
|
||||
|
||||
void OnActivate(wxDataViewEvent &event);
|
||||
void OnSelect(wxDataViewEvent &event);
|
||||
|
||||
void OnCheck(wxCommandEvent &event);
|
||||
void OnMotion(wxMouseEvent &event);
|
||||
void OnLeftDown(wxMouseEvent &event);
|
||||
|
||||
void update_list();
|
||||
|
||||
|
@ -244,12 +223,8 @@ public:
|
|||
void OnDismiss();
|
||||
void Dismiss();
|
||||
void Die();
|
||||
void ProcessSelection(wxDataViewItem selection);
|
||||
void msw_rescale();
|
||||
// void on_sys_color_changed() override;
|
||||
|
||||
protected:
|
||||
// void on_dpi_changed(const wxRect& suggested_rect) override { msw_rescale(); }
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -284,11 +259,17 @@ public:
|
|||
class SearchObjectDialog : public PopupWindow
|
||||
{
|
||||
public:
|
||||
SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent);
|
||||
SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input);
|
||||
~SearchObjectDialog();
|
||||
|
||||
void MSWDismissUnfocusedPopup();
|
||||
void Popup(wxPoint position = wxDefaultPosition);
|
||||
void OnDismiss();
|
||||
void Dismiss();
|
||||
void Die();
|
||||
|
||||
void OnInputText(wxCommandEvent& event);
|
||||
void OnLeftUpInTextCtrl(wxEvent& event);
|
||||
|
||||
void update_list();
|
||||
|
||||
|
@ -299,12 +280,13 @@ public:
|
|||
const int POPUP_WIDTH = 41;
|
||||
const int POPUP_HEIGHT = 45;
|
||||
|
||||
TextInput* search_line{nullptr};
|
||||
wxTextCtrl* search_line2{nullptr};
|
||||
|
||||
ScrolledWindow* m_scrolledWindow{ nullptr };
|
||||
|
||||
wxColour m_text_color;
|
||||
wxColour m_bg_color;
|
||||
wxColour m_thumb_color;
|
||||
wxColour m_bold_color;
|
||||
|
||||
wxBoxSizer* m_sizer_body{ nullptr };
|
||||
wxBoxSizer* m_sizer_main{ nullptr };
|
||||
|
@ -312,7 +294,9 @@ public:
|
|||
|
||||
wxWindow* m_border_panel{ nullptr };
|
||||
wxWindow* m_client_panel{ nullptr };
|
||||
wxWindow* m_listPanel{ nullptr };
|
||||
|
||||
private:
|
||||
bool m_is_dismissing{ false };
|
||||
};
|
||||
|
||||
} // namespace Search
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue