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.