diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index c5caf0d423..03f315e39c 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -30,7 +30,7 @@ var USER_MODE = "simple"; var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 }; // Search ranking weights: every contiguous match must outrank every fuzzy one regardless of field, -// and title must outrank group, which outranks source. +// and title must outrank group/category, which outranks source. var SCORE_CONTIGUOUS = 100000; var SCORE_TITLE = 2000; var SCORE_GROUP = 1000; @@ -118,6 +118,18 @@ function sourceNorm(a) { a._sn = NormText(a.source || "", false); return a._sn; } +function pluginCategoryNorm(a) { + if (a._pcn === undefined) + a._pcn = a.kind === "plugin" ? NormText(T("sd_plugins", "Plugins"), false) : ""; + return a._pcn; +} +// Plugin type is searchable metadata, so typing "plugin" can find runnable plugin actions even +// when neither their capability nor plugin name contains that word. Keep both category forms. +function pluginTypeNorm(a) { + if (a._pn === undefined) + a._pn = a.kind === "plugin" ? NormText("plugin plugins", false) : ""; + return a._pn; +} // Search-only alias for the descriptive name when the title differs (e.g. "Reverse on even" vs // "Overhang reversal"). Never rendered, so no highlight ranges. function fullNorm(a) { @@ -152,7 +164,7 @@ function fieldMatchScore(norm, needle, wwRe) { // Split a query into normalized (folded+lowercased) whitespace-separated tokens. Empty for a blank // query. These drive the multi-token path: every token must match some field, but different tokens -// may match different fields (the title, the group, or the source breadcrumb). +// may match different fields (the title, group, source breadcrumb, or plugin kind). function queryTokens(query) { var norm = NormText(String(query || "").trim(), false); return norm ? norm.split(/\s+/).filter(Boolean) : []; @@ -170,14 +182,19 @@ function tokenMatch(a, token, wwRe) { var g = fieldMatchScore(groupNorm(a), token, wwRe); var s = fieldMatchScore(sourceNorm(a), token, wwRe); var f = fieldMatchScore(fullNorm(a), token, wwRe); - if (!t && !g && !s && !f) return null; + var p = fieldMatchScore(pluginCategoryNorm(a), token, wwRe); + var typeMatch = fieldMatchScore(pluginTypeNorm(a), token, wwRe); + if (!t && !g && !s && !f && !p && !typeMatch) return null; var score = Math.max( t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity, g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity, s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity, - f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity + f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity, + p ? (p.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + p.score : -Infinity, + typeMatch ? (typeMatch.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + typeMatch.score : -Infinity ); - return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null }; + return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null, + plugin: p ? p.ranges : null }; } // Merge per-field token ranges into sorted, coalesced ranges for highlighting. Overlapping or adjacent @@ -201,8 +218,8 @@ function mergeRanges(ranges) { } // Combine per-field scores into one value, or null when nothing matched. -// Ranking: contiguous > fuzzy, then title > group > source/full alias, then start/gaps. -function scoreFields(t, g, s, f) { +// Ranking: contiguous > fuzzy, then title > group/category > source/full alias, then start/gaps. +function scoreFields(t, g, s, f, p, typeMatch) { var best = null; function consider(m, weight) { if (!m) return; @@ -213,6 +230,8 @@ function scoreFields(t, g, s, f) { consider(g, SCORE_GROUP); consider(s, 0); consider(f, 0); + consider(p, SCORE_GROUP); + consider(typeMatch, SCORE_GROUP); return best; } @@ -226,7 +245,8 @@ function scoreFields(t, g, s, f) { // - tokens: every whitespace-separated word must match SOME field, but different words may match // different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is // "Process : Speed : Acceleration" (title + source breadcrumb together). -// full_label is searchable too but never highlighted, since it is not rendered. +// full_label and canonical plugin kind are searchable aliases. The visible category is also searchable +// so its own match can be highlighted in plugin rows. // A phrase match always outranks a distributed token match. function searchActions(actions, query) { var q = (query || "").trim(); @@ -251,26 +271,31 @@ function searchActions(actions, query) { var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe); var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe); var f = fieldMatchScore(fullNorm(a), searchNeedle, wwRe); - var phrase = scoreFields(t, g, s, f); + var p = fieldMatchScore(pluginCategoryNorm(a), searchNeedle, wwRe); + var typeMatch = fieldMatchScore(pluginTypeNorm(a), searchNeedle, wwRe); + var phrase = scoreFields(t, g, s, f, p, typeMatch); var score, ranges; if (phrase !== null) { score = phrase + SCORE_PHRASE; - ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null }; + ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null, + plugin: p ? p.ranges : null }; } else { // Require every token; a token that matches nothing drops the action immediately. Ranges // from all matching tokens are merged per field so each matched word highlights. - var sum = 0, titleR = null, groupR = null, sourceR = null, all = true; - for (var k = 0; k < searchTokens.length; k++) { - var m = tokenMatch(a, searchTokens[k], searchTokenRes[k]); + var sum = 0, titleR = null, groupR = null, sourceR = null, pluginR = null, all = true; + for (var tokenIndex = 0; tokenIndex < searchTokens.length; tokenIndex++) { + var m = tokenMatch(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]); if (!m) { all = false; break; } sum += m.score; if (m.title) (titleR || (titleR = [])).push(m.title); if (m.group) (groupR || (groupR = [])).push(m.group); if (m.source) (sourceR || (sourceR = [])).push(m.source); + if (m.plugin) (pluginR || (pluginR = [])).push(m.plugin); } if (!all) continue; score = sum; - ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR) }; + ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR), + plugin: mergeRanges(pluginR) }; } // Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or // source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label. @@ -278,6 +303,7 @@ function searchActions(actions, query) { title: ranges.title, group: ranges.group, source: ranges.source, + plugin: ranges.plugin, useEyebrowGroup: !!(a.group) }; scored.push({ a: a, s: score }); @@ -390,7 +416,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); } @@ -461,6 +487,12 @@ function actionCategory(a) { return cat || T("sd_other", "Other"); } +function actionEyebrow(a, typedQuery, isRecent) { + if (a && a.kind === "plugin" && (String(typedQuery || "").trim() || isRecent)) + return T("sd_plugins", "Plugins"); + return (a && (a.group || a.source)) || ""; +} + // Stable-bucket actions by category, then order the groups alphabetically. Within a group the incoming // (frecency) order is kept. Pure so the node-vm test can exercise grouping. function groupActions(list) { @@ -711,7 +743,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 }); @@ -931,12 +963,14 @@ function beginRow(item, i, mono, ariaLabel) { function renderActionRow(a, i) { var on = FAVS.indexOf(a.id) !== -1; var shell = beginRow(a, i, false, actionLabel(a, ACTIONS)); - var mi = matchIndex[a.id]; - // The eyebrow shows group when present, else source. Highlight with the ranges of whichever of the - // two the eyebrow actually renders (so a "Recent Projects"/"Object" header match lights up like a - // setting path does - the offsets are computed against the same string we are marking). - var eyebrow = a.group || a.source; - var eyebrowMatch = mi ? (mi.useEyebrowGroup ? mi.group : mi.source) : null; + var typedQuery = String(query || "").trim(); + var isRecent = !typedQuery && i < RECENTS.length && RECENTS[i].id === a.id; + var mi = typedQuery ? matchIndex[a.id] : null; + // Plugin search/recent rows show their category; other rows show their group or source breadcrumb. + // Use match ranges from the visible field so category and breadcrumb highlights stay aligned. + var showPluginCategory = a.kind === "plugin" && (typedQuery || isRecent); + var eyebrow = actionEyebrow(a, query, isRecent); + var eyebrowMatch = mi ? (showPluginCategory ? mi.plugin : (mi.useEyebrowGroup ? mi.group : mi.source)) : null; shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line); shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null)); var badge = modeBadge(a, USER_MODE); @@ -1376,7 +1410,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..1fda833801 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -82,6 +82,20 @@ assert.equal(ctx.searchActions(pool, "layer").length >= 2, true, assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2", "a later-but-precise match still ranks by relevance, not by pool type"); +const pluginPool = [ + { id: "plugin-action", title: "Optimize G-code", source: "Gcode Optimizer", group: "", kind: "plugin" }, + { id: "command-action", title: "Open Preferences", source: "OrcaSlicer", group: "Commands", kind: "command" } +]; +assert.deepEqual(ctx.searchActions(pluginPool, "plugin").map(function (a) { return a.id; }), ["plugin-action"], + "the plugin kind makes runnable plugin actions searchable by plugin"); +assert.deepEqual(ctx.searchActions(pluginPool, "plugins").map(function (a) { return a.id; }), ["plugin-action"], + "the plural Plugins category also finds plugin actions"); +ctx.searchActions(pluginPool, "plugins"); +assert.deepEqual(ctx.matchIndex["plugin-action"].plugin, [[0, 7]], + "a category match highlights the visible Plugins label"); +assert.deepEqual(ctx.searchActions(pluginPool, "plugin optimize").map(function (a) { return a.id; }), ["plugin-action"], + "plugin kind can match one token while the action title matches another"); + // A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a // contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"), // which is what the old flat title-bonus ranking got backwards. @@ -188,6 +202,12 @@ assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Filament : Coolin "a Filament setting groups under Filament"); assert.equal(ctx.actionCategory({ id: "plugin_script_action:Foo:bar.py", group: "", source: "Gcode Optimizer", kind: "plugin" }), "Plugins", "every plugin shares one Plugins header"); +assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, "plugin"), "Plugins", + "typed results show only the Plugins category"); +assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, "", true), "Plugins", + "recent plugin actions show only the Plugins category"); +assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, ""), "Gcode Optimizer", + "the unfiltered plugin section keeps the source name on non-recent rows"); assert.equal(ctx.actionCategory({ id: "x", group: "", source: "", kind: "command" }), "Other", "a category-less action falls back to Other"); @@ -436,4 +456,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 5a0020cbfa..7a5e8fb027 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -102,6 +102,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];