From 671b53771c630c85de784dc3095429d41cf9a0e1 Mon Sep 17 00:00:00 2001 From: Sebastian Balzer Date: Fri, 1 May 2026 18:50:50 +0200 Subject: [PATCH 1/2] feat(gui): add configurable print status window Add a new floating PrintStatusFrame widget and wire it into the main window lifecycle. The widget can be opened manually, auto-shown when the main window is minimized, and kept as a single reusable instance. On Windows and Linux, closing the main window can optionally minimize Bambu Studio to the system tray while explicit quit paths still perform a real shutdown. Implement a compact print status dashboard with local printer selection, status badges, progress, remaining time, ETA, layer information, warnings, and live nozzle, bed, and chamber temperatures. The frame stores only the selected printer dev_id and resolves MachineObject instances on demand, avoiding long-lived raw printer pointers while handling missing, offline, idle, printing, paused, and finished states robustly. Add Print Status Window preferences and defaults for enablement, auto-show on minimize, close-to-tray, always-on-top, remembered position, theme, and opacity. Keep the widget lightweight and self-contained with its own timer-based refresh, fixed-size compact layout, local theme handling, deferred safe show on minimize, build integration, and i18n extraction for the new UI strings. --- bbl/i18n/list.txt | 3 +- src/libslic3r/AppConfig.cpp | 43 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/GUI_App.cpp | 6 +- src/slic3r/GUI/MainFrame.cpp | 203 ++++- src/slic3r/GUI/MainFrame.hpp | 21 +- src/slic3r/GUI/Preferences.cpp | 34 +- src/slic3r/GUI/PrintStatusFrame.cpp | 1193 +++++++++++++++++++++++++++ src/slic3r/GUI/PrintStatusFrame.hpp | 171 ++++ 9 files changed, 1665 insertions(+), 11 deletions(-) create mode 100644 src/slic3r/GUI/PrintStatusFrame.cpp create mode 100644 src/slic3r/GUI/PrintStatusFrame.hpp diff --git a/bbl/i18n/list.txt b/bbl/i18n/list.txt index d56545aef8..1c153da5c0 100644 --- a/bbl/i18n/list.txt +++ b/bbl/i18n/list.txt @@ -168,6 +168,7 @@ src/slic3r/GUI/ObjectDataViewModel.cpp src/slic3r/GUI/OpenGLManager.cpp src/slic3r/GUI/OptionsGroup.cpp src/slic3r/GUI/PrintOptionsDialog.cpp +src/slic3r/GUI/PrintStatusFrame.cpp src/slic3r/GUI/SafetyOptionsDialog.cpp src/slic3r/GUI/ParamsPanel.cpp src/slic3r/GUI/PartPlate.cpp @@ -287,4 +288,4 @@ src/slic3r/GUI/UxProgramTermsDialog.hpp src/slic3r/GUI/HelioHistoryDialog.hpp src/slic3r/GUI/HelioHistoryDialog.cpp src/slic3r/GUI/MixedFilamentDialog.cpp -src/slic3r/GUI/MixedFilamentDialog.hpp \ No newline at end of file +src/slic3r/GUI/MixedFilamentDialog.hpp diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index cfd11bf26c..840848efb3 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -340,6 +340,49 @@ void AppConfig::set_defaults() set_bool("show_print_history", true); } + if (get("print_status_window_enabled").empty()) + set_bool("print_status_window_enabled", false); + if (get("print_status_window_auto_show_on_minimize").empty()) + set_bool("print_status_window_auto_show_on_minimize", false); + if (get("print_status_window_close_to_tray").empty()) + set_bool("print_status_window_close_to_tray", false); + if (get("print_status_window_always_on_top").empty()) + set_bool("print_status_window_always_on_top", false); + if (get("print_status_window_remember_position").empty()) + set_bool("print_status_window_remember_position", true); + if (get("print_status_window_show_printer_selector").empty()) + set_bool("print_status_window_show_printer_selector", true); + if (get("print_status_window_show_printer_name").empty()) + set_bool("print_status_window_show_printer_name", true); + if (get("print_status_window_show_stage").empty()) + set_bool("print_status_window_show_stage", true); + if (get("print_status_window_show_job_name").empty()) + set_bool("print_status_window_show_job_name", true); + if (get("print_status_window_show_progress").empty()) + set_bool("print_status_window_show_progress", true); + if (get("print_status_window_show_remaining_time").empty()) + set_bool("print_status_window_show_remaining_time", true); + if (get("print_status_window_show_layers").empty()) + set_bool("print_status_window_show_layers", true); + if (get("print_status_window_show_nozzle_temp").empty()) + set_bool("print_status_window_show_nozzle_temp", true); + if (get("print_status_window_show_bed_temp").empty()) + set_bool("print_status_window_show_bed_temp", true); + if (get("print_status_window_show_warnings").empty()) + set_bool("print_status_window_show_warnings", true); + if (get("print_status_window_theme").empty()) + set("print_status_window_theme", "follow_app"); + if (get("print_status_window_opacity").empty()) + set("print_status_window_opacity", "100"); + if (get("print_status_window_pos_x").empty()) + set("print_status_window_pos_x", ""); + if (get("print_status_window_pos_y").empty()) + set("print_status_window_pos_y", ""); + if (get("print_status_window_width").empty()) + set("print_status_window_width", ""); + if (get("print_status_window_height").empty()) + set("print_status_window_height", ""); + if (get("show_printable_box").empty()) { set_bool("show_printable_box", true); } diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 25a4313212..016fb90081 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -196,6 +196,8 @@ set(SLIC3R_GUI_SOURCES GUI/ImageDPIFrame.hpp GUI/Preferences.cpp GUI/Preferences.hpp + GUI/PrintStatusFrame.cpp + GUI/PrintStatusFrame.hpp GUI/AMSSetting.cpp GUI/AMSSetting.hpp GUI/AMSDryControl.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 729d8784d3..65fe3ab17d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2908,10 +2908,12 @@ bool GUI_App::on_init_inner() wxGetApp().Bind(wxEVT_QUERY_END_SESSION, [this](auto & e) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< "received wxEVT_QUERY_END_SESSION"; if (mainframe) { + mainframe->set_real_shutdown_requested(true); wxCloseEvent e2(wxEVT_CLOSE_WINDOW); e2.SetCanVeto(true); mainframe->GetEventHandler()->ProcessEvent(e2); if (e2.GetVeto()) { + mainframe->set_real_shutdown_requested(false); e.Veto(); return; } @@ -3193,10 +3195,10 @@ bool GUI_App::on_init_inner() wxLaunchDefaultBrowser(download_url); break; case wxID_NO: - wxGetApp().mainframe->Close(true); + wxGetApp().mainframe->request_app_exit(true); break; default: - wxGetApp().mainframe->Close(true); + wxGetApp().mainframe->request_app_exit(true); } }); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 1a37b30945..909b77aa6b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -8,6 +8,9 @@ #include #include #include +#ifndef __APPLE__ +#include +#endif #include //#include #include @@ -42,6 +45,7 @@ // BBS #include "PartPlate.hpp" #include "Preferences.hpp" +#include "PrintStatusFrame.hpp" #include "Widgets/ProgressDialog.hpp" #include "BindDialog.hpp" #include "../Utils/MacDarkMode.hpp" @@ -135,6 +139,34 @@ class BambuStudioTaskBarIcon : public wxTaskBarIcon };*/ #endif // __APPLE__ +// Generic Windows/Linux tray icon used only for close-to-tray. +#ifndef __APPLE__ +class BambuStudioCloseToTrayIcon : public wxTaskBarIcon +{ +public: + explicit BambuStudioCloseToTrayIcon(MainFrame* frame) : m_frame(frame) {} + + wxMenu* CreatePopupMenu() override + { + auto* menu = new wxMenu; + if (m_frame == nullptr) + return menu; + + append_menu_item(menu, wxID_ANY, _L("Show Bambu Studio"), _L("Show Bambu Studio"), + [this](wxCommandEvent&) { if (m_frame) m_frame->restore_from_tray(); }, "", this); + append_menu_item(menu, wxID_ANY, _L("Show Print Status Window"), _L("Show Print Status Window"), + [this](wxCommandEvent&) { if (m_frame) m_frame->show_print_status_frame(); }, "", this); + menu->AppendSeparator(); + append_menu_item(menu, wxID_EXIT, _L("Quit"), _L("Quit"), + [this](wxCommandEvent&) { if (m_frame) m_frame->request_app_exit(false); }, "", this); + return menu; + } + +private: + MainFrame* m_frame { nullptr }; +}; +#endif + // Load the icon either from the exe, or from the ico file. static wxIcon main_frame_icon(GUI_App::EAppMode app_mode) { @@ -460,10 +492,18 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ // declare events Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": mainframe received close_widow event"; + if (should_close_to_tray(event) && ensure_close_to_tray_icon()) { + minimize_to_tray(); + event.Veto(); + return; + } + if (event.CanVeto() && m_plater->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) { // prevents to open the save dirty project dialog event.Veto(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< "cancelled by gizmo in editing"; + if (m_real_shutdown_requested) + m_real_shutdown_requested = false; return; } @@ -483,10 +523,14 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (event.CanVeto() && ((result = m_plater->close_with_confirm(check)) == wxID_CANCEL)) { event.Veto(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< "cancelled by close_with_confirm selection"; + if (m_real_shutdown_requested) + m_real_shutdown_requested = false; return; } if (event.CanVeto() && !wxGetApp().check_print_host_queue()) { event.Veto(); + if (m_real_shutdown_requested) + m_real_shutdown_requested = false; return; } @@ -588,6 +632,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ MarkdownTip::ExitTip(); m_plater->reset(); + remove_close_to_tray_icon(); this->shutdown(); // propagate event @@ -596,6 +641,27 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": mainframe finished process close_widow event"; }); + Bind(wxEVT_ICONIZE, [this](wxIconizeEvent& event) { + if (!event.IsIconized()) { + event.Skip(); + return; + } + + if (m_real_shutdown_requested || IsBeingDeleted() || wxGetApp().app_config == nullptr) { + event.Skip(); + return; + } + + if (wxGetApp().app_config->get("print_status_window_enabled") != "true" || + wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { + event.Skip(); + return; + } + + CallAfter([this]() { show_print_status_frame_safe_on_minimize(); }); + event.Skip(); + }); + //FIXME it seems this method is not called on application start-up, at least not on Windows. Why? // The same applies to wxEVT_CREATE, it is not being called on startup on Windows. Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) { @@ -655,7 +721,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ this->Iconize(); return; } - if (evt.CmdDown() && evt.GetKeyCode() == 'Q') { wxPostEvent(this, wxCloseEvent(wxEVT_CLOSE_WINDOW)); return;} + if (evt.CmdDown() && evt.GetKeyCode() == 'Q') { request_app_exit(false); return;} if (evt.CmdDown() && evt.RawControlDown() && evt.GetKeyCode() == 'F') { EnableFullScreenView(true); if (IsFullScreen()) { @@ -748,6 +814,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ wxGetApp().persist_window_geometry(&m_settings_dialog, true); } +MainFrame::~MainFrame() = default; + #ifdef __WIN32__ // Orca: Fix maximized window overlaps taskbar when taskbar auto hide is enabled (#8085) // Adopted from https://gist.github.com/MortenChristiansen/6463580 @@ -872,6 +940,118 @@ void MainFrame::show_log_window() m_log_window->Show(); } +void MainFrame::request_app_exit(bool force) +{ + m_real_shutdown_requested = true; + if (!Close(force)) + m_real_shutdown_requested = false; +} + +#ifndef __APPLE__ +bool MainFrame::should_close_to_tray(const wxCloseEvent& event) const +{ + return event.CanVeto() && + !m_real_shutdown_requested && + !wxGetApp().is_gcode_viewer() && + IsShown() && + wxGetApp().app_config != nullptr && + wxGetApp().app_config->get("print_status_window_close_to_tray") == "true"; +} + +bool MainFrame::ensure_close_to_tray_icon() +{ + if (m_close_to_tray_icon) + return true; + + auto tray_icon = std::make_unique(this); + wxIcon icon = main_frame_icon(wxGetApp().get_app_mode()); + if (!icon.IsOk()) + icon = wxIcon(Slic3r::var("BambuStudio.ico"), wxBITMAP_TYPE_ICO); + if (!icon.IsOk()) + return false; + if (!tray_icon->SetIcon(icon, "BambuStudio")) + return false; + + tray_icon->Bind(wxEVT_TASKBAR_LEFT_DCLICK, [this](wxTaskBarIconEvent&) { restore_from_tray(); }); + m_close_to_tray_icon = std::move(tray_icon); + return true; +} + +void MainFrame::minimize_to_tray() +{ + if (wxGetApp().app_config && + wxGetApp().app_config->get("print_status_window_enabled") == "true" && + wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") == "true") { + show_print_status_frame(true); + } + + if (m_settings_dialog.IsShown()) + m_settings_dialog.Hide(); + + Iconize(false); + Show(false); +} + +void MainFrame::restore_from_tray() +{ + Show(true); + Iconize(false); + Raise(); + remove_close_to_tray_icon(); +} + +void MainFrame::remove_close_to_tray_icon() +{ + if (!m_close_to_tray_icon) + return; + + m_close_to_tray_icon->RemoveIcon(); + m_close_to_tray_icon.reset(); +} +#else +bool MainFrame::should_close_to_tray(const wxCloseEvent& /*event*/) const { return false; } +bool MainFrame::ensure_close_to_tray_icon() { return false; } +void MainFrame::minimize_to_tray() {} +void MainFrame::restore_from_tray() {} +void MainFrame::remove_close_to_tray_icon() {} +#endif + +void MainFrame::show_print_status_frame(bool respect_enabled) +{ + if (respect_enabled && (!wxGetApp().app_config || wxGetApp().app_config->get("print_status_window_enabled") != "true")) + return; + + if (!m_print_status_frame) + m_print_status_frame = std::make_unique(this); + m_print_status_frame->show_window(); +} + +void MainFrame::show_print_status_frame_safe_on_minimize() +{ + if (m_real_shutdown_requested || IsBeingDeleted() || wxGetApp().app_config == nullptr) + return; + + if (wxGetApp().app_config->get("print_status_window_enabled") != "true" || + wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { + return; + } + + if (!m_print_status_frame) + m_print_status_frame = std::make_unique(this); + + if (m_print_status_frame) + m_print_status_frame->show_window_safe_on_minimize(); +} + +void MainFrame::destroy_print_status_frame() +{ + if (!m_print_status_frame) + return; + + m_print_status_frame->destroy_for_shutdown(); + m_print_status_frame.release(); +} + //BBS GUI refactor: remove unused layout new/dlg void MainFrame::update_layout() { @@ -1004,6 +1184,7 @@ void MainFrame::update_layout() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + m_real_shutdown_requested = true; // BBS: backup Slic3r::set_backup_callback(nullptr); #ifdef _WIN32 @@ -1034,6 +1215,8 @@ void MainFrame::shutdown() // Avoid the Paint messages by hiding the main window. // Also the application closes much faster without these unnecessary screen refreshes. // In addition, there were some crashes due to the Paint events sent to already destructed windows. + remove_close_to_tray_icon(); + destroy_print_status_frame(); this->Show(false); if (m_settings_dialog.IsShown()) @@ -2584,6 +2767,9 @@ void MainFrame::on_sys_color_changed() WebView::RecreateAll(); + if (m_print_status_frame) + m_print_status_frame->refresh_from_preferences(); + this->Refresh(); } @@ -2888,10 +3074,10 @@ void MainFrame::init_menubar_as_editor() #ifndef __APPLE__ append_menu_item(fileMenu, wxID_EXIT, _L("Quit"), wxString::Format(_L("Quit")), - [this](wxCommandEvent&) { Close(false); }, "menu_exit", nullptr); + [this](wxCommandEvent&) { request_app_exit(false); }, "menu_exit", nullptr); #else append_menu_item(fileMenu, wxID_EXIT, _L("Quit"), wxString::Format(_L("Quit")), - [this](wxCommandEvent&) { Close(false); }, "", nullptr); + [this](wxCommandEvent&) { request_app_exit(false); }, "", nullptr); #endif } @@ -3139,6 +3325,9 @@ void MainFrame::init_menubar_as_editor() [this](wxCommandEvent &) { m_plater->reset_window_layout(); }, "", this, [this]() { return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) && m_plater->is_sidebar_enabled(); }, this); + append_menu_item( + viewMenu, wxID_ANY, _L("Show Print Status Window"), _L("Show Print Status Window"), + [this](wxCommandEvent &) { show_print_status_frame(); }, "", this); viewMenu->AppendSeparator(); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Labels by Layer") + "\t" + ctrl + "E", _L("Show Labels of printing by layer in 3D scene"), [this](wxCommandEvent&) { m_plater->show_view3D_layer_labels(!m_plater->are_view3D_layer_labels_shown()); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this, @@ -3608,7 +3797,7 @@ void MainFrame::init_menubar_as_editor() wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); if (apple_menu != nullptr) { apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent &) { - Close(); + request_app_exit(false); }, wxID_EXIT); } #endif // __APPLE__ @@ -3685,7 +3874,7 @@ void MainFrame::init_menubar_as_gcodeviewer() []() {return true; }, this); fileMenu->AppendSeparator(); append_menu_item(fileMenu, wxID_EXIT, _L("&Quit"), wxString::Format(_L("Quit %s"), SLIC3R_APP_NAME), - [this](wxCommandEvent&) { Close(false); }); + [this](wxCommandEvent&) { request_app_exit(false); }); } // View menu @@ -3712,7 +3901,7 @@ void MainFrame::init_menubar_as_gcodeviewer() wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); if (apple_menu != nullptr) { apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent&) { - Close(); + request_app_exit(false); }, wxID_EXIT); } #endif // __APPLE__ @@ -4364,6 +4553,8 @@ void MainFrame::update_ui_from_settings() m_plater->update_ui_from_settings(); for (auto tab: wxGetApp().tabs_list) tab->update_ui_from_settings(); + if (m_print_status_frame) + m_print_status_frame->refresh_from_preferences(); } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index bb91a740ae..f368cecd0c 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -15,6 +15,7 @@ #include #include +#include #include "GUI_Utils.hpp" #include "Event.hpp" @@ -39,6 +40,7 @@ class Notebook; class wxBookCtrlBase; class wxProgressDialog; +class wxTaskBarIcon; namespace Slic3r { @@ -52,6 +54,7 @@ class MainFrame; class ParamsDialog; class FilamentGroupPopup; class DeviceWebPage; +class PrintStatusFrame; enum QuickSlice { @@ -111,6 +114,7 @@ class MainFrame : public DPIFrame #endif wxMenuItem* m_menu_item_reslice_now { nullptr }; wxSizer* m_main_sizer{ nullptr }; + std::unique_ptr m_print_status_frame; size_t m_last_selected_tab; @@ -206,7 +210,7 @@ class MainFrame : public DPIFrame public: MainFrame(); - ~MainFrame() = default; + ~MainFrame() override; #ifdef __APPLE__ bool get_mac_full_screen() { return m_mac_fullscreen; } #endif @@ -293,6 +297,11 @@ class MainFrame : public DPIFrame #endif //BBS void show_log_window(); + void request_app_exit(bool force = false); + void set_real_shutdown_requested(bool requested = true) { m_real_shutdown_requested = requested; } + void show_print_status_frame(bool respect_enabled = false); + void show_print_status_frame_safe_on_minimize(); + void destroy_print_status_frame(); void update_ui_from_settings(); //BBS @@ -420,10 +429,20 @@ class MainFrame : public DPIFrame bool get_enable_slice_status(); bool get_enable_print_status(); //BBS + bool should_close_to_tray(const wxCloseEvent& event) const; + bool ensure_close_to_tray_icon(); + void minimize_to_tray(); + void restore_from_tray(); + void remove_close_to_tray_icon(); void update_side_button_style(); void update_slice_print_status(SlicePrintEventType event, bool can_slice = true, bool can_print = true); int select_device_page_count{ 0 }; + bool m_real_shutdown_requested{ false }; + +#ifndef __APPLE__ + std::unique_ptr m_close_to_tray_icon; +#endif #ifdef __APPLE__ std::unique_ptr m_taskbar_icon; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 2e2ee530ba..fe1403f07d 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -14,6 +14,7 @@ #include "wx/graphics.h" #include +#include #include #include "Gizmos/GLGizmoBase.hpp" #include "OpenGLManager.hpp" @@ -112,6 +113,8 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxWindow *pa if (callback) { callback(e.GetSelection()); } + if (boost::starts_with(param, "print_status_window_")) + wxGetApp().update_ui_from_settings(); e.Skip(); }); return m_sizer_combox; @@ -497,7 +500,7 @@ wxBoxSizer *PreferencesDialog::create_item_range_input( wxString title, wxWindow *parent, wxString tooltip, std::string param, float range_min, float range_max, int keep_digital, std::function onchange) { wxBoxSizer *sizer_input = new wxBoxSizer(wxHORIZONTAL); - auto input_title = new wxStaticText(parent, wxID_ANY, title); + auto input_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, 0); input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); input_title->SetFont(::Label::Body_13); input_title->SetToolTip(tooltip); @@ -533,6 +536,8 @@ wxBoxSizer *PreferencesDialog::create_item_range_input( if (onchange) { onchange(str); } + if (boost::starts_with(param, "print_status_window_")) + wxGetApp().update_ui_from_settings(); input->GetTextCtrl()->SetValue(str); }; input->GetTextCtrl()->Bind(wxEVT_TEXT_ENTER, [this, set_value_to_app, input](wxCommandEvent &e) { @@ -852,6 +857,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa app_config->save(); } + if (boost::starts_with(param, "print_status_window_")) + wxGetApp().update_ui_from_settings(); + if (param == "staff_pick_switch") { bool pbool = app_config->get("staff_pick_switch") == "true"; wxGetApp().switch_staff_pick(pbool); @@ -1444,6 +1452,19 @@ wxWindow* PreferencesDialog::create_general_page() auto title_media = create_item_title(_L("Media"), page, _L("Media")); auto item_auto_stop_liveview = create_item_checkbox(_L("Keep liveview when printing."), page, _L("By default, Liveview will pause after 15 minutes of inactivity on the computer. Check this box to disable this feature during printing."), 50, "auto_stop_liveview"); + auto title_print_status_window = create_item_title(_L("Print Status Window"), page, _L("Print Status Window")); + auto item_print_status_window_enabled = create_item_checkbox(_L("Enable print status window"), page, _L("Enable the floating print status window feature."), 50, "print_status_window_enabled"); + auto item_print_status_window_auto_show = create_item_checkbox(_L("Auto show on minimize"), page, _L("Automatically show the print status window when the main window is minimized."), 50, "print_status_window_auto_show_on_minimize"); +#ifndef __APPLE__ + auto item_print_status_window_close_to_tray = create_item_checkbox(_L("Minimize to system tray when closing the main window"), page, _L("When enabled, closing the main window with the window close button keeps Bambu Studio running in the system tray."), 50, "print_status_window_close_to_tray"); +#endif + auto item_print_status_window_always_on_top = create_item_checkbox(_L("Always on top"), page, _L("Keep the print status window above other windows."), 50, "print_status_window_always_on_top"); + auto item_print_status_window_remember_position = create_item_checkbox(_L("Remember window position"), page, _L("Restore the print status window position on the next launch."), 50, "print_status_window_remember_position"); + std::vector print_status_window_theme_labels = { _L("Follow app"), _L("Light"), _L("Dark") }; + std::vector print_status_window_theme_values = { "follow_app", "light", "dark" }; + auto item_print_status_window_theme = create_item_combobox(_L("Theme"), page, _L("Theme for the print status window."), "print_status_window_theme", print_status_window_theme_labels, print_status_window_theme_values); + auto item_print_status_window_opacity = create_item_range_input(_L("Opacity"), page, _L("Set the opacity of the print status window. Value range:[40,100]"), "print_status_window_opacity", 40.0f, 100.0f, 0); + //dark mode #ifdef _WIN32 auto title_darkmode = create_item_title(_L("Dark Mode"), page, _L("Dark Mode")); @@ -1552,6 +1573,17 @@ wxWindow* PreferencesDialog::create_general_page() sizer_page->Add(title_media, 0, wxTOP| wxEXPAND, FromDIP(20)); sizer_page->Add(item_auto_stop_liveview, 0, wxEXPAND, FromDIP(3)); + sizer_page->Add(title_print_status_window, 0, wxTOP | wxEXPAND, FromDIP(20)); + sizer_page->Add(item_print_status_window_enabled, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_print_status_window_auto_show, 0, wxTOP, FromDIP(3)); +#ifndef __APPLE__ + sizer_page->Add(item_print_status_window_close_to_tray, 0, wxTOP, FromDIP(3)); +#endif + sizer_page->Add(item_print_status_window_always_on_top, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_print_status_window_remember_position, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_print_status_window_theme, 0, wxTOP, FromDIP(3)); + sizer_page->Add(item_print_status_window_opacity, 0, wxTOP, FromDIP(3)); + #ifdef _WIN32 sizer_page->Add(title_darkmode, 0, wxTOP | wxEXPAND, FromDIP(20)); sizer_page->Add(item_darkmode, 0, wxEXPAND, FromDIP(3)); diff --git a/src/slic3r/GUI/PrintStatusFrame.cpp b/src/slic3r/GUI/PrintStatusFrame.cpp new file mode 100644 index 0000000000..7a9bcd672d --- /dev/null +++ b/src/slic3r/GUI/PrintStatusFrame.cpp @@ -0,0 +1,1193 @@ +#include "PrintStatusFrame.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DeviceManager.hpp" +#include "GUI_App.hpp" +#include "HMS.hpp" +#include "I18N.hpp" +#include "MainFrame.hpp" +#include "DeviceCore/DevBed.h" +#include "DeviceCore/DevChamber.h" +#include "DeviceCore/DevExtruderSystem.h" +#include "DeviceCore/DevHMS.h" +#include "DeviceCore/DevManager.h" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/Utils.hpp" + +#ifdef _WIN32 +#include +#endif + +namespace Slic3r { +namespace GUI { + +class ProgressBarPanel final : public wxPanel +{ +public: + explicit ProgressBarPanel(wxWindow* parent) + : wxPanel(parent, wxID_ANY) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(-1, FromDIP(10))); + Bind(wxEVT_PAINT, &ProgressBarPanel::on_paint, this); + Bind(wxEVT_SIZE, &ProgressBarPanel::on_size, this); + } + + void SetValue(int value) + { + value = std::clamp(value, 0, 100); + if (m_value == value) + return; + + m_value = value; + Refresh(); + } + + void SetColors(const wxColour& background, const wxColour& track, const wxColour& fill) + { + m_background = background; + m_track = track; + m_fill = fill; + SetBackgroundColour(background); + Refresh(); + } + +private: + void on_paint(wxPaintEvent& /*event*/) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(m_background)); + dc.Clear(); + + const wxRect rect = GetClientRect(); + if (rect.GetWidth() <= 0 || rect.GetHeight() <= 0) + return; + + const int radius = std::max(2, rect.GetHeight() / 2); + + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_track)); + dc.DrawRoundedRectangle(rect.x, rect.y, rect.width, rect.height, radius); + + const int fill_width = std::clamp((rect.GetWidth() * m_value) / 100, 0, rect.GetWidth()); + if (fill_width <= 0) + return; + + wxRect fill_rect = rect; + fill_rect.SetWidth(fill_width); + dc.SetBrush(wxBrush(m_fill)); + dc.DrawRoundedRectangle(fill_rect.x, fill_rect.y, fill_rect.width, fill_rect.height, radius); + } + + void on_size(wxSizeEvent& event) + { + Refresh(); + Update(); + event.Skip(); + } + +private: + int m_value { 0 }; + wxColour m_background { *wxWHITE }; + wxColour m_track { wxColour(220, 224, 229) }; + wxColour m_fill { wxColour(0, 174, 66) }; +}; + +namespace { + +wxString na_text() +{ + return _L("N/A"); +} + +wxString temperature_unit() +{ + return wxString::FromUTF8("\xC2\xB0" "C"); +} + +bool cfg_bool(AppConfig* config, const char* key, bool fallback = false) +{ + if (config == nullptr) + return fallback; + const auto value = config->get(key); + if (value.empty()) + return fallback; + return value == "true"; +} + +int cfg_int(AppConfig* config, const char* key, int fallback) +{ + if (config == nullptr) + return fallback; + long parsed = fallback; + if (wxString::FromUTF8(config->get(key)).ToLong(&parsed)) + return static_cast(parsed); + return fallback; +} + +bool cfg_has_value(AppConfig* config, const char* key) +{ + return config != nullptr && !config->get(key).empty(); +} + +wxString short_machine_suffix(const std::string& dev_id) +{ + if (dev_id.size() <= 4) + return GUI::from_u8(dev_id); + return GUI::from_u8(dev_id.substr(dev_id.size() - 4)); +} + +wxRect sanitize_rect_for_displays(const wxRect& rect) +{ + if (wxDisplay::GetCount() <= 0) + return rect; + + int display_idx = wxDisplay::GetFromPoint(rect.GetTopLeft()); + if (display_idx == wxNOT_FOUND) { + const wxPoint center(rect.GetLeft() + rect.GetWidth() / 2, rect.GetTop() + rect.GetHeight() / 2); + display_idx = wxDisplay::GetFromPoint(center); + } + if (display_idx == wxNOT_FOUND) + display_idx = 0; + + const wxRect display = wxDisplay(static_cast(display_idx)).GetClientArea(); + const int width = std::min(rect.GetWidth(), display.GetWidth()); + const int height = std::min(rect.GetHeight(), display.GetHeight()); + const int max_x = display.GetRight() - width + 1; + const int max_y = display.GetBottom() - height + 1; + return wxRect(std::clamp(rect.GetLeft(), display.GetLeft(), max_x), + std::clamp(rect.GetTop(), display.GetTop(), max_y), + width, + height); +} + +bool contains_case_insensitive(const wxString& text, const wxString& token) +{ + return text.Lower().Find(token.Lower()) != wxNOT_FOUND; +} + +wxString prefixed_value(const wxString& prefix, const wxString& value) +{ + return prefix + ": " + value; +} + +void ensure_min_text_width(wxWindow* window, const wxFont& font, const wxString& sample_text, int horizontal_padding_dip = 0, int min_height = -1) +{ + if (window == nullptr) + return; + + int width = 0; + int height = 0; + window->GetTextExtent(sample_text, &width, &height, nullptr, nullptr, &font); + const wxSize current_min = window->GetMinSize(); + window->SetMinSize(wxSize(std::max(current_min.GetWidth(), width + window->FromDIP(horizontal_padding_dip)), + std::max(current_min.GetHeight(), std::max(min_height, height)))); +} + +} // namespace + +PrintStatusFrame::PrintStatusFrame(MainFrame* parent) + : wxFrame(parent, + wxID_ANY, + _L("Print Status Window"), + wxDefaultPosition, + wxDefaultSize, + (wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)) | wxFRAME_TOOL_WINDOW), + m_mainframe(parent), + m_refresh_timer(this) +{ + build_ui(); + Bind(wxEVT_TIMER, &PrintStatusFrame::on_timer, this); + Bind(wxEVT_CLOSE_WINDOW, &PrintStatusFrame::on_close, this); + Bind(wxEVT_MOVE, &PrintStatusFrame::on_move, this); + Bind(wxEVT_SIZE, &PrintStatusFrame::on_size, this); + m_printer_choice->Bind(wxEVT_CHOICE, &PrintStatusFrame::on_printer_changed, this); + + const wxSize fixed_size(FromDIP(430), FromDIP(352)); + SetMinSize(fixed_size); + SetSize(fixed_size); + m_is_initializing = false; +} + +PrintStatusFrame::~PrintStatusFrame() +{ + m_refresh_timer.Stop(); +} + +void PrintStatusFrame::show_window() +{ + Show(); + refresh_from_preferences(); + if (!m_refresh_timer.IsRunning()) + m_refresh_timer.Start(1000); + refresh_content(); + Raise(); +} + +void PrintStatusFrame::show_window_safe_on_minimize() +{ + Show(); + + if (m_safe_show_pending) + return; + + m_safe_show_pending = true; + CallAfter(&PrintStatusFrame::finish_safe_show_after_minimize); +} + +void PrintStatusFrame::refresh_from_preferences() +{ + if (!m_ui_built) + return; + + const auto config = load_config(); + apply_window_flags(config); + apply_theme(config); + apply_opacity(config); + apply_geometry(config); + if (config.remember_position && !config.has_position) + persist_geometry(true); + if (!m_is_initializing && IsShownOnScreen()) + refresh_content(); + Layout(); +} + +void PrintStatusFrame::finish_safe_show_after_minimize() +{ + m_safe_show_pending = false; + + if (m_is_shutting_down || !m_ui_built || !GetHandle()) + return; + + refresh_from_preferences(); + if (!m_refresh_timer.IsRunning()) + m_refresh_timer.Start(1000); + refresh_content(); + Raise(); +} + +void PrintStatusFrame::destroy_for_shutdown() +{ + if (m_is_shutting_down) + return; + + m_is_shutting_down = true; + m_refresh_timer.Stop(); + persist_geometry(true); + Destroy(); +} + +void PrintStatusFrame::build_ui() +{ + m_root_panel = new wxPanel(this, wxID_ANY); + + auto* root_sizer = new wxBoxSizer(wxVERTICAL); + + m_header_panel = new wxPanel(m_root_panel, wxID_ANY); + auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_printer_choice = new wxChoice(m_header_panel, wxID_ANY); + m_printer_choice->SetMinSize(wxSize(FromDIP(220), FromDIP(22))); + header_sizer->Add(m_printer_choice, 1, wxEXPAND, 0); + + m_badge_panel = new wxPanel(m_header_panel, wxID_ANY); + auto* badge_sizer = new wxBoxSizer(wxHORIZONTAL); + m_badge_label = new wxStaticText(m_badge_panel, wxID_ANY, _L("Idle")); + wxFont badge_font = m_badge_label->GetFont(); + badge_font.SetWeight(wxFONTWEIGHT_BOLD); + badge_font.SetPointSize(std::max(badge_font.GetPointSize() - 1, 8)); + m_badge_label->SetFont(badge_font); + badge_sizer->Add(m_badge_label, 0, wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(6)); + ensure_min_text_width(m_badge_panel, badge_font, _L("Finished"), 20, 26); + m_badge_panel->SetSizer(badge_sizer); + header_sizer->Add(m_badge_panel, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + m_header_panel->SetSizer(header_sizer); + root_sizer->Add(m_header_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(10)); + + m_job_label = new wxStaticText(m_root_panel, wxID_ANY, na_text()); + wxFont job_font = m_job_label->GetFont(); + job_font.SetWeight(wxFONTWEIGHT_BOLD); + job_font.SetPointSize(std::max(job_font.GetPointSize() + 3, 13)); + m_job_label->SetFont(job_font); + root_sizer->Add(m_job_label, 0, wxEXPAND | wxBOTTOM, FromDIP(10)); + + m_progress_row_panel = new wxPanel(m_root_panel, wxID_ANY); + auto* progress_row_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_percent_label = new wxStaticText(m_progress_row_panel, wxID_ANY, na_text()); + wxFont percent_font = m_percent_label->GetFont(); + percent_font.SetWeight(wxFONTWEIGHT_BOLD); + percent_font.SetPointSize(std::max(percent_font.GetPointSize() + 10, 20)); + m_percent_label->SetFont(percent_font); + progress_row_sizer->Add(m_percent_label, 0, wxALIGN_CENTER_VERTICAL, 0); + progress_row_sizer->AddStretchSpacer(); + + m_layer_summary_label = new wxStaticText(m_progress_row_panel, wxID_ANY, prefixed_value(_L("Layer"), na_text())); + progress_row_sizer->Add(m_layer_summary_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(12)); + + m_remaining_summary_label = new wxStaticText(m_progress_row_panel, wxID_ANY, na_text()); + ensure_min_text_width(m_remaining_summary_label, m_remaining_summary_label->GetFont(), _L("Finished"), 8); + progress_row_sizer->Add(m_remaining_summary_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(16)); + m_progress_row_panel->SetSizer(progress_row_sizer); + root_sizer->Add(m_progress_row_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + m_progress_bar = new ProgressBarPanel(m_root_panel); + root_sizer->Add(m_progress_bar, 0, wxEXPAND | wxBOTTOM, FromDIP(10)); + + m_status_eta_panel = new wxPanel(m_root_panel, wxID_ANY); + auto* status_eta_sizer = new wxBoxSizer(wxHORIZONTAL); + m_status_summary_label = new wxStaticText(m_status_eta_panel, wxID_ANY, prefixed_value(_L("Status"), na_text())); + status_eta_sizer->Add(m_status_summary_label, 0, wxALIGN_CENTER_VERTICAL, 0); + status_eta_sizer->AddStretchSpacer(); + m_eta_summary_label = new wxStaticText(m_status_eta_panel, wxID_ANY, _L("Estimated finish time: ") + na_text()); + ensure_min_text_width(m_eta_summary_label, m_eta_summary_label->GetFont(), _L("Estimated finish time: Finished"), 8); + status_eta_sizer->Add(m_eta_summary_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(12)); + m_status_eta_panel->SetSizer(status_eta_sizer); + root_sizer->Add(m_status_eta_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(12)); + + m_temperature_grid = new wxFlexGridSizer(0, 2, FromDIP(8), FromDIP(18)); + m_temperature_grid->AddGrowableCol(0, 1); + m_temperature_grid->AddGrowableCol(1, 1); + + m_nozzle_block = create_field_block(m_root_panel, _L("Nozzle"), true); + m_bed_block = create_field_block(m_root_panel, _L("Bed"), true); + m_chamber_block = create_field_block(m_root_panel, _L("Chamber"), true); + + m_temperature_grid->Add(m_nozzle_block.panel, 1, wxEXPAND, 0); + m_temperature_grid->Add(m_bed_block.panel, 1, wxEXPAND, 0); + m_temperature_grid->Add(m_chamber_block.panel, 1, wxEXPAND, 0); + m_temperature_grid->AddSpacer(0); + root_sizer->Add(m_temperature_grid, 0, wxEXPAND | wxBOTTOM, FromDIP(12)); + + m_warnings_panel = new wxPanel(m_root_panel, wxID_ANY); + auto* warnings_sizer = new wxBoxSizer(wxVERTICAL); + m_warnings_label = new wxStaticText(m_warnings_panel, wxID_ANY, _L("Warnings")); + wxFont warning_label_font = m_warnings_label->GetFont(); + warning_label_font.SetPointSize(std::max(warning_label_font.GetPointSize() - 1, 8)); + m_warnings_label->SetFont(warning_label_font); + warnings_sizer->Add(m_warnings_label, 0, wxBOTTOM, FromDIP(4)); + + m_warnings_value = new wxStaticText(m_warnings_panel, wxID_ANY, _L("No warnings")); + wxFont warning_value_font = m_warnings_value->GetFont(); + warning_value_font.SetWeight(wxFONTWEIGHT_BOLD); + m_warnings_value->SetFont(warning_value_font); + warnings_sizer->Add(m_warnings_value, 0, wxEXPAND, 0); + m_warnings_panel->SetMinSize(wxSize(-1, FromDIP(42))); + m_warnings_panel->SetSizer(warnings_sizer); + root_sizer->Add(m_warnings_panel, 0, wxEXPAND, 0); + + auto* frame_sizer = new wxBoxSizer(wxVERTICAL); + frame_sizer->Add(m_root_panel, 1, wxALL | wxEXPAND, FromDIP(14)); + m_root_panel->SetSizer(root_sizer); + SetSizer(frame_sizer); + m_ui_built = true; +} + +PrintStatusFrame::FieldBlock PrintStatusFrame::create_field_block(wxWindow* parent, const wxString& label, bool compact_label) +{ + FieldBlock block; + block.panel = new wxPanel(parent, wxID_ANY); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + block.label = new wxStaticText(block.panel, wxID_ANY, label); + wxFont label_font = block.label->GetFont(); + label_font.SetPointSize(std::max(label_font.GetPointSize() - (compact_label ? 1 : 0), 8)); + block.label->SetFont(label_font); + sizer->Add(block.label, 0, wxBOTTOM, FromDIP(2)); + + block.value = new wxStaticText(block.panel, wxID_ANY, na_text()); + wxFont value_font = block.value->GetFont(); + value_font.SetWeight(wxFONTWEIGHT_BOLD); + value_font.SetPointSize(std::max(value_font.GetPointSize() + 1, 10)); + block.value->SetFont(value_font); + sizer->Add(block.value, 0, wxEXPAND, 0); + + block.panel->SetSizer(sizer); + return block; +} + +PrintStatusFrame::Config PrintStatusFrame::load_config() const +{ + Config config; + auto* app_config = wxGetApp().app_config; + config.enabled = cfg_bool(app_config, "print_status_window_enabled", false); + config.always_on_top = cfg_bool(app_config, "print_status_window_always_on_top", false); + config.remember_position = cfg_bool(app_config, "print_status_window_remember_position", true); + config.theme = app_config ? app_config->get("print_status_window_theme") : "follow_app"; + if (config.theme.empty()) + config.theme = "follow_app"; + config.opacity = std::clamp(cfg_int(app_config, "print_status_window_opacity", 100), 40, 100); + + if (cfg_has_value(app_config, "print_status_window_pos_x") && cfg_has_value(app_config, "print_status_window_pos_y")) { + config.position = wxPoint(cfg_int(app_config, "print_status_window_pos_x", wxDefaultPosition.x), + cfg_int(app_config, "print_status_window_pos_y", wxDefaultPosition.y)); + config.has_position = true; + } + return config; +} + +PrintStatusFrame::Palette PrintStatusFrame::build_palette(const Config& config) const +{ + const bool dark_mode = config.theme == "dark" || (config.theme == "follow_app" && wxGetApp().dark_mode()); + const wxColour accent(0, 174, 66); + + if (dark_mode) { + return Palette{ + wxColour(34, 37, 42), + wxColour(34, 37, 42), + wxColour(44, 48, 53), + wxColour(70, 75, 82), + wxColour(242, 245, 247), + wxColour(197, 203, 208), + wxColour(148, 154, 160), + wxColour(66, 71, 77), + accent, + wxColour(82, 87, 94), + wxColour(230, 234, 237), + wxColour(16, 96, 51), + wxColour(220, 248, 229), + wxColour(114, 83, 17), + wxColour(255, 236, 188), + wxColour(118, 38, 52), + wxColour(255, 221, 228) + }; + } + + return Palette{ + *wxWHITE, + *wxWHITE, + wxColour(246, 248, 250), + wxColour(216, 222, 227), + wxColour(32, 38, 43), + wxColour(84, 93, 101), + wxColour(117, 125, 133), + wxColour(223, 227, 231), + accent, + wxColour(235, 239, 242), + wxColour(82, 91, 98), + wxColour(219, 245, 228), + wxColour(16, 96, 51), + wxColour(255, 242, 213), + wxColour(149, 96, 19), + wxColour(255, 227, 232), + wxColour(173, 34, 59) + }; +} + +void PrintStatusFrame::apply_theme(const Config& config) +{ + m_palette = build_palette(config); + + SetBackgroundColour(m_palette.background); + if (m_root_panel) + m_root_panel->SetBackgroundColour(m_palette.background); + + const auto apply_panel = [&](wxPanel* panel) { + if (panel == nullptr) + return; + panel->SetBackgroundColour(m_palette.panel_background); + panel->SetForegroundColour(m_palette.text_primary); + }; + + apply_panel(m_header_panel); + apply_panel(m_progress_row_panel); + apply_panel(m_status_eta_panel); + apply_panel(m_nozzle_block.panel); + apply_panel(m_bed_block.panel); + apply_panel(m_chamber_block.panel); + apply_panel(m_warnings_panel); + + if (m_printer_choice) { + m_printer_choice->SetBackgroundColour(m_palette.input_background); + m_printer_choice->SetForegroundColour(m_palette.text_primary); + m_printer_choice->SetToolTip(_L("Printer")); + } + + if (m_job_label) + m_job_label->SetForegroundColour(m_palette.text_primary); + if (m_percent_label) + m_percent_label->SetForegroundColour(m_palette.progress_fill); + if (m_layer_summary_label) + m_layer_summary_label->SetForegroundColour(m_palette.text_secondary); + if (m_remaining_summary_label) + m_remaining_summary_label->SetForegroundColour(m_palette.text_secondary); + if (m_status_summary_label) + m_status_summary_label->SetForegroundColour(m_palette.text_secondary); + if (m_eta_summary_label) + m_eta_summary_label->SetForegroundColour(m_palette.text_secondary); + if (m_warnings_label) + m_warnings_label->SetForegroundColour(m_palette.text_muted); + if (m_warnings_value) + m_warnings_value->SetForegroundColour(m_palette.text_primary); + + const auto apply_block_theme = [&](FieldBlock& block) { + if (block.label) + block.label->SetForegroundColour(m_palette.text_muted); + if (block.value) + block.value->SetForegroundColour(m_palette.text_primary); + }; + + apply_block_theme(m_nozzle_block); + apply_block_theme(m_bed_block); + apply_block_theme(m_chamber_block); + + if (m_progress_bar) + m_progress_bar->SetColors(m_palette.panel_background, m_palette.progress_track, m_palette.progress_fill); + + if (IsShownOnScreen()) + Refresh(); +} + +void PrintStatusFrame::apply_opacity(const Config& config) +{ + const int alpha = std::clamp((config.opacity * 255) / 100, 102, 255); + if (!SetTransparent(static_cast(alpha))) + SetTransparent(wxALPHA_OPAQUE); +} + +void PrintStatusFrame::apply_window_flags(const Config& config) +{ + const long current_style = GetWindowStyleFlag(); + const long desired_style = config.always_on_top ? (current_style | wxSTAY_ON_TOP) : (current_style & ~wxSTAY_ON_TOP); + if (desired_style != current_style) + SetWindowStyleFlag(desired_style); + +#ifdef _WIN32 + if (GetHandle() != nullptr) { + ::SetWindowPos(static_cast(GetHandle()), + config.always_on_top ? HWND_TOPMOST : HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } +#endif + + if (config.always_on_top && IsShown()) + Raise(); +} + +void PrintStatusFrame::apply_geometry(const Config& config) +{ + if (!config.remember_position) + return; + + if (!config.has_position) + return; + + const wxRect sanitized = sanitize_rect_for_displays(wxRect(config.position, GetSize())); + if (sanitized.GetPosition() == GetPosition()) + return; + + m_ignore_geometry_events = true; + SetPosition(sanitized.GetPosition()); + m_ignore_geometry_events = false; +} + +void PrintStatusFrame::persist_geometry(bool save_immediately) +{ + const auto config = load_config(); + auto* app_config = wxGetApp().app_config; + if (app_config == nullptr || !config.remember_position || IsIconized()) + return; + + const wxPoint position = GetPosition(); + if (position.x != wxDefaultPosition.x && position.y != wxDefaultPosition.y) { + app_config->set("print_status_window_pos_x", std::to_string(position.x)); + app_config->set("print_status_window_pos_y", std::to_string(position.y)); + } + + if (save_immediately) + app_config->save(); +} + +void PrintStatusFrame::refresh_content() +{ + if (m_is_shutting_down || m_is_initializing || !m_ui_built) + return; + + sync_printer_selector(); + update_snapshot_view(resolve_machine()); +} + +void PrintStatusFrame::sync_printer_selector() +{ + if (!m_ui_built || m_printer_choice == nullptr) + return; + + const std::vector machine_ids = get_machine_ids(); + + if (machine_ids.empty()) { + m_choice_dev_ids.clear(); + m_selected_dev_id.clear(); + const std::string empty_signature = "__empty__"; + if (m_printer_choice_signature != empty_signature) { + m_updating_printer_choice = true; + m_printer_choice->Clear(); + m_printer_choice->Append(_L("No printer available")); + m_printer_choice->SetSelection(0); + m_printer_choice->Enable(false); + m_updating_printer_choice = false; + m_printer_choice_signature = empty_signature; + } + return; + } + + if (m_selected_dev_id.empty()) + m_selected_dev_id = get_default_machine_id(); + if (std::find(machine_ids.begin(), machine_ids.end(), m_selected_dev_id) == machine_ids.end()) { + m_selected_dev_id = get_default_machine_id(); + if (m_selected_dev_id.empty() || std::find(machine_ids.begin(), machine_ids.end(), m_selected_dev_id) == machine_ids.end()) + m_selected_dev_id = machine_ids.front(); + } + + const std::string signature = build_printer_choice_signature(machine_ids); + std::map label_counts; + for (const auto& dev_id : machine_ids) { + if (auto* obj = get_machine_by_id(dev_id)) + label_counts[build_machine_name(obj)]++; + } + + if (m_printer_choice_signature != signature) { + m_updating_printer_choice = true; + m_printer_choice->Clear(); + m_choice_dev_ids = machine_ids; + + for (const auto& dev_id : machine_ids) { + wxString label = GUI::from_u8(dev_id); + if (auto* obj = get_machine_by_id(dev_id)) + label = build_machine_name(obj); + if (label_counts[label] > 1) + label += " (" + short_machine_suffix(dev_id) + ")"; + m_printer_choice->Append(label); + } + + const auto it = std::find(m_choice_dev_ids.begin(), m_choice_dev_ids.end(), m_selected_dev_id); + if (it != m_choice_dev_ids.end()) + m_printer_choice->SetSelection(static_cast(std::distance(m_choice_dev_ids.begin(), it))); + + m_printer_choice->Enable(machine_ids.size() > 1); + m_updating_printer_choice = false; + m_printer_choice_signature = signature; + return; + } + + m_choice_dev_ids = machine_ids; + m_printer_choice->Enable(machine_ids.size() > 1); + + const auto it = std::find(m_choice_dev_ids.begin(), m_choice_dev_ids.end(), m_selected_dev_id); + if (it == m_choice_dev_ids.end()) + return; + + const int expected_selection = static_cast(std::distance(m_choice_dev_ids.begin(), it)); + if (m_printer_choice->GetSelection() != expected_selection) { + m_updating_printer_choice = true; + m_printer_choice->SetSelection(expected_selection); + m_updating_printer_choice = false; + } +} + +void PrintStatusFrame::update_snapshot_view(MachineObject* obj) +{ + if (!m_ui_built) + return; + + const bool online = is_machine_online(obj); + const bool has_active_print = obj != nullptr && online && + (obj->is_in_printing() || obj->is_in_prepare() || obj->print_status == "SLICING" || obj->print_status == "FINISH"); + const wxString warning_text = build_warning_text(obj); + + update_header(obj, online, warning_text, has_active_print); + update_job_row(obj, online); + update_progress_row(obj, online, has_active_print); + update_status_eta_row(obj, online, has_active_print); + update_temperature_rows(obj, online); + update_warnings_row(warning_text); +} + +void PrintStatusFrame::update_header(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print) +{ + if (m_printer_choice) + m_printer_choice->SetToolTip(obj != nullptr ? build_machine_name(obj) : _L("No printer available")); + apply_badge_style(build_badge_text(obj, online, warning_text, has_active_print), + build_badge_tone(obj, online, warning_text, has_active_print)); +} + +void PrintStatusFrame::update_job_row(MachineObject* obj, bool online) +{ + const wxString full_text = build_job_name_text(obj, online); + const wxString display_text = compact_label_text(m_job_label, full_text, 320); + set_label_text(m_job_label, display_text, display_text != full_text ? full_text : wxEmptyString); +} + +void PrintStatusFrame::update_progress_row(MachineObject* obj, bool online, bool has_active_print) +{ + set_label_text(m_percent_label, build_progress_text(obj, online)); + set_label_text(m_layer_summary_label, prefixed_value(_L("Layer"), build_layers_text(obj, online))); + set_label_text(m_remaining_summary_label, build_remaining_time_text(obj, online)); + m_progress_bar->SetValue(has_active_print ? build_progress_percent(obj, online) : 0); +} + +void PrintStatusFrame::update_status_eta_row(MachineObject* obj, bool online, bool has_active_print) +{ + set_label_text(m_status_summary_label, prefixed_value(_L("Status"), build_stage_text(obj, online))); + set_label_text(m_eta_summary_label, build_estimated_finish_time_text(obj, online, has_active_print)); +} + +void PrintStatusFrame::update_temperature_rows(MachineObject* obj, bool online) +{ + update_field_block(m_nozzle_block, build_nozzle_temp_text(obj, online)); + update_field_block(m_bed_block, build_bed_temp_text(obj, online)); + update_field_block(m_chamber_block, build_chamber_temp_text(obj, online)); +} + +void PrintStatusFrame::update_warnings_row(const wxString& warning_text) +{ + set_label_text(m_warnings_value, warning_text.empty() ? _L("No warnings") : warning_text, + warning_text.empty() ? wxEmptyString : warning_text); +} + +void PrintStatusFrame::update_field_block(FieldBlock& block, const wxString& value, bool compact, int fallback_width) +{ + const wxString full_value = value.empty() ? na_text() : value; + if (compact) { + const wxString display_value = compact_label_text(block.value, full_value, fallback_width); + set_label_text(block.value, display_value, display_value != full_value ? full_value : wxEmptyString); + } else { + set_label_text(block.value, full_value); + } +} + +void PrintStatusFrame::set_label_text(wxStaticText* label, const wxString& text, const wxString& tooltip) +{ + if (label == nullptr) + return; + + if (label->GetLabelText() != text) + label->SetLabel(text); + label->SetToolTip(tooltip); +} + +void PrintStatusFrame::apply_badge_style(const wxString& text, BadgeTone tone) +{ + wxColour background = m_palette.badge_neutral_background; + wxColour foreground = m_palette.badge_neutral_text; + + switch (tone) { + case BadgeTone::Positive: + background = m_palette.badge_positive_background; + foreground = m_palette.badge_positive_text; + break; + case BadgeTone::Warning: + background = m_palette.badge_warning_background; + foreground = m_palette.badge_warning_text; + break; + case BadgeTone::Error: + background = m_palette.badge_error_background; + foreground = m_palette.badge_error_text; + break; + case BadgeTone::Neutral: + default: + break; + } + + if (m_badge_panel) { + m_badge_panel->SetBackgroundColour(background); + m_badge_panel->SetForegroundColour(foreground); + } + if (m_badge_label) { + m_badge_label->SetBackgroundColour(background); + m_badge_label->SetForegroundColour(foreground); + set_label_text(m_badge_label, text); + } + + if (m_badge_label) + m_badge_label->Refresh(); + if (m_badge_panel) + m_badge_panel->Refresh(); +} + +wxString PrintStatusFrame::compact_label_text(wxStaticText* control, const wxString& value, int fallback_width) const +{ + if (control == nullptr || value.empty()) + return value; + if (control->GetHandle() == nullptr || !control->IsShownOnScreen()) + return value; + + wxClientDC dc(control); + dc.SetFont(control->GetFont()); + const int width = std::max(control->GetClientSize().GetWidth(), FromDIP(fallback_width)); + return wxControl::Ellipsize(value, dc, wxELLIPSIZE_END, width); +} + +std::string PrintStatusFrame::build_printer_choice_signature(const std::vector& machine_ids) const +{ + std::string signature; + signature.reserve(machine_ids.size() * 32); + + for (const auto& dev_id : machine_ids) { + signature += dev_id; + signature += '|'; + if (auto* obj = get_machine_by_id(dev_id)) + signature += build_machine_name(obj).ToUTF8().data(); + signature += ';'; + } + + return signature; +} + +bool PrintStatusFrame::is_machine_online(MachineObject* obj) const +{ + // MachineObject::is_online() is already driven by the device updates / dev_online state, + // which is the smallest reliable signal we have here for the widget. + return obj != nullptr && obj->is_online(); +} + +int PrintStatusFrame::build_progress_percent(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online) + return 0; + + if (obj->subtask_ != nullptr && obj->subtask_->task_progress >= 0) + return std::clamp(obj->subtask_->task_progress, 0, 100); + if ((obj->is_in_prepare() || obj->print_status == "SLICING") && obj->gcode_file_prepare_percent >= 0 && obj->gcode_file_prepare_percent <= 100) + return obj->gcode_file_prepare_percent; + if (obj->mc_print_percent >= 0 && obj->mc_print_percent <= 100) + return obj->mc_print_percent; + return 0; +} + +PrintStatusFrame::BadgeTone PrintStatusFrame::build_badge_tone(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print) const +{ + if (obj == nullptr || !online) + return BadgeTone::Neutral; + if (obj->print_error > 0) + return BadgeTone::Error; + if (!warning_text.empty()) + return BadgeTone::Warning; + if (contains_case_insensitive(build_stage_text(obj, online), _L("Paused"))) + return BadgeTone::Warning; + if (has_active_print) + return BadgeTone::Positive; + return BadgeTone::Neutral; +} + +wxString PrintStatusFrame::build_badge_text(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print) const +{ + if (obj == nullptr || !online) + return _L("Offline"); + if (obj->print_error > 0) + return _L("Error"); + if (!warning_text.empty()) + return _L("Warning"); + + const wxString stage_text = build_stage_text(obj, online); + if (contains_case_insensitive(stage_text, _L("Paused"))) + return _L("Paused"); + if (obj->print_status == "FINISH") + return _L("Finished"); + if (has_active_print) + return _L("Printing"); + return _L("Idle"); +} + +std::vector PrintStatusFrame::get_machine_ids() const +{ + std::vector result; + auto* dev = wxGetApp().getDeviceManager(); + if (dev == nullptr) + return result; + + const auto machines = dev->get_my_machine_list(); + result.reserve(machines.size()); + for (const auto& item : machines) { + if (item.second != nullptr) + result.emplace_back(item.first); + } + return result; +} + +MachineObject* PrintStatusFrame::resolve_machine() +{ + if (!m_selected_dev_id.empty()) { + if (auto* obj = get_machine_by_id(m_selected_dev_id)) + return obj; + } + + m_selected_dev_id = get_default_machine_id(); + if (m_selected_dev_id.empty()) + return nullptr; + return get_machine_by_id(m_selected_dev_id); +} + +MachineObject* PrintStatusFrame::get_machine_by_id(const std::string& dev_id) const +{ + auto* dev = wxGetApp().getDeviceManager(); + return dev ? dev->get_my_machine(dev_id) : nullptr; +} + +std::string PrintStatusFrame::get_default_machine_id() const +{ + auto* dev = wxGetApp().getDeviceManager(); + if (dev == nullptr) + return {}; + + if (auto* selected = dev->get_selected_machine()) { + if (dev->get_my_machine(selected->get_dev_id()) != nullptr) + return selected->get_dev_id(); + } + + const auto machines = dev->get_my_machine_list(); + if (!machines.empty()) + return machines.begin()->first; + return {}; +} + +wxString PrintStatusFrame::build_machine_name(MachineObject* obj) const +{ + if (obj == nullptr) + return na_text(); + + const wxString dev_name = GUI::from_u8(obj->get_dev_name()); + return dev_name.empty() ? GUI::from_u8(obj->get_dev_id()) : dev_name; +} + +wxString PrintStatusFrame::build_stage_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr) + return na_text(); + if (!online) + return _L("Offline"); + + if (obj->is_in_prepare() || obj->print_status == "SLICING") { + wxString prepare_text; + bool show_percent = true; + + if (obj->is_in_prepare()) { + prepare_text = _L("Downloading..."); + } else if (obj->print_status == "SLICING") { + if (obj->queue_number <= 0) { + prepare_text = _L("Cloud Slicing..."); + } else { + prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %d tasks ahead."), obj->queue_number); + show_percent = false; + } + } + + if (obj->gcode_file_prepare_percent >= 0 && obj->gcode_file_prepare_percent <= 100 && show_percent) + prepare_text += wxString::Format(" (%d%%)", obj->gcode_file_prepare_percent); + + return obj->get_curr_stage().IsEmpty() ? prepare_text : obj->get_curr_stage(); + } + + wxString stage = obj->get_curr_stage(); + if (!stage.IsEmpty()) + return stage; + if (obj->print_status == "FINISH") + return _L("Finished"); + if (obj->is_in_printing()) + return get_stage_string(obj->mc_print_stage); + return _L("Idle"); +} + +wxString PrintStatusFrame::build_job_name_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online) + return na_text(); + if (!obj->subtask_name.empty()) + return GUI::from_u8(obj->subtask_name); + if (!obj->m_gcode_file.empty()) + return GUI::from_u8(obj->m_gcode_file); + return na_text(); +} + +wxString PrintStatusFrame::build_progress_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online) + return na_text(); + + const int progress = build_progress_percent(obj, online); + if (progress <= 0 && !(obj->is_in_printing() || obj->is_in_prepare() || obj->print_status == "SLICING" || obj->print_status == "FINISH")) + return na_text(); + if (obj->print_status == "FINISH") + return "100%"; + return wxString::Format("%d%%", progress); +} + +wxString PrintStatusFrame::build_remaining_time_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online) + return na_text(); + if (obj->print_status == "FINISH") + return _L("Finished"); + if (obj->mc_left_time > 0) { + try { + const auto left_time = get_bbl_monitor_time_dhm(obj->mc_left_time); + if (!left_time.empty()) + return "-" + GUI::from_u8(left_time); + } catch (...) { + ; + } + } + return na_text(); +} + +wxString PrintStatusFrame::build_estimated_finish_time_text(MachineObject* obj, bool online, bool has_active_print) const +{ + if (obj == nullptr || !online) + return _L("Estimated finish time: ") + na_text(); + if (obj->print_status == "FINISH") + return _L("Estimated finish time: ") + _L("Finished"); + if (!has_active_print || obj->mc_left_time <= 0) + return _L("Estimated finish time: ") + na_text(); + + try { + const bool use_12h_format = wxGetApp().app_config && wxGetApp().app_config->get("use_12h_time_format") == "true"; + const auto finish_time = get_bbl_finish_time_dhm(obj->mc_left_time, use_12h_format); + if (!finish_time.empty()) + return _L("Estimated finish time: ") + GUI::from_u8(finish_time); + } catch (...) { + ; + } + + return _L("Estimated finish time: ") + na_text(); +} + +wxString PrintStatusFrame::build_layers_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online) + return na_text(); + if (obj->is_support_layer_num && obj->total_layers > 0 && obj->curr_layer >= 0) + return wxString::Format("%d/%d", obj->curr_layer, obj->total_layers); + return na_text(); +} + +wxString PrintStatusFrame::build_nozzle_temp_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online || obj->GetExtderSystem() == nullptr) + return na_text(); + + auto ext = obj->GetExtderSystem()->GetCurrentExtder(); + if (!ext.has_value()) + ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); + if (!ext.has_value()) + return na_text(); + + return wxString::Format("%d / %d", ext->GetCurrentTemp(), ext->GetTargetTemp()) + temperature_unit(); +} + +wxString PrintStatusFrame::build_bed_temp_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online || obj->GetBed() == nullptr) + return na_text(); + + auto* bed = obj->GetBed(); + return wxString::Format("%d / %d", static_cast(bed->GetBedTemp()), static_cast(bed->GetBedTempTarget())) + temperature_unit(); +} + +wxString PrintStatusFrame::build_chamber_temp_text(MachineObject* obj, bool online) const +{ + if (obj == nullptr || !online || obj->GetChamber() == nullptr) + return na_text(); + + const auto& chamber = obj->GetChamber(); + if (!chamber->SupportChamberTempDisplay()) + return na_text(); + + if (chamber->SupportChamberEdit()) + return wxString::Format("%d / %d", static_cast(chamber->GetChamberTemp()), static_cast(chamber->GetChamberTempTarget())) + temperature_unit(); + + return wxString::Format("%d", static_cast(chamber->GetChamberTemp())) + temperature_unit(); +} + +wxString PrintStatusFrame::build_warning_text(MachineObject* obj) const +{ + if (obj == nullptr) + return wxEmptyString; + + if (obj->print_error > 0) { + if (auto* query = wxGetApp().get_hms_query()) { + wxString error_text = query->query_print_error_msg(obj, obj->print_error); + if (!error_text.empty()) + return error_text; + } + return _L("Error code") + ": " + GUI::from_u8(obj->get_print_error_str()); + } + + if (auto* hms = obj->GetHMS()) { + for (const auto& item : hms->GetHMSItems()) { + if (auto* query = wxGetApp().get_hms_query()) { + wxString warning_text = query->query_hms_msg(obj, item.get_long_error_code()); + if (!warning_text.empty()) + return warning_text; + } + if (!item.get_long_error_code().empty()) + return _L("Warning code") + ": " + GUI::from_u8(item.get_long_error_code()); + } + } + + return wxEmptyString; +} + +void PrintStatusFrame::on_timer(wxTimerEvent& /*event*/) +{ + if (m_is_shutting_down || m_is_initializing || !m_ui_built) + return; + + refresh_content(); +} + +void PrintStatusFrame::on_close(wxCloseEvent& event) +{ + if (m_is_shutting_down || !event.CanVeto()) { + event.Skip(); + return; + } + + persist_geometry(true); + m_refresh_timer.Stop(); + Hide(); +} + +void PrintStatusFrame::on_printer_changed(wxCommandEvent& event) +{ + if (m_updating_printer_choice) { + event.Skip(); + return; + } + + const int selection = m_printer_choice->GetSelection(); + if (selection >= 0 && selection < static_cast(m_choice_dev_ids.size())) { + m_selected_dev_id = m_choice_dev_ids[selection]; + refresh_content(); + } + event.Skip(); +} + +void PrintStatusFrame::on_move(wxMoveEvent& event) +{ + if (!m_ignore_geometry_events) + persist_geometry(false); + event.Skip(); +} + +void PrintStatusFrame::on_size(wxSizeEvent& event) +{ + event.Skip(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PrintStatusFrame.hpp b/src/slic3r/GUI/PrintStatusFrame.hpp new file mode 100644 index 0000000000..5ccbd43275 --- /dev/null +++ b/src/slic3r/GUI/PrintStatusFrame.hpp @@ -0,0 +1,171 @@ +#ifndef slic3r_PrintStatusFrame_hpp_ +#define slic3r_PrintStatusFrame_hpp_ + +#include +#include + +#include +#include +#include + +class wxBoxSizer; +class wxChoice; +class wxFlexGridSizer; +class wxPanel; +class wxStaticText; + +namespace Slic3r { + +class MachineObject; + +namespace GUI { + +class MainFrame; +class ProgressBarPanel; + +class PrintStatusFrame final : public wxFrame +{ +public: + explicit PrintStatusFrame(MainFrame* parent); + ~PrintStatusFrame() override; + + void show_window(); + void show_window_safe_on_minimize(); + void refresh_from_preferences(); + void destroy_for_shutdown(); + +private: + enum class BadgeTone { + Neutral, + Positive, + Warning, + Error + }; + + struct Config { + bool enabled { false }; + bool always_on_top { false }; + bool remember_position { true }; + std::string theme { "follow_app" }; + int opacity { 100 }; + bool has_position { false }; + wxPoint position { wxDefaultPosition }; + }; + + struct Palette { + wxColour background; + wxColour panel_background; + wxColour input_background; + wxColour border; + wxColour text_primary; + wxColour text_secondary; + wxColour text_muted; + wxColour progress_track; + wxColour progress_fill; + wxColour badge_neutral_background; + wxColour badge_neutral_text; + wxColour badge_positive_background; + wxColour badge_positive_text; + wxColour badge_warning_background; + wxColour badge_warning_text; + wxColour badge_error_background; + wxColour badge_error_text; + }; + + struct FieldBlock { + wxPanel* panel { nullptr }; + wxStaticText* label { nullptr }; + wxStaticText* value { nullptr }; + }; + + void build_ui(); + FieldBlock create_field_block(wxWindow* parent, const wxString& label, bool compact_label = false); + Config load_config() const; + Palette build_palette(const Config& config) const; + void apply_theme(const Config& config); + void apply_opacity(const Config& config); + void apply_window_flags(const Config& config); + void apply_geometry(const Config& config); + void persist_geometry(bool save_immediately); + void finish_safe_show_after_minimize(); + void refresh_content(); + void sync_printer_selector(); + void update_snapshot_view(MachineObject* obj); + void update_header(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print); + void update_job_row(MachineObject* obj, bool online); + void update_progress_row(MachineObject* obj, bool online, bool has_active_print); + void update_status_eta_row(MachineObject* obj, bool online, bool has_active_print); + void update_temperature_rows(MachineObject* obj, bool online); + void update_warnings_row(const wxString& warning_text); + void update_field_block(FieldBlock& block, const wxString& value, bool compact = false, int fallback_width = 160); + void set_label_text(wxStaticText* label, const wxString& text, const wxString& tooltip = wxEmptyString); + void apply_badge_style(const wxString& text, BadgeTone tone); + wxString compact_label_text(wxStaticText* control, const wxString& value, int fallback_width = 220) const; + bool is_machine_online(MachineObject* obj) const; + int build_progress_percent(MachineObject* obj, bool online) const; + BadgeTone build_badge_tone(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print) const; + wxString build_badge_text(MachineObject* obj, bool online, const wxString& warning_text, bool has_active_print) const; + std::string build_printer_choice_signature(const std::vector& machine_ids) const; + + std::vector get_machine_ids() const; + MachineObject* resolve_machine(); + MachineObject* get_machine_by_id(const std::string& dev_id) const; + std::string get_default_machine_id() const; + + wxString build_machine_name(MachineObject* obj) const; + wxString build_stage_text(MachineObject* obj, bool online) const; + wxString build_job_name_text(MachineObject* obj, bool online) const; + wxString build_progress_text(MachineObject* obj, bool online) const; + wxString build_remaining_time_text(MachineObject* obj, bool online) const; + wxString build_estimated_finish_time_text(MachineObject* obj, bool online, bool has_active_print) const; + wxString build_layers_text(MachineObject* obj, bool online) const; + wxString build_nozzle_temp_text(MachineObject* obj, bool online) const; + wxString build_bed_temp_text(MachineObject* obj, bool online) const; + wxString build_chamber_temp_text(MachineObject* obj, bool online) const; + wxString build_warning_text(MachineObject* obj) const; + + void on_timer(wxTimerEvent& event); + void on_close(wxCloseEvent& event); + void on_printer_changed(wxCommandEvent& event); + void on_move(wxMoveEvent& event); + void on_size(wxSizeEvent& event); + +private: + MainFrame* m_mainframe { nullptr }; + wxTimer m_refresh_timer; + wxPanel* m_root_panel { nullptr }; + wxPanel* m_header_panel { nullptr }; + wxChoice* m_printer_choice { nullptr }; + wxPanel* m_badge_panel { nullptr }; + wxStaticText* m_badge_label { nullptr }; + wxStaticText* m_job_label { nullptr }; + wxPanel* m_progress_row_panel { nullptr }; + wxStaticText* m_percent_label { nullptr }; + wxStaticText* m_layer_summary_label { nullptr }; + wxStaticText* m_remaining_summary_label { nullptr }; + ProgressBarPanel* m_progress_bar { nullptr }; + wxPanel* m_status_eta_panel { nullptr }; + wxStaticText* m_status_summary_label { nullptr }; + wxStaticText* m_eta_summary_label { nullptr }; + wxFlexGridSizer* m_temperature_grid { nullptr }; + FieldBlock m_nozzle_block; + FieldBlock m_bed_block; + FieldBlock m_chamber_block; + wxPanel* m_warnings_panel { nullptr }; + wxStaticText* m_warnings_label { nullptr }; + wxStaticText* m_warnings_value { nullptr }; + Palette m_palette; + std::vector m_choice_dev_ids; + std::string m_selected_dev_id; + std::string m_printer_choice_signature; + bool m_is_shutting_down { false }; + bool m_is_initializing { true }; + bool m_ui_built { false }; + bool m_safe_show_pending { false }; + bool m_ignore_geometry_events { false }; + bool m_updating_printer_choice { false }; +}; + +}} // namespace Slic3r::GUI + +#endif From c14d488a6442d00b95ca5853a851534a04e3aba2 Mon Sep 17 00:00:00 2001 From: Sebastian Balzer Date: Sat, 2 May 2026 11:40:11 +0200 Subject: [PATCH 2/2] feat(gui): support multiple print status windows Extend the print status window integration from a single MainFrame-owned instance to a primary window plus additional per-printer windows. The primary window still owns auto-show on minimize, settings refresh, and remembered position, while additional windows can be opened in parallel, keep their own selected dev_id, and are opened with a small cascaded offset. Add two new entry points for multi-printer monitoring: a `+` action in the PrintStatusFrame header to open another window, and a new icon button in the Device tab print progress area to open a status window for the currently shown printer. Existing windows are reused when they already track the requested device, and the additional-window picker prefers printers that are not already visible in another status window. Simplify the feature controls by removing the `print_status_window_enabled` preference and the remaining enable gates around manual open and auto-show. The print status window is now always available, while auto-show on minimize and close-to-tray remain controlled by their dedicated settings. The Device tab launch button now always uses the active `print_status_window` SVG icon and no longer has a disabled state. --- resources/images/print_status_window.svg | 61 ++++++++ src/libslic3r/AppConfig.cpp | 24 +--- src/slic3r/GUI/MainFrame.cpp | 176 ++++++++++++++++++++--- src/slic3r/GUI/MainFrame.hpp | 12 +- src/slic3r/GUI/Preferences.cpp | 2 - src/slic3r/GUI/PrintStatusFrame.cpp | 40 +++++- src/slic3r/GUI/PrintStatusFrame.hpp | 9 +- src/slic3r/GUI/StatusPanel.cpp | 24 ++++ src/slic3r/GUI/StatusPanel.hpp | 3 + 9 files changed, 292 insertions(+), 59 deletions(-) create mode 100644 resources/images/print_status_window.svg diff --git a/resources/images/print_status_window.svg b/resources/images/print_status_window.svg new file mode 100644 index 0000000000..b4174843cc --- /dev/null +++ b/resources/images/print_status_window.svg @@ -0,0 +1,61 @@ + + diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 840848efb3..e1cb526cf2 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -340,36 +340,14 @@ void AppConfig::set_defaults() set_bool("show_print_history", true); } - if (get("print_status_window_enabled").empty()) - set_bool("print_status_window_enabled", false); if (get("print_status_window_auto_show_on_minimize").empty()) set_bool("print_status_window_auto_show_on_minimize", false); if (get("print_status_window_close_to_tray").empty()) set_bool("print_status_window_close_to_tray", false); if (get("print_status_window_always_on_top").empty()) - set_bool("print_status_window_always_on_top", false); + set_bool("print_status_window_always_on_top", true); if (get("print_status_window_remember_position").empty()) set_bool("print_status_window_remember_position", true); - if (get("print_status_window_show_printer_selector").empty()) - set_bool("print_status_window_show_printer_selector", true); - if (get("print_status_window_show_printer_name").empty()) - set_bool("print_status_window_show_printer_name", true); - if (get("print_status_window_show_stage").empty()) - set_bool("print_status_window_show_stage", true); - if (get("print_status_window_show_job_name").empty()) - set_bool("print_status_window_show_job_name", true); - if (get("print_status_window_show_progress").empty()) - set_bool("print_status_window_show_progress", true); - if (get("print_status_window_show_remaining_time").empty()) - set_bool("print_status_window_show_remaining_time", true); - if (get("print_status_window_show_layers").empty()) - set_bool("print_status_window_show_layers", true); - if (get("print_status_window_show_nozzle_temp").empty()) - set_bool("print_status_window_show_nozzle_temp", true); - if (get("print_status_window_show_bed_temp").empty()) - set_bool("print_status_window_show_bed_temp", true); - if (get("print_status_window_show_warnings").empty()) - set_bool("print_status_window_show_warnings", true); if (get("print_status_window_theme").empty()) set("print_status_window_theme", "follow_app"); if (get("print_status_window_opacity").empty()) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 909b77aa6b..cef7ff0064 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -652,8 +652,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ return; } - if (wxGetApp().app_config->get("print_status_window_enabled") != "true" || - wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { + if (wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { event.Skip(); return; } @@ -980,9 +979,8 @@ bool MainFrame::ensure_close_to_tray_icon() void MainFrame::minimize_to_tray() { if (wxGetApp().app_config && - wxGetApp().app_config->get("print_status_window_enabled") == "true" && wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") == "true") { - show_print_status_frame(true); + show_print_status_frame(); } if (m_settings_dialog.IsShown()) @@ -1016,14 +1014,110 @@ void MainFrame::restore_from_tray() {} void MainFrame::remove_close_to_tray_icon() {} #endif -void MainFrame::show_print_status_frame(bool respect_enabled) +PrintStatusFrame* MainFrame::ensure_primary_print_status_frame() { - if (respect_enabled && (!wxGetApp().app_config || wxGetApp().app_config->get("print_status_window_enabled") != "true")) + if (!m_primary_print_status_frame) + m_primary_print_status_frame = std::make_unique(this, std::string(), true); + return m_primary_print_status_frame.get(); +} + +PrintStatusFrame* MainFrame::find_print_status_frame_for_device(const std::string& dev_id) const +{ + if (dev_id.empty()) + return nullptr; + + if (m_primary_print_status_frame && m_primary_print_status_frame->selected_device_id() == dev_id) + return m_primary_print_status_frame.get(); + + for (const auto& frame : m_secondary_print_status_frames) { + if (frame && frame->selected_device_id() == dev_id) + return frame.get(); + } + + return nullptr; +} + +void MainFrame::refresh_print_status_frames() +{ + if (m_primary_print_status_frame) + m_primary_print_status_frame->refresh_from_preferences(); + + for (const auto& frame : m_secondary_print_status_frames) { + if (frame) + frame->refresh_from_preferences(); + } +} + +std::string MainFrame::pick_additional_print_status_device(const std::string& preferred_dev_id) const +{ + auto* dev = wxGetApp().getDeviceManager(); + if (dev == nullptr) + return preferred_dev_id; + + std::vector machine_ids; + const auto machines = dev->get_my_machine_list(); + machine_ids.reserve(machines.size()); + for (const auto& item : machines) { + if (item.second != nullptr) + machine_ids.emplace_back(item.first); + } + + std::vector occupied_ids; + occupied_ids.reserve(1 + m_secondary_print_status_frames.size()); + if (m_primary_print_status_frame && m_primary_print_status_frame->IsShown()) { + const auto primary_id = m_primary_print_status_frame->selected_device_id(); + if (!primary_id.empty()) + occupied_ids.emplace_back(primary_id); + } + for (const auto& frame : m_secondary_print_status_frames) { + if (!frame || !frame->IsShown()) + continue; + const auto frame_id = frame->selected_device_id(); + if (!frame_id.empty()) + occupied_ids.emplace_back(frame_id); + } + + for (const auto& machine_id : machine_ids) { + if (std::find(occupied_ids.begin(), occupied_ids.end(), machine_id) == occupied_ids.end()) + return machine_id; + } + + if (!preferred_dev_id.empty()) + return preferred_dev_id; + + if (auto* selected = dev->get_selected_machine()) + return selected->get_dev_id(); + + if (!machine_ids.empty()) + return machine_ids.front(); + + return {}; +} + +void MainFrame::place_additional_print_status_frame(PrintStatusFrame* frame) const +{ + if (frame == nullptr) return; - if (!m_print_status_frame) - m_print_status_frame = std::make_unique(this); - m_print_status_frame->show_window(); + wxPoint base_position = GetPosition() + wxPoint(FromDIP(40), FromDIP(80)); + if (!m_secondary_print_status_frames.empty()) { + for (auto it = m_secondary_print_status_frames.rbegin(); it != m_secondary_print_status_frames.rend(); ++it) { + if (*it) { + base_position = (*it)->GetPosition(); + break; + } + } + } else if (m_primary_print_status_frame) { + base_position = m_primary_print_status_frame->GetPosition(); + } + + frame->SetPosition(base_position + wxPoint(FromDIP(24), FromDIP(24))); +} + +void MainFrame::show_print_status_frame() +{ + if (auto* frame = ensure_primary_print_status_frame()) + frame->show_window(); } void MainFrame::show_print_status_frame_safe_on_minimize() @@ -1031,25 +1125,63 @@ void MainFrame::show_print_status_frame_safe_on_minimize() if (m_real_shutdown_requested || IsBeingDeleted() || wxGetApp().app_config == nullptr) return; - if (wxGetApp().app_config->get("print_status_window_enabled") != "true" || - wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { + if (wxGetApp().app_config->get("print_status_window_auto_show_on_minimize") != "true") { + return; + } + + if (auto* frame = ensure_primary_print_status_frame()) + frame->show_window_safe_on_minimize(); +} + +void MainFrame::open_print_status_frame_for_device(const std::string& dev_id) +{ + if (dev_id.empty()) { + show_print_status_frame(); + return; + } + + if (auto* frame = find_print_status_frame_for_device(dev_id)) { + frame->show_window(); return; } - if (!m_print_status_frame) - m_print_status_frame = std::make_unique(this); + auto frame = std::make_unique(this, dev_id, false); + auto* raw_frame = frame.get(); + place_additional_print_status_frame(raw_frame); + m_secondary_print_status_frames.emplace_back(std::move(frame)); + raw_frame->show_window(); +} - if (m_print_status_frame) - m_print_status_frame->show_window_safe_on_minimize(); +void MainFrame::open_additional_print_status_frame(const std::string& preferred_dev_id) +{ + const std::string target_dev_id = pick_additional_print_status_device(preferred_dev_id); + if (auto* existing_frame = find_print_status_frame_for_device(target_dev_id)) { + if (!existing_frame->IsShown()) { + existing_frame->show_window(); + return; + } + } + + auto frame = std::make_unique(this, target_dev_id, false); + auto* raw_frame = frame.get(); + place_additional_print_status_frame(raw_frame); + m_secondary_print_status_frames.emplace_back(std::move(frame)); + raw_frame->show_window(); } void MainFrame::destroy_print_status_frame() { - if (!m_print_status_frame) - return; + if (m_primary_print_status_frame) { + m_primary_print_status_frame->destroy_for_shutdown(); + m_primary_print_status_frame.release(); + } - m_print_status_frame->destroy_for_shutdown(); - m_print_status_frame.release(); + for (auto& frame : m_secondary_print_status_frames) { + if (frame) + frame->destroy_for_shutdown(); + frame.release(); + } + m_secondary_print_status_frames.clear(); } //BBS GUI refactor: remove unused layout new/dlg @@ -2767,8 +2899,7 @@ void MainFrame::on_sys_color_changed() WebView::RecreateAll(); - if (m_print_status_frame) - m_print_status_frame->refresh_from_preferences(); + refresh_print_status_frames(); this->Refresh(); } @@ -4553,8 +4684,7 @@ void MainFrame::update_ui_from_settings() m_plater->update_ui_from_settings(); for (auto tab: wxGetApp().tabs_list) tab->update_ui_from_settings(); - if (m_print_status_frame) - m_print_status_frame->refresh_from_preferences(); + refresh_print_status_frames(); } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index f368cecd0c..960b5eaf43 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -114,7 +114,8 @@ class MainFrame : public DPIFrame #endif wxMenuItem* m_menu_item_reslice_now { nullptr }; wxSizer* m_main_sizer{ nullptr }; - std::unique_ptr m_print_status_frame; + std::unique_ptr m_primary_print_status_frame; + std::vector> m_secondary_print_status_frames; size_t m_last_selected_tab; @@ -299,8 +300,10 @@ class MainFrame : public DPIFrame void show_log_window(); void request_app_exit(bool force = false); void set_real_shutdown_requested(bool requested = true) { m_real_shutdown_requested = requested; } - void show_print_status_frame(bool respect_enabled = false); + void show_print_status_frame(); void show_print_status_frame_safe_on_minimize(); + void open_print_status_frame_for_device(const std::string& dev_id); + void open_additional_print_status_frame(const std::string& preferred_dev_id = {}); void destroy_print_status_frame(); void update_ui_from_settings(); @@ -434,6 +437,11 @@ class MainFrame : public DPIFrame void minimize_to_tray(); void restore_from_tray(); void remove_close_to_tray_icon(); + PrintStatusFrame* ensure_primary_print_status_frame(); + PrintStatusFrame* find_print_status_frame_for_device(const std::string& dev_id) const; + void refresh_print_status_frames(); + std::string pick_additional_print_status_device(const std::string& preferred_dev_id) const; + void place_additional_print_status_frame(PrintStatusFrame* frame) const; void update_side_button_style(); void update_slice_print_status(SlicePrintEventType event, bool can_slice = true, bool can_print = true); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index fe1403f07d..d179a030fd 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1453,7 +1453,6 @@ wxWindow* PreferencesDialog::create_general_page() auto item_auto_stop_liveview = create_item_checkbox(_L("Keep liveview when printing."), page, _L("By default, Liveview will pause after 15 minutes of inactivity on the computer. Check this box to disable this feature during printing."), 50, "auto_stop_liveview"); auto title_print_status_window = create_item_title(_L("Print Status Window"), page, _L("Print Status Window")); - auto item_print_status_window_enabled = create_item_checkbox(_L("Enable print status window"), page, _L("Enable the floating print status window feature."), 50, "print_status_window_enabled"); auto item_print_status_window_auto_show = create_item_checkbox(_L("Auto show on minimize"), page, _L("Automatically show the print status window when the main window is minimized."), 50, "print_status_window_auto_show_on_minimize"); #ifndef __APPLE__ auto item_print_status_window_close_to_tray = create_item_checkbox(_L("Minimize to system tray when closing the main window"), page, _L("When enabled, closing the main window with the window close button keeps Bambu Studio running in the system tray."), 50, "print_status_window_close_to_tray"); @@ -1574,7 +1573,6 @@ wxWindow* PreferencesDialog::create_general_page() sizer_page->Add(item_auto_stop_liveview, 0, wxEXPAND, FromDIP(3)); sizer_page->Add(title_print_status_window, 0, wxTOP | wxEXPAND, FromDIP(20)); - sizer_page->Add(item_print_status_window_enabled, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_print_status_window_auto_show, 0, wxTOP, FromDIP(3)); #ifndef __APPLE__ sizer_page->Add(item_print_status_window_close_to_tray, 0, wxTOP, FromDIP(3)); diff --git a/src/slic3r/GUI/PrintStatusFrame.cpp b/src/slic3r/GUI/PrintStatusFrame.cpp index 7a9bcd672d..42b3489c42 100644 --- a/src/slic3r/GUI/PrintStatusFrame.cpp +++ b/src/slic3r/GUI/PrintStatusFrame.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -24,6 +25,7 @@ #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevHMS.h" #include "DeviceCore/DevManager.h" +#include "Widgets/Button.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Utils.hpp" @@ -199,7 +201,7 @@ void ensure_min_text_width(wxWindow* window, const wxFont& font, const wxString& } // namespace -PrintStatusFrame::PrintStatusFrame(MainFrame* parent) +PrintStatusFrame::PrintStatusFrame(MainFrame* parent, std::string initial_dev_id, bool is_primary_window) : wxFrame(parent, wxID_ANY, _L("Print Status Window"), @@ -207,7 +209,9 @@ PrintStatusFrame::PrintStatusFrame(MainFrame* parent) wxDefaultSize, (wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)) | wxFRAME_TOOL_WINDOW), m_mainframe(parent), - m_refresh_timer(this) + m_refresh_timer(this), + m_selected_dev_id(std::move(initial_dev_id)), + m_is_primary_window(is_primary_window) { build_ui(); Bind(wxEVT_TIMER, &PrintStatusFrame::on_timer, this); @@ -303,6 +307,16 @@ void PrintStatusFrame::build_ui() m_printer_choice->SetMinSize(wxSize(FromDIP(220), FromDIP(22))); header_sizer->Add(m_printer_choice, 1, wxEXPAND, 0); + m_new_window_button = new Button(m_header_panel, _L("+")); + m_new_window_button->SetMinSize(wxSize(FromDIP(26), FromDIP(26))); + m_new_window_button->SetToolTip(_L("Open another print status window")); + m_new_window_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + if (m_mainframe) + m_mainframe->open_additional_print_status_frame(m_selected_dev_id); + event.Skip(); + }); + header_sizer->Add(m_new_window_button, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(8)); + m_badge_panel = new wxPanel(m_header_panel, wxID_ANY); auto* badge_sizer = new wxBoxSizer(wxHORIZONTAL); m_badge_label = new wxStaticText(m_badge_panel, wxID_ANY, _L("Idle")); @@ -423,8 +437,7 @@ PrintStatusFrame::Config PrintStatusFrame::load_config() const { Config config; auto* app_config = wxGetApp().app_config; - config.enabled = cfg_bool(app_config, "print_status_window_enabled", false); - config.always_on_top = cfg_bool(app_config, "print_status_window_always_on_top", false); + config.always_on_top = cfg_bool(app_config, "print_status_window_always_on_top", true); config.remember_position = cfg_bool(app_config, "print_status_window_remember_position", true); config.theme = app_config ? app_config->get("print_status_window_theme") : "follow_app"; if (config.theme.empty()) @@ -515,6 +528,11 @@ void PrintStatusFrame::apply_theme(const Config& config) m_printer_choice->SetForegroundColour(m_palette.text_primary); m_printer_choice->SetToolTip(_L("Printer")); } + if (m_new_window_button) { + m_new_window_button->SetBackgroundColorNormal(m_palette.input_background); + m_new_window_button->SetBorderColorNormal(m_palette.border); + m_new_window_button->SetTextColorNormal(m_palette.text_primary); + } if (m_job_label) m_job_label->SetForegroundColour(m_palette.text_primary); @@ -583,7 +601,7 @@ void PrintStatusFrame::apply_window_flags(const Config& config) void PrintStatusFrame::apply_geometry(const Config& config) { - if (!config.remember_position) + if (!m_is_primary_window || !config.remember_position) return; if (!config.has_position) @@ -602,7 +620,7 @@ void PrintStatusFrame::persist_geometry(bool save_immediately) { const auto config = load_config(); auto* app_config = wxGetApp().app_config; - if (app_config == nullptr || !config.remember_position || IsIconized()) + if (app_config == nullptr || !m_is_primary_window || !config.remember_position || IsIconized()) return; const wxPoint position = GetPosition(); @@ -644,6 +662,8 @@ void PrintStatusFrame::sync_printer_selector() m_updating_printer_choice = false; m_printer_choice_signature = empty_signature; } + if (m_new_window_button) + m_new_window_button->Enable(false); return; } @@ -683,11 +703,15 @@ void PrintStatusFrame::sync_printer_selector() m_printer_choice->Enable(machine_ids.size() > 1); m_updating_printer_choice = false; m_printer_choice_signature = signature; + if (m_new_window_button) + m_new_window_button->Enable(machine_ids.size() > 1); return; } m_choice_dev_ids = machine_ids; m_printer_choice->Enable(machine_ids.size() > 1); + if (m_new_window_button) + m_new_window_button->Enable(machine_ids.size() > 1); const auto it = std::find(m_choice_dev_ids.begin(), m_choice_dev_ids.end(), m_selected_dev_id); if (it == m_choice_dev_ids.end()) @@ -723,6 +747,10 @@ void PrintStatusFrame::update_header(MachineObject* obj, bool online, const wxSt { if (m_printer_choice) m_printer_choice->SetToolTip(obj != nullptr ? build_machine_name(obj) : _L("No printer available")); + if (obj != nullptr) + SetTitle(_L("Print Status Window") + " - " + build_machine_name(obj)); + else + SetTitle(_L("Print Status Window")); apply_badge_style(build_badge_text(obj, online, warning_text, has_active_print), build_badge_tone(obj, online, warning_text, has_active_print)); } diff --git a/src/slic3r/GUI/PrintStatusFrame.hpp b/src/slic3r/GUI/PrintStatusFrame.hpp index 5ccbd43275..fa282e1c91 100644 --- a/src/slic3r/GUI/PrintStatusFrame.hpp +++ b/src/slic3r/GUI/PrintStatusFrame.hpp @@ -13,6 +13,7 @@ class wxChoice; class wxFlexGridSizer; class wxPanel; class wxStaticText; +class Button; namespace Slic3r { @@ -26,13 +27,14 @@ class ProgressBarPanel; class PrintStatusFrame final : public wxFrame { public: - explicit PrintStatusFrame(MainFrame* parent); + explicit PrintStatusFrame(MainFrame* parent, std::string initial_dev_id = {}, bool is_primary_window = false); ~PrintStatusFrame() override; void show_window(); void show_window_safe_on_minimize(); void refresh_from_preferences(); void destroy_for_shutdown(); + const std::string& selected_device_id() const { return m_selected_dev_id; } private: enum class BadgeTone { @@ -43,8 +45,7 @@ class PrintStatusFrame final : public wxFrame }; struct Config { - bool enabled { false }; - bool always_on_top { false }; + bool always_on_top { true }; bool remember_position { true }; std::string theme { "follow_app" }; int opacity { 100 }; @@ -136,6 +137,7 @@ class PrintStatusFrame final : public wxFrame wxPanel* m_root_panel { nullptr }; wxPanel* m_header_panel { nullptr }; wxChoice* m_printer_choice { nullptr }; + Button* m_new_window_button { nullptr }; wxPanel* m_badge_panel { nullptr }; wxStaticText* m_badge_label { nullptr }; wxStaticText* m_job_label { nullptr }; @@ -164,6 +166,7 @@ class PrintStatusFrame final : public wxFrame bool m_safe_show_pending { false }; bool m_ignore_geometry_events { false }; bool m_updating_printer_choice { false }; + bool m_is_primary_window { false }; }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 4b78b288ed..cbbb4559ef 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -630,6 +630,12 @@ void PrintingTaskPanel::create_panel(wxWindow *parent) m_button_abort->Bind(wxEVT_LEAVE_WINDOW, [this](auto &e) { m_button_abort->SetBitmap_("print_control_stop"); }); + m_button_open_status_window = new Button(progress_lr_panel, wxEmptyString, "print_status_window", 0, 20, wxID_ANY); + m_button_open_status_window->SetMinSize(wxSize(FromDIP(32), FromDIP(24))); + m_button_open_status_window->SetBackgroundColor(white_bg); + m_button_open_status_window->SetBorderColor(*wxWHITE); + m_button_open_status_window->SetToolTip(_L("Open print status window for this printer")); + wxBoxSizer *bSizer_buttons = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *bSizer_text = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *bSizer_finish_time = new wxBoxSizer(wxHORIZONTAL); @@ -802,6 +808,8 @@ void PrintingTaskPanel::create_panel(wxWindow *parent) progress_right_sizer->Add(m_stopping_icon, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(0)); progress_right_sizer->Add(m_button_abort, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(0)); progress_right_sizer->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(18)); + progress_right_sizer->Add(m_button_open_status_window, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(0)); + progress_right_sizer->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(18)); progress_lr_sizer->Add(progress_left_sizer, 1, wxEXPAND | wxALL, 0); progress_lr_sizer->Add(progress_right_sizer, 0, wxEXPAND | wxALL, 0); @@ -2443,6 +2451,7 @@ StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, co m_project_task_panel->get_partskip_button()->Connect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_subtask_partskip), NULL, this); m_project_task_panel->get_pause_resume_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); m_project_task_panel->get_abort_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); + m_project_task_panel->get_open_status_window_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_open_print_status_window), NULL, this); m_project_task_panel->get_market_scoring_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); m_project_task_panel->get_market_retry_buttom()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); m_project_task_panel->get_clean_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); @@ -2508,6 +2517,7 @@ StatusPanel::~StatusPanel() m_project_task_panel->get_partskip_button()->Disconnect(wxEVT_LEFT_DOWN, wxCommandEventHandler(StatusPanel::on_subtask_partskip), NULL, this); m_project_task_panel->get_pause_resume_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); m_project_task_panel->get_abort_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); + m_project_task_panel->get_open_status_window_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_open_print_status_window), NULL, this); m_project_task_panel->get_market_scoring_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); m_project_task_panel->get_clean_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); @@ -2665,6 +2675,20 @@ void StatusPanel::on_subtask_partskip(wxCommandEvent &event) } } +void StatusPanel::on_open_print_status_window(wxCommandEvent& event) +{ + if (wxGetApp().mainframe == nullptr) { + event.Skip(); + return; + } + + if (obj != nullptr) + wxGetApp().mainframe->open_print_status_frame_for_device(obj->get_dev_id()); + else + wxGetApp().mainframe->show_print_status_frame(); + event.Skip(); +} + void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event) { if (obj) { diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 3bd970aa85..87fc938b2a 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -317,6 +317,7 @@ class PrintingTaskPanel : public wxPanel AnimaIcon* m_stopping_icon; ScalableButton* m_button_pause_resume; ScalableButton* m_button_abort; + Button* m_button_open_status_window { nullptr }; Button* m_button_partskip; Button* m_button_market_scoring; Button* m_button_clean; @@ -378,6 +379,7 @@ class PrintingTaskPanel : public wxPanel public: ScalableButton* get_abort_button() {return m_button_abort;}; ScalableButton* get_pause_resume_button() {return m_button_pause_resume;}; + Button* get_open_status_window_button() { return m_button_open_status_window; }; Button* get_partskip_button() { return m_button_partskip; }; Button* get_market_scoring_button() {return m_button_market_scoring;}; Button * get_market_retry_buttom() { return m_button_market_retry; }; @@ -703,6 +705,7 @@ class StatusPanel : public StatusBasePanel void on_market_scoring(wxCommandEvent &event); void on_market_retry(wxCommandEvent &event); + void on_open_print_status_window(wxCommandEvent& event); void on_subtask_partskip(wxCommandEvent &event); void on_subtask_pause_resume(wxCommandEvent &event); void on_subtask_abort(wxCommandEvent &event);