Add an option to show "rich tooltip" instead of system tooltip for setting controls.

supermerill/SuperSlicer#1720
supermerill/SuperSlicer#1291
This commit is contained in:
supermerill 2021-10-27 23:13:32 +02:00
parent f44e05091c
commit b6f5dc40c5
5 changed files with 146 additions and 23 deletions

View File

@ -194,6 +194,15 @@ void AppConfig::set_defaults()
if (get("default_action_on_new_project").empty())
set("default_action_on_new_project", "1");
if (get("use_rich_tooltip").empty())
set("use_rich_tooltip",
#ifndef WIN32
"1"
#else
"0"
#endif
);
}
#if ENABLE_CUSTOMIZABLE_FILES_ASSOCIATION_ON_WIN
else {

View File

@ -14,6 +14,7 @@
#include <wx/numformatter.h>
#include <wx/tooltip.h>
#include <wx/notebook.h>
#include <wx/richtooltip.h>
#include <wx/tokenzr.h>
#include <boost/algorithm/string/predicate.hpp>
#include "OG_CustomCtrl.hpp"
@ -212,6 +213,77 @@ wxString Field::get_tooltip_text(const wxString& default_string)
return tooltip_text;
}
wxString Field::get_rich_tooltip_text(const wxString& default_string)
{
wxString tooltip_text("");
wxString tooltip = _(m_opt.tooltip);
update_Slic3r_string(tooltip);
std::string opt_id = m_opt_id;
auto hash_pos = opt_id.find("#");
if (hash_pos != std::string::npos) {
opt_id.replace(hash_pos, 1, "[");
opt_id += "]";
}
if (tooltip.length() > 0)
tooltip_text = tooltip + "\n" + _(L("default value")) + ": " +
(boost::iends_with(opt_id, "_gcode") ? "\n" : "") + default_string;
return tooltip_text;
}
wxString Field::get_rich_tooltip_title(const wxString& default_string)
{
std::string opt_id = m_opt_id;
auto hash_pos = opt_id.find("#");
if (hash_pos != std::string::npos) {
opt_id.replace(hash_pos, 1, "[");
opt_id += "]";
}
return opt_id + ":";
}
void Field::set_tooltip(const wxString& default_string, wxWindow* window) {
if (window == nullptr)
window = getWindow();
if (get_app_config()->get("use_rich_tooltip") == "1") {
this->m_rich_tooltip_timer.m_value = default_string;
window->Bind(wxEVT_ENTER_WINDOW, [this, window](wxMouseEvent& event) {
if (wxGetActiveWindow() && !this->m_rich_tooltip_timer.IsRunning()) {
this->m_rich_tooltip_timer.m_current_window = window;
this->m_rich_tooltip_timer.m_is_rich_tooltip_ready = true;
this->m_rich_tooltip_timer.StartOnce(500);
}
});
window->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) {
this->m_rich_tooltip_timer.m_is_rich_tooltip_ready = false;
wxWindowList tipWindow = this->getWindow()->GetChildren();
if (tipWindow.size() > 0) {
wxWindow* tooltipWindow = tipWindow.GetLast()->GetData();
if (tooltipWindow && tooltipWindow == this->m_rich_tooltip_timer.m_current_rich_tooltip)
tooltipWindow->Hide();// DismissAndNotify();
}
});
}else
window->SetToolTip(get_tooltip_text(default_string));
}
void RichTooltipTimer::Notify() {
if (wxGetActiveWindow() && this->m_is_rich_tooltip_ready && m_current_window) {
this->m_current_rich_tooltip = nullptr;
wxRichToolTip richTooltip(
m_field->get_rich_tooltip_title(this->m_value),
m_field->get_rich_tooltip_text(this->m_value));
richTooltip.SetTimeout(120000, 0);
richTooltip.ShowFor(m_current_window);
wxWindowList tipWindow = m_current_window->GetChildren();
this->m_current_rich_tooltip = tipWindow.GetLast()->GetData();
}
}
bool Field::is_matched(const std::string& string, const std::string& pattern)
{
std::regex regex_pattern(pattern, std::regex_constants::icase); // use ::icase to make the matching case insensitive like /i in perl
@ -511,7 +583,7 @@ void TextCtrl::BUILD() {
m_last_meaningful_value = text_value;
const long style = m_opt.multiline ? wxTE_MULTILINE : wxTE_PROCESS_ENTER/*0*/;
auto temp = new wxTextCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, style);
wxTextCtrl* temp = new wxTextCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, style);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = (double)temp->GetSize().GetHeight()/m_em_unit;
temp->SetFont(m_opt.is_code ?
@ -526,8 +598,6 @@ void TextCtrl::BUILD() {
temp->OSXDisableAllSmartSubstitutions();
#endif // __WXOSX__
temp->SetToolTip(get_tooltip_text(text_value));
if (style == wxTE_PROCESS_ENTER) {
temp->Bind(wxEVT_TEXT_ENTER, ([this, temp](wxEvent& e)
{
@ -590,6 +660,8 @@ void TextCtrl::BUILD() {
*/
// recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
this->set_tooltip(text_value);
}
bool TextCtrl::value_was_changed()
@ -736,10 +808,10 @@ void CheckBox::BUILD() {
on_change_field();
}), temp->GetId());
temp->SetToolTip(get_tooltip_text(check_value ? "true" : "false"));
// recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
this->set_tooltip(check_value ? "true" : "false");
}
void CheckBox::set_value(const boost::any& value, bool change_event)
@ -899,11 +971,12 @@ void SpinCtrl::BUILD() {
}
#endif
}), temp->GetId());
temp->SetToolTip(get_tooltip_text(text_value));
// recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
//prblem: it has 2 window, with a child: the mouse enter event won't fire if in children!
this->set_tooltip(text_value);
}
void SpinCtrl::propagate_value()
@ -1057,7 +1130,7 @@ void Choice::BUILD() {
}), temp->GetId());
}
temp->SetToolTip(get_tooltip_text(temp->GetValue()));
this->set_tooltip(temp->GetValue());
}
void Choice::suppress_scroll()
@ -1475,9 +1548,9 @@ void ColourPicker::BUILD()
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId());
window->Bind(wxEVT_COLOURPICKER_CHANGED, ([this](wxCommandEvent e) { on_change_field(); }), window->GetId());
temp->SetToolTip(get_tooltip_text(clr.GetAsString()));
this->set_tooltip(clr.GetAsString());
}
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
@ -1588,8 +1661,8 @@ void PointCtrl::BUILD()
// // recast as a wxWindow to fit the calling convention
sizer = dynamic_cast<wxSizer*>(temp);
x_textctrl->SetToolTip(get_tooltip_text(X+", "+Y));
y_textctrl->SetToolTip(get_tooltip_text(X+", "+Y));
this->set_tooltip(X + ", " + Y, x_textctrl);
this->set_tooltip(X + ", " + Y, y_textctrl);
}
void PointCtrl::msw_rescale()
@ -1695,7 +1768,7 @@ void StaticText::BUILD()
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->SetToolTip(get_tooltip_text(legend));
this->set_tooltip(legend);
}
void StaticText::msw_rescale()

View File

@ -39,6 +39,21 @@ using t_back_to_init = std::function<void(const std::string&)>;
wxString double_to_string(double const value, const int max_precision = 6);
wxString get_points_string(const std::vector<Vec2d>& values);
class Field;
class RichTooltipTimer : public wxTimer
{
Field* m_field;
public:
bool m_is_rich_tooltip_ready = false;
wxWindow* m_current_rich_tooltip = nullptr;
wxString m_value;
wxWindow* m_window2 = nullptr; //for point
wxWindow* m_current_window = nullptr; //for point
RichTooltipTimer(Field* field) : m_field(field) {};
void Notify() override;
};
class Field {
protected:
// factory function to defer and enforce creation of derived type.
@ -110,11 +125,16 @@ public:
inline void toggle(bool en) { en ? enable() : disable(); }
virtual wxString get_tooltip_text(const wxString& default_string);
// hack via richtooltip that are also hacked
RichTooltipTimer m_rich_tooltip_timer;
virtual wxString get_rich_tooltip_text(const wxString& default_string);
virtual wxString get_rich_tooltip_title(const wxString& default_string);
void set_tooltip(const wxString& default_string, wxWindow* window = nullptr);
void field_changed() { on_change_field(); }
Field(const ConfigOptionDef& opt, const t_config_option_key& id) : m_opt(opt), m_opt_id(id) {};
Field(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : m_parent(parent), m_opt(opt), m_opt_id(id) {};
Field(const ConfigOptionDef& opt, const t_config_option_key& id) : m_opt(opt), m_opt_id(id), m_rich_tooltip_timer(this) {};
Field(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : m_parent(parent), m_opt(opt), m_opt_id(id), m_rich_tooltip_timer(this) {};
virtual ~Field();
/// If you don't know what you are getting back, check both methods for nullptr.

View File

@ -151,6 +151,7 @@ void PreferencesDialog::build()
def.set_default_value(new ConfigOptionBool{ app_config->get("no_defaults") == "1" });
option = Option(def, "no_defaults");
m_optgroups_general.back()->append_single_option_line(option);
m_values_need_restart.push_back("no_defaults");
def.label = L("Show incompatible print and filament presets");
def.type = coBool;
@ -263,6 +264,21 @@ void PreferencesDialog::build()
def.set_default_value(new ConfigOptionBool{ app_config->get("default_action_on_new_project") == "1" });
option = Option(def, "default_action_on_new_project");
m_optgroups_general.back()->append_single_option_line(option);
def.label = L("Use custom tooltip");
def.type = coBool;
def.tooltip = L("On some OS like MacOS or some Linux, tooltips can't stay on for a long time. This setting replaces native tooltips with custom dialogs to improve readability (only for settings)."
"\nNote that for the number controls, you need to hover the arrows to get the custom tooltip.");
def.set_default_value(new ConfigOptionBool{ app_config->has("use_rich_tooltip") ? app_config->get("use_rich_tooltip") == "1":
#if __APPLE__
true
#else
false
#endif
});
option = Option(def, "use_rich_tooltip");
m_optgroups_general.back()->append_single_option_line(option);
m_values_need_restart.push_back("use_rich_tooltip");
}
#if ENABLE_CUSTOMIZABLE_FILES_ASSOCIATION_ON_WIN
#ifdef _WIN32
@ -445,12 +461,12 @@ void PreferencesDialog::build()
def.label = L("Tab icon size");
def.type = coInt;
def.tooltip = L("Size of the tab icons, in pixels. Set to 0 to remove icons."
"\nYou have to restart the application before any change will be taken into account.");
def.tooltip = L("Size of the tab icons, in pixels. Set to 0 to remove icons.");
def.set_default_value(new ConfigOptionInt{ atoi(app_config->get("tab_icon_size").c_str()) });
option = Option(def, "tab_icon_size");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("tab_icon_size");
}
@ -465,54 +481,54 @@ void PreferencesDialog::build()
def.type = coString;
def.tooltip = _u8L("Very dark color, in the RGB hex format.")
+ " " + _u8L("Mainly used as background or dark text color.")
+ "\n" + _u8L("You have to restart the application before any change will be taken into account.")
+ "\n" + _u8L("Slic3r(yellow): ada230, PrusaSlicer(orange): c46737, SuperSlicer(blue): 0047c7");
def.set_default_value(new ConfigOptionString{ app_config->get("color_very_dark") });
option = Option(def, "color_very_dark");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("color_very_dark");
def.label = L("Dark gui color");
def.type = coString;
def.tooltip = _u8L("Dark color, in the RGB hex format.")
+ " " + _u8L("Mainly used as icon color.")
+ "\n" + _u8L("You have to restart the application before any change will be taken into account.")
+ "\n" + _u8L("Slic3r(yellow): cabe39, PrusaSlicer(orange): ed6b21, SuperSlicer(blue): 2172eb");
def.set_default_value(new ConfigOptionString{ app_config->get("color_dark") });
option = Option(def, "color_dark");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("color_dark");
def.label = L("Gui color");
def.type = coString;
def.tooltip = _u8L("Main color, in the RGB hex format.")
+ "\n" + _u8L("You have to restart the application before any change will be taken into account.")
+ " " + _u8L("Slic3r(yellow): eddc21, PrusaSlicer(orange): fd7e42, SuperSlicer(blue): 428dfd");
def.set_default_value(new ConfigOptionString{ app_config->get("color") });
option = Option(def, "color");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("color");
def.label = L("Light gui color");
def.type = coString;
def.tooltip = _u8L("Light color, in the RGB hex format.")
+ "\n" + _u8L("You have to restart the application before any change will be taken into account.")
+ " " + _u8L("Slic3r(yellow): ffee38, PrusaSlicer(orange): feac8b, SuperSlicer(blue): 8bb9fe");
def.set_default_value(new ConfigOptionString{ app_config->get("color_light") });
option = Option(def, "color_light");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("color_light");
def.label = L("Very light gui color");
def.type = coString;
def.tooltip = _u8L("Very light color, in the RGB hex format.")
+ " " + _u8L("Mainly used as light text color.")
+ "\n" + _u8L("You have to restart the application before any change will be taken into account.")
+ "\n" + _u8L("Slic3r(yellow): fef48b, PrusaSlicer(orange): ff7d38, SuperSlicer(blue): 428cff");
def.set_default_value(new ConfigOptionString{ app_config->get("color_very_light") });
option = Option(def, "color_very_light");
option.opt.width = 6;
m_optgroup_gui->append_single_option_line(option);
m_values_need_restart.push_back("color_very_light");
activate_options_tab(m_optgroup_gui);
@ -557,7 +573,11 @@ void PreferencesDialog::build()
void PreferencesDialog::accept()
{
if (m_values.find("no_defaults") != m_values.end()) {
bool need_restart = false;
for (auto key : m_values_need_restart)
if (m_values.find(key) != m_values.end())
need_restart = true;
if (need_restart) {
warning_catcher(this, wxString::Format(_L("You need to restart %s to make the changes effective."), SLIC3R_APP_NAME));
}

View File

@ -18,6 +18,7 @@ class ConfigOptionsGroup;
class PreferencesDialog : public DPIDialog
{
std::map<std::string, std::string> m_values;
std::vector<std::string> m_values_need_restart;
std::vector<std::shared_ptr<ConfigOptionsGroup>> m_optgroups_general;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_paths;
std::shared_ptr<ConfigOptionsGroup> m_optgroup_camera;