From a4a820e70bb0aa50667687e12a7d696344cda640 Mon Sep 17 00:00:00 2001 From: Tuomas Salokanto Date: Mon, 11 Dec 2023 19:58:26 +0200 Subject: [PATCH 1/2] Support custom IP camera Allow adding custom IP camera source while keeping possible built-in bambulab camera also functional and add button to switch between them. This uses WebView widget to show the stream. Upon loading the page, javascript is used to remove native controls and scroll- bars for aesthetic reasons. --- resources/images/camera_switch.svg | 76 ++++++++++++++++++++ resources/images/camera_switch_dark.svg | 76 ++++++++++++++++++++ src/slic3r/GUI/CameraPopup.cpp | 60 ++++++++++++++++ src/slic3r/GUI/CameraPopup.hpp | 9 +++ src/slic3r/GUI/StatusPanel.cpp | 92 +++++++++++++++++++++++++ src/slic3r/GUI/StatusPanel.hpp | 10 +++ 6 files changed, 323 insertions(+) create mode 100644 resources/images/camera_switch.svg create mode 100644 resources/images/camera_switch_dark.svg 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..227f4c1b35c 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,21 @@ 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->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 +1030,34 @@ 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(); 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 +3896,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 +3908,64 @@ 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'; + document.querySelector('video').setAttribute('style', 'width: 100% !important;'); + document.querySelector('video').removeAttribute('controls'); + )"; + 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, From 52b2f6d14aae002a6fdd244cc72744e057206fca Mon Sep 17 00:00:00 2001 From: Tuomas Salokanto Date: Wed, 3 Jan 2024 09:40:53 +0200 Subject: [PATCH 2/2] Add partial support for PiP video HTMLVideoElement supports picture-in-picture video but the dedicated control is hidden in this implementation to have more integrated look in OrcaSlicer. Add right-click listener to the camera switch icon that opens the video element in a PiP window. Only works when the video is in