mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 17:26:47 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee529e283 | ||
|
|
7ae76e44b1 | ||
|
|
54c32a54b4 |
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "Plater.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <wx/dcmemory.h>
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -1098,32 +1098,35 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
|
||||
const auto enabled_vendors = app_config->vendors();
|
||||
|
||||
std::set<std::string> bundles;
|
||||
// Orca: always install filament library
|
||||
bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
// A vendor is named by its profile or, where the build ships preset caches
|
||||
// instead of the raw profile JSONs, by its cache alone.
|
||||
for (const std::string &vendor_name : vendor_names_in(rsrc_path)) {
|
||||
// enabled_vendors lists the vendors whose printer models the user picked, and
|
||||
// neither of these two is ever in it.
|
||||
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE)
|
||||
|| (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY)
|
||||
if (bundles.find(vendor_name) != bundles.end())continue;
|
||||
|
||||
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|
||||
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
|
||||
if (is_vendor_installed(vendor_name)) {
|
||||
if (is_vendor_enabled) {
|
||||
// Orca: whichever form of the vendor resources ships at the newer
|
||||
// version is the one installing lays down, and the one to judge
|
||||
// what is installed against.
|
||||
Semver resource_ver = resource_vendor_version(vendor_name);
|
||||
// Orca: a vendor installed as a preset cache has no profile
|
||||
// beside it; the version it was installed at is in the cache.
|
||||
Semver vendor_ver = installed_vendor_version(vendor_name);
|
||||
if (enabled_config_update) {
|
||||
if (is_vendor_enabled) {
|
||||
// Orca: whichever form of the vendor resources ships at the newer
|
||||
// version is the one installing lays down, and the one to judge
|
||||
// what is installed against.
|
||||
Semver resource_ver = resource_vendor_version(vendor_name);
|
||||
// Orca: a vendor installed as a preset cache has no profile
|
||||
// beside it; the version it was installed at is in the cache.
|
||||
Semver vendor_ver = installed_vendor_version(vendor_name);
|
||||
|
||||
if (vendor_ver < resource_ver) {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
|
||||
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
|
||||
bundles.insert(vendor_name);
|
||||
if (vendor_ver < resource_ver) {
|
||||
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
|
||||
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
|
||||
bundles.insert(vendor_name);
|
||||
}
|
||||
} else {
|
||||
// need to be removed because not installed
|
||||
remove_installed_vendor(vendor_name);
|
||||
}
|
||||
} else {
|
||||
// need to be removed because not installed
|
||||
remove_installed_vendor(vendor_name);
|
||||
}
|
||||
} else if (is_vendor_enabled) {
|
||||
bundles.insert(vendor_name);
|
||||
|
||||
Reference in New Issue
Block a user