Files
OrcaSlicer/src/slic3r/GUI/FilamentGroupPopup.cpp
T
HanifKoh a671bf605d Dismiss the Slice-Plate Popup on Every Path Into the Filament Grouping Dialog (#16359)
#16284 closed the Slice-plate hover popup only when the Slice button opened
the filament grouping dialog. The dialog also opens from the plate's filament
grouping icon, the preview legend, slicing notification links and the send
dialog, and a popup still shown on those paths keeps the mouse capture on
macOS and starves the modal dialog of clicks. Dismiss it in
FilamentMapDialog::ShowModal() instead, so every path is covered.

The popup could also stay up on its own on macOS: an enter event stopped its
close timer, and when the timer fired with the cursor inside it returned
without re-arming, so without a later leave event the popup never closed. On
macOS the timer now keeps running while the popup is shown and closes it once
the cursor is outside. The popup also stops and frees its timer when destroyed.
2026-10-11 15:48:49 +08:00

518 lines
20 KiB
C++

#include "FilamentGroupPopup.hpp"
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "wx/dcgraph.h"
#include "I18N.hpp"
#include "PartPlate.hpp"
#include <wx/colour.h>
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
#include <map>
#include <string>
#include <boost/log/trivial.hpp>
#include <vector>
#include <wx/utils.h>
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include <wx/string.h>
#include <wx/popupwin.h>
#include <cstddef>
#include <wx/gdicmn.h>
#include "slic3r/GUI/Widgets/Label.hpp"
#include <wx/event.h>
#include <wx/timer.h>
#include <wx/window.h>
#include <wx/sizer.h>
#include "slic3r/GUI/Widgets/StaticBox.hpp"
#include <utility>
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include <wx/tglbtn.h>
namespace Slic3r { namespace GUI {
static const wxColour LabelEnableColor = wxColour("#262E30");
static const wxColour LabelDisableColor = wxColour("#ACACAC");
static const wxColour GreyColor = wxColour("#6B6B6B");
static const wxColour BackGroundColor = wxColour("#FFFFFF");
static bool should_pop_up()
{
const auto &preset_bundle = wxGetApp().preset_bundle;
if (!preset_bundle->is_bbl_vendor()) return false;
const auto &full_config = preset_bundle->full_config();
const auto nozzle_diameters = full_config.option<ConfigOptionFloats>("nozzle_diameter");
return nozzle_diameters->size() > 1;
}
static FilamentMapMode get_prefered_map_mode()
{
const static std::map<std::string, int> enum_keys_map = ConfigOptionEnum<FilamentMapMode>::get_enum_values();
auto &app_config = wxGetApp().app_config;
std::string mode_str = app_config->get("prefered_filament_map_mode");
auto iter = enum_keys_map.find(mode_str);
if (iter == enum_keys_map.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Could not get prefered_filament_map_mode from app config, use AutoForFlsuh mode");
return FilamentMapMode::fmmAutoForFlush;
}
return FilamentMapMode(iter->second);
}
static void set_prefered_map_mode(FilamentMapMode mode)
{
const static std::vector<std::string> enum_values = ConfigOptionEnum<FilamentMapMode>::get_enum_names();
auto &app_config = wxGetApp().app_config;
std::string mode_str;
if (mode < enum_values.size()) mode_str = enum_values[mode];
if (mode_str.empty()) BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Set empty prefered_filament_map_mode to app config");
app_config->set("prefered_filament_map_mode", mode_str);
}
bool play_dual_extruder_slice_video()
{
if (wxLaunchDefaultBrowser("https://e.bambulab.com/t?c=HDB24RlwSmt77YFH")) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Video is being played using the system's default browser.");
return true;
}
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("launch system's default browser failed");
return false;
}
bool play_dual_extruder_print_tpu_video()
{
const wxString video_url = "https://e.bambulab.com/t?c=fwWqpBg37Liel92N";
if (wxLaunchDefaultBrowser(video_url)){
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Print Tpu Video is being played using the system's default browser.");
return true;
}
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("launch system's default browser failed");
return false;
}
bool open_filament_group_wiki()
{
if (wxLaunchDefaultBrowser("https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9")) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Wiki is being displayed using the system's default browser.");
return true;
}
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format("launch system's default browser failed");
return false;
}
void FilamentGroupPopup::CreateBmps()
{
checked_bmp = create_scaled_bitmap("radio_on", nullptr, 16);; // ORCA match icons
unchecked_bmp = create_scaled_bitmap("radio_off", nullptr, 16);
disabled_bmp = create_scaled_bitmap("radio_disabled", nullptr, 16);
checked_hover_bmp = create_scaled_bitmap("radio_on_hover", nullptr, 16);
unchecked_hover_bmp = create_scaled_bitmap("radio_off_hover", nullptr, 16);
}
FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS)
{
const wxString AutoForFlushLabel = _L("Filament-Saving Mode");
const wxString AutoForMatchLabel = _L("Convenience Mode");
const wxString ManualLabel = _L("Custom Mode");
const wxString AutoForFlushDetail = _L("Generates filament grouping for the left and right nozzles based on the most filament-saving principles to minimize waste.");
const wxString AutoForMatchDetail = _L("Generates filament grouping for the left and right nozzles based on the printer's actual filament status, reducing the need for manual filament adjustment.");
const wxString ManualDetail = _L("Manually assign filament to the left or right nozzle");
const wxString AutoForFlushDesp = ""; //_L("(Post-slicing arrangement)");
const wxString ManualDesp = "";
const wxString AutoForMatchDesp = "";// _L("(Pre-slicing arrangement)");
wxBoxSizer *top_sizer = new wxBoxSizer(wxVERTICAL);
const int horizontal_margin = FromDIP(16);
const int vertical_margin = FromDIP(15);
const int vertical_padding = FromDIP(12);
const int ratio_spacing = FromDIP(4);
SetBackgroundColour(BackGroundColor);
radio_btns.resize(ButtonType::btCount);
button_labels.resize(ButtonType::btCount);
button_desps.resize(ButtonType::btCount);
detail_infos.resize(ButtonType::btCount);
//global_mode_tags.resize(ButtonType::btCount);
std::vector<wxString> btn_texts = {AutoForFlushLabel, AutoForMatchLabel, ManualLabel};
std::vector<wxString> btn_desps = {AutoForFlushDesp, AutoForMatchDesp, ManualDesp};
std::vector<wxString> mode_details = {AutoForFlushDetail, AutoForMatchDetail, ManualDetail};
top_sizer->AddSpacer(vertical_margin);
CreateBmps();
for (size_t idx = 0; idx < ButtonType::btCount; ++idx) {
wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
radio_btns[idx] = new wxBitmapButton(this, wxID_ANY, unchecked_bmp, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
radio_btns[idx]->SetBackgroundColour(BackGroundColor);
button_labels[idx] = new Label(this, btn_texts[idx]);
button_labels[idx]->SetBackgroundColour(BackGroundColor);
button_labels[idx]->SetForegroundColour(LabelEnableColor);
button_labels[idx]->SetFont(Label::Body_14);
button_desps[idx] = new Label(this, btn_desps[idx]);
button_desps[idx]->SetBackgroundColour(BackGroundColor);
button_desps[idx]->SetForegroundColour(LabelEnableColor);
button_desps[idx]->SetFont(Label::Body_14);
#if 0
global_mode_tags[idx] = new wxBitmapButton(this, wxID_ANY, global_tag_bmp, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
global_mode_tags[idx]->SetBackgroundColour(BackGroundColor);
global_mode_tags[idx]->SetToolTip(_L("Global settings"));
#endif
button_sizer->Add(radio_btns[idx], 0, wxALIGN_CENTER);
button_sizer->AddSpacer(ratio_spacing);
button_sizer->Add(button_labels[idx], 0, wxALIGN_CENTER);
button_sizer->Add(button_desps[idx], 0, wxALIGN_CENTER);
//button_sizer->AddSpacer(ratio_spacing);
//button_sizer->Add(global_mode_tags[idx], 0, wxALIGN_CENTER);
wxBoxSizer *label_sizer = new wxBoxSizer(wxHORIZONTAL);
detail_infos[idx] = new Label(this, mode_details[idx]);
detail_infos[idx]->SetBackgroundColour(BackGroundColor);
detail_infos[idx]->SetForegroundColour(GreyColor);
detail_infos[idx]->SetFont(Label::Body_12);
detail_infos[idx]->Wrap(FromDIP(320));
label_sizer->AddSpacer(radio_btns[idx]->GetRect().width + ratio_spacing);
label_sizer->Add(detail_infos[idx], 1, wxALIGN_CENTER_VERTICAL);
top_sizer->Add(button_sizer, 0, wxLEFT | wxRIGHT, horizontal_margin);
top_sizer->Add(label_sizer, 0, wxLEFT | wxRIGHT, horizontal_margin);
top_sizer->AddSpacer(vertical_padding);
radio_btns[idx]->Bind(wxEVT_LEFT_DOWN, [this, idx](auto &) { OnRadioBtn(idx);});
radio_btns[idx]->Bind(wxEVT_ENTER_WINDOW, [this, idx](auto &) { UpdateButtonStatus(idx); });
radio_btns[idx]->Bind(wxEVT_LEAVE_WINDOW, [this](auto &) { UpdateButtonStatus(); });
button_labels[idx]->Bind(wxEVT_LEFT_DOWN, [this, idx](auto &) { OnRadioBtn(idx);});
button_labels[idx]->Bind(wxEVT_ENTER_WINDOW, [this, idx](auto &) { UpdateButtonStatus(idx); });
button_labels[idx]->Bind(wxEVT_LEAVE_WINDOW, [this](auto &) { UpdateButtonStatus(); });
}
// Smart filament assign section
MakeSmartFilamentSection(top_sizer, horizontal_margin, vertical_padding);
{
wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
// ORCA Unified hyperlinks
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + "https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
wiki_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
wiki_btn->SetCanFocus(false);
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {open_filament_group_wiki();}));
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
video_btn->SetToolTip(_L("Video Guide"));
video_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
video_btn->SetCanFocus(false);
video_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {
play_dual_extruder_slice_video();
wxGetApp().app_config->set("play_slicing_video", "false");
}));
button_sizer->Add(wiki_btn , 0, wxLEFT, horizontal_margin + FromDIP(3));
button_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
top_sizer->Add(button_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin);
}
top_sizer->AddSpacer(vertical_margin);
SetSizerAndFit(top_sizer);
m_mode = get_prefered_map_mode();
m_timer = new wxTimer(this);
GUI::wxGetApp().UpdateDarkUIWin(this);
Bind(wxEVT_PAINT, &FilamentGroupPopup::OnPaint, this);
Bind(wxEVT_TIMER, &FilamentGroupPopup::OnTimer, this);
Bind(wxEVT_ENTER_WINDOW, &FilamentGroupPopup::OnEnterWindow, this);
Bind(wxEVT_LEAVE_WINDOW, &FilamentGroupPopup::OnLeaveWindow, this);
}
void FilamentGroupPopup::DrawRoundedCorner(int radius)
{
#ifdef __WIN32__
HWND hwnd = GetHWND();
if (hwnd) {
HRGN hrgn = CreateRoundRectRgn(0, 0, GetRect().GetWidth(), GetRect().GetHeight(), radius, radius);
SetWindowRgn(hwnd, hrgn, FALSE);
SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
SetLayeredWindowAttributes(hwnd, 0, 0, LWA_COLORKEY);
}
#endif
}
void FilamentGroupPopup::Init()
{
static bool is_dark_mode = wxGetApp().dark_mode();
if (is_dark_mode != wxGetApp().dark_mode()) {
CreateBmps();
is_dark_mode = wxGetApp().dark_mode();
}
const wxString AutoForMatchDesp = "";// _L("(Pre-slicing arrangement)");
const wxString MachineSyncTip = _L("(Sync with printer)");
if (m_connected) {
button_labels[ButtonType::btForMatch]->SetForegroundColour(LabelEnableColor);
button_desps[ButtonType::btForMatch]->SetForegroundColour(LabelEnableColor);
detail_infos[ButtonType::btForMatch]->SetForegroundColour(GreyColor);
radio_btns[ButtonType::btForMatch]->SetBitmap(unchecked_bmp);
button_desps[ButtonType::btForMatch]->SetLabel(AutoForMatchDesp);
}
else {
button_labels[ButtonType::btForMatch]->SetForegroundColour(LabelDisableColor);
button_desps[ButtonType::btForMatch]->SetForegroundColour(LabelDisableColor);
detail_infos[ButtonType::btForMatch]->SetForegroundColour(LabelDisableColor);
radio_btns[ButtonType::btForMatch]->SetBitmap(disabled_bmp);
button_desps[ButtonType::btForMatch]->SetLabel(MachineSyncTip);
}
m_mode = GetFilamentMapMode();
if (m_mode == fmmAutoForMatch && !m_connected) {
SetFilamentMapMode(fmmAutoForFlush);
m_mode = fmmAutoForFlush;
}
else if (m_slice_all) {
// reset the filament map mode in slice all mode
SetFilamentMapMode(m_mode);
}
UpdateSmartFilamentSection();
UpdateButtonStatus();
GUI::wxGetApp().UpdateDarkUIWin(this);
}
void FilamentGroupPopup::tryPopup(Plater* plater,PartPlate* partplate,bool slice_all)
{
if (should_pop_up()) {
bool connect_status = plater->get_machine_sync_status();
this->partplate_ref = partplate;
this->plater_ref = plater;
this->m_sync_plate = true;
this->m_slice_all = slice_all;
if (m_active) {
if (m_connected != connect_status) { Init(); }
m_connected = connect_status;
ResetTimer();
}
else {
m_connected = connect_status;
m_active = true;
Init();
ResetTimer();
DrawRoundedCorner(16);
PopupWindow::Popup();
}
}
}
FilamentMapMode FilamentGroupPopup::GetFilamentMapMode() const
{
const auto& proj_config = wxGetApp().preset_bundle->project_config;
if (m_sync_plate)
return partplate_ref->get_real_filament_map_mode(proj_config);
return plater_ref->get_global_filament_map_mode();
}
void FilamentGroupPopup::SetFilamentMapMode(const FilamentMapMode mode)
{
if (m_sync_plate) {
if (m_slice_all) {
auto plate_list = plater_ref->get_partplate_list().get_plate_list();
for (int i = 0; i < plate_list.size(); ++i) {
plate_list[i]->set_filament_map_mode(mode);
}
}
else {
partplate_ref->set_filament_map_mode(mode);
}
return;
}
plater_ref->set_global_filament_map_mode(mode);
}
void FilamentGroupPopup::tryClose() { StartTimer(); }
void FilamentGroupPopup::OnPaint(wxPaintEvent&)
{
DrawRoundedCorner(16);
}
void FilamentGroupPopup::StartTimer() { m_timer->StartOnce(300); }
void FilamentGroupPopup::ResetTimer()
{
if (m_timer->IsRunning()) { m_timer->Stop(); }
}
void FilamentGroupPopup::OnRadioBtn(int idx)
{
if (mode_list.at(idx) == FilamentMapMode::fmmAutoForMatch && !m_connected)
return;
if (m_mode != mode_list.at(idx)) {
m_mode = mode_list.at(idx);
SetFilamentMapMode(m_mode);
plater_ref->update();
UpdateButtonStatus(m_mode);
}
}
void FilamentGroupPopup::OnTimer(wxTimerEvent &event)
{
#if __APPLE__
// On macOS, when moving cursor from slice button to this popup window,
// the popup window entering event is triggered first, then the slice button
// leaving event got triggered. So the timer is stopped first, then started
// again, causing the popup being dismissed immediately.
// To fix this, we check if cursor is still inside the popup window before
// dismissing, and keep checking while it is: a leave event is not guaranteed
// to follow, and a popup left open keeps grabbing the mouse capture.
wxPoint pos = this->ScreenToClient(wxGetMousePosition());
if (this->GetClientRect().Contains(pos)) {
StartTimer();
return;
}
#endif
Dismiss();
}
FilamentGroupPopup::~FilamentGroupPopup()
{
m_timer->Stop();
delete m_timer;
}
void FilamentGroupPopup::Dismiss() {
m_active = false;
PopupWindow::Dismiss();
m_timer->Stop();
}
void FilamentGroupPopup::OnLeaveWindow(wxMouseEvent &)
{
wxPoint pos = this->ScreenToClient(wxGetMousePosition());
if (this->GetClientRect().Contains(pos)) return;
StartTimer();
}
void FilamentGroupPopup::OnEnterWindow(wxMouseEvent &)
{
// Ignore spurious ENTER synthesized by PopupWindow::OnMouseEvent2 on macOS.
wxPoint pos = this->ScreenToClient(wxGetMousePosition());
if (!this->GetClientRect().Contains(pos)) return;
// On macOS the timer keeps running instead, and OnTimer closes the popup once the cursor has left it.
#ifndef __APPLE__
ResetTimer();
#endif
}
void FilamentGroupPopup::UpdateButtonStatus(int hover_idx)
{
for (int i = 0; i < ButtonType::btCount; ++i) {
#if 0 // do not display global mode tag
if (mode_list.at(i) == global_mode)
global_mode_tags[i]->Show();
else
global_mode_tags[i]->Hide();
#endif
if (ButtonType::btForMatch == i && !m_connected) {
button_labels[i]->SetFont(Label::Body_14);
continue;
}
// process checked and unchecked status
if (mode_list.at(i) == m_mode) {
if (i == hover_idx)
radio_btns[i]->SetBitmap(checked_hover_bmp);
else
radio_btns[i]->SetBitmap(checked_bmp);
button_labels[i]->SetFont(Label::Head_14);
} else {
if (i == hover_idx)
radio_btns[i]->SetBitmap(unchecked_hover_bmp);
else
radio_btns[i]->SetBitmap(unchecked_bmp);
button_labels[i]->SetFont(Label::Body_14);
}
}
Layout();
Fit();
}
void FilamentGroupPopup::MakeSmartFilamentSection(wxSizer *top_sizer, int horizontal_margin, int vertical_padding)
{
m_smart_filament_panel = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
m_smart_filament_panel->SetCornerRadius(FromDIP(4));
m_smart_filament_panel->SetBorderWidth(FromDIP(1));
m_smart_filament_panel->SetBorderColor(wxColour("#CECECE"));
m_smart_filament_panel->SetBackgroundColor(StateColor(std::pair<wxColour, int>(wxColour("#F8F8F8"), StateColor::Normal)));
auto *label = new Label(m_smart_filament_panel, _L("Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings"));
label->SetFont(Label::Body_12);
label->SetForegroundColour(GreyColor);
label->SetBackgroundColour(wxColour("#F8F8F8"));
label->Wrap(FromDIP(240));
m_smart_filament_switch = new SwitchButton(m_smart_filament_panel);
m_smart_filament_switch->Bind(wxEVT_TOGGLEBUTTON, &FilamentGroupPopup::OnSmartFilamentToggle, this);
#ifdef __WXOSX__
// wxEVT_TOGGLEBUTTON event not handled well by PopupWindow on MacOS
// we bind a wxEVT_LEFT_DOWN event as a workaround
m_smart_filament_switch->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) {
wxCommandEvent evt(wxEVT_TOGGLEBUTTON);
evt.SetInt(!m_smart_filament_switch->GetValue());
m_smart_filament_switch->Command(evt);
});
#endif
auto *panel_sizer = new wxBoxSizer(wxHORIZONTAL);
panel_sizer->Add(label, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(10));
panel_sizer->Add(m_smart_filament_switch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(10));
m_smart_filament_panel->SetSizer(panel_sizer);
top_sizer->Add(m_smart_filament_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin);
m_smart_filament_spacer = top_sizer->AddSpacer(vertical_padding);
// Hidden by default; shown in Init() when the filament track switch is ready
m_smart_filament_panel->Show(false);
m_smart_filament_spacer->Show(false);
}
void FilamentGroupPopup::OnSmartFilamentToggle(wxCommandEvent &event)
{
auto &config = wxGetApp().preset_bundle->project_config;
auto *dynamic_filament = dynamic_cast<ConfigOptionBool *>(config.option("enable_filament_dynamic_map"));
if (dynamic_filament) { dynamic_filament->value = m_smart_filament_switch->GetValue(); }
plater_ref->update();
event.Skip();
}
void FilamentGroupPopup::UpdateSmartFilamentSection()
{
bool show = wxGetApp().sidebar().is_fila_switch_ready();
m_smart_filament_panel->Show(show);
m_smart_filament_spacer->Show(show);
if (show) {
auto &config = wxGetApp().preset_bundle->project_config;
auto *dynamic_filament = dynamic_cast<ConfigOptionBool *>(config.option("enable_filament_dynamic_map"));
if (dynamic_filament) { m_smart_filament_switch->SetValue(dynamic_filament->value); }
}
}
}} // namespace Slic3r::GUI