Preferences: notify_relese option with Choice type Field.
OptionsGroup:: Added possibility of the right alignment of the controls + Added separator line to Preferences
This commit is contained in:
parent
405a7e84d6
commit
ac6259e387
@ -122,9 +122,9 @@ void AppConfig::set_defaults()
|
|||||||
if (get("auto_toolbar_size").empty())
|
if (get("auto_toolbar_size").empty())
|
||||||
set("auto_toolbar_size", "100");
|
set("auto_toolbar_size", "100");
|
||||||
|
|
||||||
|
if (get("notify_release").empty())
|
||||||
|
set("notify_release", "all"); // or "none" or "release"
|
||||||
|
|
||||||
if (get("notify_testing_release").empty())
|
|
||||||
set("notify_testing_release", "1");
|
|
||||||
#if ENABLE_ENVIRONMENT_MAP
|
#if ENABLE_ENVIRONMENT_MAP
|
||||||
if (get("use_environment_map").empty())
|
if (get("use_environment_map").empty())
|
||||||
set("use_environment_map", "0");
|
set("use_environment_map", "0");
|
||||||
|
@ -919,7 +919,6 @@ bool GUI_App::on_init_inner()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Bind(EVT_SLIC3R_ALPHA_VERSION_ONLINE, [this](const wxCommandEvent& evt) {
|
Bind(EVT_SLIC3R_ALPHA_VERSION_ONLINE, [this](const wxCommandEvent& evt) {
|
||||||
//app_config->set("version_alpha_online", into_u8(evt.GetString()));
|
|
||||||
app_config->save();
|
app_config->save();
|
||||||
if (this->plater_ != nullptr && app_config->get("notify_testing_release") == "1") {
|
if (this->plater_ != nullptr && app_config->get("notify_testing_release") == "1") {
|
||||||
if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) {
|
if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) {
|
||||||
@ -928,7 +927,6 @@ bool GUI_App::on_init_inner()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Bind(EVT_SLIC3R_BETA_VERSION_ONLINE, [this](const wxCommandEvent& evt) {
|
Bind(EVT_SLIC3R_BETA_VERSION_ONLINE, [this](const wxCommandEvent& evt) {
|
||||||
//app_config->set("version_beta_online", into_u8(evt.GetString()));
|
|
||||||
app_config->save();
|
app_config->save();
|
||||||
if (this->plater_ != nullptr && app_config->get("notify_testing_release") == "1") {
|
if (this->plater_ != nullptr && app_config->get("notify_testing_release") == "1") {
|
||||||
if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) {
|
if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) {
|
||||||
|
@ -72,6 +72,11 @@ void OG_CustomCtrl::init_ctrl_lines()
|
|||||||
const std::vector<Line>& og_lines = opt_group->get_lines();
|
const std::vector<Line>& og_lines = opt_group->get_lines();
|
||||||
for (const Line& line : og_lines)
|
for (const Line& line : og_lines)
|
||||||
{
|
{
|
||||||
|
if (line.is_separator()) {
|
||||||
|
ctrl_lines.emplace_back(CtrlLine(0, this, line));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (line.full_width && (
|
if (line.full_width && (
|
||||||
// description line
|
// description line
|
||||||
line.widget != nullptr ||
|
line.widget != nullptr ||
|
||||||
@ -124,6 +129,15 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/)
|
|||||||
line_height = win_height;
|
line_height = win_height;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto correct_horiz_pos = [this](int& h_pos, Field* field) {
|
||||||
|
if (m_max_win_width > 0 && field->getWindow()) {
|
||||||
|
int win_width = field->getWindow()->GetSize().GetWidth();
|
||||||
|
if (dynamic_cast<CheckBox*>(field))
|
||||||
|
win_width *= 0.5;
|
||||||
|
h_pos += m_max_win_width - win_width;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (CtrlLine& ctrl_line : ctrl_lines) {
|
for (CtrlLine& ctrl_line : ctrl_lines) {
|
||||||
if (&ctrl_line.og_line == &line)
|
if (&ctrl_line.og_line == &line)
|
||||||
{
|
{
|
||||||
@ -160,6 +174,7 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/)
|
|||||||
h_pos += 3 * blinking_button_width;
|
h_pos += 3 * blinking_button_width;
|
||||||
Field* field = opt_group->get_field(option_set.front().opt_id);
|
Field* field = opt_group->get_field(option_set.front().opt_id);
|
||||||
correct_line_height(ctrl_line.height, field->getWindow());
|
correct_line_height(ctrl_line.height, field->getWindow());
|
||||||
|
correct_horiz_pos(h_pos, field);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,8 +204,10 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/)
|
|||||||
}
|
}
|
||||||
h_pos += (opt.opt.gui_type == ConfigOptionDef::GUIType::legend ? 1 : 3) * blinking_button_width;
|
h_pos += (opt.opt.gui_type == ConfigOptionDef::GUIType::legend ? 1 : 3) * blinking_button_width;
|
||||||
|
|
||||||
if (field == field_in)
|
if (field == field_in) {
|
||||||
|
correct_horiz_pos(h_pos, field);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
if (opt.opt.gui_type == ConfigOptionDef::GUIType::legend)
|
if (opt.opt.gui_type == ConfigOptionDef::GUIType::legend)
|
||||||
h_pos += 2 * blinking_button_width;
|
h_pos += 2 * blinking_button_width;
|
||||||
|
|
||||||
@ -361,6 +378,28 @@ void OG_CustomCtrl::correct_widgets_position(wxSizer* widget, const Line& line,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void OG_CustomCtrl::init_max_win_width()
|
||||||
|
{
|
||||||
|
if (opt_group->ctrl_horiz_alignment == wxALIGN_RIGHT && m_max_win_width == 0)
|
||||||
|
for (CtrlLine& line : ctrl_lines) {
|
||||||
|
if (int max_win_width = line.get_max_win_width();
|
||||||
|
m_max_win_width < max_win_width)
|
||||||
|
m_max_win_width = max_win_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OG_CustomCtrl::set_max_win_width(int max_win_width)
|
||||||
|
{
|
||||||
|
if (m_max_win_width == max_win_width)
|
||||||
|
return;
|
||||||
|
m_max_win_width = max_win_width;
|
||||||
|
for (CtrlLine& line : ctrl_lines)
|
||||||
|
line.correct_items_positions();
|
||||||
|
|
||||||
|
GetParent()->Layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void OG_CustomCtrl::msw_rescale()
|
void OG_CustomCtrl::msw_rescale()
|
||||||
{
|
{
|
||||||
#ifdef __WXOSX__
|
#ifdef __WXOSX__
|
||||||
@ -374,6 +413,8 @@ void OG_CustomCtrl::msw_rescale()
|
|||||||
m_bmp_mode_sz = create_scaled_bitmap("mode_simple", this, wxOSX ? 10 : 12).GetSize();
|
m_bmp_mode_sz = create_scaled_bitmap("mode_simple", this, wxOSX ? 10 : 12).GetSize();
|
||||||
m_bmp_blinking_sz = create_scaled_bitmap("search_blink", this).GetSize();
|
m_bmp_blinking_sz = create_scaled_bitmap("search_blink", this).GetSize();
|
||||||
|
|
||||||
|
m_max_win_width = 0;
|
||||||
|
|
||||||
wxCoord v_pos = 0;
|
wxCoord v_pos = 0;
|
||||||
for (CtrlLine& line : ctrl_lines) {
|
for (CtrlLine& line : ctrl_lines) {
|
||||||
line.msw_rescale();
|
line.msw_rescale();
|
||||||
@ -407,6 +448,21 @@ OG_CustomCtrl::CtrlLine::CtrlLine( wxCoord height,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int OG_CustomCtrl::CtrlLine::get_max_win_width()
|
||||||
|
{
|
||||||
|
int max_win_width = 0;
|
||||||
|
if (!draw_just_act_buttons) {
|
||||||
|
const std::vector<Option>& option_set = og_line.get_options();
|
||||||
|
for (auto opt : option_set) {
|
||||||
|
Field* field = ctrl->opt_group->get_field(opt.opt_id);
|
||||||
|
if (field && field->getWindow())
|
||||||
|
max_win_width = field->getWindow()->GetSize().GetWidth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max_win_width;
|
||||||
|
}
|
||||||
|
|
||||||
void OG_CustomCtrl::CtrlLine::correct_items_positions()
|
void OG_CustomCtrl::CtrlLine::correct_items_positions()
|
||||||
{
|
{
|
||||||
if (draw_just_act_buttons || !is_visible)
|
if (draw_just_act_buttons || !is_visible)
|
||||||
@ -447,6 +503,8 @@ void OG_CustomCtrl::CtrlLine::msw_rescale()
|
|||||||
|
|
||||||
void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode)
|
void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode)
|
||||||
{
|
{
|
||||||
|
if (og_line.is_separator())
|
||||||
|
return;
|
||||||
const std::vector<Option>& option_set = og_line.get_options();
|
const std::vector<Option>& option_set = og_line.get_options();
|
||||||
|
|
||||||
const ConfigOptionMode& line_mode = option_set.front().opt.mode;
|
const ConfigOptionMode& line_mode = option_set.front().opt.mode;
|
||||||
@ -480,8 +538,25 @@ void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode)
|
|||||||
correct_items_positions();
|
correct_items_positions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OG_CustomCtrl::CtrlLine::render_separator(wxDC& dc, wxCoord v_pos)
|
||||||
|
{
|
||||||
|
wxPoint begin(ctrl->m_h_gap, v_pos);
|
||||||
|
wxPoint end(ctrl->GetSize().GetWidth() - ctrl->m_h_gap, v_pos);
|
||||||
|
|
||||||
|
wxPen pen, old_pen = pen = dc.GetPen();
|
||||||
|
pen.SetColour(*wxLIGHT_GREY);
|
||||||
|
dc.SetPen(pen);
|
||||||
|
dc.DrawLine(begin, end);
|
||||||
|
dc.SetPen(old_pen);
|
||||||
|
}
|
||||||
|
|
||||||
void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos)
|
void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos)
|
||||||
{
|
{
|
||||||
|
if (is_separator()) {
|
||||||
|
render_separator(dc, v_pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Field* field = ctrl->opt_group->get_field(og_line.get_options().front().opt_id);
|
Field* field = ctrl->opt_group->get_field(og_line.get_options().front().opt_id);
|
||||||
|
|
||||||
bool suppress_hyperlinks = get_app_config()->get("suppress_hyperlinks") == "1";
|
bool suppress_hyperlinks = get_app_config()->get("suppress_hyperlinks") == "1";
|
||||||
|
@ -33,6 +33,8 @@ class OG_CustomCtrl :public wxPanel
|
|||||||
wxSize m_bmp_mode_sz;
|
wxSize m_bmp_mode_sz;
|
||||||
wxSize m_bmp_blinking_sz;
|
wxSize m_bmp_blinking_sz;
|
||||||
|
|
||||||
|
int m_max_win_width{0};
|
||||||
|
|
||||||
struct CtrlLine {
|
struct CtrlLine {
|
||||||
wxCoord height { wxDefaultCoord };
|
wxCoord height { wxDefaultCoord };
|
||||||
OG_CustomCtrl* ctrl { nullptr };
|
OG_CustomCtrl* ctrl { nullptr };
|
||||||
@ -50,16 +52,20 @@ class OG_CustomCtrl :public wxPanel
|
|||||||
bool draw_mode_bitmap = true);
|
bool draw_mode_bitmap = true);
|
||||||
~CtrlLine() { ctrl = nullptr; }
|
~CtrlLine() { ctrl = nullptr; }
|
||||||
|
|
||||||
|
int get_max_win_width();
|
||||||
void correct_items_positions();
|
void correct_items_positions();
|
||||||
void msw_rescale();
|
void msw_rescale();
|
||||||
void update_visibility(ConfigOptionMode mode);
|
void update_visibility(ConfigOptionMode mode);
|
||||||
|
|
||||||
|
void render_separator(wxDC& dc, wxCoord v_pos);
|
||||||
|
|
||||||
void render(wxDC& dc, wxCoord v_pos);
|
void render(wxDC& dc, wxCoord v_pos);
|
||||||
wxCoord draw_mode_bmp(wxDC& dc, wxCoord v_pos);
|
wxCoord draw_mode_bmp(wxDC& dc, wxCoord v_pos);
|
||||||
wxCoord draw_text (wxDC& dc, wxPoint pos, const wxString& text, const wxColour* color, int width, bool is_url = false);
|
wxCoord draw_text (wxDC& dc, wxPoint pos, const wxString& text, const wxColour* color, int width, bool is_url = false);
|
||||||
wxPoint draw_blinking_bmp(wxDC& dc, wxPoint pos, bool is_blinking);
|
wxPoint draw_blinking_bmp(wxDC& dc, wxPoint pos, bool is_blinking);
|
||||||
wxCoord draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmap& bmp_undo_to_sys, const wxBitmap& bmp_undo, bool is_blinking, size_t rect_id = 0);
|
wxCoord draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmap& bmp_undo_to_sys, const wxBitmap& bmp_undo, bool is_blinking, size_t rect_id = 0);
|
||||||
bool launch_browser() const;
|
bool launch_browser() const;
|
||||||
|
bool is_separator() const { return og_line.is_separator(); }
|
||||||
|
|
||||||
std::vector<wxRect> rects_undo_icon;
|
std::vector<wxRect> rects_undo_icon;
|
||||||
std::vector<wxRect> rects_undo_to_sys_icon;
|
std::vector<wxRect> rects_undo_to_sys_icon;
|
||||||
@ -86,6 +92,9 @@ public:
|
|||||||
bool update_visibility(ConfigOptionMode mode);
|
bool update_visibility(ConfigOptionMode mode);
|
||||||
void correct_window_position(wxWindow* win, const Line& line, Field* field = nullptr);
|
void correct_window_position(wxWindow* win, const Line& line, Field* field = nullptr);
|
||||||
void correct_widgets_position(wxSizer* widget, const Line& line, Field* field = nullptr);
|
void correct_widgets_position(wxSizer* widget, const Line& line, Field* field = nullptr);
|
||||||
|
void init_max_win_width();
|
||||||
|
void set_max_win_width(int max_win_width);
|
||||||
|
int get_max_win_width() { return m_max_win_width; }
|
||||||
|
|
||||||
void msw_rescale();
|
void msw_rescale();
|
||||||
void sys_color_changed();
|
void sys_color_changed();
|
||||||
|
@ -126,6 +126,12 @@ bool OptionsGroup::is_legend_line()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OptionsGroup::set_max_win_width(int max_win_width)
|
||||||
|
{
|
||||||
|
if (custom_ctrl)
|
||||||
|
custom_ctrl->set_max_win_width(max_win_width);
|
||||||
|
}
|
||||||
|
|
||||||
void OptionsGroup::show_field(const t_config_option_key& opt_key, bool show/* = true*/)
|
void OptionsGroup::show_field(const t_config_option_key& opt_key, bool show/* = true*/)
|
||||||
{
|
{
|
||||||
Field* field = get_field(opt_key);
|
Field* field = get_field(opt_key);
|
||||||
@ -185,8 +191,16 @@ void OptionsGroup::append_line(const Line& line)
|
|||||||
m_options_mode.push_back(option_set[0].opt.mode);
|
m_options_mode.push_back(option_set[0].opt.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OptionsGroup::append_separator()
|
||||||
|
{
|
||||||
|
m_lines.emplace_back(Line());
|
||||||
|
}
|
||||||
|
|
||||||
void OptionsGroup::activate_line(Line& line)
|
void OptionsGroup::activate_line(Line& line)
|
||||||
{
|
{
|
||||||
|
if (line.is_separator())
|
||||||
|
return;
|
||||||
|
|
||||||
m_use_custom_ctrl_as_parent = false;
|
m_use_custom_ctrl_as_parent = false;
|
||||||
|
|
||||||
if (line.full_width && (
|
if (line.full_width && (
|
||||||
@ -396,7 +410,7 @@ void OptionsGroup::activate_line(Line& line)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create all controls for the option group from the m_lines
|
// create all controls for the option group from the m_lines
|
||||||
bool OptionsGroup::activate(std::function<void()> throw_if_canceled)
|
bool OptionsGroup::activate(std::function<void()> throw_if_canceled/* = [](){}*/, int horiz_alignment/* = wxALIGN_LEFT*/)
|
||||||
{
|
{
|
||||||
if (sizer)//(!sizer->IsEmpty())
|
if (sizer)//(!sizer->IsEmpty())
|
||||||
return false;
|
return false;
|
||||||
@ -436,6 +450,10 @@ bool OptionsGroup::activate(std::function<void()> throw_if_canceled)
|
|||||||
throw_if_canceled();
|
throw_if_canceled();
|
||||||
activate_line(line);
|
activate_line(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctrl_horiz_alignment = horiz_alignment;
|
||||||
|
if (custom_ctrl)
|
||||||
|
custom_ctrl->init_max_win_width();
|
||||||
} catch (UIBuildCanceled&) {
|
} catch (UIBuildCanceled&) {
|
||||||
auto p = sizer;
|
auto p = sizer;
|
||||||
this->clear();
|
this->clear();
|
||||||
|
@ -49,6 +49,7 @@ using t_option = std::unique_ptr<Option>; //!
|
|||||||
|
|
||||||
/// Represents option lines
|
/// Represents option lines
|
||||||
class Line {
|
class Line {
|
||||||
|
bool m_is_separator{ false };
|
||||||
public:
|
public:
|
||||||
wxString label;
|
wxString label;
|
||||||
wxString label_tooltip;
|
wxString label_tooltip;
|
||||||
@ -71,6 +72,9 @@ public:
|
|||||||
}
|
}
|
||||||
Line(wxString label, wxString tooltip) :
|
Line(wxString label, wxString tooltip) :
|
||||||
label(_(label)), label_tooltip(_(tooltip)) {}
|
label(_(label)), label_tooltip(_(tooltip)) {}
|
||||||
|
Line() : m_is_separator(true) {}
|
||||||
|
|
||||||
|
bool is_separator() const { return m_is_separator; }
|
||||||
|
|
||||||
const std::vector<widget_t>& get_extra_widgets() const {return m_extra_widgets;}
|
const std::vector<widget_t>& get_extra_widgets() const {return m_extra_widgets;}
|
||||||
const std::vector<Option>& get_options() const { return m_options; }
|
const std::vector<Option>& get_options() const { return m_options; }
|
||||||
@ -95,6 +99,7 @@ public:
|
|||||||
size_t label_width = 20 ;// {200};
|
size_t label_width = 20 ;// {200};
|
||||||
wxSizer* sizer {nullptr};
|
wxSizer* sizer {nullptr};
|
||||||
OG_CustomCtrl* custom_ctrl{ nullptr };
|
OG_CustomCtrl* custom_ctrl{ nullptr };
|
||||||
|
int ctrl_horiz_alignment{ wxALIGN_LEFT};
|
||||||
column_t extra_column {nullptr};
|
column_t extra_column {nullptr};
|
||||||
t_change m_on_change { nullptr };
|
t_change m_on_change { nullptr };
|
||||||
// To be called when the field loses focus, to assign a new initial value to the field.
|
// To be called when the field loses focus, to assign a new initial value to the field.
|
||||||
@ -124,12 +129,13 @@ public:
|
|||||||
void activate_line(Line& line);
|
void activate_line(Line& line);
|
||||||
|
|
||||||
// create all controls for the option group from the m_lines
|
// create all controls for the option group from the m_lines
|
||||||
bool activate(std::function<void()> throw_if_canceled = [](){});
|
bool activate(std::function<void()> throw_if_canceled = [](){}, int horiz_alignment = wxALIGN_LEFT);
|
||||||
// delete all controls from the option group
|
// delete all controls from the option group
|
||||||
void clear(bool destroy_custom_ctrl = false);
|
void clear(bool destroy_custom_ctrl = false);
|
||||||
|
|
||||||
Line create_single_option_line(const Option& option, const wxString& path = wxEmptyString) const;
|
Line create_single_option_line(const Option& option, const wxString& path = wxEmptyString) const;
|
||||||
void append_single_option_line(const Option& option, const wxString& path = wxEmptyString) { append_line(create_single_option_line(option, path)); }
|
void append_single_option_line(const Option& option, const wxString& path = wxEmptyString) { append_line(create_single_option_line(option, path)); }
|
||||||
|
void append_separator();
|
||||||
|
|
||||||
// return a non-owning pointer reference
|
// return a non-owning pointer reference
|
||||||
inline Field* get_field(const t_config_option_key& id) const{
|
inline Field* get_field(const t_config_option_key& id) const{
|
||||||
@ -171,6 +177,9 @@ public:
|
|||||||
wxGridSizer* get_grid_sizer() { return m_grid_sizer; }
|
wxGridSizer* get_grid_sizer() { return m_grid_sizer; }
|
||||||
const std::vector<Line>& get_lines() { return m_lines; }
|
const std::vector<Line>& get_lines() { return m_lines; }
|
||||||
bool is_legend_line();
|
bool is_legend_line();
|
||||||
|
// if we have to set the same control alignment for different option groups,
|
||||||
|
// we have to set same max contrtol width to all of them
|
||||||
|
void set_max_win_width(int max_win_width);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::map<t_config_option_key, Option> m_options;
|
std::map<t_config_option_key, Option> m_options;
|
||||||
|
@ -8,8 +8,38 @@
|
|||||||
#include <wx/notebook.h>
|
#include <wx/notebook.h>
|
||||||
#include "Notebook.hpp"
|
#include "Notebook.hpp"
|
||||||
#include "ButtonsDescription.hpp"
|
#include "ButtonsDescription.hpp"
|
||||||
|
#include "OG_CustomCtrl.hpp"
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values& enum_keys_map)
|
||||||
|
{
|
||||||
|
t_config_enum_names names;
|
||||||
|
int cnt = 0;
|
||||||
|
for (const auto& kvp : enum_keys_map)
|
||||||
|
cnt = std::max(cnt, kvp.second);
|
||||||
|
cnt += 1;
|
||||||
|
names.assign(cnt, "");
|
||||||
|
for (const auto& kvp : enum_keys_map)
|
||||||
|
names[kvp.second] = kvp.first;
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NAME) \
|
||||||
|
static t_config_enum_names s_keys_names_##NAME = enum_names_from_keys_map(s_keys_map_##NAME); \
|
||||||
|
template<> const t_config_enum_values& ConfigOptionEnum<NAME>::get_enum_values() { return s_keys_map_##NAME; } \
|
||||||
|
template<> const t_config_enum_names& ConfigOptionEnum<NAME>::get_enum_names() { return s_keys_names_##NAME; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static const t_config_enum_values s_keys_map_NotifyReleaseMode = {
|
||||||
|
{"all", NotifyReleaseAll},
|
||||||
|
{"release", NotifyReleaseOnly},
|
||||||
|
{"none", NotifyReleaseNone},
|
||||||
|
};
|
||||||
|
|
||||||
|
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NotifyReleaseMode)
|
||||||
|
|
||||||
namespace GUI {
|
namespace GUI {
|
||||||
|
|
||||||
PreferencesDialog::PreferencesDialog(wxWindow* parent, int selected_tab) :
|
PreferencesDialog::PreferencesDialog(wxWindow* parent, int selected_tab) :
|
||||||
@ -39,7 +69,7 @@ static std::shared_ptr<ConfigOptionsGroup>create_options_tab(const wxString& tit
|
|||||||
|
|
||||||
static void activate_options_tab(std::shared_ptr<ConfigOptionsGroup> optgroup)
|
static void activate_options_tab(std::shared_ptr<ConfigOptionsGroup> optgroup)
|
||||||
{
|
{
|
||||||
optgroup->activate();
|
optgroup->activate([](){}, wxALIGN_RIGHT);
|
||||||
optgroup->update_visibility(comSimple);
|
optgroup->update_visibility(comSimple);
|
||||||
wxBoxSizer* sizer = static_cast<wxBoxSizer*>(static_cast<wxPanel*>(optgroup->parent())->GetSizer());
|
wxBoxSizer* sizer = static_cast<wxBoxSizer*>(static_cast<wxPanel*>(optgroup->parent())->GetSizer());
|
||||||
sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10);
|
sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10);
|
||||||
@ -116,6 +146,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "version_check");
|
option = Option(def, "version_check");
|
||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
|
|
||||||
|
m_optgroup_general->append_separator();
|
||||||
|
|
||||||
// Please keep in sync with ConfigWizard
|
// Please keep in sync with ConfigWizard
|
||||||
def.label = L("Export sources full pathnames to 3mf and amf");
|
def.label = L("Export sources full pathnames to 3mf and amf");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
@ -141,6 +173,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
|
m_optgroup_general->append_separator();
|
||||||
|
|
||||||
// Please keep in sync with ConfigWizard
|
// Please keep in sync with ConfigWizard
|
||||||
def.label = L("Update built-in Presets automatically");
|
def.label = L("Update built-in Presets automatically");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
@ -165,6 +199,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "show_incompatible_presets");
|
option = Option(def, "show_incompatible_presets");
|
||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
|
|
||||||
|
m_optgroup_general->append_separator();
|
||||||
|
|
||||||
def.label = L("Show drop project dialog");
|
def.label = L("Show drop project dialog");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
def.tooltip = L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load.");
|
def.tooltip = L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load.");
|
||||||
@ -172,7 +208,6 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "show_drop_project_dialog");
|
option = Option(def, "show_drop_project_dialog");
|
||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
|
|
||||||
|
|
||||||
#if __APPLE__
|
#if __APPLE__
|
||||||
def.label = L("Allow just a single PrusaSlicer instance");
|
def.label = L("Allow just a single PrusaSlicer instance");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
@ -186,6 +221,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "single_instance");
|
option = Option(def, "single_instance");
|
||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
|
|
||||||
|
m_optgroup_general->append_separator();
|
||||||
|
|
||||||
def.label = L("Ask for unsaved changes when closing application or loading new project");
|
def.label = L("Ask for unsaved changes when closing application or loading new project");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
def.tooltip = L("Always ask for unsaved changes, when: \n"
|
def.tooltip = L("Always ask for unsaved changes, when: \n"
|
||||||
@ -230,6 +267,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
m_optgroup_general->append_single_option_line(option);
|
m_optgroup_general->append_single_option_line(option);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
m_optgroup_general->append_separator();
|
||||||
|
|
||||||
// Show/Hide splash screen
|
// Show/Hide splash screen
|
||||||
def.label = L("Show splash screen");
|
def.label = L("Show splash screen");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
@ -291,7 +330,15 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
m_optgroup_gui->m_on_change = [this, tabs](t_config_option_key opt_key, boost::any value) {
|
m_optgroup_gui->m_on_change = [this, tabs](t_config_option_key opt_key, boost::any value) {
|
||||||
if (opt_key == "suppress_hyperlinks")
|
if (opt_key == "suppress_hyperlinks")
|
||||||
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
|
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
|
||||||
else
|
else if (opt_key == "notify_release") {
|
||||||
|
int val_int = boost::any_cast<int>(value);
|
||||||
|
for (const auto& item : s_keys_map_NotifyReleaseMode) {
|
||||||
|
if (item.second == val_int) {
|
||||||
|
m_values[opt_key] = item.first;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
|
m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
|
||||||
|
|
||||||
if (opt_key == "use_custom_toolbar_size") {
|
if (opt_key == "use_custom_toolbar_size") {
|
||||||
@ -300,6 +347,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
tabs->Layout();
|
tabs->Layout();
|
||||||
this->layout();
|
this->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
def.label = L("Sequential slider applied only to top layer");
|
def.label = L("Sequential slider applied only to top layer");
|
||||||
@ -362,6 +411,8 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
m_optgroup_gui->append_single_option_line(option);
|
m_optgroup_gui->append_single_option_line(option);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
m_optgroup_gui->append_separator();
|
||||||
|
|
||||||
def.label = L("Show \"Tip of the day\" notification after start");
|
def.label = L("Show \"Tip of the day\" notification after start");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
def.tooltip = L("If enabled, useful hints are displayed at startup.");
|
def.tooltip = L("If enabled, useful hints are displayed at startup.");
|
||||||
@ -369,6 +420,24 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "show_hints");
|
option = Option(def, "show_hints");
|
||||||
m_optgroup_gui->append_single_option_line(option);
|
m_optgroup_gui->append_single_option_line(option);
|
||||||
|
|
||||||
|
ConfigOptionDef def_enum;
|
||||||
|
def_enum.label = L("Notify about new releases");
|
||||||
|
def_enum.type = coEnum;
|
||||||
|
def_enum.tooltip = L("You will be notified about new release after startup acordingly: All = Regular release and alpha / beta releases. Release only = regular release.");
|
||||||
|
def_enum.enum_keys_map = &ConfigOptionEnum<NotifyReleaseMode>::get_enum_values();
|
||||||
|
def_enum.enum_values.push_back("all");
|
||||||
|
def_enum.enum_values.push_back("release");
|
||||||
|
def_enum.enum_values.push_back("none");
|
||||||
|
def_enum.enum_labels.push_back(L("All"));
|
||||||
|
def_enum.enum_labels.push_back(L("Release only"));
|
||||||
|
def_enum.enum_labels.push_back(L("None"));
|
||||||
|
def_enum.mode = comSimple;
|
||||||
|
def_enum.set_default_value(new ConfigOptionEnum<NotifyReleaseMode>(static_cast<NotifyReleaseMode>(s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")))));
|
||||||
|
option = Option(def_enum, "notify_release");
|
||||||
|
m_optgroup_gui->append_single_option_line(option);
|
||||||
|
|
||||||
|
m_optgroup_gui->append_separator();
|
||||||
|
|
||||||
def.label = L("Use custom size for toolbar icons");
|
def.label = L("Use custom size for toolbar icons");
|
||||||
def.type = coBool;
|
def.type = coBool;
|
||||||
def.tooltip = L("If enabled, you can change size of toolbar icons manually.");
|
def.tooltip = L("If enabled, you can change size of toolbar icons manually.");
|
||||||
@ -376,15 +445,12 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
option = Option(def, "use_custom_toolbar_size");
|
option = Option(def, "use_custom_toolbar_size");
|
||||||
m_optgroup_gui->append_single_option_line(option);
|
m_optgroup_gui->append_single_option_line(option);
|
||||||
|
|
||||||
def.label = L("Notify about testing releases");
|
|
||||||
def.type = coBool;
|
|
||||||
def.tooltip = L("If enabled, you will be notified about alpha / beta releases available for download.");
|
|
||||||
def.set_default_value(new ConfigOptionBool{ app_config->get("notify_testing_release") == "1" });
|
|
||||||
option = Option(def, "notify_testing_release");
|
|
||||||
m_optgroup_gui->append_single_option_line(option);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activate_options_tab(m_optgroup_gui);
|
activate_options_tab(m_optgroup_gui);
|
||||||
|
// set Field for notify_release to its value to activate the object
|
||||||
|
boost::any val = s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release"));
|
||||||
|
m_optgroup_gui->get_field("notify_release")->set_value(val, false);
|
||||||
|
|
||||||
if (is_editor) {
|
if (is_editor) {
|
||||||
create_icon_size_slider();
|
create_icon_size_slider();
|
||||||
@ -413,6 +479,9 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
}
|
}
|
||||||
#endif // ENABLE_ENVIRONMENT_MAP
|
#endif // ENABLE_ENVIRONMENT_MAP
|
||||||
|
|
||||||
|
// update alignment of the controls for all tabs
|
||||||
|
update_ctrls_alignment();
|
||||||
|
|
||||||
if (selected_tab < tabs->GetPageCount())
|
if (selected_tab < tabs->GetPageCount())
|
||||||
tabs->SetSelection(selected_tab);
|
tabs->SetSelection(selected_tab);
|
||||||
|
|
||||||
@ -432,6 +501,20 @@ void PreferencesDialog::build(size_t selected_tab)
|
|||||||
this->CenterOnParent();
|
this->CenterOnParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PreferencesDialog::update_ctrls_alignment()
|
||||||
|
{
|
||||||
|
int max_ctrl_width{ 0 };
|
||||||
|
std::initializer_list og_list = { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() };
|
||||||
|
for (auto og : og_list) {
|
||||||
|
if (int max = og->custom_ctrl->get_max_win_width();
|
||||||
|
max_ctrl_width < max)
|
||||||
|
max_ctrl_width = max;
|
||||||
|
}
|
||||||
|
if (max_ctrl_width)
|
||||||
|
for (auto og : og_list)
|
||||||
|
og->custom_ctrl->set_max_win_width(max_ctrl_width);
|
||||||
|
}
|
||||||
|
|
||||||
void PreferencesDialog::accept(wxEvent&)
|
void PreferencesDialog::accept(wxEvent&)
|
||||||
{
|
{
|
||||||
// if (m_values.find("no_defaults") != m_values.end()
|
// if (m_values.find("no_defaults") != m_values.end()
|
||||||
|
@ -10,6 +10,13 @@
|
|||||||
class wxColourPickerCtrl;
|
class wxColourPickerCtrl;
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
enum NotifyReleaseMode {
|
||||||
|
NotifyReleaseAll,
|
||||||
|
NotifyReleaseOnly,
|
||||||
|
NotifyReleaseNone
|
||||||
|
};
|
||||||
|
|
||||||
namespace GUI {
|
namespace GUI {
|
||||||
|
|
||||||
class ConfigOptionsGroup;
|
class ConfigOptionsGroup;
|
||||||
@ -39,6 +46,7 @@ public:
|
|||||||
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
|
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
|
||||||
bool recreate_GUI() const { return m_recreate_GUI; }
|
bool recreate_GUI() const { return m_recreate_GUI; }
|
||||||
void build(size_t selected_tab = 0);
|
void build(size_t selected_tab = 0);
|
||||||
|
void update_ctrls_alignment();
|
||||||
void accept(wxEvent&);
|
void accept(wxEvent&);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -168,6 +168,7 @@ struct PresetUpdater::priv
|
|||||||
bool get_file(const std::string &url, const fs::path &target_path) const;
|
bool get_file(const std::string &url, const fs::path &target_path) const;
|
||||||
void prune_tmps() const;
|
void prune_tmps() const;
|
||||||
void sync_version() const;
|
void sync_version() const;
|
||||||
|
void parse_version_string(const std::string& body) const;
|
||||||
void sync_config(const VendorMap vendors);
|
void sync_config(const VendorMap vendors);
|
||||||
|
|
||||||
void check_install_indices() const;
|
void check_install_indices() const;
|
||||||
@ -263,60 +264,68 @@ void PresetUpdater::priv::sync_version() const
|
|||||||
})
|
})
|
||||||
.on_complete([&](std::string body, unsigned /* http_status */) {
|
.on_complete([&](std::string body, unsigned /* http_status */) {
|
||||||
boost::trim(body);
|
boost::trim(body);
|
||||||
// release version
|
parse_version_string(body);
|
||||||
std::string version;
|
})
|
||||||
const auto first_nl_pos = body.find_first_of("\n\r");
|
.perform_sync();
|
||||||
if (first_nl_pos != std::string::npos)
|
}
|
||||||
version = body.substr(0,first_nl_pos);
|
|
||||||
else
|
// Parses version string obtained in sync_version() and sends events to UI thread.
|
||||||
version = body;
|
// Version string must contain release version on first line. Follows non-mandatory alpha / beta releases on following lines (alpha=2.0.0-alpha1).
|
||||||
if (! Semver::parse(version)) {
|
void PresetUpdater::priv::parse_version_string(const std::string& body) const
|
||||||
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
{
|
||||||
|
// release version
|
||||||
|
std::string version;
|
||||||
|
const auto first_nl_pos = body.find_first_of("\n\r");
|
||||||
|
if (first_nl_pos != std::string::npos)
|
||||||
|
version = body.substr(0, first_nl_pos);
|
||||||
|
else
|
||||||
|
version = body;
|
||||||
|
if (!Semver::parse(version)) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
||||||
|
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE);
|
||||||
|
evt->SetString(GUI::from_u8(version));
|
||||||
|
GUI::wxGetApp().QueueEvent(evt);
|
||||||
|
|
||||||
|
// alpha / beta version
|
||||||
|
size_t nexn_nl_pos = first_nl_pos;
|
||||||
|
while (nexn_nl_pos != std::string::npos && body.size() > nexn_nl_pos + 1) {
|
||||||
|
const auto last_nl_pos = nexn_nl_pos;
|
||||||
|
nexn_nl_pos = body.find_first_of("\n\r", last_nl_pos + 1);
|
||||||
|
std::string line;
|
||||||
|
if (nexn_nl_pos == std::string::npos)
|
||||||
|
line = body.substr(last_nl_pos + 1);
|
||||||
|
else
|
||||||
|
line = body.substr(last_nl_pos + 1, nexn_nl_pos - last_nl_pos - 1);
|
||||||
|
|
||||||
|
// alpha
|
||||||
|
if (line.substr(0, 6) == "alpha=") {
|
||||||
|
version = line.substr(6);
|
||||||
|
if (!Semver::parse(version)) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents for alpha release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version of alpha release: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
||||||
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE);
|
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_ALPHA_VERSION_ONLINE);
|
||||||
evt->SetString(GUI::from_u8(version));
|
evt->SetString(GUI::from_u8(version));
|
||||||
GUI::wxGetApp().QueueEvent(evt);
|
GUI::wxGetApp().QueueEvent(evt);
|
||||||
|
|
||||||
// alpha / beta version
|
// beta
|
||||||
size_t nexn_nl_pos = first_nl_pos;
|
}
|
||||||
while (nexn_nl_pos != std::string::npos && body.size() > nexn_nl_pos + 1) {
|
else if (line.substr(0, 5) == "beta=") {
|
||||||
const auto last_nl_pos = nexn_nl_pos;
|
version = line.substr(5);
|
||||||
nexn_nl_pos = body.find_first_of("\n\r", last_nl_pos + 1);
|
if (!Semver::parse(version)) {
|
||||||
std::string line;
|
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents for beta release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
||||||
if (nexn_nl_pos == std::string::npos)
|
return;
|
||||||
line = body.substr(last_nl_pos + 1);
|
|
||||||
else
|
|
||||||
line = body.substr(last_nl_pos + 1, nexn_nl_pos - last_nl_pos - 1);
|
|
||||||
|
|
||||||
// alpha
|
|
||||||
if (line.substr(0, 6) == "alpha=") {
|
|
||||||
version = line.substr(6);
|
|
||||||
if (!Semver::parse(version)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents for alpha release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version of alpha release: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
|
||||||
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_ALPHA_VERSION_ONLINE);
|
|
||||||
evt->SetString(GUI::from_u8(version));
|
|
||||||
GUI::wxGetApp().QueueEvent(evt);
|
|
||||||
|
|
||||||
// beta
|
|
||||||
} else if (line.substr(0, 5) == "beta=") {
|
|
||||||
version = line.substr(5);
|
|
||||||
if (!Semver::parse(version)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << format("Received invalid contents for beta release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version of beta release: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
|
||||||
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_BETA_VERSION_ONLINE);
|
|
||||||
evt->SetString(GUI::from_u8(version));
|
|
||||||
GUI::wxGetApp().QueueEvent(evt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
BOOST_LOG_TRIVIAL(info) << format("Got %1% online version of beta release: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version);
|
||||||
.perform_sync();
|
wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_BETA_VERSION_ONLINE);
|
||||||
|
evt->SetString(GUI::from_u8(version));
|
||||||
|
GUI::wxGetApp().QueueEvent(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
|
// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
|
||||||
|
Loading…
Reference in New Issue
Block a user