diff --git a/resources/images/camera_switch.svg b/resources/images/camera_switch.svg
new file mode 100644
index 00000000000..be8cf27a09e
--- /dev/null
+++ b/resources/images/camera_switch.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/resources/images/camera_switch_dark.svg b/resources/images/camera_switch_dark.svg
new file mode 100644
index 00000000000..5c40b35d145
--- /dev/null
+++ b/resources/images/camera_switch_dark.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/src/slic3r/GUI/CameraPopup.cpp b/src/slic3r/GUI/CameraPopup.cpp
index e7b5cf68dd5..08e4801372a 100644
--- a/src/slic3r/GUI/CameraPopup.cpp
+++ b/src/slic3r/GUI/CameraPopup.cpp
@@ -23,6 +23,7 @@ wxEND_EVENT_TABLE()
wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
+wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
#define CAMERAPOPUP_CLICK_INTERVAL 20
@@ -78,6 +79,34 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(0, 0, wxALL, 0);
}
+ // custom IP camera
+ m_custom_camera_input_confirm = new Button(m_panel, _L("Enable"));
+ m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154));
+ m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154));
+ m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE));
+ m_custom_camera_input_confirm->SetFont(Label::Body_14);
+ m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30)));
+ m_custom_camera_input_confirm->SetPosition(wxDefaultPosition);
+ m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12));
+ m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize);
+ m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP"));
+ m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14);
+ m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source"));
+ m_custom_camera_hint->Wrap(-1);
+ m_custom_camera_hint->SetFont(Label::Head_14);
+ m_custom_camera_hint->SetForegroundColour(TEXT_COL);
+
+ m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this);
+
+ if (!wxGetApp().app_config->get("camera", "custom_source").empty()) {
+ m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source"));
+ set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true");
+ }
+
+ top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
+ top_sizer->Add(0, 0, wxALL, 0);
+ top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxEXPAND | wxALL, FromDIP(5));
+ top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10));
auto url = wxString::Format(L"https://wiki.bambulab.com/%s/software/bambu-studio/virtual-camera", L"en");
@@ -132,6 +161,37 @@ void CameraPopup::sdcard_absent_hint()
GetEventHandler()->ProcessEvent(evt);
}
+void CameraPopup::on_camera_source_changed(wxCommandEvent &event)
+{
+ if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) {
+ handle_camera_source_change();
+ }
+}
+
+void CameraPopup::handle_camera_source_change()
+{
+ m_custom_camera_enabled = !m_custom_camera_enabled;
+
+ set_custom_cam_button_state(m_custom_camera_enabled);
+
+ wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString());
+ wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled);
+
+ wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE);
+ evt.SetEventObject(this);
+ GetEventHandler()->ProcessEvent(evt);
+}
+
+void CameraPopup::set_custom_cam_button_state(bool state)
+{
+ m_custom_camera_enabled = state;
+ auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154);
+ auto stateText = state ? "Disable" : "Enable";
+ m_custom_camera_input_confirm->SetBackgroundColor(stateColour);
+ m_custom_camera_input_confirm->SetBorderColor(stateColour);
+ m_custom_camera_input_confirm->SetLabel(_L(stateText));
+}
+
void CameraPopup::on_switch_recording(wxCommandEvent& event)
{
if (!m_obj) return;
diff --git a/src/slic3r/GUI/CameraPopup.hpp b/src/slic3r/GUI/CameraPopup.hpp
index a9b53a621ec..a256f317b0b 100644
--- a/src/slic3r/GUI/CameraPopup.hpp
+++ b/src/slic3r/GUI/CameraPopup.hpp
@@ -14,12 +14,14 @@
#include "Widgets/SwitchButton.hpp"
#include "Widgets/RadioBox.hpp"
#include "Widgets/PopupWindow.hpp"
+#include "Widgets/TextInput.hpp"
namespace Slic3r {
namespace GUI {
wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
+wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
class CameraPopup : public PopupWindow
{
@@ -50,6 +52,9 @@ class CameraPopup : public PopupWindow
void on_switch_recording(wxCommandEvent& event);
void on_set_resolution();
void sdcard_absent_hint();
+ void on_camera_source_changed(wxCommandEvent& event);
+ void handle_camera_source_change();
+ void set_custom_cam_button_state(bool state);
wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left);
void select_curr_radiobox(int btn_idx);
@@ -66,6 +71,10 @@ class CameraPopup : public PopupWindow
SwitchButton* m_switch_recording;
wxStaticText* m_text_vcamera;
SwitchButton* m_switch_vcamera;
+ wxStaticText* m_custom_camera_hint;
+ TextInput* m_custom_camera_input;
+ Button* m_custom_camera_input_confirm;
+ bool m_custom_camera_enabled{ false };
wxStaticText* m_text_resolution;
wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM];
wxScrolledWindow *m_panel;
diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp
index e4fed1aac6c..988f5e26cfb 100644
--- a/src/slic3r/GUI/StatusPanel.cpp
+++ b/src/slic3r/GUI/StatusPanel.cpp
@@ -5,6 +5,7 @@
#include "Widgets/Button.hpp"
#include "Widgets/StepCtrl.hpp"
#include "Widgets/SideTools.hpp"
+#include "Widgets/WebView.hpp"
#include "BitmapCache.hpp"
#include "GUI_App.hpp"
@@ -889,6 +890,11 @@ StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint
StatusBasePanel::~StatusBasePanel()
{
delete m_media_play_ctrl;
+
+ if (m_custom_camera_view) {
+ delete m_custom_camera_view;
+ m_custom_camera_view = nullptr;
+ }
}
void StatusBasePanel::init_bitmaps()
@@ -922,6 +928,7 @@ void StatusBasePanel::init_bitmaps()
m_bitmap_timelapse_off = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_timelapse_off_dark" : "monitor_timelapse_off", 20);
m_bitmap_vcamera_on = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_vcamera_on_dark" : "monitor_vcamera_on", 20);
m_bitmap_vcamera_off = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_vcamera_off_dark" : "monitor_vcamera_off", 20);
+ m_bitmap_switch_camera = ScalableBitmap(this, wxGetApp().dark_mode() ? "camera_switch_dark" : "camera_switch", 20);
}
@@ -989,12 +996,27 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_setting_button->SetBackgroundColour(STATUS_TITLE_BG);
+ m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0);
+ m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
+ m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG);
+ m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp());
+ m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this);
+ m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) {
+ const std::string js_request_pip = R"(
+ document.querySelector('video').requestPictureInPicture();
+ )";
+ m_custom_camera_view->RunScript(js_request_pip);
+ });
+ m_camera_switch_button->Hide();
+
m_bitmap_sdcard_img->SetToolTip(_L("SD Card"));
m_bitmap_timelapse_img->SetToolTip(_L("Timelapse"));
m_bitmap_recording_img->SetToolTip(_L("Video"));
m_bitmap_vcamera_img->SetToolTip(_L("Go Live"));
m_setting_button->SetToolTip(_L("Camera Setting"));
+ m_camera_switch_button->SetToolTip(_L("Switch Camera View"));
+ bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
@@ -1014,17 +1036,44 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_media_ctrl = new wxMediaCtrl2(this);
m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288)));
+ m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString);
+ m_custom_camera_view->EnableContextMenu(false);
+ Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId());
+
m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40)));
+ m_custom_camera_view->Hide();
+ m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
+ if (evt.GetString() == "leavepictureinpicture") {
+ // When leaving PiP, video gets paused in some cases and toggling play
+ // programmatically does not work.
+ m_custom_camera_view->Reload();
+ }
+ else if (evt.GetString() == "enterpictureinpicture") {
+ toggle_builtin_camera();
+ }
+ });
sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0);
+ sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0);
sizer->Add(m_media_play_ctrl, 0, wxEXPAND | wxALL, 0);
// media_ctrl_panel->SetSizer(bSizer_monitoring);
// media_ctrl_panel->Layout();
//
// sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1);
+
+ if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") {
+ handle_camera_source_change();
+ }
+
return sizer;
}
+void StatusBasePanel::on_webview_navigating(wxWebViewEvent& evt) {
+ wxGetApp().CallAfter([this] {
+ remove_controls();
+ });
+}
+
wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent)
{
wxBoxSizer *bSizer_right = new wxBoxSizer(wxVERTICAL);
@@ -3863,6 +3912,7 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
}
sdcard_hint_dlg->on_show();
});
+ m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this);
wxWindow* ctrl = (wxWindow*)event.GetEventObject();
wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0));
wxSize sz = ctrl->GetSize();
@@ -3874,6 +3924,71 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
}
}
+void StatusBasePanel::on_camera_source_change(wxCommandEvent& event)
+{
+ handle_camera_source_change();
+}
+
+void StatusBasePanel::handle_camera_source_change()
+{
+ const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source");
+ const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
+
+ if (enabled && !new_cam_url.empty()) {
+ m_custom_camera_view->LoadURL(new_cam_url);
+ toggle_custom_camera();
+ m_camera_switch_button->Show();
+ } else {
+ toggle_builtin_camera();
+ m_camera_switch_button->Hide();
+ }
+}
+
+void StatusBasePanel::toggle_builtin_camera()
+{
+ m_custom_camera_view->Hide();
+ m_media_ctrl->Show();
+ m_media_play_ctrl->Show();
+}
+
+void StatusBasePanel::toggle_custom_camera()
+{
+ const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
+
+ if (enabled) {
+ m_custom_camera_view->Show();
+ m_media_ctrl->Hide();
+ m_media_play_ctrl->Hide();
+ }
+}
+
+void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event)
+{
+ const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
+ if (enabled && m_media_ctrl->IsShown()) {
+ toggle_custom_camera();
+ } else {
+ toggle_builtin_camera();
+ }
+}
+
+void StatusBasePanel::remove_controls()
+{
+ const std::string js_cleanup_video_element = R"(
+ document.body.style.overflow='hidden';
+ const video = document.querySelector('video');
+ video.setAttribute('style', 'width: 100% !important;');
+ video.removeAttribute('controls');
+ video.addEventListener('leavepictureinpicture', () => {
+ window.wx.postMessage('leavepictureinpicture');
+ });
+ video.addEventListener('enterpictureinpicture', () => {
+ window.wx.postMessage('enterpictureinpicture');
+ });
+ )";
+ m_custom_camera_view->RunScript(js_cleanup_video_element);
+}
+
void StatusPanel::on_camera_leave(wxMouseEvent& event)
{
if (obj && m_camera_popup) {
diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp
index fba9dc2f0fe..399566373bc 100644
--- a/src/slic3r/GUI/StatusPanel.hpp
+++ b/src/slic3r/GUI/StatusPanel.hpp
@@ -287,6 +287,7 @@ class StatusBasePanel : public wxScrolledWindow
ScalableBitmap m_bitmap_timelapse_off;
ScalableBitmap m_bitmap_vcamera_on;
ScalableBitmap m_bitmap_vcamera_off;
+ ScalableBitmap m_bitmap_switch_camera;
/* title panel */
wxPanel * media_ctrl_panel;
@@ -307,6 +308,7 @@ class StatusBasePanel : public wxScrolledWindow
wxStaticBitmap *m_bitmap_sdcard_img;
wxStaticBitmap *m_bitmap_static_use_time;
wxStaticBitmap *m_bitmap_static_use_weight;
+ wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl2 * m_media_ctrl;
@@ -326,6 +328,7 @@ class StatusBasePanel : public wxScrolledWindow
ScalableButton *m_button_pause_resume;
ScalableButton *m_button_abort;
Button * m_button_clean;
+ wxWebView * m_custom_camera_view{nullptr};
wxStaticText * m_text_tasklist_caption;
@@ -410,6 +413,13 @@ class StatusBasePanel : public wxScrolledWindow
virtual void on_axis_ctrl_z_down_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); }
+ void on_camera_source_change(wxCommandEvent& event);
+ void handle_camera_source_change();
+ void remove_controls();
+ void on_webview_navigating(wxWebViewEvent& evt);
+ void on_camera_switch_toggled(wxMouseEvent& event);
+ void toggle_custom_camera();
+ void toggle_builtin_camera();
public:
StatusBasePanel(wxWindow * parent,