mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 23:01:17 +00:00
OrcaPrinterAgent Storage Tab
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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())) {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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}])");
|
||||
|
||||
Reference in New Issue
Block a user