diff --git a/resources/web/dialog/PluginsDialog/index.html b/resources/web/dialog/PluginsDialog/index.html
index ae4ffc8b9e..8981a7d62d 100644
--- a/resources/web/dialog/PluginsDialog/index.html
+++ b/resources/web/dialog/PluginsDialog/index.html
@@ -18,7 +18,7 @@
-
+
diff --git a/resources/web/homepage/js/home.js b/resources/web/homepage/js/home.js
index 188a9dded2..75d1af232f 100644
--- a/resources/web/homepage/js/home.js
+++ b/resources/web/homepage/js/home.js
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn);
+
+ if( bOn )
+ {
+ $(".BtnItem").removeClass("BtnItemSelected");
+ $(".BtnShortcut").addClass("BtnItemSelected");
+ }
}
function GotoCloud()
{
- $(".BtnItem").removeClass("BtnItemSelected");
- $(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud");
}
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index e764a9ea7e..5e14d587a5 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -5467,12 +5467,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
- // Deferred: set_cloud_mode() may create a second native webview, which must
- // not run inside this webview's script-message callback.
- CallAfter([this]() {
- if (WebViewPanel* home = WebViewPanel::if_built())
- home->set_cloud_mode(true, cloud_webview_handoff());
- });
+ this->open_orca_cloud();
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
@@ -5621,6 +5616,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
return "";
}
+void GUI_App::open_orca_cloud(const std::string& next)
+{
+ // Remembered so a login while the pane is open re-seeds the same page rather than the default.
+ m_cloud_open_path = next;
+ // Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
+ // the caller's frame (a webview script-message callback or an ImGui frame).
+ CallAfter([this]() {
+ if (mainframe != nullptr)
+ mainframe->select_tab(TAB_ID_HOME);
+ // The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
+ // it if needed, matching what showing the tab does, rather than dropping the open request.
+ if (WebViewPanel* home = WebViewPanel::ensure())
+ home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
+ });
+}
+
void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{
try {
@@ -5898,7 +5909,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/
}
-wxString GUI_App::cloud_webview_handoff()
+wxString GUI_App::cloud_webview_handoff(const std::string& next)
{
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5906,7 +5917,7 @@ wxString GUI_App::cloud_webview_handoff()
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return {};
- return from_u8(orca->build_web_session_start_url());
+ return from_u8(orca->build_web_session_start_url(next));
}
void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6049,7 +6060,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active())
- home->set_cloud_mode(true, cloud_webview_handoff());
+ home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
}
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index 475bec7e26..1b60a98cf1 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -365,6 +365,8 @@ private:
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts").
std::string m_cloud_handoff_state;
+ // The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
+ std::string m_cloud_open_path;
uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
@@ -466,6 +468,9 @@ public:
#if wxUSE_WEBVIEW_EDGE
void init_webview_runtime();
#endif
+ // Switches to the Home tab and opens the OrcaCloud pane beside the homepage; work is deferred
+ // internally. `next` is a same-site OrcaCloud path (empty = the page's default).
+ void open_orca_cloud(const std::string& next = std::string());
static unsigned get_colour_approx_luma(const wxColour& colour);
static bool dark_mode();
const wxColour get_label_default_clr_system();
@@ -572,7 +577,7 @@ public:
// called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview.
- wxString cloud_webview_handoff();
+ wxString cloud_webview_handoff(const std::string& next = std::string());
void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
index 338ec80ffb..89e1653a37 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
@@ -40,6 +40,7 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "GLGizmoUtils.hpp"
+#include
#include
#include
#include
@@ -121,11 +122,16 @@ void pop_slim_scrollbar_style()
}
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
-std::string ellipsize(std::string s, float max_w)
+std::string ellipsize(std::string s, float max_w, float font_size = 0.f)
{
- if (ImGui::CalcTextSize(s.c_str()).x <= max_w)
+ const auto text_width = [&](const std::string &text) {
+ return font_size > 0.f
+ ? ImGui::GetFont()->CalcTextSizeA(font_size, std::numeric_limits::max(), 0.f, text.c_str()).x
+ : ImGui::CalcTextSize(text.c_str()).x;
+ };
+ if (text_width(s) <= max_w)
return s;
- while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) {
+ while (!s.empty() && text_width(s + "...") > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast(s.back());
@@ -4324,6 +4330,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
std::optional chosen;
std::optional to_remove;
bool import_clicked = false;
+ bool download_clicked = false;
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4407,25 +4414,48 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
return n;
};
- // Every section collapses; the state is remembered for the app run. Looked up by section name - the
- // fixed Built-in / My textures keys, or a pack's display name. A capability name is unique per type
- // across loaded packages (PluginManager::check_registry_locked, PluginManager.cpp:211-225), and it is
- // stable across reinstalls, unlike the plugin_key, which is the wheel filename stem and so carries the
- // version: keying on that would silently reset the user's collapse choice on every pack upgrade. A
- // function-local static lives exactly as long as the app and never touches app_config: remembered
- // while the popup closes and reopens, reset on restart.
+ // Every section collapses; the state is remembered for the app run. Pack keys use pack_name, a
+ // capability name unique per type across loaded packages and stable across reinstalls. This function-local
+ // static survives popup closes, never touches app_config, and resets on restart.
static std::map s_section_collapsed; // true = collapsed; absent = expanded
+ const auto split_pack_title = [](const std::string &title) -> std::pair {
+ size_t split = std::string::npos;
+ for (size_t i = 0; i + 3 < title.size(); ++i)
+ if (title[i] == ' ' && (title[i + 1] == 'b' || title[i + 1] == 'B') &&
+ (title[i + 2] == 'y' || title[i + 2] == 'Y') && title[i + 3] == ' ')
+ split = i;
+ if (split == std::string::npos)
+ return { title, {} };
+
+ std::string name = title.substr(0, split);
+ while (!name.empty() && std::isspace(static_cast(name.back())))
+ name.pop_back();
+ std::string attribution = title.substr(split);
+ size_t first = 0;
+ while (first < attribution.size() && std::isspace(static_cast(attribution[first])))
+ ++first;
+ attribution.erase(0, first);
+ if (name.empty() || attribution.empty())
+ return { title, {} };
+ return { name, attribution };
+ };
+
// A section heading, one row as wide as the grid: a click anywhere on it collapses or expands the
- // section. The chevron takes the accent of the panel's More settings link, and the count at the right
- // says what a collapsed section holds. The grid follows closer than the next heading does, so each
- // heading reads as part of its own grid.
- // Returns true when the section is open, so the caller knows whether to draw its grid.
- const auto section_header = [&](const std::string &key, const std::string &title, size_t count) {
+ // section. Child headings indent their contents while keeping their counts right-aligned. Attribution
+ // lines use a smaller disabled style; the grid follows closer than the next heading does.
+ // Returns true when the section is open, so the caller knows whether to draw its contents.
+ const auto section_header = [&](const std::string &key, const std::string &title, size_t count, float indent,
+ const std::string &attribution, const std::string &full_title) {
bool &collapsed = s_section_collapsed[key]; // default-inserts false -> expanded
const ImVec2 row = ImGui::GetCursorScreenPos();
- const ImVec2 row_max(row.x + grid_w, row.y + frame_h);
- if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, frame_h)))
+ const bool two_lines = !attribution.empty();
+ const float line_h = ImGui::GetTextLineHeight();
+ const float small_font_size = ImGui::GetFontSize() * 0.8f;
+ const float small_line_h = line_h * 0.8f;
+ const float row_h = two_lines ? std::max(frame_h, line_h + small_line_h + spacing) : frame_h;
+ const ImVec2 row_max(row.x + grid_w, row.y + row_h);
+ if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, row_h)))
collapsed = !collapsed;
const bool hovered = ImGui::IsItemHovered();
if (hovered)
@@ -4433,7 +4463,9 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
const float inset = std::round(0.25f * frame_h);
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
- const float cx = row.x + inset + 0.5f * arrow, cy = row.y + 0.5f * frame_h, hs = 0.3f * arrow;
+ const float cx = row.x + inset + indent + 0.5f * arrow;
+ const float cy = row.y + 0.5f * row_h;
+ const float hs = 0.3f * arrow;
const ImU32 accent = ImGui::GetColorU32(orca);
if (collapsed)
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx + 0.5f * hs, cy), ImVec2(cx - 0.5f * hs, cy + hs), accent);
@@ -4442,34 +4474,36 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
const std::string n = std::to_string(count);
const float n_w = ImGui::CalcTextSize(n.c_str()).x;
- const float ty = row.y + 0.5f * (frame_h - ImGui::GetTextLineHeight());
- const float tx = row.x + inset + arrow + spacing;
- const std::string shown = ellipsize(title, std::max(0.f, row_max.x - inset - n_w - spacing - tx));
- dl->AddText(ImVec2(tx, ty), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
- dl->AddText(ImVec2(row_max.x - inset - n_w, ty), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
- if (hovered && shown != title)
- panel_tooltip(title); // a long plugin-supplied name, cut by the fixed width
+ const float tx = row.x + inset + indent + arrow + spacing;
+ const float max_text_w = std::max(0.f, row_max.x - inset - n_w - spacing - tx);
+ const std::string shown = ellipsize(title, max_text_w);
+ const std::string shown_attribution = two_lines ? ellipsize(attribution, max_text_w, small_font_size) : std::string{};
+ const float title_y = two_lines ? row.y + 0.5f * (row_h - line_h - small_line_h)
+ : row.y + 0.5f * (frame_h - line_h);
+ const float count_y = row.y + 0.5f * (row_h - line_h);
+ dl->AddText(ImVec2(tx, title_y), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
+ if (two_lines)
+ dl->AddText(ImGui::GetFont(), small_font_size, ImVec2(tx, title_y + line_h),
+ ImGui::GetColorU32(ImGuiCol_TextDisabled), shown_attribution.c_str());
+ dl->AddText(ImVec2(row_max.x - inset - n_w, count_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
+ if (hovered && (shown != title || shown_attribution != attribution))
+ panel_tooltip(full_title); // a long plugin-supplied name, cut by the fixed width
ImGui::SetCursorScreenPos(ImVec2(row.x, row_max.y + spacing));
return !collapsed;
};
- // One section per source: Built-in first, then one per pack, then My textures last (so the import tile
- // and the help line keep describing the group they sit in). Pack entries are grouped by pack_name,
- // which is the capability's get_name(): unique per capability type across loaded packages
- // (PluginManager::check_registry_locked, PluginManager.cpp:211-225), so it identifies a pack both for
- // grouping and for the remembered collapse state, and stands in for the section title. A section is
- // identified by `builtin:`, `user:` or `pack:`, so a pack named "builtin" cannot collide with
- // the fixed Built-in section.
+ // Built-in is first, plugin packs are grouped under Resource Texture, and My
+ // textures stays last so its import tile and help line describe that group. Pack names identify both
+ // their child section and state.
struct Section
{
std::string key;
std::string title;
- TextureSource source;
std::vector entries;
};
std::vector sections;
std::map pack_section; // pack_name -> index in `sections`
- sections.push_back({ "builtin:", _u8L("Built-in"), TextureSource::Shipped, {} });
+ sections.push_back({ "builtin:", _u8L("Built-in"), {} });
for (const TextureLibraryEntry &entry : library) {
if (entry.source == TextureSource::Shipped) {
sections.front().entries.push_back(&entry); // the section exists even when the library is empty
@@ -4477,51 +4511,83 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
}
if (entry.source == TextureSource::User)
continue; // added below, so My textures stays last
+ if (entry.source != TextureSource::Plugin)
+ continue;
const auto [it, inserted] = pack_section.emplace(entry.pack_name, sections.size());
if (inserted)
- sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, TextureSource::Plugin, {} });
+ sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, {} });
sections[it->second].entries.push_back(&entry);
}
- sections.push_back({ "user:", _u8L("My textures"), TextureSource::User, {} });
+ sections.push_back({ "user:", _u8L("My textures"), {} });
for (const TextureLibraryEntry &entry : library)
if (entry.source == TextureSource::User)
sections.back().entries.push_back(&entry);
- for (const Section §ion : sections) {
- if (!section_header(section.key, section.title, section.entries.size()))
- continue;
+ const float child_indent = std::round(m_imgui->scaled(0.8f));
+ const Section &built_in = sections.front();
+ if (section_header(built_in.key, built_in.title, built_in.entries.size(), 0.f, {}, built_in.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
- const int count = grid(origin, section.entries, section.source == TextureSource::User);
- if (section.source == TextureSource::User) {
- ImGui::SetCursorScreenPos(tile_pos(origin, count));
- if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
- import_clicked = true;
- if (ImGui::IsItemHovered())
- panel_tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
- "bakes from and saved to your personal texture folder, kept separate from the "
- "textures shipped with OrcaSlicer."));
- end_grid(origin, count + 1);
- // Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
- // which live here, so it is hidden with them rather than left describing controls off-screen.
- ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
- ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
- ImGui::PopTextWrapPos();
- } else {
+ const int count = grid(origin, built_in.entries, false);
+ end_grid(origin, count);
+ if (count == 0)
+ ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
+ }
+
+ size_t subscribed_count = 0;
+ for (size_t i = 1; i + 1 < sections.size(); ++i)
+ subscribed_count += sections[i].entries.size();
+
+ const std::string subscribed_title = _u8L("Resource Texture");
+ if (section_header("subscribed:", subscribed_title, subscribed_count, 0.f, {}, subscribed_title)) {
+ for (size_t i = 1; i + 1 < sections.size(); ++i) {
+ const Section &pack = sections[i];
+ const auto [name, attribution] = split_pack_title(pack.title);
+ if (!section_header(pack.key, name, pack.entries.size(), child_indent, attribution, pack.title))
+ continue;
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const int count = grid(origin, pack.entries, false);
end_grid(origin, count);
- if (section.source == TextureSource::Shipped && count == 0)
- ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
}
+
+ ImGui::Dummy(ImVec2(0.f, spacing));
+ const std::string download_label = _u8L("Download from OrcaCloud");
+ if (ImGui::Button(download_label.c_str(), ImVec2(grid_w, frame_h)))
+ download_clicked = true;
+ if (ImGui::IsItemHovered())
+ panel_tooltip(_u8L("Opens OrcaCloud in the Home page to browse for plugins."));
+ }
+
+ const Section &user = sections.back();
+ if (section_header(user.key, user.title, user.entries.size(), 0.f, {}, user.title)) {
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const int count = grid(origin, user.entries, true);
+ ImGui::SetCursorScreenPos(tile_pos(origin, count));
+ if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
+ import_clicked = true;
+ if (ImGui::IsItemHovered())
+ panel_tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
+ "bakes from and saved to your personal texture folder, kept separate from the "
+ "textures shipped with OrcaSlicer."));
+ end_grid(origin, count + 1);
+ // Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
+ // which live here, so it is hidden with them rather than left describing controls off-screen.
+ ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
+ ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
+ ImGui::PopTextWrapPos();
}
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false;
ImGui::EndChild();
pop_slim_scrollbar_style();
- if (chosen || import_clicked || close_clicked)
+ if (chosen || import_clicked || close_clicked || download_clicked)
ImGui::CloseCurrentPopup();
m_tooltip_anchor_min = prev_anchor_min;
m_tooltip_anchor_max = prev_anchor_max;
ImGui::EndPopup();
+ // The OrcaCloud plugin hub filtered to resource textures; the pane's session handoff redirects here.
+ if (download_clicked)
+ wxGetApp().open_orca_cloud("/app/plugins/plugin-hub?types=resource-textures");
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
// and removing all change the library the loop was iterating.
diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp
index fe7bd0fdfb..2b2797e88a 100644
--- a/src/slic3r/GUI/WebViewDialog.cpp
+++ b/src/slic3r/GUI/WebViewDialog.cpp
@@ -556,6 +556,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
load_cloud_after_signout(url);
}
m_cloud_browser->Show();
+ // The homepage shell owns the nav highlight and the cloud-mode layout class. A native open
+ // (not the page's own GotoCloud) must ask it to switch; the page's SetCloudMode no-ops if it
+ // already did. OnDocumentLoaded re-applies this when the homepage was still loading.
+ RunScript("SetCloudMode(true)");
} else if (m_cloud_browser) {
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
// again (guide §10). Nothing is cleared here.