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,