diff --git a/resources/images/folder-closed-active.svg b/resources/images/folder-closed-active.svg new file mode 100644 index 0000000000..074fdc83ab --- /dev/null +++ b/resources/images/folder-closed-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/folder-closed-hover.svg b/resources/images/folder-closed-hover.svg new file mode 100644 index 0000000000..bad1f4e2f7 --- /dev/null +++ b/resources/images/folder-closed-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/folder-closed.svg b/resources/images/folder-closed.svg new file mode 100644 index 0000000000..ab27d0c297 --- /dev/null +++ b/resources/images/folder-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index c409c84781..85f1638098 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -262,6 +262,7 @@ void PrintJob::process(Ctl &ctl) params.task_vibration_cali = this->task_vibration_cali; params.task_layer_inspect = this->task_layer_inspect; params.task_record_timelapse= this->task_record_timelapse; + params.task_timelapse_use_internal = this->task_timelapse_use_internal; params.nozzle_mapping = this->task_nozzle_mapping; params.ams_mapping = this->task_ams_mapping; params.ams_mapping2 = this->task_ams_mapping2; diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index d21e1f8184..9a23b04054 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -80,6 +80,7 @@ public: bool task_flow_cali; bool task_vibration_cali; bool task_record_timelapse; + bool task_timelapse_use_internal { false }; bool task_layer_inspect; bool cloud_print_only { false }; bool has_sdcard { false }; diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index fa48195d86..ee6558a822 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -14,6 +14,7 @@ #include "Widgets/StaticBox.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/Label.hpp" +#include "Widgets/RadioBox.hpp" #include "ConnectPrinter.hpp" #include "Jobs/BoostThreadWorker.hpp" #include "Jobs/PlaterWorker.hpp" @@ -544,6 +545,29 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) auto option_timelapse = new PrintOption(m_options_other, _L("Timelapse"), wxEmptyString, ops_no_auto, "timelapse"); + // timelapse storage location folder button (shown only when is_support_internal_timelapse) + m_timelapse_folder_btn = new ScalableButton(m_options_other, wxID_ANY, "folder-closed", wxEmptyString, + wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); + m_timelapse_folder_btn->SetBackgroundColour(*wxWHITE); + m_timelapse_folder_btn->SetToolTip(_L("Select timelapse storage location")); + m_timelapse_folder_btn->Hide(); + m_timelapse_folder_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + show_timelapse_folder_popup(); + }); + m_timelapse_folder_btn->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { + // hover: only switch if popup is not open (popup open = active state) + if (!m_timelapse_storage_popup || !m_timelapse_storage_popup->IsShown()) + m_timelapse_folder_btn->SetBitmap(create_scaled_bitmap("folder-closed-hover", m_timelapse_folder_btn, 16)); + e.Skip(); + }); + m_timelapse_folder_btn->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { + // restore normal (popup open state is handled separately) + if (!m_timelapse_storage_popup || !m_timelapse_storage_popup->IsShown()) + update_timelapse_folder_btn_icon(); + e.Skip(); + }); + option_timelapse->insert_extra_widget(m_timelapse_folder_btn); + auto option_auto_bed_level = new PrintOption( m_options_other, _L("Auto Bed Leveling"), _L("This checks the flatness of heatbed. Leveling makes extruded height uniform.\n*Automatic mode: Run a leveling check(about 10 seconds). Skip if surface is fine."), @@ -2657,6 +2681,16 @@ void SelectMachineDialog::update_option_opts(MachineObject *obj) /*timelapse*/ m_checkbox_list["timelapse"]->Show(); + if (obj->is_support_internal_timelapse) { + m_timelapse_folder_btn->Show(); + if (m_timelapse_storage.empty()) { + m_timelapse_storage = "internal"; + } + update_timelapse_folder_btn_icon(); + } else { + m_timelapse_folder_btn->Hide(); + m_timelapse_storage.clear(); + } /*bed_leveling*/ if (obj->is_support_bed_leveling == 2) { @@ -2792,6 +2826,99 @@ void SelectMachineDialog::Enable_Auto_Refill(bool enable) m_ams_backup_tip->Refresh(); } +void SelectMachineDialog::update_timelapse_folder_btn_icon() +{ + if (!m_timelapse_folder_btn) return; + // always restore to normal (grey) - active state is managed by popup open/close + m_timelapse_folder_btn->SetBitmap(create_scaled_bitmap("folder-closed", m_timelapse_folder_btn, 16)); + m_timelapse_folder_btn->Refresh(); +} + +void SelectMachineDialog::show_timelapse_folder_popup() +{ + if (m_timelapse_storage_popup && m_timelapse_storage_popup->IsShown()) { + m_timelapse_storage_popup->Dismiss(); + return; + } + + // build popup with rounded corners + light border + m_timelapse_storage_popup = new PopupWindow(this, wxBORDER_NONE); + m_timelapse_storage_popup->SetBackgroundColour(wxColour(0xF0, 0xF0, 0xF0)); + m_timelapse_storage_popup->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxPaintDC dc(m_timelapse_storage_popup); + auto size = m_timelapse_storage_popup->GetSize(); + dc.SetPen(wxPen(wxColour(0xCE, 0xCE, 0xCE))); + dc.SetBrush(wxBrush(wxColour(0xF0, 0xF0, 0xF0))); + dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(8)); + }); + + auto* panel = new wxPanel(m_timelapse_storage_popup, wxID_ANY); + panel->SetBackgroundColour(wxColour(0xF0, 0xF0, 0xF0)); + + // horizontal layout: [ Internal] [External] + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + DeviceManager* dev_popup = wxGetApp().getDeviceManager(); + MachineObject* obj_popup = dev_popup ? dev_popup->get_selected_machine() : nullptr; + bool has_sdcard = obj_popup && obj_popup->GetStorage()->get_sdcard_state() == DevStorage::SdcardState::HAS_SDCARD_NORMAL; + // if external was previously selected but sdcard is now absent, fall back to internal + if (!has_sdcard && m_timelapse_storage == "external") + m_timelapse_storage = "internal"; + + // Reuse the themed RadioBox widget (radio_on / radio_off bitmaps) instead of the + // native wxRadioButton: the selected state shows a clear filled dot with good + // contrast on every platform, matching the storage selector in SendToPrinter. + auto make_item = [&](const wxString& label, const std::string& val, bool enabled) { + auto* radio = new RadioBox(panel); + radio->SetValue(m_timelapse_storage == val); + if (enabled) radio->Enable(); else radio->Disable(); + + auto* text = new Label(panel, Label::Body_14, label); + text->SetForegroundColour(enabled ? wxColour(0x5C, 0x5C, 0x5C) : wxColour(0xAC, 0xAC, 0xAC)); + + if (enabled) { + auto on_select = [this, val](wxMouseEvent&) { + m_timelapse_storage = val; + update_timelapse_folder_btn_icon(); + if (m_timelapse_storage_popup) m_timelapse_storage_popup->Dismiss(); + }; + radio->Bind(wxEVT_LEFT_DOWN, on_select); + text->Bind(wxEVT_LEFT_DOWN, on_select); + } + + sizer->Add(radio, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(text, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + }; + + make_item(_L("Internal"), "internal", true); + sizer->AddSpacer(FromDIP(16)); + make_item(_L("External"), "external", has_sdcard); + + panel->SetSizer(sizer); + panel->Fit(); + + auto* outer = new wxBoxSizer(wxVERTICAL); + outer->Add(panel, 0, wxALL, FromDIP(10)); + m_timelapse_storage_popup->SetSizer(outer); + m_timelapse_storage_popup->Fit(); + + // restore normal icon when popup is dismissed + m_timelapse_storage_popup->Bind(wxEVT_SHOW, [this](wxShowEvent& e) { + if (!e.IsShown()) + update_timelapse_folder_btn_icon(); + e.Skip(); + }); + + wxPoint pos = m_timelapse_folder_btn->ClientToScreen(wxPoint(0, m_timelapse_folder_btn->GetSize().GetHeight())); + m_timelapse_storage_popup->Position(pos, wxSize(0, 0)); + + // switch to active icon before showing popup + m_timelapse_folder_btn->SetBitmap(create_scaled_bitmap("folder-closed-active", m_timelapse_folder_btn, 16)); + m_timelapse_folder_btn->Refresh(); + + m_timelapse_storage_popup->Popup(); +} + void SelectMachineDialog::on_send_print() { BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send"; @@ -2965,6 +3092,10 @@ void SelectMachineDialog::on_send_print() pa_manual_mode = (m_checkbox_list["pa_value"]->getValue() == "on") ? 0 : 1; } + if (timelapse_option && obj_->is_support_internal_timelapse && !m_timelapse_storage.empty()) { + m_print_job->task_timelapse_use_internal = (m_timelapse_storage == "internal"); + } + m_print_job->set_print_config( MachineBedTypeString[0], (m_checkbox_list["bed_leveling"]->getValue() == "on"), @@ -5774,6 +5905,18 @@ void PrintOption::msw_rescale() update_title_display(); } +void PrintOption::insert_extra_widget(wxWindow* widget) +{ + // insert after title (index 0), before tips (index 1) + // sizer layout: [title][tips][stretch][item] + // after insert: [title][widget][tips][stretch][item] + wxSizer* sizer = GetSizer(); + if (!sizer || !widget) return; + widget->Reparent(this); + sizer->Insert(1, widget, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + Layout(); +} + PrintOptionItem::PrintOptionItem(wxWindow* parent, std::vector ops, std::string param) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) { diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index 85d30be633..82889b7035 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -228,6 +228,7 @@ public: void update_options(std::vector ops, const wxString &tips); void update_tooltip(const wxString &tips); void update_title_display(); + void insert_extra_widget(wxWindow* widget); void msw_rescale(); @@ -335,6 +336,11 @@ private: std::string m_required_data_file_name; std::string m_required_data_file_path; + // timelapse internal storage selection + std::string m_timelapse_storage; // "internal" or "external"; empty = unsupported + ScalableButton* m_timelapse_folder_btn { nullptr }; + PopupWindow* m_timelapse_storage_popup { nullptr }; + std::vector ops_auto; std::vector ops_no_auto; @@ -572,6 +578,10 @@ private: void update_option_opts(MachineObject *obj); void update_options_layout(); + /* timelapse storage-location selector */ + void update_timelapse_folder_btn_icon(); + void show_timelapse_folder_popup(); + // save and restore from config void load_option_vals(MachineObject* obj); void save_option_vals();