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();