From f624c55f6f38667c2937e2163937185d0c0b67eb Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 24 Jul 2023 11:10:08 +0200 Subject: [PATCH 01/64] Store PrusaLink passwords in wxsecretstore --- CMakeLists.txt | 4 +- deps/wxWidgets/wxWidgets.cmake | 47 +++++++ src/libslic3r/Config.hpp | 2 + src/libslic3r/PresetBundle.cpp | 12 +- src/libslic3r/PresetBundle.hpp | 2 +- src/libslic3r/PrintConfig.cpp | 1 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/MainFrame.cpp | 50 +++++++- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 148 ++++++++++++++++++++++- src/slic3r/GUI/PhysicalPrinterDialog.hpp | 1 + src/slic3r/GUI/Plater.cpp | 61 ++++++++++ src/slic3r/Utils/Secrets.cpp | 87 +++++++++++++ src/slic3r/Utils/Secrets.hpp | 10 ++ tests/slic3rutils/CMakeLists.txt | 2 + tests/slic3rutils/secretstore_tests.cpp | 8 ++ 15 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 deps/wxWidgets/wxWidgets.cmake create mode 100644 src/slic3r/Utils/Secrets.cpp create mode 100644 src/slic3r/Utils/Secrets.hpp create mode 100644 tests/slic3rutils/secretstore_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 25aff34d46..b090ec3fd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,8 +41,8 @@ option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1) option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1) option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) option(SLIC3R_UBSAN "Enable UBSan on Clang and GCC" 0) -option(SLIC3R_ENABLE_FORMAT_STEP "Enable compilation of STEP file support" ON) -# If SLIC3R_FHS is 1 -> SLIC3R_DESKTOP_INTEGRATION is always 0, othrewise variable. +option(SLIC3R_ENABLE_FORMAT_STEP "Enable compilation of STEP file support" 1) +# If SLIC3R_FHS is 1 -> SLIC3R_DESKTOP_INTEGRATION is always 0, otherwise variable. CMAKE_DEPENDENT_OPTION(SLIC3R_DESKTOP_INTEGRATION "Allow perfoming desktop integration during runtime" 1 "NOT SLIC3R_FHS" 0) set(OPENVDB_FIND_MODULE_PATH "" CACHE PATH "Path to OpenVDB installation's find modules.") diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake new file mode 100644 index 0000000000..05b7a88b79 --- /dev/null +++ b/deps/wxWidgets/wxWidgets.cmake @@ -0,0 +1,47 @@ +set(_wx_toolkit "") +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(_gtk_ver 2) + if (DEP_WX_GTK3) + set(_gtk_ver 3) + endif () + set(_wx_toolkit "-DwxBUILD_TOOLKIT=gtk${_gtk_ver}") +endif() + +set(_unicode_utf8 OFF) +if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for wxString unless its forced to. + set (_unicode_utf8 ON) +endif() + +prusaslicer_add_cmake_project(wxWidgets + URL https://github.com/prusa3d/wxWidgets/archive/78aa2dc0ea7ce99dc19adc1140f74c3e2e3f3a26.zip + URL_HASH SHA256=94b7d972373503e380e5a8b0ca63b1ccb956da4006402298dd89a0c5c7041b1e + DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG + CMAKE_ARGS + -DwxBUILD_PRECOMP=ON + ${_wx_toolkit} + "-DCMAKE_DEBUG_POSTFIX:STRING=" + -DwxBUILD_DEBUG_LEVEL=0 + -DwxUSE_MEDIACTRL=OFF + -DwxUSE_DETECT_SM=OFF + -DwxUSE_UNICODE=ON + -DwxUSE_UNICODE_UTF8=${_unicode_utf8} + -DwxUSE_OPENGL=ON + -DwxUSE_LIBPNG=sys + -DwxUSE_ZLIB=sys + -DwxUSE_NANOSVG=sys + -DwxUSE_NANOSVG_EXTERNAL=ON + -DwxUSE_REGEX=OFF + -DwxUSE_LIBXPM=builtin + -DwxUSE_LIBJPEG=sys + -DwxUSE_LIBTIFF=sys + -DwxUSE_EXPAT=sys + -DwxUSE_LIBSDL=OFF + -DwxUSE_XTEST=OFF + -DwxUSE_GLCANVAS_EGL=OFF + -DwxUSE_WEBREQUEST=OFF + -DwxUSE_SECRETSTORE=ON +) + +if (MSVC) + add_debug_dep(dep_wxWidgets) +endif () \ No newline at end of file diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 24bbe7a5b1..46126a621c 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1990,6 +1990,8 @@ public: one_string, // Close parameter, string value could be one of the list values. select_close, + // Password, string vaule is hidden by asterisk. + password, }; static bool is_gui_type_enum_open(const GUIType gui_type) { return gui_type == ConfigOptionDef::GUIType::i_enum_open || gui_type == ConfigOptionDef::GUIType::f_enum_open || gui_type == ConfigOptionDef::GUIType::select_open; } diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b4fb5ce27d..b7d241abcf 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1947,7 +1947,7 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri } } -void PresetBundle::export_configbundle(const std::string &path, bool export_system_settings, bool export_physical_printers/* = false*/) +void PresetBundle::export_configbundle(const std::string &path, bool export_system_settings, bool export_physical_printers/* = false*/, std::function secret_callback) { boost::nowide::ofstream c; c.open(path, std::ios::out | std::ios::trunc); @@ -1974,8 +1974,14 @@ void PresetBundle::export_configbundle(const std::string &path, bool export_syst if (export_physical_printers) { for (const PhysicalPrinter& ph_printer : this->physical_printers) { c << std::endl << "[physical_printer:" << ph_printer.name << "]" << std::endl; - for (const std::string& opt_key : ph_printer.config.keys()) - c << opt_key << " = " << ph_printer.config.opt_serialize(opt_key) << std::endl; + for (const std::string& opt_key : ph_printer.config.keys()) { + std::string opt_val = ph_printer.config.opt_serialize(opt_key); + if (opt_val == "stored") { + secret_callback(ph_printer.name, opt_key, opt_val); + } + + c << opt_key << " = " << opt_val << std::endl; + } } } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 1e6aa1caa1..ddd22f348a 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -129,7 +129,7 @@ public: const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule); // Export a config bundle file containing all the presets and the names of the active presets. - void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); + void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false, std::function secret_callback = nullptr); // Enable / disable the "- default -" preset. void set_default_suppressed(bool default_suppressed); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 391db4309e..e47f898082 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -393,6 +393,7 @@ void PrintConfigDef::init_common_params() def = this->add("printhost_password", coString); def->label = L("Password"); // def->tooltip = L(""); + def->gui_type = ConfigOptionDef::GUIType::password; def->mode = comAdvanced; def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionString("")); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index fea794c943..2ba2a16893 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -335,6 +335,8 @@ set(SLIC3R_GUI_SOURCES Utils/WxFontUtils.hpp Utils/WifiScanner.hpp Utils/WifiScanner.cpp + Utils/Secrets.hpp + Utils/Secrets.cpp ) find_package(NanoSVG REQUIRED) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7c28b9eb36..7ca9dc045b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -23,6 +23,9 @@ //#include #include #include +#if wxUSE_SECRETSTORE +#include +#endif #include #include @@ -1789,9 +1792,54 @@ void MainFrame::export_configbundle(bool export_physical_printers /*= false*/) file = dlg.GetPath(); if (!file.IsEmpty()) { // Export the config bundle. + + bool passwords_to_plain = false; + bool passwords_dialog_shown = false; + // callback function thats going to be passed to preset bundle (so preset bundle doesnt have to include WX secret lib) + std::function load_password = [&](const std::string& printer_id, const std::string& opt, std::string& out_psswd)->bool{ + out_psswd = std::string(); +#if wxUSE_SECRETSTORE + // First password prompts user with dialog + if (!passwords_dialog_shown) { + //wxString msg = GUI::format_wxstr(L"%1%\n%2%", _L("Some of the exported printers contains passwords, which are stored in the system password store."), _L("Do you wish to include the passwords to the exported file in the plain text form?")); + wxString msg = _L("Some of the exported printers contains passwords, which are stored in the system password store." + " Do you wish to include the passwords in the plain text form to the exported file?"); + MessageDialog dlg_psswd(this, msg, wxMessageBoxCaptionStr, wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION); + if (dlg_psswd.ShowModal() == wxID_YES) + passwords_to_plain = true; + passwords_dialog_shown = true; + } + if (!passwords_to_plain) + return false; + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("Failed to load credentials from the system secret store."), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + // Do not try again. System store is not reachable. + passwords_to_plain = false; + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PhysicalPrinter/%2%/%3%", SLIC3R_APP_NAME, printer_id, opt); + wxString username; + wxSecretValue password; + if (!store.Load(service, username, password)) { + std::string msg = GUI::format(_u8L("Failed to load credentials from the system secret store for the printer %1%."), printer_id); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + out_psswd = into_u8(password.GetAsString()); + return true; +#else + return false; +#endif // wxUSE_SECRETSTORE + }; + wxGetApp().app_config->update_config_dir(get_dir_name(file)); try { - wxGetApp().preset_bundle->export_configbundle(file.ToUTF8().data(), false, export_physical_printers); + wxGetApp().preset_bundle->export_configbundle(file.ToUTF8().data(), false, export_physical_printers, load_password); } catch (const std::exception &ex) { show_error(this, ex.what()); } diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 69902545a3..8d599efd8c 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,10 @@ #include #include #include +#if wxUSE_SECRETSTORE +#include +#endif +#include #include "libslic3r/libslic3r.h" #include "libslic3r/PrintConfig.hpp" @@ -153,6 +158,78 @@ void PresetForPrinter::on_sys_color_changed() m_delete_preset_btn->sys_color_changed(); } +namespace { + +bool is_secret_store_ok() +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + BOOST_LOG_TRIVIAL(warning) << "wxSecretStore is not supported: " << errmsg; + return false; + } + return true; +#else + return false; +#endif +} +bool save_secret(const std::string& id, const std::string& opt, const std::string& usr, const std::string& psswd) +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PhysicalPrinter/%2%/%3%", SLIC3R_APP_NAME, id, opt); + const wxString username = boost::nowide::widen(usr); + const wxSecretValue password(boost::nowide::widen(psswd)); + if (!store.Save(service, username, password)) { + std::string msg(_u8L("Failed to save credentials to the system secret store.")); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + return true; +#else + BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot save password to the system store."; + return false; +#endif // wxUSE_SECRETSTORE +} +bool load_secret(const std::string& id, const std::string& opt, std::string& usr, std::string& psswd) +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PhysicalPrinter/%2%/%3%", SLIC3R_APP_NAME, id, opt); + wxString username; + wxSecretValue password; + if (!store.Load(service, username, password)) { + std::string msg(_u8L("Failed to load credentials from the system secret store.")); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + usr = into_u8(username); + psswd = into_u8(password.GetAsString()); + return true; +#else + BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot load password from the system store."; + return false; +#endif // wxUSE_SECRETSTORE +} +} + //------------------------------------------ // PhysicalPrinterDialog @@ -202,6 +279,20 @@ PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent, wxString printer_ const std::set& preset_names = printer->get_preset_names(); for (const std::string& preset_name : preset_names) m_presets.emplace_back(new PresetForPrinter(this, preset_name)); + // "stored" indicates data are stored secretly, load them from store. + if (m_printer.config.opt_string("printhost_password") == "stored" && m_printer.config.opt_string("printhost_password") == "stored") { + std::string username; + std::string password; + if (load_secret(m_printer.name, "printhost_password", username, password)) { + if (!username.empty()) + m_printer.config.opt_string("printhost_user") = username; + if (!password.empty()) + m_printer.config.opt_string("printhost_password") = password; + } else { + m_printer.config.opt_string("printhost_user") = std::string(); + m_printer.config.opt_string("printhost_password") = std::string(); + } + } } if (m_presets.size() == 1) @@ -348,6 +439,20 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr return sizer; }; + auto api_key_copy = [=](wxWindow* parent) { + auto sizer = create_sizer_with_btn(parent, &m_api_key_copy_btn, "copy", _L("Copy")); + m_api_key_copy_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { + if (Field* apikey_field = m_optgroup->get_field("printhost_apikey"); apikey_field) { + if (wxTextCtrl* temp = dynamic_cast(apikey_field->getWindow()); temp ) { + wxTheClipboard->Open(); + wxTheClipboard->SetData(new wxTextDataObject(temp->GetValue())); + wxTheClipboard->Close(); + } + } + }); + return sizer; + }; + // Set a wider width for a better alignment Option option = m_optgroup->get_option("print_host"); option.opt.width = Field::def_width_wider(); @@ -360,7 +465,9 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr option = m_optgroup->get_option("printhost_apikey"); option.opt.width = Field::def_width_wider(); - m_optgroup->append_single_option_line(option); + Line apikey_line = m_optgroup->create_single_option_line(option); + apikey_line.append_widget(api_key_copy); + m_optgroup->append_line(apikey_line); option = m_optgroup->get_option("printhost_port"); option.opt.width = Field::def_width_wider(); @@ -422,6 +529,38 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->append_line(line); } + // Text line with info how passwords and api keys are stored + if (is_secret_store_ok()) { + Line line{ "", "" }; + line.full_width = 1; + line.widget = [ca_file_hint](wxWindow* parent) { + wxString info = GUI::format_wxstr(L"%1%:\n\t%2%\n" + , _L("Storing passwords") + , GUI::format_wxstr(_L("On this system, %1% uses the system password store to safely store and read passwords and API keys."), SLIC3R_APP_NAME)); + auto txt = new wxStaticText(parent, wxID_ANY, info); + txt->SetFont(wxGetApp().normal_font()); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt, 1, wxEXPAND); + return sizer; + }; + m_optgroup->append_line(line); + } else { + Line line{ "", "" }; + line.full_width = 1; + line.widget = [ca_file_hint](wxWindow* parent) { + wxString info = GUI::format_wxstr(L"%1%:\n\t%2%\n\t%3%\n" + , _L("Storing passwords") + , GUI::format_wxstr(_L("On this system, %1% cannot access the system password store."), SLIC3R_APP_NAME) + , _L("Passwords and API keys are stored in plain text.")); + auto txt = new wxStaticText(parent, wxID_ANY, info); + txt->SetFont(wxGetApp().normal_font()); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt, 1, wxEXPAND); + return sizer; + }; + m_optgroup->append_line(line); + } + for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) { option = m_optgroup->get_option(opt_key); option.opt.width = Field::def_width_wider(); @@ -810,6 +949,13 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) //update printer name, if it was changed m_printer.set_name(into_u8(printer_name)); + // save access data secretly + if (!m_printer.config.opt_string("printhost_password").empty()) { + if (save_secret(m_printer.name, "printhost_password", m_printer.config.opt_string("printhost_user"), m_printer.config.opt_string("printhost_password"))) { + m_printer.config.opt_string("printhost_password", false) = "stored"; + } + } + // save new physical printer printers.save_printer(m_printer, renamed_from); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 0de8df235f..2a26e1bf1e 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -76,6 +76,7 @@ class PhysicalPrinterDialog : public DPIDialog ScalableButton* m_printhost_test_btn {nullptr}; ScalableButton* m_printhost_cafile_browse_btn {nullptr}; ScalableButton* m_printhost_port_browse_btn {nullptr}; + ScalableButton* m_api_key_copy_btn {nullptr}; wxBoxSizer* m_presets_sizer {nullptr}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index bbfbc57088..cd1c5e4ea3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -50,6 +50,9 @@ #include #include #include +#if wxUSE_SECRETSTORE +#include +#endif #include @@ -5700,6 +5703,37 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject & this->reslice_until_step_inner(SLAPrintObjectStep(step), object, postpone_error_messages); } +namespace { +bool load_secret(const std::string& id, const std::string& opt, std::string& usr, std::string& psswd) +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PhysicalPrinter/%2%/%3%", SLIC3R_APP_NAME, id, opt); + wxString username; + wxSecretValue password; + if (!store.Load(service, username, password)) { + std::string msg(_u8L("Failed to load credentials from the system secret store.")); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return false; + } + usr = into_u8(username); + psswd = into_u8(password.GetAsString()); + return true; +#else + BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot load password from the system store."; + return false; +#endif // wxUSE_SECRETSTORE +} +} + void Plater::send_gcode() { // if physical_printer is selected, send gcode for this printer @@ -5707,6 +5741,33 @@ void Plater::send_gcode() if (! physical_printer_config || p->model.objects.empty()) return; + // Passwords and API keys + // "stored" indicates data are stored secretly, load them from store. + std::string printer_name = wxGetApp().preset_bundle->physical_printers.get_selected_printer().name; + if (physical_printer_config->opt_string("printhost_password") == "stored" && physical_printer_config->opt_string("printhost_password") == "stored") { + std::string username; + std::string password; + if (load_secret(printer_name, "printhost_password", username, password)) { + if (!username.empty()) + physical_printer_config->opt_string("printhost_user") = username; + if (!password.empty()) + physical_printer_config->opt_string("printhost_password") = password; + } + else { + physical_printer_config->opt_string("printhost_user") = std::string(); + physical_printer_config->opt_string("printhost_password") = std::string(); + } + } + /* + if (physical_printer_config->opt_string("printhost_apikey") == "stored") { + std::string username; + std::string password; + if (load_secret(printer_name, "printhost_apikey", username, password) && !password.empty()) + physical_printer_config->opt_string("printhost_apikey") = password; + else + physical_printer_config->opt_string("printhost_apikey") = std::string(); + } + */ PrintHostJob upload_job(physical_printer_config); if (upload_job.empty()) return; diff --git a/src/slic3r/Utils/Secrets.cpp b/src/slic3r/Utils/Secrets.cpp new file mode 100644 index 0000000000..5ca28aba4e --- /dev/null +++ b/src/slic3r/Utils/Secrets.cpp @@ -0,0 +1,87 @@ +#include "Secrets.hpp" +#include +#if wxUSE_SECRETSTORE +#include +#endif +namespace Slic3r { + +#if wxUSE_SECRETSTORE + +static bool PrintResult(bool ok) +{ + wxPuts(ok ? "ok" : "ERROR"); + return ok; +} + +bool SelfTest(wxSecretStore& store, const wxString& service) +{ + wxPrintf("Running the tests...\n"); + + const wxString userTest("test"); + const wxSecretValue secret1(6, "secret"); + + wxPrintf("Storing the password:\t"); + bool ok = store.Save(service, userTest, secret1); + if ( !PrintResult(ok) ) + { + // The rest of the tests will probably fail too, no need to continue. + wxPrintf("Bailing out.\n"); + return false; + } + + wxPrintf("Loading the password:\t"); + wxSecretValue secret; + wxString user; + ok = PrintResult(store.Load(service, user, secret) && + user == userTest && + secret == secret1); + + // Overwriting the password should work. + const wxSecretValue secret2(6, "privet"); + + wxPrintf("Changing the password:\t"); + if ( PrintResult(store.Save(service, user, secret2)) ) + { + wxPrintf("Reloading the password:\t"); + if ( !PrintResult(store.Load(service, user, secret) && + secret == secret2) ) + ok = false; + } + else + ok = false; + + wxPrintf("Deleting the password:\t"); + if ( !PrintResult(store.Delete(service)) ) + ok = false; + + // This is supposed to fail now. + wxPrintf("Deleting it again:\t"); + if ( !PrintResult(!store.Delete(service)) ) + ok = false; + + // And loading should fail too. + wxPrintf("Loading after deleting:\t"); + if ( !PrintResult(!store.Load(service, user, secret)) ) + ok = false; + + if ( ok ) + wxPrintf("All tests passed!\n"); + + return ok; +} + +#endif //wxUSE_SECRETSTORE + +bool check_secrets() +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + + return SelfTest(store, "prusaslicer"); +#else + printf("wxSecret not supported.\n"); + return true; +#endif +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/Secrets.hpp b/src/slic3r/Utils/Secrets.hpp new file mode 100644 index 0000000000..43cdf206a8 --- /dev/null +++ b/src/slic3r/Utils/Secrets.hpp @@ -0,0 +1,10 @@ +#ifndef PRUSASLICER_SECRETS_HPP +#define PRUSASLICER_SECRETS_HPP + +namespace Slic3r { + +bool check_secrets(); + +} + +#endif // PRUSASLICER_SECRETS_HPP diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index f207916f67..ac025ce46c 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -4,10 +4,12 @@ add_executable(${_TEST_NAME}_tests slic3r_jobs_tests.cpp slic3r_version_tests.cpp slic3r_arrangejob_tests.cpp + secretstore_tests.cpp ) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libslic3r_gui libslic3r) + if (MSVC) target_link_libraries(${_TEST_NAME}_tests Setupapi.lib) endif () diff --git a/tests/slic3rutils/secretstore_tests.cpp b/tests/slic3rutils/secretstore_tests.cpp new file mode 100644 index 0000000000..5821305092 --- /dev/null +++ b/tests/slic3rutils/secretstore_tests.cpp @@ -0,0 +1,8 @@ +#include "catch2/catch.hpp" + +#include "slic3r/Utils/Secrets.hpp" + +TEST_CASE("Secret store test", "[secretstore]") { + + REQUIRE(Slic3r::check_secrets()); +} From adc650ef4eaa8b7e13722d32e63a987e6aa25ba3 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Tue, 25 Jul 2023 15:52:38 +0200 Subject: [PATCH 02/64] PrusaConnect communication with PrusaAuth login Background communication with keeping tokens Temporary output is notification Temporary storing tokens in prusaslicer.ini Using wx secret store to store tokens if possible Improved PrusaConnect message - number of compatible models Fix of saving refresh token Refactoring of Auth and Auth Session classes. --- src/slic3r/CMakeLists.txt | 4 + src/slic3r/GUI/Auth.cpp | 479 +++++++++++++++++++++++++ src/slic3r/GUI/Auth.hpp | 79 ++++ src/slic3r/GUI/AuthSession.cpp | 232 ++++++++++++ src/slic3r/GUI/AuthSession.hpp | 152 ++++++++ src/slic3r/GUI/GUI_App.cpp | 41 ++- src/slic3r/GUI/GUI_App.hpp | 8 + src/slic3r/GUI/InstanceCheck.cpp | 4 + src/slic3r/GUI/InstanceCheck.hpp | 2 + src/slic3r/GUI/MainFrame.hpp | 1 + src/slic3r/GUI/NotificationManager.hpp | 2 + src/slic3r/GUI/Plater.cpp | 78 ++++ src/slic3r/GUI/Plater.hpp | 8 +- src/slic3r/Utils/Http.cpp | 22 ++ src/slic3r/Utils/Http.hpp | 4 + 15 files changed, 1113 insertions(+), 3 deletions(-) create mode 100644 src/slic3r/GUI/Auth.cpp create mode 100644 src/slic3r/GUI/Auth.hpp create mode 100644 src/slic3r/GUI/AuthSession.cpp create mode 100644 src/slic3r/GUI/AuthSession.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 2ba2a16893..b2f467b99f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -19,6 +19,10 @@ set(SLIC3R_GUI_SOURCES GUI/AboutDialog.hpp GUI/ArrangeSettingsDialogImgui.hpp GUI/ArrangeSettingsDialogImgui.cpp + GUI/Auth.cpp + GUI/Auth.hpp + GUI/AuthSession.cpp + GUI/AuthSession.hpp GUI/SysInfoDialog.cpp GUI/SysInfoDialog.hpp GUI/KBShortcutsDialog.cpp diff --git a/src/slic3r/GUI/Auth.cpp b/src/slic3r/GUI/Auth.cpp new file mode 100644 index 0000000000..1a91aa4761 --- /dev/null +++ b/src/slic3r/GUI/Auth.cpp @@ -0,0 +1,479 @@ +#include "Auth.hpp" +#include "GUI_App.hpp" +#include "format.hpp" +#include "../Utils/Http.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if wxUSE_SECRETSTORE +#include +#endif + +#ifdef WIN32 +#include +#endif // WIN32 + +#ifdef __linux__ +#include +#include +#include +#endif // __linux__ + + + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { +namespace GUI { + +namespace { + +std::string get_code_from_message(const std::string& url_message) +{ + size_t pos = url_message.rfind("code="); + std::string out; + for (size_t i = pos + 5; i < url_message.size(); i++) { + const char& c = url_message[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) + out+= c; + else + break; + } + return out; +} + +bool is_secret_store_ok() +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + BOOST_LOG_TRIVIAL(warning) << "wxSecretStore is not supported: " << errmsg; + return false; + } + return true; +#else + return false; +#endif +} +bool save_secret(const std::string& opt, const std::string& usr, const std::string& psswd) +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + //show_error(nullptr, msg); + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PrusaAuth/%2%", SLIC3R_APP_NAME, opt); + const wxString username = boost::nowide::widen(usr); + const wxSecretValue password(boost::nowide::widen(psswd)); + if (!store.Save(service, username, password)) { + std::string msg(_u8L("Failed to save credentials to the system secret store.")); + BOOST_LOG_TRIVIAL(error) << msg; + //show_error(nullptr, msg); + return false; + } + return true; +#else + BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot save password to the system store."; + return false; +#endif // wxUSE_SECRETSTORE +} +bool load_secret(const std::string& opt, std::string& usr, std::string& psswd) +{ +#if wxUSE_SECRETSTORE + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg); + BOOST_LOG_TRIVIAL(error) << msg; + //show_error(nullptr, msg); + return false; + } + const wxString service = GUI::format_wxstr(L"%1%/PrusaAuth/%2%", SLIC3R_APP_NAME, opt); + wxString username; + wxSecretValue password; + if (!store.Load(service, username, password)) { + std::string msg(_u8L("Failed to load credentials from the system secret store.")); + BOOST_LOG_TRIVIAL(error) << msg; + //show_error(nullptr, msg); + return false; + } + usr = into_u8(username); + psswd = into_u8(password.GetAsString()); + return true; +#else + BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot load password from the system store."; + return false; +#endif // wxUSE_SECRETSTORE +} +} + +PrusaAuthCommunication::PrusaAuthCommunication(wxEvtHandler* evt_handler, AppConfig* app_config) + : m_evt_handler(evt_handler) +{ + std::string access_token, refresh_token, shared_session_key; + if (is_secret_store_ok()) { + std::string key0, key1; + load_secret("access_token", key0, access_token); + load_secret("refresh_token", key1, refresh_token); + assert(key0 == key1); + shared_session_key = key0; + } else { + access_token = app_config->get("access_token"); + refresh_token = app_config->get("refresh_token"); + shared_session_key = app_config->get("shared_session_key"); + } + + if (!access_token.empty() || !refresh_token.empty()) + m_remember_session = true; + m_session = std::make_unique(evt_handler, access_token, refresh_token, shared_session_key); + init_session_thread(); + // perform login at the start - do we want this + if (m_remember_session) + login(); +} + +PrusaAuthCommunication::~PrusaAuthCommunication() { + if (m_thread.joinable()) { + // Stop the worker thread, if running. + { + // Notify the worker thread to cancel wait on detection polling. + std::lock_guard lck(m_thread_stop_mutex); + m_thread_stop = true; + } + m_thread_stop_condition.notify_all(); + // Wait for the worker thread to stop. + m_thread.join(); + } +} + +void PrusaAuthCommunication::set_username(const std::string& username, AppConfig* app_config) +{ + m_username = username; + { + std::lock_guard lock(m_session_mutex); + if (is_secret_store_ok()) { + save_secret("access_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_access_token() : std::string()); + save_secret("refresh_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_refresh_token() : std::string()); + } + else { + app_config->set("access_token", m_remember_session ? m_session->get_access_token() : std::string()); + app_config->set("refresh_token", m_remember_session ? m_session->get_refresh_token() : std::string()); + app_config->set("shared_session_key", m_remember_session ? m_session->get_shared_session_key() : std::string()); + } + + } +} + +void PrusaAuthCommunication::login_redirect() +{ + const std::string AUTH_HOST = "https://test-account.prusa3d.com"; + const std::string CLIENT_ID = client_id(); + const std::string REDIRECT_URI = "prusaslicer://login"; + CodeChalengeGenerator ccg; + m_code_verifier = ccg.generate_verifier(); + std::string code_challenge = ccg.generate_chalenge(m_code_verifier); + BOOST_LOG_TRIVIAL(info) << "code verifier: " << m_code_verifier; + BOOST_LOG_TRIVIAL(info) << "code challenge: " << code_challenge; + wxString url = GUI::format_wxstr(L"%1%/o/authorize/?client_id=%2%&response_type=code&code_challenge=%3%&code_challenge_method=S256&scope=basic_info&redirect_uri=%4%", AUTH_HOST, CLIENT_ID, code_challenge, REDIRECT_URI); + + wxQueueEvent(m_evt_handler,new OpenPrusaAuthEvent(GUI::EVT_OPEN_PRUSAAUTH, std::move(url))); +} + +bool PrusaAuthCommunication::is_logged() +{ + return !m_username.empty(); +} +void PrusaAuthCommunication::login() +{ + { + std::lock_guard lock(m_session_mutex); + if (!m_session->is_initialized()) { + login_redirect(); + //return; + } + m_session->enqueue_action(UserActionID::USER_ID, nullptr, nullptr, {}); + } + wakeup_session_thread(); +} +void PrusaAuthCommunication::logout() +{ + { + std::lock_guard lock(m_session_mutex); + m_session->clear(); + } + wxQueueEvent(m_evt_handler, new PrusaAuthSuccessEvent(GUI::EVT_LOGGEDOUT_PRUSAAUTH, {})); +} + +void PrusaAuthCommunication::on_login_code_recieved(const std::string& url_message) +{ + { + std::lock_guard lock(m_session_mutex); + const std::string code = get_code_from_message(url_message); + m_session->init_with_code(code, m_code_verifier); + } + wakeup_session_thread(); +} + +void PrusaAuthCommunication::enqueue_user_id_action() +{ + { + std::lock_guard lock(m_session_mutex); + if (!m_session->is_initialized()) { + //login_redirect(); + return; + } + m_session->enqueue_action(UserActionID::USER_ID, nullptr, nullptr, {}); + } + wakeup_session_thread(); +} + +void PrusaAuthCommunication::enqueue_connect_dummy_action() +{ + { + std::lock_guard lock(m_session_mutex); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Dummy endpoint connection failed - Not Logged in."; + return; + } + m_session->enqueue_action(UserActionID::CONNECT_DUMMY, nullptr, nullptr, {}); + } + wakeup_session_thread(); +} + +void PrusaAuthCommunication::enqueue_connect_printers_action() +{ + { + std::lock_guard lock(m_session_mutex); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; + return; + } + m_session->enqueue_action(UserActionID::CONNECT_PRINTERS, nullptr, nullptr, {}); + } + wakeup_session_thread(); +} + +void PrusaAuthCommunication::init_session_thread() +{ + m_thread = std::thread([this]() { + for (;;) { + // Wait for 1 second + // Cancellable. + { + std::unique_lock lck(m_thread_stop_mutex); + m_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_thread_stop; }); + } + if (m_thread_stop) + // Stop the worker thread. + break; + { + std::lock_guard lock(m_session_mutex); + m_session->process_action_queue(); + } + } + }); +} + +void PrusaAuthCommunication::wakeup_session_thread() +{ + m_thread_stop_condition.notify_all(); +} + +namespace { +/* +void proccess_tree(const pt::ptree& tree, const std::string depth, std::string& out) +{ + + for (const auto& section : tree) { + printf("%s%s", depth.c_str(), section.first.c_str()); + if (!section.second.data().empty()) { + if (section.first == "printer_type_name") { + out += section.second.data(); + out += " : "; + } else if (section.first == "state") { + out += section.second.data(); + out += "\n"; + } + printf(" : %s\n", section.second.data().c_str()); + } else { + printf("\n"); + proccess_tree(section.second, depth + " ", out); + } + + } +} +*/ +typedef std::map ModelCounter; +void proccess_tree(const pt::ptree& tree, const std::string depth, ModelCounter& models) +{ + for (const auto& section : tree) { + //printf("%s%s", depth.c_str(), section.first.c_str()); + if (!section.second.data().empty()) { + //printf(" : %s\n", section.second.data().c_str()); + } + else { + if (section.first == "printer_type_compatible") { + for (const auto& sub : section.second) { + if (!sub.second.data().empty()) { + //printf(" : %s\n", section.second.data().c_str()); + if(models.find(sub.second.data()) == models.end()) + models.emplace(sub.second.data(), 1); + else + models[sub.second.data()]++; + } + } + } else { + //printf("\n"); + proccess_tree(section.second, depth + " ", models); + } + } + } +} +} + +std::string PrusaAuthCommunication::proccess_prusaconnect_printers_message(const std::string& message) +{ + std::string out; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + + ModelCounter counter; + proccess_tree(ptree, "", counter); + for (const auto model : counter) + { + out += GUI::format("%1%x %2%\n", std::to_string(model.second), model.first); + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); + } + return out; +} + +std::string CodeChalengeGenerator::generate_chalenge(const std::string& verifier) +{ + std::string code_challenge; + try + { + code_challenge = sha256(verifier); + code_challenge = base64_encode(code_challenge); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Code Chalenge Generator failed: " << e.what(); + } + assert(!code_challenge.empty()); + return code_challenge; + +} +std::string CodeChalengeGenerator::generate_verifier() +{ + size_t length = 40; + std::string code_verifier = generate_code_verifier(length); + assert(code_verifier.size() == length); + return code_verifier; +} +std::string CodeChalengeGenerator::base64_encode(const std::string& input) +{ + std::string output; + output.resize(boost::beast::detail::base64::encoded_size(input.size())); + boost::beast::detail::base64::encode(&output[0], input.data(), input.size()); + // save encode - replace + and / with - and _ + std::replace(output.begin(), output.end(), '+', '-'); + std::replace(output.begin(), output.end(), '/', '_'); + // remove last '=' sign + while (output.back() == '=') + output.pop_back(); + return output; +} +std::string CodeChalengeGenerator::generate_code_verifier(size_t length) +{ + const std::string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution distribution(0, chars.size() - 1); + std::string code_verifier; + for (int i = 0; i < length; ++i) { + code_verifier += chars[distribution(gen)]; + } + return code_verifier; +} + +#ifdef WIN32 +std::string CodeChalengeGenerator::sha256(const std::string& input) +{ + HCRYPTPROV prov_handle = NULL; + HCRYPTHASH hash_handle = NULL; + std::vector buffer(1024); + DWORD hash_size = 0; + DWORD buffer_size = sizeof(DWORD); + std::string output; + + if (!CryptAcquireContext(&prov_handle, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) { + throw std::exception("CryptAcquireContext failed."); + } + if (!CryptCreateHash(prov_handle, CALG_SHA_256, 0, 0, &hash_handle)) { + CryptReleaseContext(prov_handle, 0); + throw std::exception("CryptCreateHash failed."); + } + if (!CryptHashData(hash_handle, reinterpret_cast(input.c_str()), input.length(), 0)) { + CryptDestroyHash(hash_handle); + CryptReleaseContext(prov_handle, 0); + throw std::exception("CryptCreateHash failed."); + } + if (!CryptGetHashParam(hash_handle, HP_HASHSIZE, reinterpret_cast(&hash_size), &buffer_size, 0)) { + CryptDestroyHash(hash_handle); + CryptReleaseContext(prov_handle, 0); + throw std::exception("CryptGetHashParam HP_HASHSIZE failed."); + } + output.resize(hash_size); + if (!CryptGetHashParam(hash_handle, HP_HASHVAL, reinterpret_cast(&output[0]), &hash_size, 0)) { + CryptDestroyHash(hash_handle); + CryptReleaseContext(prov_handle, 0); + throw std::exception("CryptGetHashParam HP_HASHVAL failed."); + } + return output; +} +#endif // WIN32 +#ifdef __linux__ +std::string CodeChalengeGenerator::sha256(const std::string& input) { + EVP_MD_CTX* mdctx; + const EVP_MD* md; + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int digestLen; + + md = EVP_sha256(); + mdctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(mdctx, md, NULL); + EVP_DigestUpdate(mdctx, input.c_str(), input.length()); + EVP_DigestFinal_ex(mdctx, digest, &digestLen); + EVP_MD_CTX_free(mdctx); + + return std::string(reinterpret_cast(digest), digestLen); +} +#endif // __linux__ +}} // Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/Auth.hpp b/src/slic3r/GUI/Auth.hpp new file mode 100644 index 0000000000..77274a2e32 --- /dev/null +++ b/src/slic3r/GUI/Auth.hpp @@ -0,0 +1,79 @@ +#ifndef slic3r_Auth_hpp_ +#define slic3r_Auth_hpp_ + +#include "AuthSession.hpp" +#include "Event.hpp" +#include "libslic3r/AppConfig.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { +class CodeChalengeGenerator +{ +public: + CodeChalengeGenerator() {} + ~CodeChalengeGenerator() {} + std::string generate_chalenge(const std::string& verifier); + std::string generate_verifier(); +private: + std::string generate_code_verifier(size_t length); + std::string base64_encode(const std::string& input); + std::string sha256(const std::string& input); +}; + +class PrusaAuthCommunication { +public: + PrusaAuthCommunication(wxEvtHandler* evt_handler, AppConfig* app_config); + ~PrusaAuthCommunication(); + + // UI Session thread Interface + // + bool is_logged(); + void login(); + void logout(); + // Trigger function starts various remote operations + // Each user action is implemented in different UserAction class and stored in m_actions. + void enqueue_user_id_action(); + void enqueue_connect_dummy_action(); + void enqueue_connect_printers_action(); + void set_remember_session(bool b) { m_remember_session = b; } + + // Callbacks - called from UI after receiving Event from Session thread. Some might use Session thread. + // + // Called when browser returns code via prusaslicer:// custom url. + // Exchanges code for tokens and shared_session_key + void on_login_code_recieved(const std::string& url_message); + + + void set_username(const std::string& username, AppConfig* app_config); + + std::string get_username() const { return m_username; } + + std::string proccess_prusaconnect_printers_message(const std::string& message); + +private: + std::unique_ptr m_session; + std::thread m_thread; + std::mutex m_session_mutex; + std::mutex m_thread_stop_mutex; + std::condition_variable m_thread_stop_condition; + bool m_thread_stop { false }; + std::string m_code_verifier; + wxEvtHandler* m_evt_handler; + // if not empty - user is logged in + std::string m_username; + bool m_remember_session {false}; + + void wakeup_session_thread(); + void init_session_thread(); + void login_redirect(); + std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; } +}; +} +} +#endif \ No newline at end of file diff --git a/src/slic3r/GUI/AuthSession.cpp b/src/slic3r/GUI/AuthSession.cpp new file mode 100644 index 0000000000..d4ab7f3431 --- /dev/null +++ b/src/slic3r/GUI/AuthSession.cpp @@ -0,0 +1,232 @@ +#include "AuthSession.hpp" +#include "GUI_App.hpp" +#include "format.hpp" +#include "../Utils/Http.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(EVT_OPEN_PRUSAAUTH, OpenPrusaAuthEvent); +wxDEFINE_EVENT(EVT_LOGGEDOUT_PRUSAAUTH, PrusaAuthSuccessEvent); +wxDEFINE_EVENT(EVT_PA_ID_USER_SUCCESS, PrusaAuthSuccessEvent); +wxDEFINE_EVENT(EVT_PA_ID_USER_FAIL, PrusaAuthFailEvent); +wxDEFINE_EVENT(EVT_PRUSAAUTH_SUCCESS, PrusaAuthSuccessEvent); +wxDEFINE_EVENT(EVT_PRUSACONNECT_PRINTERS_SUCCESS, PrusaAuthSuccessEvent); +wxDEFINE_EVENT(EVT_PRUSAAUTH_FAIL, PrusaAuthFailEvent); + +void UserActionPost::perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) +{ + std::string url = m_url; + BOOST_LOG_TRIVIAL(info) << m_action_name <<" POST " << url << " body: " << input; + auto http = Http::post(std::move(url)); + if (!input.empty()) + http.set_post_body(input); + http.header("Content-type", "application/x-www-form-urlencoded"); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << m_action_name << " action failed. status: " << status << " Body: " << body; + if (fail_callback) + fail_callback(body); + }); + http.on_complete([&, this](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << m_action_name << "action success. Status: " << status << " Body: " << body; + if (success_callback) + success_callback(body); + }); + http.perform_sync(); +} + +void UserActionGetWithEvent::perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, /*UNUSED*/ const std::string& input) +{ + std::string url = m_url; + BOOST_LOG_TRIVIAL(info) << m_action_name << " GET " << url; + auto http = Http::get(url); + http.header("Authorization", "Bearer " + access_token); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << m_action_name << " action failed. status: " << status << " Body: " << body; + if (fail_callback) + fail_callback(body); + std::string message = GUI::format("%1% action failed (%2%): %3%", m_action_name, std::to_string(status), body); + wxQueueEvent(m_evt_handler, new PrusaAuthFailEvent(m_fail_evt_type, std::move(message))); + }); + http.on_complete([&, this](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << m_action_name << " action success. Status: " << status << " Body: " << body; + if (success_callback) + success_callback(body); + wxQueueEvent(m_evt_handler, new PrusaAuthSuccessEvent(m_succ_evt_type, body)); + }); + + http.perform_sync(); +} + +void AuthSession::process_action_queue() +{ + if (m_priority_action_queue.empty() && m_action_queue.empty()) + return; + + if (this->is_initialized()) { + // if priority queue already has some action f.e. to exchange tokens, the test should not be neccessary but also shouldn't be problem + enqueue_test_with_refresh(); + } + + while (!m_priority_action_queue.empty()) { + m_actions[m_priority_action_queue.front().action_id]->perform(m_access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input); + if (!m_priority_action_queue.empty()) + m_priority_action_queue.pop(); + } + + if (!this->is_initialized()) + return; + + while (!m_action_queue.empty()) { + m_actions[m_action_queue.front().action_id]->perform(m_access_token, m_action_queue.front().success_callback, m_action_queue.front().fail_callback, m_action_queue.front().input); + if (!m_action_queue.empty()) + m_action_queue.pop(); + } +} + +void AuthSession::enqueue_action(UserActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) +{ + m_action_queue.push({ id, success_callback, fail_callback, input }); +} + +void AuthSession::init_with_code(const std::string& code, const std::string& code_verifier) +{ + // Data we have + const std::string REDIRECT_URI = "prusaslicer://login"; + std::string post_fields = "code=" + code + + "&client_id=" + client_id() + + "&grant_type=authorization_code" + + "&redirect_uri=" + REDIRECT_URI + + "&code_verifier="+ code_verifier; + + auto succ_fn = [&](const std::string& body){ + // Data we need + std::string access_token, refresh_token, shared_session_key; + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + const auto access_token_optional = ptree.get_optional("access_token"); + const auto refresh_token_optional = ptree.get_optional("refresh_token"); + const auto shared_session_key_optional = ptree.get_optional("shared_session_key"); + + if (access_token_optional) + access_token = *access_token_optional; + if (refresh_token_optional) + refresh_token = *refresh_token_optional; + if (shared_session_key_optional) + shared_session_key = *shared_session_key_optional; + } + catch (const std::exception&) { + BOOST_LOG_TRIVIAL(error) << "Auth::http_access Could not parse server response."; + } + + assert(!access_token.empty() && !refresh_token.empty() && !shared_session_key.empty()); + if (access_token.empty() || refresh_token.empty() || shared_session_key.empty()) { + BOOST_LOG_TRIVIAL(error) << "Refreshing token has failed."; + m_access_token = std::string(); + m_refresh_token = std::string(); + m_shared_session_key = std::string(); + return; + } + + BOOST_LOG_TRIVIAL(info) << "access_token: " << access_token; + BOOST_LOG_TRIVIAL(info) << "refresh_token: " << refresh_token; + BOOST_LOG_TRIVIAL(info) << "shared_session_key: " << shared_session_key; + + m_access_token = access_token; + m_refresh_token = refresh_token; + m_shared_session_key = shared_session_key; + }; + + // fail fn might be cancel_queue here + m_priority_action_queue.push({ UserActionID::CODE_FOR_TOKEN, succ_fn, std::bind(&AuthSession::enqueue_refresh, this, std::placeholders::_1), post_fields }); +} + +void AuthSession::enqueue_test_with_refresh() +{ + // on test fail - try refresh + m_priority_action_queue.push({ UserActionID::TEST_CONNECTION, nullptr, std::bind(&AuthSession::enqueue_refresh, this, std::placeholders::_1), {} }); +} + +void AuthSession::enqueue_refresh(const std::string& body) +{ + std::string post_fields = "grant_type=refresh_token" + "&client_id=" + client_id() + + "&refresh_token=" + m_refresh_token; + + auto succ_callback = [&](const std::string& body){ + std::string new_access_token; + std::string new_refresh_token; + std::string new_shared_session_key; + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + const auto access_token_optional = ptree.get_optional("access_token"); + const auto refresh_token_optional = ptree.get_optional("refresh_token"); + const auto shared_session_key_optional = ptree.get_optional("shared_session_key"); + + if (access_token_optional) + new_access_token = *access_token_optional; + if (refresh_token_optional) + new_refresh_token = *refresh_token_optional; + if (shared_session_key_optional) + new_shared_session_key = *shared_session_key_optional; + } + catch (const std::exception&) { + BOOST_LOG_TRIVIAL(error) << "Could not parse server response."; + } + + assert(!new_access_token.empty() && !new_refresh_token.empty() && !new_shared_session_key.empty()); + if (new_access_token.empty() || new_refresh_token.empty() || new_shared_session_key.empty()) { + BOOST_LOG_TRIVIAL(error) << "Refreshing token has failed."; + m_access_token = std::string(); + m_refresh_token = std::string(); + m_shared_session_key = std::string(); + // TODO: cancel following queue + } + + BOOST_LOG_TRIVIAL(info) << "access_token: " << new_access_token; + BOOST_LOG_TRIVIAL(info) << "refresh_token: " << new_refresh_token; + BOOST_LOG_TRIVIAL(info) << "shared_session_key: " << new_shared_session_key; + + m_access_token = new_access_token; + m_refresh_token = new_refresh_token; + m_shared_session_key = new_shared_session_key; + m_priority_action_queue.push({ UserActionID::TEST_CONNECTION, nullptr, std::bind(&AuthSession::refresh_failed_callback, this, std::placeholders::_1), {} }); + }; + + m_priority_action_queue.push({ UserActionID::REFRESH_TOKEN, succ_callback, std::bind(&AuthSession::refresh_failed_callback, this, std::placeholders::_1), post_fields }); +} + +void AuthSession::refresh_failed_callback(const std::string& body) +{ + clear(); + cancel_queue(); +} +void AuthSession::cancel_queue() +{ + while (!m_priority_action_queue.empty()) { + m_priority_action_queue.pop(); + } + while (!m_action_queue.empty()) { + m_action_queue.pop(); + } +} + +}} // Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/AuthSession.hpp b/src/slic3r/GUI/AuthSession.hpp new file mode 100644 index 0000000000..afb9392e70 --- /dev/null +++ b/src/slic3r/GUI/AuthSession.hpp @@ -0,0 +1,152 @@ +#ifndef slic3r_AuthSession_hpp_ +#define slic3r_AuthSession_hpp_ + +#include "Event.hpp" +#include "libslic3r/AppConfig.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +using OpenPrusaAuthEvent = Event; +using PrusaAuthSuccessEvent = Event; +using PrusaAuthFailEvent = Event; +wxDECLARE_EVENT(EVT_OPEN_PRUSAAUTH, OpenPrusaAuthEvent); +wxDECLARE_EVENT(EVT_LOGGEDOUT_PRUSAAUTH, PrusaAuthSuccessEvent); +wxDECLARE_EVENT(EVT_PA_ID_USER_SUCCESS, PrusaAuthSuccessEvent); +wxDECLARE_EVENT(EVT_PA_ID_USER_FAIL, PrusaAuthFailEvent); +wxDECLARE_EVENT(EVT_PRUSAAUTH_SUCCESS, PrusaAuthSuccessEvent); +wxDECLARE_EVENT(EVT_PRUSACONNECT_PRINTERS_SUCCESS, PrusaAuthSuccessEvent); +wxDECLARE_EVENT(EVT_PRUSAAUTH_FAIL, PrusaAuthFailEvent); + +typedef std::function UserActionSuccessFn; +typedef std::function UserActionFailFn; + +// UserActions implements different operations via trigger() method. Stored in m_actions. +enum class UserActionID { + DUMMY_ACTION, + REFRESH_TOKEN, + CODE_FOR_TOKEN, + TEST_CONNECTION, + USER_ID, + CONNECT_DUMMY, + CONNECT_PRINTERS, +}; +class UserAction +{ +public: + UserAction(const std::string name, const std::string url) : m_action_name(name), m_url(url){} + ~UserAction() {} + virtual void perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) = 0; +protected: + std::string m_action_name; + std::string m_url; +}; + +class UserActionGetWithEvent : public UserAction +{ +public: + UserActionGetWithEvent(const std::string name, const std::string url, wxEvtHandler* evt_handler, wxEventType succ_event_type, wxEventType fail_event_type) + : m_succ_evt_type(succ_event_type) + , m_fail_evt_type(fail_event_type) + , m_evt_handler(evt_handler) + , UserAction(name, url) + {} + ~UserActionGetWithEvent() {} + void perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) override; +private: + wxEventType m_succ_evt_type; + wxEventType m_fail_evt_type; + wxEvtHandler* m_evt_handler; +}; + +class UserActionPost : public UserAction +{ +public: + UserActionPost(const std::string name, const std::string url) : UserAction(name, url) {} + ~UserActionPost() {} + void perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) override; +}; + +class DummyUserAction : public UserAction +{ +public: + DummyUserAction() : UserAction("Dummy", {}) {} + ~DummyUserAction() {} + void perform(const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) override { } +}; + +struct ActionQueueData +{ + UserActionID action_id; + UserActionSuccessFn success_callback; + UserActionFailFn fail_callback; + std::string input; +}; + +class AuthSession +{ +public: + AuthSession(wxEvtHandler* evt_handler, const std::string& access_token, const std::string& refresh_token, const std::string& shared_session_key) + : m_access_token(access_token) + , m_refresh_token(refresh_token) + , m_shared_session_key(shared_session_key) + { + // do not forget to add delete to destructor + m_actions[UserActionID::DUMMY_ACTION] = std::make_unique(); + m_actions[UserActionID::REFRESH_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); + m_actions[UserActionID::CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); + m_actions[UserActionID::TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::USER_ID] = std::make_unique("USER_ID", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::CONNECT_DUMMY] = std::make_unique("CONNECT_DUMMY", "dev.connect.prusa:8000/slicer/dummy", evt_handler, EVT_PRUSAAUTH_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::CONNECT_PRINTERS] = std::make_unique("CONNECT_PRINTERS", "dev.connect.prusa:8000/slicer/printers", evt_handler, EVT_PRUSACONNECT_PRINTERS_SUCCESS, EVT_PRUSAAUTH_FAIL); + } + ~AuthSession() + { + m_actions[UserActionID::DUMMY_ACTION].reset(nullptr); + m_actions[UserActionID::REFRESH_TOKEN].reset(nullptr); + m_actions[UserActionID::CODE_FOR_TOKEN].reset(nullptr); + m_actions[UserActionID::TEST_CONNECTION].reset(nullptr); + m_actions[UserActionID::USER_ID].reset(nullptr); + m_actions[UserActionID::CONNECT_DUMMY].reset(nullptr); + m_actions[UserActionID::CONNECT_PRINTERS].reset(nullptr); + //assert(m_actions.empty()); + } + void clear() { + m_access_token.clear(); + m_refresh_token.clear(); + m_shared_session_key.clear(); + } + + void init_with_code(const std::string& code, const std::string& code_verifier); + void process_action_queue(); + void enqueue_action(UserActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input); + bool is_initialized() { return !m_access_token.empty() || !m_refresh_token.empty(); } + std::string get_access_token() const { return m_access_token; } + std::string get_refresh_token() const { return m_refresh_token; } + std::string get_shared_session_key() const { return m_shared_session_key; } + +private: + void enqueue_test_with_refresh(); + void enqueue_refresh(const std::string& body); + void refresh_failed_callback(const std::string& body); + void cancel_queue(); + std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; } + + std::string m_access_token; + std::string m_refresh_token; + std::string m_shared_session_key; + + std::queue m_action_queue; + std::queue m_priority_action_queue; + std::map> m_actions; +}; + +} +} +#endif \ No newline at end of file diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index a5646bb52e..c0115afd8a 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -98,6 +98,7 @@ #include "Downloader.hpp" #include "PhysicalPrinterDialog.hpp" #include "WifiConfigDialog.hpp" +#include "Auth.hpp" #include "BitmapCache.hpp" #include "Notebook.hpp" @@ -2494,6 +2495,12 @@ void GUI_App::add_config_menu(wxMenuBar *menu) local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); + local_menu->AppendSeparator(); + wxMenuItem* updatable_item = local_menu->Append(config_id_base + ConfigMenuAuthLogin, _L("PrusaAuth Log in"), _L("")); + m_config_menu_updatable_items.emplace(ConfigMenuIDs::ConfigMenuAuthLogin, updatable_item); + updatable_item = local_menu->Append(config_id_base + ConfigMenuConnectDummy, _L("PrusaConnect Printers"), _L("")); + updatable_item->Enable(false); + m_config_menu_updatable_items.emplace(ConfigMenuIDs::ConfigMenuConnectDummy, updatable_item); #if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) //if (DesktopIntegrationDialog::integration_possible()) local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); @@ -2542,6 +2549,19 @@ void GUI_App::add_config_menu(wxMenuBar *menu) case ConfigMenuUpdateApp: app_version_check(true); break; + case ConfigMenuAuthLogin: + { + if (this->plater()->get_auth_communication()->is_logged()) + this->plater()->get_auth_communication()->logout(); + else + this->plater()->get_auth_communication()->login(); + } + break; + case ConfigMenuConnectDummy: + { + this->plater()->get_auth_communication()->enqueue_connect_printers_action(); + } + break; #ifdef __linux__ case ConfigMenuDesktopIntegration: show_desktop_integration_dialog(); @@ -2657,7 +2677,11 @@ void GUI_App::add_config_menu(wxMenuBar *menu) menu->Append(local_menu, _L("&Configuration")); } - +void GUI_App::update_config_menu() +{ + m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuAuthLogin]->SetItemLabel(this->plater()->get_auth_communication()->is_logged() ? _L("PrusaAuth Log out") : _L("PrusaAuth Log in")); + m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuConnectDummy]->Enable(this->plater()->get_auth_communication()->is_logged()); +} void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) { mainframe->preferences_dialog->show(highlight_option, tab_name); @@ -3431,6 +3455,21 @@ bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* pa return launch && wxLaunchDefaultBrowser(url, flags); } +bool GUI_App::open_login_browser_with_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + RichMessageDialog dialog(parent, _L("Open Log in page in default browser?"), _L("PrusaSlicer: Open Log in page"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember me"), true); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + plater()->get_auth_communication()->set_remember_session(dialog.IsCheckBoxChecked()); + + return launch && wxLaunchDefaultBrowser(url, flags); +} + // static method accepting a wxWindow object as first parameter // void warning_catcher{ // my($self, $message_dialog) = @_; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6d14123af3..6271b9a905 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -98,6 +98,8 @@ enum ConfigMenuIDs { ConfigMenuTakeSnapshot, ConfigMenuUpdateConf, ConfigMenuUpdateApp, + ConfigMenuAuthLogin, + ConfigMenuConnectDummy, ConfigMenuDesktopIntegration, ConfigMenuPreferences, ConfigMenuModeSimple, @@ -294,6 +296,7 @@ public: void update_mode(); void add_config_menu(wxMenuBar *menu); + void update_config_menu(); bool has_unsaved_preset_changes() const; bool has_current_preset_changes() const; void update_saved_preset_from_current_preset(); @@ -317,6 +320,7 @@ public: // Calls wxLaunchDefaultBrowser if user confirms in dialog. // Add "Rememeber my choice" checkbox to question dialog, when it is forced or a "suppress_hyperlinks" option has empty value bool open_browser_with_warning_dialog(const wxString& url, wxWindow* parent = nullptr, bool force_remember_choice = true, int flags = 0); + bool open_login_browser_with_dialog(const wxString& url, wxWindow* parent = nullptr, int flags = 0); #ifdef __APPLE__ void OSXStoreOpenFiles(const wxArrayString &files) override; // wxWidgets override to get an event on open files. @@ -419,6 +423,10 @@ private: void app_version_check(bool from_user); bool m_wifi_config_dialog_shown { false }; + bool m_wifi_config_dialog_was_declined { false }; + // change to vector of items when adding more items that require update + //wxMenuItem* m_login_config_menu_item { nullptr }; + std::map< ConfigMenuIDs, wxMenuItem*> m_config_menu_updatable_items; }; DECLARE_APP(GUI_App) diff --git a/src/slic3r/GUI/InstanceCheck.cpp b/src/slic3r/GUI/InstanceCheck.cpp index a8f8adfdc9..595b71fd1a 100644 --- a/src/slic3r/GUI/InstanceCheck.cpp +++ b/src/slic3r/GUI/InstanceCheck.cpp @@ -378,6 +378,7 @@ namespace GUI { wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); wxDEFINE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent); +wxDEFINE_EVENT(EVT_LOGIN_OTHER_INSTANCE, LoginOtherInstanceEvent); wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler) @@ -520,6 +521,9 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message) else if (it->rfind("prusaslicer://open?file=", 0) == 0) #endif downloads.emplace_back(*it); + else if (it->rfind("prusaslicer://login", 0) == 0) { + wxPostEvent(m_callback_evt_handler, LoginOtherInstanceEvent(GUI::EVT_LOGIN_OTHER_INSTANCE, std::string(*it))); + } } if (! paths.empty()) { //wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here? diff --git a/src/slic3r/GUI/InstanceCheck.hpp b/src/slic3r/GUI/InstanceCheck.hpp index 3d9d2e0fb2..d70f4a90a5 100644 --- a/src/slic3r/GUI/InstanceCheck.hpp +++ b/src/slic3r/GUI/InstanceCheck.hpp @@ -48,8 +48,10 @@ class MainFrame; using LoadFromOtherInstanceEvent = Event>; using StartDownloadOtherInstanceEvent = Event>; +using LoginOtherInstanceEvent = Event; wxDECLARE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); wxDECLARE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent); +wxDECLARE_EVENT(EVT_LOGIN_OTHER_INSTANCE, LoginOtherInstanceEvent); using InstanceGoToFrontEvent = SimpleEvent; wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 37fcfc3ab2..42d6afd129 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -124,6 +124,7 @@ class MainFrame : public DPIFrame miSend, // Send G-code Send to print miMaterialTab, // Filament Settings Material Settings miPrinterTab, // Different bitmap for Printer Settings + miLogin, }; // vector of a MenuBar items changeable in respect to printer technology diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index d74b28af60..9759fd7d1f 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -130,6 +130,8 @@ enum class NotificationType URLNotRegistered, // Config file was detected during startup, open wifi config dialog via hypertext WifiConfigFileDetected + // + PrusaAuthUserID, }; class NotificationManager diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cd1c5e4ea3..859a55c8c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -119,6 +119,8 @@ #include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GLGizmoCut.hpp" #include "FileArchiveDialog.hpp" +#include "Auth.hpp" +#include "DesktopIntegrationDialog.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -262,6 +264,7 @@ struct Plater::priv GLToolbar collapse_toolbar; Preview *preview; std::unique_ptr notification_manager; + std::unique_ptr auth_communication; ProjectDirtyStateManager dirty_state; @@ -608,6 +611,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) + , auth_communication(std::make_unique(q, wxGetApp().app_config)) , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} , m_sla_import_dlg{new SLAImportDialog{q}} , delayed_scene_refresh(false) @@ -853,11 +857,75 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) wxGetApp().start_download(evt.data[i]); } + }); + this->q->Bind(EVT_LOGIN_OTHER_INSTANCE, [this](LoginOtherInstanceEvent& evt) { + BOOST_LOG_TRIVIAL(trace) << "Received login from other instance event."; + auth_communication->on_login_code_recieved(evt.data); }); this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { bring_instance_forward(); }); + + this->q->Bind(EVT_OPEN_PRUSAAUTH, [this](OpenPrusaAuthEvent& evt) { + BOOST_LOG_TRIVIAL(info) << "open browser: " << evt.data; + // first register url to be sure to get the code back + auto downloader_worker = new DownloaderUtils::Worker(nullptr); + downloader_worker->perform_register(wxGetApp().app_config->get("url_downloader_dest")); +#ifdef __linux__ + if (downloader_worker->get_perform_registration_linux()) + DesktopIntegrationDialog::perform_downloader_desktop_integration(); +#endif // __linux__ + // than open url + wxGetApp().open_login_browser_with_dialog(evt.data); + }); + + this->q->Bind(EVT_LOGGEDOUT_PRUSAAUTH, [this](PrusaAuthSuccessEvent& evt) { + auth_communication->set_username({}, wxGetApp().app_config); + std::string text = _u8L("Logged out."); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); + wxGetApp().update_config_menu(); + }); + + this->q->Bind(EVT_PA_ID_USER_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { + std::string text; + try { + std::stringstream ss(evt.data); + boost::property_tree::ptree ptree; + boost::property_tree::read_json(ss, ptree); + std::string public_username; + const auto public_username_optional = ptree.get_optional("public_username"); + + if (public_username_optional) + public_username = *public_username_optional; + text = format(_u8L("Logged as %1%."), public_username); + } + catch (const std::exception&) { + BOOST_LOG_TRIVIAL(error) << "UserIDUserAction Could not parse server response."; + } + assert(!text.empty()); + + auth_communication->set_username(evt.data, wxGetApp().app_config); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); + wxGetApp().update_config_menu(); + }); + this->q->Bind(EVT_PRUSAAUTH_FAIL, [this](PrusaAuthFailEvent& evt) { + auth_communication->set_username({}, wxGetApp().app_config); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::WarningNotificationLevel, evt.data); + }); + this->q->Bind(EVT_PRUSAAUTH_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, evt.data); + }); + this->q->Bind(EVT_PRUSACONNECT_PRINTERS_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { + std::string out = GUI::format( "Printers in your PrusaConnect team:\n%1%", auth_communication->proccess_prusaconnect_printers_message(evt.data)); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + }); + wxGetApp().other_instance_message_handler()->init(this->q); // collapse sidebar according to saved value @@ -6580,6 +6648,16 @@ const NotificationManager * Plater::get_notification_manager() const return p->notification_manager.get(); } +PrusaAuthCommunication* Plater::get_auth_communication() +{ + return p->auth_communication.get(); +} + +const PrusaAuthCommunication* Plater::get_auth_communication() const +{ + return p->auth_communication.get(); +} + void Plater::init_notification_manager() { p->init_notification_manager(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index ac596a9dcb..fd0cad7f9a 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -62,6 +62,7 @@ class Mouse3DController; class NotificationManager; struct Camera; class GLToolbar; +class PrusaAuthCommunication; class Plater: public wxPanel { @@ -347,8 +348,11 @@ public: void set_bed_shape(const Pointfs& shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false) const; void set_default_bed_shape() const; - NotificationManager * get_notification_manager(); - const NotificationManager * get_notification_manager() const; + NotificationManager* get_notification_manager(); + const NotificationManager* get_notification_manager() const; + + PrusaAuthCommunication* get_auth_communication(); + const PrusaAuthCommunication* get_auth_communication() const; void init_notification_manager(); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 8ec63d7b54..38ca8782aa 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -557,6 +557,12 @@ Http& Http::set_post_body(const std::string &body) return *this; } +Http& Http::set_referer(const std::string& referer) +{ + if (p) { ::curl_easy_setopt(p->curl, CURLOPT_REFERER, referer.c_str()); } + return *this; +} + Http& Http::set_put_body(const fs::path &path) { if (p) { p->set_put_body(path);} @@ -587,6 +593,22 @@ Http& Http::on_ip_resolve(IPResolveFn fn) return *this; } +Http& Http::cookie_file(const std::string& file_path) +{ + if (p) { + ::curl_easy_setopt(p->curl, CURLOPT_COOKIEFILE, file_path.c_str()); + } + return *this; +} + +Http& Http::cookie_jar(const std::string& file_path) +{ + if (p) { + ::curl_easy_setopt(p->curl, CURLOPT_COOKIEJAR, file_path.c_str()); + } + return *this; +} + Http::Ptr Http::perform() { auto self = std::make_shared(std::move(*this)); diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 941d638006..8e00b123fc 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -128,6 +128,10 @@ public: // Called if curl_easy_getinfo resolved just used IP address. Http& on_ip_resolve(IPResolveFn fn); + Http& cookie_file(const std::string& file_path); + Http& cookie_jar(const std::string& file_path); + Http& set_referer(const std::string& referer); + // Starts performing the request in a background thread Ptr perform(); // Starts performing the request on the current thread From 2276cedc107d3fd8cfc46d5f05cfe1d663b6b152 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 25 Mar 2024 11:08:24 +0100 Subject: [PATCH 03/64] WebView prototype Web View has working panel with Connect webpage with Auth token login. Webview2 is used on windows. It uses small dll to find its runtime. WebViewDialog.cpp, WebViewDialog.hpp, WebView.cpp and WebView.hpp were taken from https://github.com/bambulab/BambuStudio and used as protype for future WebView Development. Thank you. Co-authored-by: cmguo Co-authored-by: lane.wei --- CMakeLists.txt | 7 + deps/+WebView2/WebView2.cmake | 60 ++++ deps/+wxWidgets/wxWidgets.cmake | 16 +- deps/CMakeLists.txt | 5 + deps/wxWidgets/wxWidgets.cmake | 47 --- src/CMakeLists.txt | 2 +- src/libslic3r/Preset.hpp | 1 + src/slic3r/CMakeLists.txt | 6 + src/slic3r/GUI/Auth.cpp | 124 +++---- src/slic3r/GUI/Auth.hpp | 18 +- src/slic3r/GUI/AuthSession.cpp | 11 +- src/slic3r/GUI/AuthSession.hpp | 4 +- src/slic3r/GUI/ConfigWizard.cpp | 12 +- src/slic3r/GUI/ConfigWizard.hpp | 2 +- src/slic3r/GUI/GUI_App.cpp | 94 ++++- src/slic3r/GUI/GUI_App.hpp | 20 ++ src/slic3r/GUI/MainFrame.cpp | 24 ++ src/slic3r/GUI/MainFrame.hpp | 4 + src/slic3r/GUI/NotificationManager.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 51 ++- src/slic3r/GUI/Plater.hpp | 6 +- src/slic3r/GUI/UserAccount.cpp | 217 ++++++++++++ src/slic3r/GUI/UserAccount.hpp | 78 ++++ src/slic3r/GUI/WebView.cpp | 178 ++++++++++ src/slic3r/GUI/WebView.hpp | 16 + src/slic3r/GUI/WebViewDialog.cpp | 472 +++++++++++++++++++++++++ src/slic3r/GUI/WebViewDialog.hpp | 124 +++++++ src/slic3r/Utils/MacDarkMode.hpp | 3 + src/slic3r/Utils/MacDarkMode.mm | 12 + 29 files changed, 1422 insertions(+), 194 deletions(-) create mode 100644 deps/+WebView2/WebView2.cmake delete mode 100644 deps/wxWidgets/wxWidgets.cmake create mode 100644 src/slic3r/GUI/UserAccount.cpp create mode 100644 src/slic3r/GUI/UserAccount.hpp create mode 100644 src/slic3r/GUI/WebView.cpp create mode 100644 src/slic3r/GUI/WebView.hpp create mode 100644 src/slic3r/GUI/WebViewDialog.cpp create mode 100644 src/slic3r/GUI/WebViewDialog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b090ec3fd5..a6e6e2c8ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -606,6 +606,13 @@ function(prusaslicer_copy_dlls target) COMMENT "Copy mpfr runtime to build tree" VERBATIM) + if(DEFINED DESTDIR) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${DESTDIR}/usr/local/bin/WebView2Loader.dll ${_out_dir} + COMMENT "Copy WebView2Loader runtime to build tree" + VERBATIM) + endif () + endfunction() diff --git a/deps/+WebView2/WebView2.cmake b/deps/+WebView2/WebView2.cmake new file mode 100644 index 0000000000..b47a1ca30f --- /dev/null +++ b/deps/+WebView2/WebView2.cmake @@ -0,0 +1,60 @@ + +if (MSVC) + + # Update the following variables if updating WebView2 SDK + set(WEBVIEW2_VERSION "1.0.705.50") + set(WEBVIEW2_URL "https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/${WEBVIEW2_VERSION}") + set(WEBVIEW2_SHA256 "6a34bb553e18cfac7297b4031f3eac2558e439f8d16a45945c22945ac404105d") + + set(WEBVIEW2_DEFAULT_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/dep_WebView2-prefix/packages/Microsoft.Web.WebView2.${WEBVIEW2_VERSION}") + set(WEBVIEW2_DOWNLOAD_DIR "${CMAKE_CURRENT_BINARY_DIR}/dep_WebView2-prefix/download") + + #message(STATUS "WEBVIEW2_DEFAULT_PACKAGE_DIR = ${WEBVIEW2_DEFAULT_PACKAGE_DIR}") + + if(NOT EXISTS ${WEBVIEW2_PACKAGE_DIR}) + unset(WEBVIEW2_PACKAGE_DIR CACHE) + endif() + + set(WEBVIEW2_PACKAGE_DIR ${WEBVIEW2_DEFAULT_PACKAGE_DIR} CACHE PATH "WebView2 SDK PATH" FORCE) + + #file(MAKE_DIRECTORY ${DEP_DOWNLOAD_DIR}/WebView2) + + message(STATUS "WEBVIEW2_URL = ${WEBVIEW2_URL}") + message(STATUS "WEBVIEW2_DOWNLOAD_DIR = ${WEBVIEW2_DOWNLOAD_DIR}") + file(DOWNLOAD + ${WEBVIEW2_URL} + ${WEBVIEW2_DOWNLOAD_DIR}/WebView2.nuget + EXPECTED_HASH SHA256=${WEBVIEW2_SHA256}) + + file(MAKE_DIRECTORY ${WEBVIEW2_PACKAGE_DIR}) + + execute_process(COMMAND + ${CMAKE_COMMAND} -E tar x ${WEBVIEW2_DOWNLOAD_DIR}/WebView2.nuget + WORKING_DIRECTORY ${WEBVIEW2_PACKAGE_DIR} + ) + + set(_srcdir ${WEBVIEW2_PACKAGE_DIR}/build/native) + set(_dstdir ${DESTDIR}/usr/local) + + set(_output ${_dstdir}/include/WebView2.h + ${_dstdir}/bin/WebView2Loader.dll) + + if(NOT EXISTS ${_dstdir}/include) + file(MAKE_DIRECTORY ${_dstdir}/include) + endif() + + if(NOT EXISTS ${_dstdir}/bin) + file(MAKE_DIRECTORY ${_dstdir}/bin) + endif() + + add_custom_command( + OUTPUT ${_output} + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/include/WebView2.h ${_dstdir}/include/ + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/x${DEPS_BITS}/WebView2Loader.dll ${_dstdir}/bin/ + ) + + add_custom_target(dep_WebView2 SOURCES ${_output}) + + set(WEBVIEW2_PACKAGE_DIR ${WEBVIEW2_PACKAGE_DIR} CACHE INTERNAL "" FORCE) + +endif () \ No newline at end of file diff --git a/deps/+wxWidgets/wxWidgets.cmake b/deps/+wxWidgets/wxWidgets.cmake index ac15a48d0e..f0745d115e 100644 --- a/deps/+wxWidgets/wxWidgets.cmake +++ b/deps/+wxWidgets/wxWidgets.cmake @@ -14,6 +14,18 @@ if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for set (_unicode_utf8 ON) endif() +if (MSVC) + set(_wx_webview "-DwxUSE_WEBVIEW_EDGE=ON") +else () + set(_wx_webview "-DwxUSE_WEBVIEW=ON") +endif () + +if (UNIX AND NOT APPLE) + set(_wx_secretstore "-DwxUSE_SECRETSTORE=OFF") +else () + set(_wx_secretstore "-DwxUSE_SECRETSTORE=ON") +endif () + add_cmake_project(wxWidgets URL https://github.com/prusa3d/wxWidgets/archive/78aa2dc0ea7ce99dc19adc1140f74c3e2e3f3a26.zip URL_HASH SHA256=94b7d972373503e380e5a8b0ca63b1ccb956da4006402298dd89a0c5c7041b1e @@ -21,7 +33,7 @@ add_cmake_project(wxWidgets "-DCMAKE_DEBUG_POSTFIX:STRING=" -DwxBUILD_PRECOMP=ON ${_wx_toolkit} - -DwxUSE_MEDIACTRL=OFF + -DwxUSE_MEDIACTRL=ON -DwxUSE_DETECT_SM=OFF -DwxUSE_UNICODE=ON -DwxUSE_UNICODE_UTF8=${_unicode_utf8} @@ -39,6 +51,8 @@ add_cmake_project(wxWidgets -DwxUSE_XTEST=OFF -DwxUSE_GLCANVAS_EGL=OFF -DwxUSE_WEBREQUEST=OFF + ${_wx_webview} + ${_wx_secretstore} ) set(DEP_wxWidgets_DEPENDS ZLIB PNG EXPAT TIFF JPEG NanoSVG) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 175b64a619..a50243766f 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -140,6 +140,11 @@ if (UNIX) endif () endif () +if (MSVC) + list(APPEND REQUIRED_PACKAGES WebView2) +endif() + + list(APPEND SYSTEM_PROVIDED_PACKAGES ${${PROJECT_NAME}_PLATFORM_PACKAGES}) list(REMOVE_DUPLICATES SYSTEM_PROVIDED_PACKAGES) diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake deleted file mode 100644 index 05b7a88b79..0000000000 --- a/deps/wxWidgets/wxWidgets.cmake +++ /dev/null @@ -1,47 +0,0 @@ -set(_wx_toolkit "") -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - set(_gtk_ver 2) - if (DEP_WX_GTK3) - set(_gtk_ver 3) - endif () - set(_wx_toolkit "-DwxBUILD_TOOLKIT=gtk${_gtk_ver}") -endif() - -set(_unicode_utf8 OFF) -if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for wxString unless its forced to. - set (_unicode_utf8 ON) -endif() - -prusaslicer_add_cmake_project(wxWidgets - URL https://github.com/prusa3d/wxWidgets/archive/78aa2dc0ea7ce99dc19adc1140f74c3e2e3f3a26.zip - URL_HASH SHA256=94b7d972373503e380e5a8b0ca63b1ccb956da4006402298dd89a0c5c7041b1e - DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG - CMAKE_ARGS - -DwxBUILD_PRECOMP=ON - ${_wx_toolkit} - "-DCMAKE_DEBUG_POSTFIX:STRING=" - -DwxBUILD_DEBUG_LEVEL=0 - -DwxUSE_MEDIACTRL=OFF - -DwxUSE_DETECT_SM=OFF - -DwxUSE_UNICODE=ON - -DwxUSE_UNICODE_UTF8=${_unicode_utf8} - -DwxUSE_OPENGL=ON - -DwxUSE_LIBPNG=sys - -DwxUSE_ZLIB=sys - -DwxUSE_NANOSVG=sys - -DwxUSE_NANOSVG_EXTERNAL=ON - -DwxUSE_REGEX=OFF - -DwxUSE_LIBXPM=builtin - -DwxUSE_LIBJPEG=sys - -DwxUSE_LIBTIFF=sys - -DwxUSE_EXPAT=sys - -DwxUSE_LIBSDL=OFF - -DwxUSE_XTEST=OFF - -DwxUSE_GLCANVAS_EGL=OFF - -DwxUSE_WEBREQUEST=OFF - -DwxUSE_SECRETSTORE=ON -) - -if (MSVC) - add_debug_dep(dep_wxWidgets) -endif () \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9bbddbff90..31e14c3f0f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -48,7 +48,7 @@ if (SLIC3R_GUI) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set (wxWidgets_CONFIG_OPTIONS "--toolkit=gtk${SLIC3R_GTK}") endif () - find_package(wxWidgets 3.2 MODULE REQUIRED COMPONENTS base core adv html gl) + find_package(wxWidgets 3.2 MODULE REQUIRED COMPONENTS base core adv html gl webview media) include(${wxWidgets_USE_FILE}) diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 533c693812..0594bb672e 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -125,6 +125,7 @@ public: TYPE_PHYSICAL_PRINTER, // This type is here to support search through the Preferences TYPE_PREFERENCES, + TYPE_WEBVIEW, }; Type type = TYPE_INVALID; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b2f467b99f..4c297c93dd 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -23,6 +23,12 @@ set(SLIC3R_GUI_SOURCES GUI/Auth.hpp GUI/AuthSession.cpp GUI/AuthSession.hpp + GUI/UserAccount.cpp + GUI/UserAccount.hpp + GUI/WebViewDialog.cpp + GUI/WebViewDialog.hpp + GUI/WebView.cpp + GUI/WebView.hpp GUI/SysInfoDialog.cpp GUI/SysInfoDialog.hpp GUI/KBShortcutsDialog.cpp diff --git a/src/slic3r/GUI/Auth.cpp b/src/slic3r/GUI/Auth.cpp index 1a91aa4761..599b59a993 100644 --- a/src/slic3r/GUI/Auth.cpp +++ b/src/slic3r/GUI/Auth.cpp @@ -2,12 +2,9 @@ #include "GUI_App.hpp" #include "format.hpp" #include "../Utils/Http.hpp" -#include "I18N.hpp" +#include "slic3r/GUI/I18N.hpp" #include -#include -#include -#include #include #include #include @@ -29,6 +26,10 @@ #include #endif // WIN32 +#ifdef __APPLE__ +#include +#endif + #ifdef __linux__ #include #include @@ -38,7 +39,7 @@ namespace fs = boost::filesystem; -namespace pt = boost::property_tree; + namespace Slic3r { namespace GUI { @@ -144,14 +145,13 @@ PrusaAuthCommunication::PrusaAuthCommunication(wxEvtHandler* evt_handler, AppCon refresh_token = app_config->get("refresh_token"); shared_session_key = app_config->get("shared_session_key"); } - if (!access_token.empty() || !refresh_token.empty()) m_remember_session = true; m_session = std::make_unique(evt_handler, access_token, refresh_token, shared_session_key); init_session_thread(); // perform login at the start - do we want this if (m_remember_session) - login(); + do_login(); } PrusaAuthCommunication::~PrusaAuthCommunication() { @@ -182,7 +182,14 @@ void PrusaAuthCommunication::set_username(const std::string& username, AppConfig app_config->set("refresh_token", m_remember_session ? m_session->get_refresh_token() : std::string()); app_config->set("shared_session_key", m_remember_session ? m_session->get_shared_session_key() : std::string()); } - + } +} + +std::string PrusaAuthCommunication::get_access_token() +{ + { + std::lock_guard lock(m_session_mutex); + return m_session->get_access_token(); } } @@ -205,7 +212,7 @@ bool PrusaAuthCommunication::is_logged() { return !m_username.empty(); } -void PrusaAuthCommunication::login() +void PrusaAuthCommunication::do_login() { { std::lock_guard lock(m_session_mutex); @@ -217,7 +224,7 @@ void PrusaAuthCommunication::login() } wakeup_session_thread(); } -void PrusaAuthCommunication::logout() +void PrusaAuthCommunication::do_logout() { { std::lock_guard lock(m_session_mutex); @@ -301,78 +308,9 @@ void PrusaAuthCommunication::wakeup_session_thread() m_thread_stop_condition.notify_all(); } -namespace { -/* -void proccess_tree(const pt::ptree& tree, const std::string depth, std::string& out) -{ - - for (const auto& section : tree) { - printf("%s%s", depth.c_str(), section.first.c_str()); - if (!section.second.data().empty()) { - if (section.first == "printer_type_name") { - out += section.second.data(); - out += " : "; - } else if (section.first == "state") { - out += section.second.data(); - out += "\n"; - } - printf(" : %s\n", section.second.data().c_str()); - } else { - printf("\n"); - proccess_tree(section.second, depth + " ", out); - } - - } -} -*/ -typedef std::map ModelCounter; -void proccess_tree(const pt::ptree& tree, const std::string depth, ModelCounter& models) -{ - for (const auto& section : tree) { - //printf("%s%s", depth.c_str(), section.first.c_str()); - if (!section.second.data().empty()) { - //printf(" : %s\n", section.second.data().c_str()); - } - else { - if (section.first == "printer_type_compatible") { - for (const auto& sub : section.second) { - if (!sub.second.data().empty()) { - //printf(" : %s\n", section.second.data().c_str()); - if(models.find(sub.second.data()) == models.end()) - models.emplace(sub.second.data(), 1); - else - models[sub.second.data()]++; - } - } - } else { - //printf("\n"); - proccess_tree(section.second, depth + " ", models); - } - } - } -} -} -std::string PrusaAuthCommunication::proccess_prusaconnect_printers_message(const std::string& message) -{ - std::string out; - try { - std::stringstream ss(message); - pt::ptree ptree; - pt::read_json(ss, ptree); - - ModelCounter counter; - proccess_tree(ptree, "", counter); - for (const auto model : counter) - { - out += GUI::format("%1%x %2%\n", std::to_string(model.second), model.first); - } - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); - } - return out; -} + + std::string CodeChalengeGenerator::generate_chalenge(const std::string& verifier) { @@ -458,8 +396,28 @@ std::string CodeChalengeGenerator::sha256(const std::string& input) } return output; } -#endif // WIN32 -#ifdef __linux__ +#elif __APPLE__ +std::string CodeChalengeGenerator::sha256(const std::string& input) { + // Initialize the context + CC_SHA256_CTX sha256; + CC_SHA256_Init(&sha256); + + // Update the context with the input data + CC_SHA256_Update(&sha256, input.c_str(), static_cast(input.length())); + + // Finalize the hash and retrieve the result + unsigned char digest[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256_Final(digest, &sha256); + + // Convert the result to a string + char hashString[CC_SHA256_DIGEST_LENGTH * 2 + 1]; + for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { + sprintf(&hashString[i * 2], "%02x", digest[i]); + } + + return std::string(hashString); +} +#else std::string CodeChalengeGenerator::sha256(const std::string& input) { EVP_MD_CTX* mdctx; const EVP_MD* md; diff --git a/src/slic3r/GUI/Auth.hpp b/src/slic3r/GUI/Auth.hpp index 77274a2e32..50a8a9b568 100644 --- a/src/slic3r/GUI/Auth.hpp +++ b/src/slic3r/GUI/Auth.hpp @@ -34,14 +34,13 @@ public: // UI Session thread Interface // bool is_logged(); - void login(); - void logout(); + void do_login(); + void do_logout(); // Trigger function starts various remote operations - // Each user action is implemented in different UserAction class and stored in m_actions. void enqueue_user_id_action(); void enqueue_connect_dummy_action(); void enqueue_connect_printers_action(); - void set_remember_session(bool b) { m_remember_session = b; } + // Callbacks - called from UI after receiving Event from Session thread. Some might use Session thread. // @@ -51,13 +50,15 @@ public: void set_username(const std::string& username, AppConfig* app_config); + void set_remember_session(bool b) { m_remember_session = b; } std::string get_username() const { return m_username; } - - std::string proccess_prusaconnect_printers_message(const std::string& message); + std::string get_access_token(); + + private: - std::unique_ptr m_session; + std::unique_ptr m_session; std::thread m_thread; std::mutex m_session_mutex; std::mutex m_thread_stop_mutex; @@ -73,6 +74,9 @@ private: void init_session_thread(); void login_redirect(); std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; } + + + }; } } diff --git a/src/slic3r/GUI/AuthSession.cpp b/src/slic3r/GUI/AuthSession.cpp index d4ab7f3431..7d24d87eef 100644 --- a/src/slic3r/GUI/AuthSession.cpp +++ b/src/slic3r/GUI/AuthSession.cpp @@ -72,8 +72,11 @@ void UserActionGetWithEvent::perform(const std::string& access_token, UserAction void AuthSession::process_action_queue() { - if (m_priority_action_queue.empty() && m_action_queue.empty()) + BOOST_LOG_TRIVIAL(debug) << "process_action_queue start"; + if (m_priority_action_queue.empty() && m_action_queue.empty()) { + BOOST_LOG_TRIVIAL(debug) << "process_action_queue queues empty"; return; + } if (this->is_initialized()) { // if priority queue already has some action f.e. to exchange tokens, the test should not be neccessary but also shouldn't be problem @@ -86,18 +89,22 @@ void AuthSession::process_action_queue() m_priority_action_queue.pop(); } - if (!this->is_initialized()) + if (!this->is_initialized()) { + BOOST_LOG_TRIVIAL(debug) << "process_action_queue not initialized"; return; + } while (!m_action_queue.empty()) { m_actions[m_action_queue.front().action_id]->perform(m_access_token, m_action_queue.front().success_callback, m_action_queue.front().fail_callback, m_action_queue.front().input); if (!m_action_queue.empty()) m_action_queue.pop(); } + BOOST_LOG_TRIVIAL(debug) << "process_action_queue end"; } void AuthSession::enqueue_action(UserActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) { + BOOST_LOG_TRIVIAL(info) << "enqueue_action " << (int)id; m_action_queue.push({ id, success_callback, fail_callback, input }); } diff --git a/src/slic3r/GUI/AuthSession.hpp b/src/slic3r/GUI/AuthSession.hpp index afb9392e70..4b6b7316d9 100644 --- a/src/slic3r/GUI/AuthSession.hpp +++ b/src/slic3r/GUI/AuthSession.hpp @@ -103,8 +103,8 @@ public: m_actions[UserActionID::CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); m_actions[UserActionID::TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); m_actions[UserActionID::USER_ID] = std::make_unique("USER_ID", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); - m_actions[UserActionID::CONNECT_DUMMY] = std::make_unique("CONNECT_DUMMY", "dev.connect.prusa:8000/slicer/dummy", evt_handler, EVT_PRUSAAUTH_SUCCESS, EVT_PRUSAAUTH_FAIL); - m_actions[UserActionID::CONNECT_PRINTERS] = std::make_unique("CONNECT_PRINTERS", "dev.connect.prusa:8000/slicer/printers", evt_handler, EVT_PRUSACONNECT_PRINTERS_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::CONNECT_DUMMY] = std::make_unique("CONNECT_DUMMY", "https://dev.connect.prusa3d.com/slicer/dummy"/*"dev.connect.prusa:8000/slicer/dummy"*/, evt_handler, EVT_PRUSAAUTH_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::CONNECT_PRINTERS] = std::make_unique("CONNECT_PRINTERS", "https://dev.connect.prusa3d.com/slicer/printers"/*"dev.connect.prusa:8000/slicer/printers"*/, evt_handler, EVT_PRUSACONNECT_PRINTERS_SUCCESS, EVT_PRUSAAUTH_FAIL); } ~AuthSession() { diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 596fa2b697..5971c4f97d 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1536,11 +1536,9 @@ bool PageDownloader::on_finish_downloader() const return m_downloader->on_finish(); } -bool DownloaderUtils::Worker::perform_register(const std::string& path_override/* = {}*/) +bool DownloaderUtils::Worker::perform_register(const std::string& path) { - boost::filesystem::path aux_dest (GUI::into_u8(path_name())); - if (!path_override.empty()) - aux_dest = boost::filesystem::path(path_override); + boost::filesystem::path aux_dest (path); boost::system::error_code ec; boost::filesystem::path chosen_dest = boost::filesystem::absolute(aux_dest, ec); if(ec) @@ -1549,7 +1547,7 @@ bool DownloaderUtils::Worker::perform_register(const std::string& path_override/ if (chosen_dest.empty() || !boost::filesystem::is_directory(chosen_dest, ec) || ec) { std::string err_msg = GUI::format("%1%\n\n%2%",_L("Chosen directory for downloads does not exist.") ,chosen_dest.string()); BOOST_LOG_TRIVIAL(error) << err_msg; - show_error(m_parent, err_msg); + show_error(/*m_parent*/ nullptr, err_msg); return false; } BOOST_LOG_TRIVIAL(info) << "Downloader registration: Directory for downloads: " << chosen_dest.string(); @@ -1613,12 +1611,12 @@ bool DownloaderUtils::Worker::on_finish() { BOOST_LOG_TRIVIAL(debug) << "PageDownloader::on_finish_downloader ac_value " << ac_value << " downloader_checked " << downloader_checked; if (ac_value && downloader_checked) { // already registered but we need to do it again - if (!perform_register()) + if (!perform_register(GUI::into_u8(path_name()))) return false; app_config->set("downloader_url_registered", "1"); } else if (!ac_value && downloader_checked) { // register - if (!perform_register()) + if (!perform_register(GUI::into_u8(path_name()))) return false; app_config->set("downloader_url_registered", "1"); } else if (ac_value && !downloader_checked) { diff --git a/src/slic3r/GUI/ConfigWizard.hpp b/src/slic3r/GUI/ConfigWizard.hpp index 43e3b103af..59b98c8f6f 100644 --- a/src/slic3r/GUI/ConfigWizard.hpp +++ b/src/slic3r/GUI/ConfigWizard.hpp @@ -49,7 +49,7 @@ namespace DownloaderUtils { void set_path_name(const std::string& name); bool on_finish(); - bool perform_register(const std::string& path_override = {}); + static bool perform_register(const std::string& path); #ifdef __linux__ bool get_perform_registration_linux() { return perform_registration_linux; } #endif // __linux__ diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c0115afd8a..db0eeb7bbd 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -98,7 +98,9 @@ #include "Downloader.hpp" #include "PhysicalPrinterDialog.hpp" #include "WifiConfigDialog.hpp" -#include "Auth.hpp" +#include "UserAccount.hpp" +#include "MediaControlPanel.hpp" +#include "WebViewDialog.hpp" #include "BitmapCache.hpp" #include "Notebook.hpp" @@ -2501,6 +2503,8 @@ void GUI_App::add_config_menu(wxMenuBar *menu) updatable_item = local_menu->Append(config_id_base + ConfigMenuConnectDummy, _L("PrusaConnect Printers"), _L("")); updatable_item->Enable(false); m_config_menu_updatable_items.emplace(ConfigMenuIDs::ConfigMenuConnectDummy, updatable_item); + local_menu->Append(config_id_base + ConfigMenuConnectDialog, _L("Connect Dialog"), _L("Connect Dialog")); + local_menu->Append(config_id_base + ConfigMenuMediaDialog, _L("Media Dialog"), _L("Media Dialog")); #if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) //if (DesktopIntegrationDialog::integration_possible()) local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); @@ -2551,15 +2555,15 @@ void GUI_App::add_config_menu(wxMenuBar *menu) break; case ConfigMenuAuthLogin: { - if (this->plater()->get_auth_communication()->is_logged()) - this->plater()->get_auth_communication()->logout(); + if (this->plater()->get_user_account()->is_logged()) + this->plater()->get_user_account()->do_logout(); else - this->plater()->get_auth_communication()->login(); + this->plater()->get_user_account()->do_login(); } break; case ConfigMenuConnectDummy: { - this->plater()->get_auth_communication()->enqueue_connect_printers_action(); + this->plater()->get_user_account()->enqueue_connect_printers_action(); } break; #ifdef __linux__ @@ -2661,6 +2665,13 @@ void GUI_App::add_config_menu(wxMenuBar *menu) */ } break; + case ConfigMenuMediaDialog: + //MediaDialog(nullptr).ShowModal(); + wxMediaPlayerDialog("Media").ShowModal(); + break; + case ConfigMenuConnectDialog: + WebViewDialog(plater()).ShowModal(); + break; default: break; } @@ -2679,8 +2690,8 @@ void GUI_App::add_config_menu(wxMenuBar *menu) } void GUI_App::update_config_menu() { - m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuAuthLogin]->SetItemLabel(this->plater()->get_auth_communication()->is_logged() ? _L("PrusaAuth Log out") : _L("PrusaAuth Log in")); - m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuConnectDummy]->Enable(this->plater()->get_auth_communication()->is_logged()); + m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuAuthLogin]->SetItemLabel(this->plater()->get_user_account()->is_logged() ? _L("PrusaAuth Log out") : _L("PrusaAuth Log in")); + m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuConnectDummy]->Enable(this->plater()->get_user_account()->is_logged()); } void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) { @@ -3465,7 +3476,7 @@ bool GUI_App::open_login_browser_with_dialog(const wxString& url, wxWindow* pare dialog.ShowCheckBox(_L("Remember me"), true); auto answer = dialog.ShowModal(); launch = answer == wxID_YES; - plater()->get_auth_communication()->set_remember_session(dialog.IsCheckBoxChecked()); + plater()->get_user_account()->set_remember_session(dialog.IsCheckBoxChecked()); return launch && wxLaunchDefaultBrowser(url, flags); } @@ -3665,5 +3676,72 @@ void GUI_App::open_wifi_config_dialog(bool forced, const wxString& drive_path/* m_wifi_config_dialog_shown = false; } +void GUI_App::select_printer_with_load(Preset* prst, const std::string& preset_name, const std::string& model_name, const std::string& nozzle_name, const std::string& nozzle) +{ + assert(prst); + if (prst->is_visible) + bool suc = get_tab(Preset::Type::TYPE_PRINTER)->select_preset(preset_name); + else { + AppConfig appconfig_new(AppConfig::EAppMode::Editor); + appconfig_new.set_vendors(*app_config); + prst->vendor->models; + if (auto it = std::find_if(prst->vendor->models.begin(), prst->vendor->models.end(), [model_name](const VendorProfile::PrinterModel& a) { + if (a.name == model_name) + return true; + else + return false; + }); it != prst->vendor->models.end()) + { + appconfig_new.set_variant("PrusaResearch", it->id, nozzle, true); + app_config->set_vendors(appconfig_new); + + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + { it->id, nozzle, "", "" }); + load_current_presets(); + } + } +} + +void GUI_App::handle_web_request(std::string cmd) +{ + BOOST_LOG_TRIVIAL(error) << "Handling web request: " << cmd; + // return to plater + //this->mainframe->select_tab(size_t(0)); + // parse message + std::string model_name = plater()->get_user_account()->get_model_from_json(cmd); + std::string nozzle = plater()->get_user_account()->get_nozzle_from_json(cmd); + std::string nozzle_name = nozzle.empty() ? "" : (nozzle +" nozzle"); + assert(!model_name.empty()); + assert(!nozzle_name.empty()); + if (model_name.empty() && nozzle_name.empty()) + return; + // select printer + std::string preset_name = nozzle.empty() ? model_name : format("%1% %2%",model_name, nozzle_name); + Preset* prst = preset_bundle->printers.find_preset(preset_name, false); + if (!prst) { + model_name = std::string(*preset_bundle->printers.get_preset_name_renamed(model_name)); + preset_name = nozzle.empty() ? model_name : format("%1% %2%", model_name, nozzle_name); + prst = preset_bundle->printers.find_preset(preset_name, false); + } + if (!prst) { + preset_name = model_name; + prst = preset_bundle->printers.find_preset(preset_name, false); + } + if (prst) { + select_printer_with_load(prst, preset_name, model_name, nozzle_name, nozzle); + // notification + std::string out = GUI::format("Select Printer:\n%1%", preset_name); + this->plater()->get_notification_manager()->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + } else { + // notification + std::string out = GUI::format("Printer not found:\n%1%", preset_name); + this->plater()->get_notification_manager()->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + } + + +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6271b9a905..28963c9dea 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -98,8 +98,10 @@ enum ConfigMenuIDs { ConfigMenuTakeSnapshot, ConfigMenuUpdateConf, ConfigMenuUpdateApp, + ConfigMenuMediaDialog, ConfigMenuAuthLogin, ConfigMenuConnectDummy, + ConfigMenuConnectDialog, ConfigMenuDesktopIntegration, ConfigMenuPreferences, ConfigMenuModeSimple, @@ -401,6 +403,24 @@ public: void open_wifi_config_dialog(bool forced, const wxString& drive_path = {}); bool get_wifi_config_dialog_shown() const { return m_wifi_config_dialog_shown; } + + void request_login(bool show_user_info = false) {} + bool check_login() { return false; } + void get_login_info() {} + bool is_user_login() { return true; } + + void request_user_login(int online_login) {} + void request_user_logout() {} + int request_user_unbind(std::string dev_id) { return 0; } + void handle_web_request(std::string cmd); + void select_printer_with_load(Preset* prst, const std::string& preset_name, const std::string& printer_name, const std::string& nozzle_name, const std::string& nozzle ); + void handle_script_message(std::string msg) {} + void request_model_download(std::string import_json) {} + void download_project(std::string project_id) {} + void request_project_download(std::string project_id) {} + void request_open_project(std::string project_id) {} + void request_remove_project(std::string project_id) {} + private: bool on_init_inner(); void init_app_config(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7ca9dc045b..23b9b83a92 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -62,6 +62,9 @@ #include "GalleryDialog.hpp" #include "NotificationManager.hpp" #include "Preferences.hpp" +#include "WebViewDialog.hpp" +#include "MediaControlPanel.hpp" +#include "MediaControl.hpp" #ifdef _WIN32 #include @@ -724,6 +727,10 @@ void MainFrame::init_tabpanel() if (panel == nullptr || (tab != nullptr && !tab->supports_printer_technology(m_plater->printer_technology()))) return; + // temporary fix - WebViewPanel is not inheriting from Tab -> would jump to select Plater + if (panel && !tab) + return; + auto& tabs_list = wxGetApp().tabs_list; if (tab && std::find(tabs_list.begin(), tabs_list.end(), tab) != tabs_list.end()) { // On GTK, the wxEVT_NOTEBOOK_PAGE_CHANGED event is triggered @@ -739,6 +746,13 @@ void MainFrame::init_tabpanel() select_tab(size_t(0)); // select Plater }); + if (wxGetApp().is_editor()) { + + //m_webview = new WebViewPanel(m_tabpanel); + //m_tabpanel->AddPage(m_webview, "web", "cog"/*, "tab_home_active"*/); + //m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); + } + m_plater = new Plater(this, this); m_plater->Hide(); @@ -823,6 +837,13 @@ void MainFrame::create_preset_tabs() add_created_tab(new TabSLAPrint(m_tabpanel), "cog"); add_created_tab(new TabSLAMaterial(m_tabpanel), "resin"); add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); + + m_webview = new WebViewPanel(m_tabpanel); + m_tabpanel->AddPage(m_webview, "Web View"); + /* + m_media = new MediaMainPanel(this); + m_tabpanel->AddPage(m_media, "Media"); + */ } void MainFrame::add_created_tab(Tab* panel, const std::string& bmp_name /*= ""*/) @@ -1510,6 +1531,9 @@ void MainFrame::init_menubar_as_editor() append_menu_check_item(viewMenu, wxID_ANY, _L("&Collapse Sidebar") + sep + "Shift+" + sep_space + "Tab", _L("Collapse sidebar"), [this](wxCommandEvent&) { m_plater->collapse_sidebar(!m_plater->is_sidebar_collapsed()); }, this, []() { return true; }, [this]() { return m_plater->is_sidebar_collapsed(); }, this); + append_menu_check_item(viewMenu, wxID_ANY, _L("&Load URL"), _L("Load URL"), + [this](wxCommandEvent&) { wxString url = "https://dev.connect.prusa3d.com/"/*"file:///C:/Projects/BambuStudio/resources/web/homepage/index.html"*/; m_webview->load_url(url); }, this, + []() { return true; }, []() { return true; }, this); #ifndef __APPLE__ // OSX adds its own menu item to toggle fullscreen. append_menu_check_item(viewMenu, wxID_ANY, _L("&Fullscreen") + "\t" + "F11", _L("Fullscreen"), diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 42d6afd129..6584df4a5c 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -43,6 +43,8 @@ class Plater; class MainFrame; class PreferencesDialog; class GalleryDialog; +class WebViewPanel; +class MediaMainPanel; enum QuickSlice { @@ -219,6 +221,8 @@ public: PreferencesDialog* preferences_dialog { nullptr }; PrintHostQueueDialog* m_printhost_queue_dlg; GalleryDialog* m_gallery_dialog{ nullptr }; + WebViewPanel* m_webview{ nullptr }; + MediaMainPanel* m_media{ nullptr}; #ifdef __APPLE__ std::unique_ptr m_taskbar_icon; diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 9759fd7d1f..2dfefffa35 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -129,7 +129,7 @@ enum class NotificationType // MacOS specific - PS comes forward even when downloader is not allowed URLNotRegistered, // Config file was detected during startup, open wifi config dialog via hypertext - WifiConfigFileDetected + WifiConfigFileDetected, // PrusaAuthUserID, }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 859a55c8c7..1c2ff8e7de 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include @@ -119,7 +121,7 @@ #include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GLGizmoCut.hpp" #include "FileArchiveDialog.hpp" -#include "Auth.hpp" +#include "UserAccount.hpp" #include "DesktopIntegrationDialog.hpp" #ifdef __APPLE__ @@ -264,7 +266,7 @@ struct Plater::priv GLToolbar collapse_toolbar; Preview *preview; std::unique_ptr notification_manager; - std::unique_ptr auth_communication; + std::unique_ptr user_account; ProjectDirtyStateManager dirty_state; @@ -611,7 +613,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) - , auth_communication(std::make_unique(q, wxGetApp().app_config)) + , user_account(std::make_unique(q, wxGetApp().app_config)) , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} , m_sla_import_dlg{new SLAImportDialog{q}} , delayed_scene_refresh(false) @@ -860,7 +862,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); this->q->Bind(EVT_LOGIN_OTHER_INSTANCE, [this](LoginOtherInstanceEvent& evt) { BOOST_LOG_TRIVIAL(trace) << "Received login from other instance event."; - auth_communication->on_login_code_recieved(evt.data); + user_account->on_login_code_recieved(evt.data); }); this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { @@ -870,8 +872,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_OPEN_PRUSAAUTH, [this](OpenPrusaAuthEvent& evt) { BOOST_LOG_TRIVIAL(info) << "open browser: " << evt.data; // first register url to be sure to get the code back - auto downloader_worker = new DownloaderUtils::Worker(nullptr); - downloader_worker->perform_register(wxGetApp().app_config->get("url_downloader_dest")); + //auto downloader_worker = new DownloaderUtils::Worker(nullptr); + DownloaderUtils::Worker::perform_register(wxGetApp().app_config->get("url_downloader_dest")); #ifdef __linux__ if (downloader_worker->get_perform_registration_linux()) DesktopIntegrationDialog::perform_downloader_desktop_integration(); @@ -881,7 +883,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); this->q->Bind(EVT_LOGGEDOUT_PRUSAAUTH, [this](PrusaAuthSuccessEvent& evt) { - auth_communication->set_username({}, wxGetApp().app_config); + user_account->on_logout(wxGetApp().app_config); std::string text = _u8L("Logged out."); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); @@ -889,30 +891,15 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); this->q->Bind(EVT_PA_ID_USER_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { - std::string text; - try { - std::stringstream ss(evt.data); - boost::property_tree::ptree ptree; - boost::property_tree::read_json(ss, ptree); - std::string public_username; - const auto public_username_optional = ptree.get_optional("public_username"); - - if (public_username_optional) - public_username = *public_username_optional; - text = format(_u8L("Logged as %1%."), public_username); - } - catch (const std::exception&) { - BOOST_LOG_TRIVIAL(error) << "UserIDUserAction Could not parse server response."; - } - assert(!text.empty()); - - auth_communication->set_username(evt.data, wxGetApp().app_config); + std::string username = user_account->on_user_id_success(evt.data, wxGetApp().app_config); + std::string text = format(_u8L("Logged as %1%."), username); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); wxGetApp().update_config_menu(); }); this->q->Bind(EVT_PRUSAAUTH_FAIL, [this](PrusaAuthFailEvent& evt) { - auth_communication->set_username({}, wxGetApp().app_config); + BOOST_LOG_TRIVIAL(error) << "Network error message: " << evt.data; + user_account->on_communication_fail(evt.data, wxGetApp().app_config); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::WarningNotificationLevel, evt.data); }); @@ -921,7 +908,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, evt.data); }); this->q->Bind(EVT_PRUSACONNECT_PRINTERS_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { - std::string out = GUI::format( "Printers in your PrusaConnect team:\n%1%", auth_communication->proccess_prusaconnect_printers_message(evt.data)); + BOOST_LOG_TRIVIAL(error) << "PrusaConnect printers message: " << evt.data; + std::string text = user_account->on_connect_printers_success(evt.data, wxGetApp().app_config); + std::string out = GUI::format( "Printers in your PrusaConnect team:\n%1%", text); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); }); @@ -6648,14 +6637,14 @@ const NotificationManager * Plater::get_notification_manager() const return p->notification_manager.get(); } -PrusaAuthCommunication* Plater::get_auth_communication() +UserAccount* Plater::get_user_account() { - return p->auth_communication.get(); + return p->user_account.get(); } -const PrusaAuthCommunication* Plater::get_auth_communication() const +const UserAccount* Plater::get_user_account() const { - return p->auth_communication.get(); + return p->user_account.get(); } void Plater::init_notification_manager() diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index fd0cad7f9a..b29e43e154 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -62,7 +62,7 @@ class Mouse3DController; class NotificationManager; struct Camera; class GLToolbar; -class PrusaAuthCommunication; +class UserAccount; class Plater: public wxPanel { @@ -351,8 +351,8 @@ public: NotificationManager* get_notification_manager(); const NotificationManager* get_notification_manager() const; - PrusaAuthCommunication* get_auth_communication(); - const PrusaAuthCommunication* get_auth_communication() const; + UserAccount* get_user_account(); + const UserAccount* get_user_account() const; void init_notification_manager(); diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp new file mode 100644 index 0000000000..7261f4e4c7 --- /dev/null +++ b/src/slic3r/GUI/UserAccount.cpp @@ -0,0 +1,217 @@ +#include "UserAccount.hpp" + +#include +#include +#include + +namespace pt = boost::property_tree; + +namespace Slic3r { +namespace GUI { + +UserAccount::UserAccount(wxEvtHandler* evt_handler, AppConfig* app_config) + : m_auth_communication(std::make_unique(evt_handler, app_config)) +{} + +UserAccount::~UserAccount() +{} + +void UserAccount::set_username(const std::string& username, AppConfig* app_config) +{ + m_username = username; + m_auth_communication->set_username(username, app_config); +} + +void UserAccount::set_remember_session(bool remember) +{ + m_auth_communication->set_remember_session(remember); +} + +bool UserAccount::is_logged() +{ + return m_auth_communication->is_logged(); +} +void UserAccount::do_login() +{ + m_auth_communication->do_login(); +} +void UserAccount::do_logout() +{ + m_auth_communication->do_logout(); +} + +std::string UserAccount::get_access_token() +{ + return m_auth_communication->get_access_token(); +} + +#if 0 +void UserAccount::enqueue_user_id_action() +{ + m_auth_communication->enqueue_user_id_action(); +} +void UserAccount::enqueue_connect_dummy_action() +{ + m_auth_communication->enqueue_connect_dummy_action(); +} +#endif + +void UserAccount::enqueue_connect_printers_action() +{ + m_auth_communication->enqueue_connect_printers_action(); +} + +void UserAccount::on_login_code_recieved(const std::string& url_message) +{ + m_auth_communication->on_login_code_recieved(url_message); +} + +std::string UserAccount::on_user_id_success(const std::string data, AppConfig* app_config) +{ + std::string public_username; + try { + std::stringstream ss(data); + boost::property_tree::ptree ptree; + boost::property_tree::read_json(ss, ptree); + const auto public_username_optional = ptree.get_optional("public_username"); + if (public_username_optional) + public_username = *public_username_optional; + } + catch (const std::exception&) { + BOOST_LOG_TRIVIAL(error) << "UserIDUserAction Could not parse server response."; + } + assert(!public_username.empty()); + set_username(public_username, app_config); + return public_username; +} + +void UserAccount::on_communication_fail(const std::string data, AppConfig* app_config) +{ + // TODO: should we just declare disconnect on every fail? + //set_username({}, app_config); +} + +void UserAccount::on_logout( AppConfig* app_config) +{ + set_username({}, app_config); +} + +namespace { + std::string parse_tree_for_param(const pt::ptree& tree, const std::string& param) + { + for (const auto& section : tree) { + if (section.first == param) { + return section.second.data(); + } else { + if (std::string res = parse_tree_for_param(section.second, param); !res.empty()) + return res; + } + + } + return {}; + } +} + +std::string UserAccount::on_connect_printers_success(const std::string data, AppConfig* app_config) +{ + + pt::ptree ptree; + try { + std::stringstream ss(data); + pt::read_json(ss, ptree); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); + return {}; + } + // fill m_printer_map with data from ptree + // tree string is in format {"printers": [{..}, {..}]} + m_printer_map.clear(); + assert(ptree.front().first == "printers"); + for (const auto& printer_tree : ptree.front().second) { + std::string name; + ConnectPrinterState state; + std::string type_string = parse_tree_for_param(printer_tree.second, "printer_type"); + std::string state_string = parse_tree_for_param(printer_tree.second, "connect_state"); + + assert(!type_string.empty()); + assert(!state_string.empty()); + if (type_string.empty() || state_string.empty()) + continue; + // name of printer needs to be taken from translate table, if missing + if (auto pair = printer_type_and_name_table.find(type_string); pair != printer_type_and_name_table.end()) { + name = pair->second; + } else { + assert(true); // On this assert, printer_type_and_name_table needs to be updated with type_string and correct printer name + continue; + } + // translate state string to enum value + if (auto pair = printer_state_table.find(state_string); pair != printer_state_table.end()) { + state = pair->second; + } else { + assert(true); // On this assert, printer_state_table and ConnectPrinterState needs to be updated + continue; + } + if (auto counter = m_printer_map.find(name); counter != m_printer_map.end()) { + m_printer_map[name][static_cast(state)]++; + } else { + m_printer_map[name].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); + for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { + m_printer_map[name].push_back(i == static_cast(state) ? 1 : 0); + } + } + } + std::string out; + for (const auto& it : m_printer_map) + { + out += GUI::format("%1%: O%2% I%3% P%4% F%5% \n" + , it.first + , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_OFFLINE)]) + , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_IDLE)]) + , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_PRINTING)]) + , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_FINISHED)])); + } + return out; +} + +std::string UserAccount::get_model_from_json(const std::string& message) const +{ + std::string out; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + + std::string printer_type = parse_tree_for_param(ptree, "printer_type"); + if (auto pair = printer_type_and_name_table.find(printer_type); pair != printer_type_and_name_table.end()) { + out = pair->second; + } + else { + out = parse_tree_for_param(ptree, "printer_type_name"); + } + //assert(!out.empty()); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); + } + return out; +} + +std::string UserAccount::get_nozzle_from_json(const std::string& message) const +{ + std::string out; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + + out = parse_tree_for_param(ptree, "nozzle_diameter"); + //assert(!out.empty()); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); + } + return out; +} + +}} // namespace slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp new file mode 100644 index 0000000000..a598f8257f --- /dev/null +++ b/src/slic3r/GUI/UserAccount.hpp @@ -0,0 +1,78 @@ +#ifndef slic3r_UserAccount_hpp_ +#define slic3r_UserAccount_hpp_ + +#include "Auth.hpp" +#include "libslic3r/AppConfig.hpp" + +#include +#include + +namespace Slic3r{ +namespace GUI{ + +enum class ConnectPrinterState { + CONNECT_PRINTER_OFFLINE, + CONNECT_PRINTER_IDLE, + CONNECT_PRINTER_PRINTING, + CONNECT_PRINTER_FINISHED, + CONNECT_PRINTER_STATE_COUNT +}; + +typedef std::map> ConnectPrinterStateMap; +// Class UserAccount should handle every request for entities outside PrusaSlicer like PrusaAuth or PrusaConnect. +// Outside communication is implemented in class PrusaAuthCommunication in Auth.hpp. +// All incoming data shoud be stored in UserAccount. +class UserAccount { +public: + UserAccount(wxEvtHandler* evt_handler, Slic3r::AppConfig* app_config); + ~UserAccount(); + + bool is_logged(); + void do_login(); + void do_logout(); + + void set_remember_session(bool remember); + +#if 0 + void enqueue_user_id_action(); + void enqueue_connect_dummy_action(); +#endif + void enqueue_connect_printers_action(); + + void on_login_code_recieved(const std::string& url_message); + std::string on_user_id_success(const std::string data, AppConfig* app_config); + void on_communication_fail(const std::string data, AppConfig* app_config); + void on_logout(AppConfig* app_config); + std::string on_connect_printers_success(const std::string data, AppConfig* app_config); + + std::string get_username() const { return m_username; } + std::string get_access_token(); + const ConnectPrinterStateMap& get_printer_state_map() const { return m_printer_map; } + + // standalone utility methods + std::string get_model_from_json(const std::string& message) const; + std::string get_nozzle_from_json(const std::string& message) const; +private: + void set_username(const std::string& username, AppConfig* app_config); + + std::unique_ptr m_auth_communication; + + std::string m_username; + ConnectPrinterStateMap m_printer_map; + + const std::map printer_type_and_name_table = { + {"1.3.0", "Original Prusa i3 MK3"}, + {"1.3.1", "Original Prusa i3 MK3S & MK3S+"}, + {"1.4.0", "Original Prusa MK4"}, + {"2.1.0", "Original Prusa MINI & MINI+"}, + }; + + const std::map printer_state_table = { + {"OFFLINE" , ConnectPrinterState::CONNECT_PRINTER_OFFLINE}, + {"IDLE" , ConnectPrinterState::CONNECT_PRINTER_IDLE}, + {"PRINTING" , ConnectPrinterState::CONNECT_PRINTER_PRINTING}, + {"FINISHED" , ConnectPrinterState::CONNECT_PRINTER_FINISHED}, + }; +}; +}} // namespace slic3r::GUI +#endif // slic3r_UserAccount_hpp_ diff --git a/src/slic3r/GUI/WebView.cpp b/src/slic3r/GUI/WebView.cpp new file mode 100644 index 0000000000..96c279517c --- /dev/null +++ b/src/slic3r/GUI/WebView.cpp @@ -0,0 +1,178 @@ +#include "WebView.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/Utils/MacDarkMode.hpp" + +#include +#include +#include +#include +#include "wx/private/jsscriptwrapper.h" + +#include + +#ifdef __WIN32__ +#include +#elif defined __linux__ +#include +#define WEBKIT_API +struct WebKitWebView; +struct WebKitJavascriptResult; +extern "C" { +WEBKIT_API void +webkit_web_view_run_javascript (WebKitWebView *web_view, + const gchar *script, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +WEBKIT_API WebKitJavascriptResult * +webkit_web_view_run_javascript_finish (WebKitWebView *web_view, + GAsyncResult *result, + GError **error); +WEBKIT_API void +webkit_javascript_result_unref (WebKitJavascriptResult *js_result); +} +#endif + +class FakeWebView : public wxWebView +{ + virtual bool Create(wxWindow* parent, wxWindowID id, const wxString& url, const wxPoint& pos, const wxSize& size, long style, const wxString& name) override { return false; } + virtual wxString GetCurrentTitle() const override { return wxString(); } + virtual wxString GetCurrentURL() const override { return wxString(); } + virtual bool IsBusy() const override { return false; } + virtual bool IsEditable() const override { return false; } + virtual void LoadURL(const wxString& url) override { } + virtual void Print() override { } + virtual void RegisterHandler(wxSharedPtr handler) override { } + virtual void Reload(wxWebViewReloadFlags flags = wxWEBVIEW_RELOAD_DEFAULT) override { } + virtual bool RunScript(const wxString& javascript, wxString* output = NULL) const override { return false; } + virtual void SetEditable(bool enable = true) override { } + virtual void Stop() override { } + virtual bool CanGoBack() const override { return false; } + virtual bool CanGoForward() const override { return false; } + virtual void GoBack() override { } + virtual void GoForward() override { } + virtual void ClearHistory() override { } + virtual void EnableHistory(bool enable = true) override { } + virtual wxVector> GetBackwardHistory() override { return {}; } + virtual wxVector> GetForwardHistory() override { return {}; } + virtual void LoadHistoryItem(wxSharedPtr item) override { } + virtual bool CanSetZoomType(wxWebViewZoomType type) const override { return false; } + virtual float GetZoomFactor() const override { return 0.0f; } + virtual wxWebViewZoomType GetZoomType() const override { return wxWebViewZoomType(); } + virtual void SetZoomFactor(float zoom) override { } + virtual void SetZoomType(wxWebViewZoomType zoomType) override { } + virtual bool CanUndo() const override { return false; } + virtual bool CanRedo() const override { return false; } + virtual void Undo() override { } + virtual void Redo() override { } + virtual void* GetNativeBackend() const override { return nullptr; } + virtual void DoSetPage(const wxString& html, const wxString& baseUrl) override { } +}; + +wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) +{ +#if wxUSE_WEBVIEW_EDGE + // WebView2Loader.dll in exe folder is enough? + /* + // Check if a fixed version of edge is present in + // $executable_path/edge_fixed and use it + wxFileName edgeFixedDir(wxStandardPaths::Get().GetExecutablePath()); + edgeFixedDir.SetFullName(""); + edgeFixedDir.AppendDir("edge_fixed"); + if (edgeFixedDir.DirExists()) { + wxWebViewEdge::MSWSetBrowserExecutableDir(edgeFixedDir.GetFullPath()); + wxLogMessage("Using fixed edge version"); + } + */ +#endif + wxString correct_url = url; +#ifdef __WIN32__ + correct_url.Replace("\\", "/"); +#endif + if (!correct_url.empty()) + correct_url = wxURI(correct_url).BuildURI(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << correct_url.ToUTF8(); + + auto webView = wxWebView::New(); + if (webView) { +#ifdef __WIN32__ + webView->SetUserAgent(wxString::Format("PrusaSlicer/v%s", SLIC3R_VERSION)); + webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); + //We register the wxfs:// protocol for testing purposes + //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); + //And the memory: file system + //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); +#else + // With WKWebView handlers need to be registered before creation + //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); + // And the memory: file system + //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); + webView->Create(parent, wxID_ANY, url2, wxDefaultPosition, wxDefaultSize); + webView->SetUserAgent(wxString::Format("PrusaSlicer/v%s", SLIC3R_VERSION)); +#endif +#ifndef __WIN32__ + Slic3r::GUI::wxGetApp().CallAfter([webView] { +#endif + if (!webView->AddScriptMessageHandler("wx")) { + // TODO: dialog to user + //wxLogError("Could not add script message handler"); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Could not add script message handler"; + } +#ifndef __WIN32__ + }); +#endif + webView->EnableContextMenu(false); + } else { + // TODO: dialog to user + BOOST_LOG_TRIVIAL(error) << "Failed to create wxWebView object. Using Dummy object instead. Webview won't be working."; + webView = new FakeWebView; + } + return webView; +} + +void WebView::LoadUrl(wxWebView * webView, wxString const &url) +{ + auto url2 = url; +#ifdef __WIN32__ + url2.Replace("\\", "/"); +#endif + if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << url2.ToUTF8(); + webView->LoadURL(url2); +} + +bool WebView::run_script(wxWebView *webView, wxString const &javascript) +{ + try { +#ifdef __WIN32__ + ICoreWebView2 * webView2 = (ICoreWebView2 *) webView->GetNativeBackend(); + if (webView2 == nullptr) + return false; + int count = 0; + wxJSScriptWrapper wrapJS(javascript, wxJSScriptWrapper::OutputType::JS_OUTPUT_STRING); + wxString wrapped_code = wrapJS.GetWrappedCode(); + return webView2->ExecuteScript(wrapJS.GetWrappedCode(), NULL) == 0; +#elif defined __WXMAC__ + WKWebView * wkWebView = (WKWebView *) webView->GetNativeBackend(); + int count = 0; + wxJSScriptWrapper wrapJS(javascript, wxJSScriptWrapper::OutputType::JS_OUTPUT_STRING); + Slic3r::GUI::WKWebView_evaluateJavaScript(wkWebView, wrapJS.GetWrappedCode(), nullptr); + return true; +#else + WebKitWebView *wkWebView = (WebKitWebView *) webView->GetNativeBackend(); + webkit_web_view_run_javascript( + wkWebView, javascript.utf8_str(), NULL, + [](GObject *wkWebView, GAsyncResult *res, void *) { + GError * error = NULL; + auto result = webkit_web_view_run_javascript_finish((WebKitWebView*)wkWebView, res, &error); + if (!result) + g_error_free (error); + else + webkit_javascript_result_unref (result); + }, NULL); + return true; +#endif + } catch (std::exception &e) { + return false; + } +} diff --git a/src/slic3r/GUI/WebView.hpp b/src/slic3r/GUI/WebView.hpp new file mode 100644 index 0000000000..21ace5c15f --- /dev/null +++ b/src/slic3r/GUI/WebView.hpp @@ -0,0 +1,16 @@ +#ifndef slic3r_GUI_WebView_hpp_ +#define slic3r_GUI_WebView_hpp_ + +#include + +class WebView +{ +public: + static wxWebView *CreateWebView(wxWindow *parent, wxString const &url); + + static void LoadUrl(wxWebView * webView, wxString const &url); + + static bool run_script(wxWebView * webView, wxString const & msg); +}; + +#endif // !slic3r_GUI_WebView_hpp_ diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp new file mode 100644 index 0000000000..5769d30b83 --- /dev/null +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -0,0 +1,472 @@ +#include "WebViewDialog.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "libslic3r_version.h" +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/UserAccount.hpp" +#include "slic3r/GUI/format.hpp" + +#include +#include +#include + +#include + +#include "slic3r/GUI/WebView.hpp" + + +namespace Slic3r { +namespace GUI { + + +WebViewPanel::WebViewPanel(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + { + wxString url = "https://dev.connect.prusa3d.com/prusa-slicer/printers"; + //std::string strlang = wxGetApp().app_config->get("language"); + //if (strlang != "") + // url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); + //m_bbl_user_agent = wxString::Format("BBL-Slicer/v%s", SLIC3R_VERSION); + + wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); +#ifdef DEBUG_URL_PANEL + // Create the button + bSizer_toolbar = new wxBoxSizer(wxHORIZONTAL); + + m_button_back = new wxButton(this, wxID_ANY, wxT("Back"), wxDefaultPosition, wxDefaultSize, 0); + m_button_back->Enable(false); + bSizer_toolbar->Add(m_button_back, 0, wxALL, 5); + + m_button_forward = new wxButton(this, wxID_ANY, wxT("Forward"), wxDefaultPosition, wxDefaultSize, 0); + m_button_forward->Enable(false); + bSizer_toolbar->Add(m_button_forward, 0, wxALL, 5); + + m_button_stop = new wxButton(this, wxID_ANY, wxT("Stop"), wxDefaultPosition, wxDefaultSize, 0); + + bSizer_toolbar->Add(m_button_stop, 0, wxALL, 5); + + m_button_reload = new wxButton(this, wxID_ANY, wxT("Reload"), wxDefaultPosition, wxDefaultSize, 0); + bSizer_toolbar->Add(m_button_reload, 0, wxALL, 5); + + m_url = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + bSizer_toolbar->Add(m_url, 1, wxALL | wxEXPAND, 5); + + m_button_tools = new wxButton(this, wxID_ANY, wxT("Tools"), wxDefaultPosition, wxDefaultSize, 0); + bSizer_toolbar->Add(m_button_tools, 0, wxALL, 5); + + // Create panel for find toolbar. + wxPanel* panel = new wxPanel(this); + topsizer->Add(bSizer_toolbar, 0, wxEXPAND, 0); + topsizer->Add(panel, wxSizerFlags().Expand()); + + // Create sizer for panel. + wxBoxSizer* panel_sizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(panel_sizer); + + // Create the info panel + m_info = new wxInfoBar(this); + topsizer->Add(m_info, wxSizerFlags().Expand()); +#endif + + // Create the webview + m_browser = WebView::CreateWebView(this, url); + if (m_browser == nullptr) { + wxLogError("Could not init m_browser"); + return; + } + + SetSizer(topsizer); + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + +#ifdef DEBUG_URL_PANEL + // Create the Tools menu + m_tools_menu = new wxMenu(); + wxMenuItem* viewSource = m_tools_menu->Append(wxID_ANY, _L("View Source")); + wxMenuItem* viewText = m_tools_menu->Append(wxID_ANY, _L("View Text")); + m_tools_menu->AppendSeparator(); + + wxMenu* script_menu = new wxMenu; + + m_script_custom = script_menu->Append(wxID_ANY, "Custom script"); + m_tools_menu->AppendSubMenu(script_menu, _L("Run Script")); + wxMenuItem* addUserScript = m_tools_menu->Append(wxID_ANY, _L("Add user script")); + wxMenuItem* setCustomUserAgent = m_tools_menu->Append(wxID_ANY, _L("Set custom user agent")); + +#endif + //Zoom + m_zoomFactor = 100; + + + Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); + + // Connect the webview events + Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::on_error, this, m_browser->GetId()); + Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::on_script_message, this, m_browser->GetId()); + +#ifdef DEBUG_URL_PANEL + // Connect the button events + Bind(wxEVT_BUTTON, &WebViewPanel::on_back, this, m_button_back->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_forward, this, m_button_forward->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_stop, this, m_button_stop->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_reload, this, m_button_reload->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_tools_clicked, this, m_button_tools->GetId()); + Bind(wxEVT_TEXT_ENTER, &WebViewPanel::on_url, this, m_url->GetId()); + + // Connect the menu events + Bind(wxEVT_MENU, &WebViewPanel::on_view_source_request, this, viewSource->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::on_view_text_request, this, viewText->GetId()); + + Bind(wxEVT_MENU, &WebViewPanel::on_run_script_custom, this, m_script_custom->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::on_add_user_script, this, addUserScript->GetId()); +#endif + //Connect the idle events + Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); + Bind(wxEVT_CLOSE_WINDOW, &WebViewPanel::on_close, this); + + m_LoginUpdateTimer = nullptr; + } + +WebViewPanel::~WebViewPanel() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Start"; + SetEvtHandlerEnabled(false); +#ifdef DEBUG_URL_PANEL + delete m_tools_menu; + + if (m_LoginUpdateTimer != nullptr) { + m_LoginUpdateTimer->Stop(); + delete m_LoginUpdateTimer; + m_LoginUpdateTimer = NULL; + } +#endif + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " End"; +} + + +void WebViewPanel::load_url(wxString& url) +{ + this->Show(); + this->Raise(); +#ifdef DEBUG_URL_PANEL + m_url->SetLabelText(url); +#endif + m_browser->LoadURL(url); + m_browser->SetFocus(); +} + +void WebViewPanel::on_show(wxShowEvent& evt) +{ + // run script with access token to login + if (evt.IsShown()) { + std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); + // TODO: should this be happening every OnShow? + run_script(script); + } + +} + +void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) +{ + if (m_browser->IsBusy()) + wxSetCursor(wxCURSOR_ARROWWAIT); + else + wxSetCursor(wxNullCursor); + +#ifdef DEBUG_URL_PANEL + m_button_stop->Enable(m_browser->IsBusy()); +#endif +} + +/** + * Callback invoked when user entered an URL and pressed enter + */ +void WebViewPanel::on_url(wxCommandEvent& WXUNUSED(evt)) +{ +#ifdef DEBUG_URL_PANEL + m_browser->LoadURL(m_url->GetValue()); + m_browser->SetFocus(); +#endif +} + +/** + * Callback invoked when user pressed the "back" button + */ +void WebViewPanel::on_back(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->GoBack(); +} + +/** + * Callback invoked when user pressed the "forward" button + */ +void WebViewPanel::on_forward(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->GoForward(); +} + +/** + * Callback invoked when user pressed the "stop" button + */ +void WebViewPanel::on_stop(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->Stop(); +} + +/** + * Callback invoked when user pressed the "reload" button + */ +void WebViewPanel::on_reload(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->Reload(); +} + + + +void WebViewPanel::on_close(wxCloseEvent& evt) +{ + this->Hide(); +} + + +void WebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + wxGetApp().handle_web_request(evt.GetString().ToUTF8().data()); +} + + + +/** + * Invoked when user selects the "View Source" menu item + */ +void WebViewPanel::on_view_source_request(wxCommandEvent& WXUNUSED(evt)) +{ + SourceViewDialog dlg(this, m_browser->GetPageSource()); + dlg.ShowModal(); +} + +/** + * Invoked when user selects the "View Text" menu item + */ +void WebViewPanel::on_view_text_request(wxCommandEvent& WXUNUSED(evt)) +{ + wxDialog textViewDialog(this, wxID_ANY, "Page Text", + wxDefaultPosition, wxSize(700, 500), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); + + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, m_browser->GetPageText(), + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | + wxTE_RICH | + wxTE_READONLY); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(text, 1, wxEXPAND); + SetSizer(sizer); + textViewDialog.ShowModal(); +} + +/** + * Invoked when user selects the "Menu" item + */ +void WebViewPanel::on_tools_clicked(wxCommandEvent& WXUNUSED(evt)) +{ +#ifdef DEBUG_URL_PANEL + wxPoint position = ScreenToClient(wxGetMousePosition()); + PopupMenu(m_tools_menu, position.x, position.y); +#endif +} + +void WebViewPanel::run_script(const wxString& javascript) +{ + // Remember the script we run in any case, so the next time the user opens + // the "Run Script" dialog box, it is shown there for convenient updating. + m_javascript = javascript; + + if (!m_browser) return; + + bool res = WebView::run_script(m_browser, javascript); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << " " << res; +} + + +void WebViewPanel::on_run_script_custom(wxCommandEvent& WXUNUSED(evt)) +{ + wxTextEntryDialog dialog + ( + this, + "Please enter JavaScript code to execute", + wxGetTextFromUserPromptStr, + m_javascript, + wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + run_script(dialog.GetValue()); +} + +void WebViewPanel::on_add_user_script(wxCommandEvent& WXUNUSED(evt)) +{ + wxString userScript = "window.wx_test_var = 'wxWidgets webview sample';"; + wxTextEntryDialog dialog + ( + this, + "Enter the JavaScript code to run as the initialization script that runs before any script in the HTML document.", + wxGetTextFromUserPromptStr, + userScript, + wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + if (!m_browser->AddUserScript(dialog.GetValue())) + wxLogError("Could not add user script"); +} + +void WebViewPanel::on_set_custom_user_agent(wxCommandEvent& WXUNUSED(evt)) +{ + wxString customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1"; + wxTextEntryDialog dialog + ( + this, + "Enter the custom user agent string you would like to use.", + wxGetTextFromUserPromptStr, + customUserAgent, + wxOK | wxCANCEL | wxCENTRE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + if (!m_browser->SetUserAgent(customUserAgent)) + wxLogError("Could not set custom user agent"); +} + +void WebViewPanel::on_clear_selection(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->ClearSelection(); +} + +void WebViewPanel::on_delete_selection(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->DeleteSelection(); +} + +void WebViewPanel::on_select_all(wxCommandEvent& WXUNUSED(evt)) +{ + m_browser->SelectAll(); +} + +/** + * Callback invoked when a loading error occurs + */ +void WebViewPanel::on_error(wxWebViewEvent& evt) +{ +#define WX_ERROR_CASE(type) \ +case type: \ + category = #type; \ + break; + + wxString category; + switch (evt.GetInt()) + { + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CONNECTION); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CERTIFICATE); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_AUTH); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_SECURITY); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_NOT_FOUND); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_REQUEST); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_USER_CANCELLED); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_OTHER); + } + + //Show the info bar with an error +#ifdef DEBUG_URL_PANEL + + m_info->ShowMessage(_L("An error occurred loading ") + evt.GetURL() + "\n" + + "'" + category + "'", wxICON_ERROR); +#endif +} + + +SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : + wxDialog(parent, wxID_ANY, "Source Code", + wxDefaultPosition, wxSize(700,500), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, source, + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | + wxTE_RICH | + wxTE_READONLY); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(text, 1, wxEXPAND); + SetSizer(sizer); +} + + +WebViewDialog::WebViewDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, "Webview Dialog", wxDefaultPosition, wxSize(1366, 768)/* wxSize(100 * wxGetApp().em_unit(), 100 * wxGetApp().em_unit())*/) +{ + wxString url = "https://dev.connect.prusa3d.com/prusa-slicer/printers"; + ////std::string strlang = wxGetApp().app_config->get("language"); + ////if (strlang != "") + //// url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); + ////m_bbl_user_agent = wxString::Format("BBL-Slicer/v%s", SLIC3R_VERSION); + + wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); + + + + // Create the webview + m_browser = WebView::CreateWebView(this, url); + if (m_browser == nullptr) { + wxLogError("Could not init m_browser"); + return; + } + + SetSizer(topsizer); + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + + Bind(wxEVT_SHOW, &WebViewDialog::on_show, this); + Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewDialog::on_script_message, this, m_browser->GetId()); +} + +WebViewDialog::~WebViewDialog() +{ + +} + +void WebViewDialog::on_show(wxShowEvent& evt) +{ + if (evt.IsShown()) { + std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); + // TODO: should this be happening every OnShow? + run_script(script); + } + +} + +void WebViewDialog::run_script(const wxString& javascript) +{ + if (!m_browser) + return; + bool res = WebView::run_script(m_browser, javascript); +} + +void WebViewDialog::on_script_message(wxWebViewEvent& evt) +{ + this->EndModal(wxID_OK); +} + +} // GUI +} // Slic3r diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp new file mode 100644 index 0000000000..f7b6908e52 --- /dev/null +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -0,0 +1,124 @@ +#ifndef slic3r_WebViewDialog_hpp_ +#define slic3r_WebViewDialog_hpp_ + + +#include "wx/artprov.h" +#include "wx/cmdline.h" +#include "wx/notifmsg.h" +#include "wx/settings.h" +#include "wx/webview.h" + +#if wxUSE_WEBVIEW_EDGE +#include "wx/msw/webview_edge.h" +#endif + +#include "wx/webviewarchivehandler.h" +#include "wx/webviewfshandler.h" +#include "wx/numdlg.h" +#include "wx/infobar.h" +#include "wx/filesys.h" +#include "wx/fs_arc.h" +#include "wx/fs_mem.h" +#include "wx/stdpaths.h" +#include +#include +#include "wx/textctrl.h" +#include + + +namespace Slic3r { +namespace GUI { + +#define DEBUG_URL_PANEL +class WebViewPanel : public wxPanel +{ +public: + WebViewPanel(wxWindow *parent); + virtual ~WebViewPanel(); + + void load_url(wxString& url); + + void on_show(wxShowEvent& evt); + + void on_idle(wxIdleEvent& evt); + void on_url(wxCommandEvent& evt); + void on_back(wxCommandEvent& evt); + void on_forward(wxCommandEvent& evt); + void on_stop(wxCommandEvent& evt); + void on_reload(wxCommandEvent& evt); + + void on_script_message(wxWebViewEvent& evt); + void on_view_source_request(wxCommandEvent& evt); + void on_view_text_request(wxCommandEvent& evt); + void on_tools_clicked(wxCommandEvent& evt); + void on_error(wxWebViewEvent& evt); + + void run_script(const wxString& javascript); + void on_run_script_custom(wxCommandEvent& evt); + void on_add_user_script(wxCommandEvent& evt); + void on_set_custom_user_agent(wxCommandEvent& evt); + void on_clear_selection(wxCommandEvent& evt); + void on_delete_selection(wxCommandEvent& evt); + void on_select_all(wxCommandEvent& evt); + + void on_close(wxCloseEvent& evt); + + wxTimer * m_LoginUpdateTimer{nullptr}; + +private: + + wxWebView* m_browser; +#ifdef DEBUG_URL_PANEL + + wxBoxSizer *bSizer_toolbar; + wxButton * m_button_back; + wxButton * m_button_forward; + wxButton * m_button_stop; + wxButton * m_button_reload; + wxTextCtrl *m_url; + wxButton * m_button_tools; + + wxMenu* m_tools_menu; + wxMenuItem* m_script_custom; + + + wxInfoBar *m_info; + wxStaticText* m_info_text; +#endif + long m_zoomFactor; + + // Last executed JavaScript snippet, for convenience. + wxString m_javascript; + wxString m_response_js; + + wxString m_bbl_user_agent; + + //DECLARE_EVENT_TABLE() +}; + + +class WebViewDialog : public wxDialog +{ +public: + WebViewDialog(wxWindow* parent); + virtual ~WebViewDialog(); + + void on_show(wxShowEvent& evt); + void run_script(const wxString& javascript); + void on_script_message(wxWebViewEvent& evt); + +private: + wxWebView* m_browser; +}; + + +class SourceViewDialog : public wxDialog +{ +public: + SourceViewDialog(wxWindow* parent, wxString source); +}; + +} // GUI +} // Slic3r + +#endif /* slic3r_Tab_hpp_ */ diff --git a/src/slic3r/Utils/MacDarkMode.hpp b/src/slic3r/Utils/MacDarkMode.hpp index 0a233049f3..9e3701ea91 100644 --- a/src/slic3r/Utils/MacDarkMode.hpp +++ b/src/slic3r/Utils/MacDarkMode.hpp @@ -5,12 +5,15 @@ #ifndef slic3r_MacDarkMode_hpp_ #define slic3r_MacDarkMode_hpp_ +#include + namespace Slic3r { namespace GUI { #if __APPLE__ extern bool mac_dark_mode(); extern double mac_max_scaling_factor(); +void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &)); #endif diff --git a/src/slic3r/Utils/MacDarkMode.mm b/src/slic3r/Utils/MacDarkMode.mm index 512e96b94d..96f421d035 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -1,5 +1,7 @@ #import "MacDarkMode.hpp" +#include "wx/osx/core/cfstring.h" + #import #import @@ -33,6 +35,16 @@ double mac_max_scaling_factor() return scaling; } +void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &)) +{ + [(WKWebView*)web evaluateJavaScript:wxCFStringRef(script).AsNSString() completionHandler: ^(id result, NSError *error) { + if (callback && error != nil) { + wxString err = wxCFStringRef(error.localizedFailureReason).AsString(); + callback(err); + } + }]; +} + } } From e774d25f7aff7806117eed49494e4abd1a8d588e Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 8 Dec 2023 11:32:26 +0100 Subject: [PATCH 04/64] Implemented UI to show a state of printers in PrusaConnect --- src/libslic3r/Preset.cpp | 6 ++ src/libslic3r/Preset.hpp | 2 + src/slic3r/GUI/GUI_App.cpp | 74 ++++++++-------------- src/slic3r/GUI/GUI_App.hpp | 3 +- src/slic3r/GUI/Plater.cpp | 2 + src/slic3r/GUI/PresetComboBoxes.cpp | 96 +++++++++++++++++++++++++++++ src/slic3r/GUI/PresetComboBoxes.hpp | 2 + src/slic3r/GUI/Sidebar.cpp | 10 +++ src/slic3r/GUI/Sidebar.hpp | 1 + src/slic3r/GUI/UserAccount.hpp | 31 ++++++++-- 10 files changed, 170 insertions(+), 57 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index bc428a73f2..3f26b3f35c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1211,6 +1211,12 @@ Preset* PresetCollection::find_preset(const std::string &name, bool first_visibl first_visible_if_not_found ? &this->first_visible() : nullptr; } +size_t PresetCollection::get_preset_idx_by_name(const std::string name) const +{ + auto it = this->find_preset_internal(name); + return it != m_presets.end() ? it - m_presets.begin() : size_t(-1); +} + // Return index of the first visible preset. Certainly at least the '- default -' preset shall be visible. size_t PresetCollection::first_visible_idx() const { diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 0594bb672e..13f1185031 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -437,6 +437,8 @@ public: const Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false, bool respect_active_preset = true) const { return const_cast(this)->find_preset(name, first_visible_if_not_found, respect_active_preset); } + size_t get_preset_idx_by_name(const std::string preset_name) const; + size_t first_visible_idx() const; // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. // If one of the prefered_alternates is compatible, select it. diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index db0eeb7bbd..7880d07606 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3676,71 +3676,45 @@ void GUI_App::open_wifi_config_dialog(bool forced, const wxString& drive_path/* m_wifi_config_dialog_shown = false; } -void GUI_App::select_printer_with_load(Preset* prst, const std::string& preset_name, const std::string& model_name, const std::string& nozzle_name, const std::string& nozzle) +bool GUI_App::select_printer_from_connect(const Preset* preset) { - assert(prst); - if (prst->is_visible) - bool suc = get_tab(Preset::Type::TYPE_PRINTER)->select_preset(preset_name); - else { - AppConfig appconfig_new(AppConfig::EAppMode::Editor); - appconfig_new.set_vendors(*app_config); - prst->vendor->models; - if (auto it = std::find_if(prst->vendor->models.begin(), prst->vendor->models.end(), [model_name](const VendorProfile::PrinterModel& a) { - if (a.name == model_name) - return true; - else - return false; - }); it != prst->vendor->models.end()) - { - appconfig_new.set_variant("PrusaResearch", it->id, nozzle, true); - app_config->set_vendors(appconfig_new); + assert(preset); - preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, - { it->id, nozzle, "", "" }); - load_current_presets(); - } + bool is_installed{ false }; + + if (!preset->is_visible) { + size_t preset_id = preset_bundle->printers.get_preset_idx_by_name(preset->name); + assert(preset_id != size_t(-1)); + preset_bundle->printers.select_preset(preset_id); + is_installed = true; } + + get_tab(Preset::Type::TYPE_PRINTER)->select_preset(preset->name); + return is_installed; } void GUI_App::handle_web_request(std::string cmd) { BOOST_LOG_TRIVIAL(error) << "Handling web request: " << cmd; // return to plater - //this->mainframe->select_tab(size_t(0)); + this->mainframe->select_tab(size_t(0)); // parse message std::string model_name = plater()->get_user_account()->get_model_from_json(cmd); std::string nozzle = plater()->get_user_account()->get_nozzle_from_json(cmd); - std::string nozzle_name = nozzle.empty() ? "" : (nozzle +" nozzle"); assert(!model_name.empty()); - assert(!nozzle_name.empty()); - if (model_name.empty() && nozzle_name.empty()) + if (model_name.empty()) return; + // select printer - std::string preset_name = nozzle.empty() ? model_name : format("%1% %2%",model_name, nozzle_name); - Preset* prst = preset_bundle->printers.find_preset(preset_name, false); - if (!prst) { - model_name = std::string(*preset_bundle->printers.get_preset_name_renamed(model_name)); - preset_name = nozzle.empty() ? model_name : format("%1% %2%", model_name, nozzle_name); - prst = preset_bundle->printers.find_preset(preset_name, false); - } - if (!prst) { - preset_name = model_name; - prst = preset_bundle->printers.find_preset(preset_name, false); - } - if (prst) { - select_printer_with_load(prst, preset_name, model_name, nozzle_name, nozzle); - // notification - std::string out = GUI::format("Select Printer:\n%1%", preset_name); - this->plater()->get_notification_manager()->close_notification_of_type(NotificationType::PrusaAuthUserID); - this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); - } else { - // notification - std::string out = GUI::format("Printer not found:\n%1%", preset_name); - this->plater()->get_notification_manager()->close_notification_of_type(NotificationType::PrusaAuthUserID); - this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); - } - - + const Preset* preset = preset_bundle->printers.find_system_preset_by_model_and_variant(model_name, nozzle); + bool is_installed = preset && select_printer_from_connect(preset); + // notification + std::string out = preset ? + (is_installed ? GUI::format(_L("Installed and Select Printer:\n%1%"), preset->name) : + GUI::format(_L("Select Printer:\n%1%"), preset->name) ): + GUI::format(_L("Printer not found:\n%1%"), model_name); + this->plater()->get_notification_manager()->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); } } // GUI diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 28963c9dea..007ba7f8d9 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -413,7 +413,8 @@ public: void request_user_logout() {} int request_user_unbind(std::string dev_id) { return 0; } void handle_web_request(std::string cmd); - void select_printer_with_load(Preset* prst, const std::string& preset_name, const std::string& printer_name, const std::string& nozzle_name, const std::string& nozzle ); + // return true if preset vas invisible and we have to installed it to make it selectable + bool select_printer_from_connect(const Preset* printer_preset); void handle_script_message(std::string msg) {} void request_model_download(std::string import_json) {} void download_project(std::string project_id) {} diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1c2ff8e7de..94926e7643 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -913,6 +913,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string out = GUI::format( "Printers in your PrusaConnect team:\n%1%", text); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + + sidebar->update_printer_presets_combobox(); }); wxGetApp().other_instance_message_handler()->init(this->q); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c40978b03d..875c6ad778 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -44,6 +45,7 @@ #include "BitmapCache.hpp" #include "PhysicalPrinterDialog.hpp" #include "MsgDialog.hpp" +#include "UserAccount.hpp" #include "Widgets/ComboBox.hpp" @@ -618,6 +620,12 @@ bool PresetComboBox::selection_is_changed_according_to_physical_printers() // *** PlaterPresetComboBox *** // --------------------------------- +static bool is_active_connect() +{ + auto user_account = wxGetApp().plater()->get_user_account(); + return user_account && user_account->is_logged(); +} + PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset_type) : PresetComboBox(parent, preset_type, wxSize(15 * wxGetApp().em_unit(), -1)) { @@ -657,6 +665,9 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset else switch_to_tab(); }); + + if (m_type == Preset::TYPE_PRINTER) + connect_info = new wxGenericStaticText(parent, wxID_ANY, "Info about Connect for printer preset"); } PlaterPresetComboBox::~PlaterPresetComboBox() @@ -832,6 +843,83 @@ wxString PlaterPresetComboBox::get_preset_name(const Preset& preset) return from_u8(name + suffix(preset)); } + +struct PrinterStatesCount +{ + size_t offline_cnt { 0 }; + size_t busy_cnt { 0 }; + size_t available_cnt { 0 }; + size_t total { 0 }; +}; + +static PrinterStatesCount get_printe_states_count(const std::vector& states) +{ + PrinterStatesCount states_cnt; + + for (size_t i = 0; i < states.size(); i++) { + if (states[i] == 0) + continue; + + ConnectPrinterState state = static_cast(i); + + if (state == ConnectPrinterState::CONNECT_PRINTER_OFFLINE) + states_cnt.offline_cnt += states[i]; + else if (state == ConnectPrinterState::CONNECT_PRINTER_PAUSED || + state == ConnectPrinterState::CONNECT_PRINTER_STOPED || + state == ConnectPrinterState::CONNECT_PRINTER_PRINTING) + states_cnt.busy_cnt += states[i]; + else + states_cnt.available_cnt += states[i]; + } + states_cnt.total = states_cnt.offline_cnt + states_cnt.busy_cnt + states_cnt.available_cnt; + + return states_cnt; +} + +static std::string get_connect_state_suffix_for_printer(const Preset& printer_preset) +{ + // process real data from Connect + if (auto printer_state_map = wxGetApp().plater()->get_user_account()->get_printer_state_map(); + !printer_state_map.empty()) { + + for (const auto& [printer_model_id, states] : printer_state_map) { + if (printer_model_id == printer_preset.config.opt_string("printer_model")) { + + PrinterStatesCount states_cnt = get_printe_states_count(states); + + if (states_cnt.available_cnt > 0) + return "_available"; + if (states_cnt.busy_cnt > 0) + return "_busy"; + return "_offline"; + } + } + } + + return ""; +} + +static wxString get_connect_info_line(const Preset& printer_preset) +{ + if (auto printer_state_map = wxGetApp().plater()->get_user_account()->get_printer_state_map(); + !printer_state_map.empty()) { + + for (const auto& [printer_model_id, states] : printer_state_map) { + if (printer_model_id == printer_preset.config.opt_string("printer_model")) { + + PrinterStatesCount states_cnt = get_printe_states_count(states); + + return format_wxstr(_L("Available: %1%, Offline: %2%, Busy: %3%. Total: %4% printers"), + format("%1%" , states_cnt.available_cnt), + format("%1%" , states_cnt.offline_cnt), + format("%1%", states_cnt.busy_cnt), + format("%1%", states_cnt.total)); + } + } + } + return " "; // to correct update of strinh height don't use empty string +} + // Only the compatible presets are shown. // If an incompatible preset is selected, it is shown as well. void PlaterPresetComboBox::update() @@ -904,6 +992,12 @@ void PlaterPresetComboBox::update() std::string bitmap_key, filament_rgb, extruder_rgb, material_rgb; std::string bitmap_type_name = bitmap_key = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + if (m_type == Preset::TYPE_PRINTER) { + bitmap_type_name = bitmap_key += get_connect_state_suffix_for_printer(preset); + if (is_selected) + connect_info->SetLabelMarkup(get_connect_info_line(preset)); + } + bool single_bar = false; if (m_type == Preset::TYPE_FILAMENT) { @@ -1032,6 +1126,8 @@ void PlaterPresetComboBox::update() validate_selection(data.selected); } } + + connect_info->Show(is_active_connect()); } if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_SLA_MATERIAL) { diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 421d96d481..5a9821b7de 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -16,6 +16,7 @@ class wxString; class wxTextCtrl; class wxStaticText; +class wxGenericStaticText; class ScalableButton; class wxBoxSizer; class wxComboBox; @@ -153,6 +154,7 @@ public: ~PlaterPresetComboBox(); ScalableButton* edit_btn { nullptr }; + wxGenericStaticText* connect_info { nullptr }; void set_extruder_idx(const int extr_idx) { m_extruder_idx = extr_idx; } int get_extruder_idx() const { return m_extruder_idx; } diff --git a/src/slic3r/GUI/Sidebar.cpp b/src/slic3r/GUI/Sidebar.cpp index 11eabb7f0a..87d026794d 100644 --- a/src/slic3r/GUI/Sidebar.cpp +++ b/src/slic3r/GUI/Sidebar.cpp @@ -32,6 +32,7 @@ #include #include #include +#include "wx/generic/stattextg.h" #ifdef _WIN32 #include #include @@ -381,6 +382,9 @@ Sidebar::Sidebar(Plater *parent) wxBOTTOM, 1); (void)margin_5; // supress unused capture warning #endif // __WXGTK3__ + if ((*combo)->connect_info) + sizer_presets->Add((*combo)->connect_info, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxBOTTOM, + int(0.3 * wxGetApp().em_unit())); } else { sizer_filaments->Add(combo_and_btn_sizer, 0, wxEXPAND | #ifdef __WXGTK3__ @@ -603,6 +607,12 @@ void Sidebar::update_all_preset_comboboxes() } } +void Sidebar::update_printer_presets_combobox() +{ + m_combo_printer->update(); + Layout(); +} + void Sidebar::update_presets(Preset::Type preset_type) { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; diff --git a/src/slic3r/GUI/Sidebar.hpp b/src/slic3r/GUI/Sidebar.hpp index 826a2015ed..2ec2af653f 100644 --- a/src/slic3r/GUI/Sidebar.hpp +++ b/src/slic3r/GUI/Sidebar.hpp @@ -133,6 +133,7 @@ public: void update_objects_list_extruder_column(size_t extruders_count); void update_presets(Preset::Type preset_type); void update_mode_markers(); + void update_printer_presets_combobox(); void msw_rescale(); void sys_color_changed(); diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index a598f8257f..ae5b1f9e14 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -12,9 +12,12 @@ namespace GUI{ enum class ConnectPrinterState { CONNECT_PRINTER_OFFLINE, - CONNECT_PRINTER_IDLE, CONNECT_PRINTER_PRINTING, + CONNECT_PRINTER_PAUSED,//? + CONNECT_PRINTER_STOPED,//? + CONNECT_PRINTER_IDLE, CONNECT_PRINTER_FINISHED, + CONNECT_PRINTER_READY, //? CONNECT_PRINTER_STATE_COUNT }; @@ -61,17 +64,33 @@ private: ConnectPrinterStateMap m_printer_map; const std::map printer_type_and_name_table = { - {"1.3.0", "Original Prusa i3 MK3"}, - {"1.3.1", "Original Prusa i3 MK3S & MK3S+"}, - {"1.4.0", "Original Prusa MK4"}, - {"2.1.0", "Original Prusa MINI & MINI+"}, + {"1.3.0", "MK3" }, + {"1.3.1", "MK3S" }, + {"1.4.0", "MK4" }, + {"2.1.0", "MINI" }, + // ysFIXME : needs to add Connect ids for next printers + {"0.0.0", "MK4IS" }, + {"0.0.0", "MK3SMMU2S" }, + {"0.0.0", "MK3MMU2" }, + {"0.0.0", "XL" }, + {"0.0.0", "MK2.5S" }, + {"0.0.0", "MK2.5" }, + {"0.0.0", "MK2.5SMMU2S" }, + {"0.0.0", "MK2.5MMU2" }, + {"0.0.0", "MK2S" }, + {"0.0.0", "MK2SMM" }, + {"0.0.0", "SL1" }, + {"0.0.0", "SL1S" }, }; const std::map printer_state_table = { {"OFFLINE" , ConnectPrinterState::CONNECT_PRINTER_OFFLINE}, - {"IDLE" , ConnectPrinterState::CONNECT_PRINTER_IDLE}, {"PRINTING" , ConnectPrinterState::CONNECT_PRINTER_PRINTING}, + {"PAUSED" , ConnectPrinterState::CONNECT_PRINTER_PAUSED}, + {"STOPED" , ConnectPrinterState::CONNECT_PRINTER_STOPED}, + {"IDLE" , ConnectPrinterState::CONNECT_PRINTER_IDLE}, {"FINISHED" , ConnectPrinterState::CONNECT_PRINTER_FINISHED}, + {"READY" , ConnectPrinterState::CONNECT_PRINTER_READY}, }; }; }} // namespace slic3r::GUI From 5f34abf4735ffd7578961507f883d6cc24007a1d Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 8 Dec 2023 13:35:12 +0100 Subject: [PATCH 05/64] Follow-up a6324174: Add missed icons --- resources/icons/printer_available.svg | 16 ++++++++++++++++ resources/icons/printer_busy.svg | 16 ++++++++++++++++ resources/icons/printer_offline.svg | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 resources/icons/printer_available.svg create mode 100644 resources/icons/printer_busy.svg create mode 100644 resources/icons/printer_offline.svg diff --git a/resources/icons/printer_available.svg b/resources/icons/printer_available.svg new file mode 100644 index 0000000000..12e295098b --- /dev/null +++ b/resources/icons/printer_available.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/resources/icons/printer_busy.svg b/resources/icons/printer_busy.svg new file mode 100644 index 0000000000..32c8149174 --- /dev/null +++ b/resources/icons/printer_busy.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/resources/icons/printer_offline.svg b/resources/icons/printer_offline.svg new file mode 100644 index 0000000000..c56326470f --- /dev/null +++ b/resources/icons/printer_offline.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + From ada1fc37ff11a7f18969705973f09058dfa18ff6 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 26 Jul 2023 16:31:15 +0200 Subject: [PATCH 06/64] WIP: Experiments with New simple top bar --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/GUI_App.cpp | 34 ++- src/slic3r/GUI/GUI_App.hpp | 10 +- src/slic3r/GUI/MainFrame.cpp | 84 +++--- src/slic3r/GUI/Tab.cpp | 25 +- src/slic3r/GUI/Tab.hpp | 9 +- src/slic3r/GUI/TopBar.cpp | 308 +++++++++++++++++++++ src/slic3r/GUI/TopBar.hpp | 459 ++++++++++++++++++++++++++++++++ src/slic3r/GUI/wxExtensions.cpp | 2 + 9 files changed, 861 insertions(+), 72 deletions(-) create mode 100644 src/slic3r/GUI/TopBar.cpp create mode 100644 src/slic3r/GUI/TopBar.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 4c297c93dd..db1ac120f3 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -252,6 +252,8 @@ set(SLIC3R_GUI_SOURCES GUI/DoubleSlider.hpp GUI/Notebook.cpp GUI/Notebook.hpp + GUI/TopBar.cpp + GUI/TopBar.hpp GUI/ObjectDataViewModel.cpp GUI/ObjectDataViewModel.hpp GUI/InstanceCheck.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7880d07606..99ee684a0e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -103,7 +103,8 @@ #include "WebViewDialog.hpp" #include "BitmapCache.hpp" -#include "Notebook.hpp" +//#include "Notebook.hpp" +#include "TopBar.hpp" #ifdef __WXMSW__ #include @@ -1519,16 +1520,16 @@ void GUI_App::init_ui_colours() m_mode_palette = get_mode_default_palette(); bool is_dark_mode = dark_mode(); -#ifdef _WIN32 +//#ifdef _WIN32 m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif +//#else +// m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +//#endif m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } @@ -1564,12 +1565,13 @@ void GUI_App::update_label_colours() tab->update_label_colours(); } -#ifdef _WIN32 +#if 0//def _WIN32 static bool is_focused(HWND hWnd) { HWND hFocusedWnd = ::GetFocus(); return hFocusedWnd && hWnd == hFocusedWnd; } +#endif static bool is_default(wxWindow* win) { @@ -1579,7 +1581,6 @@ static bool is_default(wxWindow* win) return win == tlw->GetDefaultItem(); } -#endif void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) { @@ -1592,6 +1593,7 @@ void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool ju highlited = true; } // button marking + if (!dynamic_cast(window->GetParent())) // don't marking the button if it is from TopBar { auto mark_button = [this, btn, highlited](const bool mark) { if (btn->GetLabel().IsEmpty()) @@ -1604,12 +1606,12 @@ void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool ju // hovering btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(btn->HasFocus()); event.Skip(); }); // focusing btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - is_focused_button = is_focused(btn->GetHWND()); + is_focused_button = btn->HasFocus();// is_focused(btn->GetHWND()); is_default_button = is_default(btn); if (is_focused_button || is_default_button) mark_button(is_focused_button); @@ -2471,10 +2473,8 @@ void GUI_App::update_mode() { sidebar().update_mode(); -#ifdef _WIN32 //_MSW_DARK_MODE if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); for (auto tab : tabs_list) tab->update_mode(); @@ -2483,7 +2483,8 @@ void GUI_App::update_mode() plater()->canvas3D()->update_gizmos_on_off_state(); } -void GUI_App::add_config_menu(wxMenuBar *menu) +//void GUI_App::add_config_menu(wxMenuBar *menu) +wxMenu* GUI_App::get_config_menu() { auto local_menu = new wxMenu(); wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); @@ -2518,6 +2519,7 @@ void GUI_App::add_config_menu(wxMenuBar *menu) "\tCtrl+P", #endif _L("Application preferences")); + /* wxMenu* mode_menu = nullptr; if (is_editor()) { local_menu->AppendSeparator(); @@ -2532,6 +2534,7 @@ void GUI_App::add_config_menu(wxMenuBar *menu) local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); } + */ local_menu->AppendSeparator(); local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); if (is_editor()) { @@ -2677,6 +2680,10 @@ void GUI_App::add_config_menu(wxMenuBar *menu) } }); +#if 1 + return local_menu; +#else + using std::placeholders::_1; if (mode_menu != nullptr) { @@ -2687,6 +2694,7 @@ void GUI_App::add_config_menu(wxMenuBar *menu) } menu->Append(local_menu, _L("&Configuration")); +#endif } void GUI_App::update_config_menu() { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 007ba7f8d9..81782dc27c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -146,12 +146,13 @@ private: wxColour m_color_label_sys; wxColour m_color_label_default; wxColour m_color_window_default; -#ifdef _WIN32 +//#ifdef _WIN32 wxColour m_color_highlight_label_default; wxColour m_color_hovered_btn_label; wxColour m_color_default_btn_label; wxColour m_color_highlight_default; wxColour m_color_selected_btn_bg; +#ifdef _WIN32 bool m_force_colors_update { false }; #endif std::vector m_mode_palette; @@ -251,7 +252,7 @@ public: std::vector get_mode_palette(); void set_mode_palette(const std::vector &palette); -#ifdef _WIN32 +//#ifdef _WIN32 const wxColour& get_label_highlight_clr() { return m_color_highlight_label_default; } const wxColour& get_highlight_default_clr() { return m_color_highlight_default; } const wxColour& get_color_hovered_btn_label() { return m_color_hovered_btn_label; } @@ -260,7 +261,7 @@ public: #ifdef _MSW_DARK_MODE void force_menu_update(); #endif //_MSW_DARK_MODE -#endif +//#endif const wxFont& small_font() { return m_small_font; } const wxFont& bold_font() { return m_bold_font; } @@ -297,7 +298,8 @@ public: bool save_mode(const /*ConfigOptionMode*/int mode) ; void update_mode(); - void add_config_menu(wxMenuBar *menu); +// void add_config_menu(wxMenuBar *menu); + wxMenu* get_config_menu(); void update_config_menu(); bool has_unsaved_preset_changes() const; bool has_current_preset_changes() const; diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 23b9b83a92..4314296b32 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -56,7 +56,8 @@ #include "GUI_App.hpp" #include "UnsavedChangesDialog.hpp" #include "MsgDialog.hpp" -#include "Notebook.hpp" +//#include "Notebook.hpp" +#include "TopBar.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectList.hpp" #include "GalleryDialog.hpp" @@ -457,13 +458,15 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); -#ifdef _MSW_DARK_MODE m_plater->Layout(); +#ifdef _WIN32 if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->InsertPage(0, m_plater, _L("Plater"), std::string("plater"), true); - else #endif + dynamic_cast(m_tabpanel)->InsertPage(0, m_plater, _L("Plater"), std::string("plater"), true); +#ifdef _WIN32 + else m_tabpanel->InsertPage(0, m_plater, _L("Plater")); +#endif m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 1); m_plater->Show(); m_tabpanel->Show(); @@ -483,12 +486,14 @@ void MainFrame::update_layout() m_tabpanel->Hide(); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND); m_plater_page = new wxPanel(m_tabpanel); -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->InsertPage(0, m_plater_page, _L("Plater"), std::string("plater"), true); - else #endif + dynamic_cast(m_tabpanel)->InsertPage(0, m_plater_page, _L("Plater"), std::string("plater"), true); +#ifdef _WIN32 + else m_tabpanel->InsertPage(0, m_plater_page, _L("Plater")); // empty panel just for Plater tab */ +#endif m_plater->Show(); break; } @@ -500,7 +505,7 @@ void MainFrame::update_layout() m_tabpanel->Show(); m_plater->Show(); -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 if (wxGetApp().tabs_as_menu()) show_tabs_menu(false); #endif @@ -697,22 +702,14 @@ void MainFrame::init_tabpanel() // wxGetApp().UpdateDarkUI(m_tabpanel); } else - m_tabpanel = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME, true); -#else - m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); #endif - - wxGetApp().UpdateDarkUI(m_tabpanel); + m_tabpanel = new TopBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME, true); m_tabpanel->SetFont(Slic3r::GUI::wxGetApp().normal_font()); m_tabpanel->Hide(); m_settings_dialog.set_tabpanel(m_tabpanel); -#ifdef __WXMSW__ m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { -#else - m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { -#endif if (int old_selection = e.GetOldSelection(); old_selection != wxNOT_FOUND && old_selection < static_cast(m_tabpanel->GetPageCount())) { Tab* old_tab = dynamic_cast(m_tabpanel->GetPage(old_selection)); @@ -853,12 +850,14 @@ void MainFrame::add_created_tab(Tab* panel, const std::string& bmp_name /*= ""* const auto printer_tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); if (panel->supports_printer_technology(printer_tech)) -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->AddPage(panel, panel->title(), bmp_name); - else #endif + dynamic_cast(m_tabpanel)->AddPage(panel, panel->title(), bmp_name); +#ifdef _WIN32 + else m_tabpanel->AddPage(panel, panel->title()); +#endif } bool MainFrame::is_active_and_shown_tab(Tab* tab) @@ -1041,10 +1040,10 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) wxGetApp().update_fonts(this); this->SetFont(this->normal_font()); -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 // update common mode sizer if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->Rescale(); + dynamic_cast(m_tabpanel)->Rescale(); #endif // update Plater @@ -1089,12 +1088,9 @@ void MainFrame::on_sys_color_changed() wxGetApp().update_ui_colours_from_appconfig(); #ifdef __WXMSW__ wxGetApp().UpdateDarkUI(m_tabpanel); -#ifdef _MSW_DARK_MODE - // update common mode sizer if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->OnColorsChanged(); -#endif #endif + dynamic_cast(m_tabpanel)->OnColorsChanged(); // update Plater wxGetApp().plater()->sys_color_changed(); @@ -1112,13 +1108,9 @@ void MainFrame::on_sys_color_changed() void MainFrame::update_mode_markers() { -#ifdef __WXMSW__ -#ifdef _MSW_DARK_MODE // update markers in common mode sizer if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->UpdateModeMarkers(); -#endif -#endif + dynamic_cast(m_tabpanel)->UpdateModeMarkers(); // update mode markers on side_bar wxGetApp().sidebar().update_mode_markers(); @@ -1547,6 +1539,28 @@ void MainFrame::init_menubar_as_editor() // Help menu auto helpMenu = generate_help_menu(); +#if 1 + // append menus for Menu button from TopBar + + TopBar* top_bar = dynamic_cast(m_tabpanel); + top_bar->AppendMenuItem(fileMenu, _L("&File")); + if (editMenu) + top_bar->AppendMenuItem(editMenu, _L("&Edit")); + + top_bar->AppendMenuSeparaorItem(); + + top_bar->AppendMenuItem(windowMenu, _L("&Window")); + if (viewMenu) + top_bar->AppendMenuItem(viewMenu, _L("&View")); + + top_bar->AppendMenuItem(wxGetApp().get_config_menu(), _L("&Configuration")); + + top_bar->AppendMenuSeparaorItem(); + + top_bar->AppendMenuItem(helpMenu, _L("&Help")); + +#else + // menubar // assign menubar to frame after appending items, otherwise special items // will not be handled correctly @@ -1569,6 +1583,8 @@ void MainFrame::init_menubar_as_editor() #endif SetMenuBar(m_menubar); +#endif + #ifdef _MSW_DARK_MODE if (wxGetApp().tabs_as_menu()) m_menubar->EnableTop(6, false); @@ -1662,7 +1678,7 @@ void MainFrame::init_menubar_as_gcodeviewer() m_menubar->Append(fileMenu, _L("&File")); if (viewMenu != nullptr) m_menubar->Append(viewMenu, _L("&View")); // Add additional menus from C++ - wxGetApp().add_config_menu(m_menubar); +// wxGetApp().add_config_menu(m_menubar); m_menubar->Append(helpMenu, _L("&Help")); SetMenuBar(m_menubar); @@ -2243,10 +2259,10 @@ void SettingsDialog::on_dpi_changed(const wxRect& suggested_rect) const int& em = em_unit(); const wxSize& size = wxSize(85 * em, 50 * em); -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 // update common mode sizer if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->Rescale(); + dynamic_cast(m_tabpanel)->Rescale(); #endif // update Tabs diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 3950d5a140..053e17b161 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -67,7 +67,8 @@ #include "SavePresetDialog.hpp" #include "EditGCodeDialog.hpp" #include "MsgDialog.hpp" -#include "Notebook.hpp" +//#include "Notebook.hpp" +#include "TopBar.hpp" #include "Widgets/CheckBox.hpp" @@ -3648,13 +3649,6 @@ void Tab::load_current_preset() // m_undo_to_sys_btn->Enable(!preset.is_default); -#if 0 - // use CallAfter because some field triggers schedule on_change calls using CallAfter, - // and we don't want them to be called after this update_dirty() as they would mark the - // preset dirty again - // (not sure this is true anymore now that update_dirty is idempotent) - wxTheApp->CallAfter([this] -#endif { // checking out if this Tab exists till this moment if (!wxGetApp().checked_tab(this)) @@ -3682,16 +3676,15 @@ void Tab::load_current_preset() } if (tab->supports_printer_technology(printer_technology)) { -#ifdef _MSW_DARK_MODE +#ifdef _WIN32 if (!wxGetApp().tabs_as_menu()) { - std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : - tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "resin" : "cog"; - tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab - dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); +#endif + dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(),""); +#ifdef _WIN32 } else -#endif wxGetApp().tab_panel()->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title()); +#endif #ifdef __linux__ // the tabs apparently need to be explicitly shown on Linux (pull request #1563) int page_id = wxGetApp().tab_panel()->FindPage(tab); wxGetApp().tab_panel()->GetPage(page_id)->Show(true); @@ -3705,10 +3698,6 @@ void Tab::load_current_preset() } static_cast(this)->m_printer_technology = printer_technology; m_active_page = tmp_page; -#ifdef _MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(wxGetApp().tab_panel())->SetPageImage(wxGetApp().tab_panel()->FindPage(this), printer_technology == ptFFF ? "printer" : "sla_printer"); -#endif } on_presets_changed(); if (printer_technology == ptFFF) { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 505c47b199..444e1ecf3f 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -478,7 +478,8 @@ class TabFilament : public Tab std::map m_overrides_options; public: TabFilament(wxBookCtrlBase* parent) : - Tab(parent, _(L("Filament Settings")), Slic3r::Preset::TYPE_FILAMENT) {} +// Tab(parent, _(L("Filament Settings")), Slic3r::Preset::TYPE_FILAMENT) {} + Tab(parent, _L("Filaments"), Slic3r::Preset::TYPE_FILAMENT) {} ~TabFilament() {} void build() override; @@ -536,7 +537,8 @@ public: PrinterTechnology m_printer_technology = ptFFF; TabPrinter(wxBookCtrlBase* parent) : - Tab(parent, _L("Printer Settings"), Slic3r::Preset::TYPE_PRINTER) {} +// Tab(parent, _L("Printer Settings"), Slic3r::Preset::TYPE_PRINTER) {} + Tab(parent, _L("Printers"), Slic3r::Preset::TYPE_PRINTER) {} ~TabPrinter() {} void build() override; @@ -574,7 +576,8 @@ class TabSLAMaterial : public Tab std::map m_overrides_options; public: TabSLAMaterial(wxBookCtrlBase* parent) : - Tab(parent, _(L("Material Settings")), Slic3r::Preset::TYPE_SLA_MATERIAL) {} +// Tab(parent, _(L("Material Settings")), Slic3r::Preset::TYPE_SLA_MATERIAL) {} + Tab(parent, _L("Materials"), Slic3r::Preset::TYPE_SLA_MATERIAL) {} ~TabSLAMaterial() {} void build() override; diff --git a/src/slic3r/GUI/TopBar.cpp b/src/slic3r/GUI/TopBar.cpp new file mode 100644 index 0000000000..6e46780f7c --- /dev/null +++ b/src/slic3r/GUI/TopBar.cpp @@ -0,0 +1,308 @@ +#include "TopBar.hpp" + +#include "GUI_App.hpp" +#include "Plater.hpp" +//#include "wxExtensions.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include + +wxDEFINE_EVENT(wxCUSTOMEVT_TOPBAR_SEL_CHANGED, wxCommandEvent); + +using namespace Slic3r::GUI; + +#define down_arrow L"\u23f7"; + + +TopBarItemsCtrl::Button::Button(wxWindow* parent, const wxString& label, const std::string& icon_name, const int px_cnt) +:ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, wxNO_BORDER, px_cnt) +{ + int btn_margin = em_unit(this); + wxSize size = GetTextExtent(label) + wxSize(6 * btn_margin, int(1.5 * btn_margin)); + if (icon_name.empty()) + this->SetMinSize(size); + else + this->SetMinSize(wxSize(-1, size.y)); + + const wxColour& selected_btn_bg = wxGetApp().get_label_clr_default(); + const wxColour& default_btn_bg = wxGetApp().get_window_default_clr(); + + this->Bind(wxEVT_SET_FOCUS, [this, selected_btn_bg, default_btn_bg](wxFocusEvent& event) { + this->SetBackgroundColour(selected_btn_bg); + this->SetForegroundColour(default_btn_bg); + event.Skip(); + }); + this->Bind(wxEVT_KILL_FOCUS, [this, selected_btn_bg, default_btn_bg](wxFocusEvent& event) { + if (!m_is_selected) { + this->SetBackgroundColour(default_btn_bg); + this->SetForegroundColour(selected_btn_bg); + } + event.Skip(); + }); +} + +TopBarItemsCtrl::ButtonWithPopup::ButtonWithPopup(wxWindow* parent, const wxString& label, const std::string& icon_name) + :TopBarItemsCtrl::Button(parent, label, icon_name, 24) +{ + this->SetLabel(label); +} + +void TopBarItemsCtrl::ButtonWithPopup::SetLabel(const wxString& label) +{ + wxString full_label = " " + label + " " + down_arrow; + ScalableButton::SetLabel(full_label); +} + +static wxString get_workspace_name(Slic3r::ConfigOptionMode mode) +{ + return mode == Slic3r::ConfigOptionMode::comSimple ? _L("Beginner workspace") : + mode == Slic3r::ConfigOptionMode::comAdvanced ? _L("Regular workspace") : _L("Josef Prusa's workspace"); +} + +TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent, bool add_mode_buttons/* = false*/) : + wxControl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxTAB_TRAVERSAL) +{ +#ifdef __WINDOWS__ + SetDoubleBuffered(true); +#endif //__WINDOWS__ + + int em = em_unit(this);// Slic3r::GUI::wxGetApp().em_unit(); + m_btn_margin = std::lround(0.9 * em); + m_line_margin = std::lround(0.1 * em); + + m_sizer = new wxBoxSizer(wxHORIZONTAL); + this->SetSizer(m_sizer); + + m_menu_btn = new ButtonWithPopup(this, _L("Menu"), wxGetApp().logo_name()); + m_sizer->Add(m_menu_btn, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); + + m_buttons_sizer = new wxFlexGridSizer(1, m_btn_margin, m_btn_margin); + m_sizer->Add(m_buttons_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 2 * m_btn_margin); + + if (add_mode_buttons) { + m_mode_sizer = new ModeSizer(this, m_btn_margin); + m_sizer->AddStretchSpacer(20); + m_sizer->Add(m_mode_sizer, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxBOTTOM, m_btn_margin); + + m_mode_sizer->ShowItems(false); + } + + m_workspace_btn = new ButtonWithPopup(this, _L("Workspace"), "mode_simple"); + m_sizer->AddStretchSpacer(20); + m_sizer->Add(m_workspace_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT, 2 * m_btn_margin); + + // create modes menu + { + for (const Slic3r::ConfigOptionMode& mode : { Slic3r::ConfigOptionMode::comSimple, Slic3r::ConfigOptionMode::comAdvanced, Slic3r::ConfigOptionMode::comExpert } ) { + const wxString label = get_workspace_name(mode); + append_menu_item(&m_workspace_modes, wxID_ANY, label, label, + [this, mode](wxCommandEvent&) { + if (wxGetApp().get_mode() != mode) + wxGetApp().save_mode(mode); + }, get_bmp_bundle("mode", 16, -1, wxGetApp().get_mode_btn_color(mode)), nullptr, []() { return true; }, this); + + if (mode < Slic3r::ConfigOptionMode::comExpert) + m_workspace_modes.AppendSeparator(); + } + } + + + this->Bind(wxEVT_PAINT, &TopBarItemsCtrl::OnPaint, this); + + m_menu_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + wxPoint pos = m_menu_btn->GetPosition(); + wxGetApp().plater()->PopupMenu(&m_menu, pos); + }); + + m_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { + if (m_menu_btn->HasFocus()) { + wxPostEvent(m_menu_btn->GetEventHandler(), wxFocusEvent(wxEVT_KILL_FOCUS)); + } + }); + + m_workspace_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + wxPoint pos = m_workspace_btn->GetPosition(); + wxGetApp().plater()->PopupMenu(&m_workspace_modes, pos); + }); + + m_workspace_modes.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { + if (m_workspace_btn->HasFocus()) { + wxPostEvent(m_workspace_btn->GetEventHandler(), wxFocusEvent(wxEVT_KILL_FOCUS)); + } + }); +} + +void TopBarItemsCtrl::OnPaint(wxPaintEvent&) +{ + wxGetApp().UpdateDarkUI(this); + const wxSize sz = GetSize(); + wxPaintDC dc(this); + + if (m_selection < 0 || m_selection >= (int)m_pageButtons.size()) + return; + + const wxColour& selected_btn_bg = wxGetApp().get_label_clr_default(); + const wxColour& default_btn_bg = wxGetApp().get_window_default_clr(); + const wxColour& btn_marker_color = wxGetApp().get_highlight_default_clr(); + + // highlight selected notebook button + + for (int idx = 0; idx < int(m_pageButtons.size()); idx++) { + wxButton* btn = m_pageButtons[idx]; + + btn->SetBackgroundColour(idx == m_selection ? selected_btn_bg : default_btn_bg); + btn->SetForegroundColour(idx == m_selection ? default_btn_bg : selected_btn_bg); + } + + // highlight selected mode button + + //bool mode_is_focused = m_workspace_btn->HasFocus(); + + //m_workspace_btn->SetBackgroundColour(mode_is_focused ? selected_btn_bg : default_btn_bg); + //m_workspace_btn->SetForegroundColour(mode_is_focused ? default_btn_bg : selected_btn_bg); + + //if (m_mode_sizer) { + // const std::vector& mode_btns = m_mode_sizer->get_btns(); + // for (int idx = 0; idx < int(mode_btns.size()); idx++) { + // ModeButton* btn = mode_btns[idx]; + // btn->SetBackgroundColour(btn->is_selected() ? selected_btn_bg : default_btn_bg); + + // //wxPoint pos = btn->GetPosition(); + // //wxSize size = btn->GetSize(); + // //const wxColour& clr = btn->is_selected() ? btn_marker_color : default_btn_bg; + // //dc.SetPen(clr); + // //dc.SetBrush(clr); + // //dc.DrawRectangle(pos.x, pos.y + size.y, size.x, sz.y - size.y); + // } + //} + + // Draw orange bottom line + + dc.SetPen(btn_marker_color); + dc.SetBrush(btn_marker_color); + dc.DrawRectangle(1, sz.y - m_line_margin, sz.x, m_line_margin); +} + +void TopBarItemsCtrl::UpdateMode() +{ + auto mode = wxGetApp().get_mode(); + m_mode_sizer->SetMode(mode); + + + auto m_bmp = *get_bmp_bundle("mode", 16, -1, wxGetApp().get_mode_btn_color(mode)); + + m_workspace_btn->SetBitmap(m_bmp); + m_workspace_btn->SetBitmapCurrent(m_bmp); + m_workspace_btn->SetBitmapPressed(m_bmp); + m_workspace_btn->SetLabel(get_workspace_name(mode)); + + m_workspace_btn->SetBitmapMargins(int(0.5 * em_unit(this)), 0); + this->Layout(); +} + +void TopBarItemsCtrl::Rescale() +{ + int em = em_unit(this); + m_btn_margin = std::lround(0.3 * em); + m_line_margin = std::lround(0.1 * em); + m_buttons_sizer->SetVGap(m_btn_margin); + m_buttons_sizer->SetHGap(m_btn_margin); + + m_sizer->Layout(); +} + +void TopBarItemsCtrl::OnColorsChanged() +{ + m_menu_btn->sys_color_changed(); + + for (ScalableButton* btn : m_pageButtons) + btn->sys_color_changed(); + + m_mode_sizer->sys_color_changed(); + + m_sizer->Layout(); +} + +void TopBarItemsCtrl::UpdateModeMarkers() +{ + m_mode_sizer->update_mode_markers(); +} + +void TopBarItemsCtrl::UpdateSelection() +{ + for (Button* btn : m_pageButtons) + btn->set_selected(false); + + if (m_selection >= 0) + m_pageButtons[m_selection]->set_selected(true); + + Refresh(); +} + +void TopBarItemsCtrl::SetSelection(int sel) +{ + if (m_selection == sel) + return; + m_selection = sel; + UpdateSelection(); +} + +bool TopBarItemsCtrl::InsertPage(size_t n, const wxString& text, bool bSelect/* = false*/, const std::string& bmp_name/* = ""*/) +{ + Button* btn = new Button(this, text); + btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { + if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { + m_selection = it - m_pageButtons.begin(); + wxCommandEvent evt = wxCommandEvent(wxCUSTOMEVT_TOPBAR_SEL_CHANGED); + evt.SetId(m_selection); + wxPostEvent(this->GetParent(), evt); + UpdateSelection(); + } + }); + + m_pageButtons.insert(m_pageButtons.begin() + n, btn); + m_buttons_sizer->Insert(n, new wxSizerItem(btn, 0, wxALIGN_CENTER_VERTICAL)); + m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1); + m_sizer->Layout(); + return true; +} + +void TopBarItemsCtrl::RemovePage(size_t n) +{ + ScalableButton* btn = m_pageButtons[n]; + m_pageButtons.erase(m_pageButtons.begin() + n); + m_buttons_sizer->Remove(n); + btn->Reparent(nullptr); + btn->Destroy(); + m_sizer->Layout(); +} + +void TopBarItemsCtrl::SetPageText(size_t n, const wxString& strText) +{ + ScalableButton* btn = m_pageButtons[n]; + btn->SetLabel(strText); +} + +wxString TopBarItemsCtrl::GetPageText(size_t n) const +{ + ScalableButton* btn = m_pageButtons[n]; + return btn->GetLabel(); +} + +void TopBarItemsCtrl::AppendMenuItem(wxMenu* menu, const wxString& title) +{ + append_submenu(&m_menu, menu, wxID_ANY, title, "cog"); +} + +void TopBarItemsCtrl::AppendMenuSeparaorItem() +{ + m_menu.AppendSeparator(); +} + +void TopBarItemsCtrl::ShowMenu() +{ + wxPoint pos = m_menu_btn->GetPosition(); + wxGetApp().plater()->PopupMenu(&m_menu, pos); +} diff --git a/src/slic3r/GUI/TopBar.hpp b/src/slic3r/GUI/TopBar.hpp new file mode 100644 index 0000000000..1812b08180 --- /dev/null +++ b/src/slic3r/GUI/TopBar.hpp @@ -0,0 +1,459 @@ +#ifndef slic3r_TopBar_hpp_ +#define slic3r_TopBar_hpp_ + +//#ifdef _WIN32 + +#include +#include "wxExtensions.hpp" + +class ModeSizer; +//class ScalableButton; + +// custom message the TopBarItemsCtrl sends to its parent (TopBar) to notify a selection change: +wxDECLARE_EVENT(wxCUSTOMEVT_TOPBAR_SEL_CHANGED, wxCommandEvent); + +class TopBarItemsCtrl : public wxControl +{ + class Button : public ScalableButton + { + bool m_is_selected{ false }; + public: + Button() {}; + Button( wxWindow* parent, + const wxString& label, + const std::string& icon_name = "", + const int px_cnt = 16); + + ~Button() {} + + void set_selected(bool selected) { m_is_selected = selected; } + }; + + class ButtonWithPopup : public Button + { + public: + ButtonWithPopup() {}; + ButtonWithPopup(wxWindow* parent, + const wxString& label, + const std::string& icon_name = ""); + + ~ButtonWithPopup() {} + + void SetLabel(const wxString& label) override; + }; + + MenuWithSeparators m_menu; + MenuWithSeparators m_workspace_modes; + +public: + TopBarItemsCtrl(wxWindow* parent, bool add_mode_buttons = false); + ~TopBarItemsCtrl() {} + + void OnPaint(wxPaintEvent&); + void SetSelection(int sel); + void UpdateMode(); + void Rescale(); + void OnColorsChanged(); + void UpdateModeMarkers(); + void UpdateSelection(); + bool InsertPage(size_t n, const wxString& text, bool bSelect = false, const std::string& bmp_name = ""); + void RemovePage(size_t n); + void SetPageText(size_t n, const wxString& strText); + wxString GetPageText(size_t n) const; + + void AppendMenuItem(wxMenu* menu, const wxString& title); + void AppendMenuSeparaorItem(); + void ShowMenu(); + void AddModeItem(); + void ShowModes(); + +private: + wxWindow* m_parent; + wxFlexGridSizer* m_buttons_sizer; + wxBoxSizer* m_sizer; + ButtonWithPopup* m_menu_btn {nullptr}; + ButtonWithPopup* m_workspace_btn {nullptr}; + std::vector m_pageButtons; + int m_selection {-1}; + int m_btn_margin; + int m_line_margin; + ModeSizer* m_mode_sizer {nullptr}; +}; + +class TopBar : public wxBookCtrlBase +{ +public: + TopBar(wxWindow * parent, + wxWindowID winid = wxID_ANY, + const wxPoint & pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + bool add_mode_buttons = false) + { + Init(); + Create(parent, winid, pos, size, style, add_mode_buttons); + } + + bool Create(wxWindow * parent, + wxWindowID winid = wxID_ANY, + const wxPoint & pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + bool add_mode_buttons = false) + { + if (!wxBookCtrlBase::Create(parent, winid, pos, size, style | wxBK_TOP)) + return false; + + m_bookctrl = new TopBarItemsCtrl(this, add_mode_buttons); + + wxSizer* mainSizer = new wxBoxSizer(IsVertical() ? wxVERTICAL : wxHORIZONTAL); + + if (style & wxBK_RIGHT || style & wxBK_BOTTOM) + mainSizer->Add(0, 0, 1, wxEXPAND, 0); + + m_controlSizer = new wxBoxSizer(IsVertical() ? wxHORIZONTAL : wxVERTICAL); + m_controlSizer->Add(m_bookctrl, wxSizerFlags(1).Expand()); + wxSizerFlags flags; + if (IsVertical()) + flags.Expand(); + else + flags.CentreVertical(); + mainSizer->Add(m_controlSizer, flags.Border(wxALL, m_controlMargin)); + SetSizer(mainSizer); + + this->Bind(wxCUSTOMEVT_TOPBAR_SEL_CHANGED, [this](wxCommandEvent& evt) + { + if (int page_idx = evt.GetId(); page_idx >= 0) + SetSelection(page_idx); + }); + + this->Bind(wxEVT_NAVIGATION_KEY, &TopBar::OnNavigationKey, this); + + return true; + } + + + // Methods specific to this class. + + // A method allowing to add a new page without any label (which is unused + // by this control) and show it immediately. + bool ShowNewPage(wxWindow * page) + { + return AddPage(page, wxString(), ""/*true *//* select it */); + } + + + // Set effect to use for showing/hiding pages. + void SetEffects(wxShowEffect showEffect, wxShowEffect hideEffect) + { + m_showEffect = showEffect; + m_hideEffect = hideEffect; + } + + // Or the same effect for both of them. + void SetEffect(wxShowEffect effect) + { + SetEffects(effect, effect); + } + + // And the same for time outs. + void SetEffectsTimeouts(unsigned showTimeout, unsigned hideTimeout) + { + m_showTimeout = showTimeout; + m_hideTimeout = hideTimeout; + } + + void SetEffectTimeout(unsigned timeout) + { + SetEffectsTimeouts(timeout, timeout); + } + + + // Implement base class pure virtual methods. + + // adds a new page to the control + bool AddPage(wxWindow* page, + const wxString& text, + const std::string& bmp_name, + bool bSelect = false) + { + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), page, text, bmp_name, bSelect); + } + + // Page management + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int imageId = NO_IMAGE) override + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) + return false; + + GetTopBarItemsCtrl()->InsertPage(n, text, bSelect); + + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); + + return true; + } + + bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + const std::string& bmp_name = "", + bool bSelect = false) + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) + return false; + + GetTopBarItemsCtrl()->InsertPage(n, text, bSelect, bmp_name); + + if (bSelect) + SetSelection(n); + + return true; + } + + virtual int SetSelection(size_t n) override + { + GetTopBarItemsCtrl()->SetSelection(n); + int ret = DoSetSelection(n, SetSelection_SendEvent); + + // check that only the selected page is visible and others are hidden: + for (size_t page = 0; page < m_pages.size(); page++) + if (page != n) + m_pages[page]->Hide(); + + return ret; + } + + virtual int ChangeSelection(size_t n) override + { + GetTopBarItemsCtrl()->SetSelection(n); + return DoSetSelection(n); + } + + // Neither labels nor images are supported but we still store the labels + // just in case the user code attaches some importance to them. + virtual bool SetPageText(size_t n, const wxString & strText) override + { + wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); + + GetTopBarItemsCtrl()->SetPageText(n, strText); + + return true; + } + + virtual wxString GetPageText(size_t n) const override + { + wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page")); + return GetTopBarItemsCtrl()->GetPageText(n); + } + + virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override + { + return false; + } + + virtual int GetPageImage(size_t WXUNUSED(n)) const override + { + return NO_IMAGE; + } + + // Override some wxWindow methods too. + virtual void SetFocus() override + { + wxWindow* const page = GetCurrentPage(); + if (page) + page->SetFocus(); + } + + TopBarItemsCtrl* GetTopBarItemsCtrl() const { return static_cast(m_bookctrl); } + + void UpdateMode() + { + GetTopBarItemsCtrl()->UpdateMode(); + } + + void Rescale() + { + GetTopBarItemsCtrl()->Rescale(); + } + + void OnColorsChanged() + { + GetTopBarItemsCtrl()->OnColorsChanged(); + } + + void UpdateModeMarkers() + { + GetTopBarItemsCtrl()->UpdateModeMarkers(); + } + + void OnNavigationKey(wxNavigationKeyEvent& event) + { + if (event.IsWindowChange()) { + // change pages + AdvanceSelection(event.GetDirection()); + } + else { + // we get this event in 3 cases + // + // a) one of our pages might have generated it because the user TABbed + // out from it in which case we should propagate the event upwards and + // our parent will take care of setting the focus to prev/next sibling + // + // or + // + // b) the parent panel wants to give the focus to us so that we + // forward it to our selected page. We can't deal with this in + // OnSetFocus() because we don't know which direction the focus came + // from in this case and so can't choose between setting the focus to + // first or last panel child + // + // or + // + // c) we ourselves (see MSWTranslateMessage) generated the event + // + wxWindow* const parent = GetParent(); + + // the wxObject* casts are required to avoid MinGW GCC 2.95.3 ICE + const bool isFromParent = event.GetEventObject() == (wxObject*)parent; + const bool isFromSelf = event.GetEventObject() == (wxObject*)this; + const bool isForward = event.GetDirection(); + + if (isFromSelf && !isForward) + { + // focus is currently on notebook tab and should leave + // it backwards (Shift-TAB) + event.SetCurrentFocus(this); + parent->HandleWindowEvent(event); + } + else if (isFromParent || isFromSelf) + { + // no, it doesn't come from child, case (b) or (c): forward to a + // page but only if entering notebook page (i.e. direction is + // backwards (Shift-TAB) comething from out-of-notebook, or + // direction is forward (TAB) from ourselves), + if (m_selection != wxNOT_FOUND && + (!event.GetDirection() || isFromSelf)) + { + // so that the page knows that the event comes from it's parent + // and is being propagated downwards + event.SetEventObject(this); + + wxWindow* page = m_pages[m_selection]; + if (!page->HandleWindowEvent(event)) + { + page->SetFocus(); + } + //else: page manages focus inside it itself + } + else // otherwise set the focus to the notebook itself + { + SetFocus(); + } + } + else + { + // it comes from our child, case (a), pass to the parent, but only + // if the direction is forwards. Otherwise set the focus to the + // notebook itself. The notebook is always the 'first' control of a + // page. + if (!isForward) + { + SetFocus(); + } + else if (parent) + { + event.SetCurrentFocus(this); + parent->HandleWindowEvent(event); + } + } + } + } + + // Methods for extensions of this class + + void AppendMenuItem(wxMenu* menu, const wxString& title) { + GetTopBarItemsCtrl()->AppendMenuItem(menu, title); + } + + void AppendMenuSeparaorItem() { + GetTopBarItemsCtrl()->AppendMenuSeparaorItem(); + } + + +protected: + virtual void UpdateSelectedPage(size_t WXUNUSED(newsel)) override + { + // Nothing to do here, but must be overridden to avoid the assert in + // the base class version. + } + + virtual wxBookCtrlEvent * CreatePageChangingEvent() const override + { + return new wxBookCtrlEvent(wxEVT_BOOKCTRL_PAGE_CHANGING, + GetId()); + } + + virtual void MakeChangedEvent(wxBookCtrlEvent & event) override + { + event.SetEventType(wxEVT_BOOKCTRL_PAGE_CHANGED); + } + + virtual wxWindow * DoRemovePage(size_t page) override + { + wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); + if (win) + { + GetTopBarItemsCtrl()->RemovePage(page); + DoSetSelectionAfterRemoval(page); + } + + return win; + } + + virtual void DoSize() override + { + wxWindow* const page = GetCurrentPage(); + if (page) + page->SetSize(GetPageRect()); + } + + virtual void DoShowPage(wxWindow * page, bool show) override + { + if (show) + page->ShowWithEffect(m_showEffect, m_showTimeout); + else + page->HideWithEffect(m_hideEffect, m_hideTimeout); + } + +private: + void Init() + { + // We don't need any border as we don't have anything to separate the + // page contents from. + SetInternalBorder(0); + + // No effects by default. + m_showEffect = + m_hideEffect = wxSHOW_EFFECT_NONE; + + m_showTimeout = + m_hideTimeout = 0; + } + + wxShowEffect m_showEffect, + m_hideEffect; + + unsigned m_showTimeout, + m_hideTimeout; + + TopBarItemsCtrl* m_ctrl{ nullptr }; + +}; +//#endif // _WIN32 +#endif // slic3r_TopBar_hpp_ diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 71ace3dbe0..157418dd75 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -906,6 +906,8 @@ int ScalableButton::GetBitmapHeight() void ScalableButton::sys_color_changed() { + if (m_current_icon_name.empty()) + return; Slic3r::GUI::wxGetApp().UpdateDarkUI(this, m_has_border); wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_bmp_width, m_bmp_height); From cecfe485ab88072d422d5c72aa98eabf6e3e4863 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 31 Jul 2023 16:43:51 +0200 Subject: [PATCH 07/64] WIP: New simple top bar : Improve/delete redundant mode sizers + UI-fixes for OSX --- src/slic3r/GUI/GUI_App.cpp | 30 ------ src/slic3r/GUI/GUI_App.hpp | 1 - src/slic3r/GUI/GUI_Factories.cpp | 2 + src/slic3r/GUI/MainFrame.cpp | 22 +--- src/slic3r/GUI/Tab.cpp | 6 -- src/slic3r/GUI/TopBar.cpp | 178 +++++++++++++------------------ src/slic3r/GUI/TopBar.hpp | 23 ++-- 7 files changed, 89 insertions(+), 173 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 99ee684a0e..52952840ef 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2483,7 +2483,6 @@ void GUI_App::update_mode() plater()->canvas3D()->update_gizmos_on_off_state(); } -//void GUI_App::add_config_menu(wxMenuBar *menu) wxMenu* GUI_App::get_config_menu() { auto local_menu = new wxMenu(); @@ -2519,22 +2518,7 @@ wxMenu* GUI_App::get_config_menu() "\tCtrl+P", #endif _L("Application preferences")); - /* - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX("Advanced", "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - */ local_menu->AppendSeparator(); local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); if (is_editor()) { @@ -2680,21 +2664,7 @@ wxMenu* GUI_App::get_config_menu() } }); -#if 1 return local_menu; -#else - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -#endif } void GUI_App::update_config_menu() { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 81782dc27c..d2f3b47a86 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -298,7 +298,6 @@ public: bool save_mode(const /*ConfigOptionMode*/int mode) ; void update_mode(); -// void add_config_menu(wxMenuBar *menu); wxMenu* get_config_menu(); void update_config_menu(); bool has_unsaved_preset_changes() const; diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 45381e8f75..bed6c6368e 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1463,6 +1463,8 @@ void MenuFactory::sys_color_changed() void MenuFactory::sys_color_changed(wxMenuBar* menubar) { + if (!menubar) + return; for (size_t id = 0; id < menubar->GetMenuCount(); id++) { wxMenu* menu = menubar->GetMenu(id); sys_color_changed_menu(menu); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 4314296b32..ba7155564e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -703,7 +703,7 @@ void MainFrame::init_tabpanel() } else #endif - m_tabpanel = new TopBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME, true); + m_tabpanel = new TopBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); m_tabpanel->SetFont(Slic3r::GUI::wxGetApp().normal_font()); m_tabpanel->Hide(); @@ -1539,7 +1539,7 @@ void MainFrame::init_menubar_as_editor() // Help menu auto helpMenu = generate_help_menu(); -#if 1 +#ifndef __APPLE__ // append menus for Menu button from TopBar TopBar* top_bar = dynamic_cast(m_tabpanel); @@ -1571,27 +1571,13 @@ void MainFrame::init_menubar_as_editor() m_menubar->Append(windowMenu, _L("&Window")); if (viewMenu) m_menubar->Append(viewMenu, _L("&View")); // Add additional menus from C++ - wxGetApp().add_config_menu(m_menubar); + m_menubar->Append(wxGetApp().get_config_menu(), _L("&Configuration")); m_menubar->Append(helpMenu, _L("&Help")); -#ifdef _MSW_DARK_MODE - if (wxGetApp().tabs_as_menu()) { - // Add separator - m_menubar->Append(new wxMenu(), " "); - add_tabs_as_menu(m_menubar, this, this); - } -#endif SetMenuBar(m_menubar); -#endif - -#ifdef _MSW_DARK_MODE - if (wxGetApp().tabs_as_menu()) - m_menubar->EnableTop(6, false); -#endif - -#ifdef __APPLE__ init_macos_application_menu(m_menubar, this); + #endif // __APPLE__ if (plater()->printer_technology() == ptSLA) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 053e17b161..01b4292c2f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -237,12 +237,6 @@ void Tab::create_preset_tab() m_modified_label_clr = wxGetApp().get_label_clr_modified(); m_default_text_clr = wxGetApp().get_label_clr_default(); -#ifdef _MSW_DARK_MODE - // Sizer with buttons for mode changing - if (wxGetApp().tabs_as_menu()) -#endif - m_mode_sizer = new ModeSizer(panel, int (0.5*em_unit(this))); - const float scale_factor = em_unit(this)*0.1;// GetContentScaleFactor(); m_top_hsizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_top_hsizer, 0, wxEXPAND | wxBOTTOM, 3); diff --git a/src/slic3r/GUI/TopBar.cpp b/src/slic3r/GUI/TopBar.cpp index 6e46780f7c..6c2c2e1ea9 100644 --- a/src/slic3r/GUI/TopBar.cpp +++ b/src/slic3r/GUI/TopBar.cpp @@ -13,7 +13,11 @@ wxDEFINE_EVENT(wxCUSTOMEVT_TOPBAR_SEL_CHANGED, wxCommandEvent); using namespace Slic3r::GUI; +#ifdef __APPLE__ +#define down_arrow L"\u25BC"; +#else #define down_arrow L"\u23f7"; +#endif TopBarItemsCtrl::Button::Button(wxWindow* parent, const wxString& label, const std::string& icon_name, const int px_cnt) @@ -25,22 +29,31 @@ TopBarItemsCtrl::Button::Button(wxWindow* parent, const wxString& label, const s this->SetMinSize(size); else this->SetMinSize(wxSize(-1, size.y)); +} - const wxColour& selected_btn_bg = wxGetApp().get_label_clr_default(); - const wxColour& default_btn_bg = wxGetApp().get_window_default_clr(); +void TopBarItemsCtrl::Button::set_selected(bool selected) +{ + m_is_selected = selected; - this->Bind(wxEVT_SET_FOCUS, [this, selected_btn_bg, default_btn_bg](wxFocusEvent& event) { - this->SetBackgroundColour(selected_btn_bg); - this->SetForegroundColour(default_btn_bg); - event.Skip(); - }); - this->Bind(wxEVT_KILL_FOCUS, [this, selected_btn_bg, default_btn_bg](wxFocusEvent& event) { - if (!m_is_selected) { - this->SetBackgroundColour(default_btn_bg); - this->SetForegroundColour(selected_btn_bg); - } - event.Skip(); - }); + if (m_is_selected) { +#ifdef __APPLE__ + this->SetBackgroundColour(wxGetApp().get_highlight_default_clr()); +#else + this->SetBackgroundColour(wxGetApp().get_label_clr_default()); + this->SetForegroundColour(wxGetApp().get_window_default_clr()); +#endif + } + else { +#ifdef _WIN32 + this->SetBackgroundColour(wxGetApp().get_window_default_clr()); +#else + this->SetBackgroundColour(wxTransparentColor); +#endif + +#ifndef __APPLE__ + this->SetForegroundColour(wxGetApp().get_label_clr_default()); +#endif + } } TopBarItemsCtrl::ButtonWithPopup::ButtonWithPopup(wxWindow* parent, const wxString& label, const std::string& icon_name) @@ -61,77 +74,76 @@ static wxString get_workspace_name(Slic3r::ConfigOptionMode mode) mode == Slic3r::ConfigOptionMode::comAdvanced ? _L("Regular workspace") : _L("Josef Prusa's workspace"); } -TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent, bool add_mode_buttons/* = false*/) : +void TopBarItemsCtrl::ApplyWorkspacesMenu() +{ + wxMenuItemList& items = m_workspaces_menu.GetMenuItems(); + if (!items.IsEmpty()) { + for (int id = int(m_workspaces_menu.GetMenuItemCount()) - 1; id >= 0; id--) + m_workspaces_menu.Destroy(items[id]); + } + + for (const Slic3r::ConfigOptionMode& mode : { Slic3r::ConfigOptionMode::comSimple, + Slic3r::ConfigOptionMode::comAdvanced, + Slic3r::ConfigOptionMode::comExpert }) { + const wxString label = get_workspace_name(mode); + append_menu_item(&m_workspaces_menu, wxID_ANY, label, label, + [this, mode](wxCommandEvent&) { + if (wxGetApp().get_mode() != mode) + wxGetApp().save_mode(mode); + }, get_bmp_bundle("mode", 16, -1, wxGetApp().get_mode_btn_color(mode)), nullptr, []() { return true; }, this); + + if (mode < Slic3r::ConfigOptionMode::comExpert) + m_workspaces_menu.AppendSeparator(); + } +} + +TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : wxControl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxTAB_TRAVERSAL) { #ifdef __WINDOWS__ SetDoubleBuffered(true); #endif //__WINDOWS__ - int em = em_unit(this);// Slic3r::GUI::wxGetApp().em_unit(); + int em = em_unit(this); m_btn_margin = std::lround(0.9 * em); m_line_margin = std::lround(0.1 * em); m_sizer = new wxBoxSizer(wxHORIZONTAL); this->SetSizer(m_sizer); +#ifdef __APPLE__ + auto logo = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle(wxGetApp().logo_name(), 24)); + m_sizer->Add(logo, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); +#else m_menu_btn = new ButtonWithPopup(this, _L("Menu"), wxGetApp().logo_name()); m_sizer->Add(m_menu_btn, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); + + m_menu_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + m_menu_btn->set_selected(true); + wxPoint pos = m_menu_btn->GetPosition(); + wxGetApp().plater()->PopupMenu(&m_main_menu, pos); + }); + m_main_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { m_menu_btn->set_selected(false); }); +#endif m_buttons_sizer = new wxFlexGridSizer(1, m_btn_margin, m_btn_margin); m_sizer->Add(m_buttons_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 2 * m_btn_margin); - if (add_mode_buttons) { - m_mode_sizer = new ModeSizer(this, m_btn_margin); - m_sizer->AddStretchSpacer(20); - m_sizer->Add(m_mode_sizer, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxBOTTOM, m_btn_margin); - - m_mode_sizer->ShowItems(false); - } + // create modes menu + ApplyWorkspacesMenu(); m_workspace_btn = new ButtonWithPopup(this, _L("Workspace"), "mode_simple"); m_sizer->AddStretchSpacer(20); m_sizer->Add(m_workspace_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT, 2 * m_btn_margin); - - // create modes menu - { - for (const Slic3r::ConfigOptionMode& mode : { Slic3r::ConfigOptionMode::comSimple, Slic3r::ConfigOptionMode::comAdvanced, Slic3r::ConfigOptionMode::comExpert } ) { - const wxString label = get_workspace_name(mode); - append_menu_item(&m_workspace_modes, wxID_ANY, label, label, - [this, mode](wxCommandEvent&) { - if (wxGetApp().get_mode() != mode) - wxGetApp().save_mode(mode); - }, get_bmp_bundle("mode", 16, -1, wxGetApp().get_mode_btn_color(mode)), nullptr, []() { return true; }, this); - - if (mode < Slic3r::ConfigOptionMode::comExpert) - m_workspace_modes.AppendSeparator(); - } - } - - - this->Bind(wxEVT_PAINT, &TopBarItemsCtrl::OnPaint, this); - - m_menu_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - wxPoint pos = m_menu_btn->GetPosition(); - wxGetApp().plater()->PopupMenu(&m_menu, pos); - }); - - m_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { - if (m_menu_btn->HasFocus()) { - wxPostEvent(m_menu_btn->GetEventHandler(), wxFocusEvent(wxEVT_KILL_FOCUS)); - } - }); m_workspace_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + m_workspace_btn->set_selected(true); wxPoint pos = m_workspace_btn->GetPosition(); - wxGetApp().plater()->PopupMenu(&m_workspace_modes, pos); + wxGetApp().plater()->PopupMenu(&m_workspaces_menu, pos); }); + m_workspaces_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { m_workspace_btn->set_selected(false); }); - m_workspace_modes.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { - if (m_workspace_btn->HasFocus()) { - wxPostEvent(m_workspace_btn->GetEventHandler(), wxFocusEvent(wxEVT_KILL_FOCUS)); - } - }); + this->Bind(wxEVT_PAINT, &TopBarItemsCtrl::OnPaint, this); } void TopBarItemsCtrl::OnPaint(wxPaintEvent&) @@ -143,41 +155,8 @@ void TopBarItemsCtrl::OnPaint(wxPaintEvent&) if (m_selection < 0 || m_selection >= (int)m_pageButtons.size()) return; - const wxColour& selected_btn_bg = wxGetApp().get_label_clr_default(); - const wxColour& default_btn_bg = wxGetApp().get_window_default_clr(); const wxColour& btn_marker_color = wxGetApp().get_highlight_default_clr(); - // highlight selected notebook button - - for (int idx = 0; idx < int(m_pageButtons.size()); idx++) { - wxButton* btn = m_pageButtons[idx]; - - btn->SetBackgroundColour(idx == m_selection ? selected_btn_bg : default_btn_bg); - btn->SetForegroundColour(idx == m_selection ? default_btn_bg : selected_btn_bg); - } - - // highlight selected mode button - - //bool mode_is_focused = m_workspace_btn->HasFocus(); - - //m_workspace_btn->SetBackgroundColour(mode_is_focused ? selected_btn_bg : default_btn_bg); - //m_workspace_btn->SetForegroundColour(mode_is_focused ? default_btn_bg : selected_btn_bg); - - //if (m_mode_sizer) { - // const std::vector& mode_btns = m_mode_sizer->get_btns(); - // for (int idx = 0; idx < int(mode_btns.size()); idx++) { - // ModeButton* btn = mode_btns[idx]; - // btn->SetBackgroundColour(btn->is_selected() ? selected_btn_bg : default_btn_bg); - - // //wxPoint pos = btn->GetPosition(); - // //wxSize size = btn->GetSize(); - // //const wxColour& clr = btn->is_selected() ? btn_marker_color : default_btn_bg; - // //dc.SetPen(clr); - // //dc.SetBrush(clr); - // //dc.DrawRectangle(pos.x, pos.y + size.y, size.x, sz.y - size.y); - // } - //} - // Draw orange bottom line dc.SetPen(btn_marker_color); @@ -188,8 +167,6 @@ void TopBarItemsCtrl::OnPaint(wxPaintEvent&) void TopBarItemsCtrl::UpdateMode() { auto mode = wxGetApp().get_mode(); - m_mode_sizer->SetMode(mode); - auto m_bmp = *get_bmp_bundle("mode", 16, -1, wxGetApp().get_mode_btn_color(mode)); @@ -220,14 +197,13 @@ void TopBarItemsCtrl::OnColorsChanged() for (ScalableButton* btn : m_pageButtons) btn->sys_color_changed(); - m_mode_sizer->sys_color_changed(); - m_sizer->Layout(); } void TopBarItemsCtrl::UpdateModeMarkers() { - m_mode_sizer->update_mode_markers(); + UpdateMode(); + ApplyWorkspacesMenu(); } void TopBarItemsCtrl::UpdateSelection() @@ -293,16 +269,10 @@ wxString TopBarItemsCtrl::GetPageText(size_t n) const void TopBarItemsCtrl::AppendMenuItem(wxMenu* menu, const wxString& title) { - append_submenu(&m_menu, menu, wxID_ANY, title, "cog"); + append_submenu(&m_main_menu, menu, wxID_ANY, title, "cog"); } void TopBarItemsCtrl::AppendMenuSeparaorItem() { - m_menu.AppendSeparator(); -} - -void TopBarItemsCtrl::ShowMenu() -{ - wxPoint pos = m_menu_btn->GetPosition(); - wxGetApp().plater()->PopupMenu(&m_menu, pos); + m_main_menu.AppendSeparator(); } diff --git a/src/slic3r/GUI/TopBar.hpp b/src/slic3r/GUI/TopBar.hpp index 1812b08180..9ec522c2d0 100644 --- a/src/slic3r/GUI/TopBar.hpp +++ b/src/slic3r/GUI/TopBar.hpp @@ -26,7 +26,7 @@ class TopBarItemsCtrl : public wxControl ~Button() {} - void set_selected(bool selected) { m_is_selected = selected; } + void set_selected(bool selected); }; class ButtonWithPopup : public Button @@ -42,11 +42,11 @@ class TopBarItemsCtrl : public wxControl void SetLabel(const wxString& label) override; }; - MenuWithSeparators m_menu; - MenuWithSeparators m_workspace_modes; + wxMenu m_main_menu; + wxMenu m_workspaces_menu; public: - TopBarItemsCtrl(wxWindow* parent, bool add_mode_buttons = false); + TopBarItemsCtrl(wxWindow* parent); ~TopBarItemsCtrl() {} void OnPaint(wxPaintEvent&); @@ -63,9 +63,7 @@ public: void AppendMenuItem(wxMenu* menu, const wxString& title); void AppendMenuSeparaorItem(); - void ShowMenu(); - void AddModeItem(); - void ShowModes(); + void ApplyWorkspacesMenu(); private: wxWindow* m_parent; @@ -77,7 +75,6 @@ private: int m_selection {-1}; int m_btn_margin; int m_line_margin; - ModeSizer* m_mode_sizer {nullptr}; }; class TopBar : public wxBookCtrlBase @@ -87,24 +84,22 @@ public: wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, const wxSize & size = wxDefaultSize, - long style = 0, - bool add_mode_buttons = false) + long style = 0) { Init(); - Create(parent, winid, pos, size, style, add_mode_buttons); + Create(parent, winid, pos, size, style); } bool Create(wxWindow * parent, wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, const wxSize & size = wxDefaultSize, - long style = 0, - bool add_mode_buttons = false) + long style = 0) { if (!wxBookCtrlBase::Create(parent, winid, pos, size, style | wxBK_TOP)) return false; - m_bookctrl = new TopBarItemsCtrl(this, add_mode_buttons); + m_bookctrl = new TopBarItemsCtrl(this); wxSizer* mainSizer = new wxBoxSizer(IsVertical() ? wxVERTICAL : wxHORIZONTAL); From 78c9aa668cbfdf63f3840283885eaea021efdcfd Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 8 Dec 2023 17:06:47 +0100 Subject: [PATCH 08/64] User Account: Store more user data, not only username Compare new and old printer maps --- src/slic3r/GUI/MainFrame.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 35 ++++++++++------ src/slic3r/GUI/UserAccount.cpp | 77 ++++++++++++++++++++++++---------- src/slic3r/GUI/UserAccount.hpp | 18 ++++---- 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ba7155564e..ece8cfc1e4 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -836,7 +836,7 @@ void MainFrame::create_preset_tabs() add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); m_webview = new WebViewPanel(m_tabpanel); - m_tabpanel->AddPage(m_webview, "Web View"); + m_tabpanel->AddPage(m_webview, "PrusaConnect"); /* m_media = new MediaMainPanel(this); m_tabpanel->AddPage(m_media, "Media"); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 94926e7643..f8a8160c91 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -891,11 +891,17 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); this->q->Bind(EVT_PA_ID_USER_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { - std::string username = user_account->on_user_id_success(evt.data, wxGetApp().app_config); - std::string text = format(_u8L("Logged as %1%."), username); - this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); - this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); - wxGetApp().update_config_menu(); + std::string username; + bool succ = user_account->on_user_id_success(evt.data, wxGetApp().app_config, username); + if (succ) { + std::string text = format(_u8L("Logged as %1%."), username); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); + wxGetApp().update_config_menu(); + } else { + // TODO + } + }); this->q->Bind(EVT_PRUSAAUTH_FAIL, [this](PrusaAuthFailEvent& evt) { BOOST_LOG_TRIVIAL(error) << "Network error message: " << evt.data; @@ -908,13 +914,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, evt.data); }); this->q->Bind(EVT_PRUSACONNECT_PRINTERS_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { - BOOST_LOG_TRIVIAL(error) << "PrusaConnect printers message: " << evt.data; - std::string text = user_account->on_connect_printers_success(evt.data, wxGetApp().app_config); - std::string out = GUI::format( "Printers in your PrusaConnect team:\n%1%", text); - this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); - this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); - - sidebar->update_printer_presets_combobox(); + std::string text; + bool printers_changed = false; + bool succ = user_account->on_connect_printers_success(evt.data, wxGetApp().app_config, printers_changed, text); + if (succ) { + std::string out = GUI::format("Printers in your PrusaConnect team %1%:\n%2%", (printers_changed ? "changed" : "didn't changed"), text); + this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); + this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + if (printers_changed) + sidebar->update_printer_presets_combobox(); + } else { + // TODO + } }); wxGetApp().other_instance_message_handler()->init(this->q); diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp index 7261f4e4c7..3d780db2de 100644 --- a/src/slic3r/GUI/UserAccount.cpp +++ b/src/slic3r/GUI/UserAccount.cpp @@ -61,39 +61,50 @@ void UserAccount::enqueue_connect_printers_action() m_auth_communication->enqueue_connect_printers_action(); } -void UserAccount::on_login_code_recieved(const std::string& url_message) +bool UserAccount::on_login_code_recieved(const std::string& url_message) { m_auth_communication->on_login_code_recieved(url_message); + return true; } -std::string UserAccount::on_user_id_success(const std::string data, AppConfig* app_config) +bool UserAccount::on_user_id_success(const std::string data, AppConfig* app_config, std::string& out_username) { - std::string public_username; + boost::property_tree::ptree ptree; try { std::stringstream ss(data); - boost::property_tree::ptree ptree; boost::property_tree::read_json(ss, ptree); - const auto public_username_optional = ptree.get_optional("public_username"); - if (public_username_optional) - public_username = *public_username_optional; } catch (const std::exception&) { BOOST_LOG_TRIVIAL(error) << "UserIDUserAction Could not parse server response."; + return false; } - assert(!public_username.empty()); + m_user_data.clear(); + for (const auto& section : ptree) { + const auto opt = ptree.get_optional(section.first); + if (opt) { + BOOST_LOG_TRIVIAL(debug) << static_cast(section.first) << *opt; + m_user_data[section.first] = *opt; + } + + } + assert(m_user_data.find("public_username") != m_user_data.end()); + std::string public_username = m_user_data["public_username"]; set_username(public_username, app_config); - return public_username; + out_username = public_username; + return true; } -void UserAccount::on_communication_fail(const std::string data, AppConfig* app_config) +bool UserAccount::on_communication_fail(const std::string data, AppConfig* app_config) { // TODO: should we just declare disconnect on every fail? //set_username({}, app_config); + return true; } -void UserAccount::on_logout( AppConfig* app_config) +bool UserAccount::on_logout( AppConfig* app_config) { set_username({}, app_config); + return true; } namespace { @@ -112,9 +123,9 @@ namespace { } } -std::string UserAccount::on_connect_printers_success(const std::string data, AppConfig* app_config) +bool UserAccount::on_connect_printers_success(const std::string data, AppConfig* app_config, bool& out_printers_changed, std::string& out_message) { - + BOOST_LOG_TRIVIAL(debug) << "PrusaConnect printers message: " << data; pt::ptree ptree; try { std::stringstream ss(data); @@ -122,11 +133,13 @@ std::string UserAccount::on_connect_printers_success(const std::string data, App } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Could not parse prusaconnect message. " << e.what(); - return {}; + return false; } // fill m_printer_map with data from ptree // tree string is in format {"printers": [{..}, {..}]} - m_printer_map.clear(); + + ConnectPrinterStateMap new_printer_map; + assert(ptree.front().first == "printers"); for (const auto& printer_tree : ptree.front().second) { std::string name; @@ -152,26 +165,48 @@ std::string UserAccount::on_connect_printers_success(const std::string data, App assert(true); // On this assert, printer_state_table and ConnectPrinterState needs to be updated continue; } - if (auto counter = m_printer_map.find(name); counter != m_printer_map.end()) { - m_printer_map[name][static_cast(state)]++; + if (auto counter = new_printer_map.find(name); counter != new_printer_map.end()) { + new_printer_map[name][static_cast(state)]++; } else { - m_printer_map[name].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); + new_printer_map[name].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { - m_printer_map[name].push_back(i == static_cast(state) ? 1 : 0); + new_printer_map[name].push_back(i == static_cast(state) ? 1 : 0); } } } + + // compare new and old printer map and update old map into new + out_printers_changed = true; + for (const auto& it : new_printer_map) { + if (m_printer_map.find(it.first) == m_printer_map.end()) { + // printer is not in old map, add it by copying data from new map + out_printers_changed = false; + m_printer_map[it.first].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); + for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { + m_printer_map[it.first].push_back(new_printer_map[it.first][i]); + } + } else { + // printer is in old map, check state by state + for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { + if (m_printer_map[it.first][i] != new_printer_map[it.first][i]) { + out_printers_changed = false; + m_printer_map[it.first][i] = new_printer_map[it.first][i]; + } + } + } + } + std::string out; for (const auto& it : m_printer_map) { - out += GUI::format("%1%: O%2% I%3% P%4% F%5% \n" + out_message += GUI::format("%1%: O%2% I%3% P%4% F%5% \n" , it.first , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_OFFLINE)]) , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_IDLE)]) , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_PRINTING)]) , std::to_string(it.second[static_cast(ConnectPrinterState::CONNECT_PRINTER_FINISHED)])); } - return out; + return true; } std::string UserAccount::get_model_from_json(const std::string& message) const diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index ae5b1f9e14..4cb1184caf 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -42,15 +42,18 @@ public: #endif void enqueue_connect_printers_action(); - void on_login_code_recieved(const std::string& url_message); - std::string on_user_id_success(const std::string data, AppConfig* app_config); - void on_communication_fail(const std::string data, AppConfig* app_config); - void on_logout(AppConfig* app_config); - std::string on_connect_printers_success(const std::string data, AppConfig* app_config); + // Functions called from UI where events emmited from AuthSession are binded + // Returns bool if data were correctly proccessed + bool on_login_code_recieved(const std::string& url_message); + bool on_user_id_success(const std::string data, AppConfig* app_config, std::string& out_username); + bool on_communication_fail(const std::string data, AppConfig* app_config); + bool on_logout(AppConfig* app_config); + bool on_connect_printers_success(const std::string data, AppConfig* app_config, bool& out_printers_changed, std::string& out_message); std::string get_username() const { return m_username; } std::string get_access_token(); const ConnectPrinterStateMap& get_printer_state_map() const { return m_printer_map; } + const std::map get_user_data() const { return m_user_data; } // standalone utility methods std::string get_model_from_json(const std::string& message) const; @@ -60,8 +63,9 @@ private: std::unique_ptr m_auth_communication; - std::string m_username; - ConnectPrinterStateMap m_printer_map; + ConnectPrinterStateMap m_printer_map; + std::map m_user_data; + std::string m_username; const std::map printer_type_and_name_table = { {"1.3.0", "MK3" }, From 55ba9f23514407c389962df8e765b2d0daafc2da Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 11 Dec 2023 11:20:29 +0100 Subject: [PATCH 09/64] Topbar: Implement Auth menu --- resources/icons/login.svg | 18 +++++++++++ resources/icons/logout.svg | 16 ++++++++++ resources/icons/user.svg | 18 +++++++++++ src/slic3r/GUI/GUI_App.cpp | 24 +------------- src/slic3r/GUI/GUI_App.hpp | 3 -- src/slic3r/GUI/MainFrame.cpp | 4 +-- src/slic3r/GUI/Plater.cpp | 2 -- src/slic3r/GUI/TopBar.cpp | 62 +++++++++++++++++++++++++++++++++++- src/slic3r/GUI/TopBar.hpp | 16 ++++++++-- 9 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 resources/icons/login.svg create mode 100644 resources/icons/logout.svg create mode 100644 resources/icons/user.svg diff --git a/resources/icons/login.svg b/resources/icons/login.svg new file mode 100644 index 0000000000..0b01b66613 --- /dev/null +++ b/resources/icons/login.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/logout.svg b/resources/icons/logout.svg new file mode 100644 index 0000000000..b2aa004057 --- /dev/null +++ b/resources/icons/logout.svg @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/user.svg b/resources/icons/user.svg new file mode 100644 index 0000000000..b750021fc4 --- /dev/null +++ b/resources/icons/user.svg @@ -0,0 +1,18 @@ + + + + + Abstract user icon + + + + + \ No newline at end of file diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 52952840ef..c0b7600483 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2498,11 +2498,6 @@ wxMenu* GUI_App::get_config_menu() local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); local_menu->AppendSeparator(); - wxMenuItem* updatable_item = local_menu->Append(config_id_base + ConfigMenuAuthLogin, _L("PrusaAuth Log in"), _L("")); - m_config_menu_updatable_items.emplace(ConfigMenuIDs::ConfigMenuAuthLogin, updatable_item); - updatable_item = local_menu->Append(config_id_base + ConfigMenuConnectDummy, _L("PrusaConnect Printers"), _L("")); - updatable_item->Enable(false); - m_config_menu_updatable_items.emplace(ConfigMenuIDs::ConfigMenuConnectDummy, updatable_item); local_menu->Append(config_id_base + ConfigMenuConnectDialog, _L("Connect Dialog"), _L("Connect Dialog")); local_menu->Append(config_id_base + ConfigMenuMediaDialog, _L("Media Dialog"), _L("Media Dialog")); #if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) @@ -2540,19 +2535,6 @@ wxMenu* GUI_App::get_config_menu() case ConfigMenuUpdateApp: app_version_check(true); break; - case ConfigMenuAuthLogin: - { - if (this->plater()->get_user_account()->is_logged()) - this->plater()->get_user_account()->do_logout(); - else - this->plater()->get_user_account()->do_login(); - } - break; - case ConfigMenuConnectDummy: - { - this->plater()->get_user_account()->enqueue_connect_printers_action(); - } - break; #ifdef __linux__ case ConfigMenuDesktopIntegration: show_desktop_integration_dialog(); @@ -2666,11 +2648,7 @@ wxMenu* GUI_App::get_config_menu() return local_menu; } -void GUI_App::update_config_menu() -{ - m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuAuthLogin]->SetItemLabel(this->plater()->get_user_account()->is_logged() ? _L("PrusaAuth Log out") : _L("PrusaAuth Log in")); - m_config_menu_updatable_items[ConfigMenuIDs::ConfigMenuConnectDummy]->Enable(this->plater()->get_user_account()->is_logged()); -} + void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) { mainframe->preferences_dialog->show(highlight_option, tab_name); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index d2f3b47a86..e062cb0373 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -99,8 +99,6 @@ enum ConfigMenuIDs { ConfigMenuUpdateConf, ConfigMenuUpdateApp, ConfigMenuMediaDialog, - ConfigMenuAuthLogin, - ConfigMenuConnectDummy, ConfigMenuConnectDialog, ConfigMenuDesktopIntegration, ConfigMenuPreferences, @@ -299,7 +297,6 @@ public: void update_mode(); wxMenu* get_config_menu(); - void update_config_menu(); bool has_unsaved_preset_changes() const; bool has_current_preset_changes() const; void update_saved_preset_from_current_preset(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ece8cfc1e4..caff932c98 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -836,10 +836,10 @@ void MainFrame::create_preset_tabs() add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); m_webview = new WebViewPanel(m_tabpanel); - m_tabpanel->AddPage(m_webview, "PrusaConnect"); + dynamic_cast(m_tabpanel)->AddPage(m_webview, "PrusaConnect", ""); /* m_media = new MediaMainPanel(this); - m_tabpanel->AddPage(m_media, "Media"); + dynamic_cast(m_tabpanel)->AddPage(m_media, "Media"); */ } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f8a8160c91..dca2019296 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -887,7 +887,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string text = _u8L("Logged out."); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); - wxGetApp().update_config_menu(); }); this->q->Bind(EVT_PA_ID_USER_SUCCESS, [this](PrusaAuthSuccessEvent& evt) { @@ -897,7 +896,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string text = format(_u8L("Logged as %1%."), username); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); - wxGetApp().update_config_menu(); } else { // TODO } diff --git a/src/slic3r/GUI/TopBar.cpp b/src/slic3r/GUI/TopBar.cpp index 6c2c2e1ea9..291c70824e 100644 --- a/src/slic3r/GUI/TopBar.cpp +++ b/src/slic3r/GUI/TopBar.cpp @@ -2,6 +2,7 @@ #include "GUI_App.hpp" #include "Plater.hpp" +#include "UserAccount.hpp" //#include "wxExtensions.hpp" #include "format.hpp" #include "I18N.hpp" @@ -27,6 +28,10 @@ TopBarItemsCtrl::Button::Button(wxWindow* parent, const wxString& label, const s wxSize size = GetTextExtent(label) + wxSize(6 * btn_margin, int(1.5 * btn_margin)); if (icon_name.empty()) this->SetMinSize(size); + if (label.IsEmpty()) { + const int btn_side = px_cnt + btn_margin; + this->SetMinSize(wxSize(btn_side, btn_side)); + } else this->SetMinSize(wxSize(-1, size.y)); } @@ -62,6 +67,11 @@ TopBarItemsCtrl::ButtonWithPopup::ButtonWithPopup(wxWindow* parent, const wxStri this->SetLabel(label); } +TopBarItemsCtrl::ButtonWithPopup::ButtonWithPopup(wxWindow* parent, const std::string& icon_name, int icon_width/* = 20*/, int icon_height/* = 20*/) + :TopBarItemsCtrl::Button(parent, "", icon_name, icon_width) +{ +} + void TopBarItemsCtrl::ButtonWithPopup::SetLabel(const wxString& label) { wxString full_label = " " + label + " " + down_arrow; @@ -97,6 +107,42 @@ void TopBarItemsCtrl::ApplyWorkspacesMenu() } } +void TopBarItemsCtrl::CreateAuthMenu() +{ + m_user_menu_item = append_menu_item(&m_auth_menu, wxID_ANY, "", "", + [this](wxCommandEvent& e) { + m_auth_btn->set_selected(true); + wxGetApp().plater()->PopupMenu(&m_auth_menu, m_auth_btn->GetPosition()); + }, get_bmp_bundle("user", 16), nullptr, []() { return true; }, this); + + m_auth_menu.AppendSeparator(); + + m_connect_dummy_menu_item = append_menu_item(&m_auth_menu, wxID_ANY, _L("PrusaConnect Printers"), "", + [this](wxCommandEvent&) { wxGetApp().plater()->get_user_account()->enqueue_connect_printers_action(); }, + "", nullptr, []() { return wxGetApp().plater()->get_user_account()->is_logged(); }, this->GetParent()); + + m_login_menu_item = append_menu_item(&m_auth_menu, wxID_ANY, "", "", + [this](wxCommandEvent&) { + auto user_account = wxGetApp().plater()->get_user_account(); + if (user_account->is_logged()) + user_account->do_logout(); + else + user_account->do_login(); + }, get_bmp_bundle("login", 16), nullptr, []() { return true; }, this); +} + +void TopBarItemsCtrl::UpdateAuthMenu() +{ + auto user_account = wxGetApp().plater()->get_user_account(); + if (m_login_menu_item) { + m_login_menu_item->SetItemLabel(user_account->is_logged() ? _L("PrusaAuth Log out") : _L("PrusaAuth Log in")); + m_login_menu_item->SetBitmap(user_account->is_logged() ? *get_bmp_bundle("logout", 16) : *get_bmp_bundle("login", 16)); + } + + if (m_user_menu_item) + m_user_menu_item->SetItemLabel(user_account->is_logged() ? from_u8(user_account->get_username()) : _L("Anonymus")); +} + TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : wxControl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxTAB_TRAVERSAL) { @@ -134,7 +180,7 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : m_workspace_btn = new ButtonWithPopup(this, _L("Workspace"), "mode_simple"); m_sizer->AddStretchSpacer(20); - m_sizer->Add(m_workspace_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT, 2 * m_btn_margin); + m_sizer->Add(m_workspace_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT); m_workspace_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { m_workspace_btn->set_selected(true); @@ -143,6 +189,20 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : }); m_workspaces_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { m_workspace_btn->set_selected(false); }); + // create Auth menu + CreateAuthMenu(); + + m_auth_btn = new ButtonWithPopup(this, "user", 35); + m_sizer->Add(m_auth_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT | wxLEFT, m_btn_margin); + + m_auth_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + UpdateAuthMenu(); + m_auth_btn->set_selected(true); + wxPoint pos = m_auth_btn->GetPosition(); + wxGetApp().plater()->PopupMenu(&m_auth_menu, pos); + }); + m_auth_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { m_auth_btn->set_selected(false); }); + this->Bind(wxEVT_PAINT, &TopBarItemsCtrl::OnPaint, this); } diff --git a/src/slic3r/GUI/TopBar.hpp b/src/slic3r/GUI/TopBar.hpp index 9ec522c2d0..e330a6d35a 100644 --- a/src/slic3r/GUI/TopBar.hpp +++ b/src/slic3r/GUI/TopBar.hpp @@ -36,14 +36,23 @@ class TopBarItemsCtrl : public wxControl ButtonWithPopup(wxWindow* parent, const wxString& label, const std::string& icon_name = ""); + ButtonWithPopup(wxWindow* parent, + const std::string& icon_name, + int icon_width = 20, + int icon_height = 20); ~ButtonWithPopup() {} void SetLabel(const wxString& label) override; }; - wxMenu m_main_menu; - wxMenu m_workspaces_menu; + wxMenu m_main_menu; + wxMenu m_workspaces_menu; + wxMenu m_auth_menu; + + wxMenuItem* m_user_menu_item{ nullptr }; + wxMenuItem* m_login_menu_item{ nullptr }; + wxMenuItem* m_connect_dummy_menu_item{ nullptr }; public: TopBarItemsCtrl(wxWindow* parent); @@ -64,6 +73,8 @@ public: void AppendMenuItem(wxMenu* menu, const wxString& title); void AppendMenuSeparaorItem(); void ApplyWorkspacesMenu(); + void CreateAuthMenu(); + void UpdateAuthMenu(); private: wxWindow* m_parent; @@ -71,6 +82,7 @@ private: wxBoxSizer* m_sizer; ButtonWithPopup* m_menu_btn {nullptr}; ButtonWithPopup* m_workspace_btn {nullptr}; + ButtonWithPopup* m_auth_btn {nullptr}; std::vector m_pageButtons; int m_selection {-1}; int m_btn_margin; From 822b862fce236164554fd88f1f58b62c04e7e110 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 20 Dec 2023 13:16:56 +0100 Subject: [PATCH 10/64] Printers action and webview development Automatic "Connect printers" action after login periodic connect printers update adding and removing connect tab on login Webview refactor remove unused code load url on adding panel monitor tab upload to connect button --- resources/icons/connect_gcode.svg | 65 +++++++++++++++++++++ src/slic3r/GUI/AuthSession.cpp | 10 +++- src/slic3r/GUI/AuthSession.hpp | 2 +- src/slic3r/GUI/GUI_App.cpp | 24 +++++++- src/slic3r/GUI/GUI_App.hpp | 1 + src/slic3r/GUI/MainFrame.cpp | 57 ++++++++++++++++-- src/slic3r/GUI/MainFrame.hpp | 17 +++++- src/slic3r/GUI/NotificationManager.hpp | 4 +- src/slic3r/GUI/Plater.cpp | 60 +++++++++++++++++-- src/slic3r/GUI/Plater.hpp | 1 + src/slic3r/GUI/PresetComboBoxes.cpp | 2 +- src/slic3r/GUI/Sidebar.cpp | 17 +++++- src/slic3r/GUI/Sidebar.hpp | 5 +- src/slic3r/GUI/UserAccount.cpp | 20 ++++--- src/slic3r/GUI/UserAccount.hpp | 2 +- src/slic3r/GUI/WebView.cpp | 2 +- src/slic3r/GUI/WebViewDialog.cpp | 80 ++++++++++++++++---------- src/slic3r/GUI/WebViewDialog.hpp | 45 ++++++++++----- 18 files changed, 340 insertions(+), 74 deletions(-) create mode 100644 resources/icons/connect_gcode.svg diff --git a/resources/icons/connect_gcode.svg b/resources/icons/connect_gcode.svg new file mode 100644 index 0000000000..fb57be9c46 --- /dev/null +++ b/resources/icons/connect_gcode.svg @@ -0,0 +1,65 @@ + +image/svg+xml + + + + + + + diff --git a/src/slic3r/GUI/AuthSession.cpp b/src/slic3r/GUI/AuthSession.cpp index 7d24d87eef..56a5f2a3c1 100644 --- a/src/slic3r/GUI/AuthSession.cpp +++ b/src/slic3r/GUI/AuthSession.cpp @@ -58,13 +58,15 @@ void UserActionGetWithEvent::perform(const std::string& access_token, UserAction if (fail_callback) fail_callback(body); std::string message = GUI::format("%1% action failed (%2%): %3%", m_action_name, std::to_string(status), body); - wxQueueEvent(m_evt_handler, new PrusaAuthFailEvent(m_fail_evt_type, std::move(message))); + if (m_succ_evt_type != wxEVT_NULL) + wxQueueEvent(m_evt_handler, new PrusaAuthFailEvent(m_fail_evt_type, std::move(message))); }); http.on_complete([&, this](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << m_action_name << " action success. Status: " << status << " Body: " << body; if (success_callback) success_callback(body); - wxQueueEvent(m_evt_handler, new PrusaAuthSuccessEvent(m_succ_evt_type, body)); + if (m_succ_evt_type != wxEVT_NULL) + wxQueueEvent(m_evt_handler, new PrusaAuthSuccessEvent(m_succ_evt_type, body)); }); http.perform_sync(); @@ -75,7 +77,9 @@ void AuthSession::process_action_queue() BOOST_LOG_TRIVIAL(debug) << "process_action_queue start"; if (m_priority_action_queue.empty() && m_action_queue.empty()) { BOOST_LOG_TRIVIAL(debug) << "process_action_queue queues empty"; - return; + // update printers on every periodic wakeup call + enqueue_action(UserActionID::CONNECT_PRINTERS, nullptr, nullptr, {}); + //return; } if (this->is_initialized()) { diff --git a/src/slic3r/GUI/AuthSession.hpp b/src/slic3r/GUI/AuthSession.hpp index 4b6b7316d9..27f8681514 100644 --- a/src/slic3r/GUI/AuthSession.hpp +++ b/src/slic3r/GUI/AuthSession.hpp @@ -101,7 +101,7 @@ public: m_actions[UserActionID::DUMMY_ACTION] = std::make_unique(); m_actions[UserActionID::REFRESH_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); m_actions[UserActionID::CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); - m_actions[UserActionID::TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); + m_actions[UserActionID::TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, wxEVT_NULL, EVT_PRUSAAUTH_FAIL); m_actions[UserActionID::USER_ID] = std::make_unique("USER_ID", "https://test-account.prusa3d.com/api/v1/me/", evt_handler, EVT_PA_ID_USER_SUCCESS, EVT_PRUSAAUTH_FAIL); m_actions[UserActionID::CONNECT_DUMMY] = std::make_unique("CONNECT_DUMMY", "https://dev.connect.prusa3d.com/slicer/dummy"/*"dev.connect.prusa:8000/slicer/dummy"*/, evt_handler, EVT_PRUSAAUTH_SUCCESS, EVT_PRUSAAUTH_FAIL); m_actions[UserActionID::CONNECT_PRINTERS] = std::make_unique("CONNECT_PRINTERS", "https://dev.connect.prusa3d.com/slicer/printers"/*"dev.connect.prusa:8000/slicer/printers"*/, evt_handler, EVT_PRUSACONNECT_PRINTERS_SUCCESS, EVT_PRUSAAUTH_FAIL); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c0b7600483..7b87097c3b 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2639,7 +2639,7 @@ wxMenu* GUI_App::get_config_menu() wxMediaPlayerDialog("Media").ShowModal(); break; case ConfigMenuConnectDialog: - WebViewDialog(plater()).ShowModal(); + //WebViewDialog(plater()).ShowModal(); break; default: break; @@ -2681,7 +2681,7 @@ void GUI_App::open_preferences(const std::string& highlight_option /*= std::stri if (mainframe->preferences_dialog->settings_layout_changed()) { // hide full main_sizer for mainFrame mainframe->GetSizer()->Show(false); - mainframe->update_layout(); + mainframe->update_layout(); mainframe->select_tab(size_t(0)); } } @@ -3673,5 +3673,25 @@ void GUI_App::handle_web_request(std::string cmd) this->plater()->get_notification_manager()->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); } +void GUI_App::show_monitor_tab(bool show, const std::string& address/* = {}*/) +{ + std::string url = address; + if(url.find("http://") != 0 && url.find("https://") != 0) { + url = "https://" + url; + } + + if (!show) { + this->mainframe->select_tab(size_t(0)); + mainframe->remove_monitor_tab(); + } + else { + if (mainframe->get_monitor_tab_added()) + mainframe->set_monitor_tab_url(boost::nowide::widen(url)); + else + mainframe->add_monitor_tab(boost::nowide::widen(url)); + this->mainframe->select_tab(size_t(5)); + this->mainframe->select_tab(size_t(0)); + } +} } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index e062cb0373..f329f11938 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -411,6 +411,7 @@ public: void request_user_logout() {} int request_user_unbind(std::string dev_id) { return 0; } void handle_web_request(std::string cmd); + void show_monitor_tab(bool show, const std::string& address = {}); // return true if preset vas invisible and we have to installed it to make it selectable bool select_printer_from_connect(const Preset* printer_preset); void handle_script_message(std::string msg) {} diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index caff932c98..693ce063d2 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -835,14 +835,64 @@ void MainFrame::create_preset_tabs() add_created_tab(new TabSLAMaterial(m_tabpanel), "resin"); add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); - m_webview = new WebViewPanel(m_tabpanel); - dynamic_cast(m_tabpanel)->AddPage(m_webview, "PrusaConnect", ""); + m_connect_webview = new ConnectWebViewPanel(m_tabpanel); + m_monitor_webview = new WebViewPanel(m_tabpanel, L""); /* m_media = new MediaMainPanel(this); dynamic_cast(m_tabpanel)->AddPage(m_media, "Media"); */ } +void MainFrame::enable_connect_tab() +{ + if (m_connect_webview_added) + return; + size_t selected = m_tabpanel->GetSelection(); + dynamic_cast(m_tabpanel)->AddPage(m_connect_webview, "PrusaConnect", "", true); + m_connect_webview->load_default_url(); + m_tabpanel->SetSelection(selected); + m_connect_webview_added = true; +} +void MainFrame::disable_connect_tab() +{ + if (!m_connect_webview_added) + return; + size_t selected = m_tabpanel->GetSelection(); + if (selected == 4) + m_tabpanel->SetSelection(0); + dynamic_cast(m_tabpanel)->RemovePage(4); + m_connect_webview_added = false; +} + +void MainFrame::add_monitor_tab(const wxString& url) +{ + if (m_monitor_webview_added) + return; + size_t selected = m_tabpanel->GetSelection(); + dynamic_cast(m_tabpanel)->AddPage(m_monitor_webview, "Monitor", "", true); + m_monitor_webview->set_default_url(url); + m_monitor_webview->load_default_url(); + m_tabpanel->SetSelection(selected); + m_monitor_webview_added = true; + +} +void MainFrame::remove_monitor_tab() +{ + if (!m_monitor_webview_added) + return; + size_t selected = m_tabpanel->GetSelection(); + if (selected == 4) + m_tabpanel->SetSelection(0); + dynamic_cast(m_tabpanel)->RemovePage(m_tabpanel->GetPageCount() - 1); + m_monitor_webview_added = false; +} +void MainFrame::set_monitor_tab_url(const wxString& url) +{ + assert(m_monitor_webview_added); + m_monitor_webview->set_default_url(url); + m_monitor_webview->load_default_url(); +} + void MainFrame::add_created_tab(Tab* panel, const std::string& bmp_name /*= ""*/) { panel->create_preset_tab(); @@ -1523,9 +1573,6 @@ void MainFrame::init_menubar_as_editor() append_menu_check_item(viewMenu, wxID_ANY, _L("&Collapse Sidebar") + sep + "Shift+" + sep_space + "Tab", _L("Collapse sidebar"), [this](wxCommandEvent&) { m_plater->collapse_sidebar(!m_plater->is_sidebar_collapsed()); }, this, []() { return true; }, [this]() { return m_plater->is_sidebar_collapsed(); }, this); - append_menu_check_item(viewMenu, wxID_ANY, _L("&Load URL"), _L("Load URL"), - [this](wxCommandEvent&) { wxString url = "https://dev.connect.prusa3d.com/"/*"file:///C:/Projects/BambuStudio/resources/web/homepage/index.html"*/; m_webview->load_url(url); }, this, - []() { return true; }, []() { return true; }, this); #ifndef __APPLE__ // OSX adds its own menu item to toggle fullscreen. append_menu_check_item(viewMenu, wxID_ANY, _L("&Fullscreen") + "\t" + "F11", _L("Fullscreen"), diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 6584df4a5c..83382fb5a9 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -98,6 +98,11 @@ class MainFrame : public DPIFrame size_t m_last_selected_tab; Search::OptionsSearcher m_searcher; + WebViewPanel* m_connect_webview{ nullptr }; + bool m_connect_webview_added{ false }; + WebViewPanel* m_monitor_webview{ nullptr }; + bool m_monitor_webview_added{ false }; + std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const; std::string get_dir_name(const wxString &full_name) const; @@ -210,6 +215,14 @@ public: void add_to_recent_projects(const wxString& filename); void technology_changed(); + void enable_connect_tab(); + void disable_connect_tab(); + + void add_monitor_tab(const wxString& url); + void remove_monitor_tab(); + void set_monitor_tab_url(const wxString& url); + bool get_monitor_tab_added() const { return m_monitor_webview_added; } + PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; } Plater* m_plater { nullptr }; @@ -221,9 +234,7 @@ public: PreferencesDialog* preferences_dialog { nullptr }; PrintHostQueueDialog* m_printhost_queue_dlg; GalleryDialog* m_gallery_dialog{ nullptr }; - WebViewPanel* m_webview{ nullptr }; - MediaMainPanel* m_media{ nullptr}; - + #ifdef __APPLE__ std::unique_ptr m_taskbar_icon; #endif // __APPLE__ diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 2dfefffa35..ff42f50c1c 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -130,8 +130,10 @@ enum class NotificationType URLNotRegistered, // Config file was detected during startup, open wifi config dialog via hypertext WifiConfigFileDetected, - // + // Info abouty successful login or logout PrusaAuthUserID, + // Debug notification for connect communication + PrusaConnectPrinters, }; class NotificationManager diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index dca2019296..7f088f8c14 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -123,6 +123,7 @@ #include "FileArchiveDialog.hpp" #include "UserAccount.hpp" #include "DesktopIntegrationDialog.hpp" +#include "WebViewDialog.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -884,6 +885,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_LOGGEDOUT_PRUSAAUTH, [this](PrusaAuthSuccessEvent& evt) { user_account->on_logout(wxGetApp().app_config); + this->main_frame->disable_connect_tab(); std::string text = _u8L("Logged out."); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); @@ -893,6 +895,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string username; bool succ = user_account->on_user_id_success(evt.data, wxGetApp().app_config, username); if (succ) { + // show connect tab + this->main_frame->enable_connect_tab(); + // login notification std::string text = format(_u8L("Logged as %1%."), username); this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); @@ -916,11 +921,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) bool printers_changed = false; bool succ = user_account->on_connect_printers_success(evt.data, wxGetApp().app_config, printers_changed, text); if (succ) { - std::string out = GUI::format("Printers in your PrusaConnect team %1%:\n%2%", (printers_changed ? "changed" : "didn't changed"), text); - this->notification_manager->close_notification_of_type(NotificationType::PrusaAuthUserID); - this->notification_manager->push_notification(NotificationType::PrusaAuthUserID, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); if (printers_changed) + { + // this could be moved outside if to notify for "no change" + //std::string out = GUI::format("Printers in your PrusaConnect team %1%:\n%2%", (printers_changed ? "changed" : "didn't changed"), text); + std::string out = GUI::format("Printers in your PrusaConnect team:\n%1%", text); + this->notification_manager->close_notification_of_type(NotificationType::PrusaConnectPrinters); + this->notification_manager->push_notification(NotificationType::PrusaConnectPrinters, NotificationManager::NotificationLevel::ImportantNotificationLevel, out); + // this should be done only if printers_changed sidebar->update_printer_presets_combobox(); + } } else { // TODO } @@ -3479,7 +3489,7 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice_) const DynamicPrintConfig* selected_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); const auto print_host_opt = selected_printer_config ? selected_printer_config->option("print_host") : nullptr; const bool send_gcode_shown = print_host_opt != nullptr && !print_host_opt->value.empty(); - + const bool connect_gcode_shown = print_host_opt == nullptr && user_account->is_logged(); // when a background processing is ON, export_btn and/or send_btn are showing if (get_config_bool("background_processing")) { @@ -3487,6 +3497,7 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice_) const if (sidebar->show_reslice(false) | sidebar->show_export(true) | sidebar->show_send(send_gcode_shown) | + sidebar->show_connect(connect_gcode_shown) | sidebar->show_export_removable(removable_media_status.has_removable_drives)) sidebar->Layout(); } @@ -3498,6 +3509,7 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice_) const if (sidebar->show_reslice(ready_to_slice) | sidebar->show_export(!ready_to_slice) | sidebar->show_send(send_gcode_shown && !ready_to_slice) | + sidebar->show_connect(connect_gcode_shown && !ready_to_slice) | sidebar->show_export_removable(!ready_to_slice && removable_media_status.has_removable_drives)) sidebar->Layout(); } @@ -5801,6 +5813,46 @@ bool load_secret(const std::string& id, const std::string& opt, std::string& usr #endif // wxUSE_SECRETSTORE } } +void Plater::connect_gcode() +{ + assert(p->user_account->is_logged()); + std::string dialog_msg; + if(PrinterPickWebViewDialog(this, dialog_msg).ShowModal() != wxID_OK) { + return; + } + if (dialog_msg.empty()) { + return; + } + BOOST_LOG_TRIVIAL(error) << dialog_msg; + + std::string model_name = p->user_account->get_model_from_json(dialog_msg); + std::string nozzle = p->user_account->get_nozzle_from_json(dialog_msg); + assert(!model_name.empty()); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + const Preset* preset = preset_bundle->printers.find_system_preset_by_model_and_variant(model_name, nozzle); + + // if selected (in connect) preset is not visible, make it visible and selected + if (!preset->is_visible) { + size_t preset_id = preset_bundle->printers.get_preset_idx_by_name(preset->name); + assert(preset_id != size_t(-1)); + preset_bundle->printers.select_preset(preset_id); + wxGetApp().get_tab(Preset::Type::TYPE_PRINTER)->select_preset(preset->name); + return; + } + + // if selected (in connect) preset is not selected in slicer, select it + if (preset_bundle->printers.get_selected_preset_name() != preset->name) + { + size_t preset_id = preset_bundle->printers.get_preset_idx_by_name(preset->name); + assert(preset_id != size_t(-1)); + preset_bundle->printers.select_preset(preset_id); + wxGetApp().get_tab(Preset::Type::TYPE_PRINTER)->select_preset(preset->name); + return; + } + + // TODO: get api key from dialog_msg and upload the file + // api key is not currently in the message +} void Plater::send_gcode() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index b29e43e154..80f07e4f40 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -225,6 +225,7 @@ public: void suppress_background_process(const bool stop_background_process) ; void send_gcode(); void eject_drive(); + void connect_gcode(); void take_snapshot(const std::string &snapshot_name); void take_snapshot(const wxString &snapshot_name); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 875c6ad778..d97ca3b9e4 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -667,7 +667,7 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset }); if (m_type == Preset::TYPE_PRINTER) - connect_info = new wxGenericStaticText(parent, wxID_ANY, "Info about Connect for printer preset"); + connect_info = new wxGenericStaticText(parent, wxID_ANY, /*"Info about Connect for printer preset"*/ ""); } PlaterPresetComboBox::~PlaterPresetComboBox() diff --git a/src/slic3r/GUI/Sidebar.cpp b/src/slic3r/GUI/Sidebar.cpp index 87d026794d..7b9b356be9 100644 --- a/src/slic3r/GUI/Sidebar.cpp +++ b/src/slic3r/GUI/Sidebar.cpp @@ -483,6 +483,7 @@ Sidebar::Sidebar(Plater *parent) init_scalable_btn(&m_btn_send_gcode , "export_gcode", _L("Send to printer") + " " +GUI::shortkey_ctrl_prefix() + "Shift+G"); init_scalable_btn(&m_btn_export_gcode_removable, "export_to_sd", _L("Export to SD card / Flash drive") + " " + GUI::shortkey_ctrl_prefix() + "U"); + init_scalable_btn(&m_btn_connect_gcode, "connect_gcode", _L("Send to Connect") + " " + GUI::shortkey_ctrl_prefix() + "Shift+G"); // regular buttons "Slice now" and "Export G-code" @@ -510,6 +511,7 @@ Sidebar::Sidebar(Plater *parent) complect_btns_sizer->Add(m_btn_export_gcode, 1, wxEXPAND); complect_btns_sizer->Add(m_btn_send_gcode, 0, wxLEFT, margin_5); complect_btns_sizer->Add(m_btn_export_gcode_removable, 0, wxLEFT, margin_5); + complect_btns_sizer->Add(m_btn_connect_gcode, 0, wxLEFT, margin_5); btns_sizer->Add(m_btn_reslice, 0, wxEXPAND | wxTOP, margin_5); btns_sizer->Add(complect_btns_sizer, 0, wxEXPAND | wxTOP, margin_5); @@ -547,6 +549,7 @@ Sidebar::Sidebar(Plater *parent) m_btn_send_gcode->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { m_plater->send_gcode(); }); m_btn_export_gcode_removable->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { m_plater->export_gcode(true); }); + m_btn_connect_gcode->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { m_plater->connect_gcode(); }); this->Bind(wxEVT_COMBOBOX, &Sidebar::on_select_preset, this); @@ -721,6 +724,13 @@ void Sidebar::on_select_preset(wxCommandEvent& evt) * and for SLA presets they should be deleted */ m_object_list->update_object_list_by_printer_technology(); + + if (combo->is_selected_physical_printer()) + { + wxGetApp().show_monitor_tab(true, wxGetApp().preset_bundle->physical_printers.get_selected_printer().config.opt_string("print_host")); + } + else + wxGetApp().show_monitor_tab(false); } #ifdef __WXMSW__ @@ -812,6 +822,7 @@ void Sidebar::sys_color_changed() // btn...->msw_rescale() updates icon on button, so use it m_btn_send_gcode ->sys_color_changed(); m_btn_export_gcode_removable->sys_color_changed(); + m_btn_connect_gcode ->sys_color_changed(); m_scrolled_panel->Layout(); m_scrolled_panel->Refresh(); @@ -1085,12 +1096,15 @@ void Sidebar::enable_buttons(bool enable) m_btn_export_gcode->Enable(enable); m_btn_send_gcode->Enable(enable); m_btn_export_gcode_removable->Enable(enable); + m_btn_connect_gcode->Enable(enable); } bool Sidebar::show_reslice(bool show) const { return m_btn_reslice->Show(show); } bool Sidebar::show_export(bool show) const { return m_btn_export_gcode->Show(show); } bool Sidebar::show_send(bool show) const { return m_btn_send_gcode->Show(show); } bool Sidebar::show_export_removable(bool show) const { return m_btn_export_gcode_removable->Show(show); } +bool Sidebar::show_connect(bool show) const { return m_btn_connect_gcode->Show(show); } + void Sidebar::update_mode() { @@ -1119,7 +1133,8 @@ void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& lab { case ActionButtonType::Reslice: m_btn_reslice->SetLabelText(label); break; case ActionButtonType::Export: m_btn_export_gcode->SetLabelText(label); break; - case ActionButtonType::SendGCode: /*m_btn_send_gcode->SetLabelText(label);*/ break; + case ActionButtonType::SendGCode: /*m_btn_send_gcode->SetLabelText(label);*/ break; + case ActionButtonType::Connect: /*m_btn_connect_gcode->SetLabelText(label);*/ break; } } diff --git a/src/slic3r/GUI/Sidebar.hpp b/src/slic3r/GUI/Sidebar.hpp index 2ec2af653f..498fc65d87 100644 --- a/src/slic3r/GUI/Sidebar.hpp +++ b/src/slic3r/GUI/Sidebar.hpp @@ -49,7 +49,8 @@ class Plater; enum class ActionButtonType : int { Reslice, Export, - SendGCode + SendGCode, + Connect }; class Sidebar : public wxPanel @@ -79,6 +80,7 @@ class Sidebar : public wxPanel wxButton* m_btn_reslice { nullptr }; ScalableButton* m_btn_send_gcode { nullptr }; ScalableButton* m_btn_export_gcode_removable{ nullptr }; //exports to removable drives (appears only if removable drive is connected) + ScalableButton* m_btn_connect_gcode { nullptr }; std::unique_ptr m_frequently_changed_parameters; std::unique_ptr m_object_manipulation; @@ -123,6 +125,7 @@ public: bool show_export(bool show) const; bool show_send(bool show) const; bool show_export_removable(bool show) const; + bool show_connect(bool show) const; void collapse(bool collapse); void change_top_border_for_mode_sizer(bool increase_border); diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp index 3d780db2de..91114c30e2 100644 --- a/src/slic3r/GUI/UserAccount.cpp +++ b/src/slic3r/GUI/UserAccount.cpp @@ -1,5 +1,7 @@ #include "UserAccount.hpp" +#include "format.hpp" + #include #include #include @@ -88,9 +90,16 @@ bool UserAccount::on_user_id_success(const std::string data, AppConfig* app_conf } assert(m_user_data.find("public_username") != m_user_data.end()); + if (m_user_data.find("public_username") == m_user_data.end()) + { + BOOST_LOG_TRIVIAL(error) << "User ID message from Connect did not contain public_username. Login failed. Message data: " << data; + return false; + } std::string public_username = m_user_data["public_username"]; set_username(public_username, app_config); out_username = public_username; + // update printers list + enqueue_connect_printers_action(); return true; } @@ -136,7 +145,7 @@ bool UserAccount::on_connect_printers_success(const std::string data, AppConfig* return false; } // fill m_printer_map with data from ptree - // tree string is in format {"printers": [{..}, {..}]} + // tree string is in format {"printers": [{..}, {..}]} ConnectPrinterStateMap new_printer_map; @@ -176,11 +185,11 @@ bool UserAccount::on_connect_printers_success(const std::string data, AppConfig* } // compare new and old printer map and update old map into new - out_printers_changed = true; + out_printers_changed = false; for (const auto& it : new_printer_map) { if (m_printer_map.find(it.first) == m_printer_map.end()) { // printer is not in old map, add it by copying data from new map - out_printers_changed = false; + out_printers_changed = true; m_printer_map[it.first].reserve(static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT)); for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { m_printer_map[it.first].push_back(new_printer_map[it.first][i]); @@ -189,7 +198,7 @@ bool UserAccount::on_connect_printers_success(const std::string data, AppConfig* // printer is in old map, check state by state for (size_t i = 0; i < static_cast(ConnectPrinterState::CONNECT_PRINTER_STATE_COUNT); i++) { if (m_printer_map[it.first][i] != new_printer_map[it.first][i]) { - out_printers_changed = false; + out_printers_changed = true; m_printer_map[it.first][i] = new_printer_map[it.first][i]; } } @@ -221,9 +230,6 @@ std::string UserAccount::get_model_from_json(const std::string& message) const if (auto pair = printer_type_and_name_table.find(printer_type); pair != printer_type_and_name_table.end()) { out = pair->second; } - else { - out = parse_tree_for_param(ptree, "printer_type_name"); - } //assert(!out.empty()); } catch (const std::exception& e) { diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index 4cb1184caf..e7cb914d51 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -72,11 +72,11 @@ private: {"1.3.1", "MK3S" }, {"1.4.0", "MK4" }, {"2.1.0", "MINI" }, + {"3.1.0", "XL" }, // ysFIXME : needs to add Connect ids for next printers {"0.0.0", "MK4IS" }, {"0.0.0", "MK3SMMU2S" }, {"0.0.0", "MK3MMU2" }, - {"0.0.0", "XL" }, {"0.0.0", "MK2.5S" }, {"0.0.0", "MK2.5" }, {"0.0.0", "MK2.5SMMU2S" }, diff --git a/src/slic3r/GUI/WebView.cpp b/src/slic3r/GUI/WebView.cpp index 96c279517c..0662447d76 100644 --- a/src/slic3r/GUI/WebView.cpp +++ b/src/slic3r/GUI/WebView.cpp @@ -107,7 +107,7 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); // And the memory: file system //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); - webView->Create(parent, wxID_ANY, url2, wxDefaultPosition, wxDefaultSize); + webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); webView->SetUserAgent(wxString::Format("PrusaSlicer/v%s", SLIC3R_VERSION)); #endif #ifndef __WIN32__ diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 5769d30b83..01b355caec 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -24,10 +24,11 @@ namespace Slic3r { namespace GUI { -WebViewPanel::WebViewPanel(wxWindow *parent) +WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_default_url (default_url) { - wxString url = "https://dev.connect.prusa3d.com/prusa-slicer/printers"; + //wxString url = "https://dev.connect.prusa3d.com/prusa-slicer/printers"; //std::string strlang = wxGetApp().app_config->get("language"); //if (strlang != "") // url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); @@ -74,7 +75,7 @@ WebViewPanel::WebViewPanel(wxWindow *parent) #endif // Create the webview - m_browser = WebView::CreateWebView(this, url); + m_browser = WebView::CreateWebView(this, m_default_url); if (m_browser == nullptr) { wxLogError("Could not init m_browser"); return; @@ -160,16 +161,15 @@ void WebViewPanel::load_url(wxString& url) m_browser->SetFocus(); } +void WebViewPanel::load_default_url() +{ + assert(!m_default_url.empty()); + load_url(m_default_url); +} + void WebViewPanel::on_show(wxShowEvent& evt) { - // run script with access token to login - if (evt.IsShown()) { - std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); - wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); - // TODO: should this be happening every OnShow? - run_script(script); - } - + } void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) @@ -237,7 +237,6 @@ void WebViewPanel::on_close(wxCloseEvent& evt) void WebViewPanel::on_script_message(wxWebViewEvent& evt) { - wxGetApp().handle_web_request(evt.GetString().ToUTF8().data()); } @@ -411,11 +410,30 @@ SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : SetSizer(sizer); } +ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) + : WebViewPanel(parent, L"https://dev.connect.prusa3d.com/prusa-slicer/printers") +{ +} +void ConnectWebViewPanel::on_show(wxShowEvent& evt) +{ + // run script with access token to login + if (evt.IsShown()) { + std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); + // TODO: should this be happening every OnShow? + run_script(script); + } +} -WebViewDialog::WebViewDialog(wxWindow* parent) +void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + wxGetApp().handle_web_request(evt.GetString().ToUTF8().data()); +} + + +WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url) : wxDialog(parent, wxID_ANY, "Webview Dialog", wxDefaultPosition, wxSize(1366, 768)/* wxSize(100 * wxGetApp().em_unit(), 100 * wxGetApp().em_unit())*/) { - wxString url = "https://dev.connect.prusa3d.com/prusa-slicer/printers"; ////std::string strlang = wxGetApp().app_config->get("language"); ////if (strlang != "") //// url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); @@ -423,8 +441,6 @@ WebViewDialog::WebViewDialog(wxWindow* parent) wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); - - // Create the webview m_browser = WebView::CreateWebView(this, url); if (m_browser == nullptr) { @@ -439,21 +455,8 @@ WebViewDialog::WebViewDialog(wxWindow* parent) Bind(wxEVT_SHOW, &WebViewDialog::on_show, this); Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewDialog::on_script_message, this, m_browser->GetId()); } - WebViewDialog::~WebViewDialog() { - -} - -void WebViewDialog::on_show(wxShowEvent& evt) -{ - if (evt.IsShown()) { - std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); - wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); - // TODO: should this be happening every OnShow? - run_script(script); - } - } void WebViewDialog::run_script(const wxString& javascript) @@ -463,8 +466,25 @@ void WebViewDialog::run_script(const wxString& javascript) bool res = WebView::run_script(m_browser, javascript); } -void WebViewDialog::on_script_message(wxWebViewEvent& evt) + + +PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val) + : WebViewDialog(parent, L"https://dev.connect.prusa3d.com/prusa-slicer/printers") + , m_ret_val(ret_val) { +} +void PrinterPickWebViewDialog::on_show(wxShowEvent& evt) +{ + if (evt.IsShown()) { + std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + wxString script = GUI::format_wxstr("window.setAccessToken(\'%1%\')", token); + // TODO: should this be happening every OnShow? + run_script(script); + } +} +void PrinterPickWebViewDialog::on_script_message(wxWebViewEvent& evt) +{ + m_ret_val = evt.GetString().ToUTF8().data(); this->EndModal(wxID_OK); } diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index f7b6908e52..5b06b363ad 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -29,16 +29,18 @@ namespace Slic3r { namespace GUI { -#define DEBUG_URL_PANEL +//#define DEBUG_URL_PANEL class WebViewPanel : public wxPanel { public: - WebViewPanel(wxWindow *parent); + WebViewPanel(wxWindow *parent, const wxString& default_url); virtual ~WebViewPanel(); void load_url(wxString& url); + void load_default_url(); - void on_show(wxShowEvent& evt); + virtual void on_show(wxShowEvent& evt); + virtual void on_script_message(wxWebViewEvent& evt); void on_idle(wxIdleEvent& evt); void on_url(wxCommandEvent& evt); @@ -47,7 +49,6 @@ public: void on_stop(wxCommandEvent& evt); void on_reload(wxCommandEvent& evt); - void on_script_message(wxWebViewEvent& evt); void on_view_source_request(wxCommandEvent& evt); void on_view_text_request(wxCommandEvent& evt); void on_tools_clicked(wxCommandEvent& evt); @@ -65,7 +66,9 @@ public: wxTimer * m_LoginUpdateTimer{nullptr}; -private: + wxString get_default_url() const { return m_default_url; } + void set_default_url(const wxString& url) { m_default_url = url; } +protected: wxWebView* m_browser; #ifdef DEBUG_URL_PANEL @@ -80,7 +83,6 @@ private: wxMenu* m_tools_menu; wxMenuItem* m_script_custom; - wxInfoBar *m_info; wxStaticText* m_info_text; @@ -90,27 +92,44 @@ private: // Last executed JavaScript snippet, for convenience. wxString m_javascript; wxString m_response_js; - - wxString m_bbl_user_agent; + wxString m_default_url; //DECLARE_EVENT_TABLE() }; +class ConnectWebViewPanel : public WebViewPanel +{ +public: + ConnectWebViewPanel(wxWindow* parent); + void on_show(wxShowEvent& evt) override; + void on_script_message(wxWebViewEvent& evt) override; +}; + class WebViewDialog : public wxDialog { public: - WebViewDialog(wxWindow* parent); + WebViewDialog(wxWindow* parent, const wxString& url); virtual ~WebViewDialog(); - void on_show(wxShowEvent& evt); - void run_script(const wxString& javascript); - void on_script_message(wxWebViewEvent& evt); + virtual void on_show(wxShowEvent& evt) = 0; + virtual void on_script_message(wxWebViewEvent& evt) = 0; -private: + void run_script(const wxString& javascript); + +protected: wxWebView* m_browser; }; +class PrinterPickWebViewDialog : public WebViewDialog +{ +public: + PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val); + void on_show(wxShowEvent& evt) override; + void on_script_message(wxWebViewEvent& evt) override; +private: + std::string& m_ret_val; +}; class SourceViewDialog : public wxDialog { From ba5e8b87a9afd190c92aa8d4990976ce47e2d9d5 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 15 Dec 2023 16:13:08 +0100 Subject: [PATCH 11/64] Fix for using of accelerators from menu items. After TopBar implementation we don't use wxMenuBar anymore. As a result menu accelerators was unused. To workaround this issue the accelerator_entries_cache is generated during creating menu items and than items from this cache are added to the acceleration table for mainframe. --- src/slic3r/GUI/MainFrame.cpp | 34 ++++++++++++++++++++++++--------- src/slic3r/GUI/wxExtensions.cpp | 21 ++++++++++++++++++++ src/slic3r/GUI/wxExtensions.hpp | 5 +++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 693ce063d2..431c79c80e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -20,6 +20,7 @@ #include #include #include +#include //#include #include #include @@ -182,19 +183,34 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S else init_menubar_as_editor(); +#ifndef __APPLE__ + std::vector& entries_cache = accelerator_entries_cache(); + assert(entries_cache.size() + 6 < 100); + wxAcceleratorEntry entries[100]; + + int id = 0; + for (const auto* entry : entries_cache) + entries[id++].Set(entry->GetFlags(), entry->GetKeyCode(), entry->GetMenuItem()->GetId()); + #if _WIN32 // This is needed on Windows to fake the CTRL+# of the window menu when using the numpad - wxAcceleratorEntry entries[6]; - entries[0].Set(wxACCEL_CTRL, WXK_NUMPAD1, wxID_HIGHEST + 1); - entries[1].Set(wxACCEL_CTRL, WXK_NUMPAD2, wxID_HIGHEST + 2); - entries[2].Set(wxACCEL_CTRL, WXK_NUMPAD3, wxID_HIGHEST + 3); - entries[3].Set(wxACCEL_CTRL, WXK_NUMPAD4, wxID_HIGHEST + 4); - entries[4].Set(wxACCEL_CTRL, WXK_NUMPAD5, wxID_HIGHEST + 5); - entries[5].Set(wxACCEL_CTRL, WXK_NUMPAD6, wxID_HIGHEST + 6); - wxAcceleratorTable accel(6, entries); - SetAcceleratorTable(accel); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD1, wxID_HIGHEST + 1); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD2, wxID_HIGHEST + 2); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD3, wxID_HIGHEST + 3); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD4, wxID_HIGHEST + 4); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD5, wxID_HIGHEST + 5); + entries[id++].Set(wxACCEL_CTRL, WXK_NUMPAD6, wxID_HIGHEST + 6); #endif // _WIN32 + wxAcceleratorTable accel(id, entries); + SetAcceleratorTable(accel); + + // clear cache with wxAcceleratorEntry, because it's no need anymore + for (auto entry : entries_cache) + delete entry; + entries_cache.clear(); +#endif + // set default tooltip timer in msec // SetAutoPop supposedly accepts long integers but some bug doesn't allow for larger values // (SetAutoPop is not available on GTK.) diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 157418dd75..1fd5d5ce73 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -9,6 +9,7 @@ #include #include +#include #include @@ -51,6 +52,14 @@ void sys_color_changed_menu(wxMenu* menu) } #endif /* no __linux__ */ +#ifndef __APPLE__ +std::vector& accelerator_entries_cache() +{ + static std::vector entries; + return entries; +} +#endif + void enable_menu_item(wxUpdateUIEvent& evt, std::function const cb_condition, wxMenuItem* item, wxWindow* win) { const bool enable = cb_condition(); @@ -78,8 +87,20 @@ wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const event_handler->Bind(wxEVT_MENU, cb, id); else #endif // __WXMSW__ +#ifndef __APPLE__ + if (parent) + parent->Bind(wxEVT_MENU, cb, id); + else +#endif // n__APPLE__ menu->Bind(wxEVT_MENU, cb, id); +#ifndef __APPLE__ + if (wxAcceleratorEntry* entry = wxAcceleratorEntry::Create(string)) { + entry->SetMenuItem(item); + accelerator_entries_cache().push_back(entry); + } +#endif + if (parent) { parent->Bind(wxEVT_UPDATE_UI, [cb_condition, item, parent](wxUpdateUIEvent& evt) { enable_menu_item(evt, cb_condition, item, parent); }, id); diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 803c0cc656..e4b62bfda2 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -26,6 +26,11 @@ void sys_color_changed_menu(wxMenu* menu); inline void sys_color_changed_menu(wxMenu* /* menu */) {} #endif // no __linux__ +#ifndef __APPLE__ +// Caching wxAcceleratorEntries to use them during mainframe creation +std::vector& accelerator_entries_cache(); +#endif // no __APPLE__ + wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const wxString& description, std::function cb, wxBitmapBundle* icon, wxEvtHandler* event_handler = nullptr, std::function const cb_condition = []() { return true;}, wxWindow* parent = nullptr, int insert_pos = wxNOT_FOUND); From 4263ca62831cce46a0e4a959873b75031b32ebd5 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 18 Dec 2023 09:07:22 +0100 Subject: [PATCH 12/64] TopBar: Fixed a hovering for the bar items --- src/slic3r/GUI/TopBar.cpp | 65 ++++++++++++++++++++++++++++++++++----- src/slic3r/GUI/TopBar.hpp | 3 +- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/TopBar.cpp b/src/slic3r/GUI/TopBar.cpp index 291c70824e..16fcd275ad 100644 --- a/src/slic3r/GUI/TopBar.cpp +++ b/src/slic3r/GUI/TopBar.cpp @@ -34,6 +34,12 @@ TopBarItemsCtrl::Button::Button(wxWindow* parent, const wxString& label, const s } else this->SetMinSize(wxSize(-1, size.y)); + + //button events + Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& event) { set_hovered(true ); event.Skip(); }); + Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& event) { set_hovered(false); event.Skip(); }); + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& event) { set_hovered(true ); event.Skip(); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { set_hovered(false); event.Skip(); }); } void TopBarItemsCtrl::Button::set_selected(bool selected) @@ -61,6 +67,35 @@ void TopBarItemsCtrl::Button::set_selected(bool selected) } } +void TopBarItemsCtrl::Button::set_hovered(bool hovered) +{ + using namespace Slic3r::GUI; + const wxFont& new_font = hovered ? wxGetApp().bold_font() : wxGetApp().normal_font(); + + this->SetFont(new_font); +#ifdef _WIN32 + this->GetParent()->Refresh(); // force redraw a background of the selected mode button +#else + SetForegroundColour(wxSystemSettings::GetColour(set_focus ? wxSYS_COLOUR_BTNTEXT : +#if defined (__linux__) && defined (__WXGTK3__) + wxSYS_COLOUR_GRAYTEXT +#elif defined (__linux__) && defined (__WXGTK2__) + wxSYS_COLOUR_BTNTEXT +#else + wxSYS_COLOUR_BTNSHADOW +#endif + )); +#endif /* no _WIN32 */ + + const wxColour& color = hovered ? wxGetApp().get_color_selected_btn_bg() : + m_is_selected ? wxGetApp().get_label_clr_default() : + wxGetApp().get_window_default_clr(); + this->SetBackgroundColour(color); + + this->Refresh(); + this->Update(); +} + TopBarItemsCtrl::ButtonWithPopup::ButtonWithPopup(wxWindow* parent, const wxString& label, const std::string& icon_name) :TopBarItemsCtrl::Button(parent, label, icon_name, 24) { @@ -80,6 +115,9 @@ void TopBarItemsCtrl::ButtonWithPopup::SetLabel(const wxString& label) static wxString get_workspace_name(Slic3r::ConfigOptionMode mode) { + return mode == Slic3r::ConfigOptionMode::comSimple ? _L("Beginners") : + mode == Slic3r::ConfigOptionMode::comAdvanced ? _L("Regulars") : _L("Josef Prusa"); + return mode == Slic3r::ConfigOptionMode::comSimple ? _L("Beginner workspace") : mode == Slic3r::ConfigOptionMode::comAdvanced ? _L("Regular workspace") : _L("Josef Prusa's workspace"); } @@ -154,15 +192,19 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : m_btn_margin = std::lround(0.9 * em); m_line_margin = std::lround(0.1 * em); - m_sizer = new wxBoxSizer(wxHORIZONTAL); + m_sizer = new wxFlexGridSizer(2); + m_sizer->AddGrowableCol(0); + m_sizer->SetFlexibleDirection(wxHORIZONTAL); this->SetSizer(m_sizer); + wxBoxSizer* left_sizer = new wxBoxSizer(wxHORIZONTAL); + #ifdef __APPLE__ auto logo = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle(wxGetApp().logo_name(), 24)); - m_sizer->Add(logo, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); + left_sizer->Add(logo, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); #else m_menu_btn = new ButtonWithPopup(this, _L("Menu"), wxGetApp().logo_name()); - m_sizer->Add(m_menu_btn, 1, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); + left_sizer->Add(m_menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxALL, m_btn_margin); m_menu_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { m_menu_btn->set_selected(true); @@ -173,14 +215,18 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : #endif m_buttons_sizer = new wxFlexGridSizer(1, m_btn_margin, m_btn_margin); - m_sizer->Add(m_buttons_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 2 * m_btn_margin); + left_sizer->Add(m_buttons_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 2 * m_btn_margin); + + m_sizer->Add(left_sizer, 1, wxEXPAND); + + wxBoxSizer* right_sizer = new wxBoxSizer(wxHORIZONTAL); // create modes menu ApplyWorkspacesMenu(); m_workspace_btn = new ButtonWithPopup(this, _L("Workspace"), "mode_simple"); - m_sizer->AddStretchSpacer(20); - m_sizer->Add(m_workspace_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT); + right_sizer->AddStretchSpacer(20); + right_sizer->Add(m_workspace_btn, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT); m_workspace_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { m_workspace_btn->set_selected(true); @@ -193,7 +239,7 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : CreateAuthMenu(); m_auth_btn = new ButtonWithPopup(this, "user", 35); - m_sizer->Add(m_auth_btn, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT | wxLEFT, m_btn_margin); + right_sizer->Add(m_auth_btn, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxRIGHT | wxLEFT, m_btn_margin); m_auth_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { UpdateAuthMenu(); @@ -203,12 +249,17 @@ TopBarItemsCtrl::TopBarItemsCtrl(wxWindow *parent) : }); m_auth_menu.Bind(wxEVT_MENU_CLOSE, [this](wxMenuEvent&) { m_auth_btn->set_selected(false); }); + m_sizer->Add(right_sizer); + + m_sizer->SetItemMinSize(1, wxSize(42 * wxGetApp().em_unit(), -1)); + this->Bind(wxEVT_PAINT, &TopBarItemsCtrl::OnPaint, this); } void TopBarItemsCtrl::OnPaint(wxPaintEvent&) { wxGetApp().UpdateDarkUI(this); + return; const wxSize sz = GetSize(); wxPaintDC dc(this); diff --git a/src/slic3r/GUI/TopBar.hpp b/src/slic3r/GUI/TopBar.hpp index e330a6d35a..8032e9e022 100644 --- a/src/slic3r/GUI/TopBar.hpp +++ b/src/slic3r/GUI/TopBar.hpp @@ -27,6 +27,7 @@ class TopBarItemsCtrl : public wxControl ~Button() {} void set_selected(bool selected); + void set_hovered (bool hovered); }; class ButtonWithPopup : public Button @@ -79,7 +80,7 @@ public: private: wxWindow* m_parent; wxFlexGridSizer* m_buttons_sizer; - wxBoxSizer* m_sizer; + wxFlexGridSizer* m_sizer; ButtonWithPopup* m_menu_btn {nullptr}; ButtonWithPopup* m_workspace_btn {nullptr}; ButtonWithPopup* m_auth_btn {nullptr}; From e065f0c83b237378181cbd3e28927a0113bda956 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 18 Dec 2023 16:10:40 +0100 Subject: [PATCH 13/64] TopBar: Added "Search" control + Deleted old search controls from tab and canvas3D --- src/slic3r/GUI/GLCanvas3D.cpp | 133 +--------------------------------- src/slic3r/GUI/GLCanvas3D.hpp | 4 - src/slic3r/GUI/GUI_App.cpp | 2 +- src/slic3r/GUI/MainFrame.cpp | 5 +- src/slic3r/GUI/Plater.cpp | 17 ----- src/slic3r/GUI/Plater.hpp | 1 - src/slic3r/GUI/Search.cpp | 89 ++++++++++++++--------- src/slic3r/GUI/Search.hpp | 15 ++-- src/slic3r/GUI/Tab.cpp | 6 -- src/slic3r/GUI/Tab.hpp | 1 - src/slic3r/GUI/TopBar.cpp | 13 ++++ src/slic3r/GUI/TopBar.hpp | 18 ++++- 12 files changed, 97 insertions(+), 207 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 6bbe209f32..0c129803a5 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2858,7 +2858,7 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) int keyCode = evt.GetKeyCode(); int ctrlMask = wxMOD_CONTROL; int shiftMask = wxMOD_SHIFT; - if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu())) + if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_arrange_menu())) return; if (m_gizmos.on_char(evt)) { @@ -2890,14 +2890,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) #endif /* __APPLE__ */ post_event(SimpleEvent(EVT_GLTOOLBAR_COPY)); break; -#ifdef __APPLE__ - case 'f': - case 'F': -#else /* __APPLE__ */ - case WXK_CONTROL_F: -#endif /* __APPLE__ */ - _activate_search_toolbar_item(); - break; #ifdef __APPLE__ case 'm': case 'M': @@ -3354,10 +3346,9 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) } } - // If the Search window or Undo/Redo list is opened, + // If Undo/Redo list is opened, // update them according to the event - if (m_main_toolbar.is_item_pressed("search") || - m_undoredo_toolbar.is_item_pressed("undo") || + if (m_undoredo_toolbar.is_item_pressed("undo") || m_undoredo_toolbar.is_item_pressed("redo")) { m_mouse_wheel = int((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); return; @@ -3664,7 +3655,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_dirty = true; } else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) { - if (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu()) + if (_deactivate_undo_redo_toolbar_items() || _deactivate_arrange_menu()) return; // If user pressed left or right button we first check whether this happened @@ -4765,73 +4756,6 @@ bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) return action_taken; } -// Getter for the const char*[] for the search list -static bool search_string_getter(int idx, const char** label, const char** tooltip) -{ - const Search::OptionsSearcher& search_list = wxGetApp().searcher(); - if (0 <= idx && (size_t)idx < search_list.size()) { - search_list[idx].get_marked_label_and_tooltip(label, tooltip); - return true; - } - return false; -} - -bool GLCanvas3D::_render_search_list(float pos_x) -{ - bool action_taken = false; - ImGuiWrapper* imgui = wxGetApp().imgui(); - - imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); - std::string title = L("Search"); - imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - int selected = -1; - bool edited = false; - float em = static_cast(wxGetApp().em_unit()); -#if ENABLE_RETINA_GL - em *= m_retina_helper->get_scale_factor(); -#endif // ENABLE_RETINA_GL - - // update searcher before show imGui search dialog on the plater, if printer technology or mode was changed - wxGetApp().check_and_update_searcher(wxGetApp().get_mode()); - Search::OptionsSearcher& searcher = wxGetApp().searcher(); - - std::string& search_line = searcher.search_string(); - char *s = new char[255]; - strcpy(s, search_line.empty() ? _u8L("Enter a search term").c_str() : search_line.c_str()); - - imgui->search_list(ImVec2(45 * em, 30 * em), &search_string_getter, s, - wxGetApp().searcher().view_params, - selected, edited, m_mouse_wheel, wxGetApp().is_localized()); - - search_line = s; - delete [] s; - if (search_line == _u8L("Enter a search term")) - search_line.clear(); - - if (edited) - searcher.search(); - - if (selected >= 0) { - // selected == 9999 means that Esc kye was pressed - /*// revert commit https://github.com/prusa3d/PrusaSlicer/commit/91897589928789b261ca0dc735ffd46f2b0b99f2 - if (selected == 9999) - action_taken = true; - else - sidebar.jump_to_option(selected);*/ - if (selected != 9999) { - imgui->end(); // end imgui before the jump to option - wxGetApp().jump_to_option(selected); - return true; - } - action_taken = true; - } - - imgui->end(); - - return action_taken; -} - bool GLCanvas3D::_render_arrange_menu(float pos_x) { m_arrange_settings_dialog.render(pos_x, m_main_toolbar.get_height()); @@ -5395,30 +5319,6 @@ bool GLCanvas3D::_init_main_toolbar() if (!m_main_toolbar.add_item(item)) return false; - /* - if (!m_main_toolbar.add_separator()) - return false; - */ - - item.name = "search"; - item.icon_filename = "search_.svg"; - item.tooltip = _u8L("Search") + " [" + GUI::shortkey_ctrl_prefix() + "F]"; - item.sprite_id = 11; - item.left.toggable = true; - item.left.render_callback = [this](float left, float right, float, float) { - if (m_canvas != nullptr) { - if (!m_canvas->HasFocus()) - m_canvas->SetFocus(); - if (_render_search_list(0.5f * (left + right))) - _deactivate_search_toolbar_item(); - } - }; - item.left.action_callback = GLToolbarItem::Default_Action_Callback; - item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; - item.enabling_callback = [this]()->bool { return m_gizmos.get_current_type() == GLGizmosManager::Undefined; }; - if (!m_main_toolbar.add_item(item)) - return false; - if (!m_main_toolbar.add_separator()) return false; @@ -7653,11 +7553,6 @@ bool GLCanvas3D::_deactivate_undo_redo_toolbar_items() return false; } -bool GLCanvas3D::is_search_pressed() const -{ - return m_main_toolbar.is_item_pressed("search"); -} - bool GLCanvas3D::_deactivate_arrange_menu() { if (m_main_toolbar.is_item_pressed("arrange")) { @@ -7668,26 +7563,6 @@ bool GLCanvas3D::_deactivate_arrange_menu() return false; } -bool GLCanvas3D::_deactivate_search_toolbar_item() -{ - if (is_search_pressed()) { - m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); - return true; - } - - return false; -} - -bool GLCanvas3D::_activate_search_toolbar_item() -{ - if (!m_main_toolbar.is_item_pressed("search")) { - m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this); - return true; - } - - return false; -} - bool GLCanvas3D::_deactivate_collapse_toolbar_items() { GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 0660ca7cc9..5f714aae30 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -780,7 +780,6 @@ public: bool is_layers_editing_enabled() const; bool is_layers_editing_allowed() const; - bool is_search_pressed() const; void reset_layer_height_profile(); void adaptive_layer_height_profile(float quality_factor); @@ -1064,7 +1063,6 @@ private: void _render_sla_slices(); void _render_selection_sidebar_hints(); bool _render_undo_redo_stack(const bool is_undo, float pos_x); - bool _render_search_list(float pos_x); bool _render_arrange_menu(float pos_x); void _render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type); // render thumbnail using an off-screen framebuffer @@ -1113,8 +1111,6 @@ private: void _update_selection_from_hover(); bool _deactivate_undo_redo_toolbar_items(); - bool _deactivate_search_toolbar_item(); - bool _activate_search_toolbar_item(); bool _deactivate_collapse_toolbar_items(); bool _deactivate_arrange_menu(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7b87097c3b..7a71ca9d5c 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1829,7 +1829,7 @@ bool GUI_App::suppress_round_corners() const wxSize GUI_App::get_min_size(wxWindow* display_win) const { - wxSize min_size(76*m_em_unit, 49 * m_em_unit); + wxSize min_size(120 * m_em_unit, 49 * m_em_unit); const wxDisplay display = wxDisplay(display_win); wxRect display_rect = display.GetGeometry(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 431c79c80e..8aa32177d3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1516,7 +1516,7 @@ void MainFrame::init_menubar_as_editor() editMenu->AppendSeparator(); append_menu_item(editMenu, wxID_ANY, _L("Searc&h") + "\tCtrl+F", - _L("Search in settings"), [this](wxCommandEvent&) { m_plater->IsShown() ? m_plater->search() : wxGetApp().show_search_dialog(); }, + _L("Search in settings"), [this](wxCommandEvent&) { wxGetApp().show_search_dialog(); }, "search", nullptr, []() {return true; }, this); } @@ -2055,9 +2055,6 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) if (tab==0) { if (m_settings_dialog.IsShown()) this->SetFocus(); - // plater should be focused for correct navigation inside search window - if (m_plater->canvas3D()->is_search_pressed()) - m_plater->SetFocus(); return; } // Show/Activate Settings Dialog diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7f088f8c14..5e9afdc627 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6552,23 +6552,6 @@ void Plater::paste_from_clipboard() p->view3D->get_canvas3d()->get_selection().paste_from_clipboard(); } -void Plater::search() -{ - if (is_preview_shown()) - return; - // plater should be focused for correct navigation inside search window - this->SetFocus(); - - wxKeyEvent evt; -#ifdef __APPLE__ - evt.m_keyCode = 'f'; -#else /* __APPLE__ */ - evt.m_keyCode = WXK_CONTROL_F; -#endif /* __APPLE__ */ - evt.SetControlDown(true); - canvas3D()->on_char(evt); -} - void Plater::msw_rescale() { p->preview->msw_rescale(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 80f07e4f40..915948f580 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -284,7 +284,6 @@ public: void copy_selection_to_clipboard(); void paste_from_clipboard(); - void search(); void mirror(Axis axis); void split_object(); void split_volume(); diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 4fcd2d0f9d..5792316a42 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -306,6 +306,7 @@ bool OptionsSearcher::search(const std::string& search, bool force/* = false*/) OptionsSearcher::OptionsSearcher() { + default_string = _L("Enter a search term"); } OptionsSearcher::~OptionsSearcher() @@ -423,18 +424,22 @@ Option OptionsSearcher::get_option(const std::string& opt_key, const wxString& l void OptionsSearcher::show_dialog() { - if (!search_dialog) { + if (!search_dialog) search_dialog = new SearchDialog(this); - auto parent = search_dialog->GetParent(); - wxPoint pos = parent->ClientToScreen(wxPoint(0, 0)); - pos.x += em_unit(parent) * 40; - pos.y += em_unit(parent) * 4; + wxSize srch_sz = search_input->GetSize(); + if (search_dialog->GetPosition().x != search_input->GetPosition().x) + search_dialog->SetPosition(search_input->GetScreenPosition() + wxPoint(0, srch_sz.y)); - search_dialog->SetPosition(pos); - } + wxSize dlg_sz = search_dialog->GetSize(); + if (dlg_sz.x < srch_sz.x) + search_dialog->SetSize(wxSize(srch_sz.x, dlg_sz.y)); + + search_string(); + search_input->SetSelection(-1,-1); search_dialog->Popup(); + search_input->SetFocus(); } void OptionsSearcher::dlg_sys_color_changed() @@ -449,6 +454,42 @@ void OptionsSearcher::dlg_msw_rescale() search_dialog->msw_rescale(); } +void OptionsSearcher::set_search_input(TextInput* input_ctrl) +{ + search_input = input_ctrl; + + search_input->Bind(wxEVT_TEXT, [this](wxEvent& e) + { + if (search_dialog && search_dialog->IsShown()) + search_dialog->input_text(search_input->GetValue()); + else { + GUI::wxGetApp().check_and_update_searcher(GUI::wxGetApp().get_mode()); + show_dialog(); + } + }); + + wxTextCtrl* ctrl = search_input->GetTextCtrl(); + ctrl->SetToolTip(GUI::format_wxstr(_L("Search in settings [%1%]"), "Ctrl+F")); + + ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) + { + if (e.GetKeyCode() == WXK_TAB) + search_input->Navigate(e.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward); + else if (e.GetKeyCode() == WXK_ESCAPE) + search_dialog->EndModal(wxID_CLOSE); + else if (search_dialog) + search_dialog->KeyDown(e); + e.Skip(); + }); + + ctrl->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& event) + { + if (search_input->GetValue() == default_string) + search_input->SetValue(""); + event.Skip(); + }); +} + void OptionsSearcher::add_key(const std::string& opt_key, Preset::Type type, const wxString& group, const wxString& category) { groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category}; @@ -469,7 +510,7 @@ static const std::map icon_idxs = { }; SearchDialog::SearchDialog(OptionsSearcher* searcher) - : GUI::DPIDialog(GUI::wxGetApp().tab_panel(), wxID_ANY, _L("Search"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + : GUI::DPIDialog(GUI::wxGetApp().tab_panel(), wxID_ANY, _L("Search"), wxDefaultPosition, wxDefaultSize, wxSTAY_ON_TOP | wxRESIZE_BORDER), searcher(searcher) { SetFont(GUI::wxGetApp().normal_font()); @@ -479,13 +520,9 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); #endif - default_string = _L("Enter a search term"); int border = 10; int em = em_unit(); - search_line = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); - GUI::wxGetApp().UpdateDarkUI(search_line); - search_list = new wxDataViewCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(em * 40, em * 30), wxDV_NO_HEADER | wxDV_SINGLE #ifdef _WIN32 | wxBORDER_SIMPLE @@ -531,15 +568,9 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); - topSizer->Add(search_line, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); topSizer->Add(search_list, 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); topSizer->Add(check_sizer, 0, wxEXPAND | wxALL, border); - search_line->Bind(wxEVT_TEXT, &SearchDialog::OnInputText, this); - search_line->Bind(wxEVT_LEFT_UP, &SearchDialog::OnLeftUpInTextCtrl, this); - // process wxEVT_KEY_DOWN to navigate inside search_list, if ArrowUp/Down was pressed - search_line->Bind(wxEVT_KEY_DOWN,&SearchDialog::OnKeyDown, this); - search_list->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, &SearchDialog::OnSelect, this); search_list->Bind(wxEVT_DATAVIEW_ITEM_ACTIVATED, &SearchDialog::OnActivate, this); #ifdef __WXMSW__ @@ -573,11 +604,6 @@ SearchDialog::~SearchDialog() void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/) { - const std::string& line = searcher->search_string(); - search_line->SetValue(line.empty() ? default_string : from_u8(line)); - search_line->SetFocus(); - search_line->SelectAll(); - update_list(); const OptionViewParameters& params = searcher->view_params; @@ -587,7 +613,7 @@ void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/) if (position != wxDefaultPosition) this->SetPosition(position); - this->ShowModal(); + this->Show(); } void SearchDialog::ProcessSelection(wxDataViewItem selection) @@ -606,10 +632,9 @@ void SearchDialog::ProcessSelection(wxDataViewItem selection) wxPostEvent(GUI::wxGetApp().mainframe, event); } -void SearchDialog::OnInputText(wxCommandEvent&) +void SearchDialog::input_text(wxString input_string) { - wxString input_string = search_line->GetValue(); - if (input_string == default_string) + if (input_string == searcher->default_string) input_string.Clear(); searcher->search(into_u8(input_string)); @@ -617,14 +642,6 @@ void SearchDialog::OnInputText(wxCommandEvent&) update_list(); } -void SearchDialog::OnLeftUpInTextCtrl(wxEvent& event) -{ - if (search_line->GetValue() == default_string) - search_line->SetValue(""); - - event.Skip(); -} - void SearchDialog::OnKeyDown(wxKeyEvent& event) { int key = event.GetKeyCode(); @@ -749,7 +766,7 @@ void SearchDialog::on_sys_color_changed() #ifdef _WIN32 GUI::wxGetApp().UpdateAllStaticTextDarkUI(this); GUI::wxGetApp().UpdateDarkUI(static_cast(this->FindWindowById(wxID_CANCEL, this)), true); - for (wxWindow* win : std::vector {search_line, search_list, check_category, check_english}) + for (wxWindow* win : std::vector {search_list, check_category, check_english}) if (win) GUI::wxGetApp().UpdateDarkUI(win); #endif diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 571448d492..cc018ab2e5 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -27,6 +27,7 @@ #include "Widgets/CheckBox.hpp" class CheckBox; +class TextInput; namespace Slic3r { @@ -91,6 +92,8 @@ class OptionsSearcher std::map groups_and_categories; PrinterTechnology printer_technology {ptAny}; ConfigOptionMode mode{ comUndef }; + TextInput* search_input { nullptr }; + SearchDialog* search_dialog { nullptr }; std::vector