mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
Initial Commit of speed dial improvement
This commit is contained in:
@@ -1,14 +1,22 @@
|
|||||||
// Speed Dial launcher page. Static-safe module: no DOM access at load time so a
|
// Speed Dial launcher page. Static-safe module: no DOM access at load time so a
|
||||||
// node vm can exercise the pure helpers (filterActions / actionLabel / nextSel).
|
// node vm can exercise the pure helpers (filterActions / actionLabel / nextSel / commandList).
|
||||||
|
|
||||||
// ---- state (populated by the C++ bridge via window.HandleStudio) ----
|
// ---- state (populated by the C++ bridge via window.HandleStudio) ----
|
||||||
var ACTIONS = []; // [{id,title,source,shortcut}], already frecency-sorted by C++
|
var ACTIONS = []; // [{id,title,source,group,input,shortcut}], already frecency-sorted by C++
|
||||||
var FAVS = []; // [id...]
|
var FAVS = []; // [id...]
|
||||||
|
var RECENTS = []; // [{id,title,source,group,input,shortcut}] - last-N launched
|
||||||
var query = "";
|
var query = "";
|
||||||
var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
||||||
var lastResizeHeight = 0;
|
var lastResizeHeight = 0;
|
||||||
var matchIndex = {};
|
var matchIndex = {};
|
||||||
|
|
||||||
|
// Palette phase: 'commands' (search actions/commands, show recents), 'settings' ("Go to
|
||||||
|
// setting..." second phase: search config options), 'percent' ("Go to layer" second phase:
|
||||||
|
// enter a 0-100 percentage), 'tab' ("Go to tab..." second phase: pick a notebook tab).
|
||||||
|
var phase = "commands";
|
||||||
|
var settingsResults = []; // [{opt_key,type,label,category,group}]
|
||||||
|
var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering the tab phase
|
||||||
|
|
||||||
// why: fuzzy matcher (FoldChar/Norm/FuzzyRanges) lives in shared ../../js/fuzzy-search.js, loaded before
|
// why: fuzzy matcher (FoldChar/Norm/FuzzyRanges) lives in shared ../../js/fuzzy-search.js, loaded before
|
||||||
// this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive.
|
// this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive.
|
||||||
|
|
||||||
@@ -27,7 +35,7 @@ function filterActions(actions, query) {
|
|||||||
for (var i = 0; i < list.length; i++) {
|
for (var i = 0; i < list.length; i++) {
|
||||||
var a = list[i];
|
var a = list[i];
|
||||||
var titleMatch = FuzzyRanges(a.title, q, false);
|
var titleMatch = FuzzyRanges(a.title, q, false);
|
||||||
var sourceMatch = FuzzyRanges(a.source, q, false);
|
var sourceMatch = FuzzyRanges(a.source || "", q, false);
|
||||||
if (!titleMatch && !sourceMatch)
|
if (!titleMatch && !sourceMatch)
|
||||||
continue;
|
continue;
|
||||||
matchIndex[a.id] = { title: titleMatch, source: sourceMatch, useTitle: !!titleMatch };
|
matchIndex[a.id] = { title: titleMatch, source: sourceMatch, useTitle: !!titleMatch };
|
||||||
@@ -50,18 +58,44 @@ function resultCountText(total, shown, query) {
|
|||||||
return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions";
|
return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display label for a notebook tab. Notebook's ButtonsListCtrl labels every non-empty page as
|
||||||
|
// " <text>" (a leading space), so trim it; pages added with an empty title (Home, MainFrame adds
|
||||||
|
// TAB_ID_HOME with "") fall back to the title-cased id ("home" -> "Home").
|
||||||
|
function tabTitle(t) {
|
||||||
|
var title = (t && t.title) ? String(t.title).trim() : "";
|
||||||
|
return title || prettySource((t && t.id) || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the tab list by a fuzzy title/id match. Pure so the node-vm test can exercise it.
|
||||||
|
function filterTabs(tabs, query) {
|
||||||
|
var q = (query || "").trim();
|
||||||
|
if (!q)
|
||||||
|
return (tabs || []).slice(0);
|
||||||
|
return (tabs || []).filter(function (t) {
|
||||||
|
return FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function shouldRenderActionList(query) {
|
function shouldRenderActionList(query) {
|
||||||
return !!(query || "").trim();
|
return !!((query || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active list for the commands phase. A typed query filters every action (plugins +
|
||||||
|
// commands); an empty query shows the recent list instead (recents live below the search bar).
|
||||||
|
function commandList(actions, recents, query) {
|
||||||
|
if (shouldRenderActionList(query))
|
||||||
|
return filterActions(actions || [], query);
|
||||||
|
return (recents || []).slice(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the selection cursor {zone,i} to the action id it points at: fav zone indexes the
|
// Resolve the selection cursor {zone,i} to the action id it points at: fav zone indexes the
|
||||||
// visible favourites, list zone the filtered actions. Pure so runSelected() shares one lookup.
|
// visible favourites, list zone the active commands list. `actions` must be the already-resolved
|
||||||
|
// list (recents for an empty query, the filtered list otherwise) - pure so runSelected() shares
|
||||||
|
// one lookup and the node-vm test can call it directly.
|
||||||
function selectedActionId(sel, actions, favIds, query) {
|
function selectedActionId(sel, actions, favIds, query) {
|
||||||
if (sel.zone === "fav")
|
if (sel.zone === "fav")
|
||||||
return favIds[sel.i];
|
return favIds[sel.i];
|
||||||
// why: search-first keeps the list blank until the user types; an empty query still
|
if (!actions || !actions.length)
|
||||||
// filters to ALL actions, so without this gate Enter fires an action never shown.
|
|
||||||
if (!shouldRenderActionList(query))
|
|
||||||
return null;
|
return null;
|
||||||
var a = actions[sel.i];
|
var a = actions[sel.i];
|
||||||
return a && a.id;
|
return a && a.id;
|
||||||
@@ -77,11 +111,11 @@ function prettySource(source) {
|
|||||||
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
||||||
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
||||||
function actionLabel(action, actions) {
|
function actionLabel(action, actions) {
|
||||||
var label = action.title + " from " + prettySource(action.source);
|
var label = action.title + " from " + prettySource(action.source || action.group || "");
|
||||||
if (actions && actions.length) {
|
if (actions && actions.length) {
|
||||||
var mine = foldLabel(action.title) + "|" + foldLabel(action.source);
|
var mine = foldLabel(action.title) + "|" + foldLabel(action.source || action.group || "");
|
||||||
var clash = actions.some(function (o) {
|
var clash = actions.some(function (o) {
|
||||||
return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source) === mine;
|
return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source || o.group || "") === mine;
|
||||||
});
|
});
|
||||||
if (clash)
|
if (clash)
|
||||||
label += " (" + action.id + ")";
|
label += " (" + action.id + ")";
|
||||||
@@ -90,26 +124,55 @@ function actionLabel(action, actions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
|
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
|
||||||
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled actions stay distinct.
|
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
|
||||||
// why: ordinal is assigned by id, not by ACTIONS order - ACTIONS is frecency-sorted and
|
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
|
||||||
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
|
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
|
||||||
function tileCode(action, actions) {
|
function monogramFor(item, list, titleOf, sourceOf, idOf) {
|
||||||
var list = actions || [];
|
var items = list || [];
|
||||||
var ti = (action.title || " ").charAt(0).toUpperCase();
|
var title = titleOf(item) || " ";
|
||||||
var sameTitle = list.filter(function (o) { return (o.title || " ").charAt(0).toUpperCase() === ti; });
|
var ti = title.charAt(0).toUpperCase();
|
||||||
|
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
|
||||||
if (sameTitle.length <= 1)
|
if (sameTitle.length <= 1)
|
||||||
return ti;
|
return ti;
|
||||||
var pi = (action.source || " ").charAt(0).toUpperCase();
|
var source = sourceOf(item) || " ";
|
||||||
var sameSource = sameTitle.filter(function (o) { return (o.source || " ").charAt(0).toUpperCase() === pi; });
|
var pi = source.charAt(0).toUpperCase();
|
||||||
|
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
|
||||||
if (sameSource.length <= 1)
|
if (sameSource.length <= 1)
|
||||||
return pi + ti;
|
return pi + ti;
|
||||||
sameSource.sort(function (a, b) { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; });
|
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
|
||||||
for (var i = 0; i < sameSource.length; i++)
|
for (var i = 0; i < sameSource.length; i++)
|
||||||
if (sameSource[i].id === action.id)
|
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
|
||||||
return pi + ti + (i + 1);
|
return pi + ti + (i + 1);
|
||||||
return pi + ti;
|
return pi + ti;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action tile code - see monogramFor for the escalation ladder. Accessed via accessors so the
|
||||||
|
// same helper serves settings rows (leaf label + category) without duplicating the logic.
|
||||||
|
function tileCode(action, actions) {
|
||||||
|
return monogramFor(action, actions,
|
||||||
|
function (o) { return o.title; },
|
||||||
|
function (o) { return o.source; },
|
||||||
|
function (o) { return o.id; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last " : "-separated segment of a "category : group : label" setting label = the option name
|
||||||
|
// (e.g. "Quality : Layer Height" -> "Layer Height"); empty labels fall back to the opt_key.
|
||||||
|
function leafLabel(s) {
|
||||||
|
var label = String(s && (s.label || s.opt_key || "") || "");
|
||||||
|
var parts = label.split(" : ");
|
||||||
|
var leaf = parts[parts.length - 1];
|
||||||
|
return (leaf || "").trim() || label.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting tile code - the option name initial, then the section (category) on collision, then a
|
||||||
|
// stable ordinal, so settings no longer all collapse to a generic "S".
|
||||||
|
function settingCode(s, list) {
|
||||||
|
return monogramFor(s, list,
|
||||||
|
function (o) { return leafLabel(o); },
|
||||||
|
function (o) { return o.category; },
|
||||||
|
function (o) { return o.opt_key; });
|
||||||
|
}
|
||||||
|
|
||||||
function syncClearButton() {
|
function syncClearButton() {
|
||||||
if (clearEl)
|
if (clearEl)
|
||||||
clearEl.hidden = !query;
|
clearEl.hidden = !query;
|
||||||
@@ -119,9 +182,13 @@ function stateFromPayload(payload) {
|
|||||||
return {
|
return {
|
||||||
actions: payload.actions || [],
|
actions: payload.actions || [],
|
||||||
favourites: payload.favourites || [],
|
favourites: payload.favourites || [],
|
||||||
|
recent: payload.recent || [],
|
||||||
query: "",
|
query: "",
|
||||||
sel: { zone: "list", i: 0 },
|
sel: { zone: "list", i: 0 },
|
||||||
lastResizeHeight: 0
|
lastResizeHeight: 0,
|
||||||
|
phase: "commands",
|
||||||
|
settingsResults: [],
|
||||||
|
tabOptions: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +232,7 @@ function SendMessage(msg) {
|
|||||||
SendWXMessage(JSON.stringify(msg));
|
SendWXMessage(JSON.stringify(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// C++ pushes payloads here. Only list_actions is handled; it (re)seeds all state.
|
// C++ pushes payloads here.
|
||||||
window.HandleStudio = function (payload) {
|
window.HandleStudio = function (payload) {
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } }
|
if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } }
|
||||||
@@ -173,9 +240,13 @@ window.HandleStudio = function (payload) {
|
|||||||
var next = stateFromPayload(payload);
|
var next = stateFromPayload(payload);
|
||||||
ACTIONS = next.actions;
|
ACTIONS = next.actions;
|
||||||
FAVS = next.favourites;
|
FAVS = next.favourites;
|
||||||
|
RECENTS = next.recent;
|
||||||
query = next.query;
|
query = next.query;
|
||||||
sel = next.sel;
|
sel = next.sel;
|
||||||
lastResizeHeight = next.lastResizeHeight;
|
lastResizeHeight = next.lastResizeHeight;
|
||||||
|
phase = next.phase;
|
||||||
|
settingsResults = next.settingsResults;
|
||||||
|
tabOptions = next.tabOptions;
|
||||||
if (qEl) {
|
if (qEl) {
|
||||||
qEl.value = "";
|
qEl.value = "";
|
||||||
qEl.placeholder = "Search " + ACTIONS.length + " actions";
|
qEl.placeholder = "Search " + ACTIONS.length + " actions";
|
||||||
@@ -183,6 +254,16 @@ window.HandleStudio = function (payload) {
|
|||||||
}
|
}
|
||||||
render({ resize: true, resetScroll: true });
|
render({ resize: true, resetScroll: true });
|
||||||
focusInput();
|
focusInput();
|
||||||
|
} else if (payload.command === "settings_results") {
|
||||||
|
settingsResults = payload.results || [];
|
||||||
|
if (sel.zone === "list")
|
||||||
|
sel.i = Math.max(0, Math.min(sel.i, settingsResults.length - 1));
|
||||||
|
render({ resize: true });
|
||||||
|
} else if (payload.command === "tab_results") {
|
||||||
|
tabOptions = payload.tabs || [];
|
||||||
|
if (sel.zone === "list")
|
||||||
|
sel.i = Math.max(0, Math.min(sel.i, tabOptions.length - 1));
|
||||||
|
render({ resize: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,8 +275,21 @@ function byId(id) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findActionByInput(input) {
|
||||||
|
for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i].input === input) return ACTIONS[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function currentVisibleFavs() { return visibleFavourites(FAVS, ACTIONS); }
|
function currentVisibleFavs() { return visibleFavourites(FAVS, ACTIONS); }
|
||||||
|
|
||||||
|
// Active list for the current phase (drives list rendering + arrow nav).
|
||||||
|
function currentList() {
|
||||||
|
if (phase === "settings") return settingsResults;
|
||||||
|
if (phase === "tab") return filterTabs(tabOptions, query);
|
||||||
|
if (phase === "commands") return commandList(ACTIONS, RECENTS, query);
|
||||||
|
return []; // percent - the input itself is the only field
|
||||||
|
}
|
||||||
|
|
||||||
function hue(id) {
|
function hue(id) {
|
||||||
var h = 0;
|
var h = 0;
|
||||||
for (var i = 0; i < id.length; i++)
|
for (var i = 0; i < id.length; i++)
|
||||||
@@ -236,6 +330,12 @@ function starSvg(on) {
|
|||||||
|
|
||||||
// ---- render ------------------------------------------------------------------
|
// ---- render ------------------------------------------------------------------
|
||||||
function renderFav() {
|
function renderFav() {
|
||||||
|
// Only the commands phase shows the pinned quick-bar.
|
||||||
|
if (phase !== "commands") {
|
||||||
|
if (favEl) { favEl.innerHTML = ""; favEl.hidden = true; }
|
||||||
|
if (eyeEl) eyeEl.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
favEl.innerHTML = "";
|
favEl.innerHTML = "";
|
||||||
var favs = currentVisibleFavs();
|
var favs = currentVisibleFavs();
|
||||||
favEl.hidden = favs.length === 0;
|
favEl.hidden = favs.length === 0;
|
||||||
@@ -252,7 +352,16 @@ function renderFav() {
|
|||||||
tile.textContent = tileCode(a, ACTIONS);
|
tile.textContent = tileCode(a, ACTIONS);
|
||||||
tile.title = a.title;
|
tile.title = a.title;
|
||||||
tile.setAttribute("aria-label", actionLabel(a, ACTIONS));
|
tile.setAttribute("aria-label", actionLabel(a, ACTIONS));
|
||||||
tile.onclick = function () { sel = { zone: "fav", i: i }; run(a); };
|
tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); };
|
||||||
|
// Direct removal: a hover-revealed ✕ in the tile's corner. click() stops propagation so it
|
||||||
|
// unpins without activating the action.
|
||||||
|
var unpin = document.createElement("button");
|
||||||
|
unpin.className = "fav-unpin";
|
||||||
|
unpin.title = "Remove from favourites";
|
||||||
|
unpin.setAttribute("aria-label", "Remove from favourites");
|
||||||
|
unpin.innerHTML = '<svg width="9" height="9" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>';
|
||||||
|
unpin.onclick = function (ev) { ev.stopPropagation(); toggleFav(id); };
|
||||||
|
tile.appendChild(unpin);
|
||||||
tile.oncontextmenu = function (ev) {
|
tile.oncontextmenu = function (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
// why: selecting shows the eyebrow, which grows the launcher - resize so the popup
|
// why: selecting shows the eyebrow, which grows the launcher - resize so the popup
|
||||||
@@ -320,33 +429,37 @@ function updateFavEyebrow(favs) {
|
|||||||
eyeEl.hidden = !a;
|
eyeEl.hidden = !a;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
// One settings row; has no star/tile because settings aren't pinnable.
|
||||||
var q = (query || "").trim();
|
function renderSettingRow(s, i) {
|
||||||
var arr = filterActions(ACTIONS, query);
|
var row = document.createElement("div");
|
||||||
if (sel.zone === "list")
|
row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "");
|
||||||
sel.i = Math.max(0, Math.min(sel.i, arr.length - 1));
|
row.setAttribute("aria-label", s.label);
|
||||||
listEl.innerHTML = "";
|
|
||||||
// why: search-first - the list stays blank until the user types; only a
|
var tile = document.createElement("div");
|
||||||
// non-empty query with zero hits earns the "No actions match" message.
|
tile.className = "tile";
|
||||||
if (!shouldRenderActionList(query)) {
|
tile.style.setProperty("--h", hue(s.opt_key));
|
||||||
listEl.className = "dial-list empty";
|
tile.textContent = settingCode(s, settingsResults);
|
||||||
if (countEl) countEl.hidden = true;
|
|
||||||
return;
|
var left = document.createElement("div");
|
||||||
|
left.className = "row-left";
|
||||||
|
// why: the label already packs "Category : Group : Label", so no separate eyebrow.
|
||||||
|
var line = document.createElement("div");
|
||||||
|
line.className = "row-line";
|
||||||
|
var name = document.createElement("div");
|
||||||
|
name.className = "row-name";
|
||||||
|
name.textContent = s.label;
|
||||||
|
line.appendChild(name);
|
||||||
|
left.appendChild(line);
|
||||||
|
|
||||||
|
row.appendChild(tile);
|
||||||
|
row.appendChild(left);
|
||||||
|
row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); };
|
||||||
|
row.ondblclick = function () { sel = { zone: "list", i: i }; jumpToSetting(s); };
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
listEl.className = "dial-list" + (!arr.length ? " empty" : "");
|
|
||||||
if (!arr.length) {
|
// A command/action row (used for both recents and filtered results).
|
||||||
if (countEl) countEl.hidden = true;
|
function renderActionRow(a, i) {
|
||||||
var empty = document.createElement("div");
|
|
||||||
empty.className = "dial-empty";
|
|
||||||
empty.textContent = "No actions match (Total: " + ACTIONS.length + ")";
|
|
||||||
listEl.appendChild(empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (countEl) {
|
|
||||||
countEl.hidden = false;
|
|
||||||
countEl.textContent = resultCountText(ACTIONS.length, arr.length, query);
|
|
||||||
}
|
|
||||||
arr.forEach(function (a, i) {
|
|
||||||
var on = FAVS.indexOf(a.id) !== -1;
|
var on = FAVS.indexOf(a.id) !== -1;
|
||||||
var row = document.createElement("div");
|
var row = document.createElement("div");
|
||||||
row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "");
|
row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "");
|
||||||
@@ -360,7 +473,7 @@ function renderList() {
|
|||||||
var left = document.createElement("div");
|
var left = document.createElement("div");
|
||||||
left.className = "row-left";
|
left.className = "row-left";
|
||||||
var mi = matchIndex[a.id];
|
var mi = matchIndex[a.id];
|
||||||
var sourceEl = markedText("row-eyebrow", a.source, mi ? mi.source : null);
|
var sourceEl = markedText("row-eyebrow", a.group || a.source, mi ? mi.source : null);
|
||||||
var line = document.createElement("div");
|
var line = document.createElement("div");
|
||||||
line.className = "row-line";
|
line.className = "row-line";
|
||||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||||
@@ -389,10 +502,159 @@ function renderList() {
|
|||||||
star.ondblclick = function (ev) { ev.stopPropagation(); };
|
star.ondblclick = function (ev) { ev.stopPropagation(); };
|
||||||
row.appendChild(star);
|
row.appendChild(star);
|
||||||
|
|
||||||
row.onclick = function () { sel = { zone: "list", i: i }; render(); };
|
row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); };
|
||||||
row.ondblclick = function () { sel = { zone: "list", i: i }; run(a); };
|
row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); };
|
||||||
listEl.appendChild(row);
|
return row;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function renderCommandsList() {
|
||||||
|
var q = (query || "").trim();
|
||||||
|
var list = currentList();
|
||||||
|
if (sel.zone === "list")
|
||||||
|
sel.i = Math.max(0, Math.min(sel.i, list.length - 1));
|
||||||
|
// Recents are not filtered, so clear any stale match marks from a previous typed query.
|
||||||
|
if (!shouldRenderActionList(query))
|
||||||
|
matchIndex = {};
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
|
||||||
|
if (!shouldRenderActionList(query) && list.length) {
|
||||||
|
var head = document.createElement("div");
|
||||||
|
head.className = "dial-group";
|
||||||
|
head.textContent = "Recent";
|
||||||
|
listEl.appendChild(head);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.length) {
|
||||||
|
listEl.className = "dial-list empty";
|
||||||
|
if (countEl) countEl.hidden = true;
|
||||||
|
var empty = document.createElement("div");
|
||||||
|
empty.className = "dial-empty";
|
||||||
|
empty.textContent = shouldRenderActionList(query) ? ("No actions match (Total: " + ACTIONS.length + ")") : "No actions yet";
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listEl.className = "dial-list";
|
||||||
|
if (countEl) {
|
||||||
|
countEl.hidden = false;
|
||||||
|
countEl.textContent = shouldRenderActionList(query) ? resultCountText(ACTIONS.length, list.length, query) : list.length + " recent";
|
||||||
|
}
|
||||||
|
list.forEach(function (a, i) { listEl.appendChild(renderActionRow(a, i)); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettingsList() {
|
||||||
|
var q = (query || "").trim();
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
// Empty query + recents -> show the recent settings under a "Recent" group header.
|
||||||
|
var showingRecents = !q && settingsResults.length > 0;
|
||||||
|
if (!q && !showingRecents) {
|
||||||
|
listEl.className = "dial-list empty";
|
||||||
|
if (countEl) countEl.hidden = true;
|
||||||
|
var hint = document.createElement("div");
|
||||||
|
hint.className = "dial-empty";
|
||||||
|
hint.textContent = "Type to search print, filament and printer settings";
|
||||||
|
listEl.appendChild(hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (q && !settingsResults.length) {
|
||||||
|
listEl.className = "dial-list empty";
|
||||||
|
if (countEl) countEl.hidden = true;
|
||||||
|
var empty = document.createElement("div");
|
||||||
|
empty.className = "dial-empty";
|
||||||
|
empty.textContent = "No settings match";
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sel.zone === "list")
|
||||||
|
sel.i = Math.max(0, Math.min(sel.i, settingsResults.length - 1));
|
||||||
|
listEl.className = "dial-list";
|
||||||
|
if (countEl) {
|
||||||
|
countEl.hidden = false;
|
||||||
|
countEl.textContent = showingRecents ? settingsResults.length + " recent" : settingsResults.length + " matches";
|
||||||
|
}
|
||||||
|
if (showingRecents) {
|
||||||
|
var head = document.createElement("div");
|
||||||
|
head.className = "dial-group";
|
||||||
|
head.textContent = "Recent";
|
||||||
|
listEl.appendChild(head);
|
||||||
|
}
|
||||||
|
settingsResults.forEach(function (s, i) { listEl.appendChild(renderSettingRow(s, i)); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tab row: no star/unpin (tabs aren't pinnable), tile monogram from the title. Uses tabTitle so
|
||||||
|
// pages added with an empty text (e.g. Home) still show a label and an icon letter.
|
||||||
|
function renderTabRow(t, i) {
|
||||||
|
var label = tabTitle(t);
|
||||||
|
var row = document.createElement("div");
|
||||||
|
row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "");
|
||||||
|
row.setAttribute("aria-label", label);
|
||||||
|
|
||||||
|
var tile = document.createElement("div");
|
||||||
|
tile.className = "tile";
|
||||||
|
tile.style.setProperty("--h", hue(t.id));
|
||||||
|
tile.textContent = label.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
var left = document.createElement("div");
|
||||||
|
left.className = "row-left";
|
||||||
|
var line = document.createElement("div");
|
||||||
|
line.className = "row-line";
|
||||||
|
var name = document.createElement("div");
|
||||||
|
name.className = "row-name";
|
||||||
|
name.textContent = label;
|
||||||
|
line.appendChild(name);
|
||||||
|
left.appendChild(line);
|
||||||
|
|
||||||
|
row.appendChild(tile);
|
||||||
|
row.appendChild(left);
|
||||||
|
row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); };
|
||||||
|
row.ondblclick = function () { sel = { zone: "list", i: i }; jumpToTab(t); };
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabList() {
|
||||||
|
var q = (query || "").trim();
|
||||||
|
var list = currentList();
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
|
||||||
|
if (!list.length) {
|
||||||
|
listEl.className = "dial-list empty";
|
||||||
|
if (countEl) countEl.hidden = true;
|
||||||
|
var empty = document.createElement("div");
|
||||||
|
empty.className = "dial-empty";
|
||||||
|
empty.textContent = q ? "No tabs match" : "No tabs";
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sel.zone === "list")
|
||||||
|
sel.i = Math.max(0, Math.min(sel.i, list.length - 1));
|
||||||
|
listEl.className = "dial-list";
|
||||||
|
if (countEl) {
|
||||||
|
countEl.hidden = false;
|
||||||
|
countEl.textContent = q ? list.length + " matches" : list.length + " tabs";
|
||||||
|
}
|
||||||
|
list.forEach(function (t, i) { listEl.appendChild(renderTabRow(t, i)); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPercentList() {
|
||||||
|
var q = (query || "").trim();
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
listEl.className = "dial-list empty";
|
||||||
|
if (countEl) countEl.hidden = true;
|
||||||
|
var ph = document.createElement("div");
|
||||||
|
ph.className = "dial-empty";
|
||||||
|
ph.textContent = q ? ("Go to " + q + "% of the layer range") : "Enter a layer percentage (0-100)";
|
||||||
|
listEl.appendChild(ph);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
if (phase === "settings")
|
||||||
|
renderSettingsList();
|
||||||
|
else if (phase === "tab")
|
||||||
|
renderTabList();
|
||||||
|
else if (phase === "percent")
|
||||||
|
renderPercentList();
|
||||||
|
else
|
||||||
|
renderCommandsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(opts) {
|
function render(opts) {
|
||||||
@@ -441,18 +703,99 @@ function toggleFav(id) {
|
|||||||
render({ resize: true });
|
render({ resize: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire the action; C++ owns the run-confirm (native dialog) + suppression, then closes the popup + toasts.
|
// Fire a command/plugin action; C++ owns the run-confirm (native dialog) + suppression, then
|
||||||
|
// closes the popup + toasts.
|
||||||
function run(a) {
|
function run(a) {
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
SendMessage({ command: "run_action", id: a.id, title: a.title });
|
SendMessage({ command: "run_action", id: a.id, title: a.title, param: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate an entry in the commands phase. Two-phase commands switch the palette to their input
|
||||||
|
// phase instead of running; everything else runs immediately.
|
||||||
|
function activateEntry(a) {
|
||||||
|
if (!a) return;
|
||||||
|
if (a.input === "settings") { enterSettingsPhase(); return; }
|
||||||
|
if (a.input === "percent") { enterPercentPhase(); return; }
|
||||||
|
if (a.input === "tab") { enterTabsPhase(); return; }
|
||||||
|
run(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runSelected() {
|
function runSelected() {
|
||||||
var id = selectedActionId(sel, filterActions(ACTIONS, query), currentVisibleFavs(), query);
|
if (phase === "percent") {
|
||||||
if (id) run(byId(id));
|
runJumpToLayer(query.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phase === "settings") {
|
||||||
|
var s = settingsResults[sel.i];
|
||||||
|
if (s) jumpToSetting(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phase === "tab") {
|
||||||
|
var t = currentList()[sel.i];
|
||||||
|
if (t) jumpToTab(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var list = currentList();
|
||||||
|
var id = selectedActionId(sel, list, currentVisibleFavs(), query);
|
||||||
|
if (id) activateEntry(byId(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusInput() { setTimeout(function () { qEl.focus(); }, 0); }
|
function jumpToSetting(s) {
|
||||||
|
SendMessage({ command: "go_to_setting", opt_key: s.opt_key, type: s.type, category: s.category || "", label: s.label || "", group: s.group || "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function runJumpToLayer(pct) {
|
||||||
|
if (pct === "") return;
|
||||||
|
var n = parseFloat(pct);
|
||||||
|
if (!isFinite(n) || n < 0 || n > 100) return;
|
||||||
|
var a = findActionByInput("percent");
|
||||||
|
if (!a) return;
|
||||||
|
SendMessage({ command: "run_action", id: a.id, title: a.title, param: String(n) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpToTab(t) {
|
||||||
|
SendMessage({ command: "go_to_tab", id: t.id, title: tabTitle(t) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterSettingsPhase() {
|
||||||
|
phase = "settings"; settingsResults = []; query = ""; qEl.value = "";
|
||||||
|
sel = { zone: "list", i: 0 };
|
||||||
|
qEl.placeholder = "Search settings...";
|
||||||
|
syncClearButton();
|
||||||
|
render({ resize: true, resetScroll: true });
|
||||||
|
qEl.focus();
|
||||||
|
SendMessage({ command: "search_settings", q: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterPercentPhase() {
|
||||||
|
phase = "percent"; query = ""; qEl.value = "";
|
||||||
|
sel = { zone: "list", i: 0 };
|
||||||
|
qEl.placeholder = "Go to layer % (0-100)";
|
||||||
|
syncClearButton();
|
||||||
|
render({ resize: true, resetScroll: true });
|
||||||
|
qEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterTabsPhase() {
|
||||||
|
phase = "tab"; tabOptions = []; query = ""; qEl.value = "";
|
||||||
|
sel = { zone: "list", i: 0 };
|
||||||
|
qEl.placeholder = "Go to tab";
|
||||||
|
syncClearButton();
|
||||||
|
render({ resize: true, resetScroll: true });
|
||||||
|
qEl.focus();
|
||||||
|
SendMessage({ command: "search_tabs" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitPhase() {
|
||||||
|
phase = "commands"; settingsResults = []; tabOptions = []; query = ""; qEl.value = "";
|
||||||
|
sel = { zone: "list", i: 0 };
|
||||||
|
qEl.placeholder = "Search " + ACTIONS.length + " actions";
|
||||||
|
syncClearButton();
|
||||||
|
render({ resize: true, resetScroll: true });
|
||||||
|
qEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); }
|
||||||
|
|
||||||
// ---- init --------------------------------------------------------------------
|
// ---- init --------------------------------------------------------------------
|
||||||
function OnInit() {
|
function OnInit() {
|
||||||
@@ -460,11 +803,15 @@ function OnInit() {
|
|||||||
syncClearButton();
|
syncClearButton();
|
||||||
|
|
||||||
$("clear").onclick = function () {
|
$("clear").onclick = function () {
|
||||||
query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; render({ resize: true, resetScroll: true }); qEl.focus();
|
query = ""; qEl.value = ""; sel = { zone: "list", i: 0 };
|
||||||
|
if (phase === "settings") SendMessage({ command: "search_settings", q: "" });
|
||||||
|
render({ resize: true, resetScroll: true }); qEl.focus();
|
||||||
syncClearButton();
|
syncClearButton();
|
||||||
};
|
};
|
||||||
qEl.addEventListener("input", function () {
|
qEl.addEventListener("input", function () {
|
||||||
query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true });
|
query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton();
|
||||||
|
if (phase === "settings") SendMessage({ command: "search_settings", q: query });
|
||||||
|
render({ resize: true, resetScroll: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers).
|
// why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers).
|
||||||
@@ -473,23 +820,29 @@ function OnInit() {
|
|||||||
|
|
||||||
document.addEventListener("keydown", function (e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; }
|
if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; }
|
||||||
var arr = filterActions(ACTIONS, query);
|
var list = currentList();
|
||||||
var favs = currentVisibleFavs();
|
// why: fav bar only exists in the commands phase; other phases are list-only, so an
|
||||||
|
// ArrowUp at the top must not jump into a hidden fav zone.
|
||||||
|
var favs = (phase === "commands") ? currentVisibleFavs() : [];
|
||||||
// why: Up/Down always navigate; Left/Right only navigate the fav bar. In the list zone,
|
// why: Up/Down always navigate; Left/Right only navigate the fav bar. In the list zone,
|
||||||
// let Left/Right fall through so they move the caret in the focused search field.
|
// let Left/Right fall through so they move the caret in the focused search field.
|
||||||
var lr = e.key === "ArrowLeft" || e.key === "ArrowRight";
|
var lr = e.key === "ArrowLeft" || e.key === "ArrowRight";
|
||||||
if (e.key === "ArrowDown" || e.key === "ArrowUp" || (lr && sel.zone === "fav")) {
|
if (e.key === "ArrowDown" || e.key === "ArrowUp" || (lr && sel.zone === "fav")) {
|
||||||
|
// In the percent phase the input is the whole UI - arrows move the caret, not rows.
|
||||||
|
if (phase === "percent") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sel = nextSel(sel, e.key, arr.length, favs.length);
|
sel = nextSel(sel, e.key, list.length, favs.length);
|
||||||
// why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height;
|
// why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height;
|
||||||
// resize so the popup grows/shrinks instead of clipping. requestResize no-ops when unchanged.
|
// resize so the popup grows/shrinks instead of clipping. requestResize no-ops when unchanged.
|
||||||
render({ resize: true });
|
render({ resize: true });
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
runSelected();
|
if (phase === "percent") runJumpToLayer(query.trim());
|
||||||
|
else runSelected();
|
||||||
} else if (e.key === "Escape") {
|
} else if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (query) { query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); }
|
if (phase !== "commands") { exitPhase(); }
|
||||||
|
else if (query) { query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); qEl.focus(); }
|
||||||
else SendMessage({ command: "close_page" });
|
else SendMessage({ command: "close_page" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,20 +22,92 @@ assert.equal(
|
|||||||
"duplicate labels should use the opaque id without interpreting its contents"
|
"duplicate labels should use the opaque id without interpreting its contents"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps the action list hidden");
|
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list");
|
||||||
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps the action list hidden");
|
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps recent/empty list");
|
||||||
assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions");
|
assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions");
|
||||||
|
|
||||||
|
// commandList: an empty query shows recents; a typed query filters all actions.
|
||||||
|
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [],
|
||||||
|
"empty query + no recents shows nothing");
|
||||||
|
assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""),
|
||||||
|
[duplicateActions[0]],
|
||||||
|
"empty query shows the recent list");
|
||||||
|
assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions,
|
||||||
|
"a typed query filters actions (both identical titles match) instead of showing recents");
|
||||||
|
|
||||||
|
// filterTabs (tab phase): an empty query keeps the whole list; a typed query filters by title/id.
|
||||||
|
const tabOptions = [
|
||||||
|
{ id: "home", title: "Home" },
|
||||||
|
{ id: "prepare", title: "Prepare" },
|
||||||
|
{ id: "monitor", title: "Device" },
|
||||||
|
{ id: "project", title: "Project" }
|
||||||
|
];
|
||||||
|
assert.deepEqual(ctx.filterTabs(tabOptions, ""), tabOptions,
|
||||||
|
"empty query keeps the whole tab list");
|
||||||
|
assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1,
|
||||||
|
"a typed query filters tabs by title");
|
||||||
|
assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1,
|
||||||
|
"a typed query matches a tab title");
|
||||||
|
assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [],
|
||||||
|
"a typed query with no match returns an empty list");
|
||||||
|
|
||||||
|
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
|
||||||
|
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
|
||||||
|
"an empty title falls back to the title-cased id");
|
||||||
|
assert.equal(ctx.tabTitle({ id: "home" }), "Home",
|
||||||
|
"a missing title falls back to the title-cased id");
|
||||||
|
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
|
||||||
|
"a populated title is kept as-is");
|
||||||
|
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
|
||||||
|
"a leading space from the Notebook button label is trimmed so the icon letter shows");
|
||||||
|
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
|
||||||
|
"an untitled tab still matches a typed query via the id/title fallback");
|
||||||
|
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
|
||||||
|
"a leading-space tab title still matches a typed query");
|
||||||
|
|
||||||
|
// settingCode: a setting tile uses the leaf option-name initial (label's last " : "-segment),
|
||||||
|
// escalated by category, then a stable ordinal - so settings aren't all a generic "S".
|
||||||
|
const settings = [
|
||||||
|
{ opt_key: "layer_height", label: "Quality : Layer Height", category: "Quality", type: 0 },
|
||||||
|
{ opt_key: "smooth", label: "Quality : Smooth", category: "Quality", type: 0 }
|
||||||
|
];
|
||||||
|
assert.equal(ctx.leafLabel({ label: "Quality : Layer Height", opt_key: "x" }), "Layer Height",
|
||||||
|
"leafLabel takes the last segment of the label");
|
||||||
|
assert.equal(ctx.settingCode(settings[0], settings), "L",
|
||||||
|
"a unique leaf initial yields a single letter");
|
||||||
|
// renderSettingRow iterates settingsResults, so the coded item is always a member of the list.
|
||||||
|
const infillQ = { opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 };
|
||||||
|
const infillS = { opt_key: "b", label: "Supports : Infill", category: "Supports", type: 0 };
|
||||||
|
const sameLeaf = [infillQ, infillS];
|
||||||
|
assert.equal(ctx.settingCode(infillQ, sameLeaf), "QI",
|
||||||
|
"a colliding leaf initial escalates to category + initial");
|
||||||
|
assert.equal(ctx.settingCode(infillS, sameLeaf), "SI",
|
||||||
|
"a different category disambiguates the same leaf initial");
|
||||||
|
const dupQ = [
|
||||||
|
{ opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 },
|
||||||
|
{ opt_key: "b", label: "Quality : Infill", category: "Quality", type: 0 }
|
||||||
|
];
|
||||||
|
assert.equal(ctx.settingCode(dupQ[0], dupQ), "QI1",
|
||||||
|
"both title and category collide -> stable ordinal by opt_key");
|
||||||
|
assert.equal(ctx.settingCode(dupQ[1], dupQ), "QI2",
|
||||||
|
"the ordinal advances by opt_key order");
|
||||||
|
|
||||||
|
// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise).
|
||||||
assert.equal(
|
assert.equal(
|
||||||
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], ""),
|
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""),
|
||||||
null,
|
null,
|
||||||
"Enter with an empty query must not resolve to an action the blank list never showed"
|
"Enter with an empty query and no recents must not resolve to an action the list never showed"
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], "rep"),
|
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"),
|
||||||
"0123456789abcdef",
|
"0123456789abcdef",
|
||||||
"a typed query resolves the list selection"
|
"a typed query resolves the list selection"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""),
|
||||||
|
"0123456789abcdef",
|
||||||
|
"Enter with an empty query resolves the recent entry"
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""),
|
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""),
|
||||||
"fedcba9876543210",
|
"fedcba9876543210",
|
||||||
|
|||||||
@@ -55,6 +55,28 @@ body {
|
|||||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||||
}
|
}
|
||||||
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
|
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
|
||||||
|
/* Hover-revealed remove button in the tile's corner; sits inside the tile bounds so overflow:hidden clips it. */
|
||||||
|
.fav-tile { position: relative; }
|
||||||
|
.fav-unpin {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--muted, var(--orca-muted, #6b7280));
|
||||||
|
background: rgba(127,127,127,.35);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.fav-tile:hover .fav-unpin,
|
||||||
|
.fav-tile.sel .fav-unpin { opacity: 1; }
|
||||||
|
.fav-unpin:hover { background: rgba(127,127,127,.6); }
|
||||||
.ctx-menu {
|
.ctx-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -127,6 +149,15 @@ body {
|
|||||||
color: var(--muted, var(--orca-muted, #6b7280));
|
color: var(--muted, var(--orca-muted, #6b7280));
|
||||||
}
|
}
|
||||||
.dial-count[hidden] { display: none; }
|
.dial-count[hidden] { display: none; }
|
||||||
|
/* Section header inside the list (e.g. the "Recent" row above the recent entries). */
|
||||||
|
.dial-group {
|
||||||
|
padding: 8px 8px 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
color: var(--muted, var(--orca-muted, #6b7280));
|
||||||
|
}
|
||||||
.dial-list {
|
.dial-list {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
#include "ActionRegistry.hpp"
|
#include "ActionRegistry.hpp"
|
||||||
|
|
||||||
|
#include "GCodeViewer.hpp"
|
||||||
|
#include "GLCanvas3D.hpp"
|
||||||
#include "GUI.hpp"
|
#include "GUI.hpp"
|
||||||
#include "GUI_App.hpp"
|
#include "GUI_App.hpp"
|
||||||
#include "I18N.hpp"
|
#include "I18N.hpp"
|
||||||
|
#include "IMSlider.hpp"
|
||||||
|
#include "MainFrame.hpp"
|
||||||
|
#include "Notebook.hpp"
|
||||||
|
#include "Plater.hpp"
|
||||||
|
#include "Search.hpp"
|
||||||
|
#include "Tab.hpp"
|
||||||
#include "slic3r/plugin/PluginManager.hpp"
|
#include "slic3r/plugin/PluginManager.hpp"
|
||||||
|
|
||||||
#include <libslic3r/AppConfig.hpp>
|
#include <libslic3r/AppConfig.hpp>
|
||||||
|
#include <libslic3r/Config.hpp>
|
||||||
|
#include <libslic3r/PresetBundle.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
|
|
||||||
#include <wx/thread.h>
|
#include <wx/thread.h>
|
||||||
|
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
#include <boost/algorithm/string/trim.hpp>
|
||||||
|
#include <boost/nowide/convert.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -28,14 +42,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
|
|||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json read_section(const char* key, nlohmann::json fallback)
|
nlohmann::json read_section(const char* key, nlohmann::json fallback)
|
||||||
{
|
{ return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
|
||||||
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
|
|
||||||
}
|
|
||||||
|
|
||||||
void write_section(const char* key, const nlohmann::json& j)
|
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
|
||||||
{
|
|
||||||
wxGetApp().app_config->set(kConfigSection, key, j.dump());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> read_string_array(const char* key)
|
std::vector<std::string> read_string_array(const char* key)
|
||||||
{
|
{
|
||||||
@@ -79,19 +88,18 @@ struct PluginScriptAction : AppAction
|
|||||||
// The id an action for (plugin_key, capability) would have - lets refresh_capability
|
// The id an action for (plugin_key, capability) would have - lets refresh_capability
|
||||||
// remove a gone capability without materialising the action.
|
// remove a gone capability without materialising the action.
|
||||||
static std::string id_for(const std::string& plugin_key, const std::string& capability)
|
static std::string id_for(const std::string& plugin_key, const std::string& capability)
|
||||||
{
|
{ return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
|
||||||
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
|
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
|
||||||
: AppAction(kIdPrefix,
|
: AppAction(kIdPrefix,
|
||||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||||
plugin_key_in, // source_key
|
plugin_key_in, // source_key
|
||||||
std::move(source_name)),
|
std::move(source_name))
|
||||||
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
|
, plugin_key(std::move(plugin_key_in))
|
||||||
|
, capability(std::move(capability_in))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
AppActionRunResult run() const override
|
AppActionRunResult run(const std::string& /*param*/) const override
|
||||||
{
|
{
|
||||||
std::string error;
|
std::string error;
|
||||||
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
||||||
@@ -107,8 +115,7 @@ struct PluginScriptAction : AppAction
|
|||||||
|
|
||||||
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
|
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
|
||||||
// enabled script capability.
|
// enabled script capability.
|
||||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
|
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
|
||||||
const std::string& source_name)
|
|
||||||
{
|
{
|
||||||
PluginManager& manager = PluginManager::instance();
|
PluginManager& manager = PluginManager::instance();
|
||||||
if (!manager.is_plugin_loaded(plugin_key))
|
if (!manager.is_plugin_loaded(plugin_key))
|
||||||
@@ -119,8 +126,167 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
|
|||||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- built-in command actions (the speed dial "commands" section) ------
|
||||||
|
|
||||||
|
constexpr const char* kCommandPrefix = "orca_command";
|
||||||
|
constexpr const char* kOrcaSourceKey = "orca";
|
||||||
|
constexpr const char* kOrcaSourceName = "OrcaSlicer";
|
||||||
|
|
||||||
|
// Jump the preview to a layer selected by a 0-100 percent of the layer range. Best-effort:
|
||||||
|
// switches to the preview tab and requests a slice (select_view_3D("Preview", false)); if the
|
||||||
|
// slicer result is already present the slider is repositioned immediately, otherwise the user
|
||||||
|
// can re-run after slicing.
|
||||||
|
void go_to_layer(Plater* plater, const std::string& param)
|
||||||
|
{
|
||||||
|
if (!plater)
|
||||||
|
return;
|
||||||
|
double pct = 50.0;
|
||||||
|
try {
|
||||||
|
pct = std::stod(param);
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
pct = std::clamp(pct, 0.0, 100.0);
|
||||||
|
|
||||||
|
GLCanvas3D* canvas = plater->get_current_canvas3D();
|
||||||
|
if (!canvas)
|
||||||
|
return;
|
||||||
|
GCodeViewer& viewer = canvas->get_gcode_viewer();
|
||||||
|
IMSlider* layers = viewer.get_layers_slider();
|
||||||
|
IMSlider* moves = viewer.get_moves_slider();
|
||||||
|
if (!layers || layers->GetMaxValue() <= 0)
|
||||||
|
return; // no slice result yet - the slice request above will populate it
|
||||||
|
|
||||||
|
const double max = double(layers->GetMaxValue());
|
||||||
|
const int target = int(std::lround(pct / 100.0 * max));
|
||||||
|
layers->SetHigherValue(target);
|
||||||
|
// In "one layer" mode the lower handle follows the higher one (mirrors arrow-key nav).
|
||||||
|
if (layers->is_one_layer())
|
||||||
|
layers->SetLowerValue(target);
|
||||||
|
layers->set_as_dirty();
|
||||||
|
if (moves) {
|
||||||
|
moves->SetHigherValue(moves->GetMaxValue());
|
||||||
|
moves->set_as_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch a built-in command. The CommandAction stays a thin value; the actual GUI work
|
||||||
|
// lives here so it can touch the live app state.
|
||||||
|
AppActionRunResult run_native_command(const std::string& command_key, const std::string& param)
|
||||||
|
{
|
||||||
|
GUI_App& app = wxGetApp();
|
||||||
|
if (app.is_closing())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Plater* plater = app.plater();
|
||||||
|
|
||||||
|
if (command_key == "save_project") {
|
||||||
|
if (plater)
|
||||||
|
plater->save_project(false);
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "save_project_as") {
|
||||||
|
if (plater)
|
||||||
|
plater->save_project(true);
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "load_project") {
|
||||||
|
if (plater)
|
||||||
|
plater->load_project();
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "open_preferences") {
|
||||||
|
app.open_preferences();
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "mode_simple" || command_key == "mode_advanced" || command_key == "mode_expert") {
|
||||||
|
const int mode = command_key == "mode_simple" ? comSimple : command_key == "mode_advanced" ? comAdvanced : comExpert;
|
||||||
|
app.save_mode(mode);
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "slice_and_preview") {
|
||||||
|
if (plater) {
|
||||||
|
plater->select_view_3D("Preview", false);
|
||||||
|
if (app.mainframe)
|
||||||
|
app.mainframe->select_tab(TAB_ID_PREVIEW);
|
||||||
|
}
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
if (command_key == "go_to_layer") {
|
||||||
|
if (plater) {
|
||||||
|
plater->select_view_3D("Preview", false);
|
||||||
|
if (app.mainframe)
|
||||||
|
app.mainframe->select_tab(TAB_ID_PREVIEW);
|
||||||
|
go_to_layer(plater, param);
|
||||||
|
}
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
}
|
||||||
|
// "go_to_setting"/"go_to_tab" are two-phase: the palette collects the option after
|
||||||
|
// activating it, so dispatch here is a no-op (the actual jump goes through the web command).
|
||||||
|
if (command_key == "go_to_setting" || command_key == "go_to_tab")
|
||||||
|
return {AppActionRunResult::Level::Success};
|
||||||
|
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A built-in command action. source_key is the constant "orca" so a renamed title never
|
||||||
|
// re-keys the action (matches the plugin source-key contract).
|
||||||
|
struct CommandAction : AppAction
|
||||||
|
{
|
||||||
|
std::string command_key;
|
||||||
|
|
||||||
|
CommandAction(std::string command_key, std::string title, std::string group, std::string input = "")
|
||||||
|
: AppAction(kCommandPrefix, std::move(title), kOrcaSourceKey, kOrcaSourceName), command_key(std::move(command_key))
|
||||||
|
{
|
||||||
|
this->kind = AppActionKind::Command;
|
||||||
|
this->group = std::move(group);
|
||||||
|
this->input = std::move(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppActionRunResult run(const std::string& param) const override { return run_native_command(command_key, param); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<AppAction> make_command(std::string key, std::string title, std::string group, std::string input = "")
|
||||||
|
{ return std::make_unique<CommandAction>(std::move(key), std::move(title), std::move(group), std::move(input)); }
|
||||||
|
|
||||||
|
// The built-in palette commands, registered once at init().
|
||||||
|
std::vector<std::unique_ptr<AppAction>> native_commands()
|
||||||
|
{
|
||||||
|
std::vector<std::unique_ptr<AppAction>> out;
|
||||||
|
// why: _u8L (std::string) for titles/groups - make_command takes std::string; _L would
|
||||||
|
// return a wxString and silently fail to convert here.
|
||||||
|
out.push_back(make_command("slice_and_preview", _u8L("Slice and Preview"), _u8L("Commands")));
|
||||||
|
// Two-phase commands: activating them collects input in the palette, then runs.
|
||||||
|
out.push_back(make_command("go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "percent"));
|
||||||
|
// The "…" is avoided in the msgid: use ASCII "..." to keep the .pot extraction simple.
|
||||||
|
out.push_back(make_command("go_to_setting", _u8L("Go to setting..."), _u8L("Commands"), "settings"));
|
||||||
|
out.push_back(make_command("go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), "tab"));
|
||||||
|
out.push_back(make_command("load_project", _u8L("Load Project"), _u8L("Commands")));
|
||||||
|
out.push_back(make_command("save_project", _u8L("Save Project"), _u8L("Commands")));
|
||||||
|
out.push_back(make_command("save_project_as", _u8L("Save Project As"), _u8L("Commands")));
|
||||||
|
out.push_back(make_command("open_preferences", _u8L("Preferences"), _u8L("Commands")));
|
||||||
|
out.push_back(make_command("mode_simple", _u8L("Mode: Simple"), _u8L("Mode")));
|
||||||
|
out.push_back(make_command("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode")));
|
||||||
|
out.push_back(make_command("mode_expert", _u8L("Mode: Expert"), _u8L("Mode")));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicates Sidebar's get_search_inputs(): the configs of every tab supporting the current
|
||||||
|
// printer technology, in the current UI mode.
|
||||||
|
std::vector<Search::InputInfo> settings_inputs()
|
||||||
|
{
|
||||||
|
std::vector<Search::InputInfo> ret;
|
||||||
|
GUI_App& app = wxGetApp();
|
||||||
|
if (!app.preset_bundle)
|
||||||
|
return ret;
|
||||||
|
auto print_tech = app.preset_bundle->printers.get_selected_preset().printer_technology();
|
||||||
|
for (Tab* tab : app.tabs_list)
|
||||||
|
if (tab && tab->supports_printer_technology(print_tech))
|
||||||
|
ret.emplace_back(Search::InputInfo{tab->get_config(), tab->type(), app.get_mode()});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
ActionRegistry::~ActionRegistry() = default;
|
||||||
|
|
||||||
void ActionRegistry::init()
|
void ActionRegistry::init()
|
||||||
{
|
{
|
||||||
assert(wxThread::IsMain());
|
assert(wxThread::IsMain());
|
||||||
@@ -151,18 +317,12 @@ void ActionRegistry::init()
|
|||||||
// Subscribe before enumerating so a concurrent load cannot land between the initial
|
// Subscribe before enumerating so a concurrent load cannot land between the initial
|
||||||
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
|
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
|
||||||
// id and the m_actions scan in refresh_source is idempotent.
|
// id and the m_actions scan in refresh_source is idempotent.
|
||||||
manager.subscribe_on_load_callback(
|
manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||||
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||||
manager.subscribe_on_unload_callback(
|
|
||||||
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
|
||||||
manager.subscribe_on_capability_load_callback(
|
manager.subscribe_on_capability_load_callback(
|
||||||
[on_capability](const PluginCapabilityId& capability) {
|
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
|
||||||
on_capability(capability, ActionChange::Added);
|
|
||||||
});
|
|
||||||
manager.subscribe_on_capability_unload_callback(
|
manager.subscribe_on_capability_unload_callback(
|
||||||
[on_capability](const PluginCapabilityId& capability) {
|
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
|
||||||
on_capability(capability, ActionChange::Removed);
|
|
||||||
});
|
|
||||||
|
|
||||||
// enumerate current script capabilities
|
// enumerate current script capabilities
|
||||||
std::unordered_map<std::string, std::string> source_names;
|
std::unordered_map<std::string, std::string> source_names;
|
||||||
@@ -179,6 +339,12 @@ void ActionRegistry::init()
|
|||||||
if (auto action = make_action(key, capability->name(), source_name))
|
if (auto action = make_action(key, capability->name(), source_name))
|
||||||
upsert(std::move(action));
|
upsert(std::move(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
|
||||||
|
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
|
||||||
|
// by prefix, so this is order-independent.
|
||||||
|
for (auto& action : native_commands())
|
||||||
|
upsert(std::move(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
||||||
@@ -208,8 +374,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
|
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||||
ActionChange change)
|
|
||||||
{
|
{
|
||||||
assert(wxThread::IsMain());
|
assert(wxThread::IsMain());
|
||||||
|
|
||||||
@@ -269,14 +434,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
|
|||||||
return it == m_actions.end() ? nullptr : it->second.get();
|
return it == m_actions.end() ? nullptr : it->second.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
AppAction* ActionRegistry::find(const std::string& id)
|
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
|
||||||
{
|
|
||||||
return const_cast<AppAction*>(by_id(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- dispatch + write-through ----------------------------------------------
|
// ---- dispatch + write-through ----------------------------------------------
|
||||||
|
|
||||||
AppActionRunResult ActionRegistry::run(const std::string& id)
|
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
|
||||||
{
|
{
|
||||||
assert(wxThread::IsMain());
|
assert(wxThread::IsMain());
|
||||||
auto it = m_actions.find(id);
|
auto it = m_actions.find(id);
|
||||||
@@ -286,7 +448,7 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
|||||||
// nested event loop; a queued source refresh can erase the entry while the
|
// nested event loop; a queued source refresh can erase the entry while the
|
||||||
// keep-alive preserves the action until run returns.
|
// keep-alive preserves the action until run returns.
|
||||||
std::shared_ptr<AppAction> keep = it->second;
|
std::shared_ptr<AppAction> keep = it->second;
|
||||||
AppActionRunResult o = keep->run();
|
AppActionRunResult o = keep->run(param);
|
||||||
if (o.level == AppActionRunResult::Level::Busy)
|
if (o.level == AppActionRunResult::Level::Busy)
|
||||||
return o;
|
return o;
|
||||||
|
|
||||||
@@ -300,7 +462,10 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
|||||||
e["count"] = e.value("count", 0) + 1;
|
e["count"] = e.value("count", 0) + 1;
|
||||||
e["last"] = (long long) std::time(nullptr);
|
e["last"] = (long long) std::time(nullptr);
|
||||||
write_section("stats", stats);
|
write_section("stats", stats);
|
||||||
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; }
|
if (AppAction* live = find(id)) {
|
||||||
|
live->count = e["count"];
|
||||||
|
live->last = e["last"];
|
||||||
|
}
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,8 +490,7 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
|||||||
std::vector<std::string> next;
|
std::vector<std::string> next;
|
||||||
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
|
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
|
||||||
for (const auto& id : ids)
|
for (const auto& id : ids)
|
||||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
|
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
|
||||||
std::find(next.begin(), next.end(), id) == next.end())
|
|
||||||
next.push_back(id);
|
next.push_back(id);
|
||||||
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
|
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
|
||||||
for (const auto& id : cur)
|
for (const auto& id : cur)
|
||||||
@@ -374,17 +538,139 @@ nlohmann::json ActionRegistry::snapshot() const
|
|||||||
return a->id() < b->id();
|
return a->id() < b->id();
|
||||||
});
|
});
|
||||||
|
|
||||||
nlohmann::json actions = nlohmann::json::array();
|
auto action_to_json = [](const AppAction* a) {
|
||||||
for (const AppAction* a : sorted)
|
return nlohmann::json({{"id", a->id()},
|
||||||
actions.push_back({{"id", a->id()},
|
|
||||||
{"title", a->title()},
|
{"title", a->title()},
|
||||||
{"source", a->source_name()},
|
{"source", a->source_name()},
|
||||||
|
{"group", a->group},
|
||||||
|
{"input", a->input},
|
||||||
{"shortcut", ""}});
|
{"shortcut", ""}});
|
||||||
|
};
|
||||||
|
|
||||||
|
nlohmann::json actions = nlohmann::json::array();
|
||||||
|
for (const AppAction* a : sorted)
|
||||||
|
actions.push_back(action_to_json(a));
|
||||||
|
|
||||||
// why: favourites is the ORDERED pin list - it must come from favourite_actions
|
// why: favourites is the ORDERED pin list - it must come from favourite_actions
|
||||||
// as stored, not be re-derived from the frecency-sorted actions (that would
|
// as stored, not be re-derived from the frecency-sorted actions (that would
|
||||||
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
|
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
|
||||||
nlohmann::json favourites(read_string_array("favourite_actions"));
|
nlohmann::json favourites(read_string_array("favourite_actions"));
|
||||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
|
|
||||||
|
// Recent = the last-N launched actions by recency (only actions with a run history).
|
||||||
|
constexpr size_t kRecentLimit = 5;
|
||||||
|
std::vector<const AppAction*> recent;
|
||||||
|
for (const auto& entry : m_actions)
|
||||||
|
if (entry.second->last > 0)
|
||||||
|
recent.push_back(entry.second.get());
|
||||||
|
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
|
||||||
|
if (a->last != b->last)
|
||||||
|
return a->last > b->last;
|
||||||
|
return a->id() < b->id();
|
||||||
|
});
|
||||||
|
if (recent.size() > kRecentLimit)
|
||||||
|
recent.resize(kRecentLimit);
|
||||||
|
nlohmann::json recent_json = nlohmann::json::array();
|
||||||
|
for (const AppAction* a : recent)
|
||||||
|
recent_json.push_back(action_to_json(a));
|
||||||
|
|
||||||
|
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}, {"recent", std::move(recent_json)}};
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json ActionRegistry::settings_search(const std::string& query)
|
||||||
|
{
|
||||||
|
assert(wxThread::IsMain());
|
||||||
|
std::string q = boost::trim_copy(query);
|
||||||
|
// Empty query: show the recently-jumped-to settings instead of a blank list.
|
||||||
|
if (q.empty())
|
||||||
|
return settings_recent();
|
||||||
|
|
||||||
|
// Use the sidebar's live searcher. It is the instance Tab registration (add_key) populates
|
||||||
|
// with each option's group/category, and it carries the current printer technology. A fresh
|
||||||
|
// OptionsSearcher has an empty groups_and_categories, so init()/append_options() drops every
|
||||||
|
// option and the search returns nothing.
|
||||||
|
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||||
|
searcher.init(settings_inputs());
|
||||||
|
searcher.search(q, true);
|
||||||
|
auto& found = searcher.found_options();
|
||||||
|
|
||||||
|
constexpr size_t kLimit = 20;
|
||||||
|
const size_t n = std::min<size_t>(kLimit, found.size());
|
||||||
|
nlohmann::json out = nlohmann::json::array();
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const auto& opt = searcher.get_option(i);
|
||||||
|
// Clean plain label "category : group : label" - OptionsSearcher's own label string
|
||||||
|
// carries ImGui icon control chars + <b>/</b> markup (SUPPORTS_MARKUP), which render
|
||||||
|
// as garbage in the webview. Build it from the Option's localized strings instead.
|
||||||
|
std::wstring plain;
|
||||||
|
const std::wstring* prev = nullptr;
|
||||||
|
for (const std::wstring* const s : {&opt.category_local, &opt.group_local, &opt.label_local})
|
||||||
|
if (s != nullptr && !s->empty() && (prev == nullptr || *prev != *s)) {
|
||||||
|
if (!plain.empty())
|
||||||
|
plain += L" : ";
|
||||||
|
plain += *s;
|
||||||
|
prev = s;
|
||||||
|
}
|
||||||
|
out.push_back({{"opt_key", opt.opt_key()},
|
||||||
|
{"type", int(opt.type)},
|
||||||
|
{"label", boost::nowide::narrow(plain)},
|
||||||
|
{"category", boost::nowide::narrow(opt.category)},
|
||||||
|
{"group", boost::nowide::narrow(opt.group)}});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
||||||
|
|
||||||
|
nlohmann::json ActionRegistry::tab_options() const
|
||||||
|
{
|
||||||
|
assert(wxThread::IsMain());
|
||||||
|
nlohmann::json out = nlohmann::json::array();
|
||||||
|
if (!wxTheApp || wxGetApp().is_closing())
|
||||||
|
return out;
|
||||||
|
MainFrame* mf = wxGetApp().mainframe;
|
||||||
|
if (!mf || !mf->m_tabpanel)
|
||||||
|
return out;
|
||||||
|
Notebook* notebook = mf->m_tabpanel;
|
||||||
|
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
|
||||||
|
const wxString id = notebook->GetPageName(i);
|
||||||
|
if (id.empty())
|
||||||
|
continue;
|
||||||
|
out.push_back({{"id", id.ToStdString()}, {"title", notebook->GetPageText(i).ToStdString()}});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- settings recents (persisted, most-recent-first, capped at 8) -----------
|
||||||
|
|
||||||
|
nlohmann::json ActionRegistry::settings_recent() const
|
||||||
|
{
|
||||||
|
assert(wxThread::IsMain());
|
||||||
|
return read_section("recent_settings", nlohmann::json::array());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActionRegistry::record_setting_recent(
|
||||||
|
const std::string& opt_key, int type, const std::string& label, const std::string& category, const std::string& group)
|
||||||
|
{
|
||||||
|
assert(wxThread::IsMain());
|
||||||
|
if (opt_key.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
constexpr size_t kLimit = 8;
|
||||||
|
auto arr = read_section("recent_settings", nlohmann::json::array());
|
||||||
|
if (!arr.is_array())
|
||||||
|
arr = nlohmann::json::array();
|
||||||
|
auto same = [&](const nlohmann::json& e) {
|
||||||
|
return e.is_object() && e.value("opt_key", std::string()) == opt_key && e.value("type", int(-1)) == type;
|
||||||
|
};
|
||||||
|
|
||||||
|
nlohmann::json next = nlohmann::json::array();
|
||||||
|
next.push_back({{"opt_key", opt_key}, {"type", type}, {"label", label}, {"category", category}, {"group", group}});
|
||||||
|
for (const auto& e : arr)
|
||||||
|
if (!same(e))
|
||||||
|
next.push_back(e);
|
||||||
|
if (next.size() > kLimit)
|
||||||
|
next.erase(next.begin() + long(kLimit), next.end());
|
||||||
|
write_section("recent_settings", next);
|
||||||
}
|
}
|
||||||
|
|
||||||
}} // namespace Slic3r::GUI
|
}} // namespace Slic3r::GUI
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ namespace Slic3r { namespace GUI {
|
|||||||
// How a source's action set changed. Drives the registry's refresh handlers.
|
// How a source's action set changed. Drives the registry's refresh handlers.
|
||||||
enum class ActionChange { Added, Removed };
|
enum class ActionChange { Added, Removed };
|
||||||
|
|
||||||
|
// What kind of runnable thing an action is. Drives the palette section + dispatch.
|
||||||
|
enum class AppActionKind { Plugin, Command };
|
||||||
|
|
||||||
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
|
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
|
||||||
// actions translate their runner-specific result into this generic shape.
|
// actions translate their runner-specific result into this generic shape.
|
||||||
struct AppActionRunResult
|
struct AppActionRunResult
|
||||||
@@ -56,8 +59,18 @@ struct AppAction
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
long long last = 0; // epoch seconds
|
long long last = 0; // epoch seconds
|
||||||
|
|
||||||
|
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
|
||||||
|
// source name); Command sets a section label (e.g. "Commands", "Mode").
|
||||||
|
AppActionKind kind = AppActionKind::Plugin;
|
||||||
|
std::string group;
|
||||||
|
// Second-phase input descriptor for the palette: "settings" (jump to a config option)
|
||||||
|
// or "percent" (jump to layer by a 0-100 value). Empty = run immediately on activation.
|
||||||
|
std::string input;
|
||||||
|
|
||||||
virtual ~AppAction() = default;
|
virtual ~AppAction() = default;
|
||||||
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
|
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
||||||
|
// commands (e.g. a layer percentage); plugins ignore it.
|
||||||
|
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// The definition is constructor-set and immutable. Refreshes replace an action
|
// The definition is constructor-set and immutable. Refreshes replace an action
|
||||||
@@ -92,6 +105,8 @@ private:
|
|||||||
class ActionRegistry
|
class ActionRegistry
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
~ActionRegistry();
|
||||||
|
|
||||||
// Subscribes to the plugin loader and enumerates its current actions. Call once
|
// Subscribes to the plugin loader and enumerates its current actions. Call once
|
||||||
// on the UI thread after the plugin system is up; wires the initial list and live
|
// on the UI thread after the plugin system is up; wires the initial list and live
|
||||||
// updates together.
|
// updates together.
|
||||||
@@ -108,7 +123,7 @@ public:
|
|||||||
const AppAction* by_id(const std::string& id) const;
|
const AppAction* by_id(const std::string& id) const;
|
||||||
|
|
||||||
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
||||||
AppActionRunResult run(const std::string& id); // runs + bumps stats
|
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
|
||||||
void set_favourite(const std::string& id, bool on);
|
void set_favourite(const std::string& id, bool on);
|
||||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||||
|
|
||||||
@@ -116,9 +131,33 @@ public:
|
|||||||
bool should_ask(const std::string& id) const;
|
bool should_ask(const std::string& id) const;
|
||||||
void suppress_ask(const std::string& id);
|
void suppress_ask(const std::string& id);
|
||||||
|
|
||||||
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
|
// Flat, frecency-sorted snapshot for the webview:
|
||||||
|
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
|
||||||
nlohmann::json snapshot() const;
|
nlohmann::json snapshot() const;
|
||||||
|
|
||||||
|
// "Go to setting..." Speed Dial helper: query the current print/filament/printer
|
||||||
|
// config options via the sidebar's live OptionsSearcher (the instance Tab registration
|
||||||
|
// populates with group/category, and which carries the current printer technology) and
|
||||||
|
// return the top matches as JSON. The searcher is re-seeded from the current configs +
|
||||||
|
// user mode on every call so the result always reflects what the sidebar's own search
|
||||||
|
// would show. An empty/whitespace query returns the recent settings list (below), and the
|
||||||
|
// page shows a "type to search" hint when there are no recents.
|
||||||
|
nlohmann::json settings_search(const std::string& query);
|
||||||
|
|
||||||
|
// Recently-jumped-to settings, persisted (most-recent-first, capped at 8). Returns the
|
||||||
|
// stored JSON array [{opt_key,type,label,category,group},...]; record_setting_recent()
|
||||||
|
// prepends an entry (deduped by opt_key+type) and re-persists.
|
||||||
|
nlohmann::json settings_recent() const;
|
||||||
|
void record_setting_recent(const std::string& opt_key, int type, const std::string& label,
|
||||||
|
const std::string& category, const std::string& group);
|
||||||
|
|
||||||
|
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
|
||||||
|
// as [{id,title},...]. Live by construction - built-in tabs (Home/Prepare/Preview/Device/
|
||||||
|
// Project/Calibration) and plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a
|
||||||
|
// page appears/disappears with the notebook. Plugin tabs hidden in the overflow menu (many
|
||||||
|
// plugins) aren't separate pages and are not listed. Call on the UI thread; null-safe.
|
||||||
|
nlohmann::json tab_options() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void seed_state(AppAction& a) const; // favourite/stats from config
|
void seed_state(AppAction& a) const; // favourite/stats from config
|
||||||
AppAction* find(const std::string& id);
|
AppAction* find(const std::string& id);
|
||||||
|
|||||||
@@ -1039,7 +1039,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
|||||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
|
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
|
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
|
||||||
@@ -3502,11 +3501,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
|
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
|
||||||
case ' ': {
|
|
||||||
if (m_canvas_type == ECanvasType::CanvasView3D)
|
|
||||||
post_event(SimpleEvent(EVT_GLCANVAS_OPEN_SPEED_DIAL));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'A':
|
case 'A':
|
||||||
case 'a':
|
case 'a':
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
|||||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
|
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||||
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
|
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ void KBShortcutsDialog::fill_shortcuts()
|
|||||||
|
|
||||||
// Switch table page
|
// Switch table page
|
||||||
{ ctrl + L("Tab"), L("Switch table page")},
|
{ ctrl + L("Tab"), L("Switch table page")},
|
||||||
|
// Open speed dial
|
||||||
|
{ ctrl + "K", L("Open speed dial") },
|
||||||
//DEL
|
//DEL
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
{"fn+⌫", L("Delete Selected")},
|
{"fn+⌫", L("Delete Selected")},
|
||||||
@@ -267,8 +269,6 @@ void KBShortcutsDialog::fill_shortcuts()
|
|||||||
{ "O", L("Zoom out") },
|
{ "O", L("Zoom out") },
|
||||||
{ "V", L("Toggle printable for object/part") },
|
{ "V", L("Toggle printable for object/part") },
|
||||||
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
|
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
|
||||||
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
|
|
||||||
|
|
||||||
};
|
};
|
||||||
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
|
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
|
||||||
|
|
||||||
|
|||||||
@@ -701,6 +701,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
|||||||
}
|
}
|
||||||
return;}
|
return;}
|
||||||
#endif
|
#endif
|
||||||
|
// Orca: open the speed dial from any page. CmdDown() = Ctrl on Win/Linux, Cmd on macOS.
|
||||||
|
if (evt.CmdDown() && evt.GetKeyCode() == 'K') { wxGetApp().open_speed_dial(); return; }
|
||||||
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
|
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
|
||||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
|
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
|
||||||
m_plater->apply_background_progress();
|
m_plater->apply_background_progress();
|
||||||
@@ -3346,6 +3348,11 @@ void MainFrame::init_menubar_as_editor()
|
|||||||
wxGetApp().open_preferences();
|
wxGetApp().open_preferences();
|
||||||
},
|
},
|
||||||
"", nullptr, []() { return true; }, this, 1);
|
"", nullptr, []() { return true; }, this, 1);
|
||||||
|
parent_menu->AppendSeparator();
|
||||||
|
append_menu_item(
|
||||||
|
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "",
|
||||||
|
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||||
|
"", nullptr, []() { return true; }, this);
|
||||||
//parent_menu->Insert(1, preference_item);
|
//parent_menu->Insert(1, preference_item);
|
||||||
#endif
|
#endif
|
||||||
// Help menu
|
// Help menu
|
||||||
@@ -3370,6 +3377,12 @@ void MainFrame::init_menubar_as_editor()
|
|||||||
auto top_menu = m_topbar->GetTopMenu();
|
auto top_menu = m_topbar->GetTopMenu();
|
||||||
top_menu->AppendSeparator();
|
top_menu->AppendSeparator();
|
||||||
|
|
||||||
|
append_menu_item(
|
||||||
|
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + ctrl + "K", "",
|
||||||
|
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||||
|
"", nullptr, []() { return true; }, this);
|
||||||
|
top_menu->AppendSeparator();
|
||||||
|
|
||||||
append_menu_item(
|
append_menu_item(
|
||||||
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
|
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
|
||||||
[this](wxCommandEvent &) {
|
[this](wxCommandEvent &) {
|
||||||
@@ -3508,6 +3521,10 @@ void MainFrame::init_menubar_as_editor()
|
|||||||
#else
|
#else
|
||||||
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
|
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
|
||||||
fileMenu->AppendSeparator();
|
fileMenu->AppendSeparator();
|
||||||
|
append_menu_item(
|
||||||
|
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "",
|
||||||
|
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
|
||||||
|
"", nullptr, []() { return true; }, this);
|
||||||
append_menu_item(
|
append_menu_item(
|
||||||
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
|
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
|
||||||
[this](wxCommandEvent&) {
|
[this](wxCommandEvent&) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "Widgets/Label.hpp"
|
#include "Widgets/Label.hpp"
|
||||||
|
|
||||||
#include <wx/button.h>
|
#include <wx/button.h>
|
||||||
|
#include <wx/dcclient.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
|
|
||||||
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
|
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
|
||||||
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
|
|||||||
|
|
||||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
||||||
{
|
{
|
||||||
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
|
Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
|
||||||
btn->SetCornerRadius(0);
|
btn->SetCornerRadius(0);
|
||||||
|
|
||||||
if (bmp_name.empty() && bmp.IsOk())
|
if (bmp_name.empty() && bmp.IsOk())
|
||||||
btn->SetIcon(bmp);
|
btn->SetIcon(bmp);
|
||||||
|
|
||||||
|
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
|
||||||
|
// original spacing between a tab's icon and its caption.
|
||||||
|
{
|
||||||
|
wxClientDC dc(btn);
|
||||||
|
dc.SetFont(btn->GetFont());
|
||||||
|
int space_w = 0;
|
||||||
|
dc.GetTextExtent(" ", &space_w, nullptr);
|
||||||
|
btn->SetIconSpacing(5 + space_w);
|
||||||
|
}
|
||||||
|
|
||||||
int em = em_unit(this);
|
int em = em_unit(this);
|
||||||
//BBS set size for button
|
//BBS set size for button
|
||||||
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
||||||
@@ -245,7 +256,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
|
|||||||
int em = em_unit(this);
|
int em = em_unit(this);
|
||||||
Button* btn = m_pageButtons[n];
|
Button* btn = m_pageButtons[n];
|
||||||
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
|
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
|
||||||
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n]));
|
btn->SetLabel(compact ? "" : m_pageLabels[n]);
|
||||||
}
|
}
|
||||||
|
|
||||||
wxString ButtonsListCtrl::GetPageText(size_t n) const
|
wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||||
|
|||||||
@@ -7567,10 +7567,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|||||||
|
|
||||||
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
||||||
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||||
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
|
|
||||||
if (this->q->is_view3D_shown())
|
|
||||||
wxGetApp().open_speed_dial();
|
|
||||||
});
|
|
||||||
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
|
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
|
||||||
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
|
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
|
||||||
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
||||||
|
|||||||
@@ -9,12 +9,18 @@
|
|||||||
#include "Plater.hpp"
|
#include "Plater.hpp"
|
||||||
#include "Widgets/WebViewHostDialog.hpp"
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Preset.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <wx/display.h>
|
#include <wx/display.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
#include <wx/stattext.h>
|
#include <wx/stattext.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -33,23 +39,49 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
|||||||
|
|
||||||
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
||||||
|
|
||||||
|
// Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container.
|
||||||
|
// (browser()->SetFocus() grabs focus on the container and doesn't reach the web content,
|
||||||
|
// so typing only works after the user clicks.) On Linux the native backend is the
|
||||||
|
// WebKitWebView widget; grab focus there directly. Elsewhere SetFocus() is correct.
|
||||||
|
void focus_webview(wxWebView* browser, bool page_ready)
|
||||||
|
{
|
||||||
|
if (!browser)
|
||||||
|
return;
|
||||||
|
#ifdef __linux__
|
||||||
|
if (void* nb = browser->GetNativeBackend())
|
||||||
|
gtk_widget_grab_focus((GtkWidget*) nb);
|
||||||
|
#else
|
||||||
|
browser->SetFocus();
|
||||||
|
#endif
|
||||||
|
if (page_ready)
|
||||||
|
browser->RunScript("focusInput();");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
|
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
|
||||||
: WebViewHostDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
: WebViewHostDialog(parent,
|
||||||
wxBORDER_NONE | wxFRAME_NO_TASKBAR)
|
wxID_ANY,
|
||||||
|
wxEmptyString,
|
||||||
|
wxDefaultPosition,
|
||||||
|
wxDefaultSize,
|
||||||
|
wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT)
|
||||||
{
|
{
|
||||||
SetBackgroundColour(bg_color());
|
SetBackgroundColour(bg_color());
|
||||||
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
|
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
|
||||||
if (!event.GetActive() && IsShown())
|
// Focus the WebKit widget exactly when the WM makes the popup the active window
|
||||||
|
// (modeless focus is granted asynchronously, so a focus request made right after
|
||||||
|
// Show() is dropped). Also re-corrects focus on every re-open.
|
||||||
|
if (event.GetActive() && IsShown())
|
||||||
|
focus_webview(browser(), m_page_ready);
|
||||||
|
else if (!event.GetActive() && IsShown())
|
||||||
Hide();
|
Hide();
|
||||||
event.Skip();
|
event.Skip();
|
||||||
});
|
});
|
||||||
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString,
|
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight),
|
||||||
wxSize(kPopupWidth, kPopupMaxHeight), wxSize(kPopupWidth, kPopupMinHeight))) {
|
wxSize(kPopupWidth, kPopupMinHeight))) {
|
||||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")),
|
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20));
|
||||||
wxSizerFlags().Border(wxALL, 20));
|
|
||||||
SetSizer(sizer);
|
SetSizer(sizer);
|
||||||
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
|
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
|
||||||
}
|
}
|
||||||
@@ -61,8 +93,7 @@ void SpeedDialWebDialog::request_show()
|
|||||||
{
|
{
|
||||||
if (IsShown()) {
|
if (IsShown()) {
|
||||||
Raise();
|
Raise();
|
||||||
if (browser())
|
focus_webview(browser(), m_page_ready);
|
||||||
browser()->SetFocus();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +101,9 @@ void SpeedDialWebDialog::request_show()
|
|||||||
Raise();
|
Raise();
|
||||||
if (m_page_ready)
|
if (m_page_ready)
|
||||||
send_actions();
|
send_actions();
|
||||||
if (browser())
|
// Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is
|
||||||
browser()->SetFocus();
|
// what makes typing reach the search field immediately on open.
|
||||||
|
focus_webview(browser(), m_page_ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
|
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
|
||||||
@@ -95,8 +127,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
|
|||||||
if (command == "request_actions") {
|
if (command == "request_actions") {
|
||||||
m_page_ready = true;
|
m_page_ready = true;
|
||||||
send_actions();
|
send_actions();
|
||||||
}
|
} else if (command == "toggle_favourite")
|
||||||
else if (command == "toggle_favourite")
|
|
||||||
wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false));
|
wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false));
|
||||||
else if (command == "reorder_favourites") {
|
else if (command == "reorder_favourites") {
|
||||||
std::vector<std::string> ids;
|
std::vector<std::string> ids;
|
||||||
@@ -105,13 +136,61 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
|
|||||||
if (id.is_string())
|
if (id.is_string())
|
||||||
ids.push_back(id.get<std::string>());
|
ids.push_back(id.get<std::string>());
|
||||||
wxGetApp().action_registry().reorder_favourites(ids);
|
wxGetApp().action_registry().reorder_favourites(ids);
|
||||||
|
} else if (command == "run_action")
|
||||||
|
run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", ""));
|
||||||
|
else if (command == "go_to_setting") {
|
||||||
|
// "Go to setting..." second phase: the page hands back the option it matched.
|
||||||
|
const std::string opt_key = payload.value("opt_key", "");
|
||||||
|
if (!opt_key.empty()) {
|
||||||
|
const int type = json_int_or(payload, "type", int(Preset::TYPE_INVALID));
|
||||||
|
const std::string label = payload.value("label", "");
|
||||||
|
const std::string group = payload.value("group", "");
|
||||||
|
const std::string cat = payload.value("category", "");
|
||||||
|
// Track it in the palette's recent-settings list before jumping (persisted).
|
||||||
|
wxGetApp().action_registry().record_setting_recent(opt_key, type, label, cat, group);
|
||||||
|
Hide();
|
||||||
|
wxGetApp().sidebar().jump_to_option(opt_key, Preset::Type(type), from_u8(cat).ToStdWstring());
|
||||||
}
|
}
|
||||||
else if (command == "run_action")
|
} else if (command == "search_settings")
|
||||||
run_action(payload.value("id", ""), payload.value("title", ""));
|
search_settings(payload.value("q", ""));
|
||||||
else if (command == "resize")
|
else if (command == "search_tabs")
|
||||||
|
search_tabs();
|
||||||
|
else if (command == "go_to_tab") {
|
||||||
|
// "Go to tab..." second phase: the page hands back the tab id it matched.
|
||||||
|
const std::string tab_id = payload.value("id", "");
|
||||||
|
if (!tab_id.empty()) {
|
||||||
|
Hide();
|
||||||
|
if (wxGetApp().mainframe)
|
||||||
|
wxGetApp().mainframe->select_tab(from_u8(tab_id));
|
||||||
|
}
|
||||||
|
} else if (command == "resize")
|
||||||
resize_to_content(json_int_or(payload, "height", 0));
|
resize_to_content(json_int_or(payload, "height", 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpeedDialWebDialog::search_tabs()
|
||||||
|
{
|
||||||
|
// Round-trip is async because the webview delivers script messages synchronously on the
|
||||||
|
// GTK/macOS stack; defer the (cheap) enumeration and push the result back to the page.
|
||||||
|
wxGetApp().CallAfter([this, alive = m_alive]() {
|
||||||
|
if (!alive->load(std::memory_order_acquire))
|
||||||
|
return;
|
||||||
|
auto tabs = wxGetApp().action_registry().tab_options();
|
||||||
|
call_web_handler({{"command", "tab_results"}, {"tabs", std::move(tabs)}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpeedDialWebDialog::search_settings(const std::string& query)
|
||||||
|
{
|
||||||
|
// Round-trip is async because the webview delivers script messages synchronously on the
|
||||||
|
// GTK/macOS stack; defer the (cheap) search and push the result back to the page.
|
||||||
|
wxGetApp().CallAfter([this, alive = m_alive, query]() {
|
||||||
|
if (!alive->load(std::memory_order_acquire))
|
||||||
|
return;
|
||||||
|
auto results = wxGetApp().action_registry().settings_search(query);
|
||||||
|
call_web_handler({{"command", "settings_results"}, {"results", std::move(results)}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void SpeedDialWebDialog::resize_to_content(int height)
|
void SpeedDialWebDialog::resize_to_content(int height)
|
||||||
{
|
{
|
||||||
if (height <= 0)
|
if (height <= 0)
|
||||||
@@ -127,14 +206,15 @@ void SpeedDialWebDialog::resize_to_content(int height)
|
|||||||
Layout();
|
Layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title)
|
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title, const std::string& param)
|
||||||
{
|
{
|
||||||
ActionRegistry& reg = wxGetApp().action_registry();
|
ActionRegistry& reg = wxGetApp().action_registry();
|
||||||
const AppAction* a = reg.by_id(id);
|
const AppAction* a = reg.by_id(id);
|
||||||
if (!a)
|
if (!a)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const bool ask = reg.should_ask(id);
|
// 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 std::string atitle = a->title();
|
||||||
if (IsModal())
|
if (IsModal())
|
||||||
EndModal(wxID_CANCEL);
|
EndModal(wxID_CANCEL);
|
||||||
@@ -143,8 +223,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
|||||||
|
|
||||||
if (ask) {
|
if (ask) {
|
||||||
const wxString label = title.empty() ? from_u8(atitle) : from_u8(title);
|
const wxString label = title.empty() ? from_u8(atitle) : from_u8(title);
|
||||||
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label),
|
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), _L("Run plugin"), wxOK | wxCANCEL);
|
||||||
_L("Run plugin"), wxOK | wxCANCEL);
|
|
||||||
dlg.ShowCheckBox(_L("Don't ask again for this action"));
|
dlg.ShowCheckBox(_L("Don't ask again for this action"));
|
||||||
if (dlg.ShowModal() != wxID_OK)
|
if (dlg.ShowModal() != wxID_OK)
|
||||||
return;
|
return;
|
||||||
@@ -152,16 +231,19 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
|||||||
wxGetApp().action_registry().suppress_ask(id);
|
wxGetApp().action_registry().suppress_ask(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
wxGetApp().CallAfter([id] {
|
wxGetApp().CallAfter([id, param] {
|
||||||
if (wxGetApp().is_closing())
|
if (wxGetApp().is_closing())
|
||||||
return;
|
return;
|
||||||
AppActionRunResult result = wxGetApp().action_registry().run(id);
|
AppActionRunResult result = wxGetApp().action_registry().run(id, param);
|
||||||
if (result.level == AppActionRunResult::Level::Busy)
|
if (result.level == AppActionRunResult::Level::Busy)
|
||||||
return;
|
return;
|
||||||
if (!result.message.IsEmpty() && wxGetApp().plater())
|
if (!result.message.IsEmpty() && wxGetApp().plater())
|
||||||
wxGetApp().plater()->get_notification_manager()->push_notification(
|
wxGetApp()
|
||||||
NotificationType::CustomNotification,
|
.plater()
|
||||||
result.level == AppActionRunResult::Level::Error ? NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
->get_notification_manager()
|
||||||
|
->push_notification(NotificationType::CustomNotification,
|
||||||
|
result.level == AppActionRunResult::Level::Error ?
|
||||||
|
NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
||||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||||
into_u8(result.message));
|
into_u8(result.message));
|
||||||
});
|
});
|
||||||
@@ -172,7 +254,8 @@ void SpeedDialWebDialog::send_actions()
|
|||||||
nlohmann::json snap = wxGetApp().action_registry().snapshot();
|
nlohmann::json snap = wxGetApp().action_registry().snapshot();
|
||||||
call_web_handler({{"command", "list_actions"},
|
call_web_handler({{"command", "list_actions"},
|
||||||
{"actions", std::move(snap["actions"])},
|
{"actions", std::move(snap["actions"])},
|
||||||
{"favourites", std::move(snap["favourites"])}});
|
{"favourites", std::move(snap["favourites"])},
|
||||||
|
{"recent", std::move(snap["recent"])}});
|
||||||
}
|
}
|
||||||
|
|
||||||
}}
|
}} // namespace Slic3r::GUI
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ private:
|
|||||||
void on_script_message(const nlohmann::json& payload) override;
|
void on_script_message(const nlohmann::json& payload) override;
|
||||||
void handle_web_command(const nlohmann::json& payload);
|
void handle_web_command(const nlohmann::json& payload);
|
||||||
void resize_to_content(int height);
|
void resize_to_content(int height);
|
||||||
void run_action(const std::string& id, const std::string& title);
|
void run_action(const std::string& id, const std::string& title, const std::string& param = "");
|
||||||
void send_actions();
|
void send_actions();
|
||||||
|
void search_settings(const std::string& query);
|
||||||
|
void search_tabs();
|
||||||
|
|
||||||
bool m_page_ready{false};
|
bool m_page_ready{false};
|
||||||
// Guards the CallAfter in on_script_message across dialog destruction, same as
|
// Guards the CallAfter in on_script_message across dialog destruction, same as
|
||||||
|
|||||||
@@ -25,25 +25,18 @@ END_EVENT_TABLE()
|
|||||||
* calling Refresh()/Update().
|
* calling Refresh()/Update().
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Button::Button()
|
Button::Button() : paddingSize(10, 8)
|
||||||
: paddingSize(10, 8)
|
|
||||||
{
|
{
|
||||||
background_color = StateColor(
|
background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
||||||
std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
|
||||||
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
||||||
std::make_pair(0x009688, (int) StateColor::Checked),
|
std::make_pair(0x009688, (int) StateColor::Checked),
|
||||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
||||||
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
||||||
text_color = StateColor(
|
text_color = StateColor(std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled), std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
||||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled),
|
|
||||||
std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) : Button()
|
||||||
: Button()
|
{ Create(parent, text, icon, style, iconSize, btn_id); }
|
||||||
{
|
|
||||||
Create(parent, text, icon, style, iconSize, btn_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
||||||
{
|
{
|
||||||
@@ -87,9 +80,7 @@ void Button::SetIcon(const wxString& icon)
|
|||||||
this->active_icon = tmpBitmap;
|
this->active_icon = tmpBitmap;
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
this->active_icon = ScalableBitmap();
|
this->active_icon = ScalableBitmap();
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
@@ -121,6 +112,13 @@ void Button::SetPaddingSize(const wxSize& size)
|
|||||||
messureSize();
|
messureSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Button::SetIconSpacing(int spacing)
|
||||||
|
{
|
||||||
|
m_icon_spacing = spacing;
|
||||||
|
messureSize();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void Button::SetTextColor(StateColor const& color)
|
void Button::SetTextColor(StateColor const& color)
|
||||||
{
|
{
|
||||||
text_color = color;
|
text_color = color;
|
||||||
@@ -145,22 +143,22 @@ bool Button::Enable(bool enable)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::SetCanFocus(bool canFocus) {
|
void Button::SetCanFocus(bool canFocus)
|
||||||
|
{
|
||||||
StaticBox::SetCanFocus(canFocus);
|
StaticBox::SetCanFocus(canFocus);
|
||||||
this->canFocus = canFocus;
|
this->canFocus = canFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::SetValue(bool state)
|
void Button::SetValue(bool state)
|
||||||
{
|
{
|
||||||
if (GetValue() == state) return;
|
if (GetValue() == state)
|
||||||
|
return;
|
||||||
state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked);
|
state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; }
|
bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; }
|
||||||
|
|
||||||
void Button::SetCenter(bool isCenter)
|
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
|
||||||
{
|
|
||||||
this->isCenter = isCenter; }
|
|
||||||
|
|
||||||
void Button::SetVertical(bool vertical)
|
void Button::SetVertical(bool vertical)
|
||||||
{
|
{
|
||||||
@@ -181,32 +179,27 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
|||||||
this->SetPaddingSize(FromDIP(wxSize(8, 3)));
|
this->SetPaddingSize(FromDIP(wxSize(8, 3)));
|
||||||
this->SetCornerRadius(this->FromDIP(8));
|
this->SetCornerRadius(this->FromDIP(8));
|
||||||
this->SetFont(Label::Body_10);
|
this->SetFont(Label::Body_10);
|
||||||
}
|
} else if (type == ButtonType::Window) {
|
||||||
else if (type == ButtonType::Window) {
|
|
||||||
this->SetSize(FromDIP(wxSize(58, 24)));
|
this->SetSize(FromDIP(wxSize(58, 24)));
|
||||||
this->SetMinSize(FromDIP(wxSize(58, 24)));
|
this->SetMinSize(FromDIP(wxSize(58, 24)));
|
||||||
this->SetCornerRadius(this->FromDIP(12));
|
this->SetCornerRadius(this->FromDIP(12));
|
||||||
this->SetFont(Label::Body_12);
|
this->SetFont(Label::Body_12);
|
||||||
}
|
} else if (type == ButtonType::Choice) {
|
||||||
else if (type == ButtonType::Choice) {
|
|
||||||
this->SetMinSize(FromDIP(wxSize(100, 32)));
|
this->SetMinSize(FromDIP(wxSize(100, 32)));
|
||||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||||
this->SetCornerRadius(this->FromDIP(4));
|
this->SetCornerRadius(this->FromDIP(4));
|
||||||
this->SetFont(Label::Body_14);
|
this->SetFont(Label::Body_14);
|
||||||
}
|
} else if (type == ButtonType::Parameter) {
|
||||||
else if (type == ButtonType::Parameter) {
|
|
||||||
this->SetMinSize(FromDIP(wxSize(120, 26)));
|
this->SetMinSize(FromDIP(wxSize(120, 26)));
|
||||||
this->SetSize(FromDIP(wxSize(120, 26)));
|
this->SetSize(FromDIP(wxSize(120, 26)));
|
||||||
this->SetCornerRadius(this->FromDIP(4));
|
this->SetCornerRadius(this->FromDIP(4));
|
||||||
this->SetFont(Label::Body_14);
|
this->SetFont(Label::Body_14);
|
||||||
}
|
} else if (type == ButtonType::Icon) {
|
||||||
else if (type == ButtonType::Icon) {
|
|
||||||
this->SetPaddingSize(FromDIP(wxSize(5, 5)));
|
this->SetPaddingSize(FromDIP(wxSize(5, 5)));
|
||||||
this->SetMinSize(FromDIP(wxSize(26, 26)));
|
this->SetMinSize(FromDIP(wxSize(26, 26)));
|
||||||
this->SetSize(FromDIP(wxSize(26, 26)));
|
this->SetSize(FromDIP(wxSize(26, 26)));
|
||||||
this->SetCornerRadius(this->FromDIP(4));
|
this->SetCornerRadius(this->FromDIP(4));
|
||||||
}
|
} else if (type == ButtonType::Expanded) {
|
||||||
else if (type == ButtonType::Expanded) {
|
|
||||||
this->SetMinSize(FromDIP(wxSize(-1, 32)));
|
this->SetMinSize(FromDIP(wxSize(-1, 32)));
|
||||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||||
this->SetCornerRadius(this->FromDIP(4));
|
this->SetCornerRadius(this->FromDIP(4));
|
||||||
@@ -223,29 +216,23 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
|||||||
style == ButtonStyle::Disabled ? btn_disabled :
|
style == ButtonStyle::Disabled ? btn_disabled :
|
||||||
btn_regular;
|
btn_regular;
|
||||||
|
|
||||||
auto bg_color = StateColor(
|
auto bg_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
|
||||||
std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed),
|
std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed),
|
||||||
std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered),
|
std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered),
|
||||||
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal),
|
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal),
|
||||||
std::pair(wxColour(clr_arr[4]), (int)StateColor::Enabled)
|
std::pair(wxColour(clr_arr[4]), (int) StateColor::Enabled));
|
||||||
);
|
|
||||||
bg_color.setTakeFocusedAsHovered(false);
|
bg_color.setTakeFocusedAsHovered(false);
|
||||||
this->SetBackgroundColor(bg_color);
|
this->SetBackgroundColor(bg_color);
|
||||||
wxColour focus_clr = clr_arr[is_dark ? 8 : 9];
|
wxColour focus_clr = clr_arr[is_dark ? 8 : 9];
|
||||||
auto border_color = StateColor(
|
auto border_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
|
||||||
std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)),
|
std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)),
|
||||||
std::pair(wxColour(focus_clr), (int) StateColor::Focused),
|
std::pair(wxColour(focus_clr), (int) StateColor::Focused),
|
||||||
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal)
|
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal));
|
||||||
);
|
|
||||||
border_color.setTakeFocusedAsHovered(false);
|
border_color.setTakeFocusedAsHovered(false);
|
||||||
this->SetBorderColor(border_color);
|
this->SetBorderColor(border_color);
|
||||||
this->SetTextColor(StateColor(
|
this->SetTextColor(StateColor(std::pair(wxColour(clr_arr[5]), (int) StateColor::Disabled),
|
||||||
std::pair(wxColour(clr_arr[5]), (int)StateColor::Disabled),
|
|
||||||
std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered),
|
std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered),
|
||||||
std::pair(wxColour(clr_arr[6]), (int)StateColor::Normal)
|
std::pair(wxColour(clr_arr[6]), (int) StateColor::Normal)));
|
||||||
));
|
|
||||||
|
|
||||||
m_has_style = true;
|
m_has_style = true;
|
||||||
m_style = style;
|
m_style = style;
|
||||||
@@ -290,7 +277,7 @@ void Button::render(wxDC& dc)
|
|||||||
|
|
||||||
const ScalableBitmap& icon = active_icon;
|
const ScalableBitmap& icon = active_icon;
|
||||||
wxSize padding = this->paddingSize;
|
wxSize padding = this->paddingSize;
|
||||||
int spacing = 5;
|
int spacing = m_icon_spacing;
|
||||||
// Wrap text
|
// Wrap text
|
||||||
auto text = GetLabel();
|
auto text = GetLabel();
|
||||||
if (vertical && textSize.x + padding.x * 2 > size.x) {
|
if (vertical && textSize.x + padding.x * 2 > size.x) {
|
||||||
@@ -313,10 +300,12 @@ void Button::render(wxDC& dc)
|
|||||||
szIcon = icon.GetBmpSize();
|
szIcon = icon.GetBmpSize();
|
||||||
if (vertical) {
|
if (vertical) {
|
||||||
szContent.y += szIcon.y;
|
szContent.y += szIcon.y;
|
||||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
if (szIcon.x > szContent.x)
|
||||||
|
szContent.x = szIcon.x;
|
||||||
} else {
|
} else {
|
||||||
szContent.x += szIcon.x;
|
szContent.x += szIcon.x;
|
||||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
if (szIcon.y > szContent.y)
|
||||||
|
szContent.y = szIcon.y;
|
||||||
}
|
}
|
||||||
if (szContent.x > size.x) {
|
if (szContent.x > size.x) {
|
||||||
int d = std::min(padding.x, (szContent.x - size.x) / 2);
|
int d = std::min(padding.x, (szContent.x - size.x) / 2);
|
||||||
@@ -328,7 +317,8 @@ void Button::render(wxDC& dc)
|
|||||||
wxRect rcContent = {{0, 0}, size};
|
wxRect rcContent = {{0, 0}, size};
|
||||||
if (isCenter) {
|
if (isCenter) {
|
||||||
wxSize offset = (size - szContent) / 2;
|
wxSize offset = (size - szContent) / 2;
|
||||||
if (offset.x < 0) offset.x = 0;
|
if (offset.x < 0)
|
||||||
|
offset.x = 0;
|
||||||
rcContent.Deflate(offset.x, offset.y);
|
rcContent.Deflate(offset.x, offset.y);
|
||||||
}
|
}
|
||||||
// start draw
|
// start draw
|
||||||
@@ -373,19 +363,21 @@ void Button::messureSize()
|
|||||||
wxSize szContent = textSize.GetSize();
|
wxSize szContent = textSize.GetSize();
|
||||||
if (this->active_icon.bmp().IsOk()) {
|
if (this->active_icon.bmp().IsOk()) {
|
||||||
if (szContent.y > 0) {
|
if (szContent.y > 0) {
|
||||||
//BBS norrow size between text and icon
|
// BBS narrow size between text and icon
|
||||||
if (vertical)
|
if (vertical)
|
||||||
szContent.y += 5;
|
szContent.y += m_icon_spacing;
|
||||||
else
|
else
|
||||||
szContent.x += 5;
|
szContent.x += m_icon_spacing;
|
||||||
}
|
}
|
||||||
wxSize szIcon = this->active_icon.GetBmpSize();
|
wxSize szIcon = this->active_icon.GetBmpSize();
|
||||||
if (vertical) {
|
if (vertical) {
|
||||||
szContent.y += szIcon.y;
|
szContent.y += szIcon.y;
|
||||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
if (szIcon.x > szContent.x)
|
||||||
|
szContent.x = szIcon.x;
|
||||||
} else {
|
} else {
|
||||||
szContent.x += szIcon.x;
|
szContent.x += szIcon.x;
|
||||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
if (szIcon.y > szContent.y)
|
||||||
|
szContent.y = szIcon.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wxSize size = szContent + paddingSize * 2;
|
wxSize size = szContent + paddingSize * 2;
|
||||||
@@ -444,8 +436,8 @@ void Button::keyDownUp(wxKeyEvent &event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.GetEventType() == wxEVT_KEY_DOWN &&
|
if (event.GetEventType() == wxEVT_KEY_DOWN &&
|
||||||
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT
|
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT ||
|
||||||
|| event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
||||||
HandleAsNavigationKey(event);
|
HandleAsNavigationKey(event);
|
||||||
else
|
else
|
||||||
event.Skip();
|
event.Skip();
|
||||||
@@ -462,7 +454,9 @@ void Button::sendButtonEvent()
|
|||||||
|
|
||||||
WXLRESULT Button::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
WXLRESULT Button::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||||
{
|
{
|
||||||
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTMESSAGE; }
|
if (nMsg == WM_GETDLGCODE) {
|
||||||
|
return DLGC_WANTMESSAGE;
|
||||||
|
}
|
||||||
if (nMsg == WM_KEYDOWN) {
|
if (nMsg == WM_KEYDOWN) {
|
||||||
wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam));
|
wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam));
|
||||||
switch (wParam) {
|
switch (wParam) {
|
||||||
@@ -483,8 +477,7 @@ void Button::EnableTooltipEvenDisabled()
|
|||||||
{
|
{
|
||||||
#if defined(_MSC_VER) || defined(_WIN32)
|
#if defined(_MSC_VER) || defined(_WIN32)
|
||||||
auto parent = this->GetParent();
|
auto parent = this->GetParent();
|
||||||
if (parent)
|
if (parent) {
|
||||||
{
|
|
||||||
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
|
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
|
||||||
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
|
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
|
||||||
};
|
};
|
||||||
@@ -494,22 +487,21 @@ void Button::EnableTooltipEvenDisabled()
|
|||||||
void Button::OnParentMotion(wxMouseEvent& event)
|
void Button::OnParentMotion(wxMouseEvent& event)
|
||||||
{
|
{
|
||||||
auto parent = this->GetParent();
|
auto parent = this->GetParent();
|
||||||
if (!parent) return event.Skip();
|
if (!parent)
|
||||||
|
return event.Skip();
|
||||||
|
|
||||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||||
wxRect screen_rect = this->GetScreenRect();
|
wxRect screen_rect = this->GetScreenRect();
|
||||||
wxString tip = this->GetToolTipText();
|
wxString tip = this->GetToolTipText();
|
||||||
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos))
|
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) {
|
||||||
{
|
if (!tipWindow) {
|
||||||
if (!tipWindow)
|
|
||||||
{
|
|
||||||
tipWindow = wxTipWindow::New(this, tip);
|
tipWindow = wxTipWindow::New(this, tip);
|
||||||
if (!tipWindow) return event.Skip();
|
if (!tipWindow)
|
||||||
|
return event.Skip();
|
||||||
tipWindow->Enable(false);
|
tipWindow->Enable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tipWindow->GetLabel() != tip)
|
if (tipWindow->GetLabel() != tip) {
|
||||||
{
|
|
||||||
tipWindow->SetLabel(tip);
|
tipWindow->SetLabel(tip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,11 +509,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
|||||||
// using wxGetMousePosition() which returns (0,0) on Wayland.
|
// using wxGetMousePosition() which returns (0,0) on Wayland.
|
||||||
tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize());
|
tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize());
|
||||||
tipWindow->Popup();
|
tipWindow->Popup();
|
||||||
}
|
} else {
|
||||||
else
|
if (tipWindow) {
|
||||||
{
|
|
||||||
if (tipWindow)
|
|
||||||
{
|
|
||||||
tipWindow->Dismiss();
|
tipWindow->Dismiss();
|
||||||
tipWindow->Destroy();
|
tipWindow->Destroy();
|
||||||
tipWindow = nullptr;
|
tipWindow = nullptr;
|
||||||
@@ -534,15 +523,14 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
|||||||
void Button::OnParentLeave(wxMouseEvent& event)
|
void Button::OnParentLeave(wxMouseEvent& event)
|
||||||
{
|
{
|
||||||
auto parent = this->GetParent();
|
auto parent = this->GetParent();
|
||||||
if (!parent) return event.Skip();
|
if (!parent)
|
||||||
|
return event.Skip();
|
||||||
|
|
||||||
if (tipWindow)
|
if (tipWindow) {
|
||||||
{
|
|
||||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||||
wxRect screen_rect = this->GetScreenRect();
|
wxRect screen_rect = this->GetScreenRect();
|
||||||
wxString tip = this->GetToolTipText();
|
wxString tip = this->GetToolTipText();
|
||||||
if (!screen_rect.Contains(pos))
|
if (!screen_rect.Contains(pos)) {
|
||||||
{
|
|
||||||
tipWindow->Dismiss();
|
tipWindow->Dismiss();
|
||||||
tipWindow->Destroy();
|
tipWindow->Destroy();
|
||||||
tipWindow = nullptr;
|
tipWindow = nullptr;
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ enum class ButtonType{
|
|||||||
Window, // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
Window, // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
||||||
Choice, // Font14 Semi-Rounded For dialog/window choice buttons
|
Choice, // Font14 Semi-Rounded For dialog/window choice buttons
|
||||||
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
|
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
|
||||||
Icon , // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while creation of button
|
Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while
|
||||||
|
// creation of button
|
||||||
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class Button : public StaticBox
|
|||||||
wxRect textSize;
|
wxRect textSize;
|
||||||
wxSize minSize; // set by outer
|
wxSize minSize; // set by outer
|
||||||
wxSize paddingSize;
|
wxSize paddingSize;
|
||||||
|
int m_icon_spacing = 5;
|
||||||
ScalableBitmap active_icon;
|
ScalableBitmap active_icon;
|
||||||
|
|
||||||
StateColor text_color;
|
StateColor text_color;
|
||||||
@@ -66,6 +68,8 @@ public:
|
|||||||
|
|
||||||
void SetPaddingSize(const wxSize& size);
|
void SetPaddingSize(const wxSize& size);
|
||||||
|
|
||||||
|
void SetIconSpacing(int spacing);
|
||||||
|
|
||||||
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
|
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
|
||||||
|
|
||||||
void SetTextColor(StateColor const& color);
|
void SetTextColor(StateColor const& color);
|
||||||
|
|||||||
Reference in New Issue
Block a user