diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index c5caf0d423..7382c075a3 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -390,7 +390,7 @@ function favDigitFromEvent(e) { function resultCountText(total, shown, query) { return (query || "").trim() ? - T("sd_result_count", "Showing %s of %s actions", shown, total) : + T("sd_result_count", "Showing %s actions", shown) : T("sd_result_count_all", "%s actions", total); } @@ -711,7 +711,7 @@ window.HandleStudio = function (payload) { builtKey = ""; if (qEl) { qEl.value = ""; - qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length); + qEl.placeholder = T("sd_search", "Search actions"); syncClearButton(); } render({ resize: true, resetScroll: true }); @@ -1376,7 +1376,7 @@ function exitPhase() { // It survives a second-phase exit (which never goes through exitPhase from the commands view), // so without a reset the cached empty-query key would skip the rebuild and leave stale content. builtKey = ""; - qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length); + qEl.placeholder = T("sd_search", "Search actions"); render({ resize: true, resetScroll: true }); qEl.focus(); } diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 03099df91b..93d296098d 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -436,4 +436,9 @@ assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored"); assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored"); +// resultCountText: a search counts the shown matches only ("Showing N actions"); the total is used +// solely for the empty-query count. +assert.equal(ctx.resultCountText(100, 3, "lay"), "Showing 3 actions", "a search reports the shown match count only"); +assert.equal(ctx.resultCountText(100, 100, ""), "100 actions", "an empty query reports the total"); + console.log("ok"); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 7be8bee123..1d81021a60 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,6 +9,8 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" +#include "slic3r/Utils/MacDarkMode.hpp" + #include #include @@ -82,39 +84,38 @@ nlohmann::json speed_dial_ui_strings() {"shortcut_alt", alt}, {"shortcut_ctrl", ctrl}, - {"sd_search", _u8L("Search actions")}, - {"sd_clear", _u8L("Clear")}, - {"sd_search_n", _u8L("Search %s actions")}, - {"sd_recent", _u8L("Recent")}, - {"sd_plugins", _u8L("Plugins")}, - {"sd_other", _u8L("Other")}, - {"sd_no_match_total", _u8L("No actions match (Total: %s)")}, - {"sd_no_actions", _u8L("No actions yet")}, - {"sd_no_tabs_match", _u8L("No tabs match")}, - {"sd_no_tabs", _u8L("No tabs")}, - {"sd_result_count", _u8L("Showing %s of %s actions")}, + {"sd_search", _u8L("Search actions")}, + {"sd_clear", _u8L("Clear")}, + {"sd_recent", _u8L("Recent")}, + {"sd_plugins", _u8L("Plugins")}, + {"sd_other", _u8L("Other")}, + {"sd_no_match_total", _u8L("No actions match (Total: %s)")}, + {"sd_no_actions", _u8L("No actions yet")}, + {"sd_no_tabs_match", _u8L("No tabs match")}, + {"sd_no_tabs", _u8L("No tabs")}, + {"sd_result_count", _u8L("Showing %s actions")}, {"sd_result_count_all", _u8L("%s actions")}, - {"sd_tab_count", _u8L("%s tabs")}, + {"sd_tab_count", _u8L("%s tabs")}, {"sd_tab_match_count", _u8L("%s matches")}, - {"sd_favs_full", _u8L("Favourites are full (%s max)")}, - {"sd_go_to_pct", _u8L("Go to %s%% of the layer range")}, - {"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")}, - {"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")}, - {"sd_go_tab_ph", _u8L("Go to tab")}, - {"sd_fav_slot", _u8L("Favourite %s (%s)")}, - {"sd_pin_fav", _u8L("Pin to favourites (%s)")}, - {"sd_unpin_fav", _u8L("Unpin from favourites (%s)")}, - {"sd_remove_fav", _u8L("Remove from favourites")}, - {"sd_move_left", _u8L("Move left")}, - {"sd_move_right", _u8L("Move right")}, - {"sd_unpin", _u8L("Unpin")}, - {"sd_mode_advanced", _u8L("Advanced")}, - {"sd_mode_expert", _u8L("Expert")}, - {"sd_mode_develop", _u8L("Developer")}, - {"sd_wiki_f1", _u8L("Wiki (F1)")}, - {"sd_no_wiki", _u8L("No wiki page for this action")}, - {"sd_show_details", _u8L("Show details")}, - {"sd_hide_details", _u8L("Hide details")}, + {"sd_favs_full", _u8L("Favourites are full (%s max)")}, + {"sd_go_to_pct", _u8L("Go to %s%% of the layer range")}, + {"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")}, + {"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")}, + {"sd_go_tab_ph", _u8L("Go to tab")}, + {"sd_fav_slot", _u8L("Favourite %s (%s)")}, + {"sd_pin_fav", _u8L("Pin to favourites (%s)")}, + {"sd_unpin_fav", _u8L("Unpin from favourites (%s)")}, + {"sd_remove_fav", _u8L("Remove from favourites")}, + {"sd_move_left", _u8L("Move left")}, + {"sd_move_right", _u8L("Move right")}, + {"sd_unpin", _u8L("Unpin")}, + {"sd_mode_advanced", _u8L("Advanced")}, + {"sd_mode_expert", _u8L("Expert")}, + {"sd_mode_develop", _u8L("Developer")}, + {"sd_wiki_f1", _u8L("Wiki (F1)")}, + {"sd_no_wiki", _u8L("No wiki page for this action")}, + {"sd_show_details", _u8L("Show details")}, + {"sd_hide_details", _u8L("Hide details")}, }; } @@ -178,6 +179,7 @@ void SpeedDialWebDialog::request_show() if (IsShown()) { Raise(); focus_webview(browser(), m_page_ready); + repaint_webview(); return; } @@ -189,6 +191,7 @@ void SpeedDialWebDialog::request_show() // Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is // what makes typing reach the search field immediately on open. focus_webview(browser(), m_page_ready); + repaint_webview(); } void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload) @@ -216,7 +219,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) // set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the // pin and show a "favourites are full" hint instead of silently losing the favourite. const std::string fav_id = payload.value("id", ""); - const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false)); + const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false)); if (!ok) call_web_handler({{"command", "favourite_full"}, {"limit", (int) ActionRegistry::kFavLimit}, {"id", fav_id}}); } else if (command == "reorder_favourites") { @@ -265,14 +268,36 @@ void SpeedDialWebDialog::resize_to_content(int height) Layout(); #ifdef __WXOSX__ // WKWebView can lag the dialog's new client size; force the viewport to match so the page is - // never painted (and clipped by the rounded layer) below the footer. - if (wxWebView* wv = browser()) { - const wxSize client = GetClientSize(); - if (wv->GetSize() != client) - wv->SetSize(client); - } + // never painted (and clipped by the rounded layer) below the footer. Unconditional: on a + // re-open the size is often unchanged, and skipping the sync leaves the fresh render unpainted. + if (wxWebView* wv = browser()) + wv->SetSize(GetClientSize()); #endif apply_rounded_shape(); + // A re-open re-renders at (usually) the same size, so nothing above may generate damage. + // Repaint explicitly so the newly rendered list is shown without needing user input. + repaint_webview(); +} + +void SpeedDialWebDialog::repaint_webview() +{ + wxWebView* wv = browser(); + if (!wv) + return; + // Portable invalidate; the platform blocks below reach the widget/layer that actually paints. + wv->Refresh(); +#ifdef __WXOSX__ + if (void* nb = wv->GetNativeBackend()) + WKWebView_force_display(nb); + wv->Update(); +#elif defined(__linux__) + // WebKitGTK's WebKitWebView owns its own GdkWindow, so invalidating the wxWebView wrapper + // (the GtkScrolledWindow) does not redraw it. + if (void* nb = wv->GetNativeBackend()) + gtk_widget_queue_draw((GtkWidget*) nb); +#else + wv->Update(); +#endif } // Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window. @@ -331,8 +356,8 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti return; // Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately. - const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id); - const std::string atitle = a->title(); + const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id); + const std::string atitle = a->title(); const ConfigOptionMode required = a->required_mode; // Settings the current mode hides require a switch first. Ask while the dial is still up; a @@ -341,16 +366,15 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti const wxString setting = title.empty() ? from_u8(atitle) : from_u8(title); if (required == comDevelop) { RichMessageDialog dlg(wxGetApp().mainframe, - wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"), - setting), + wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"), setting), _L("Developer setting"), wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; wxGetApp().enable_developer_mode(); } else { RichMessageDialog dlg(wxGetApp().mainframe, - wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"), - setting, mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)), + wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"), setting, + mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)), _L("Switch settings mode"), wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index 8ae745b446..d3b75f2256 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -28,6 +28,10 @@ private: void send_actions(); void search_tabs(); void apply_rounded_shape(); + // Forces the webview to repaint after it is mapped / re-rendered. The popup is transparent and + // chrome-less, so a missed frame leaves it blank until input; platform-specific because the + // widget that actually paints is not always the wxWebView wrapper. + void repaint_webview(); void on_dpi_changed(const wxRect& suggested_rect) override; bool m_page_ready{false}; diff --git a/src/slic3r/Utils/MacDarkMode.hpp b/src/slic3r/Utils/MacDarkMode.hpp index 435a164d1e..8c1cea4663 100644 --- a/src/slic3r/Utils/MacDarkMode.hpp +++ b/src/slic3r/Utils/MacDarkMode.hpp @@ -12,6 +12,7 @@ extern double mac_max_scaling_factor(); extern void set_miniaturizable(void * window); void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &)); void WKWebView_setTransparentBackground(void * web); +void WKWebView_force_display(void * web); void set_tag_when_enter_full_screen(bool isfullscreen); void set_title_colour_after_set_title(void * window); void initGestures(void * view, wxEvtHandler * handler); diff --git a/src/slic3r/Utils/MacDarkMode.mm b/src/slic3r/Utils/MacDarkMode.mm index 2bce7835e8..9926309335 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -100,6 +100,20 @@ void WKWebView_setTransparentBackground(void * web) [webView registerForDraggedTypes: @[NSFilenamesPboardType]]; } +// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is +// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible +// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the +// web-content layer, so flag layout and both the view and its layer. +void WKWebView_force_display(void * web) +{ + WKWebView * webView = (WKWebView*)web; + if (!webView) + return; + [webView setNeedsLayout:YES]; + [webView setNeedsDisplay:YES]; + [[webView layer] setNeedsDisplay]; +} + void openFolderForFile(wxString const & file) { NSArray *fileURLs = [NSArray arrayWithObjects:wxCFStringRef(file).AsNSString(), /* ... */ nil];