OrcaPrinterAgent Storage Tab

This commit is contained in:
Lam Wei Lun
2026-10-05 18:22:29 +08:00
parent 0110572a77
commit 3942ba7005
21 changed files with 2213 additions and 104 deletions
+9
View File
@@ -366,6 +366,10 @@ set(SLIC3R_GUI_SOURCES
GUI/MarkdownTip.hpp
GUI/MediaFilePanel.cpp
GUI/MediaFilePanel.h
GUI/OrcaFilesPanel.cpp
GUI/OrcaFilesPanel.h
GUI/StoragePanel.cpp
GUI/StoragePanel.h
GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp
@@ -455,6 +459,11 @@ set(SLIC3R_GUI_SOURCES
GUI/PresetHints.hpp
GUI/PrinterCloudAuthDialog.cpp
GUI/PrinterCloudAuthDialog.hpp
GUI/Printer/BambuFileGridModel.cpp
GUI/Printer/BambuFileGridModel.h
GUI/Printer/IFileGridModel.h
GUI/Printer/RemoteFileGridModel.cpp
GUI/Printer/RemoteFileGridModel.h
GUI/Printer/PrinterFileSystem.cpp
GUI/Printer/PrinterFileSystem.h
GUI/PrinterWebView.cpp
+118 -90
View File
@@ -1,5 +1,6 @@
#include "ImageGrid.h"
#include "Printer/PrinterFileSystem.h"
#include "Printer/BambuFileGridModel.h"
#include "wxExtensions.hpp"
#include "Widgets/Label.hpp"
#include "I18N.hpp"
@@ -8,6 +9,7 @@
#include <utility>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/smart_ptr/make_shared_object.hpp>
#include <wx/colour.h>
#include <cstddef>
#include <wx/dcclient.h>
@@ -76,18 +78,11 @@ ImageGrid::ImageGrid(wxWindow * parent)
void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
{
if (m_file_sys) {
m_file_sys->Unbind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
m_file_sys->Unbind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
}
m_file_sys = file_sys;
if (m_file_sys) {
m_file_sys->Bind(EVT_MODE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_FILE_CHANGED, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_THUMBNAIL, &ImageGrid::changedEvent, this);
m_file_sys->Bind(EVT_DOWNLOAD, &ImageGrid::changedEvent, this);
m_model.reset();
if (file_sys) {
m_model = boost::make_shared<BambuFileGridModel>(file_sys);
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
}
m_row_count = 0;
m_col_count = 1;
@@ -96,9 +91,22 @@ void ImageGrid::SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys)
UpdateFileSystem();
}
void ImageGrid::SetModel(boost::shared_ptr<IFileGridModel> model)
{
m_file_sys.reset();
m_model = std::move(model);
if (m_model)
m_model->SetChangeHandler([this](FileGridChange c) { onModelChange(c); });
m_row_count = 0;
m_col_count = 1;
m_row_offset = 0;
m_scroll_offset = 0;
UpdateFileSystem();
}
void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
{
int code = m_file_sys ? m_file_sys->GetLastError() : 1;
int code = m_model ? m_model->GetLastError() : 1;
m_status_icon = icon;
m_status_msg = wxString::Format(msg, code);
BOOST_LOG_TRIVIAL(info) << "ImageGrid::SetStatus: " << m_status_msg.ToUTF8().data();
@@ -107,26 +115,26 @@ void ImageGrid::SetStatus(ScalableBitmap const & icon, wxString const &msg)
void Slic3r::GUI::ImageGrid::SetFileType(int type, std::string const &storage)
{
if (!m_file_sys)
if (!m_model)
return;
m_file_sys->SetFileType((PrinterFileSystem::FileType) type, storage);
m_model->SetFileType((FileGridType) type, storage);
}
void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
{
if (!m_file_sys)
if (!m_model)
return;
if (m_file_sys->GetCount() == 0) {
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
if (m_model->GetCount() == 0) {
m_model->SetGroupMode((FileGridGroup) mode);
return;
}
wxSize size = GetClientSize();
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0)
? m_row_offset / 4 * m_col_count
: ((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
auto & file = m_file_sys->GetFile(index);
m_file_sys->SetGroupMode((PrinterFileSystem::GroupMode) mode);
index = m_file_sys->GetIndexAtTime(file.time);
: ((m_model->GetCount() + m_col_count - 1) / m_col_count - (size.y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
time_t time = m_model->GetFile(index).time;
m_model->SetGroupMode((FileGridGroup) mode);
index = m_model->GetIndexAtTime(time);
// UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count)
@@ -137,8 +145,8 @@ void Slic3r::GUI::ImageGrid::SetGroupMode(int mode)
void Slic3r::GUI::ImageGrid::SetSelecting(bool selecting)
{
m_selecting = selecting;
if (m_file_sys)
m_file_sys->SelectAll(false);
if (m_model)
m_model->SelectAll(false);
Refresh();
}
@@ -166,14 +174,14 @@ void Slic3r::GUI::ImageGrid::Rescale()
void Slic3r::GUI::ImageGrid::Select(size_t index)
{
if (m_selecting) {
m_file_sys->ToggleSelect(index);
m_model->ToggleSelect(index);
Refresh();
return;
}
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
return;
}
index = m_file_sys->EnterSubGroup(index);
index = m_model->EnterSubGroup(index);
// UpdateFileSystem(); call by changed event
m_row_offset = index / m_col_count * 4;
if (m_row_offset >= m_row_count)
@@ -193,9 +201,9 @@ void Slic3r::GUI::ImageGrid::DoAction(size_t index, int action)
void Slic3r::GUI::ImageGrid::UpdateFileSystem()
{
if (!m_file_sys) return;
if (m_file_sys->GetFileType() < PrinterFileSystem::F_MODEL) {
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (!m_model) return;
if (m_model->GetFileType() < FileGridType::Model) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
m_cell_size.Set(396, 228);
m_border_size.Set(384, 216);
}
@@ -216,10 +224,10 @@ void Slic3r::GUI::ImageGrid::UpdateFileSystem()
void ImageGrid::UpdateLayout()
{
if (!m_file_sys) return;
if (!m_model) return;
wxSize size = GetClientSize();
wxSize title_mask_size{0, 60 * em_unit(this) / 10};
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
title_mask_size.y = 20 * em_unit(this) / 10;
size.y -= title_mask_size.y;
}
@@ -227,7 +235,7 @@ void ImageGrid::UpdateLayout()
int cell_height = m_cell_size.GetHeight();
int ncol = (size.GetWidth() - cell_width + m_border_size.GetWidth()) / cell_width;
if (ncol <= 0) ncol = 1;
int total_height = (m_file_sys->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
int total_height = (m_model->GetCount() + ncol - 1) / ncol * cell_height + cell_height - m_border_size.GetHeight();
int nrow = (total_height - size.GetHeight() + cell_height / 4 - 1) / (cell_height / 4);
m_row_offset = m_row_offset * m_col_count / ncol;
m_col_count = ncol;
@@ -236,7 +244,7 @@ void ImageGrid::UpdateLayout()
m_row_offset = m_row_count == 0 ? 0 : m_row_count - 1;
m_scroll_offset = 0;
// create mask
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
if (m_model->GetGroupMode() == FileGridGroup::All) {
title_mask_size.x = (m_col_count - 1) * m_cell_size.GetWidth() + m_border_size.GetWidth();
}
else {
@@ -252,7 +260,7 @@ void ImageGrid::UpdateLayout()
void Slic3r::GUI::ImageGrid::UpdateFocusRange()
{
if (!m_file_sys) return;
if (!m_model) return;
wxSize size = GetClientSize();
wxPoint off;
int index = firstItem(size, off);
@@ -261,15 +269,21 @@ void Slic3r::GUI::ImageGrid::UpdateFocusRange()
count += m_col_count;
off.y += m_cell_size.y;
}
m_file_sys->SetFocusRange(index, count);
m_model->SetFocusRange(index, count);
}
bool Slic3r::GUI::ImageGrid::hasSecondAction(const FileGridCard &card) const
{
return m_show_download || card.downloading ||
(m_model->GetFileType() == FileGridType::Model && m_model->supports_print_action());
}
std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
{
if (!m_file_sys)
if (!m_model)
return {HIT_NONE, -1};
wxSize size = GetClientSize();
if (m_file_sys->GetCount() == 0) {
if (m_model->GetCount() == 0) {
if (wxRect({0, 0}, m_border_size).CenterIn(wxRect({0, 0}, size)).Contains(pt))
return {HIT_STATUS, 0};
return {HIT_NONE, -1};
@@ -287,17 +301,19 @@ std::pair<int, size_t> Slic3r::GUI::ImageGrid::HitTest(wxPoint const &pt)
index += m_col_count;
off.y -= m_cell_size.GetHeight();
}
if (index >= m_file_sys->GetCount()) { return {HIT_NONE, -1}; }
if (index >= m_model->GetCount()) { return {HIT_NONE, -1}; }
if (!m_content_rect.Contains(off)) { return {HIT_NONE, -1}; }
if (!m_selecting) {
wxRect hover_rect{0, m_content_rect.GetHeight() - m_buttons_background.GetHeight(), m_content_rect.GetWidth(), m_buttons_background.GetHeight()};
auto & file = m_file_sys->GetFile(index);
int btn = file.IsDownload() && file.DownloadProgress() >= 0 ? 3 : 2;
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
if (m_show_download)
const FileGridCard &card = m_model->GetFile(index);
int btn = card.downloading && card.download_progress >= 0 ? 3 : 2;
if (m_model->GetFileType() == FileGridType::Model) {
if (m_show_download && m_model->supports_print_action())
btn = 3;
hover_rect.y -= m_content_rect.GetHeight() * 64 / 264;
}
if (!hasSecondAction(card))
btn = 1;
if (hover_rect.Contains(off.x, off.y)) {
return {HIT_ACTION, index * 4 + off.x * btn / hover_rect.GetWidth()};
} // Two buttons
@@ -315,12 +331,12 @@ void ImageGrid::mouseMoved(wxMouseEvent& event)
m_hit_item = hit.second;
if (hit.first == HIT_ITEM) {
SetToolTip({});
auto & file = m_file_sys->GetFile(hit.second);
if (auto title = file.Title(); !title.empty()) {
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(file.name), from_u8(title));
const FileGridCard &card = m_model->GetFile(hit.second);
if (auto title = card.title; !title.empty()) {
auto tip = wxString::Format(_L("File: %s\nTitle: %s\n"), from_u8(card.name), from_u8(title));
SetToolTip(tip);
} else {
SetToolTip(from_u8(file.name));
SetToolTip(from_u8(card.name));
}
} else
SetToolTip({});
@@ -372,9 +388,9 @@ void ImageGrid::mouseReleased(wxMouseEvent& event)
else if (m_hit_type == HIT_ACTION)
DoAction(m_hit_item / 4, m_hit_item & 3);
else if (m_hit_type == HIT_MODE)
SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(2 - m_hit_item));
SetGroupMode(static_cast<int>(2 - m_hit_item));
else if (m_hit_type == HIT_STATUS)
m_file_sys->Retry();
m_model->Retry();
else
Refresh();
} else {
@@ -403,20 +419,11 @@ void ImageGrid::mouseWheelMoved(wxMouseEvent &event)
Refresh();
}
void Slic3r::GUI::ImageGrid::changedEvent(wxCommandEvent& evt)
void Slic3r::GUI::ImageGrid::onModelChange(FileGridChange change)
{
evt.Skip();
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::changedEvent: " << evt.GetEventType() << " index: " << evt.GetInt()
<< " name: " << evt.GetString().ToUTF8().data() << " extra: " << evt.GetExtraLong();
if (evt.GetEventType() == EVT_FILE_CHANGED) {
if (evt.GetInt() == -1)
m_file_sys->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
BOOST_LOG_TRIVIAL(debug) << "ImageGrid::onModelChange: " << (int) change;
if (change == FileGridChange::Files || change == FileGridChange::Mode)
UpdateFileSystem();
}
else if (evt.GetEventType() == EVT_MODE_CHANGED)
UpdateFileSystem();
//else if (evt.GetEventType() == EVT_THUMBNAIL)
// RefreshRect(itemRect(evt.GetInt()), false);
else
Refresh();
}
@@ -431,7 +438,7 @@ void ImageGrid::paintEvent(wxPaintEvent& evt)
size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
{
int size_y = size.y;
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
if (m_model->GetGroupMode() == FileGridGroup::All)
size_y -= m_title_mask.GetHeight();
int offx = (size.x - (m_col_count - 1) * m_cell_size.GetWidth() - m_border_size.GetWidth()) / 2;
int offy = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
@@ -439,8 +446,8 @@ size_t Slic3r::GUI::ImageGrid::firstItem(wxSize const &size, wxPoint &off)
size_y - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight() * m_cell_size.GetHeight();
int index = (m_row_offset + 1 < m_row_count || m_row_count == 0) ?
m_row_offset / 4 * m_col_count :
((m_file_sys->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE)
((m_model->GetCount() + m_col_count - 1) / m_col_count - (size_y + m_border_size.GetHeight() - 1) / m_cell_size.GetHeight()) * m_col_count;
if (m_model->GetGroupMode() == FileGridGroup::All)
offy += m_title_mask.GetHeight();
off = wxPoint{offx, offy};
return index;
@@ -534,7 +541,7 @@ void ImageGrid::render(wxDC& dc)
wxSize size = GetClientSize();
dc.SetPen(wxPen(GetBackgroundColour()));
dc.SetBrush(wxBrush(GetBackgroundColour()));
if (!m_file_sys || m_file_sys->GetCount() == 0) {
if (!m_model || m_model->GetCount() == 0) {
dc.DrawRectangle({ 0, 0, size.x, size.y });
if (!m_status_msg.IsEmpty()) {
auto si = m_status_icon.GetBmpSize();
@@ -562,7 +569,7 @@ void ImageGrid::render(wxDC& dc)
{
// Draw one line
wxPoint pt{off.x, off.y};
end = (index + m_col_count) < m_file_sys->GetCount() ? index + m_col_count : m_file_sys->GetCount();
end = (index + m_col_count) < m_model->GetCount() ? index + m_col_count : m_model->GetCount();
while (index < end) {
pt += m_content_rect.GetTopLeft();
// Draw content
@@ -571,7 +578,7 @@ void ImageGrid::render(wxDC& dc)
&ImageGrid::renderContent1,
&ImageGrid::renderContent2
};
(this->*contentRender[m_file_sys->GetFileType()])(dc, pt, index, hit_image == index);
(this->*contentRender[static_cast<int>(m_model->GetFileType())])(dc, pt, index, hit_image == index);
pt -= m_content_rect.GetTopLeft();
// Draw colume spacing at right
dc.DrawRectangle({pt.x + m_border_size.GetWidth(), pt.y, m_cell_size.GetWidth() - m_border_size.GetWidth(), m_border_size.GetHeight()});
@@ -588,13 +595,13 @@ void ImageGrid::render(wxDC& dc)
off.y += m_cell_size.GetHeight();
}
// Draw floating date range for non-group list
if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE && m_file_sys->GetCount() > 0) {
if (m_model->GetGroupMode() == FileGridGroup::All && m_model->GetCount() > 0) {
//dc.DrawBitmap(m_title_mask, {off.x, 0});
dc.DrawRectangle({off.x, 0}, m_title_mask.GetSize());
auto & file1 = m_file_sys->GetFile(start);
auto & file2 = m_file_sys->GetFile(end - 1);
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
const FileGridCard & file1 = m_model->GetFile(start);
const FileGridCard & file2 = m_model->GetFile(end - 1);
auto date1 = wxDateTime((time_t) file1.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
auto date2 = wxDateTime((time_t) file2.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
dc.SetFont(Label::Head_16);
dc.SetTextForeground(StateColor::darkModeColorFor("#262E30"));
dc.DrawText(date1 + " - " + date2, wxPoint{off.x, 2});
@@ -604,7 +611,7 @@ void ImageGrid::render(wxDC& dc)
dc.DrawRectangle({off.x, off.y, size.x - off.x * 2, size.y - off.y});
// Draw position bar
if (m_timer.IsRunning()) {
int total_height = (m_file_sys->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
int total_height = (m_model->GetCount() + m_col_count - 1) / m_col_count * m_cell_size.GetHeight() + m_cell_size.GetHeight() - m_border_size.GetHeight();
if (total_height > size.y) {
int offset = (m_row_offset + 1 < m_row_count || m_row_count == 0) ? m_row_offset * (m_cell_size.GetHeight() / 4) : total_height - size.y;
wxRect rect = {size.x - 16, offset * size.y / total_height, 8,
@@ -618,15 +625,35 @@ void ImageGrid::render(wxDC& dc)
void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int index, bool hit)
{
bool selected = false;
auto &file = m_file_sys->GetFile(index, selected);
const FileGridCard &card = m_model->GetFile(index, selected);
// Draw thumbnail
if (file.thumbnail.IsOk()) {
float hs = (float) m_content_rect.GetWidth() / file.thumbnail.GetWidth();
float vs = (float) m_content_rect.GetHeight() / file.thumbnail.GetHeight();
dc.SetUserScale(hs, vs);
dc.DrawBitmap(file.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
dc.SetUserScale(1, 1);
if (m_file_sys->GetGroupMode() != PrinterFileSystem::G_NONE) { dc.DrawBitmap(m_title_mask, pt); }
if (card.thumbnail.IsOk()) {
if (m_model->preserve_thumbnail_aspect()) {
// Contain: fit the whole thumbnail and letterbox against the tile background.
float scale = std::min((float) m_content_rect.GetWidth() / card.thumbnail.GetWidth(),
(float) m_content_rect.GetHeight() / card.thumbnail.GetHeight());
int thumb_w = (int) (card.thumbnail.GetWidth() * scale);
int thumb_h = (int) (card.thumbnail.GetHeight() * scale);
int offx = (m_content_rect.GetWidth() - thumb_w) / 2;
int offy = (m_content_rect.GetHeight() - thumb_h) / 2;
auto brush = dc.GetBrush();
auto pen = dc.GetPen();
dc.SetBrush(StateColor::darkModeColorFor(0xEEEEEE));
dc.SetPen(StateColor::darkModeColorFor(0xEEEEEE));
dc.DrawRectangle(pt, m_content_rect.GetSize());
dc.SetUserScale(scale, scale);
dc.DrawBitmap(card.thumbnail, {(int) ((pt.x + offx) / scale), (int) ((pt.y + offy) / scale)});
dc.SetUserScale(1, 1);
dc.SetBrush(brush);
dc.SetPen(pen);
} else {
float hs = (float) m_content_rect.GetWidth() / card.thumbnail.GetWidth();
float vs = (float) m_content_rect.GetHeight() / card.thumbnail.GetHeight();
dc.SetUserScale(hs, vs);
dc.DrawBitmap(card.thumbnail, {(int) (pt.x / hs), (int) (pt.y / vs)});
dc.SetUserScale(1, 1);
}
if (m_model->GetGroupMode() != FileGridGroup::All) { dc.DrawBitmap(m_title_mask, pt); }
}
bool show_download_state_always = true;
// Draw checked icon
@@ -634,14 +661,14 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
dc.DrawBitmap(selected ? m_checked_icon.bmp() : m_unchecked_icon.bmp(), pt + wxPoint{10, 10});
// can't handle alpha
// dc.GradientFillLinear({pt.x, pt.y, m_border_size.GetWidth(), 60}, wxColour(0x6F, 0x6F, 0x6F, 0x99), wxColour(0x6F, 0x6F, 0x6F, 0), wxBOTTOM);
else if (m_file_sys->GetGroupMode() == PrinterFileSystem::G_NONE) {
else if (m_model->GetGroupMode() == FileGridGroup::All) {
wxString nonHoverText;
wxString secondAction = m_show_download ? _L("Download") : "";
wxString thirdAction;
int states = 0;
// Draw download progress
if (file.IsDownload()) {
int progress = file.DownloadProgress();
if (card.downloading) {
int progress = card.download_progress;
if (progress == -1) {
secondAction = _L("Cancel");
nonHoverText = _L("Download waiting...");
@@ -659,7 +686,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
thirdAction = wxString::Format(L"%d%%...", progress);
}
}
if (m_file_sys->GetFileType() == PrinterFileSystem::F_MODEL) {
if (m_model->GetFileType() == FileGridType::Model && m_model->supports_print_action()) {
if (secondAction != _L("Play"))
thirdAction = secondAction;
secondAction = _L_CONTEXT("Print", "Verb");
@@ -669,7 +696,8 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
wxArrayString texts;
if (hit) {
texts.Add(_L("Delete"));
texts.Add(secondAction);
if (hasSecondAction(card))
texts.Add(secondAction);
if (!thirdAction.IsEmpty())
texts.Add(thirdAction);
renderButtons(dc, texts, rect, m_hit_type == HIT_ACTION ? m_hit_item & 3 : -1, states);
@@ -679,7 +707,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
}
} else {
dc.SetTextForeground(*wxWHITE); // time text color
auto date = wxDateTime((time_t) file.time).Format(_L(TIME_FORMATS[m_file_sys->GetGroupMode()]));
auto date = wxDateTime((time_t) card.time).Format(_L(TIME_FORMATS[static_cast<int>(m_model->GetGroupMode())]));
dc.DrawText(date, pt + wxPoint{24, 16});
}
if (m_selecting && show_download_state_always)
@@ -688,7 +716,7 @@ void Slic3r::GUI::ImageGrid::renderContent1(wxDC &dc, wxPoint const &pt, int ind
void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int index, bool hit)
{
auto &file = m_file_sys->GetFile(index);
const FileGridCard &card = m_model->GetFile(index);
// Draw thumbnail & buttons
int h = m_content_rect.GetHeight() * 64 / 264;
m_content_rect.SetHeight(m_content_rect.GetHeight() - h);
@@ -711,14 +739,14 @@ void Slic3r::GUI::ImageGrid::renderContent2(wxDC &dc, wxPoint const &pt, int ind
auto em = em_unit(this);
wxRect rect{pt.x, pt.y + m_content_rect.GetHeight() - h, m_content_rect.GetWidth(), h / 2};
rect.Deflate(em, 0);
renderText2(dc, from_u8(file.name), rect);
renderText2(dc, from_u8(card.name), rect);
rect.Offset(0, h / 2);
rect.SetWidth(rect.GetWidth() / 2 - em);
dc.SetFont(Label::Body_13);
dc.SetTextForeground(StateColor::darkModeColorFor("#6B6B6B"));
renderIconText(dc, m_model_time_icon, file.Metadata("Time", "0m"), rect);
renderIconText(dc, m_model_time_icon, card.time_text.empty() ? wxString("0m") : from_u8(card.time_text), rect);
rect.Offset(m_content_rect.GetWidth() / 2, 0);
renderIconText(dc, m_model_weight_icon, file.Metadata("Weight", "0g"), rect);
renderIconText(dc, m_model_weight_icon, card.weight_text.empty() ? wxString("0g") : from_u8(card.weight_text), rect);
}
void Slic3r::GUI::ImageGrid::renderButtons(wxDC &dc, wxArrayString const &texts, wxRect const &rect2, size_t hit, int states)
+8 -1
View File
@@ -23,6 +23,7 @@
#include "Widgets/StateColor.hpp"
#include "wxExtensions.hpp"
#include "Printer/IFileGridModel.h"
class Button;
class Label;
@@ -44,6 +45,8 @@ public:
void SetFileSystem(boost::shared_ptr<PrinterFileSystem> file_sys);
void SetModel(boost::shared_ptr<IFileGridModel> model);
void SetStatus(ScalableBitmap const & icon, wxString const &msg);
boost::shared_ptr<PrinterFileSystem> GetFileSystem() { return m_file_sys; }
@@ -76,9 +79,12 @@ protected:
std::pair<int, size_t> HitTest(wxPoint const &pt);
// True when the hover bar shows a second action next to Delete.
bool hasSecondAction(const FileGridCard &card) const;
protected:
void changedEvent(wxCommandEvent& evt);
void onModelChange(FileGridChange change);
void paintEvent(wxPaintEvent& evt);
@@ -117,6 +123,7 @@ protected:
private:
boost::shared_ptr<PrinterFileSystem> m_file_sys;
boost::shared_ptr<IFileGridModel> m_model;
ScalableBitmap m_status_icon;
wxString m_status_msg;
+7 -1
View File
@@ -230,7 +230,13 @@ MediaFilePanel::MediaFilePanel(wxWindow * parent)
});
};
Bind(wxEVT_SHOW, onShowHide);
parent->GetParent()->Bind(wxEVT_SHOW, onShowHide);
// Watch the page host's show/hide. Walk up past any wrapper containers (e.g. a
// StoragePanel) so the target stays the same regardless of nesting depth.
wxWindow* host = parent;
while (host && !host->IsTopLevel() && host->GetParent() && !host->GetParent()->IsTopLevel())
host = host->GetParent();
if (host && host != this)
host->Bind(wxEVT_SHOW, onShowHide);
m_lan_user = "bblp";
}
+10 -10
View File
@@ -16,7 +16,6 @@
#include "slic3r/GUI/StatusPanel.hpp"
#include "slic3r/GUI/UpgradePanel.hpp"
#include "slic3r/GUI/Widgets/SideTools.hpp"
#include "slic3r/GUI/Printer/PrinterFileSystem.h"
#include <wx/app.h>
#include <wx/bookctrl.h>
#include <wx/button.h>
@@ -47,7 +46,7 @@
#include "Widgets/Label.hpp"
#include "format.hpp"
#include "MediaPlayCtrl.h"
#include "MediaFilePanel.h"
#include "StoragePanel.h"
#include "Plater.hpp"
#include "BindDialog.hpp"
@@ -198,9 +197,9 @@ void MonitorPanel::init_tabpanel()
m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF"));
m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) {
auto page = m_tabpanel->GetCurrentPage();
if (page == m_media_file_panel) {
if (page == m_storage_panel) {
auto title = m_tabpanel->GetPageText(m_tabpanel->GetSelection());
m_media_file_panel->SwitchStorage(title == _L("Storage"));
m_storage_panel->SwitchStorage(title == _L("Storage"));
}
// The first page is selected while the panel is built off screen.
if (page->IsShownOnScreen())
@@ -214,8 +213,8 @@ void MonitorPanel::init_tabpanel()
add_build_steps_of(*m_status_info_panel);
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
add_build_step([this] {
m_media_file_panel = new MediaFilePanel(m_tabpanel);
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
m_storage_panel = new StoragePanel(m_tabpanel);
m_tabpanel->AddPage(m_storage_panel, _L("Storage"), false);
});
add_build_step([this] {
m_upgrade_panel = new UpgradePanel(m_tabpanel);
@@ -264,7 +263,8 @@ void MonitorPanel::on_sys_color_changed()
{
m_status_info_panel->on_sys_color_changed();
m_upgrade_panel->on_sys_color_changed();
m_media_file_panel->Rescale();
m_storage_panel->on_sys_color_changed();
m_storage_panel->Rescale();
}
void MonitorPanel::msw_rescale()
@@ -276,7 +276,7 @@ void MonitorPanel::msw_rescale()
m_tabpanel->Rescale();
//m_status_add_machine_panel->msw_rescale();
m_status_info_panel->msw_rescale();
m_media_file_panel->Rescale();
m_storage_panel->Rescale();
m_upgrade_panel->msw_rescale();
m_hms_panel->msw_rescale();
@@ -423,8 +423,8 @@ void MonitorPanel::update_all()
}
} else if (current_page == m_upgrade_panel) {
m_upgrade_panel->update(obj);
} else if (current_page == m_media_file_panel) {
m_media_file_panel->UpdateByObj(obj);
} else if (current_page == m_storage_panel) {
m_storage_panel->UpdateByObj(obj);
}
if (current_page == m_hms_panel || (obj->GetHMS()->GetHMSItems().size() != m_hms_panel->temp_hms_list.size())) {
+2 -2
View File
@@ -60,7 +60,7 @@
namespace Slic3r {
namespace GUI {
class MediaFilePanel;
class StoragePanel;
class AddMachinePanel : public wxPanel
{
@@ -86,7 +86,7 @@ private:
wxSizer* m_main_sizer{ nullptr };
StatusPanel* m_status_info_panel{ nullptr };
MediaFilePanel* m_media_file_panel{ nullptr };
StoragePanel* m_storage_panel{ nullptr };
UpgradePanel* m_upgrade_panel{ nullptr };
HMSPanel* m_hms_panel{ nullptr };
+246
View File
@@ -0,0 +1,246 @@
#include "OrcaFilesPanel.h"
#include "DeviceManager.hpp"
#include "GUI_App.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include "ImageGrid.h"
#include "MsgDialog.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/StaticBox.hpp"
#include "Widgets/StateColor.hpp"
#include <boost/make_shared.hpp>
#include <wx/colour.h>
#include <wx/sizer.h>
#include <utility>
namespace Slic3r {
namespace GUI {
OrcaFilesPanel::OrcaFilesPanel(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
, m_bmp_loading(this, "media_loading", 0)
, m_bmp_failed(this, "media_failed", 0)
, m_bmp_empty(this, "media_empty", 0)
{
const wxColour light_background("#EEEEEE");
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
toolbar->SetMinSize({-1, 75 * em_unit(this) / 10});
m_root_label = new Label(this, _L("gcodes"));
m_root_label->SetFont(Label::Head_16);
toolbar->Add(m_root_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 24);
m_time_panel = new ::StaticBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_time_panel->SetCornerRadius(0);
m_button_year = new ::Button(m_time_panel, _L("Year"), "", wxBORDER_NONE);
m_button_month = new ::Button(m_time_panel, _L("Month"), "", wxBORDER_NONE);
m_button_all = new ::Button(m_time_panel, _L("All Files"), "", wxBORDER_NONE);
m_button_year->SetToolTip(_L("Group files by year, recent first."));
m_button_month->SetToolTip(_L("Group files by month, recent first."));
m_button_all->SetToolTip(_L("Show all files, recent first."));
for (auto* button : {m_button_year, m_button_month, m_button_all}) {
button->SetBackgroundColor(StateColor(
std::make_pair(0xEEEEEE, static_cast<int>(StateColor::Checked)),
std::make_pair(*wxLIGHT_GREY, static_cast<int>(StateColor::Hovered)),
std::make_pair(*wxWHITE, static_cast<int>(StateColor::Normal))));
button->SetTextColor(StateColor(
std::make_pair(0x3B4446, static_cast<int>(StateColor::Checked)),
std::make_pair(*wxLIGHT_GREY, static_cast<int>(StateColor::Hovered)),
std::make_pair(0xABACAC, static_cast<int>(StateColor::Normal))));
button->SetCanFocus(false);
}
auto* time_sizer = new wxBoxSizer(wxHORIZONTAL);
time_sizer->Add(m_button_year, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 24);
time_sizer->Add(m_button_month, 0, wxALIGN_CENTER_VERTICAL);
time_sizer->Add(m_button_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 24);
m_time_panel->SetSizer(time_sizer);
toolbar->AddStretchSpacer(1);
toolbar->Add(m_time_panel, 0, wxALIGN_CENTER_VERTICAL);
toolbar->AddStretchSpacer(1);
m_button_refresh = new ::Button(this, _L("Refresh"));
m_button_refresh->SetToolTip(_L("Reload file list from printer."));
m_button_refresh->SetFont(Label::Body_12);
m_button_refresh->SetCornerRadius(12);
m_button_refresh->SetPaddingSize({10, 6});
m_button_refresh->SetCanFocus(false);
m_button_refresh->SetBorderWidth(0);
toolbar->Add(m_button_refresh, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 24);
sizer->Add(toolbar, 0, wxEXPAND);
m_image_grid = new ImageGrid(this);
// Theme this grid instance without changing the shared Bambu defaults.
m_image_grid->SetBackgroundColour(GetBackgroundColour());
m_image_grid->SetSelecting(false);
m_image_grid->ShowDownload(false);
m_image_grid->SetStatus(m_bmp_failed, _L("Please confirm if the printer is connected."));
sizer->Add(m_image_grid, 1, wxEXPAND);
SetSizer(sizer);
applyThemeColors();
updateGroupButtons(m_group_mode);
auto group_clicked = [this](wxCommandEvent& event) {
const FileGridGroup mode = event.GetEventObject() == m_button_year ? FileGridGroup::Year :
event.GetEventObject() == m_button_month ? FileGridGroup::Month : FileGridGroup::All;
m_image_grid->SetGroupMode(static_cast<int>(mode));
updateGroupButtons(mode);
};
m_button_year->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
m_button_month->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
m_button_all->Bind(wxEVT_COMMAND_BUTTON_CLICKED, group_clicked);
m_button_refresh->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) {
event.Skip();
if (m_model)
m_model->Refresh();
});
m_image_grid->Bind(EVT_ITEM_ACTION, [this](wxCommandEvent& event) {
if (event.GetInt() != 0) // only Delete is wired for this grid
return;
if (!m_model || m_device_id.empty())
return;
const size_t index = static_cast<size_t>(event.GetExtraLong());
if (index >= m_model->GetCount())
return;
const FileGridCard& card = m_model->GetFile(index);
if (card.id.empty())
return;
const std::string path = card.id;
const std::string name = card.name;
MessageDialog dlg(this,
wxString::Format(_L("Do you want to delete the file '%s' from printer?"), from_u8(name)),
_L("Delete file"), wxYES_NO | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return;
m_model->DeleteFile(path, [this](bool ok) {
if (ok)
return;
MessageDialog(this, _L("Failed to delete the file from printer."), _L("Delete file"),
wxOK | wxICON_ERROR).ShowModal();
});
});
}
OrcaFilesPanel::~OrcaFilesPanel()
{
if (m_model)
m_model->SetStatusHandler({});
if (m_model)
m_model->SetGroupModeHandler({});
m_image_grid->SetModel(nullptr);
m_model.reset();
}
void OrcaFilesPanel::UpdateByObj(MachineObject* obj)
{
const std::string device_id = obj ? obj->get_dev_id() : std::string();
if (device_id == m_device_id)
return;
m_device_id = device_id;
if (m_model)
m_model->SetStatusHandler({});
if (m_model)
m_model->SetGroupModeHandler({});
m_image_grid->SetModel(nullptr);
m_model.reset();
if (m_device_id.empty()) {
m_button_refresh->Enable(false);
m_image_grid->SetStatus(m_bmp_failed, _L("Please confirm if the printer is connected."));
return;
}
m_model = boost::make_shared<RemoteFileGridModel>(m_device_id);
m_model->SetStatusHandler([this](RemoteFileGridModel::Status status) { updateStatus(status); });
m_model->SetGroupModeHandler([this](FileGridGroup mode) { updateGroupButtons(mode); });
m_model->SetGroupMode(m_group_mode);
m_image_grid->SetModel(m_model);
m_image_grid->SetFileType(static_cast<int>(FileGridType::Model), {});
m_button_refresh->Enable(true);
m_model->Refresh();
}
void OrcaFilesPanel::updateStatus(RemoteFileGridModel::Status status)
{
switch (status) {
case RemoteFileGridModel::Status::Loading:
m_image_grid->SetStatus(m_bmp_loading, _L("Loading file list..."));
break;
case RemoteFileGridModel::Status::Ready:
break;
case RemoteFileGridModel::Status::Empty:
m_image_grid->SetStatus(m_bmp_empty, _L("No files"));
break;
case RemoteFileGridModel::Status::Failed:
m_image_grid->SetStatus(m_bmp_failed, _L("Load failed"));
break;
}
}
void OrcaFilesPanel::Rescale()
{
m_bmp_loading.msw_rescale();
m_bmp_failed.msw_rescale();
m_bmp_empty.msw_rescale();
auto* toolbar = GetSizer()->GetItem(static_cast<size_t>(0))->GetSizer();
toolbar->SetMinSize({-1, 75 * em_unit(this) / 10});
m_root_label->SetFont(Label::Head_16);
m_button_refresh->Rescale();
m_button_year->Rescale();
m_button_month->Rescale();
m_button_all->Rescale();
m_image_grid->Rescale();
}
void OrcaFilesPanel::on_sys_color_changed()
{
applyThemeColors();
Layout();
Refresh();
}
void OrcaFilesPanel::applyThemeColors()
{
const bool dark = wxGetApp().dark_mode();
const wxColour light_background("#EEEEEE");
const wxColour light_text("#262E30");
const wxColour light_teal("#009688");
const wxColour background = dark ? StateColor::darkModeColorFor(light_background) : light_background;
SetBackgroundColour(background);
m_root_label->SetBackgroundColour(background);
m_root_label->SetForegroundColour(dark ? StateColor::darkModeColorFor(light_text) : light_text);
m_time_panel->SetBackgroundColour(background);
m_button_refresh->SetBackgroundColorNormal(dark ? StateColor::darkModeColorFor(light_teal) : light_teal);
m_button_refresh->SetTextColorNormal(*wxWHITE);
m_image_grid->SetBackgroundColour(background);
m_image_grid->Refresh();
m_button_year->Refresh();
m_button_month->Refresh();
m_button_all->Refresh();
}
void OrcaFilesPanel::updateGroupButtons(FileGridGroup mode)
{
m_group_mode = mode;
::Button* buttons[] = {m_button_all, m_button_month, m_button_year};
const int selected = static_cast<int>(mode);
for (int i = 0; i < 3; ++i) {
buttons[i]->SetValue(i == selected);
buttons[i]->SetFont(i == selected ? Label::Head_14 : Label::Body_14);
}
}
}}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
#include "Printer/RemoteFileGridModel.h"
#include <boost/smart_ptr/shared_ptr.hpp>
#include <wx/panel.h>
class Button;
class Label;
class StaticBox;
namespace Slic3r {
class MachineObject;
namespace GUI {
class ImageGrid;
class OrcaFilesPanel : public wxPanel
{
public:
explicit OrcaFilesPanel(wxWindow* parent);
~OrcaFilesPanel() override;
void UpdateByObj(MachineObject* obj);
void Rescale();
void on_sys_color_changed();
private:
void applyThemeColors();
void updateGroupButtons(FileGridGroup mode);
void updateStatus(RemoteFileGridModel::Status status);
ScalableBitmap m_bmp_loading;
ScalableBitmap m_bmp_failed;
ScalableBitmap m_bmp_empty;
Label* m_root_label = nullptr;
::StaticBox* m_time_panel = nullptr;
::Button* m_button_year = nullptr;
::Button* m_button_month = nullptr;
::Button* m_button_all = nullptr;
::Button* m_button_refresh = nullptr;
ImageGrid* m_image_grid = nullptr;
boost::shared_ptr<RemoteFileGridModel> m_model;
std::string m_device_id;
FileGridGroup m_group_mode = FileGridGroup::All;
};
}}
@@ -0,0 +1,197 @@
#include "BambuFileGridModel.h"
#include "slic3r/GUI/GUI_App.hpp"
#include <utility>
namespace Slic3r {
namespace GUI {
static const FileGridCard& emptyCard()
{
static const FileGridCard card;
return card;
}
BambuFileGridModel::BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs)
: m_fs(std::move(fs))
{
if (m_fs) {
m_fs->Bind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
m_fs->Bind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
m_fs->Bind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
m_fs->Bind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
}
}
BambuFileGridModel::~BambuFileGridModel()
{
if (m_fs) {
m_fs->Unbind(EVT_FILE_CHANGED, &BambuFileGridModel::onFileChanged, this);
m_fs->Unbind(EVT_MODE_CHANGED, &BambuFileGridModel::onModeChanged, this);
m_fs->Unbind(EVT_THUMBNAIL, &BambuFileGridModel::onThumbnail, this);
m_fs->Unbind(EVT_DOWNLOAD, &BambuFileGridModel::onDownload, this);
}
}
void BambuFileGridModel::EnsureCards() const
{
if (!m_dirty && (!m_fs || m_cards.size() == m_fs->GetCount()))
return;
m_dirty = false;
m_cards.clear();
if (!m_fs)
return;
size_t count = m_fs->GetCount();
m_cards.reserve(count);
for (size_t i = 0; i < count; ++i) {
bool selected = false;
PrinterFileSystem::File const &file = m_fs->GetFile(i, selected);
FileGridCard card;
card.id = file.path;
card.name = file.name;
card.title = file.Title();
card.time_text = file.Metadata("Time", "");
card.weight_text = file.Metadata("Weight", "");
card.time = file.time;
card.downloading = file.IsDownload();
card.download_progress = file.DownloadProgress();
card.selected = selected;
card.thumbnail = file.thumbnail;
m_cards.push_back(std::move(card));
}
}
size_t BambuFileGridModel::GetCount() const
{
return m_fs ? m_fs->GetCount() : 0;
}
const FileGridCard& BambuFileGridModel::GetFile(size_t index, bool& selected) const
{
EnsureCards();
if (!m_fs || index >= m_cards.size()) {
selected = false;
return emptyCard();
}
m_fs->GetFile(index, selected);
return m_cards[index];
}
const FileGridCard& BambuFileGridModel::GetFile(size_t index) const
{
EnsureCards();
if (index >= m_cards.size())
return emptyCard();
return m_cards[index];
}
FileGridType BambuFileGridModel::GetFileType() const
{
return m_fs ? static_cast<FileGridType>(m_fs->GetFileType()) : FileGridType::Timelapse;
}
void BambuFileGridModel::SetFileType(FileGridType type, const std::string& storage)
{
if (m_fs)
m_fs->SetFileType(static_cast<PrinterFileSystem::FileType>(type), storage);
}
FileGridGroup BambuFileGridModel::GetGroupMode() const
{
return m_fs ? static_cast<FileGridGroup>(m_fs->GetGroupMode()) : FileGridGroup::All;
}
void BambuFileGridModel::SetGroupMode(FileGridGroup mode)
{
if (m_fs)
m_fs->SetGroupMode(static_cast<PrinterFileSystem::GroupMode>(mode));
}
size_t BambuFileGridModel::GetIndexAtTime(time_t time) const
{
return m_fs ? m_fs->GetIndexAtTime(static_cast<boost::uint32_t>(time)) : 0;
}
size_t BambuFileGridModel::EnterSubGroup(size_t index)
{
return m_fs ? m_fs->EnterSubGroup(index) : index;
}
void BambuFileGridModel::ToggleSelect(size_t index)
{
if (m_fs)
m_fs->ToggleSelect(index);
}
void BambuFileGridModel::SelectAll(bool select)
{
if (m_fs)
m_fs->SelectAll(select);
}
void BambuFileGridModel::SetFocusRange(size_t start, size_t count)
{
if (m_fs)
m_fs->SetFocusRange(start, count);
}
void BambuFileGridModel::Retry()
{
if (m_fs)
m_fs->Retry();
}
int BambuFileGridModel::GetLastError() const
{
return m_fs ? m_fs->GetLastError() : 1;
}
void BambuFileGridModel::DownloadCheckFiles(const std::string& path)
{
if (m_fs)
m_fs->DownloadCheckFiles(path);
}
void BambuFileGridModel::SetChangeHandler(std::function<void(FileGridChange)> handler)
{
m_handler = std::move(handler);
}
void BambuFileGridModel::notify(FileGridChange change)
{
if (m_handler)
m_handler(change);
}
void BambuFileGridModel::onFileChanged(wxCommandEvent& evt)
{
evt.Skip();
if (evt.GetInt() == -1 && m_fs)
m_fs->DownloadCheckFiles(wxGetApp().app_config->get("download_path"));
m_dirty = true;
notify(FileGridChange::Files);
}
void BambuFileGridModel::onModeChanged(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Mode);
}
void BambuFileGridModel::onThumbnail(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Thumbnail);
}
void BambuFileGridModel::onDownload(wxCommandEvent& evt)
{
evt.Skip();
m_dirty = true;
notify(FileGridChange::Download);
}
}}
@@ -0,0 +1,61 @@
#pragma once
#include "IFileGridModel.h"
#include "PrinterFileSystem.h"
#include <boost/smart_ptr/shared_ptr.hpp>
#include <wx/event.h>
#include <cstddef>
#include <ctime>
#include <functional>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
// IFileGridModel over PrinterFileSystem, behavior-identical to ImageGrid's direct use.
class BambuFileGridModel : public wxEvtHandler, public IFileGridModel
{
public:
explicit BambuFileGridModel(boost::shared_ptr<PrinterFileSystem> fs);
~BambuFileGridModel() override;
size_t GetCount() const override;
const FileGridCard& GetFile(size_t index, bool& selected) const override;
const FileGridCard& GetFile(size_t index) const override;
FileGridType GetFileType() const override;
void SetFileType(FileGridType type, const std::string& storage) override;
FileGridGroup GetGroupMode() const override;
void SetGroupMode(FileGridGroup mode) override;
size_t GetIndexAtTime(time_t time) const override;
size_t EnterSubGroup(size_t index) override;
void ToggleSelect(size_t index) override;
void SelectAll(bool select) override;
void SetFocusRange(size_t start, size_t count) override;
void Retry() override;
int GetLastError() const override;
void DownloadCheckFiles(const std::string& path) override;
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
private:
void EnsureCards() const;
void notify(FileGridChange change);
void onFileChanged(wxCommandEvent& evt);
void onModeChanged(wxCommandEvent& evt);
void onThumbnail(wxCommandEvent& evt);
void onDownload(wxCommandEvent& evt);
boost::shared_ptr<PrinterFileSystem> m_fs;
std::function<void(FileGridChange)> m_handler;
mutable std::vector<FileGridCard> m_cards;
mutable bool m_dirty = true;
};
}}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <wx/bitmap.h>
#include <cstddef>
#include <ctime>
#include <functional>
#include <string>
namespace Slic3r {
namespace GUI {
// Mirrors PrinterFileSystem::FileType ordering (F_TIMELAPSE=0, F_VIDEO=1, F_MODEL=2).
enum class FileGridType { Timelapse = 0, Video = 1, Model = 2 };
// Mirrors PrinterFileSystem::GroupMode ordering (G_NONE=0, G_MONTH=1, G_YEAR=2).
enum class FileGridGroup { All = 0, Month = 1, Year = 2 };
// Mirrors the PrinterFileSystem wx events ImageGrid listens to.
enum class FileGridChange { Files, Mode, Thumbnail, Download };
// One rendered card. Neutral: no Bambu transfer internals, no filesystem paths beyond id.
struct FileGridCard {
std::string id;
std::string name;
std::string title;
std::string time_text;
std::string weight_text;
time_t time = 0;
bool downloading = false;
int download_progress = 0; // -1 waiting, <0 failed, 0..100 progress
bool selected = false;
wxBitmap thumbnail;
};
// Data source behind ImageGrid. Implemented today by BambuFileGridModel (wrapping
// PrinterFileSystem); a non-Bambu model can implement the same surface later.
class IFileGridModel {
public:
virtual ~IFileGridModel() = default;
virtual size_t GetCount() const = 0;
virtual const FileGridCard& GetFile(size_t index, bool& selected) const = 0;
virtual const FileGridCard& GetFile(size_t index) const = 0;
virtual FileGridType GetFileType() const = 0;
virtual void SetFileType(FileGridType type, const std::string& storage) = 0;
virtual FileGridGroup GetGroupMode() const = 0;
virtual void SetGroupMode(FileGridGroup mode) = 0;
// When true, the thumbnail is fitted inside the tile instead of stretched to fill it.
virtual bool preserve_thumbnail_aspect() const { return false; }
// When false, a Model tile omits the Print action. Default keeps Bambu behavior.
virtual bool supports_print_action() const { return true; }
virtual size_t GetIndexAtTime(time_t time) const = 0;
virtual size_t EnterSubGroup(size_t index) = 0;
virtual void ToggleSelect(size_t index) = 0;
virtual void SelectAll(bool select) = 0;
virtual void SetFocusRange(size_t start, size_t count) = 0;
virtual void Retry() = 0;
virtual int GetLastError() const = 0;
virtual void DownloadCheckFiles(const std::string& path) = 0;
// ImageGrid sets one handler; the model invokes it on the UI thread on change.
virtual void SetChangeHandler(std::function<void(FileGridChange)>) = 0;
};
}}
@@ -0,0 +1,524 @@
#include "RemoteFileGridModel.h"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include <wx/datetime.h>
#include <wx/image.h>
#include <wx/mstream.h>
#include <boost/format.hpp>
#include <boost/regex.hpp>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <utility>
namespace Slic3r {
namespace GUI {
namespace {
const FileGridCard& emptyCard()
{
static const FileGridCard card;
return card;
}
// Same display format as PrinterFileSystem: "1d2h3m", dropping a leading "0d"/"0h".
std::string durationString(long duration)
{
static boost::regex rx("^0d(0h)?");
auto time = boost::format("%1%d%2%h%3%m") % (duration / 86400) % ((duration % 86400) / 3600) % ((duration % 3600) / 60);
return boost::regex_replace(time.str(), rx, "");
}
// .gcode / .3mf only, case-insensitive; a .gcode.3mf name matches via the .3mf suffix.
bool is_displayable_file(const std::string& name)
{
auto ends_with_ci = [&name](const std::string& suffix) {
return name.size() >= suffix.size() &&
std::equal(suffix.rbegin(), suffix.rend(), name.rbegin(),
[](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
};
return ends_with_ci(".gcode") || ends_with_ci(".3mf");
}
}
RemoteFileGridModel::RemoteFileGridModel(std::string device_id)
: m_device_id(std::move(device_id))
{
}
RemoteFileGridModel::~RemoteFileGridModel()
{
m_lifetime.reset();
}
void RemoteFileGridModel::Refresh()
{
const std::uint64_t request = ++m_request;
const std::string device_id = m_device_id;
m_thumbnail_cache.clear();
m_thumbnail_requested.clear();
m_thumbnail_queue.clear();
m_metadata_cache.clear();
m_metadata_requested.clear();
m_metadata_queue.clear();
for (FileGridCard& card : m_cards)
card.thumbnail = wxBitmap();
m_last_error = 0;
setStatus(Status::Loading);
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent) {
m_last_error = -1;
m_cards.clear();
m_month.clear();
m_year.clear();
setStatus(Status::Failed);
if (m_change_handler)
m_change_handler(FileGridChange::Files);
return;
}
std::weak_ptr<int> lifetime = m_lifetime;
int result = agent->list_printer_files(device_id,
[this, lifetime, request, device_id](int result, std::vector<PrinterFileEntry> files) {
if (lifetime.expired())
return;
onFilesLoaded(request, device_id, result, std::move(files));
});
if (result != 0 && request == m_request && m_status == Status::Loading) {
m_last_error = result;
m_cards.clear();
m_month.clear();
m_year.clear();
setStatus(Status::Failed);
if (m_change_handler)
m_change_handler(FileGridChange::Files);
}
}
void RemoteFileGridModel::onFilesLoaded(std::uint64_t request, const std::string& device_id, int result,
std::vector<PrinterFileEntry> files)
{
if (request != m_request || device_id != m_device_id)
return;
m_last_error = result;
m_cards.clear();
if (result == 0) {
files.erase(std::remove_if(files.begin(), files.end(),
[](const PrinterFileEntry& file) { return !is_displayable_file(file.name); }),
files.end());
std::sort(files.begin(), files.end(), [](const PrinterFileEntry& lhs, const PrinterFileEntry& rhs) {
return lhs.modified > rhs.modified;
});
m_cards.reserve(files.size());
for (const PrinterFileEntry& file : files) {
FileGridCard card;
card.id = file.path;
card.name = file.name;
card.time = static_cast<time_t>(file.modified);
m_cards.emplace_back(std::move(card));
}
buildGroups();
setStatus(m_cards.empty() ? Status::Empty : Status::Ready);
} else {
m_month.clear();
m_year.clear();
setStatus(Status::Failed);
}
if (m_change_handler)
m_change_handler(FileGridChange::Files);
}
void RemoteFileGridModel::setStatus(Status status)
{
m_status = status;
if (m_status_handler)
m_status_handler(status);
}
void RemoteFileGridModel::SetStatusHandler(std::function<void(Status)> handler)
{
m_status_handler = std::move(handler);
}
size_t RemoteFileGridModel::GetCount() const
{
if (m_group_mode == FileGridGroup::Year)
return m_year.size();
if (m_group_mode == FileGridGroup::Month)
return m_month.size();
return m_cards.size();
}
const FileGridCard& RemoteFileGridModel::GetFile(size_t index, bool& selected) const
{
selected = false;
const size_t card_index = cardIndexForGroup(index);
if (card_index >= m_cards.size())
return emptyCard();
FileGridCard& card = m_cards[card_index];
const auto thumbnail = m_thumbnail_cache.find(card.id);
card.thumbnail = thumbnail == m_thumbnail_cache.end() ? wxBitmap() : thumbnail->second;
return card;
}
const FileGridCard& RemoteFileGridModel::GetFile(size_t index) const
{
bool selected = false;
return GetFile(index, selected);
}
size_t RemoteFileGridModel::cardIndexForGroup(size_t index) const
{
if (m_group_mode == FileGridGroup::All)
return index;
if (m_group_mode == FileGridGroup::Month)
return index < m_month.size() ? m_month[index] : m_cards.size();
return index < m_year.size() && m_year[index] < m_month.size() ? m_month[m_year[index]] : m_cards.size();
}
FileGridType RemoteFileGridModel::GetFileType() const
{
return m_file_type;
}
void RemoteFileGridModel::SetFileType(FileGridType type, const std::string&)
{
m_file_type = type;
}
FileGridGroup RemoteFileGridModel::GetGroupMode() const
{
return m_group_mode;
}
void RemoteFileGridModel::SetGroupMode(FileGridGroup mode)
{
if (mode != FileGridGroup::All && mode != FileGridGroup::Month && mode != FileGridGroup::Year)
return;
if (m_group_mode == mode)
return;
m_group_mode = mode;
if (m_group_mode_handler)
m_group_mode_handler(mode);
if (m_change_handler)
m_change_handler(FileGridChange::Mode);
}
size_t RemoteFileGridModel::GetIndexAtTime(time_t time) const
{
if (m_cards.empty())
return 0;
size_t low = 0;
size_t high = m_cards.size();
while (low < high) {
const size_t middle = low + (high - low) / 2;
if (m_cards[middle].time >= time)
low = middle + 1;
else
high = middle;
}
const size_t card_index = low == 0 ? 0 : low - 1;
if (m_group_mode == FileGridGroup::All)
return card_index;
const auto month = std::upper_bound(m_month.begin(), m_month.end(), card_index);
const size_t month_index = month == m_month.begin() ? 0 : static_cast<size_t>(month - m_month.begin() - 1);
if (m_group_mode == FileGridGroup::Month)
return month_index;
const auto year = std::upper_bound(m_year.begin(), m_year.end(), card_index,
[this](size_t file_index, size_t month_index) {
return file_index < m_month[month_index];
});
return year == m_year.begin() ? 0 : static_cast<size_t>(year - m_year.begin() - 1);
}
size_t RemoteFileGridModel::EnterSubGroup(size_t index)
{
if (m_group_mode == FileGridGroup::All)
return index;
if (m_group_mode == FileGridGroup::Year) {
if (index >= m_year.size())
return 0;
index = m_year[index];
} else {
if (index >= m_month.size())
return 0;
index = m_month[index];
}
SetGroupMode(static_cast<FileGridGroup>(static_cast<int>(m_group_mode) - 1));
return index;
}
void RemoteFileGridModel::ToggleSelect(size_t)
{
}
void RemoteFileGridModel::SelectAll(bool)
{
}
void RemoteFileGridModel::SetFocusRange(size_t start, size_t count)
{
if (m_status == Status::Loading || start >= GetCount())
return;
const size_t end = start + std::min(count, GetCount() - start);
for (size_t i = start; i < end; ++i) {
const FileGridCard& card = GetFile(i);
if (!card.id.empty()) {
requestThumbnail(card.id);
requestMetadata(card.id);
}
}
pumpThumbnailQueue();
pumpMetadataQueue();
}
void RemoteFileGridModel::Retry()
{
Refresh();
}
int RemoteFileGridModel::GetLastError() const
{
return m_last_error;
}
void RemoteFileGridModel::DownloadCheckFiles(const std::string&)
{
}
void RemoteFileGridModel::DeleteFile(const std::string& path, std::function<void(bool ok)> done)
{
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent || m_device_id.empty() || path.empty()) {
if (done)
done(false);
return;
}
const std::uint64_t request = m_request;
const std::string device_id = m_device_id;
const std::weak_ptr<int> lifetime = m_lifetime;
const auto completed = std::make_shared<bool>(false);
auto finish = [this, lifetime, completed, request, device_id, path, done = std::move(done)](int result) {
if (*completed)
return;
*completed = true;
if (lifetime.expired())
return;
const bool ok = result == 0;
if (ok && request == m_request && device_id == m_device_id) {
m_thumbnail_cache.erase(path);
m_thumbnail_requested.erase(path);
m_cards.erase(std::remove_if(m_cards.begin(), m_cards.end(),
[&path](const FileGridCard& card) { return card.id == path; }),
m_cards.end());
buildGroups();
setStatus(m_cards.empty() ? Status::Empty : Status::Ready);
if (m_change_handler)
m_change_handler(FileGridChange::Files);
}
if (done)
done(ok);
};
const int result = agent->delete_printer_file(device_id, path, finish);
if (result != 0)
finish(result);
}
void RemoteFileGridModel::SetChangeHandler(std::function<void(FileGridChange)> handler)
{
m_change_handler = std::move(handler);
}
void RemoteFileGridModel::SetGroupModeHandler(std::function<void(FileGridGroup)> handler)
{
m_group_mode_handler = std::move(handler);
}
void RemoteFileGridModel::buildGroups()
{
m_month.clear();
m_year.clear();
if (m_cards.empty())
return;
m_month.push_back(0);
m_year.push_back(0);
wxDateTime previous(static_cast<time_t>(m_cards.front().time));
for (size_t i = 1; i < m_cards.size(); ++i) {
const wxDateTime current(static_cast<time_t>(m_cards[i].time));
if (current.GetYear() != previous.GetYear()) {
m_year.push_back(m_month.size());
m_month.push_back(i);
} else if (current.GetMonth() != previous.GetMonth()) {
m_month.push_back(i);
}
previous = current;
}
}
void RemoteFileGridModel::requestThumbnail(const std::string& path)
{
if (m_thumbnail_cache.find(path) != m_thumbnail_cache.end() || !m_thumbnail_requested.insert(path).second)
return;
m_thumbnail_queue.push_back(path);
}
void RemoteFileGridModel::pumpThumbnailQueue()
{
constexpr size_t MAX_THUMBNAIL_REQUESTS = 4;
if (m_pumping_thumbnail_queue)
return;
m_pumping_thumbnail_queue = true;
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent) {
while (!m_thumbnail_queue.empty()) {
m_thumbnail_cache.emplace(m_thumbnail_queue.front(), wxBitmap());
m_thumbnail_queue.pop_front();
}
m_pumping_thumbnail_queue = false;
return;
}
while (!m_thumbnail_queue.empty() && m_thumbnail_requests_in_flight < MAX_THUMBNAIL_REQUESTS) {
std::string path = std::move(m_thumbnail_queue.front());
m_thumbnail_queue.pop_front();
++m_thumbnail_requests_in_flight;
const std::uint64_t request = m_request;
const std::string device_id = m_device_id;
const std::weak_ptr<int> lifetime = m_lifetime;
const auto completed = std::make_shared<bool>(false);
auto finish = [this, lifetime, completed, request, device_id, path](int result, std::string image) {
if (*completed)
return;
*completed = true;
if (lifetime.expired())
return;
onThumbnailLoaded(request, device_id, path, result, std::move(image));
};
const int result = agent->get_printer_file_thumbnail(device_id, path, finish);
if (result != 0)
finish(result, {});
}
m_pumping_thumbnail_queue = false;
}
void RemoteFileGridModel::onThumbnailLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, std::string image)
{
if (m_thumbnail_requests_in_flight > 0)
--m_thumbnail_requests_in_flight;
if (request == m_request && device_id == m_device_id) {
wxBitmap bitmap;
if (result == 0 && !image.empty()) {
wxMemoryInputStream stream(image.data(), image.size());
wxImage decoded(stream, wxBITMAP_TYPE_PNG);
if (decoded.IsOk())
bitmap = wxBitmap(decoded);
}
m_thumbnail_cache[path] = std::move(bitmap);
if (m_change_handler)
m_change_handler(FileGridChange::Thumbnail);
}
pumpThumbnailQueue();
}
void RemoteFileGridModel::requestMetadata(const std::string& path)
{
if (m_metadata_cache.find(path) != m_metadata_cache.end() || !m_metadata_requested.insert(path).second)
return;
m_metadata_queue.push_back(path);
}
void RemoteFileGridModel::pumpMetadataQueue()
{
constexpr size_t MAX_METADATA_REQUESTS = 4;
if (m_pumping_metadata_queue)
return;
m_pumping_metadata_queue = true;
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent) {
while (!m_metadata_queue.empty()) {
m_metadata_cache.emplace(m_metadata_queue.front(), PrinterFileMetadata{});
m_metadata_queue.pop_front();
}
m_pumping_metadata_queue = false;
return;
}
while (!m_metadata_queue.empty() && m_metadata_requests_in_flight < MAX_METADATA_REQUESTS) {
std::string path = std::move(m_metadata_queue.front());
m_metadata_queue.pop_front();
++m_metadata_requests_in_flight;
const std::uint64_t request = m_request;
const std::string device_id = m_device_id;
const std::weak_ptr<int> lifetime = m_lifetime;
const auto completed = std::make_shared<bool>(false);
auto finish = [this, lifetime, completed, request, device_id, path](int result, PrinterFileMetadata meta) {
if (*completed)
return;
*completed = true;
if (lifetime.expired())
return;
onMetadataLoaded(request, device_id, path, result, meta);
};
const int result = agent->get_printer_file_metadata(device_id, path, finish);
if (result != 0)
finish(result, {});
}
m_pumping_metadata_queue = false;
}
void RemoteFileGridModel::onMetadataLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, PrinterFileMetadata meta)
{
if (m_metadata_requests_in_flight > 0)
--m_metadata_requests_in_flight;
if (request == m_request && device_id == m_device_id) {
m_metadata_cache[path] = meta;
if (result == 0) {
for (FileGridCard& card : m_cards) {
if (card.id != path)
continue;
card.time_text = meta.estimated_time > 0 ? durationString(meta.estimated_time) : std::string();
card.weight_text = meta.filament_weight > 0
? std::to_string(int(std::round(meta.filament_weight))) + 'g'
: std::string();
break;
}
if (m_change_handler)
m_change_handler(FileGridChange::Thumbnail);
}
}
pumpMetadataQueue();
}
}}
@@ -0,0 +1,99 @@
#pragma once
#include "IFileGridModel.h"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace Slic3r {
namespace GUI {
class RemoteFileGridModel : public IFileGridModel
{
public:
enum class Status { Loading, Ready, Empty, Failed };
explicit RemoteFileGridModel(std::string device_id);
~RemoteFileGridModel() override;
void Refresh();
Status GetStatus() const { return m_status; }
void SetStatusHandler(std::function<void(Status)> handler);
void SetGroupModeHandler(std::function<void(FileGridGroup)> handler);
size_t GetCount() const override;
const FileGridCard& GetFile(size_t index, bool& selected) const override;
const FileGridCard& GetFile(size_t index) const override;
FileGridType GetFileType() const override;
void SetFileType(FileGridType type, const std::string& storage) override;
FileGridGroup GetGroupMode() const override;
void SetGroupMode(FileGridGroup mode) override;
size_t GetIndexAtTime(time_t time) const override;
size_t EnterSubGroup(size_t index) override;
void ToggleSelect(size_t index) override;
void SelectAll(bool select) override;
void SetFocusRange(size_t start, size_t count) override;
void Retry() override;
int GetLastError() const override;
void DownloadCheckFiles(const std::string& path) override;
bool preserve_thumbnail_aspect() const override { return true; }
bool supports_print_action() const override { return false; }
// Delete `path` from the printer; `done` reports success on the UI thread.
void DeleteFile(const std::string& path, std::function<void(bool ok)> done);
void SetChangeHandler(std::function<void(FileGridChange)> handler) override;
private:
void setStatus(Status status);
void onFilesLoaded(std::uint64_t request, const std::string& device_id, int result,
std::vector<PrinterFileEntry> files);
void buildGroups();
size_t cardIndexForGroup(size_t index) const;
void requestThumbnail(const std::string& path);
void pumpThumbnailQueue();
void onThumbnailLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, std::string image);
void requestMetadata(const std::string& path);
void pumpMetadataQueue();
void onMetadataLoaded(std::uint64_t request, const std::string& device_id,
const std::string& path, int result, PrinterFileMetadata meta);
std::string m_device_id;
mutable std::vector<FileGridCard> m_cards;
std::vector<size_t> m_month;
std::vector<size_t> m_year;
std::function<void(FileGridChange)> m_change_handler;
std::function<void(Status)> m_status_handler;
std::function<void(FileGridGroup)> m_group_mode_handler;
std::shared_ptr<int> m_lifetime = std::make_shared<int>(0);
std::unordered_map<std::string, wxBitmap> m_thumbnail_cache;
std::unordered_set<std::string> m_thumbnail_requested;
std::deque<std::string> m_thumbnail_queue;
std::unordered_map<std::string, PrinterFileMetadata> m_metadata_cache;
std::unordered_set<std::string> m_metadata_requested;
std::deque<std::string> m_metadata_queue;
std::uint64_t m_request = 0;
size_t m_thumbnail_requests_in_flight = 0;
bool m_pumping_thumbnail_queue = false;
size_t m_metadata_requests_in_flight = 0;
bool m_pumping_metadata_queue = false;
int m_last_error = 0;
FileGridType m_file_type = FileGridType::Timelapse;
FileGridGroup m_group_mode = FileGridGroup::All;
Status m_status = Status::Loading;
};
}}
+110
View File
@@ -0,0 +1,110 @@
#include "StoragePanel.h"
#include "DeviceManager.hpp"
#include "GUI_App.hpp"
#include "MediaFilePanel.h"
#include "OrcaFilesPanel.h"
#include "Widgets/StateColor.hpp"
#include <wx/colour.h>
#include <wx/sizer.h>
namespace Slic3r {
namespace GUI {
StoragePanel::StoragePanel(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
const wxColour light_background("#EEEEEE");
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
SetSizer(new wxBoxSizer(wxVERTICAL));
}
MediaFilePanel* StoragePanel::ensureBambuPanel()
{
if (!m_bambu_panel) {
m_bambu_panel = new MediaFilePanel(this);
m_bambu_panel->Hide();
GetSizer()->Add(m_bambu_panel, 1, wxEXPAND);
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
}
return m_bambu_panel;
}
OrcaFilesPanel* StoragePanel::ensureOrcaPanel()
{
if (!m_orca_panel) {
m_orca_panel = new OrcaFilesPanel(this);
m_orca_panel->Hide();
GetSizer()->Add(m_orca_panel, 1, wxEXPAND);
wxGetApp().UpdateDarkUIWin(m_orca_panel);
}
return m_orca_panel;
}
void StoragePanel::showChild(Child child)
{
if (m_bambu_panel)
m_bambu_panel->Show(child == Child::Bambu);
if (m_orca_panel)
m_orca_panel->Show(child == Child::Orca);
m_active_child = child;
Layout();
}
void StoragePanel::UpdateByObj(MachineObject* obj)
{
// Agent capability controls the page; Bambu remains the fallback for other printers.
Child target = m_active_child;
if (obj)
target = obj->printer_supports_feature("printer_files") ? Child::Orca : Child::Bambu;
else if (target == Child::None)
target = Child::Bambu;
if (target != m_active_child) {
if (m_active_child == Child::Bambu && m_bambu_panel)
m_bambu_panel->UpdateByObj(nullptr);
else if (m_active_child == Child::Orca && m_orca_panel)
m_orca_panel->UpdateByObj(nullptr);
if (target == Child::Bambu)
ensureBambuPanel();
else
ensureOrcaPanel();
showChild(target);
}
if (target == Child::Bambu)
ensureBambuPanel()->UpdateByObj(obj);
else if (target == Child::Orca)
ensureOrcaPanel()->UpdateByObj(obj);
}
void StoragePanel::SwitchStorage(bool external)
{
if (m_bambu_panel)
m_bambu_panel->SwitchStorage(external);
}
void StoragePanel::Rescale()
{
if (m_bambu_panel)
m_bambu_panel->Rescale();
if (m_orca_panel)
m_orca_panel->Rescale();
}
void StoragePanel::on_sys_color_changed()
{
const wxColour light_background("#EEEEEE");
SetBackgroundColour(wxGetApp().dark_mode() ? StateColor::darkModeColorFor(light_background) : light_background);
if (m_bambu_panel)
wxGetApp().UpdateDarkUIWin(m_bambu_panel);
if (m_orca_panel)
wxGetApp().UpdateDarkUIWin(m_orca_panel);
if (m_orca_panel)
m_orca_panel->on_sys_color_changed();
Refresh();
}
}}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <wx/panel.h>
namespace Slic3r {
class MachineObject;
namespace GUI {
class MediaFilePanel;
class OrcaFilesPanel;
class StoragePanel : public wxPanel
{
public:
explicit StoragePanel(wxWindow* parent);
void UpdateByObj(MachineObject* obj);
void SwitchStorage(bool external);
void Rescale();
void on_sys_color_changed();
private:
enum class Child { None, Bambu, Orca };
MediaFilePanel* ensureBambuPanel();
OrcaFilesPanel* ensureOrcaPanel();
void showChild(Child child);
MediaFilePanel* m_bambu_panel = nullptr;
OrcaFilesPanel* m_orca_panel = nullptr;
Child m_active_child = Child::None;
};
}}
+71
View File
@@ -13,6 +13,7 @@
#include <memory>
#include <vector>
#include <functional>
#include <cstdint>
#include <cmath>
#include <nlohmann/json.hpp>
#include <boost/format.hpp>
@@ -68,6 +69,34 @@ enum class CameraStreamMode {
http_snapshot // HTTP endpoint returning one image per request
};
struct PrinterFileEntry {
std::string path; // gcodes-root-relative, e.g. "sub/foo.gcode"
std::string name; // basename for display
std::uint64_t size = 0; // bytes
std::int64_t modified = 0; // unix seconds
};
// Moonraker per-file metadata: print time and filament usage. Missing fields are 0.
struct PrinterFileMetadata {
int estimated_time = 0; // seconds
double filament_total = 0; // millimetres
double filament_weight = 0; // grams
};
// Async listing of the printer's G-code files. The return value reports whether
// the request was dispatched; `callback` is invoked once with the result.
using PrinterFileListFn = std::function<void(int result, std::vector<PrinterFileEntry> files)>;
// Async fetch of one G-code file's embedded thumbnail. `image` is the raw image
// bytes (empty when the file has no thumbnail); result reports dispatch/transfer.
using PrinterFileThumbnailFn = std::function<void(int result, std::string image)>;
// Async deletion of one printer G-code file. `result` reports dispatch and outcome.
using PrinterFileDeleteFn = std::function<void(int result)>;
// Async fetch of one G-code file's Moonraker metadata. `meta` fields default to 0.
using PrinterFileMetadataFn = std::function<void(int result, PrinterFileMetadata meta)>;
/**
* IPrinterAgent - Interface for printer operations.
*
@@ -326,6 +355,48 @@ public:
return -1;
}
/**
* List the printer's G-code files, delivered asynchronously via callback.
* Agents without a file listing report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
{
(void) dev_id;
(void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Fetch one G-code file's embedded thumbnail, delivered asynchronously via
* callback. Agents without thumbnail support report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Delete one G-code file from the printer, delivered asynchronously via
* callback. Agents without file deletion report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Fetch one G-code file's Moonraker metadata (print time and filament usage),
* delivered asynchronously via callback. Agents without metadata support
* report ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED.
*/
virtual int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
{
(void) dev_id; (void) path; (void) callback;
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
/**
* Register callback for fatal HTTP errors.
*/
+28
View File
@@ -1136,4 +1136,32 @@ int NetworkAgent::get_hms_snapshot(std::string dev_id, std::string file_name, st
return -1;
}
int NetworkAgent::list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
{
if (m_printer_agent)
return m_printer_agent->list_printer_files(dev_id, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
{
if (m_printer_agent)
return m_printer_agent->get_printer_file_thumbnail(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
{
if (m_printer_agent)
return m_printer_agent->delete_printer_file(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int NetworkAgent::get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
{
if (m_printer_agent)
return m_printer_agent->get_printer_file_metadata(dev_id, path, callback);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
} // namespace Slic3r
+4
View File
@@ -198,6 +198,10 @@ public:
std::string from_orca_filament_id(const std::string& orca_filament_id) const;
int request_bind_ticket(std::string* ticket);
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback);
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback);
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback);
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback);
private:
struct PrinterCallbacks {
+420
View File
@@ -819,6 +819,8 @@ bool OrcaPrinterAgent::supports_feature(const std::string& dev_id, const std::st
return capabilities.filament_slots;
if (feature == "filament_mapping")
return capabilities.filament_mapping;
if (feature == "printer_files")
return true;
return false;
}
@@ -1735,6 +1737,424 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
return BAMBU_NETWORK_SUCCESS;
}
// Pure normalization of Moonraker's /server/files/list reply: the `result` array
// of {path, modified, size}. `name` is the basename of `path`; malformed entries
// (non-object, missing/empty path) are skipped. size/modified default to 0.
std::vector<PrinterFileEntry> OrcaPrinterAgent::parse_file_list(const std::string& body)
{
std::vector<PrinterFileEntry> files;
const nlohmann::json envelope = nlohmann::json::parse(body, nullptr, false);
if (envelope.is_discarded() || !envelope.is_object())
return files;
const auto result_it = envelope.find("result");
if (result_it == envelope.end() || !result_it->is_array())
return files;
for (const auto& item : *result_it) {
if (!item.is_object())
continue;
const auto path_it = item.find("path");
if (path_it == item.end() || !path_it->is_string())
continue;
const std::string path = path_it->get<std::string>();
if (path.empty())
continue;
PrinterFileEntry entry;
entry.path = path;
entry.name = fs::path(path).filename().string();
const auto size_it = item.find("size");
if (size_it != item.end() && size_it->is_number())
entry.size = size_it->get<std::uint64_t>();
const auto modified_it = item.find("modified");
if (modified_it != item.end() && modified_it->is_number())
entry.modified = static_cast<std::int64_t>(modified_it->get<double>());
files.push_back(std::move(entry));
}
return files;
}
int OrcaPrinterAgent::list_printer_files(const std::string& dev_id, PrinterFileListFn callback)
{
std::string origin;
bool use_ssl = false;
std::string ca_file;
QueueOnMainFn queue;
bool live = false;
{
std::lock_guard<std::mutex> lock(state_mutex);
live = m_current_connection == LAN && m_lan_dev_id == dev_id;
if (live) {
origin = http_origin_from_lan_ws(m_lan_url);
use_ssl = m_lan_use_ssl;
ca_file = m_lan_ca_file;
queue = queue_on_main_fn;
}
}
if (!live) {
if (callback)
callback(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED, {});
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
if (origin.empty()) {
if (callback)
callback(BAMBU_NETWORK_ERR_INVALID_HANDLE, {});
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
// perform_sync blocks, so the request runs off the UI thread. The worker captures
// only values (never `this`). A trusted LAN facade needs no API key.
std::thread([dev_id, origin, use_ssl, ca_file, queue, callback = std::move(callback)]() mutable {
std::string body;
int result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
auto http = Http::get(origin + "/server/files/list?root=gcodes");
http.tls_verify(use_ssl);
if (!ca_file.empty())
http.ca_file(ca_file);
http.timeout_connect(5)
.timeout_max(15)
.on_complete([&](std::string b, unsigned status) {
if (status == 200) {
body = std::move(b);
result = BAMBU_NETWORK_SUCCESS;
}
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: file list request failed status=" << status << " err=" << err;
})
.perform_sync();
std::vector<PrinterFileEntry> files;
if (result == BAMBU_NETWORK_SUCCESS) {
files = parse_file_list(body);
// Empty is a valid listing; an unparseable body is not.
if (nlohmann::json::parse(body, nullptr, false).is_discarded())
result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
if (!callback)
return;
if (queue)
queue([callback, result, files = std::move(files)]() mutable { callback(result, std::move(files)); });
else
callback(result, std::move(files));
}).detach();
return BAMBU_NETWORK_SUCCESS;
}
// Pure pick of the widest thumbnail path from Moonraker's /server/files/thumbnails
// reply. The `result` array is ordered smallest-first, so the largest `width` is
// chosen. The key was renamed across Moonraker versions; both spellings are accepted.
// Malformed replies and entries without a usable path yield an empty string.
std::string OrcaPrinterAgent::parse_thumbnail_path(const std::string& body)
{
const nlohmann::json envelope = nlohmann::json::parse(body, nullptr, false);
if (envelope.is_discarded() || !envelope.is_object())
return {};
const auto result_it = envelope.find("result");
if (result_it == envelope.end() || !result_it->is_array())
return {};
std::string path;
int best_width = -1;
for (const auto& item : *result_it) {
if (!item.is_object())
continue;
const char* key = item.contains("thumbnail_path") ? "thumbnail_path" : "relative_path";
if (!item.contains(key) || !item[key].is_string())
continue;
const int width = (item.contains("width") && item["width"].is_number()) ? item["width"].get<int>() : 0;
if (width > best_width) {
best_width = width;
path = item[key].get<std::string>();
}
}
return path;
}
// Pure normalization of Moonraker's /server/files/metadata reply: the `result`
// object's estimated_time (seconds), filament_total (mm) and filament_weight_total
// (grams). A malformed reply or any non-numeric field defaults to 0.
PrinterFileMetadata OrcaPrinterAgent::parse_file_metadata(const std::string& body)
{
PrinterFileMetadata meta;
const nlohmann::json envelope = nlohmann::json::parse(body, nullptr, false);
if (envelope.is_discarded() || !envelope.is_object())
return meta;
const auto result_it = envelope.find("result");
if (result_it == envelope.end() || !result_it->is_object())
return meta;
const auto time_it = result_it->find("estimated_time");
if (time_it != result_it->end() && time_it->is_number())
meta.estimated_time = static_cast<int>(time_it->get<double>());
const auto total_it = result_it->find("filament_total");
if (total_it != result_it->end() && total_it->is_number())
meta.filament_total = total_it->get<double>();
const auto weight_it = result_it->find("filament_weight_total");
if (weight_it != result_it->end() && weight_it->is_number())
meta.filament_weight = weight_it->get<double>();
return meta;
}
// Percent-encode each '/'-separated segment, leaving the separators themselves
// intact so the caller keeps the directory structure of a gcodes-relative path.
std::string OrcaPrinterAgent::encode_file_path(const std::string& path)
{
std::string encoded;
size_t segment_start = 0;
while (segment_start <= path.size()) {
const size_t segment_end = path.find('/', segment_start);
if (!encoded.empty() || segment_start > 0)
encoded += '/';
encoded += Http::url_encode(path.substr(segment_start, segment_end - segment_start));
if (segment_end == std::string::npos)
break;
segment_start = segment_end + 1;
}
return encoded;
}
// Fetch one file's embedded thumbnail: first resolve its path via the thumbnails
// listing, then download the image bytes. Mirrors list_printer_files for the
// connection snapshot and off-thread marshalling; the worker captures no `this`.
int OrcaPrinterAgent::get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback)
{
std::string origin;
bool use_ssl = false;
std::string ca_file;
QueueOnMainFn queue;
bool live = false;
{
std::lock_guard<std::mutex> lock(state_mutex);
live = m_current_connection == LAN && m_lan_dev_id == dev_id;
if (live) {
origin = http_origin_from_lan_ws(m_lan_url);
use_ssl = m_lan_use_ssl;
ca_file = m_lan_ca_file;
queue = queue_on_main_fn;
}
}
if (!live) {
if (callback)
callback(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED, {});
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
if (origin.empty()) {
if (callback)
callback(BAMBU_NETWORK_ERR_INVALID_HANDLE, {});
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
std::thread([path, origin, use_ssl, ca_file, queue, callback = std::move(callback)]() mutable {
std::string image;
int result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
std::string thumb_body;
bool listed = false;
auto http = Http::get(origin + "/server/files/thumbnails?filename=" + Http::url_encode(path));
http.tls_verify(use_ssl);
if (!ca_file.empty())
http.ca_file(ca_file);
http.timeout_connect(5)
.timeout_max(15)
.on_complete([&](std::string b, unsigned status) {
if (status == 200) {
thumb_body = std::move(b);
listed = true;
}
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: thumbnail list request failed status=" << status << " err=" << err;
})
.perform_sync();
if (listed) {
if (nlohmann::json::parse(thumb_body, nullptr, false).is_discarded()) {
result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
} else {
const std::string thumb_path = parse_thumbnail_path(thumb_body);
if (thumb_path.empty()) {
// No embedded thumbnail: a success the caller caches.
result = BAMBU_NETWORK_SUCCESS;
} else {
// The returned path is relative to the gcodes root, served at
// /server/files/gcodes; an explicit gcodes/ prefix is served at /server/files.
const std::string root = thumb_path.rfind("gcodes/", 0) == 0 ? "/server/files/" : "/server/files/gcodes/";
auto image_http = Http::get(origin + root + encode_file_path(thumb_path));
image_http.tls_verify(use_ssl);
if (!ca_file.empty())
image_http.ca_file(ca_file);
image_http.timeout_connect(5)
.timeout_max(15)
.on_complete([&](std::string b, unsigned status) {
if (status == 200) {
image = std::move(b);
result = BAMBU_NETWORK_SUCCESS;
}
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: thumbnail fetch failed status=" << status << " err=" << err;
})
.perform_sync();
}
}
}
if (!callback)
return;
if (queue)
queue([callback, result, image = std::move(image)]() mutable { callback(result, std::move(image)); });
else
callback(result, std::move(image));
}).detach();
return BAMBU_NETWORK_SUCCESS;
}
// Delete one G-code file via Moonraker's HTTP DELETE endpoint. Mirrors
// list_printer_files for the connection snapshot and off-thread marshalling; the
// worker captures no `this`.
int OrcaPrinterAgent::delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback)
{
std::string origin;
bool use_ssl = false;
std::string ca_file;
QueueOnMainFn queue;
bool live = false;
{
std::lock_guard<std::mutex> lock(state_mutex);
live = m_current_connection == LAN && m_lan_dev_id == dev_id;
if (live) {
origin = http_origin_from_lan_ws(m_lan_url);
use_ssl = m_lan_use_ssl;
ca_file = m_lan_ca_file;
queue = queue_on_main_fn;
}
}
if (!live) {
if (callback)
callback(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
if (origin.empty()) {
if (callback)
callback(BAMBU_NETWORK_ERR_INVALID_HANDLE);
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
std::thread([path, origin, use_ssl, ca_file, queue, callback = std::move(callback)]() mutable {
int result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
auto http = Http::del(origin + "/server/files/gcodes/" + encode_file_path(path));
http.tls_verify(use_ssl);
if (!ca_file.empty())
http.ca_file(ca_file);
http.timeout_connect(5)
.timeout_max(15)
.on_complete([&](std::string, unsigned status) {
if (status == 200)
result = BAMBU_NETWORK_SUCCESS;
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: file delete request failed status=" << status << " err=" << err;
})
.perform_sync();
if (!callback)
return;
if (queue)
queue([callback, result]() mutable { callback(result); });
else
callback(result);
}).detach();
return BAMBU_NETWORK_SUCCESS;
}
// Fetch one file's Moonraker metadata (print time and filament usage). Mirrors
// list_printer_files for the connection snapshot and off-thread marshalling; the
// worker captures no `this`.
int OrcaPrinterAgent::get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback)
{
std::string origin;
bool use_ssl = false;
std::string ca_file;
QueueOnMainFn queue;
bool live = false;
{
std::lock_guard<std::mutex> lock(state_mutex);
live = m_current_connection == LAN && m_lan_dev_id == dev_id;
if (live) {
origin = http_origin_from_lan_ws(m_lan_url);
use_ssl = m_lan_use_ssl;
ca_file = m_lan_ca_file;
queue = queue_on_main_fn;
}
}
if (!live) {
if (callback)
callback(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED, {});
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
if (origin.empty()) {
if (callback)
callback(BAMBU_NETWORK_ERR_INVALID_HANDLE, {});
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
std::thread([path, origin, use_ssl, ca_file, queue, callback = std::move(callback)]() mutable {
PrinterFileMetadata meta;
int result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
auto http = Http::get(origin + "/server/files/metadata?filename=" + Http::url_encode(path));
http.tls_verify(use_ssl);
if (!ca_file.empty())
http.ca_file(ca_file);
http.timeout_connect(5)
.timeout_max(15)
.on_complete([&](std::string b, unsigned status) {
if (status == 200) {
if (nlohmann::json::parse(b, nullptr, false).is_discarded())
result = BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
else {
meta = parse_file_metadata(b);
result = BAMBU_NETWORK_SUCCESS;
}
}
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: file metadata request failed status=" << status << " err=" << err;
})
.perform_sync();
if (!callback)
return;
if (queue)
queue([callback, result, meta]() mutable { callback(result, meta); });
else
callback(result, meta);
}).detach();
return BAMBU_NETWORK_SUCCESS;
}
// Upload the sliced G-code, then start it: the LAN "print now" path.
int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
+20
View File
@@ -37,6 +37,10 @@ public:
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
int list_printer_files(const std::string& dev_id, PrinterFileListFn callback) override;
int get_printer_file_thumbnail(const std::string& dev_id, const std::string& path, PrinterFileThumbnailFn callback) override;
int delete_printer_file(const std::string& dev_id, const std::string& path, PrinterFileDeleteFn callback) override;
int get_printer_file_metadata(const std::string& dev_id, const std::string& path, PrinterFileMetadataFn callback) override;
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -162,6 +166,22 @@ protected:
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
const std::string& target,
const nlohmann::json& filament_mapping);
// Pure JSON -> entries normalization for OrcaSonar's /server/files/list reply.
// protected static so the test Probe reaches it.
static std::vector<PrinterFileEntry> parse_file_list(const std::string& body);
// Pick the widest thumbnail path from OrcaSonar's /server/files/thumbnails
// reply (the array is smallest-first). Empty when none carry a path.
static std::string parse_thumbnail_path(const std::string& body);
// Pure JSON -> metadata normalization for OrcaSonar's /server/files/metadata
// reply. Missing or malformed fields default to 0. protected static for the Probe.
static PrinterFileMetadata parse_file_metadata(const std::string& body);
// Percent-encode each '/'-separated segment for a Moonraker URL while keeping
// the separators intact. protected static for the test Probe.
static std::string encode_file_path(const std::string& path);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
@@ -1,4 +1,5 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <slic3r/GUI/FilamentMappingUtils.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
@@ -26,6 +27,10 @@ struct Probe : OrcaPrinterAgent {
using OrcaPrinterAgent::build_filament_mapping;
using OrcaPrinterAgent::build_gcode_file_payload;
using OrcaPrinterAgent::prepare_outgoing_request;
using OrcaPrinterAgent::parse_file_list;
using OrcaPrinterAgent::parse_thumbnail_path;
using OrcaPrinterAgent::encode_file_path;
using OrcaPrinterAgent::parse_file_metadata;
};
}
@@ -173,6 +178,125 @@ TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaP
CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0);
}
TEST_CASE("OrcaPrinterAgent::parse_file_list normalizes Moonraker entries", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files = Probe::parse_file_list(R"({
"result": [
{"path": "sub/foo.gcode", "modified": 1700000000.75, "size": 1234, "permissions": "rw"},
{"path": "bar.gcode", "modified": 42, "size": 7}
]
})");
REQUIRE(files.size() == 2);
CHECK(files[0].path == "sub/foo.gcode");
CHECK(files[0].name == "foo.gcode");
CHECK(files[0].size == 1234);
CHECK(files[0].modified == 1700000000); // fractional seconds truncate
CHECK(files[1].path == "bar.gcode");
CHECK(files[1].name == "bar.gcode");
CHECK(files[1].size == 7);
CHECK(files[1].modified == 42);
}
TEST_CASE("OrcaPrinterAgent::parse_file_list handles an empty result", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_file_list(R"({"result": []})").empty());
}
TEST_CASE("OrcaPrinterAgent::parse_file_list rejects malformed JSON", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_file_list("not json").empty());
CHECK(Probe::parse_file_list(R"({"result": "nope"})").empty());
}
TEST_CASE("OrcaPrinterAgent::parse_file_list skips entries missing fields", "[OrcaPrinterAgent]") {
const std::vector<Slic3r::PrinterFileEntry> files = Probe::parse_file_list(R"({
"result": [
{"size": 5},
{"path": ""},
{"path": "kept.gcode"},
"not-an-object"
]
})");
REQUIRE(files.size() == 1);
CHECK(files[0].path == "kept.gcode");
CHECK(files[0].name == "kept.gcode");
CHECK(files[0].size == 0);
CHECK(files[0].modified == 0);
}
TEST_CASE("OrcaPrinterAgent::parse_thumbnail_path picks the largest width", "[OrcaPrinterAgent]") {
const std::string path = Probe::parse_thumbnail_path(R"({
"result": [
{"width": 32, "height": 32, "thumbnail_path": ".thumbs/foo.gcode-32x32.png"},
{"width": 300, "height": 300, "thumbnail_path": ".thumbs/foo.gcode-300x300.png"},
{"width": 100, "height": 100, "thumbnail_path": ".thumbs/foo.gcode-100x100.png"}
]
})");
CHECK(path == ".thumbs/foo.gcode-300x300.png");
}
TEST_CASE("OrcaPrinterAgent::parse_thumbnail_path accepts both key spellings", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_thumbnail_path(R"({"result": [{"width": 32, "relative_path": ".thumbs/old.png"}]})") == ".thumbs/old.png");
CHECK(Probe::parse_thumbnail_path(R"({"result": [{"width": 32, "thumbnail_path": ".thumbs/new.png"}]})") == ".thumbs/new.png");
}
TEST_CASE("OrcaPrinterAgent::parse_thumbnail_path handles an empty result", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_thumbnail_path(R"({"result": []})").empty());
}
TEST_CASE("OrcaPrinterAgent::parse_thumbnail_path rejects malformed JSON", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_thumbnail_path("not json").empty());
CHECK(Probe::parse_thumbnail_path(R"({"result": "nope"})").empty());
}
TEST_CASE("OrcaPrinterAgent::parse_thumbnail_path skips an entry without a path", "[OrcaPrinterAgent]") {
CHECK(Probe::parse_thumbnail_path(R"({"result": [{"width": 300, "height": 300}]})").empty());
CHECK(Probe::parse_thumbnail_path(R"({"result": [{"width": 300, "thumbnail_path": 7}]})").empty());
}
TEST_CASE("OrcaPrinterAgent::encode_file_path preserves separators and encodes segments", "[OrcaPrinterAgent]") {
CHECK(Probe::encode_file_path("foo.gcode") == "foo.gcode"); // flat path
CHECK(Probe::encode_file_path("sub/foo.gcode") == "sub/foo.gcode"); // '/' kept as separator
CHECK(Probe::encode_file_path("a/b/c.gcode") == "a/b/c.gcode");
CHECK(Probe::encode_file_path("sub dir/my file #1.gcode") == "sub%20dir/my%20file%20%231.gcode");
CHECK(Probe::encode_file_path("design+part.gcode") == "design%2Bpart.gcode");
}
TEST_CASE("OrcaPrinterAgent::parse_file_metadata parses the Moonraker fields", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata meta = Probe::parse_file_metadata(R"({
"result": {
"filename": "sub/foo.gcode",
"size": 1234,
"modified": 1700000000.5,
"estimated_time": 3725,
"filament_total": 10500.5,
"filament_weight_total": 31.6,
"thumbnails": []
}
})");
CHECK(meta.estimated_time == 3725);
CHECK_THAT(meta.filament_total, WithinAbs(10500.5, 1e-9));
CHECK_THAT(meta.filament_weight, WithinAbs(31.6, 1e-9));
}
TEST_CASE("OrcaPrinterAgent::parse_file_metadata defaults missing fields to zero", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata meta = Probe::parse_file_metadata(R"({"result": {"filename": "foo.gcode"}})");
CHECK(meta.estimated_time == 0);
CHECK_THAT(meta.filament_total, WithinAbs(0.0, 1e-12));
CHECK_THAT(meta.filament_weight, WithinAbs(0.0, 1e-12));
}
TEST_CASE("OrcaPrinterAgent::parse_file_metadata yields zeros for a malformed reply", "[OrcaPrinterAgent]") {
using Catch::Matchers::WithinAbs;
const Slic3r::PrinterFileMetadata malformed = Probe::parse_file_metadata("not json");
CHECK(malformed.estimated_time == 0);
CHECK_THAT(malformed.filament_total, WithinAbs(0.0, 1e-12));
CHECK_THAT(malformed.filament_weight, WithinAbs(0.0, 1e-12));
const Slic3r::PrinterFileMetadata wrong_type = Probe::parse_file_metadata(R"({"result": "nope"})");
CHECK(wrong_type.estimated_time == 0);
CHECK_THAT(wrong_type.filament_total, WithinAbs(0.0, 1e-12));
}
TEST_CASE("filament mapping is keyed by the ams_mapping2 array position", "[OrcaPrinterAgent]") {
const nlohmann::json mapping = Probe::build_filament_mapping(
R"([{"ams_id":1,"slot_id":5},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");